From e7238dd12595cb6be953aed4271636df90de17fc Mon Sep 17 00:00:00 2001 From: Selim Cinek Date: Thu, 14 Dec 2017 17:48:32 -0800 Subject: [PATCH] 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 --- api/current.txt | 22 +- core/java/android/app/Notification.java | 218 +++++++++++++++++- .../NotificationListenerService.java | 21 ++ .../ValidateNotificationPeople.java | 16 +- .../ValidateNotificationPeopleTest.java | 33 ++- 5 files changed, 290 insertions(+), 20 deletions(-) diff --git a/api/current.txt b/api/current.txt index 60d314f5cc1b1..68d71dc6ec259 100644 --- a/api/current.txt +++ b/api/current.txt @@ -5207,7 +5207,8 @@ package android.app { 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 @@ package android.app { 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 @@ package android.app { 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 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 85c3be826c0de..ebfba837f32e1 100644 --- a/core/java/android/app/Notification.java +++ b/core/java/android/app/Notification.java @@ -1022,9 +1022,17 @@ public class Notification implements Parcelable /** * {@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. @@ -2819,7 +2827,7 @@ public class Notification implements Parcelable private Bundle mUserExtras = new Bundle(); private Style mStyle; private ArrayList mActions = new ArrayList(MAX_ACTION_BUTTONS); - private ArrayList mPersonList = new ArrayList(); + private ArrayList mPersonList = new ArrayList<>(); private NotificationColorUtil mColorUtil; private boolean mIsLegacy; private boolean mIsLegacyInitialized; @@ -2910,8 +2918,9 @@ public class Notification implements Parcelable 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 people = mN.extras.getParcelableArrayList(EXTRA_PEOPLE_LIST); + mPersonList.addAll(people); } if (mN.getSmallIcon() == null && mN.icon != 0) { @@ -3621,13 +3630,41 @@ public class Notification implements Parcelable * 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. *

* * @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. + * + *

+ * 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. + *

+ * + *

+ * 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. + *

+ * + * @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 @@ public class Notification implements Parcelable 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 @@ public class Notification implements Parcelable } } + /** + * 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. + *
+ * 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. + * + *

+ * 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. + *

+ * + *

+ * The person should be specified by the {@code String} representation of a + * {@link android.provider.ContactsContract.Contacts#CONTENT_LOOKUP_URI}. + *

+ * + *

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}. + *

+ * + * @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. + * + *

If no key is provided, the name serves as as the key for the purpose of + * identification.

+ * + * @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 CREATOR = new Creator() { + @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 18d4a1e638049..20cd90679195d 100644 --- a/core/java/android/service/notification/NotificationListenerService.java +++ b/core/java/android/service/notification/NotificationListenerService.java @@ -890,6 +890,8 @@ public abstract class NotificationListenerService extends Service { 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 @@ public abstract class NotificationListenerService extends Service { } } + /** + * Populates remote views for pre-P targeting apps. + */ + private void maybePopulatePeople(Notification notification) { + if (getContext().getApplicationInfo().targetSdkVersion < Build.VERSION_CODES.P) { + ArrayList 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 a30e06399614b..fd9ffb2231744 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 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor { // 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 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor { return array; } + if (arrayList.get(0) instanceof Notification.Person) { + ArrayList list = (ArrayList) 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 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor { 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 58f0ded9b9a13..a60d715c9abee 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 class ValidateNotificationPeopleTest extends UiServiceTestCase { 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 class ValidateNotificationPeopleTest extends UiServiceTestCase { 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 class ValidateNotificationPeopleTest extends UiServiceTestCase { 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 class ValidateNotificationPeopleTest extends UiServiceTestCase { 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 class ValidateNotificationPeopleTest extends UiServiceTestCase { 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 class ValidateNotificationPeopleTest extends UiServiceTestCase { 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 @@ public class ValidateNotificationPeopleTest extends UiServiceTestCase { 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 @@ public class ValidateNotificationPeopleTest extends UiServiceTestCase { 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 @@ public class ValidateNotificationPeopleTest extends UiServiceTestCase { 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 @@ public class ValidateNotificationPeopleTest extends UiServiceTestCase { 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 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);