Added People to the Notification API
In order to support people without a URI and further
changes in MessagingStyle, a new person API is
introduced that allows for a richer presentation.
In addition are we now properly supporting people
without a URI, which is useful for non-handheld
clients
Test: runtest -x tests/app/src/android/app/cts/NotificationTest.java
Bug: 63708826
Change-Id: I496c893273803a2ec4fd3a5b731a6b4d483801ea
diff --git a/api/current.txt b/api/current.txt
index 60d314f..68d71dc 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -5207,7 +5207,8 @@
field public static final java.lang.String EXTRA_MESSAGES = "android.messages";
field public static final java.lang.String EXTRA_NOTIFICATION_ID = "android.intent.extra.NOTIFICATION_ID";
field public static final java.lang.String EXTRA_NOTIFICATION_TAG = "android.intent.extra.NOTIFICATION_TAG";
- field public static final java.lang.String EXTRA_PEOPLE = "android.people";
+ field public static final deprecated java.lang.String EXTRA_PEOPLE = "android.people";
+ field public static final java.lang.String EXTRA_PEOPLE_LIST = "android.people.list";
field public static final java.lang.String EXTRA_PICTURE = "android.picture";
field public static final java.lang.String EXTRA_PROGRESS = "android.progress";
field public static final java.lang.String EXTRA_PROGRESS_INDETERMINATE = "android.progressIndeterminate";
@@ -5353,7 +5354,8 @@
method public deprecated android.app.Notification.Builder addAction(int, java.lang.CharSequence, android.app.PendingIntent);
method public android.app.Notification.Builder addAction(android.app.Notification.Action);
method public android.app.Notification.Builder addExtras(android.os.Bundle);
- method public android.app.Notification.Builder addPerson(java.lang.String);
+ method public deprecated android.app.Notification.Builder addPerson(java.lang.String);
+ method public android.app.Notification.Builder addPerson(android.app.Notification.Person);
method public android.app.Notification build();
method public android.widget.RemoteViews createBigContentView();
method public android.widget.RemoteViews createContentView();
@@ -5501,6 +5503,22 @@
method public android.app.Notification.MessagingStyle.Message setData(java.lang.String, android.net.Uri);
}
+ public static final class Notification.Person implements android.os.Parcelable {
+ ctor protected Notification.Person(android.os.Parcel);
+ ctor public Notification.Person();
+ method public int describeContents();
+ method public android.graphics.drawable.Icon getIcon();
+ method public java.lang.String getKey();
+ method public java.lang.CharSequence getName();
+ method public java.lang.String getUri();
+ method public android.app.Notification.Person setIcon(android.graphics.drawable.Icon);
+ method public android.app.Notification.Person setKey(java.lang.String);
+ method public android.app.Notification.Person setName(java.lang.CharSequence);
+ method public android.app.Notification.Person setUri(java.lang.String);
+ method public void writeToParcel(android.os.Parcel, int);
+ field public static final android.os.Parcelable.Creator<android.app.Notification.Person> CREATOR;
+ }
+
public static abstract class Notification.Style {
ctor public Notification.Style();
method public android.app.Notification build();
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index 85c3be8..ebfba83 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -1022,10 +1022,18 @@
/**
* {@link #extras} key: A String array containing the people that this notification relates to,
* each of which was supplied to {@link Builder#addPerson(String)}.
+ *
+ * @deprecated the actual objects are now in {@link #EXTRA_PEOPLE_LIST}
*/
public static final String EXTRA_PEOPLE = "android.people";
/**
+ * {@link #extras} key: An arrayList of {@link Person} objects containing the people that
+ * this notification relates to.
+ */
+ public static final String EXTRA_PEOPLE_LIST = "android.people.list";
+
+ /**
* Allow certain system-generated notifications to appear before the device is provisioned.
* Only available to notifications coming from the android package.
* @hide
@@ -2819,7 +2827,7 @@
private Bundle mUserExtras = new Bundle();
private Style mStyle;
private ArrayList<Action> mActions = new ArrayList<Action>(MAX_ACTION_BUTTONS);
- private ArrayList<String> mPersonList = new ArrayList<String>();
+ private ArrayList<Person> mPersonList = new ArrayList<>();
private NotificationColorUtil mColorUtil;
private boolean mIsLegacy;
private boolean mIsLegacyInitialized;
@@ -2910,8 +2918,9 @@
Collections.addAll(mActions, mN.actions);
}
- if (mN.extras.containsKey(EXTRA_PEOPLE)) {
- Collections.addAll(mPersonList, mN.extras.getStringArray(EXTRA_PEOPLE));
+ if (mN.extras.containsKey(EXTRA_PEOPLE_LIST)) {
+ ArrayList<Person> people = mN.extras.getParcelableArrayList(EXTRA_PEOPLE_LIST);
+ mPersonList.addAll(people);
}
if (mN.getSmallIcon() == null && mN.icon != 0) {
@@ -3621,13 +3630,41 @@
* URIs. The path part of these URIs must exist in the contacts database, in the
* appropriate column, or the reference will be discarded as invalid. Telephone schema
* URIs will be resolved by {@link android.provider.ContactsContract.PhoneLookup}.
+ * It is also possible to provide a URI with the schema {@code name:} in order to uniquely
+ * identify a person without an entry in the contacts database.
* </P>
*
* @param uri A URI for the person.
* @see Notification#EXTRA_PEOPLE
+ * @deprecated use {@link #addPerson(Person)}
*/
public Builder addPerson(String uri) {
- mPersonList.add(uri);
+ addPerson(new Person().setUri(uri));
+ return this;
+ }
+
+ /**
+ * Add a person that is relevant to this notification.
+ *
+ * <P>
+ * Depending on user preferences, this annotation may allow the notification to pass
+ * through interruption filters, if this notification is of category {@link #CATEGORY_CALL}
+ * or {@link #CATEGORY_MESSAGE}. The addition of people may also cause this notification to
+ * appear more prominently in the user interface.
+ * </P>
+ *
+ * <P>
+ * A person should usually contain a uri in order to benefit from the ranking boost.
+ * However, even if no uri is provided, it's beneficial to provide other people in the
+ * notification, such that listeners and voice only devices can announce and handle them
+ * properly.
+ * </P>
+ *
+ * @param person the person to add.
+ * @see Notification#EXTRA_PEOPLE_LIST
+ */
+ public Builder addPerson(Person person) {
+ mPersonList.add(person);
return this;
}
@@ -4968,8 +5005,7 @@
mActions.toArray(mN.actions);
}
if (!mPersonList.isEmpty()) {
- mN.extras.putStringArray(EXTRA_PEOPLE,
- mPersonList.toArray(new String[mPersonList.size()]));
+ mN.extras.putParcelableArrayList(EXTRA_PEOPLE_LIST, mPersonList);
}
if (mN.bigContentView != null || mN.contentView != null
|| mN.headsUpContentView != null) {
@@ -7102,6 +7138,176 @@
}
}
+ /**
+ * A Person associated with this Notification.
+ */
+ public static final class Person implements Parcelable {
+ @Nullable private CharSequence mName;
+ @Nullable private Icon mIcon;
+ @Nullable private String mUri;
+ @Nullable private String mKey;
+
+ protected Person(Parcel in) {
+ mName = in.readCharSequence();
+ if (in.readInt() != 0) {
+ mIcon = Icon.CREATOR.createFromParcel(in);
+ }
+ mUri = in.readString();
+ mKey = in.readString();
+ }
+
+ /**
+ * Create a new person.
+ */
+ public Person() {
+ }
+
+ /**
+ * Give this person a name.
+ *
+ * @param name the name of this person
+ */
+ public Person setName(@Nullable CharSequence name) {
+ this.mName = name;
+ return this;
+ }
+
+ /**
+ * Add an icon for this person.
+ * <br />
+ * This is currently only used for {@link MessagingStyle} notifications and should not be
+ * provided otherwise, in order to save memory. The system will prefer this icon over any
+ * images that are resolved from the URI.
+ *
+ * @param icon the icon of the person
+ */
+ public Person setIcon(@Nullable Icon icon) {
+ this.mIcon = icon;
+ return this;
+ }
+
+ /**
+ * Set a URI associated with this person.
+ *
+ * <P>
+ * Depending on user preferences, adding a URI to a Person may allow the notification to
+ * pass through interruption filters, if this notification is of
+ * category {@link #CATEGORY_CALL} or {@link #CATEGORY_MESSAGE}.
+ * The addition of people may also cause this notification to appear more prominently in
+ * the user interface.
+ * </P>
+ *
+ * <P>
+ * The person should be specified by the {@code String} representation of a
+ * {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}.
+ * </P>
+ *
+ * <P>The system will also attempt to resolve {@code mailto:} and {@code tel:} schema
+ * URIs. The path part of these URIs must exist in the contacts database, in the
+ * appropriate column, or the reference will be discarded as invalid. Telephone schema
+ * URIs will be resolved by {@link android.provider.ContactsContract.PhoneLookup}.
+ * </P>
+ *
+ * @param uri a URI for the person
+ */
+ public Person setUri(@Nullable String uri) {
+ mUri = uri;
+ return this;
+ }
+
+ /**
+ * Add a key to this person in order to uniquely identify it.
+ * This is especially useful if the name doesn't uniquely identify this person or if the
+ * display name is a short handle of the actual name.
+ *
+ * <P>If no key is provided, the name serves as as the key for the purpose of
+ * identification.</P>
+ *
+ * @param key the key that uniquely identifies this person
+ */
+ public Person setKey(@Nullable String key) {
+ mKey = key;
+ return this;
+ }
+
+
+ /**
+ * @return the uri provided for this person or {@code null} if no Uri was provided
+ */
+ @Nullable
+ public String getUri() {
+ return mUri;
+ }
+
+ /**
+ * @return the name provided for this person or {@code null} if no name was provided
+ */
+ @Nullable
+ public CharSequence getName() {
+ return mName;
+ }
+
+ /**
+ * @return the icon provided for this person or {@code null} if no icon was provided
+ */
+ @Nullable
+ public Icon getIcon() {
+ return mIcon;
+ }
+
+ /**
+ * @return the key provided for this person or {@code null} if no key was provided
+ */
+ @Nullable
+ public String getKey() {
+ return mKey;
+ }
+
+ /**
+ * @return the URI associated with this person, or "name:mName" otherwise
+ * @hide
+ */
+ public String resolveToLegacyUri() {
+ if (mUri != null) {
+ return mUri;
+ }
+ if (mName != null) {
+ return "name:" + mName;
+ }
+ return "";
+ }
+
+ @Override
+ public int describeContents() {
+ return 0;
+ }
+
+ @Override
+ public void writeToParcel(Parcel dest, @WriteFlags int flags) {
+ dest.writeCharSequence(mName);
+ if (mIcon != null) {
+ dest.writeInt(1);
+ mIcon.writeToParcel(dest, 0);
+ } else {
+ dest.writeInt(0);
+ }
+ dest.writeString(mUri);
+ dest.writeString(mKey);
+ }
+
+ public static final Creator<Person> CREATOR = new Creator<Person>() {
+ @Override
+ public Person createFromParcel(Parcel in) {
+ return new Person(in);
+ }
+
+ @Override
+ public Person[] newArray(int size) {
+ return new Person[size];
+ }
+ };
+ }
+
// When adding a new Style subclass here, don't forget to update
// Builder.getNotificationStyleClass.
diff --git a/core/java/android/service/notification/NotificationListenerService.java b/core/java/android/service/notification/NotificationListenerService.java
index 18d4a1e..20cd906 100644
--- a/core/java/android/service/notification/NotificationListenerService.java
+++ b/core/java/android/service/notification/NotificationListenerService.java
@@ -890,6 +890,8 @@
createLegacyIconExtras(notification);
// populate remote views for older clients.
maybePopulateRemoteViews(notification);
+ // populate people for older clients.
+ maybePopulatePeople(notification);
} catch (IllegalArgumentException e) {
if (corruptNotifications == null) {
corruptNotifications = new ArrayList<>(N);
@@ -1178,6 +1180,25 @@
}
}
+ /**
+ * Populates remote views for pre-P targeting apps.
+ */
+ private void maybePopulatePeople(Notification notification) {
+ if (getContext().getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.P) {
+ ArrayList<Notification.Person> people = notification.extras.getParcelableArrayList(
+ Notification.EXTRA_PEOPLE_LIST);
+ if (people != null && people.isEmpty()) {
+ int size = people.size();
+ String[] peopleArray = new String[size];
+ for (int i = 0; i < size; i++) {
+ Notification.Person person = people.get(i);
+ peopleArray[i] = person.resolveToLegacyUri();
+ }
+ notification.extras.putStringArray(Notification.EXTRA_PEOPLE, peopleArray);
+ }
+ }
+ }
+
/** @hide */
protected class NotificationListenerWrapper extends INotificationListener.Stub {
@Override
diff --git a/services/core/java/com/android/server/notification/ValidateNotificationPeople.java b/services/core/java/com/android/server/notification/ValidateNotificationPeople.java
index a30e0639..fd9ffb2 100644
--- a/services/core/java/com/android/server/notification/ValidateNotificationPeople.java
+++ b/services/core/java/com/android/server/notification/ValidateNotificationPeople.java
@@ -278,7 +278,7 @@
// VisibleForTesting
public static String[] getExtraPeople(Bundle extras) {
- Object people = extras.get(Notification.EXTRA_PEOPLE);
+ Object people = extras.get(Notification.EXTRA_PEOPLE_LIST);
if (people instanceof String[]) {
return (String[]) people;
}
@@ -305,6 +305,16 @@
return array;
}
+ if (arrayList.get(0) instanceof Notification.Person) {
+ ArrayList<Notification.Person> list = (ArrayList<Notification.Person>) arrayList;
+ final int N = list.size();
+ String[] array = new String[N];
+ for (int i = 0; i < N; i++) {
+ array[i] = list.get(i).resolveToLegacyUri();
+ }
+ return array;
+ }
+
return null;
}
@@ -459,7 +469,9 @@
lookupResult = searchContacts(mContext, uri);
} else {
lookupResult = new LookupResult(); // invalid person for the cache
- Slog.w(TAG, "unsupported URI " + handle);
+ if (!"name".equals(uri.getScheme())) {
+ Slog.w(TAG, "unsupported URI " + handle);
+ }
}
if (lookupResult != null) {
synchronized (mPeopleCache) {
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ValidateNotificationPeopleTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ValidateNotificationPeopleTest.java
index 58f0ded..a60d715 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ValidateNotificationPeopleTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ValidateNotificationPeopleTest.java
@@ -47,7 +47,7 @@
public void testSingleString() throws Exception {
String[] expected = { "foobar" };
Bundle bundle = new Bundle();
- bundle.putString(Notification.EXTRA_PEOPLE, expected[0]);
+ bundle.putString(Notification.EXTRA_PEOPLE_LIST, expected[0]);
String[] result = ValidateNotificationPeople.getExtraPeople(bundle);
assertStringArrayEquals("string should be in result[0]", expected, result);
}
@@ -56,7 +56,7 @@
public void testSingleCharArray() throws Exception {
String[] expected = { "foobar" };
Bundle bundle = new Bundle();
- bundle.putCharArray(Notification.EXTRA_PEOPLE, expected[0].toCharArray());
+ bundle.putCharArray(Notification.EXTRA_PEOPLE_LIST, expected[0].toCharArray());
String[] result = ValidateNotificationPeople.getExtraPeople(bundle);
assertStringArrayEquals("char[] should be in result[0]", expected, result);
}
@@ -65,7 +65,7 @@
public void testSingleCharSequence() throws Exception {
String[] expected = { "foobar" };
Bundle bundle = new Bundle();
- bundle.putCharSequence(Notification.EXTRA_PEOPLE, new SpannableString(expected[0]));
+ bundle.putCharSequence(Notification.EXTRA_PEOPLE_LIST, new SpannableString(expected[0]));
String[] result = ValidateNotificationPeople.getExtraPeople(bundle);
assertStringArrayEquals("charSequence should be in result[0]", expected, result);
}
@@ -74,7 +74,7 @@
public void testStringArraySingle() throws Exception {
Bundle bundle = new Bundle();
String[] expected = { "foobar" };
- bundle.putStringArray(Notification.EXTRA_PEOPLE, expected);
+ bundle.putStringArray(Notification.EXTRA_PEOPLE_LIST, expected);
String[] result = ValidateNotificationPeople.getExtraPeople(bundle);
assertStringArrayEquals("wrapped string should be in result[0]", expected, result);
}
@@ -83,7 +83,7 @@
public void testStringArrayMultiple() throws Exception {
Bundle bundle = new Bundle();
String[] expected = { "foo", "bar", "baz" };
- bundle.putStringArray(Notification.EXTRA_PEOPLE, expected);
+ bundle.putStringArray(Notification.EXTRA_PEOPLE_LIST, expected);
String[] result = ValidateNotificationPeople.getExtraPeople(bundle);
assertStringArrayEquals("testStringArrayMultiple", expected, result);
}
@@ -92,7 +92,7 @@
public void testStringArrayNulls() throws Exception {
Bundle bundle = new Bundle();
String[] expected = { "foo", null, "baz" };
- bundle.putStringArray(Notification.EXTRA_PEOPLE, expected);
+ bundle.putStringArray(Notification.EXTRA_PEOPLE_LIST, expected);
String[] result = ValidateNotificationPeople.getExtraPeople(bundle);
assertStringArrayEquals("testStringArrayNulls", expected, result);
}
@@ -105,7 +105,7 @@
for (int i = 0; i < expected.length; i++) {
charSeqArray[i] = new SpannableString(expected[i]);
}
- bundle.putCharSequenceArray(Notification.EXTRA_PEOPLE, charSeqArray);
+ bundle.putCharSequenceArray(Notification.EXTRA_PEOPLE_LIST, charSeqArray);
String[] result = ValidateNotificationPeople.getExtraPeople(bundle);
assertStringArrayEquals("testCharSequenceArrayMultiple", expected, result);
}
@@ -122,7 +122,7 @@
charSeqArray[i] = new SpannableString(expected[i]);
}
}
- bundle.putCharSequenceArray(Notification.EXTRA_PEOPLE, charSeqArray);
+ bundle.putCharSequenceArray(Notification.EXTRA_PEOPLE_LIST, charSeqArray);
String[] result = ValidateNotificationPeople.getExtraPeople(bundle);
assertStringArrayEquals("testMixedCharSequenceArrayList", expected, result);
}
@@ -135,7 +135,7 @@
for (int i = 0; i < expected.length; i++) {
stringArrayList.add(expected[i]);
}
- bundle.putStringArrayList(Notification.EXTRA_PEOPLE, stringArrayList);
+ bundle.putStringArrayList(Notification.EXTRA_PEOPLE_LIST, stringArrayList);
String[] result = ValidateNotificationPeople.getExtraPeople(bundle);
assertStringArrayEquals("testStringArrayList", expected, result);
}
@@ -149,11 +149,24 @@
for (int i = 0; i < expected.length; i++) {
stringArrayList.add(new SpannableString(expected[i]));
}
- bundle.putCharSequenceArrayList(Notification.EXTRA_PEOPLE, stringArrayList);
+ bundle.putCharSequenceArrayList(Notification.EXTRA_PEOPLE_LIST, stringArrayList);
String[] result = ValidateNotificationPeople.getExtraPeople(bundle);
assertStringArrayEquals("testCharSequenceArrayList", expected, result);
}
+ @Test
+ public void testPeopleArrayList() throws Exception {
+ Bundle bundle = new Bundle();
+ String[] expected = { "name:test" , "tel:1234" };
+ final ArrayList<Notification.Person> arrayList =
+ new ArrayList<>(expected.length);
+ arrayList.add(new Notification.Person().setName("test"));
+ arrayList.add(new Notification.Person().setUri(expected[1]));
+ bundle.putParcelableArrayList(Notification.EXTRA_PEOPLE_LIST, arrayList);
+ String[] result = ValidateNotificationPeople.getExtraPeople(bundle);
+ assertStringArrayEquals("testPeopleArrayList", expected, result);
+ }
+
private void assertStringArrayEquals(String message, String[] expected, String[] result) {
String expectedString = Arrays.toString(expected);
String resultString = Arrays.toString(result);