Merge "More details polish."
diff --git a/v17/leanback/res/layout/lb_search_bar.xml b/v17/leanback/res/layout/lb_search_bar.xml
index 8e1a1b7..b2a5d6a 100644
--- a/v17/leanback/res/layout/lb_search_bar.xml
+++ b/v17/leanback/res/layout/lb_search_bar.xml
@@ -43,10 +43,22 @@
                 android:orientation="horizontal"
                 android:background="@android:color/transparent"
                 android:layout_weight="1">
-                <android.support.v17.leanback.widget.SearchEditText
+
+            <ImageView
+                    android:id="@+id/lb_search_bar_badge"
+                    android:layout_width="@dimen/lb_search_bar_icon_width"
+                    android:layout_height="@dimen/lb_search_bar_icon_height"
+                    android:layout_gravity="center_vertical|left"
+                    android:src="@null"
+                    android:visibility="gone"
+                    style="?attr/browseTitleIconStyle"/>
+
+            <android.support.v17.leanback.widget.SearchEditText
                         android:id="@+id/lb_search_text_editor"
                         android:layout_width="match_parent"
                         android:layout_height="match_parent"
+                        android:layout_gravity="center_vertical|right"
+                        android:layout_marginLeft="@dimen/lb_search_bar_hint_margin_left"
                         android:cursorVisible="true"
                         android:editable="true"
                         android:background="@null"
diff --git a/v17/leanback/res/values/dimens.xml b/v17/leanback/res/values/dimens.xml
index ac3bd25..6ea6043 100644
--- a/v17/leanback/res/values/dimens.xml
+++ b/v17/leanback/res/values/dimens.xml
@@ -103,6 +103,10 @@
     <dimen name="lb_search_bar_text_size">28sp</dimen>
     <dimen name="lb_search_bar_items_layout_margin_top">27dp</dimen>
     <dimen name="lb_search_bar_items_width">660dp</dimen>
+    <dimen name="lb_search_bar_icon_height">32dp</dimen>
+    <dimen name="lb_search_bar_icon_width">32dp</dimen>
+    <dimen name="lb_search_bar_hint_margin_left">52dp</dimen>
+
 
     <!-- Search Fragment -->
     <dimen name="lb_search_browse_rows_align_top">120dp</dimen>
diff --git a/v17/leanback/res/values/strings.xml b/v17/leanback/res/values/strings.xml
index 333c06a..8921f03 100644
--- a/v17/leanback/res/values/strings.xml
+++ b/v17/leanback/res/values/strings.xml
@@ -18,4 +18,5 @@
     <string name="orb_search_label">Search</string>
     <string name="orb_search_action">Search Action</string>
     <string name="lb_search_bar_hint">Search</string>
+    <string name="lb_search_bar_hint_with_title">Search %1$s</string>
 </resources>
\ No newline at end of file
diff --git a/v17/leanback/src/android/support/v17/leanback/app/SearchFragment.java b/v17/leanback/src/android/support/v17/leanback/app/SearchFragment.java
index 9720634..c0d60e1 100644
--- a/v17/leanback/src/android/support/v17/leanback/app/SearchFragment.java
+++ b/v17/leanback/src/android/support/v17/leanback/app/SearchFragment.java
@@ -14,6 +14,7 @@
 package android.support.v17.leanback.app;
 
 import android.app.Fragment;
+import android.graphics.drawable.Drawable;
 import android.os.Bundle;
 import android.os.Handler;
 import android.support.v17.leanback.widget.ObjectAdapter;
@@ -35,7 +36,10 @@
 public class SearchFragment extends Fragment {
     private static final String TAG = SearchFragment.class.getSimpleName();
     private static final boolean DEBUG = false;
-    private static final String ARG_QUERY = SearchFragment.class.getCanonicalName() + ".query";
+
+    private static final String ARG_PREFIX = SearchFragment.class.getCanonicalName();
+    private static final String ARG_QUERY =  ARG_PREFIX + ".query";
+    private static final String ARG_TITLE = ARG_PREFIX  + ".title";
 
     /**
      * Search API exposed to application
@@ -84,14 +88,22 @@
     private OnItemClickedListener mOnItemClickedListener;
     private ObjectAdapter mResultAdapter;
 
+    private String mTitle;
+    private Drawable mBadgeDrawable;
+
     /**
      * @param args Bundle to use for the arguments, if null a new Bundle will be created.
      */
     public static Bundle createArgs(Bundle args, String query) {
+        return createArgs(args, query, null);
+    }
+
+    public static Bundle createArgs(Bundle args, String query, String title)  {
         if (args == null) {
             args = new Bundle();
         }
         args.putString(ARG_QUERY, query);
+        args.putString(ARG_TITLE, title);
         return args;
     }
 
@@ -150,10 +162,12 @@
             }
         });
 
-        Bundle args = getArguments();
-        if (null != args) {
-            String query = args.getString(ARG_QUERY, "");
-            mSearchBar.setSearchQuery(query);
+        readArguments(getArguments());
+        if (null != mBadgeDrawable) {
+            setBadgeDrawable(mBadgeDrawable);
+        }
+        if (null != mTitle) {
+            setTitle(mTitle);
         }
 
         // Inject the RowsFragment in the results container
@@ -231,6 +245,47 @@
         mOnItemClickedListener = listener;
     }
 
+    /**
+     * Sets the title string to be be shown in an empty search bar
+     */
+    public void setTitle(String title) {
+        mTitle = title;
+        if (null != mSearchBar) {
+            mSearchBar.setTitle(title);
+        }
+    }
+
+    /**
+     * Returns the title set
+     */
+    public String getTitle() {
+        if (null != mSearchBar) {
+            return mSearchBar.getTitle();
+        }
+        return null;
+    }
+
+    /**
+     * Sets the badge drawable that will be shown inside the search bar, next to the hint
+     */
+    public void setBadgeDrawable(Drawable drawable) {
+        mBadgeDrawable = drawable;
+        if (null != mSearchBar) {
+            mSearchBar.setBadgeDrawable(drawable);
+        }
+    }
+
+    /**
+     * Returns the badge drawable
+     */
+    public Drawable getBadgeDrawable() {
+        if (null != mSearchBar) {
+            return mSearchBar.getBadgeDrawable();
+        }
+        return null;
+    }
+
+
     private void retrieveResults(String searchQuery) {
         if (DEBUG) Log.v(TAG, String.format("retrieveResults %s", searchQuery));
         mProvider.onQueryTextChange(searchQuery);
@@ -258,4 +313,23 @@
         }
     }
 
+    private void readArguments(Bundle args) {
+        if (null == args) {
+            return;
+        }
+        if (args.containsKey(ARG_QUERY)) {
+            setSearchQuery(args.getString(ARG_QUERY));
+        }
+
+        if (args.containsKey(ARG_TITLE)) {
+            setTitle(args.getString(ARG_TITLE));
+        }
+    }
+
+    private void setSearchQuery(String query) {
+        mSearchBar.setSearchQuery(query);
+    }
+
+
+
 }
diff --git a/v17/leanback/src/android/support/v17/leanback/widget/SearchBar.java b/v17/leanback/src/android/support/v17/leanback/widget/SearchBar.java
index 029db3e..7716288 100644
--- a/v17/leanback/src/android/support/v17/leanback/widget/SearchBar.java
+++ b/v17/leanback/src/android/support/v17/leanback/widget/SearchBar.java
@@ -14,9 +14,11 @@
 package android.support.v17.leanback.widget;
 
 import android.content.Context;
+import android.graphics.drawable.Drawable;
 import android.os.Handler;
 import android.os.SystemClock;
 import android.text.Editable;
+import android.text.TextUtils;
 import android.text.TextWatcher;
 import android.util.AttributeSet;
 import android.util.Log;
@@ -24,6 +26,7 @@
 import android.view.KeyEvent;
 import android.view.MotionEvent;
 import android.view.View;
+import android.widget.ImageView;
 import android.widget.RelativeLayout;
 import android.support.v17.leanback.R;
 import android.widget.TextView;
@@ -64,7 +67,10 @@
 
     private SearchBarListener mSearchBarListener;
     private SearchEditText mSearchTextEditor;
+    private ImageView mBadgeView;
     private String mSearchQuery;
+    private String mTitle;
+    private Drawable mBadgeDrawable;
     private final Handler mHandler = new Handler();
 
     public SearchBar(Context context) {
@@ -85,6 +91,11 @@
         super.onFinishInflate();
 
         mSearchTextEditor = (SearchEditText)findViewById(R.id.lb_search_text_editor);
+        mBadgeView = (ImageView)findViewById(R.id.lb_search_bar_badge);
+        if (null != mBadgeDrawable) {
+            mBadgeView.setImageDrawable(mBadgeDrawable);
+        }
+
         mSearchTextEditor.setOnFocusChangeListener(new OnFocusChangeListener() {
             @Override
             public void onFocusChange(View view, boolean hasFocus) {
@@ -129,6 +140,8 @@
                 return false;
             }
         });
+
+        updateHint();
     }
 
     @Override
@@ -166,11 +179,42 @@
     }
 
     /**
-     * Set the hint text shown in the search bar.
-     * @param hint The hint to use.
+     * Set the title text used in the hint shown in the search bar.
+     * @param title The hint to use.
      */
-    public void setHint(String hint) {
-        mSearchTextEditor.setHint(hint);
+    public void setTitle(String title) {
+        mTitle = title;
+        updateHint();
+    }
+
+    /**
+     * Returns the current title
+     */
+    public String getTitle() {
+        return mTitle;
+    }
+
+    /**
+     * Set the badge drawable showing inside the search bar.
+     * @param drawable The drawable to be used in the search bar.
+     */
+    public void setBadgeDrawable(Drawable drawable) {
+        mBadgeDrawable = drawable;
+        if (null != mBadgeView) {
+            mBadgeView.setImageDrawable(drawable);
+            if (null != drawable) {
+                mBadgeView.setVisibility(View.VISIBLE);
+            } else {
+                mBadgeView.setVisibility(View.GONE);
+            }
+        }
+    }
+
+    /**
+     * Returns the badge drawable
+     */
+    public Drawable getBadgeDrawable() {
+        return mBadgeDrawable;
     }
 
     protected void showNativeKeyboard() {
@@ -188,4 +232,17 @@
         });
     }
 
+    /**
+     * This will update the hint for the search bar properly depending on state and provided title
+     */
+    protected void updateHint() {
+        if (null == mSearchTextEditor) return;
+
+        String title = getResources().getString(R.string.lb_search_bar_hint);
+        if (!TextUtils.isEmpty(mTitle)) {
+            title = getResources().getString(R.string.lb_search_bar_hint_with_title, mTitle);
+        }
+        mSearchTextEditor.setHint(title);
+    }
+
 }
diff --git a/v4/Android.mk b/v4/Android.mk
index b29ddc8..8db06fd 100644
--- a/v4/Android.mk
+++ b/v4/Android.mk
@@ -14,11 +14,21 @@
 
 LOCAL_PATH := $(call my-dir)
 
+# A common helper sub-library that only uses base (API 4) APIs.
+include $(CLEAR_VARS)
+LOCAL_MODULE := android-support-v4-base
+LOCAL_SDK_VERSION := 4
+LOCAL_SRC_FILES := $(call all-java-files-under, base)
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+# -----------------------------------------------------------------------
+
 # A helper sub-library that makes direct use of Donut APIs.
 include $(CLEAR_VARS)
 LOCAL_MODULE := android-support-v4-donut
 LOCAL_SDK_VERSION := 4
 LOCAL_SRC_FILES := $(call all-java-files-under, donut)
+LOCAL_STATIC_JAVA_LIBRARIES := android-support-v4-base
 include $(BUILD_STATIC_JAVA_LIBRARY)
 
 # -----------------------------------------------------------------------
@@ -169,7 +179,10 @@
 include $(CLEAR_VARS)
 LOCAL_MODULE := android-support-v4
 LOCAL_SDK_VERSION := 4
-LOCAL_SRC_FILES := $(call all-java-files-under, java)
+
+LOCAL_SRC_FILES := $(call all-java-files-under, java) \
+    $(call all-Iaidl-files-under, java)
+
 LOCAL_STATIC_JAVA_LIBRARIES += android-support-v4-api21
 LOCAL_STATIC_JAVA_LIBRARIES += android-support-annotations
 include $(BUILD_STATIC_JAVA_LIBRARY)
diff --git a/v4/api20/android/support/v4/app/NotificationCompatApi20.java b/v4/api20/android/support/v4/app/NotificationCompatApi20.java
index 43bb143..e8cbe05 100644
--- a/v4/api20/android/support/v4/app/NotificationCompatApi20.java
+++ b/v4/api20/android/support/v4/app/NotificationCompatApi20.java
@@ -18,6 +18,7 @@
 
 import android.app.Notification;
 import android.app.PendingIntent;
