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
This commit is contained in:
Selim Cinek
2017-12-14 17:48:32 -08:00
parent 0be794a14c
commit e7238dd125
5 changed files with 290 additions and 20 deletions

View File

@@ -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<android.app.Notification.Person> CREATOR;
}
public static abstract class Notification.Style {
ctor public Notification.Style();
method public android.app.Notification build();

View File

@@ -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<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 @@ 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<Person> 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.
* </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 @@ 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.
* <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.

View File

@@ -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<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

View File

@@ -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<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 @@ 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) {

View File

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