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);