Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit c69d39d7 authored by Yuri Lin's avatar Yuri Lin Committed by Android (Google) Code Review
Browse files

Merge "Look up phone numbers associated with a NotificationRecord's contact URI." into sc-v2-dev

parents 7cc0fa7a 4e3be680
Loading
Loading
Loading
Loading
+24 −0
Original line number Diff line number Diff line
@@ -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;
@@ -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;
+90 −2
Original line number Diff line number Diff line
@@ -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;

@@ -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) {
@@ -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;
@@ -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) {
@@ -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() {
@@ -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,
@@ -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())) {
@@ -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");
                }
@@ -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() {
+36 −4
Original line number Diff line number Diff line
@@ -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;
@@ -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());
    }

    /**
@@ -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
@@ -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);
@@ -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) {
@@ -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
@@ -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;
@@ -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) {
+42 −0
Original line number Diff line number Diff line
@@ -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;
@@ -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"));
    }
}
+120 −0
Original line number Diff line number Diff line
@@ -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;

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