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

Commit 4e3be680 authored by Yuri Lin's avatar Yuri Lin
Browse files

Look up phone numbers associated with a NotificationRecord's contact URI.

Then, when a call comes in, store the additional phone numbers in the NotificationRecord (if any) in the repeat callers map.

This allows us to store those phone numbers in the repeat callers data to confirm whether future calls from that contact's phone are considered "repeat" when a caller passes in the phone number. This matches on any number that belongs to the contact, and not just the one used to give in the phone call (as contact lookup seems not to distinguish).

Bug: 183924362
Bug: 203806689
Test: ValidateNotificationPeopleTest; NotificationRecordTest; ZenModeFilteringTest; manually confirmed that the phone rings audibly when repeat callers is allowed and a contact calls

Change-Id: I83cc7fc036a3962d2468557d41b546f40a3cfbcf
Merged-In: I83cc7fc036a3962d2468557d41b546f40a3cfbcf
(cherry picked from commit fa8fd0d9)
parent da1701ce
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