Loading services/core/java/com/android/server/notification/NotificationRecord.java +24 −0 Original line number Diff line number Diff line Loading @@ -200,6 +200,10 @@ public final class NotificationRecord { private boolean mIsAppImportanceLocked; private ArraySet<Uri> mGrantableUris; // Storage for phone numbers that were found to be associated with // contacts in this notification. private ArraySet<String> mPhoneNumbers; // Whether this notification record should have an update logged the next time notifications // are sorted. private boolean mPendingLogUpdate = false; Loading Loading @@ -1525,6 +1529,26 @@ public final class NotificationRecord { return mPendingLogUpdate; } /** * Merge the given set of phone numbers into the list of phone numbers that * are cached on this notification record. */ public void mergePhoneNumbers(ArraySet<String> phoneNumbers) { // if the given phone numbers are null or empty then don't do anything if (phoneNumbers == null || phoneNumbers.size() == 0) { return; } // initialize if not already if (mPhoneNumbers == null) { mPhoneNumbers = new ArraySet<>(); } mPhoneNumbers.addAll(phoneNumbers); } public ArraySet<String> getPhoneNumbers() { return mPhoneNumbers; } @VisibleForTesting static final class Light { public final int color; Loading services/core/java/com/android/server/notification/ValidateNotificationPeople.java +90 −2 Original line number Diff line number Diff line Loading @@ -68,7 +68,10 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor { private static final boolean ENABLE_PEOPLE_VALIDATOR = true; private static final String SETTING_ENABLE_PEOPLE_VALIDATOR = "validate_notification_people_enabled"; private static final String[] LOOKUP_PROJECTION = { Contacts._ID, Contacts.STARRED }; private static final String[] LOOKUP_PROJECTION = { Contacts._ID, Contacts.LOOKUP_KEY, Contacts.STARRED, Contacts.HAS_PHONE_NUMBER }; private static final String[] PHONE_LOOKUP_PROJECTION = { ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER }; private static final int MAX_PEOPLE = 10; private static final int PEOPLE_CACHE_SIZE = 200; Loading Loading @@ -409,6 +412,35 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor { return lookupResult; } @VisibleForTesting // Performs a contacts search using searchContacts, and then follows up by looking up // any phone numbers associated with the resulting contact information and merge those // into the lookup result as well. Will have no additional effect if the contact does // not have any phone numbers. LookupResult searchContactsAndLookupNumbers(Context context, Uri lookupUri) { LookupResult lookupResult = searchContacts(context, lookupUri); String phoneLookupKey = lookupResult.getPhoneLookupKey(); if (phoneLookupKey != null) { String selection = Contacts.LOOKUP_KEY + " = ?"; String[] selectionArgs = new String[] { phoneLookupKey }; try (Cursor cursor = context.getContentResolver().query( ContactsContract.CommonDataKinds.Phone.CONTENT_URI, PHONE_LOOKUP_PROJECTION, selection, selectionArgs, /* sortOrder= */ null)) { if (cursor == null) { Slog.w(TAG, "Cursor is null when querying contact phone number."); return lookupResult; } while (cursor.moveToNext()) { lookupResult.mergePhoneNumber(cursor); } } catch (Throwable t) { Slog.w(TAG, "Problem getting content resolver or querying phone numbers.", t); } } return lookupResult; } private void addWorkContacts(LookupResult lookupResult, Context context, Uri corpLookupUri) { final int workUserId = findWorkUserId(context); if (workUserId == -1) { Loading Loading @@ -454,6 +486,9 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor { private final long mExpireMillis; private float mAffinity = NONE; private boolean mHasPhone = false; private String mPhoneLookupKey = null; private ArraySet<String> mPhoneNumbers = new ArraySet<>(); public LookupResult() { mExpireMillis = System.currentTimeMillis() + CONTACT_REFRESH_MILLIS; Loading @@ -473,6 +508,15 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor { Slog.i(TAG, "invalid cursor: no _ID"); } // Lookup key for potentially looking up contact phone number later final int lookupKeyIdx = cursor.getColumnIndex(Contacts.LOOKUP_KEY); if (lookupKeyIdx >= 0) { mPhoneLookupKey = cursor.getString(lookupKeyIdx); if (DEBUG) Slog.d(TAG, "contact LOOKUP_KEY is: " + mPhoneLookupKey); } else { if (DEBUG) Slog.d(TAG, "invalid cursor: no LOOKUP_KEY"); } // Starred final int starIdx = cursor.getColumnIndex(Contacts.STARRED); if (starIdx >= 0) { Loading @@ -484,6 +528,39 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor { } else { if (DEBUG) Slog.d(TAG, "invalid cursor: no STARRED"); } // whether a phone number is present final int hasPhoneIdx = cursor.getColumnIndex(Contacts.HAS_PHONE_NUMBER); if (hasPhoneIdx >= 0) { mHasPhone = cursor.getInt(hasPhoneIdx) != 0; if (DEBUG) Slog.d(TAG, "contact HAS_PHONE_NUMBER is: " + mHasPhone); } else { if (DEBUG) Slog.d(TAG, "invalid cursor: no HAS_PHONE_NUMBER"); } } // Returns the phone lookup key that is cached in this result, or null // if the contact has no known phone info. public String getPhoneLookupKey() { if (!mHasPhone) { return null; } return mPhoneLookupKey; } // Merge phone number found in this lookup and store it in mPhoneNumbers. public void mergePhoneNumber(Cursor cursor) { final int phoneNumIdx = cursor.getColumnIndex( ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER); if (phoneNumIdx >= 0) { mPhoneNumbers.add(cursor.getString(phoneNumIdx)); } else { if (DEBUG) Slog.d(TAG, "invalid cursor: no NORMALIZED_NUMBER"); } } public ArraySet<String> getPhoneNumbers() { return mPhoneNumbers; } private boolean isExpired() { Loading @@ -509,6 +586,7 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor { // Amount of time to wait for a result from the contacts db before rechecking affinity. private static final long LOOKUP_TIME = 1000; private float mContactAffinity = NONE; private ArraySet<String> mPhoneNumbers = null; private NotificationRecord mRecord; private PeopleRankingReconsideration(Context context, String key, Loading Loading @@ -543,7 +621,9 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor { lookupResult = resolveEmailContact(mContext, uri.getSchemeSpecificPart()); } else if (handle.startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) { if (DEBUG) Slog.d(TAG, "checking lookup URI: " + handle); lookupResult = searchContacts(mContext, uri); // only look up phone number if this is a contact lookup uri and thus isn't // already directly a phone number. lookupResult = searchContactsAndLookupNumbers(mContext, uri); } else { lookupResult = new LookupResult(); // invalid person for the cache if (!"name".equals(uri.getScheme())) { Loading @@ -561,6 +641,13 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor { Slog.d(TAG, "lookup contactAffinity is " + lookupResult.getAffinity()); } mContactAffinity = Math.max(mContactAffinity, lookupResult.getAffinity()); // merge any phone numbers found in this lookup result if (lookupResult.getPhoneNumbers() != null) { if (mPhoneNumbers == null) { mPhoneNumbers = new ArraySet<>(); } mPhoneNumbers.addAll(lookupResult.getPhoneNumbers()); } } else { if (DEBUG) Slog.d(TAG, "lookupResult is null"); } Loading @@ -581,6 +668,7 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor { float affinityBound = operand.getContactAffinity(); operand.setContactAffinity(Math.max(mContactAffinity, affinityBound)); if (VERBOSE) Slog.i(TAG, "final affinity: " + operand.getContactAffinity()); operand.mergePhoneNumbers(mPhoneNumbers); } public float getContactAffinity() { Loading services/core/java/com/android/server/notification/ZenModeFiltering.java +36 −4 Original line number Diff line number Diff line Loading @@ -33,6 +33,7 @@ import android.telecom.TelecomManager; import android.telephony.PhoneNumberUtils; import android.telephony.TelephonyManager; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Slog; import com.android.internal.messages.nano.SystemMessageProto; Loading Loading @@ -126,7 +127,7 @@ public class ZenModeFiltering { } protected void recordCall(NotificationRecord record) { REPEAT_CALLERS.recordCall(mContext, extras(record)); REPEAT_CALLERS.recordCall(mContext, extras(record), record.getPhoneNumbers()); } /** Loading Loading @@ -325,6 +326,10 @@ public class ZenModeFiltering { } } protected void cleanUpCallersAfter(long timeThreshold) { REPEAT_CALLERS.cleanUpCallsAfter(timeThreshold); } private static class RepeatCallers { // We keep a separate map per uri scheme to do more generous number-matching // handling on telephone numbers specifically. For other inputs, we Loading @@ -333,7 +338,8 @@ public class ZenModeFiltering { private final ArrayMap<String, Long> mOtherCalls = new ArrayMap<>(); private int mThresholdMinutes; private synchronized void recordCall(Context context, Bundle extras) { private synchronized void recordCall(Context context, Bundle extras, ArraySet<String> phoneNumbers) { setThresholdMinutes(context); if (mThresholdMinutes <= 0 || extras == null) return; final String[] extraPeople = ValidateNotificationPeople.getExtraPeople(extras); Loading @@ -341,7 +347,7 @@ public class ZenModeFiltering { final long now = System.currentTimeMillis(); cleanUp(mTelCalls, now); cleanUp(mOtherCalls, now); recordCallers(extraPeople, now); recordCallers(extraPeople, phoneNumbers, now); } private synchronized boolean isRepeat(Context context, Bundle extras) { Loading @@ -365,6 +371,23 @@ public class ZenModeFiltering { } } // Clean up all calls that occurred after the given time. // Used only for tests, to clean up after testing. private synchronized void cleanUpCallsAfter(long timeThreshold) { for (int i = mTelCalls.size() - 1; i >= 0; i--) { final long time = mTelCalls.valueAt(i); if (time > timeThreshold) { mTelCalls.removeAt(i); } } for (int j = mOtherCalls.size() - 1; j >= 0; j--) { final long time = mOtherCalls.valueAt(j); if (time > timeThreshold) { mOtherCalls.removeAt(j); } } } private void setThresholdMinutes(Context context) { if (mThresholdMinutes <= 0) { mThresholdMinutes = context.getResources().getInteger(com.android.internal.R.integer Loading @@ -372,7 +395,8 @@ public class ZenModeFiltering { } } private synchronized void recordCallers(String[] people, long now) { private synchronized void recordCallers(String[] people, ArraySet<String> phoneNumbers, long now) { for (int i = 0; i < people.length; i++) { String person = people[i]; if (person == null) continue; Loading @@ -393,6 +417,14 @@ public class ZenModeFiltering { mOtherCalls.put(person, now); } } // record any additional numbers from the notification record if // provided; these are in the format of just a phone number string if (phoneNumbers != null) { for (String num : phoneNumbers) { mTelCalls.put(num, now); } } } private synchronized boolean checkCallers(Context context, String[] people) { Loading services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java +42 −0 Original line number Diff line number Diff line Loading @@ -66,6 +66,7 @@ import android.os.Vibrator; import android.provider.Settings; import android.service.notification.Adjustment; import android.service.notification.StatusBarNotification; import android.util.ArraySet; import android.widget.RemoteViews; import androidx.test.filters.SmallTest; Loading Loading @@ -1328,4 +1329,45 @@ public class NotificationRecordTest extends UiServiceTestCase { assertFalse(record.isConversation()); } @Test public void mergePhoneNumbers_nulls() { // make sure nothing dies if we just don't have any phone numbers StatusBarNotification sbn = getNotification(PKG_N_MR1, true /* noisy */, true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */, false /* lights */, false /* defaultLights */, null /* group */); NotificationRecord record = new NotificationRecord(mMockContext, sbn, defaultChannel); // by default, no phone numbers assertNull(record.getPhoneNumbers()); // nothing happens if we attempt to merge phone numbers but there aren't any record.mergePhoneNumbers(null); assertNull(record.getPhoneNumbers()); } @Test public void mergePhoneNumbers_addNumbers() { StatusBarNotification sbn = getNotification(PKG_N_MR1, true /* noisy */, true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */, false /* lights */, false /* defaultLights */, null /* group */); NotificationRecord record = new NotificationRecord(mMockContext, sbn, defaultChannel); // by default, no phone numbers assertNull(record.getPhoneNumbers()); // make sure it behaves properly when we merge in some real content record.mergePhoneNumbers(new ArraySet<>( new String[]{"16175551212", "16175552121"})); assertTrue(record.getPhoneNumbers().contains("16175551212")); assertTrue(record.getPhoneNumbers().contains("16175552121")); assertFalse(record.getPhoneNumbers().contains("16175553434")); // now merge in a new number, make sure old ones are still there and the new one // is also there record.mergePhoneNumbers(new ArraySet<>(new String[]{"16175553434"})); assertTrue(record.getPhoneNumbers().contains("16175551212")); assertTrue(record.getPhoneNumbers().contains("16175552121")); assertTrue(record.getPhoneNumbers().contains("16175553434")); } } services/tests/uiservicestests/src/com/android/server/notification/ValidateNotificationPeopleTest.java +120 −0 Original line number Diff line number Diff line Loading @@ -19,8 +19,13 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.contains; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; Loading @@ -29,6 +34,7 @@ import android.app.Person; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.UserManager; Loading @@ -43,6 +49,8 @@ import com.android.server.UiServiceTestCase; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import java.util.ArrayList; import java.util.Arrays; Loading Loading @@ -240,6 +248,118 @@ public class ValidateNotificationPeopleTest extends UiServiceTestCase { assertFalse(ContentProvider.uriHasUserId(queryUri.getValue())); } @Test public void testMergePhoneNumbers_noPhoneNumber() { // If merge phone number is called but the contacts lookup turned up no available // phone number (HAS_PHONE_NUMBER is false), then no query should happen. // setup of various bits required for querying final Context mockContext = mock(Context.class); final ContentResolver mockContentResolver = mock(ContentResolver.class); when(mockContext.getContentResolver()).thenReturn(mockContentResolver); final int contactId = 12345; final Uri lookupUri = Uri.withAppendedPath( ContactsContract.Contacts.CONTENT_LOOKUP_URI, String.valueOf(contactId)); // when the contact is looked up, we return a cursor that has one entry whose info is: // _ID: 1 // LOOKUP_KEY: "testlookupkey" // STARRED: 0 // HAS_PHONE_NUMBER: 0 Cursor cursor = makeMockCursor(1, "testlookupkey", 0, 0); when(mockContentResolver.query(any(), any(), any(), any(), any())).thenReturn(cursor); // call searchContacts and then mergePhoneNumbers, make sure we never actually // query the content resolver for a phone number new ValidateNotificationPeople().searchContactsAndLookupNumbers(mockContext, lookupUri); verify(mockContentResolver, never()).query( eq(ContactsContract.CommonDataKinds.Phone.CONTENT_URI), eq(new String[] { ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER }), contains(ContactsContract.Contacts.LOOKUP_KEY), any(), // selection args isNull()); // sort order } @Test public void testMergePhoneNumbers_hasNumber() { // If merge phone number is called and the contact lookup has a phone number, // make sure there's then a subsequent query for the phone number. // setup of various bits required for querying final Context mockContext = mock(Context.class); final ContentResolver mockContentResolver = mock(ContentResolver.class); when(mockContext.getContentResolver()).thenReturn(mockContentResolver); final int contactId = 12345; final Uri lookupUri = Uri.withAppendedPath( ContactsContract.Contacts.CONTENT_LOOKUP_URI, String.valueOf(contactId)); // when the contact is looked up, we return a cursor that has one entry whose info is: // _ID: 1 // LOOKUP_KEY: "testlookupkey" // STARRED: 0 // HAS_PHONE_NUMBER: 1 Cursor cursor = makeMockCursor(1, "testlookupkey", 0, 1); // make sure to add some specifics so this cursor is only returned for the // contacts database lookup. when(mockContentResolver.query(eq(lookupUri), any(), isNull(), isNull(), isNull())).thenReturn(cursor); // in the case of a phone lookup, return null cursor; that's not an error case // and we're not checking the actual storing of the phone data here. when(mockContentResolver.query(eq(ContactsContract.CommonDataKinds.Phone.CONTENT_URI), eq(new String[] { ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER }), contains(ContactsContract.Contacts.LOOKUP_KEY), any(), isNull())).thenReturn(null); // call searchContacts and then mergePhoneNumbers, and check that we query // once for the new ValidateNotificationPeople().searchContactsAndLookupNumbers(mockContext, lookupUri); verify(mockContentResolver, times(1)).query( eq(ContactsContract.CommonDataKinds.Phone.CONTENT_URI), eq(new String[] { ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER }), contains(ContactsContract.Contacts.LOOKUP_KEY), eq(new String[] { "testlookupkey" }), // selection args isNull()); // sort order } // Creates a cursor that points to one item of Contacts data with the specified // columns. private Cursor makeMockCursor(int id, String lookupKey, int starred, int hasPhone) { Cursor mockCursor = mock(Cursor.class); when(mockCursor.moveToFirst()).thenReturn(true); doAnswer(new Answer<Boolean>() { boolean mAccessed = false; @Override public Boolean answer(InvocationOnMock invocation) throws Throwable { if (!mAccessed) { mAccessed = true; return true; } return false; } }).when(mockCursor).moveToNext(); // id when(mockCursor.getColumnIndex(ContactsContract.Contacts._ID)).thenReturn(0); when(mockCursor.getInt(0)).thenReturn(id); // lookup key when(mockCursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY)).thenReturn(1); when(mockCursor.getString(1)).thenReturn(lookupKey); // starred when(mockCursor.getColumnIndex(ContactsContract.Contacts.STARRED)).thenReturn(2); when(mockCursor.getInt(2)).thenReturn(starred); // has phone number when(mockCursor.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER)).thenReturn(3); when(mockCursor.getInt(3)).thenReturn(hasPhone); return mockCursor; } private void assertStringArrayEquals(String message, String[] expected, String[] result) { String expectedString = Arrays.toString(expected); String resultString = Arrays.toString(result); Loading Loading
services/core/java/com/android/server/notification/NotificationRecord.java +24 −0 Original line number Diff line number Diff line Loading @@ -200,6 +200,10 @@ public final class NotificationRecord { private boolean mIsAppImportanceLocked; private ArraySet<Uri> mGrantableUris; // Storage for phone numbers that were found to be associated with // contacts in this notification. private ArraySet<String> mPhoneNumbers; // Whether this notification record should have an update logged the next time notifications // are sorted. private boolean mPendingLogUpdate = false; Loading Loading @@ -1525,6 +1529,26 @@ public final class NotificationRecord { return mPendingLogUpdate; } /** * Merge the given set of phone numbers into the list of phone numbers that * are cached on this notification record. */ public void mergePhoneNumbers(ArraySet<String> phoneNumbers) { // if the given phone numbers are null or empty then don't do anything if (phoneNumbers == null || phoneNumbers.size() == 0) { return; } // initialize if not already if (mPhoneNumbers == null) { mPhoneNumbers = new ArraySet<>(); } mPhoneNumbers.addAll(phoneNumbers); } public ArraySet<String> getPhoneNumbers() { return mPhoneNumbers; } @VisibleForTesting static final class Light { public final int color; Loading
services/core/java/com/android/server/notification/ValidateNotificationPeople.java +90 −2 Original line number Diff line number Diff line Loading @@ -68,7 +68,10 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor { private static final boolean ENABLE_PEOPLE_VALIDATOR = true; private static final String SETTING_ENABLE_PEOPLE_VALIDATOR = "validate_notification_people_enabled"; private static final String[] LOOKUP_PROJECTION = { Contacts._ID, Contacts.STARRED }; private static final String[] LOOKUP_PROJECTION = { Contacts._ID, Contacts.LOOKUP_KEY, Contacts.STARRED, Contacts.HAS_PHONE_NUMBER }; private static final String[] PHONE_LOOKUP_PROJECTION = { ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER }; private static final int MAX_PEOPLE = 10; private static final int PEOPLE_CACHE_SIZE = 200; Loading Loading @@ -409,6 +412,35 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor { return lookupResult; } @VisibleForTesting // Performs a contacts search using searchContacts, and then follows up by looking up // any phone numbers associated with the resulting contact information and merge those // into the lookup result as well. Will have no additional effect if the contact does // not have any phone numbers. LookupResult searchContactsAndLookupNumbers(Context context, Uri lookupUri) { LookupResult lookupResult = searchContacts(context, lookupUri); String phoneLookupKey = lookupResult.getPhoneLookupKey(); if (phoneLookupKey != null) { String selection = Contacts.LOOKUP_KEY + " = ?"; String[] selectionArgs = new String[] { phoneLookupKey }; try (Cursor cursor = context.getContentResolver().query( ContactsContract.CommonDataKinds.Phone.CONTENT_URI, PHONE_LOOKUP_PROJECTION, selection, selectionArgs, /* sortOrder= */ null)) { if (cursor == null) { Slog.w(TAG, "Cursor is null when querying contact phone number."); return lookupResult; } while (cursor.moveToNext()) { lookupResult.mergePhoneNumber(cursor); } } catch (Throwable t) { Slog.w(TAG, "Problem getting content resolver or querying phone numbers.", t); } } return lookupResult; } private void addWorkContacts(LookupResult lookupResult, Context context, Uri corpLookupUri) { final int workUserId = findWorkUserId(context); if (workUserId == -1) { Loading Loading @@ -454,6 +486,9 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor { private final long mExpireMillis; private float mAffinity = NONE; private boolean mHasPhone = false; private String mPhoneLookupKey = null; private ArraySet<String> mPhoneNumbers = new ArraySet<>(); public LookupResult() { mExpireMillis = System.currentTimeMillis() + CONTACT_REFRESH_MILLIS; Loading @@ -473,6 +508,15 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor { Slog.i(TAG, "invalid cursor: no _ID"); } // Lookup key for potentially looking up contact phone number later final int lookupKeyIdx = cursor.getColumnIndex(Contacts.LOOKUP_KEY); if (lookupKeyIdx >= 0) { mPhoneLookupKey = cursor.getString(lookupKeyIdx); if (DEBUG) Slog.d(TAG, "contact LOOKUP_KEY is: " + mPhoneLookupKey); } else { if (DEBUG) Slog.d(TAG, "invalid cursor: no LOOKUP_KEY"); } // Starred final int starIdx = cursor.getColumnIndex(Contacts.STARRED); if (starIdx >= 0) { Loading @@ -484,6 +528,39 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor { } else { if (DEBUG) Slog.d(TAG, "invalid cursor: no STARRED"); } // whether a phone number is present final int hasPhoneIdx = cursor.getColumnIndex(Contacts.HAS_PHONE_NUMBER); if (hasPhoneIdx >= 0) { mHasPhone = cursor.getInt(hasPhoneIdx) != 0; if (DEBUG) Slog.d(TAG, "contact HAS_PHONE_NUMBER is: " + mHasPhone); } else { if (DEBUG) Slog.d(TAG, "invalid cursor: no HAS_PHONE_NUMBER"); } } // Returns the phone lookup key that is cached in this result, or null // if the contact has no known phone info. public String getPhoneLookupKey() { if (!mHasPhone) { return null; } return mPhoneLookupKey; } // Merge phone number found in this lookup and store it in mPhoneNumbers. public void mergePhoneNumber(Cursor cursor) { final int phoneNumIdx = cursor.getColumnIndex( ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER); if (phoneNumIdx >= 0) { mPhoneNumbers.add(cursor.getString(phoneNumIdx)); } else { if (DEBUG) Slog.d(TAG, "invalid cursor: no NORMALIZED_NUMBER"); } } public ArraySet<String> getPhoneNumbers() { return mPhoneNumbers; } private boolean isExpired() { Loading @@ -509,6 +586,7 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor { // Amount of time to wait for a result from the contacts db before rechecking affinity. private static final long LOOKUP_TIME = 1000; private float mContactAffinity = NONE; private ArraySet<String> mPhoneNumbers = null; private NotificationRecord mRecord; private PeopleRankingReconsideration(Context context, String key, Loading Loading @@ -543,7 +621,9 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor { lookupResult = resolveEmailContact(mContext, uri.getSchemeSpecificPart()); } else if (handle.startsWith(Contacts.CONTENT_LOOKUP_URI.toString())) { if (DEBUG) Slog.d(TAG, "checking lookup URI: " + handle); lookupResult = searchContacts(mContext, uri); // only look up phone number if this is a contact lookup uri and thus isn't // already directly a phone number. lookupResult = searchContactsAndLookupNumbers(mContext, uri); } else { lookupResult = new LookupResult(); // invalid person for the cache if (!"name".equals(uri.getScheme())) { Loading @@ -561,6 +641,13 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor { Slog.d(TAG, "lookup contactAffinity is " + lookupResult.getAffinity()); } mContactAffinity = Math.max(mContactAffinity, lookupResult.getAffinity()); // merge any phone numbers found in this lookup result if (lookupResult.getPhoneNumbers() != null) { if (mPhoneNumbers == null) { mPhoneNumbers = new ArraySet<>(); } mPhoneNumbers.addAll(lookupResult.getPhoneNumbers()); } } else { if (DEBUG) Slog.d(TAG, "lookupResult is null"); } Loading @@ -581,6 +668,7 @@ public class ValidateNotificationPeople implements NotificationSignalExtractor { float affinityBound = operand.getContactAffinity(); operand.setContactAffinity(Math.max(mContactAffinity, affinityBound)); if (VERBOSE) Slog.i(TAG, "final affinity: " + operand.getContactAffinity()); operand.mergePhoneNumbers(mPhoneNumbers); } public float getContactAffinity() { Loading
services/core/java/com/android/server/notification/ZenModeFiltering.java +36 −4 Original line number Diff line number Diff line Loading @@ -33,6 +33,7 @@ import android.telecom.TelecomManager; import android.telephony.PhoneNumberUtils; import android.telephony.TelephonyManager; import android.util.ArrayMap; import android.util.ArraySet; import android.util.Slog; import com.android.internal.messages.nano.SystemMessageProto; Loading Loading @@ -126,7 +127,7 @@ public class ZenModeFiltering { } protected void recordCall(NotificationRecord record) { REPEAT_CALLERS.recordCall(mContext, extras(record)); REPEAT_CALLERS.recordCall(mContext, extras(record), record.getPhoneNumbers()); } /** Loading Loading @@ -325,6 +326,10 @@ public class ZenModeFiltering { } } protected void cleanUpCallersAfter(long timeThreshold) { REPEAT_CALLERS.cleanUpCallsAfter(timeThreshold); } private static class RepeatCallers { // We keep a separate map per uri scheme to do more generous number-matching // handling on telephone numbers specifically. For other inputs, we Loading @@ -333,7 +338,8 @@ public class ZenModeFiltering { private final ArrayMap<String, Long> mOtherCalls = new ArrayMap<>(); private int mThresholdMinutes; private synchronized void recordCall(Context context, Bundle extras) { private synchronized void recordCall(Context context, Bundle extras, ArraySet<String> phoneNumbers) { setThresholdMinutes(context); if (mThresholdMinutes <= 0 || extras == null) return; final String[] extraPeople = ValidateNotificationPeople.getExtraPeople(extras); Loading @@ -341,7 +347,7 @@ public class ZenModeFiltering { final long now = System.currentTimeMillis(); cleanUp(mTelCalls, now); cleanUp(mOtherCalls, now); recordCallers(extraPeople, now); recordCallers(extraPeople, phoneNumbers, now); } private synchronized boolean isRepeat(Context context, Bundle extras) { Loading @@ -365,6 +371,23 @@ public class ZenModeFiltering { } } // Clean up all calls that occurred after the given time. // Used only for tests, to clean up after testing. private synchronized void cleanUpCallsAfter(long timeThreshold) { for (int i = mTelCalls.size() - 1; i >= 0; i--) { final long time = mTelCalls.valueAt(i); if (time > timeThreshold) { mTelCalls.removeAt(i); } } for (int j = mOtherCalls.size() - 1; j >= 0; j--) { final long time = mOtherCalls.valueAt(j); if (time > timeThreshold) { mOtherCalls.removeAt(j); } } } private void setThresholdMinutes(Context context) { if (mThresholdMinutes <= 0) { mThresholdMinutes = context.getResources().getInteger(com.android.internal.R.integer Loading @@ -372,7 +395,8 @@ public class ZenModeFiltering { } } private synchronized void recordCallers(String[] people, long now) { private synchronized void recordCallers(String[] people, ArraySet<String> phoneNumbers, long now) { for (int i = 0; i < people.length; i++) { String person = people[i]; if (person == null) continue; Loading @@ -393,6 +417,14 @@ public class ZenModeFiltering { mOtherCalls.put(person, now); } } // record any additional numbers from the notification record if // provided; these are in the format of just a phone number string if (phoneNumbers != null) { for (String num : phoneNumbers) { mTelCalls.put(num, now); } } } private synchronized boolean checkCallers(Context context, String[] people) { Loading
services/tests/uiservicestests/src/com/android/server/notification/NotificationRecordTest.java +42 −0 Original line number Diff line number Diff line Loading @@ -66,6 +66,7 @@ import android.os.Vibrator; import android.provider.Settings; import android.service.notification.Adjustment; import android.service.notification.StatusBarNotification; import android.util.ArraySet; import android.widget.RemoteViews; import androidx.test.filters.SmallTest; Loading Loading @@ -1328,4 +1329,45 @@ public class NotificationRecordTest extends UiServiceTestCase { assertFalse(record.isConversation()); } @Test public void mergePhoneNumbers_nulls() { // make sure nothing dies if we just don't have any phone numbers StatusBarNotification sbn = getNotification(PKG_N_MR1, true /* noisy */, true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */, false /* lights */, false /* defaultLights */, null /* group */); NotificationRecord record = new NotificationRecord(mMockContext, sbn, defaultChannel); // by default, no phone numbers assertNull(record.getPhoneNumbers()); // nothing happens if we attempt to merge phone numbers but there aren't any record.mergePhoneNumbers(null); assertNull(record.getPhoneNumbers()); } @Test public void mergePhoneNumbers_addNumbers() { StatusBarNotification sbn = getNotification(PKG_N_MR1, true /* noisy */, true /* defaultSound */, false /* buzzy */, false /* defaultBuzz */, false /* lights */, false /* defaultLights */, null /* group */); NotificationRecord record = new NotificationRecord(mMockContext, sbn, defaultChannel); // by default, no phone numbers assertNull(record.getPhoneNumbers()); // make sure it behaves properly when we merge in some real content record.mergePhoneNumbers(new ArraySet<>( new String[]{"16175551212", "16175552121"})); assertTrue(record.getPhoneNumbers().contains("16175551212")); assertTrue(record.getPhoneNumbers().contains("16175552121")); assertFalse(record.getPhoneNumbers().contains("16175553434")); // now merge in a new number, make sure old ones are still there and the new one // is also there record.mergePhoneNumbers(new ArraySet<>(new String[]{"16175553434"})); assertTrue(record.getPhoneNumbers().contains("16175551212")); assertTrue(record.getPhoneNumbers().contains("16175552121")); assertTrue(record.getPhoneNumbers().contains("16175553434")); } }
services/tests/uiservicestests/src/com/android/server/notification/ValidateNotificationPeopleTest.java +120 −0 Original line number Diff line number Diff line Loading @@ -19,8 +19,13 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.contains; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; Loading @@ -29,6 +34,7 @@ import android.app.Person; import android.content.ContentProvider; import android.content.ContentResolver; import android.content.Context; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.os.UserManager; Loading @@ -43,6 +49,8 @@ import com.android.server.UiServiceTestCase; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.invocation.InvocationOnMock; import org.mockito.stubbing.Answer; import java.util.ArrayList; import java.util.Arrays; Loading Loading @@ -240,6 +248,118 @@ public class ValidateNotificationPeopleTest extends UiServiceTestCase { assertFalse(ContentProvider.uriHasUserId(queryUri.getValue())); } @Test public void testMergePhoneNumbers_noPhoneNumber() { // If merge phone number is called but the contacts lookup turned up no available // phone number (HAS_PHONE_NUMBER is false), then no query should happen. // setup of various bits required for querying final Context mockContext = mock(Context.class); final ContentResolver mockContentResolver = mock(ContentResolver.class); when(mockContext.getContentResolver()).thenReturn(mockContentResolver); final int contactId = 12345; final Uri lookupUri = Uri.withAppendedPath( ContactsContract.Contacts.CONTENT_LOOKUP_URI, String.valueOf(contactId)); // when the contact is looked up, we return a cursor that has one entry whose info is: // _ID: 1 // LOOKUP_KEY: "testlookupkey" // STARRED: 0 // HAS_PHONE_NUMBER: 0 Cursor cursor = makeMockCursor(1, "testlookupkey", 0, 0); when(mockContentResolver.query(any(), any(), any(), any(), any())).thenReturn(cursor); // call searchContacts and then mergePhoneNumbers, make sure we never actually // query the content resolver for a phone number new ValidateNotificationPeople().searchContactsAndLookupNumbers(mockContext, lookupUri); verify(mockContentResolver, never()).query( eq(ContactsContract.CommonDataKinds.Phone.CONTENT_URI), eq(new String[] { ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER }), contains(ContactsContract.Contacts.LOOKUP_KEY), any(), // selection args isNull()); // sort order } @Test public void testMergePhoneNumbers_hasNumber() { // If merge phone number is called and the contact lookup has a phone number, // make sure there's then a subsequent query for the phone number. // setup of various bits required for querying final Context mockContext = mock(Context.class); final ContentResolver mockContentResolver = mock(ContentResolver.class); when(mockContext.getContentResolver()).thenReturn(mockContentResolver); final int contactId = 12345; final Uri lookupUri = Uri.withAppendedPath( ContactsContract.Contacts.CONTENT_LOOKUP_URI, String.valueOf(contactId)); // when the contact is looked up, we return a cursor that has one entry whose info is: // _ID: 1 // LOOKUP_KEY: "testlookupkey" // STARRED: 0 // HAS_PHONE_NUMBER: 1 Cursor cursor = makeMockCursor(1, "testlookupkey", 0, 1); // make sure to add some specifics so this cursor is only returned for the // contacts database lookup. when(mockContentResolver.query(eq(lookupUri), any(), isNull(), isNull(), isNull())).thenReturn(cursor); // in the case of a phone lookup, return null cursor; that's not an error case // and we're not checking the actual storing of the phone data here. when(mockContentResolver.query(eq(ContactsContract.CommonDataKinds.Phone.CONTENT_URI), eq(new String[] { ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER }), contains(ContactsContract.Contacts.LOOKUP_KEY), any(), isNull())).thenReturn(null); // call searchContacts and then mergePhoneNumbers, and check that we query // once for the new ValidateNotificationPeople().searchContactsAndLookupNumbers(mockContext, lookupUri); verify(mockContentResolver, times(1)).query( eq(ContactsContract.CommonDataKinds.Phone.CONTENT_URI), eq(new String[] { ContactsContract.CommonDataKinds.Phone.NORMALIZED_NUMBER }), contains(ContactsContract.Contacts.LOOKUP_KEY), eq(new String[] { "testlookupkey" }), // selection args isNull()); // sort order } // Creates a cursor that points to one item of Contacts data with the specified // columns. private Cursor makeMockCursor(int id, String lookupKey, int starred, int hasPhone) { Cursor mockCursor = mock(Cursor.class); when(mockCursor.moveToFirst()).thenReturn(true); doAnswer(new Answer<Boolean>() { boolean mAccessed = false; @Override public Boolean answer(InvocationOnMock invocation) throws Throwable { if (!mAccessed) { mAccessed = true; return true; } return false; } }).when(mockCursor).moveToNext(); // id when(mockCursor.getColumnIndex(ContactsContract.Contacts._ID)).thenReturn(0); when(mockCursor.getInt(0)).thenReturn(id); // lookup key when(mockCursor.getColumnIndex(ContactsContract.Contacts.LOOKUP_KEY)).thenReturn(1); when(mockCursor.getString(1)).thenReturn(lookupKey); // starred when(mockCursor.getColumnIndex(ContactsContract.Contacts.STARRED)).thenReturn(2); when(mockCursor.getInt(2)).thenReturn(starred); // has phone number when(mockCursor.getColumnIndex(ContactsContract.Contacts.HAS_PHONE_NUMBER)).thenReturn(3); when(mockCursor.getInt(3)).thenReturn(hasPhone); return mockCursor; } private void assertStringArrayEquals(String message, String[] expected, String[] result) { String expectedString = Arrays.toString(expected); String resultString = Arrays.toString(result); Loading