+import android.app.RemoteInput;
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.os.Bundle;
@@ -36,8 +37,9 @@
                 PendingIntent contentIntent, PendingIntent fullScreenIntent, Bitmap largeIcon,
                 int mProgressMax, int mProgress, boolean mProgressIndeterminate,
                 boolean useChronometer, int priority, CharSequence subText, boolean localOnly,
-                String category, ArrayList<String> people, Bundle extras,
-                int color, int visibility, Notification publicVersion) {
+                String category, ArrayList<String> people, Bundle extras, int color,
+                int visibility, Notification publicVersion, String groupKey, boolean groupSummary,
+                String sortKey) {
             b = new Notification.Builder(context)
                 .setWhen(n.when)
                 .setSmallIcon(n.icon, n.iconLevel)
@@ -68,15 +70,29 @@
                 .setExtras(extras)
                 .setColor(color)
                 .setVisibility(visibility)
-                .setPublicVersion(publicVersion);
+                .setPublicVersion(publicVersion)
+                .setGroup(groupKey)
+                .setGroupSummary(groupSummary)
+                .setSortKey(sortKey);
             for (String person: people) {
                 b.addPerson(person);
             }
         }
 
         @Override
-        public void addAction(int icon, CharSequence title, PendingIntent intent) {
-            b.addAction(icon, title, intent);
+        public void addAction(NotificationCompatBase.Action action) {
+            Notification.Action.Builder actionBuilder = new Notification.Action.Builder(
+                    action.getIcon(), action.getTitle(), action.getActionIntent());
+            if (action.getRemoteInputs() != null) {
+                for (RemoteInput remoteInput : RemoteInputCompatApi20.fromCompat(
+                        action.getRemoteInputs())) {
+                    actionBuilder.addRemoteInput(remoteInput);
+                }
+            }
+            if (action.getExtras() != null) {
+                actionBuilder.addExtras(action.getExtras());
+            }
+            b.addAction(actionBuilder.build());
         }
 
         @Override
@@ -89,7 +105,29 @@
         }
     }
 
+    public static NotificationCompatBase.Action getAction(Notification notif,
+            int actionIndex, NotificationCompatBase.Action.Factory factory,
+            RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) {
+        Notification.Action action = notif.actions[actionIndex];
+        RemoteInputCompatBase.RemoteInput[] remoteInputs = RemoteInputCompatApi20.toCompat(
+                action.getRemoteInputs(), remoteInputFactory);
+        return factory.build(action.icon, action.title, action.actionIntent,
+                action.getExtras(), remoteInputs);
+    }
+
     public static boolean getLocalOnly(Notification notif) {
         return (notif.flags & Notification.FLAG_LOCAL_ONLY) != 0;
     }
+
+    public static String getGroup(Notification notif) {
+        return notif.getGroup();
+    }
+
+    public static boolean isGroupSummary(Notification notif) {
+        return (notif.flags & Notification.FLAG_GROUP_SUMMARY) != 0;
+    }
+
+    public static String getSortKey(Notification notif) {
+        return notif.getSortKey();
+    }
 }
diff --git a/v4/api20/android/support/v4/app/RemoteInputCompatApi20.java b/v4/api20/android/support/v4/app/RemoteInputCompatApi20.java
new file mode 100644
index 0000000..4fae50f
--- /dev/null
+++ b/v4/api20/android/support/v4/app/RemoteInputCompatApi20.java
@@ -0,0 +1,62 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.app;
+
+import android.app.RemoteInput;
+import android.content.Intent;
+import android.os.Bundle;
+
+class RemoteInputCompatApi20 {
+    static RemoteInputCompatBase.RemoteInput[] toCompat(RemoteInput[] srcArray,
+            RemoteInputCompatBase.RemoteInput.Factory factory) {
+        if (srcArray == null) {
+            return null;
+        }
+        RemoteInputCompatBase.RemoteInput[] result = factory.newArray(srcArray.length);
+        for (int i = 0; i < srcArray.length; i++) {
+            RemoteInput src = srcArray[i];
+            result[i] = factory.build(src.getResultKey(), src.getLabel(), src.getChoices(),
+                    src.getAllowFreeFormInput(), src.getExtras());
+        }
+        return result;
+    }
+
+    static RemoteInput[] fromCompat(RemoteInputCompatBase.RemoteInput[] srcArray) {
+        if (srcArray == null) {
+            return null;
+        }
+        RemoteInput[] result = new RemoteInput[srcArray.length];
+        for (int i = 0; i < srcArray.length; i++) {
+            RemoteInputCompatBase.RemoteInput src = srcArray[i];
+            result[i] = new RemoteInput.Builder(src.getResultKey())
+                    .setLabel(src.getLabel())
+                    .setChoices(src.getChoices())
+                    .setAllowFreeFormInput(src.getAllowFreeFormInput())
+                    .build();
+        }
+        return result;
+    }
+
+    static Bundle getResultsFromIntent(Intent intent) {
+        return RemoteInput.getResultsFromIntent(intent);
+    }
+
+    static void addResultsToIntent(RemoteInputCompatBase.RemoteInput[] remoteInputs,
+            Intent intent, Bundle results) {
+        RemoteInput.addResultsToIntent(fromCompat(remoteInputs), intent, results);
+    }
+}
diff --git a/v4/base/android/support/v4/app/NotificationCompatBase.java b/v4/base/android/support/v4/app/NotificationCompatBase.java
new file mode 100644
index 0000000..d8e99af
--- /dev/null
+++ b/v4/base/android/support/v4/app/NotificationCompatBase.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.app;
+
+import android.app.PendingIntent;
+import android.os.Bundle;
+
+class NotificationCompatBase {
+
+    public static abstract class Action {
+        protected abstract int getIcon();
+        protected abstract CharSequence getTitle();
+        protected abstract PendingIntent getActionIntent();
+        protected abstract Bundle getExtras();
+        protected abstract RemoteInputCompatBase.RemoteInput[] getRemoteInputs();
+
+        public interface Factory {
+            Action build(int icon, CharSequence title, PendingIntent actionIntent,
+                    Bundle extras, RemoteInputCompatBase.RemoteInput[] remoteInputs);
+            public Action[] newArray(int length);
+        }
+    }
+}
diff --git a/v4/base/android/support/v4/app/RemoteInputCompatBase.java b/v4/base/android/support/v4/app/RemoteInputCompatBase.java
new file mode 100644
index 0000000..2449336
--- /dev/null
+++ b/v4/base/android/support/v4/app/RemoteInputCompatBase.java
@@ -0,0 +1,36 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.app;
+
+import android.os.Bundle;
+
+class RemoteInputCompatBase {
+
+    public static abstract class RemoteInput {
+        protected abstract String getResultKey();
+        protected abstract CharSequence getLabel();
+        protected abstract CharSequence[] getChoices();
+        protected abstract boolean getAllowFreeFormInput();
+        protected abstract Bundle getExtras();
+
+        public interface Factory {
+            public RemoteInput build(String resultKey, CharSequence label,
+                    CharSequence[] choices, boolean allowFreeFormInput, Bundle extras);
+            public RemoteInput[] newArray(int length);
+        }
+    }
+}
diff --git a/v4/eclair/android/support/v4/app/NotificationManagerCompatEclair.java b/v4/eclair/android/support/v4/app/NotificationManagerCompatEclair.java
new file mode 100644
index 0000000..45d96e4
--- /dev/null
+++ b/v4/eclair/android/support/v4/app/NotificationManagerCompatEclair.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.app;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+
+class NotificationManagerCompatEclair {
+    static void cancelNotification(NotificationManager notificationManager, String tag,
+            int id) {
+        notificationManager.cancel(tag, id);
+    }
+
+    public static void postNotification(NotificationManager notificationManager, String tag, int id,
+            Notification notification) {
+        notificationManager.notify(tag, id, notification);
+    }
+}
diff --git a/v4/ics/android/support/v4/app/NotificationManagerCompatIceCreamSandwich.java b/v4/ics/android/support/v4/app/NotificationManagerCompatIceCreamSandwich.java
new file mode 100644
index 0000000..f088179
--- /dev/null
+++ b/v4/ics/android/support/v4/app/NotificationManagerCompatIceCreamSandwich.java
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.app;
+
+import android.app.Service;
+
+class NotificationManagerCompatIceCreamSandwich {
+    static final int SIDE_CHANNEL_BIND_FLAGS = Service.BIND_AUTO_CREATE
+            | Service.BIND_WAIVE_PRIORITY;
+}
diff --git a/v4/java/android/support/v4/app/INotificationSideChannel.aidl b/v4/java/android/support/v4/app/INotificationSideChannel.aidl
new file mode 100644
index 0000000..9df1577
--- /dev/null
+++ b/v4/java/android/support/v4/app/INotificationSideChannel.aidl
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.app;
+
+import android.app.Notification;
+
+/**
+ * Interface used for delivering notifications via a side channel that bypasses
+ * the NotificationManagerService.
+ *
+ * @hide
+ */
+oneway interface INotificationSideChannel {
+    /**
+     * Send an ambient notification to the service.
+     */
+    void notify(String packageName, int id, String tag, in Notification notification);
+
+    /**
+     * Cancel an already-notified notification.
+     */
+    void cancel(String packageName, int id, String tag);
+
+    /**
+     * Cancel all notifications for the given package.
+     */
+    void cancelAll(String packageName);
+}
diff --git a/v4/java/android/support/v4/app/NotificationCompat.java b/v4/java/android/support/v4/app/NotificationCompat.java
index 17d518d..da7f872 100644
--- a/v4/java/android/support/v4/app/NotificationCompat.java
+++ b/v4/java/android/support/v4/app/NotificationCompat.java
@@ -17,7 +17,6 @@
 package android.support.v4.app;
 
 import android.app.Notification;
-import android.app.NotificationManager;
 import android.app.PendingIntent;
 import android.content.Context;
 import android.graphics.Bitmap;
@@ -27,6 +26,7 @@
 import android.os.Build;
 import android.os.Bundle;
 import android.widget.RemoteViews;
+
 import java.util.ArrayList;
 
 /**
@@ -123,7 +123,21 @@
      *
      * @deprecated Use {@link NotificationCompat.Builder#setPriority(int)} with a positive value.
      */
