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>
+ * <service android:name="com.example.NotificationSideChannelService">
+ * <intent-filter>
+ * <action android:name="android.support.wearable.app.BIND_SIDE_CHANNEL" />
+ * </intent-filter>
+ * </service></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);
}
}