-    public static final int FLAG_HIGH_PRIORITY = 0x00000080;
+    public static final int FLAG_HIGH_PRIORITY      = 0x00000080;
+
+    /**
+     * Bit set in the Notification flags field if this notification is relevant to the current
+     * device only and it is not recommended that it bridge to other devices.
+     */
+    public static final int FLAG_LOCAL_ONLY         = 0x00000100;
+
+    /**
+     * Bit set in the Notification flags field if this notification is the group summary for a
+     * group of notifications. Grouped notifications may display in a cluster or stack on devices
+     * which support such rendering. Requires a group key also be set using
+     * {@link Builder#setGroup}.
+     */
+    public static final int FLAG_GROUP_SUMMARY      = 0x00000200;
 
     /**
      * Default notification priority for {@link NotificationCompat.Builder#setPriority(int)}.
@@ -298,7 +312,12 @@
     interface NotificationCompatImpl {
         public Notification build(Builder b);
         public Bundle getExtras(Notification n);
+        public int getActionCount(Notification n);
+        public Action getAction(Notification n, int actionIndex);
         public boolean getLocalOnly(Notification n);
+        public String getGroup(Notification n);
+        public boolean isGroupSummary(Notification n);
+        public String getSortKey(Notification n);
     }
 
     static class NotificationCompatImplBase implements NotificationCompatImpl {
@@ -320,9 +339,34 @@
         }
 
         @Override
+        public int getActionCount(Notification n) {
+            return 0;
+        }
+
+        @Override
+        public Action getAction(Notification n, int actionIndex) {
+            return null;
+        }
+
+        @Override
         public boolean getLocalOnly(Notification n) {
             return false;
         }
+
+        @Override
+        public String getGroup(Notification n) {
+            return null;
+        }
+
+        @Override
+        public boolean isGroupSummary(Notification n) {
+            return false;
+        }
+
+        @Override
+        public String getSortKey(Notification n) {
+            return null;
+        }
     }
 
     static class NotificationCompatImplGingerbread extends NotificationCompatImplBase {
@@ -367,7 +411,8 @@
                     b.mContext, b.mNotification, b.mContentTitle, b.mContentText, b.mContentInfo,
                     b.mTickerView, b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon,
                     b.mProgressMax, b.mProgress, b.mProgressIndeterminate,
-                    b.mUseChronometer, b.mPriority, b.mSubText, b.mLocalOnly, b.mExtras);
+                    b.mUseChronometer, b.mPriority, b.mSubText, b.mLocalOnly, b.mExtras,
+                    b.mGroupKey, b.mGroupSummary, b.mSortKey);
             addActionsToBuilder(builder, b.mActions);
             addStyleToBuilderJellybean(builder, b.mStyle);
             return builder.build();
@@ -379,19 +424,46 @@
         }
 
         @Override
+        public int getActionCount(Notification n) {
+            return NotificationCompatJellybean.getActionCount(n);
+        }
+
+        @Override
+        public Action getAction(Notification n, int actionIndex) {
+            return (Action) NotificationCompatJellybean.getAction(n, actionIndex, Action.FACTORY,
+                    RemoteInput.FACTORY);
+        }
+
+        @Override
         public boolean getLocalOnly(Notification n) {
             return NotificationCompatJellybean.getLocalOnly(n);
         }
+
+        @Override
+        public String getGroup(Notification n) {
+            return NotificationCompatJellybean.getGroup(n);
+        }
+
+        @Override
+        public boolean isGroupSummary(Notification n) {
+            return NotificationCompatJellybean.isGroupSummary(n);
+        }
+
+        @Override
+        public String getSortKey(Notification n) {
+            return NotificationCompatJellybean.getSortKey(n);
+        }
     }
 
-    static class NotificationCompatImplKitKat extends NotificationCompatImplBase {
+    static class NotificationCompatImplKitKat extends NotificationCompatImplJellybean {
         @Override
         public Notification build(Builder b) {
             NotificationCompatKitKat.Builder builder = new NotificationCompatKitKat.Builder(
                     b.mContext, b.mNotification, b.mContentTitle, b.mContentText, b.mContentInfo,
                     b.mTickerView, b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon,
                     b.mProgressMax, b.mProgress, b.mProgressIndeterminate,
-                    b.mUseChronometer, b.mPriority, b.mSubText, b.mLocalOnly, b.mPeople, b.mExtras);
+                    b.mUseChronometer, b.mPriority, b.mSubText, b.mLocalOnly, b.mPeople, b.mExtras,
+                    b.mGroupKey, b.mGroupSummary, b.mSortKey);
             addActionsToBuilder(builder, b.mActions);
             addStyleToBuilderJellybean(builder, b.mStyle);
             return builder.build();
@@ -403,12 +475,38 @@
         }
 
         @Override
+        public int getActionCount(Notification n) {
+            return NotificationCompatKitKat.getActionCount(n);
+        }
+
+        @Override
+        public Action getAction(Notification n, int actionIndex) {
+            return (Action) NotificationCompatKitKat.getAction(n, actionIndex, Action.FACTORY,
+                    RemoteInput.FACTORY);
+        }
+
+        @Override
         public boolean getLocalOnly(Notification n) {
             return NotificationCompatKitKat.getLocalOnly(n);
         }
+
+        @Override
+        public String getGroup(Notification n) {
+            return NotificationCompatKitKat.getGroup(n);
+        }
+
+        @Override
+        public boolean isGroupSummary(Notification n) {
+            return NotificationCompatKitKat.isGroupSummary(n);
+        }
+
+        @Override
+        public String getSortKey(Notification n) {
+            return NotificationCompatKitKat.getSortKey(n);
+        }
     }
 
-    static class NotificationCompatImplApi20 extends NotificationCompatImplBase {
+    static class NotificationCompatImplApi20 extends NotificationCompatImplKitKat {
         @Override
         public Notification build(Builder b) {
             NotificationCompatApi20.Builder builder = new NotificationCompatApi20.Builder(
@@ -416,27 +514,44 @@
                     b.mTickerView, b.mNumber, b.mContentIntent, b.mFullScreenIntent, b.mLargeIcon,
                     b.mProgressMax, b.mProgress, b.mProgressIndeterminate,
                     b.mUseChronometer, b.mPriority, b.mSubText, b.mLocalOnly, b.mCategory,
-                    b.mPeople, b.mExtras, b.mColor, b.mVisibility, b.mPublicVersion);
+                    b.mPeople, b.mExtras, b.mColor, b.mVisibility, b.mPublicVersion,
+                    b.mGroupKey, b.mGroupSummary, b.mSortKey);
             addActionsToBuilder(builder, b.mActions);
             addStyleToBuilderJellybean(builder, b.mStyle);
             return builder.build();
         }
 
         @Override
-        public Bundle getExtras(Notification n) {
-            return NotificationCompatKitKat.getExtras(n);
+        public Action getAction(Notification n, int actionIndex) {
+            return (Action) NotificationCompatApi20.getAction(n, actionIndex, Action.FACTORY,
+                    RemoteInput.FACTORY);
         }
 
         @Override
         public boolean getLocalOnly(Notification n) {
             return NotificationCompatApi20.getLocalOnly(n);
         }
+
+        @Override
+        public String getGroup(Notification n) {
+            return NotificationCompatApi20.getGroup(n);
+        }
+
+        @Override
+        public boolean isGroupSummary(Notification n) {
+            return NotificationCompatApi20.isGroupSummary(n);
+        }
+
+        @Override
+        public String getSortKey(Notification n) {
+            return NotificationCompatApi20.getSortKey(n);
+        }
     }
 
     private static void addActionsToBuilder(NotificationBuilderWithActions builder,
             ArrayList<Action> actions) {
         for (Action action : actions) {
-            builder.addAction(action.icon, action.title, action.actionIntent);
+            builder.addAction(action);
         }
     }
 
@@ -527,6 +642,9 @@
         int mProgressMax;
         int mProgress;
         boolean mProgressIndeterminate;
+        String mGroupKey;
+        boolean mGroupSummary;
+        String mSortKey;
         ArrayList<Action> mActions = new ArrayList<Action>();
         boolean mLocalOnly = false;
         String mCategory;
@@ -696,8 +814,8 @@
          * Supply a {@link PendingIntent} to send when the notification is cleared by the user
          * directly from the notification panel.  For example, this intent is sent when the user
          * clicks the "Clear all" button, or the individual "X" buttons on notifications.  This
-         * intent is not sent when the application calls {@link NotificationManager#cancel
-         * NotificationManager.cancel(int)}.
+         * intent is not sent when the application calls
+         * {@link android.app.NotificationManager#cancel NotificationManager.cancel(int)}.
          */
         public Builder setDeleteIntent(PendingIntent intent) {
             mNotification.deleteIntent = intent;
@@ -916,17 +1034,64 @@
         }
 
         /**
+         * Set this notification to be part of a group of notifications sharing the same key.
+         * Grouped notifications may display in a cluster or stack on devices which
+         * support such rendering.
+         *
+         * <p>To make this notification the summary for its group, also call
+         * {@link #setGroupSummary}. A sort order can be specified for group members by using
+         * {@link #setSortKey}.
+         * @param groupKey The group key of the group.
+         * @return this object for method chaining
+         */
+        public Builder setGroup(String groupKey) {
+            mGroupKey = groupKey;
+            return this;
+        }
+
+        /**
+         * Set this notification to be the group summary for a group of notifications.
+         * Grouped notifications may display in a cluster or stack on devices which
+         * support such rendering. Requires a group key also be set using {@link #setGroup}.
+         * @param isGroupSummary Whether this notification should be a group summary.
+         * @return this object for method chaining
+         */
+        public Builder setGroupSummary(boolean isGroupSummary) {
+            mGroupSummary = isGroupSummary;
+            return this;
+        }
+
+        /**
+         * Set a sort key that orders this notification among other notifications from the
+         * same package. This can be useful if an external sort was already applied and an app
+         * would like to preserve this. Notifications will be sorted lexicographically using this
+         * value, although providing different priorities in addition to providing sort key may
+         * cause this value to be ignored.
+         *
+         * <p>This sort key can also be used to order members of a notification group. See
+         * {@link Builder#setGroup}.
+         *
+         * @see String#compareTo(String)
+         */
+        public Builder setSortKey(String sortKey) {
+            mSortKey = sortKey;
+            return this;
+        }
+
+        /**
          * Merge additional metadata into this notification.
          *
          * <p>Values within the Bundle will replace existing extras values in this Builder.
          *
          * @see Notification#extras
          */
-        public Builder addExtras(Bundle bag) {
-            if (mExtras == null) {
-                mExtras = new Bundle(bag);
-            } else {
-                mExtras.putAll(bag);
+        public Builder addExtras(Bundle extras) {
+            if (extras != null) {
+                if (mExtras == null) {
+                    mExtras = new Bundle(extras);
+                } else {
+                    mExtras.putAll(extras);
+                }
             }
             return this;
         }
@@ -943,8 +1108,8 @@
          *
          * @see Notification#extras
          */
-        public Builder setExtras(Bundle bag) {
-            mExtras = bag;
+        public Builder setExtras(Bundle extras) {
+            mExtras = extras;
             return this;
         }
 
@@ -987,6 +1152,25 @@
         }
 
         /**
+         * Add an action to this notification. Actions are typically displayed by
+         * the system as a button adjacent to the notification content.
+         * <br>
+         * Action buttons won't appear on platforms prior to Android 4.1. Action
+         * buttons depend on expanded notifications, which are only available in Android 4.1
+         * and later. To ensure that an action button's functionality is always available, first
+         * implement the functionality in the {@link android.app.Activity} that starts when a user
+         * clicks the  notification (see {@link #setContentIntent setContentIntent()}), and then
+         * enhance the notification by implementing the same functionality with
+         * {@link #addAction addAction()}.
+         *
+         * @param action The action to add.
+         */
+        public Builder addAction(Action action) {
+            mActions.add(action);
+            return this;
+        }
+
+        /**
          * Add a rich notification style to be applied at build time.
          * <br>
          * If the platform does not provide rich notification styles, this method has no effect. The
@@ -1042,6 +1226,28 @@
         }
 
         /**
+         * Apply an extender to this notification builder. Extenders may be used to add
+         * metadata or change options on this builder.
+         */
+        public Builder apply(Extender extender) {
+            extender.applyTo(this);
+            return this;
+        }
+
+        /**
+         * Extender interface for use with {@link #apply}. Extenders may be used to add
+         * metadata or change options on this builder.
+         */
+        public interface Extender {
+            /**
+             * Apply this extender to a notification builder.
+             * @param builder the builder to be modified.
+             * @return the build object for chaining.
+             */
+            public Builder applyTo(Builder builder);
+        }
+
+        /**
          * @deprecated Use {@link #build()} instead.
          */
         @Deprecated
@@ -1277,16 +1483,199 @@
         }
     }
 
-    public static class Action {
+    /**
+     * Structure to encapsulate a named action that can be shown as part of this notification.
+     * It must include an icon, a label, and a {@link PendingIntent} to be fired when the action is
+     * selected by the user. Action buttons won't appear on platforms prior to Android 4.1.
+     * <p>
+     * Apps should use {@link NotificationCompat.Builder#addAction(int, CharSequence, PendingIntent)}
+     * or {@link NotificationCompat.Builder#addAction(NotificationCompat.Action)}
+     * to attach actions.
+     */
+    public static class Action extends NotificationCompatBase.Action {
+        private final Bundle mExtras;
+        private RemoteInput[] mRemoteInputs;
+
+        /**
+         * Small icon representing the action.
+         */
         public int icon;
+        /**
+         * Title of the action.
+         */
         public CharSequence title;
+        /**
+         * Intent to send when the user invokes this action. May be null, in which case the action
+         * may be rendered in a disabled presentation.
+         */
         public PendingIntent actionIntent;
 
-        public Action(int icon_, CharSequence title_, PendingIntent intent_) {
-            this.icon = icon_;
-            this.title = title_;
-            this.actionIntent = intent_;
+        public Action(int icon, CharSequence title, PendingIntent intent) {
+            this(icon, title, intent, new Bundle(), null);
         }
+
+        private Action(int icon, CharSequence title, PendingIntent intent, Bundle extras,
+                RemoteInput[] remoteInputs) {
+            this.icon = icon;
+            this.title = title;
+            this.actionIntent = intent;
+            this.mExtras = extras != null ? extras : new Bundle();
+            this.mRemoteInputs = remoteInputs;
+        }
+
+        @Override
+        protected int getIcon() {
+            return icon;
+        }
+
+        @Override
+        protected CharSequence getTitle() {
+            return title;
+        }
+
+        @Override
+        protected PendingIntent getActionIntent() {
+            return actionIntent;
+        }
+
+        /**
+         * Get additional metadata carried around with this Action.
+         */
+        public Bundle getExtras() {
+            return mExtras;
+        }
+
+        /**
+         * Get the list of inputs to be collected from the user when this action is sent.
+         * May return null if no remote inputs were added.
+         */
+        public RemoteInput[] getRemoteInputs() {
+            return mRemoteInputs;
+        }
+
+        /**
+         * Builder class for {@link Action} objects.
+         */
+        public static final class Builder {
+            private final int mIcon;
+            private final CharSequence mTitle;
+            private final PendingIntent mIntent;
+            private final Bundle mExtras;
+            private ArrayList<RemoteInput> mRemoteInputs;
+
+            /**
+             * Construct a new builder for {@link Action} object.
+             * @param icon icon to show for this action
+             * @param title the title of the action
+             * @param intent the {@link PendingIntent} to fire when users trigger this action
+             */
+            public Builder(int icon, CharSequence title, PendingIntent intent) {
+                this(icon, title, intent, new Bundle());
+            }
+
+            /**
+             * Construct a new builder for {@link Action} object using the fields from an
+             * {@link Action}.
+             * @param action the action to read fields from.
+             */
+            public Builder(Action action) {
+                this(action.icon, action.title, action.actionIntent, new Bundle(action.mExtras));
+            }
+
+            private Builder(int icon, CharSequence title, PendingIntent intent, Bundle extras) {
+                mIcon = icon;
+                mTitle = title;
+                mIntent = intent;
+                mExtras = extras;
+            }
+
+            /**
+             * Merge additional metadata into this builder.
+             *
+             * <p>Values within the Bundle will replace existing extras values in this Builder.
+             *
+             * @see NotificationCompat.Action#getExtras
+             */
+            public Builder addExtras(Bundle extras) {
+                if (extras != null) {
+                    mExtras.putAll(extras);
+                }
+                return this;
+            }
+
+            /**
+             * Get the metadata Bundle used by this Builder.
+             *
+             * <p>The returned Bundle is shared with this Builder.
+             */
+            public Bundle getExtras() {
+                return mExtras;
+            }
+
+            /**
+             * Add an input to be collected from the user when this action is sent.
+             * Response values can be retrieved from the fired intent by using the
+             * {@link RemoteInput#getResultsFromIntent} function.
+             * @param remoteInput a {@link RemoteInput} to add to the action
+             * @return this object for method chaining
+             */
+            public Builder addRemoteInput(RemoteInput remoteInput) {
+                if (mRemoteInputs == null) {
+                    mRemoteInputs = new ArrayList<RemoteInput>();
+                }
+                mRemoteInputs.add(remoteInput);
+                return this;
+            }
+
+            /**
+             * Apply an extender to this action builder. Extenders may be used to add
+             * metadata or change options on this builder.
+             */
+            public Builder apply(Extender extender) {
+                extender.applyTo(this);
+                return this;
+            }
+
+            /**
+             * Extender interface for use with {@link #apply}. Extenders may be used to add
+             * metadata or change options on this builder.
+             */
+            public interface Extender {
+                /**
+                 * Apply this extender to a notification action builder.
+                 * @param builder the builder to be modified.
+                 * @return the build object for chaining.
+                 */
+                public Builder applyTo(Builder builder);
+            }
+
+            /**
+             * Combine all of the options that have been set and return a new {@link Action}
+             * object.
+             * @return the built action
+             */
+            public Action build() {
+                RemoteInput[] remoteInputs = mRemoteInputs != null
+                        ? mRemoteInputs.toArray(new RemoteInput[mRemoteInputs.size()]) : null;
+                return new Action(mIcon, mTitle, mIntent, mExtras, remoteInputs);
+            }
+        }
+
+        /** @hide */
+        public static final Factory FACTORY = new Factory() {
+            @Override
+            public Action build(int icon, CharSequence title,
+                    PendingIntent actionIntent, Bundle extras,
+                    RemoteInputCompatBase.RemoteInput[] remoteInputs) {
+                return new Action(icon, title, actionIntent, extras,
+                        (RemoteInput[]) remoteInputs);
+            }
+
+            @Override
+            public Action[] newArray(int length) {
+                return new Action[length];
+            }
+        };
     }
 
     /**
@@ -1299,6 +1688,24 @@
     }
 
     /**
+     * Get the number of actions in this notification in a backwards compatible
+     * manner. Actions were supported from JellyBean (Api level 16) forwards.
+     */
+    public static int getActionCount(Notification notif) {
+        return IMPL.getActionCount(notif);
+    }
+
+    /**
+     * Get an action on this notification in a backwards compatible
+     * manner. Actions were supported from JellyBean (Api level 16) forwards.
+     * @param notif The notification to inspect.
+     * @param actionIndex The index of the action to retrieve.
+     */
+    public static Action getAction(Notification notif, int actionIndex) {
+        return IMPL.getAction(notif, actionIndex);
+    }
+
+    /**
      * Get whether or not this notification is only relevant to the current device.
      *
      * <p>Some notifications can be bridged to other devices for remote display.
@@ -1307,4 +1714,38 @@
     public static boolean getLocalOnly(Notification notif) {
         return IMPL.getLocalOnly(notif);
     }
+
+    /**
+     * Get the key used to group this notification into a cluster or stack
+     * with other notifications on devices which support such rendering.
+     */
+    public static String getGroup(Notification notif) {
+        return IMPL.getGroup(notif);
+    }
+
+    /**
+     * Get whether this notification to be the group summary for a group of notifications.
+     * Grouped notifications may display in a cluster or stack on devices which
+     * support such rendering. Requires a group key also be set using {@link Builder#setGroup}.
+     * @return Whether this notification is a group summary.
+     */
+    public static boolean isGroupSummary(Notification notif) {
+        return IMPL.isGroupSummary(notif);
+    }
+
+    /**
+     * Get a sort key that orders this notification among other notifications from the
+     * same package. This can be useful if an external sort was already applied and an app
+     * would like to preserve this. Notifications will be sorted lexicographically using this
+     * value, although providing different priorities in addition to providing sort key may
+     * cause this value to be ignored.
+     *
+     * <p>This sort key can also be used to order members of a notification group. See
+     * {@link Builder#setGroup}.
+     *
+     * @see String#compareTo(String)
+     */
+    public static String getSortKey(Notification notif) {
+        return IMPL.getSortKey(notif);
+    }
 }
diff --git a/v4/java/android/support/v4/app/NotificationCompatExtras.java b/v4/java/android/support/v4/app/NotificationCompatExtras.java
index de6e8cd..6a2ee93 100644
--- a/v4/java/android/support/v4/app/NotificationCompatExtras.java
+++ b/v4/java/android/support/v4/app/NotificationCompatExtras.java
@@ -22,10 +22,49 @@
 public final class NotificationCompatExtras {
     /**
      * Extras key used internally by {@link NotificationCompat} to store the value of
-     * the {@code Notification.FLAG_LOCAL_ONLY} field before it was available.
-     * If possible, use {@link NotificationCompat#getLocalOnly} instead.
+     * the {@link android.app.Notification#FLAG_LOCAL_ONLY} field before it was available.
+     * If possible, use {@link NotificationCompat#getLocalOnly} to access this field.
      */
     public static final String EXTRA_LOCAL_ONLY = NotificationCompatJellybean.EXTRA_LOCAL_ONLY;
 
+    /**
+     * Extras key used internally by {@link NotificationCompat} to store the value set
+     * by {@link android.app.Notification.Builder#setGroup} before it was available.
+     * If possible, use {@link NotificationCompat#getGroup} to access this value.
+     */
+    public static final String EXTRA_GROUP_KEY = NotificationCompatJellybean.EXTRA_GROUP_KEY;
+
+    /**
+     * Extras key used internally by {@link NotificationCompat} to store the value set
+     * by {@link android.app.Notification.Builder#setGroupSummary} before it was available.
+     * If possible, use {@link NotificationCompat#isGroupSummary} to access this value.
+     */
+    public static final String EXTRA_GROUP_SUMMARY =
+            NotificationCompatJellybean.EXTRA_GROUP_SUMMARY;
+
+    /**
+     * Extras key used internally by {@link NotificationCompat} to store the value set
+     * by {@link android.app.Notification.Builder#setSortKey} before it was available.
+     * If possible, use {@link NotificationCompat#getSortKey} to access this value.
+     */
+    public static final String EXTRA_SORT_KEY = NotificationCompatJellybean.EXTRA_SORT_KEY;
+
+    /**
+     * Extras key used internally by {@link NotificationCompat} to store the value of
+     * the {@link android.app.Notification.Action#extras} field before it was available.
+     * If possible, use {@link NotificationCompat#getAction} to access this field.
+     */
+    public static final String EXTRA_ACTION_EXTRAS =
+            NotificationCompatJellybean.EXTRA_ACTION_EXTRAS;
+
+    /**
+     * Extras key used internally by {@link NotificationCompat} to store the value of
+     * the {@link android.app.Notification.Action#getRemoteInputs} before the field
+     * was available.
+     * If possible, use {@link NotificationCompat.Action#getRemoteInputs to access this field.
+     */
+    public static final String EXTRA_REMOTE_INPUTS =
+            NotificationCompatJellybean.EXTRA_REMOTE_INPUTS;
+
     private NotificationCompatExtras() {}
 }
diff --git a/v4/java/android/support/v4/app/NotificationCompatSideChannelService.java b/v4/java/android/support/v4/app/NotificationCompatSideChannelService.java
new file mode 100644
index 0000000..f78124d
--- /dev/null
+++ b/v4/java/android/support/v4/app/NotificationCompatSideChannelService.java
@@ -0,0 +1,112 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.app;
+
+import android.app.Notification;
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+import android.os.RemoteException;
+
+/**
+ * Abstract service to receive side channel notifications sent from
+ * {@link android.support.v4.app.NotificationManagerCompat}.
+ *
+ * <p>To receive side channel notifications, extend this service and register it in your
+ * android manifest with an intent filter for the BIND_SIDE_CHANNEL action.
+ * Note: you must also have an enabled
+ * {@link android.service.notification.NotificationListenerService} within your package.
+ *
+ * <p>Example AndroidManifest.xml addition:
+ * <pre>
+ * &lt;service android:name="com.example.NotificationSideChannelService"&gt;
+ *     &lt;intent-filter&gt;
+ *         &lt;action android:name="android.support.wearable.app.BIND_SIDE_CHANNEL" /&gt;
+ *     &lt;/intent-filter&gt;
+ * &lt;/service&gt;</pre>
+ *
+ */
+public abstract class NotificationCompatSideChannelService extends Service {
+    @Override
+    public IBinder onBind(Intent intent) {
+        if (intent.getAction().equals(NotificationManagerCompat.ACTION_BIND_SIDE_CHANNEL)) {
+            return new NotificationSideChannelStub();
+        }
+        return null;
+    }
+
+    /**
+     * Handle a side-channel notification was posted to the service.
+     */
+    public abstract void notify(String packageName, int id, String tag, Notification notification);
+
+    /**
+     * Handle a side-channel cancelling of an already-notified notification.
+     */
+    public abstract void cancel(String packageName, int id, String tag);
+
+    /**
+     * Handle a side-channel cancelling of all notifications for the given package.
+     */
+    public abstract void cancelAll(String packageName);
+
+    private class NotificationSideChannelStub extends INotificationSideChannel.Stub {
+        @Override
+        public void notify(String packageName, int id, String tag, Notification notification)
+                throws RemoteException {
+            checkPermission(getCallingUid(), packageName);
+            long idToken = clearCallingIdentity();
+            try {
+                NotificationCompatSideChannelService.this.notify(packageName, id, tag, notification);
+            } finally {
+                restoreCallingIdentity(idToken);
+            }
+        }
+
+        @Override
+        public void cancel(String packageName, int id, String tag) throws RemoteException {
+            checkPermission(getCallingUid(), packageName);
+            long idToken = clearCallingIdentity();
+            try {
+                NotificationCompatSideChannelService.this.cancel(packageName, id, tag);
+            } finally {
+                restoreCallingIdentity(idToken);
+            }
+        }
+
+        @Override
+        public void cancelAll(String packageName) {
+            checkPermission(getCallingUid(), packageName);
+            long idToken = clearCallingIdentity();
+            try {
+                NotificationCompatSideChannelService.this.cancelAll(packageName);
+            } finally {
+                restoreCallingIdentity(idToken);
+            }
+        }
+    }
+
+    private void checkPermission(int callingUid, String packageName) {
+        for (String validPackage : getPackageManager().getPackagesForUid(callingUid)) {
+            if (validPackage.equals(packageName)) {
+                return;
+            }
+        }
+        throw new SecurityException("NotificationSideChannelService: Uid " + callingUid
+                + " is not authorized for package " + packageName);
+    }
+}
diff --git a/v4/java/android/support/v4/app/NotificationManagerCompat.java b/v4/java/android/support/v4/app/NotificationManagerCompat.java
new file mode 100644
index 0000000..f12dc5b
--- /dev/null
+++ b/v4/java/android/support/v4/app/NotificationManagerCompat.java
@@ -0,0 +1,613 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.app;
+
+import android.app.Notification;
+import android.app.NotificationManager;
+import android.app.Service;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.ServiceConnection;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.DeadObjectException;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Message;
+import android.os.RemoteException;
+import android.provider.Settings;
+import android.util.Log;
+
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Compatibility library for NotificationManager with fallbacks for older platforms.
+ *
+ * <p>To use this class, call the static function {@link #from} to get a
+ * {@link NotificationManagerCompat} object, and then call one of its
+ * methods to post or cancel notifications.
+ */
+public class NotificationManagerCompat {
+    private static final String TAG = "NotifManCompat";
+
+    /**
+     * Notification extras key: if set to true, the posted notification should use
+     * the side channel for delivery instead of using notification manager.
+     */
+    public static final String EXTRA_USE_SIDE_CHANNEL =
+            NotificationCompatJellybean.EXTRA_USE_SIDE_CHANNEL;
+
+    /**
+     * Intent action to register for on a service to receive side channel
+     * notifications. The listening service must be in the same package as an enabled
+     * {@link android.service.notification.NotificationListenerService}.
+     */
+    public static final String ACTION_BIND_SIDE_CHANNEL =
+            "android.support.wearable.app.BIND_SIDE_CHANNEL";
+
+    /** Base time delay for a side channel listener queue retry. */
+    private static final int SIDE_CHANNEL_RETRY_BASE_INTERVAL_MS = 1000;
+    /** Maximum retries for a side channel listener before dropping tasks. */
+    private static final int SIDE_CHANNEL_RETRY_MAX_COUNT = 6;
+    /** Hidden field Settings.Secure.ENABLED_NOTIFICATION_LISTENERS */
+    private static final String SETTING_ENABLED_NOTIFICATION_LISTENERS =
+            "enabled_notification_listeners";
+    private static final int SIDE_CHANNEL_BIND_FLAGS;
+
+    /** Cache of enabled notification listener components */
+    private static final Object sEnabledNotificationListenersLock = new Object();
+    /** Guarded by {@link #sEnabledNotificationListenersLock} */
+    private static String sEnabledNotificationListeners;
+    /** Guarded by {@link #sEnabledNotificationListenersLock} */
+    private static Set<String> sEnabledNotificationListenerPackages = new HashSet<String>();
+
+    private final Context mContext;
+    private final NotificationManager mNotificationManager;
+    /** Lock for mutable static fields */
+    private static final Object sLock = new Object();
+    /** Guarded by {@link #sLock} */
+    private static SideChannelManager sSideChannelManager;
+
+    /** Get a {@link NotificationManagerCompat} instance for a provided context. */
+    public static NotificationManagerCompat from(Context context) {
+        return new NotificationManagerCompat(context);
+    }
+
+    private NotificationManagerCompat(Context context) {
+        mContext = context;
+        mNotificationManager = (NotificationManager) mContext.getSystemService(
+                Context.NOTIFICATION_SERVICE);
+    }
+
+    private static final Impl IMPL;
+
+    interface Impl {
+        void cancelNotification(NotificationManager notificationManager, String tag, int id);
+
+        void postNotification(NotificationManager notificationManager, String tag, int id,
+                Notification notification);
+
+        int getSideChannelBindFlags();
+    }
+
+    static class ImplBase implements Impl {
+        @Override
+        public void cancelNotification(NotificationManager notificationManager, String tag,
+                int id) {
+            notificationManager.cancel(id);
+        }
+
+        @Override
+        public void postNotification(NotificationManager notificationManager, String tag, int id,
+                Notification notification) {
+            notificationManager.notify(id, notification);
+        }
+
+        @Override
+        public int getSideChannelBindFlags() {
+            return Service.BIND_AUTO_CREATE;
+        }
+    }
+
+    static class ImplEclair extends ImplBase {
+        @Override
+        public void cancelNotification(NotificationManager notificationManager, String tag,
+                int id) {
+            NotificationManagerCompatEclair.cancelNotification(notificationManager, tag, id);
+        }
+
+        @Override
+        public void postNotification(NotificationManager notificationManager, String tag, int id,
+                Notification notification) {
+            NotificationManagerCompatEclair.postNotification(notificationManager, tag, id,
+                    notification);
+        }
+    }
+
+    static class ImplIceCreamSandwich extends ImplBase {
+        @Override
+        public int getSideChannelBindFlags() {
+            return NotificationManagerCompatIceCreamSandwich.SIDE_CHANNEL_BIND_FLAGS;
+        }
+    }
+
+    static {
+        if (Build.VERSION.SDK_INT >= 5) {
+            IMPL = new ImplEclair();
+        } else {
+            IMPL = new ImplBase();
+        }
+        SIDE_CHANNEL_BIND_FLAGS = IMPL.getSideChannelBindFlags();
+    }
+
+    /**
+     * Cancel a previously shown notification.
+     * @param id the ID of the notification
+     */
+    public void cancel(int id) {
+        cancel(null, id);
+    }
+
+    /**
+     * Cancel a previously shown notification.
+     * @param tag the string identifier of the notification.
+     * @param id the ID of the notification
+     */
+    public void cancel(String tag, int id) {
+        IMPL.cancelNotification(mNotificationManager, tag, id);
+        pushSideChannelQueue(new CancelTask(mContext.getPackageName(), id, tag));
+    }
+
+    /** Cancel all previously shown notifications. */
+    public void cancelAll() {
+        mNotificationManager.cancelAll();
+        pushSideChannelQueue(new CancelTask(mContext.getPackageName()));
+    }
+
+    /**
+     * Post a notification to be shown in the status bar, stream, etc.
+     * @param id the ID of the notification
+     * @param notification the notification to post to the system
+     */
+    public void notify(int id, Notification notification) {
+        notify(null, id, notification);
+    }
+
+    /**
+     * Post a notification to be shown in the status bar, stream, etc.
+     * @param tag the string identifier for a notification. Can be {@code null}.
+     * @param id the ID of the notification. The pair (tag, id) must be unique within your app.
+     * @param notification the notification to post to the system
+    */
+    public void notify(String tag, int id, Notification notification) {
+        if (useSideChannelForNotification(notification)) {
+            pushSideChannelQueue(new NotifyTask(mContext.getPackageName(), id, tag, notification));
+        } else {
+            IMPL.postNotification(mNotificationManager, tag, id, notification);
+        }
+    }
+
+    /**
+     * Get the set of packages that have an enabled notification listener component within them.
+     */
+    public static Set<String> getEnabledListenerPackages(Context context) {
+        final String enabledNotificationListeners = Settings.Secure.getString(
+                context.getContentResolver(),
+                SETTING_ENABLED_NOTIFICATION_LISTENERS);
+        // Parse the string again if it is different from the last time this method was called.
+        if (enabledNotificationListeners != null
+                && !enabledNotificationListeners.equals(sEnabledNotificationListeners)) {
+            final String[] components = enabledNotificationListeners.split(":");
+            Set<String> packageNames = new HashSet<String>(components.length);
+            for (String component : components) {
+                ComponentName componentName = ComponentName.unflattenFromString(component);
+                if (componentName != null) {
+                    packageNames.add(componentName.getPackageName());
+                }
+            }
+            synchronized (sEnabledNotificationListenersLock) {
+                sEnabledNotificationListenerPackages = packageNames;
+                sEnabledNotificationListeners = enabledNotificationListeners;
+            }
+        }
+        return sEnabledNotificationListenerPackages;
+    }
+
+    /**
+     * Returns true if this notification should use the side channel for delivery.
+     */
+    private static boolean useSideChannelForNotification(Notification notification) {
+        Bundle extras = NotificationCompat.getExtras(notification);
+        return extras != null && extras.getBoolean(EXTRA_USE_SIDE_CHANNEL);
+    }
+
+    /**
+     * Push a notification task for distribution to notification side channels.
+     */
+    private void pushSideChannelQueue(Task task) {
+        synchronized (sLock) {
+            if (sSideChannelManager == null) {
+                sSideChannelManager = new SideChannelManager(mContext.getApplicationContext());
+            }
+        }
+        sSideChannelManager.queueTask(task);
+    }
+
+    /**
+     * Helper class to manage a queue of pending tasks to send to notification side channel
+     * listeners.
+     */
+    private static class SideChannelManager implements Handler.Callback, ServiceConnection {
+        private static final int MSG_QUEUE_TASK = 0;
+        private static final int MSG_SERVICE_CONNECTED = 1;
+        private static final int MSG_SERVICE_DISCONNECTED = 2;
+        private static final int MSG_RETRY_LISTENER_QUEUE = 3;
+
+        private static final String KEY_BINDER = "binder";
+
+        private final Context mContext;
+        private final HandlerThread mHandlerThread;
+        private final Handler mHandler;
+        private final Map<ComponentName, ListenerRecord> mRecordMap =
+                new HashMap<ComponentName, ListenerRecord>();
+        private Set<String> mCachedEnabledPackages = new HashSet<String>();
+
+        public SideChannelManager(Context context) {
+            mContext = context;
+            mHandlerThread = new HandlerThread("NotificationManagerCompat");
+            mHandlerThread.start();
+            mHandler = new Handler(mHandlerThread.getLooper(), this);
+        }
+
+        /**
+         * Queue a new task to be sent to all listeners. This function can be called
+         * from any thread.
+         */
+        public void queueTask(Task task) {
+            mHandler.obtainMessage(MSG_QUEUE_TASK, task).sendToTarget();
+        }
+
+        @Override
+        public boolean handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_QUEUE_TASK:
+                    handleQueueTask((Task) msg.obj);
+                    return true;
+                case MSG_SERVICE_CONNECTED:
+                    ServiceConnectedEvent event = (ServiceConnectedEvent) msg.obj;
+                    handleServiceConnected(event.componentName, event.iBinder);
+                    return true;
+                case MSG_SERVICE_DISCONNECTED:
+                    handleServiceDisconnected((ComponentName) msg.obj);
+                    return true;
+                case MSG_RETRY_LISTENER_QUEUE:
+                    handleRetryListenerQueue((ComponentName) msg.obj);
+                    return true;
+            }
+            return false;
+        }
+
+        private void handleQueueTask(Task task) {
+            updateListenerMap();
+            for (ListenerRecord record : mRecordMap.values()) {
+                record.taskQueue.add(task);
+                processListenerQueue(record);
+            }
+        }
+
+        private void handleServiceConnected(ComponentName componentName, IBinder iBinder) {
+            ListenerRecord record = mRecordMap.get(componentName);
+            if (record != null) {
+                record.service = INotificationSideChannel.Stub.asInterface(iBinder);
+                record.retryCount = 0;
+                processListenerQueue(record);
+            }
+        }
+
+        private void handleServiceDisconnected(ComponentName componentName) {
+            ListenerRecord record = mRecordMap.get(componentName);
+            if (record != null) {
+                ensureServiceUnbound(record);
+            }
+        }
+
+        private void handleRetryListenerQueue(ComponentName componentName) {
+            ListenerRecord record = mRecordMap.get(componentName);
+            if (record != null) {
+                processListenerQueue(record);
+            }
+        }
+
+        @Override
+        public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Connected to service " + componentName);
+            }
+            mHandler.obtainMessage(MSG_SERVICE_CONNECTED,
+                    new ServiceConnectedEvent(componentName, iBinder))
+                    .sendToTarget();
+        }
+
+        @Override
+        public void onServiceDisconnected(ComponentName componentName) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Disconnected from service " + componentName);
+            }
+            mHandler.obtainMessage(MSG_SERVICE_DISCONNECTED, componentName).sendToTarget();
+        }
+
+        /**
+         * Check the current list of enabled listener packages and update the records map
+         * accordingly.
+         */
+        private void updateListenerMap() {
+            Set<String> enabledPackages = getEnabledListenerPackages(mContext);
+            if (enabledPackages.equals(mCachedEnabledPackages)) {
+                // Short-circuit when the list of enabled packages has not changed.
+                return;
+            }
+            mCachedEnabledPackages = enabledPackages;
+            List<ResolveInfo> resolveInfos = mContext.getPackageManager().queryIntentServices(
+                    new Intent().setAction(ACTION_BIND_SIDE_CHANNEL), PackageManager.GET_SERVICES);
+            Set<ComponentName> enabledComponents = new HashSet<ComponentName>();
+            for (ResolveInfo resolveInfo : resolveInfos) {
+                if (!enabledPackages.contains(resolveInfo.serviceInfo.packageName)) {
+                    continue;
+                }
+                ComponentName componentName = new ComponentName(
+                        resolveInfo.serviceInfo.packageName, resolveInfo.serviceInfo.name);
+                if (resolveInfo.serviceInfo.permission != null) {
+                    Log.w(TAG, "Permission present on component " + componentName
+                            + ", not adding listener record.");
+                    continue;
+                }
+                enabledComponents.add(componentName);
+            }
+            // Ensure all enabled components have a record in the listener map.
+            for (ComponentName componentName : enabledComponents) {
+                if (!mRecordMap.containsKey(componentName)) {
+                    if (Log.isLoggable(TAG, Log.DEBUG)) {
+                        Log.d(TAG, "Adding listener record for " + componentName);
+                    }
+                    mRecordMap.put(componentName, new ListenerRecord(componentName));
+                }
+            }
+            // Remove listener records that are no longer for enabled components.
+            Iterator<Map.Entry<ComponentName, ListenerRecord>> it =
+                    mRecordMap.entrySet().iterator();
+            while (it.hasNext()) {
+                Map.Entry<ComponentName, ListenerRecord> entry = it.next();
+                if (!enabledComponents.contains(entry.getKey())) {
+                    if (Log.isLoggable(TAG, Log.DEBUG)) {
+                        Log.d(TAG, "Removing listener record for " + entry.getKey());
+                    }
+                    ensureServiceUnbound(entry.getValue());
+                    it.remove();
+                }
+            }
+        }
+
+        /**
+         * Ensure we are already attempting to bind to a service, or start a new binding if not.
+         * @return Whether the service bind attempt was successful.
+         */
+        private boolean ensureServiceBound(ListenerRecord record) {
+            if (record.bound) {
+                return true;
+            }
+            Intent intent = new Intent(ACTION_BIND_SIDE_CHANNEL).setComponent(record.componentName);
+            record.bound = mContext.bindService(intent, this, SIDE_CHANNEL_BIND_FLAGS);
+            if (record.bound) {
+                record.retryCount = 0;
+            } else {
+                Log.w(TAG, "Unable to bind to listener " + record.componentName);
+                mContext.unbindService(this);
+            }
+            return record.bound;
+        }
+
+        /**
+         * Ensure we have unbound from a service.
+         */
+        private void ensureServiceUnbound(ListenerRecord record) {
+            if (record.bound) {
+                mContext.unbindService(this);
+                record.bound = false;
+            }
+            record.service = null;
+        }
+
+        /**
+         * Schedule a delayed retry to communicate with a listener service.
+         * After a maximum number of attempts (with exponential back-off), start
+         * dropping pending tasks for this listener.
+         */
+        private void scheduleListenerRetry(ListenerRecord record) {
+            if (mHandler.hasMessages(MSG_RETRY_LISTENER_QUEUE, record.componentName)) {
+                return;
+            }
+            record.retryCount++;
+            if (record.retryCount > SIDE_CHANNEL_RETRY_MAX_COUNT) {
+                Log.w(TAG, "Giving up on delivering " + record.taskQueue.size() + " tasks to "
+                        + record.componentName + " after " + record.retryCount + " retries");
+                record.taskQueue.clear();
+                return;
+            }
+            int delayMs = SIDE_CHANNEL_RETRY_BASE_INTERVAL_MS * (1 << (record.retryCount - 1));
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Scheduling retry for " + delayMs + " ms");
+            }
+            Message msg = mHandler.obtainMessage(MSG_RETRY_LISTENER_QUEUE, record.componentName);
+            mHandler.sendMessageDelayed(msg, delayMs);
+        }
+
+        /**
+         * Perform a processing step for a listener. First check the bind state, then attempt
+         * to flush the task queue, and if an error is encountered, schedule a retry.
+         */
+        private void processListenerQueue(ListenerRecord record) {
+            if (Log.isLoggable(TAG, Log.DEBUG)) {
+                Log.d(TAG, "Processing component " + record.componentName + ", "
+                        + record.taskQueue.size() + " queued tasks");
+            }
+            if (record.taskQueue.isEmpty()) {
+                return;
+            }
+            if (!ensureServiceBound(record) || record.service == null) {
+                // Ensure bind has started and that a service interface is ready to use.
+                scheduleListenerRetry(record);
+                return;
+            }
+            // Attempt to flush all items in the task queue.
+            while (true) {
+                Task task = record.taskQueue.peek();
+                if (task == null) {
+                    break;
+                }
+                try {
+                    if (Log.isLoggable(TAG, Log.DEBUG)) {
+                        Log.d(TAG, "Sending task " + task);
+                    }
+                    task.send(record.service);
+                    record.taskQueue.remove();
+                } catch (DeadObjectException e) {
+                    if (Log.isLoggable(TAG, Log.DEBUG)) {
+                        Log.d(TAG, "Remote service has died: " + record.componentName);
+                    }
+                    break;
+                } catch (RemoteException e) {
+                    Log.w(TAG, "RemoteException communicating with " + record.componentName, e);
+                    break;
+                }
+            }
+            if (!record.taskQueue.isEmpty()) {
+                // Some tasks were not sent, meaning an error was encountered, schedule a retry.
+                scheduleListenerRetry(record);
+            }
+        }
+
+        /** A per-side-channel-service listener state record */
+        private static class ListenerRecord {
+            public final ComponentName componentName;
+            /** Whether the service is currently bound to. */
+            public boolean bound = false;
+            /** The service stub provided by onServiceConnected */
+            public INotificationSideChannel service;
+            /** Queue of pending tasks to send to this listener service */
+            public LinkedList<Task> taskQueue = new LinkedList<Task>();
+            /** Number of retries attempted while connecting to this listener service */
+            public int retryCount = 0;
+
+            public ListenerRecord(ComponentName componentName) {
+                this.componentName = componentName;
+            }
+        }
+    }
+
+    private static class ServiceConnectedEvent {
+        final ComponentName componentName;
+        final IBinder iBinder;
+
+        public ServiceConnectedEvent(ComponentName componentName,
+                final IBinder iBinder) {
+            this.componentName = componentName;
+            this.iBinder = iBinder;
+        }
+    }
+
+    private interface Task {
+        public void send(INotificationSideChannel service) throws RemoteException;
+    }
+
+    private static class NotifyTask implements Task {
+        final String packageName;
+        final int id;
+        final String tag;
+        final Notification notif;
+
+        public NotifyTask(String packageName, int id, String tag, Notification notif) {
+            this.packageName = packageName;
+            this.id = id;
+            this.tag = tag;
+            this.notif = notif;
+        }
+
+        @Override
+        public void send(INotificationSideChannel service) throws RemoteException {
+            service.notify(packageName, id, tag, notif);
+        }
+
+        public String toString() {
+            StringBuilder sb = new StringBuilder("NotifyTask[");
+            sb.append("packageName:").append(packageName);
+            sb.append(", id:").append(id);
+            sb.append(", tag:").append(tag);
+            sb.append("]");
+            return sb.toString();
+        }
+    }
+
+    private static class CancelTask implements Task {
+        final String packageName;
+        final int id;
+        final String tag;
+        final boolean all;
+
+        public CancelTask(String packageName) {
+            this.packageName = packageName;
+            this.id = 0;
+            this.tag = null;
+            this.all = true;
+        }
+
+        public CancelTask(String packageName, int id, String tag) {
+            this.packageName = packageName;
+            this.id = id;
+            this.tag = tag;
+            this.all = false;
+        }
+
+        @Override
+        public void send(INotificationSideChannel service) throws RemoteException {
+            if (all) {
+                service.cancelAll(packageName);
+            } else {
+                service.cancel(packageName, id, tag);
+            }
+        }
+
+        public String toString() {
+            StringBuilder sb = new StringBuilder("CancelTask[");
+            sb.append("packageName:").append(packageName);
+            sb.append(", id:").append(id);
+            sb.append(", tag:").append(tag);
+            sb.append(", all:").append(all);
+            sb.append("]");
+            return sb.toString();
+        }
+    }
+}
diff --git a/v4/java/android/support/v4/app/RemoteInput.java b/v4/java/android/support/v4/app/RemoteInput.java
new file mode 100644
index 0000000..38ecad0
--- /dev/null
+++ b/v4/java/android/support/v4/app/RemoteInput.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.app;
+
+import android.content.Intent;
+import android.os.Build;
+import android.os.Bundle;
+import android.util.Log;
+
+/**
+ * Helper for using the {@link android.app.RemoteInput} API
+ * introduced after API level 4 in a backwards compatible fashion.
+ */
+public class RemoteInput extends RemoteInputCompatBase.RemoteInput {
+    private static final String TAG = "RemoteInput";
+
+    /** Label used to denote the clip data type used for remote input transport */
+    public static final String RESULTS_CLIP_LABEL = RemoteInputCompatJellybean.RESULTS_CLIP_LABEL;
+
+    /** Extra added to a clip data intent object to hold the results bundle. */
+    public static final String EXTRA_RESULTS_DATA = RemoteInputCompatJellybean.EXTRA_RESULTS_DATA;
+
+    private final String mResultKey;
+    private final CharSequence mLabel;
+    private final CharSequence[] mChoices;
+    private final boolean mAllowFreeFormInput;
+    private final Bundle mExtras;
+
+    RemoteInput(String resultKey, CharSequence label, CharSequence[] choices,
+            boolean allowFreeFormInput, Bundle extras) {
+        this.mResultKey = resultKey;
+        this.mLabel = label;
+        this.mChoices = choices;
+        this.mAllowFreeFormInput = allowFreeFormInput;
+        this.mExtras = extras;
+    }
+
+    /**
+     * Get the key that the result of this input will be set in from the Bundle returned by
+     * {@link #getResultsFromIntent} when the {@link android.app.PendingIntent} is sent.
+     */
+    public String getResultKey() {
+        return mResultKey;
+    }
+
+    /**
+     * Get the label to display to users when collecting this input.
+     */
+    public CharSequence getLabel() {
+        return mLabel;
+    }
+
+    /**
+     * Get possible input choices. This can be {@code null} if there are no choices to present.
+     */
+    public CharSequence[] getChoices() {
+        return mChoices;
+    }
+
+    /**
+     * Get whether or not users can provide an arbitrary value for
+     * input. If you set this to {@code false}, users must select one of the
+     * choices in {@link #getChoices}. An {@link IllegalArgumentException} is thrown
+     * if you set this to false and {@link #getChoices} returns {@code null} or empty.
+     */
+    public boolean getAllowFreeFormInput() {
+        return mAllowFreeFormInput;
+    }
+
+    /**
+     * Get additional metadata carried around with this remote input.
+     */
+    public Bundle getExtras() {
+        return mExtras;
+    }
+
+    /**
+     * Builder class for {@link android.support.v4.app.RemoteInput} objects.
+     */
+    public static final class Builder {
+        private final String mResultKey;
+        private CharSequence mLabel;
+        private CharSequence[] mChoices;
+        private boolean mAllowFreeFormInput = true;
+        private Bundle mExtras = new Bundle();
+
+        /**
+         * Create a builder object for {@link android.support.v4.app.RemoteInput} objects.
+         * @param resultKey the Bundle key that refers to this input when collected from the user
+         */
+        public Builder(String resultKey) {
+            if (resultKey == null) {
+                throw new IllegalArgumentException("Result key can't be null");
+            }
+            mResultKey = resultKey;
+        }
+
+        /**
+         * Set a label to be displayed to the user when collecting this input.
+         * @param label The label to show to users when they input a response.
+         * @return this object for method chaining
+         */
+        public Builder setLabel(CharSequence label) {
+            mLabel = label;
+            return this;
+        }
+
+        /**
+         * Specifies choices available to the user to satisfy this input.
+         * @param choices an array of pre-defined choices for users input.
+         *        You must provide a non-null and non-empty array if
+         *        you disabled free form input using {@link #setAllowFreeFormInput}.
+         * @return this object for method chaining
+         */
+        public Builder setChoices(CharSequence[] choices) {
+            mChoices = choices;
+            return this;
+        }
+
+        /**
+         * Specifies whether the user can provide arbitrary values.
+         *
+         * @param allowFreeFormInput The default is {@code true}.
+         *         If you specify {@code false}, you must provide a non-null
+         *         and non-empty array to {@link #setChoices} or an
+         *         {@link IllegalArgumentException} is thrown.
+         * @return this object for method chaining
+         */
+        public Builder setAllowFreeFormInput(boolean allowFreeFormInput) {
+            mAllowFreeFormInput = allowFreeFormInput;
+            return this;
+        }
+
+        /**
+         * Merge additional metadata into this builder.
+         *
+         * <p>Values within the Bundle will replace existing extras values in this Builder.
+         *
+         * @see RemoteInput#getExtras
+         */
+        public Builder addExtras(Bundle extras) {
+            if (extras != null) {
+                mExtras.putAll(extras);
+            }
+            return this;
+        }
+
+        /**
+         * Get the metadata Bundle used by this Builder.
+         *
+         * <p>The returned Bundle is shared with this Builder.
+         */
+        public Bundle getExtras() {
+            return mExtras;
+        }
+
+        /**
+         * Combine all of the options that have been set and return a new
+         * {@link android.support.v4.app.RemoteInput} object.
+         */
+        public RemoteInput build() {
+            return new RemoteInput(mResultKey, mLabel, mChoices, mAllowFreeFormInput, mExtras);
+        }
+    }
+
+    /**
+     * Get the remote input results bundle from an intent. The returned Bundle will
+     * contain a key/value for every result key populated by remote input collector.
+     * Use the {@link Bundle#getCharSequence(String)} method to retrieve a value.
+     * @param intent The intent object that fired in response to an action or content intent
+     *               which also had one or more remote input requested.
+     */
+    public static Bundle getResultsFromIntent(Intent intent) {
+        return IMPL.getResultsFromIntent(intent);
+    }
+
+    /**
+     * Populate an intent object with the results gathered from remote input. This method
+     * should only be called by remote input collection services when sending results to a
+     * pending intent.
+     * @param remoteInputs The remote inputs for which results are being provided
+     * @param intent The intent to add remote inputs to. The {@link android.content.ClipData}
+     *               field of the intent will be modified to contain the results.
+     * @param results A bundle holding the remote input results. This bundle should
+     *                be populated with keys matching the result keys specified in
+     *                {@code remoteInputs} with values being the result per key.
+     */
+    public static void addResultsToIntent(RemoteInput[] remoteInputs, Intent intent,
+            Bundle results) {
+        IMPL.addResultsToIntent(remoteInputs, intent, results);
+    }
+
+    private static final Impl IMPL;
+
+    interface Impl {
+        Bundle getResultsFromIntent(Intent intent);
+        void addResultsToIntent(RemoteInput[] remoteInputs, Intent intent,
+                Bundle results);
+    }
+
+    static class ImplBase implements Impl {
+        @Override
+        public Bundle getResultsFromIntent(Intent intent) {
+            Log.w(TAG, "RemoteInput is only supported from API Level 16");
+            return null;
+        }
+
+        @Override
+        public void addResultsToIntent(RemoteInput[] remoteInputs, Intent intent, Bundle results) {
+            Log.w(TAG, "RemoteInput is only supported from API Level 16");
+        }
+    }
+
+    static class ImplJellybean implements Impl {
+        @Override
+        public Bundle getResultsFromIntent(Intent intent) {
+            return RemoteInputCompatJellybean.getResultsFromIntent(intent);
+        }
+
+        @Override
+        public void addResultsToIntent(RemoteInput[] remoteInputs, Intent intent, Bundle results) {
+            RemoteInputCompatJellybean.addResultsToIntent(remoteInputs, intent, results);
+        }
+    }
+
+    static class ImplApi20 implements Impl {
+        @Override
+        public Bundle getResultsFromIntent(Intent intent) {
+            return RemoteInputCompatApi20.getResultsFromIntent(intent);
+        }
+
+        @Override
+        public void addResultsToIntent(RemoteInput[] remoteInputs, Intent intent, Bundle results) {
+            RemoteInputCompatApi20.addResultsToIntent(remoteInputs, intent, results);
+        }
+    }
+
+    static {
+        if (Build.VERSION.SDK_INT >= 20) {
+            IMPL = new ImplApi20();
+        } else if (Build.VERSION.SDK_INT >= 16) {
+            IMPL = new ImplJellybean();
+        } else {
+            IMPL = new ImplBase();
+        }
+    }
+
+    /** @hide */
+    public static final Factory FACTORY = new Factory() {
+        @Override
+        public RemoteInput build(String resultKey,
+                CharSequence label, CharSequence[] choices, boolean allowFreeFormInput,
+                Bundle extras) {
+            return new RemoteInput(resultKey, label, choices, allowFreeFormInput, extras);
+        }
+
+        @Override
+        public RemoteInput[] newArray(int size) {
+            return new RemoteInput[size];
+        }
+    };
+}
diff --git a/v4/jellybean/android/support/v4/app/BundleUtil.java b/v4/jellybean/android/support/v4/app/BundleUtil.java
new file mode 100644
index 0000000..fb6350d
--- /dev/null
+++ b/v4/jellybean/android/support/v4/app/BundleUtil.java
@@ -0,0 +1,27 @@
+package android.support.v4.app;
+
+import android.os.Bundle;
+import android.os.Parcelable;
+
+import java.util.Arrays;
+
+/**
+ * @hide
+ */
+class BundleUtil {
+    /**
+     * Get an array of Bundle objects from a parcelable array field in a bundle.
+     * Update the bundle to have a typed array so fetches in the future don't need
+     * to do an array copy.
+     */
+    public static Bundle[] getBundleArrayFromBundle(Bundle bundle, String key) {
+        Parcelable[] array = bundle.getParcelableArray(key);
+        if (array instanceof Bundle[] || array == null) {
+            return (Bundle[]) array;
+        }
+        Bundle[] typedArray = Arrays.copyOf(array, array.length,
+                Bundle[].class);
+        bundle.putParcelableArray(key, typedArray);
+        return typedArray;
+    }
+}
diff --git a/v4/jellybean/android/support/v4/app/NotificationBuilderWithActions.java b/v4/jellybean/android/support/v4/app/NotificationBuilderWithActions.java
index 656dc43..8e8d8ce 100644
--- a/v4/jellybean/android/support/v4/app/NotificationBuilderWithActions.java
+++ b/v4/jellybean/android/support/v4/app/NotificationBuilderWithActions.java
@@ -16,12 +16,9 @@
 
 package android.support.v4.app;
 
-import android.app.PendingIntent;
-import android.graphics.Bitmap;
-
 /**
  * Interface implemented by notification compat builders that support adding actions.
  */
 interface NotificationBuilderWithActions {
-    public void addAction(int icon, CharSequence title, PendingIntent intent);
+    public void addAction(NotificationCompatBase.Action action);
 }
diff --git a/v4/jellybean/android/support/v4/app/NotificationCompatJellybean.java b/v4/jellybean/android/support/v4/app/NotificationCompatJellybean.java
index b5968a5..661a09b 100644
--- a/v4/jellybean/android/support/v4/app/NotificationCompatJellybean.java
+++ b/v4/jellybean/android/support/v4/app/NotificationCompatJellybean.java
@@ -22,26 +22,42 @@
 import android.graphics.Bitmap;
 import android.os.Bundle;
 import android.util.Log;
+import android.util.SparseArray;
 import android.widget.RemoteViews;
 
 import java.lang.reflect.Field;
 import java.util.ArrayList;
+import java.util.List;
 
 class NotificationCompatJellybean {
     public static final String TAG = "NotificationCompat";
 
-    /** Extras key used for Jellybean SDK and below. */
+    // Extras keys used for Jellybean SDK and above.
     static final String EXTRA_LOCAL_ONLY = "android.support.localOnly";
+    static final String EXTRA_ACTION_EXTRAS = "android.support.actionExtras";
+    static final String EXTRA_REMOTE_INPUTS = "android.support.remoteInputs";
+    static final String EXTRA_GROUP_KEY = "android.support.groupKey";
+    static final String EXTRA_GROUP_SUMMARY = "android.support.isGroupSummary";
+    static final String EXTRA_SORT_KEY = "android.support.sortKey";
+    static final String EXTRA_USE_SIDE_CHANNEL = "android.support.useSideChannel";
 
     private static final Object sExtrasLock = new Object();
     private static Field sExtrasField;
     private static boolean sExtrasFieldAccessFailed;
 
+    private static final Object sActionsLock = new Object();
+    private static Class<?> sActionClass;
+    private static Field sActionsField;
+    private static Field sActionIconField;
+    private static Field sActionTitleField;
+    private static Field sActionIntentField;
+    private static boolean sActionsAccessFailed;
+
     public static class Builder implements NotificationBuilderWithBuilderAccessor,
             NotificationBuilderWithActions {
         private Notification.Builder b;
-        private final boolean mLocalOnly;
         private final Bundle mExtras;
+        private List<Bundle> mActionExtrasList = new ArrayList<Bundle>();
 
         public Builder(Context context, Notification n,
                 CharSequence contentTitle, CharSequence contentText, CharSequence contentInfo,
@@ -49,7 +65,7 @@
                 PendingIntent contentIntent, PendingIntent fullScreenIntent, Bitmap largeIcon,
                 int mProgressMax, int mProgress, boolean mProgressIndeterminate,
                 boolean useChronometer, int priority, CharSequence subText, boolean localOnly,
-                Bundle extras) {
+                Bundle extras, String groupKey, boolean groupSummary, String sortKey) {
             b = new Notification.Builder(context)
                 .setWhen(n.when)
                 .setSmallIcon(n.icon, n.iconLevel)
@@ -75,13 +91,29 @@
                 .setUsesChronometer(useChronometer)
                 .setPriority(priority)
                 .setProgress(mProgressMax, mProgress, mProgressIndeterminate);
-            mLocalOnly = localOnly;
-            mExtras = extras;
+            mExtras = new Bundle();
+            if (extras != null) {
+                mExtras.putAll(extras);
+            }
+            if (localOnly) {
+                mExtras.putBoolean(EXTRA_LOCAL_ONLY, true);
+            }
+            if (groupKey != null) {
+                mExtras.putString(EXTRA_GROUP_KEY, groupKey);
+                if (groupSummary) {
+                    mExtras.putBoolean(EXTRA_GROUP_SUMMARY, true);
+                } else {
+                    mExtras.putBoolean(EXTRA_USE_SIDE_CHANNEL, true);
+                }
+            }
+            if (sortKey != null) {
+                mExtras.putString(EXTRA_SORT_KEY, sortKey);
+            }
         }
 
         @Override
-        public void addAction(int icon, CharSequence title, PendingIntent intent) {
-            b.addAction(icon, title, intent);
+        public void addAction(NotificationCompatBase.Action action) {
+            mActionExtrasList.add(writeActionAndGetExtras(b, action));
         }
 
         @Override
@@ -91,20 +123,20 @@
 
         public Notification build() {
             Notification notif = b.build();
-            if (mExtras != null) {
-                // Merge in developer provided extras, but let the values already set
-                // for keys take precedence.
-                Bundle extras = getExtras(notif);
-                Bundle mergeBundle = new Bundle(mExtras);
-                for (String key : mExtras.keySet()) {
-                    if (extras.containsKey(key)) {
-                        mergeBundle.remove(key);
-                    }
+            // Merge in developer provided extras, but let the values already set
+            // for keys take precedence.
+            Bundle extras = getExtras(notif);
+            Bundle mergeBundle = new Bundle(mExtras);
+            for (String key : mExtras.keySet()) {
+                if (extras.containsKey(key)) {
+                    mergeBundle.remove(key);
                 }
-                extras.putAll(mergeBundle);
             }
-            if (mLocalOnly) {
-                getExtras(notif).putBoolean(EXTRA_LOCAL_ONLY, mLocalOnly);
+            extras.putAll(mergeBundle);
+            SparseArray<Bundle> actionExtrasMap = buildActionExtrasMap(mActionExtrasList);
+            if (actionExtrasMap != null) {
+                // Add the action extras sparse array if any action was added with extras.
+                getExtras(notif).putSparseParcelableArray(EXTRA_ACTION_EXTRAS, actionExtrasMap);
             }
             return notif;
         }
@@ -149,6 +181,21 @@
         }
     }
 
+    /** Return an SparseArray for action extras or null if none was needed. */
+    public static SparseArray<Bundle> buildActionExtrasMap(List<Bundle> actionExtrasList) {
+        SparseArray<Bundle> actionExtrasMap = null;
+        for (int i = 0, count = actionExtrasList.size(); i < count; i++) {
+            Bundle actionExtras = actionExtrasList.get(i);
+            if (actionExtras != null) {
+                if (actionExtrasMap == null) {
+                    actionExtrasMap = new SparseArray<Bundle>();
+                }
+                actionExtrasMap.put(i, actionExtras);
+            }
+        }
+        return actionExtrasMap;
+    }
+
     /**
      * Get the extras Bundle from a notification using reflection. Extras were present in
      * Jellybean notifications, but the field was private until KitKat.
@@ -185,7 +232,114 @@
         }
     }
 
+    public static NotificationCompatBase.Action readAction(NotificationCompatBase.Action.Factory factory,
+            RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory, int icon, CharSequence title,
+            PendingIntent actionIntent, Bundle extras) {
+        RemoteInputCompatBase.RemoteInput[] remoteInputs = null;
+        if (extras != null) {
+            remoteInputs = RemoteInputCompatJellybean.fromBundleArray(
+                    BundleUtil.getBundleArrayFromBundle(extras, EXTRA_REMOTE_INPUTS),
+                    remoteInputFactory);
+        }
+        return factory.build(icon, title, actionIntent, extras, remoteInputs);
+    }
+
+    public static Bundle writeActionAndGetExtras(
+            Notification.Builder builder, NotificationCompatBase.Action action) {
+        builder.addAction(action.getIcon(), action.getTitle(), action.getActionIntent());
+        Bundle actionExtras = new Bundle(action.getExtras());
+        if (action.getRemoteInputs() != null) {
+            actionExtras.putParcelableArray(EXTRA_REMOTE_INPUTS,
+                    RemoteInputCompatJellybean.toBundleArray(action.getRemoteInputs()));
+        }
+        return actionExtras;
+    }
+
+    public static int getActionCount(Notification notif) {
+        synchronized (sActionsLock) {
+            Object[] actionObjects = getActionObjectsLocked(notif);
+            return actionObjects != null ? actionObjects.length : 0;
+        }
+    }
+
+    public static NotificationCompatBase.Action getAction(Notification notif, int actionIndex,
+            NotificationCompatBase.Action.Factory factory, RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) {
+        synchronized (sActionsLock) {
+            try {
+                Object actionObject = getActionObjectsLocked(notif)[actionIndex];
+                Bundle actionExtras = null;
+                Bundle extras = getExtras(notif);
+                if (extras != null) {
+                    SparseArray<Bundle> actionExtrasMap = extras.getSparseParcelableArray(
+                            EXTRA_ACTION_EXTRAS);
+                    if (actionExtrasMap != null) {
+                        actionExtras = actionExtrasMap.get(actionIndex);
+                    }
+                }
+                return readAction(factory, remoteInputFactory,
+                        sActionIconField.getInt(actionObject),
+                        (CharSequence) sActionTitleField.get(actionObject),
+                        (PendingIntent) sActionIntentField.get(actionObject),
+                        actionExtras);
+            } catch (IllegalAccessException e) {
+                Log.e(TAG, "Unable to access notification actions", e);
+                sActionsAccessFailed = true;
+            }
+        }
+        return null;
+    }
+
+    private static Object[] getActionObjectsLocked(Notification notif) {
+        synchronized (sActionsLock) {
+            if (!ensureActionReflectionReadyLocked()) {
+                return null;
+            }
+            try {
+                return (Object[]) sActionsField.get(notif);
+            } catch (IllegalAccessException e) {
+                Log.e(TAG, "Unable to access notification actions", e);
+                sActionsAccessFailed = true;
+                return null;
+            }
+        }
+    }
+
+    private static boolean ensureActionReflectionReadyLocked() {
+        if (sActionsAccessFailed) {
+            return false;
+        }
+        try {
+            if (sActionsField == null) {
+                sActionClass = Class.forName("android.app.Notification$Action");
+                sActionIconField = sActionClass.getDeclaredField("icon");
+                sActionTitleField = sActionClass.getDeclaredField("title");
+                sActionIntentField = sActionClass.getDeclaredField("actionIntent");
+                sActionsField = Notification.class.getDeclaredField("actions");
+                sActionsField.setAccessible(true);
+            }
+        } catch (ClassNotFoundException e) {
+            Log.e(TAG, "Unable to access notification actions", e);
+            sActionsAccessFailed = true;
+        } catch (NoSuchFieldException e) {
+            Log.e(TAG, "Unable to access notification actions", e);
+            sActionsAccessFailed = true;
+        }
+        return !sActionsAccessFailed;
+    }
+
     public static boolean getLocalOnly(Notification notif) {
         return getExtras(notif).getBoolean(EXTRA_LOCAL_ONLY);
     }
+
+    public static String getGroup(Notification n) {
+        return getExtras(n).getString(EXTRA_GROUP_KEY);
+    }
+
+    public static boolean isGroupSummary(Notification n) {
+        return getExtras(n).getBoolean(EXTRA_GROUP_SUMMARY);
+    }
+
+    public static String getSortKey(Notification n) {
+        return getExtras(n).getString(EXTRA_SORT_KEY);
+    }
 }
diff --git a/v4/jellybean/android/support/v4/app/RemoteInputCompatJellybean.java b/v4/jellybean/android/support/v4/app/RemoteInputCompatJellybean.java
new file mode 100644
index 0000000..5002769
--- /dev/null
+++ b/v4/jellybean/android/support/v4/app/RemoteInputCompatJellybean.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package android.support.v4.app;
+
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.Intent;
+import android.os.Bundle;
+
+class RemoteInputCompatJellybean {
+    /** Label used to denote the clip data type used for remote input transport */
+    public static final String RESULTS_CLIP_LABEL = "android.remoteinput.results";
+
+    /** Extra added to a clip data intent object to hold the results bundle. */
+    public static final String EXTRA_RESULTS_DATA = "android.remoteinput.resultsData";
+
+    private static final String KEY_RESULT_KEY = "resultKey";
+    private static final String KEY_LABEL = "label";
+    private static final String KEY_CHOICES = "choices";
+    private static final String KEY_ALLOW_FREE_FORM_INPUT = "allowFreeFormInput";
+    private static final String KEY_EXTRAS = "extras";
+
+    static RemoteInputCompatBase.RemoteInput fromBundle(Bundle data, RemoteInputCompatBase.RemoteInput.Factory factory) {
+        return factory.build(data.getString(KEY_RESULT_KEY),
+                data.getCharSequence(KEY_LABEL),
+                data.getCharSequenceArray(KEY_CHOICES),
+                data.getBoolean(KEY_ALLOW_FREE_FORM_INPUT),
+                data.getBundle(KEY_EXTRAS));
+    }
+
+    static Bundle toBundle(RemoteInputCompatBase.RemoteInput remoteInput) {
+        Bundle data = new Bundle();
+        data.putString(KEY_RESULT_KEY, remoteInput.getResultKey());
+        data.putCharSequence(KEY_LABEL, remoteInput.getLabel());
+        data.putCharSequenceArray(KEY_CHOICES, remoteInput.getChoices());
+        data.putBoolean(KEY_ALLOW_FREE_FORM_INPUT, remoteInput.getAllowFreeFormInput());
+        data.putBundle(KEY_EXTRAS, remoteInput.getExtras());
+        return data;
+    }
+
+    static RemoteInputCompatBase.RemoteInput[] fromBundleArray(Bundle[] bundles, RemoteInputCompatBase.RemoteInput.Factory factory) {
+        if (bundles == null) {
+            return null;
+        }
+        RemoteInputCompatBase.RemoteInput[] remoteInputs = factory.newArray(bundles.length);
+        for (int i = 0; i < bundles.length; i++) {
+            remoteInputs[i] = fromBundle(bundles[i], factory);
+        }
+        return remoteInputs;
+    }
+
+    static Bundle[] toBundleArray(RemoteInputCompatBase.RemoteInput[] remoteInputs) {
+        if (remoteInputs == null) {
+            return null;
+        }
+        Bundle[] bundles = new Bundle[remoteInputs.length];
+        for (int i = 0; i < remoteInputs.length; i++) {
+            bundles[i] = toBundle(remoteInputs[i]);
+        }
+        return bundles;
+    }
+
+    static Bundle getResultsFromIntent(Intent intent) {
+        ClipData clipData = intent.getClipData();
+        if (clipData == null) {
+            return null;
+        }
+        ClipDescription clipDescription = clipData.getDescription();
+        if (!clipDescription.hasMimeType(ClipDescription.MIMETYPE_TEXT_INTENT)) {
+            return null;
+        }
+        if (clipDescription.getLabel().equals(RESULTS_CLIP_LABEL)) {
+            return clipData.getItemAt(0).getIntent().getExtras().getParcelable(EXTRA_RESULTS_DATA);
+        }
+        return null;
+    }
+
+    static void addResultsToIntent(RemoteInputCompatBase.RemoteInput[] remoteInputs, Intent intent,
+            Bundle results) {
+        Bundle resultsBundle = new Bundle();
+        for (RemoteInputCompatBase.RemoteInput remoteInput : remoteInputs) {
+            Object result = results.get(remoteInput.getResultKey());
+            if (result instanceof CharSequence) {
+                resultsBundle.putCharSequence(remoteInput.getResultKey(), (CharSequence) result);
+            }
+        }
+        Intent clipIntent = new Intent();
+        clipIntent.putExtra(EXTRA_RESULTS_DATA, resultsBundle);
+        intent.setClipData(ClipData.newIntent(RESULTS_CLIP_LABEL, clipIntent));
+    }
+}
diff --git a/v4/kitkat/android/support/v4/app/NotificationCompatKitKat.java b/v4/kitkat/android/support/v4/app/NotificationCompatKitKat.java
index bff8f5c..9ad8992 100644
--- a/v4/kitkat/android/support/v4/app/NotificationCompatKitKat.java
+++ b/v4/kitkat/android/support/v4/app/NotificationCompatKitKat.java
@@ -21,15 +21,18 @@
 import android.content.Context;
 import android.graphics.Bitmap;
 import android.os.Bundle;
+import android.util.SparseArray;
 import android.widget.RemoteViews;
 
 import java.util.ArrayList;
+import java.util.List;
 
 class NotificationCompatKitKat {
     public static class Builder implements NotificationBuilderWithBuilderAccessor,
             NotificationBuilderWithActions {
         private Notification.Builder b;
         private Bundle mExtras;
+        private List<Bundle> mActionExtrasList = new ArrayList<Bundle>();
 
         public Builder(Context context, Notification n,
                 CharSequence contentTitle, CharSequence contentText, CharSequence contentInfo,
@@ -37,7 +40,8 @@
                 PendingIntent contentIntent, PendingIntent fullScreenIntent, Bitmap largeIcon,
                 int mProgressMax, int mProgress, boolean mProgressIndeterminate,
                 boolean useChronometer, int priority, CharSequence subText, boolean localOnly,
-                ArrayList<String> people, Bundle extras) {
+                ArrayList<String> people, Bundle extras, String groupKey, boolean groupSummary,
+                String sortKey) {
             b = new Notification.Builder(context)
                 .setWhen(n.when)
                 .setSmallIcon(n.icon, n.iconLevel)
@@ -63,19 +67,33 @@
                 .setUsesChronometer(useChronometer)
                 .setPriority(priority)
                 .setProgress(mProgressMax, mProgress, mProgressIndeterminate);
-            mExtras = extras;
+            mExtras = new Bundle();
+            if (extras != null) {
+                mExtras.putAll(extras);
+            }
             if (people != null && !people.isEmpty()) {
-                getExtras().putStringArray(Notification.EXTRA_PEOPLE,
+                mExtras.putStringArray(Notification.EXTRA_PEOPLE,
                         people.toArray(new String[people.size()]));
             }
             if (localOnly) {
-                getExtras().putBoolean(NotificationCompatJellybean.EXTRA_LOCAL_ONLY, localOnly);
+                mExtras.putBoolean(NotificationCompatJellybean.EXTRA_LOCAL_ONLY, true);
+            }
+            if (groupKey != null) {
+                mExtras.putString(NotificationCompatJellybean.EXTRA_GROUP_KEY, groupKey);
+                if (groupSummary) {
+                    mExtras.putBoolean(NotificationCompatJellybean.EXTRA_GROUP_SUMMARY, true);
+                } else {
+                    mExtras.putBoolean(NotificationCompatJellybean.EXTRA_USE_SIDE_CHANNEL, true);
+                }
+            }
+            if (sortKey != null) {
+                mExtras.putString(NotificationCompatJellybean.EXTRA_SORT_KEY, sortKey);
             }
         }
 
         @Override
-        public void addAction(int icon, CharSequence title, PendingIntent intent) {
-            b.addAction(icon, title, intent);
+        public void addAction(NotificationCompatBase.Action action) {
+            mActionExtrasList.add(NotificationCompatJellybean.writeActionAndGetExtras(b, action));
         }
 
         @Override
@@ -84,25 +102,53 @@
         }
 
         public Notification build() {
-            if (mExtras != null) {
-                b.setExtras(mExtras);
+            SparseArray<Bundle> actionExtrasMap = NotificationCompatJellybean.buildActionExtrasMap(
+                    mActionExtrasList);
+            if (actionExtrasMap != null) {
+                // Add the action extras sparse array if any action was added with extras.
+                mExtras.putSparseParcelableArray(
+                        NotificationCompatJellybean.EXTRA_ACTION_EXTRAS, actionExtrasMap);
             }
+            b.setExtras(mExtras);
             return b.build();
         }
-
-        private Bundle getExtras() {
-            if (mExtras == null) {
-                mExtras = new Bundle();
-            }
-            return mExtras;
-        }
     }
 
     public static Bundle getExtras(Notification notif) {
         return notif.extras;
     }
 
+    public static int getActionCount(Notification notif) {
+        return notif.actions != null ? notif.actions.length : 0;
+    }
+
+    public static NotificationCompatBase.Action getAction(Notification notif,
+            int actionIndex, NotificationCompatBase.Action.Factory factory,
+            RemoteInputCompatBase.RemoteInput.Factory remoteInputFactory) {
+        Notification.Action action = notif.actions[actionIndex];
+        Bundle actionExtras = null;
+        SparseArray<Bundle> actionExtrasMap = notif.extras.getSparseParcelableArray(
+                NotificationCompatJellybean.EXTRA_ACTION_EXTRAS);
+        if (actionExtrasMap != null) {
+            actionExtras = actionExtrasMap.get(actionIndex);
+        }
+        return NotificationCompatJellybean.readAction(factory, remoteInputFactory,
+                action.icon, action.title, action.actionIntent, actionExtras);
+    }
+
     public static boolean getLocalOnly(Notification notif) {
-        return getExtras(notif).getBoolean(NotificationCompatJellybean.EXTRA_LOCAL_ONLY);
+        return notif.extras.getBoolean(NotificationCompatJellybean.EXTRA_LOCAL_ONLY);
+    }
+
+    public static String getGroup(Notification notif) {
+        return notif.extras.getString(NotificationCompatJellybean.EXTRA_GROUP_KEY);
+    }
+
+    public static boolean isGroupSummary(Notification notif) {
+        return notif.extras.getBoolean(NotificationCompatJellybean.EXTRA_GROUP_SUMMARY);
+    }
+
+    public static String getSortKey(Notification notif) {
+        return notif.extras.getString(NotificationCompatJellybean.EXTRA_SORT_KEY);
     }
 }