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

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

Merge "In repeat callers, record tel: URIs separately from other people strings."

parents 324fd197 771f6d64
Loading
Loading
Loading
Loading
+101 −32
Original line number Diff line number Diff line
@@ -24,11 +24,14 @@ import android.app.NotificationManager;
import android.content.ComponentName;
import android.content.Context;
import android.media.AudioAttributes;
import android.net.Uri;
import android.os.Bundle;
import android.os.UserHandle;
import android.provider.Settings.Global;
import android.service.notification.ZenModeConfig;
import android.telecom.TelecomManager;
import android.telephony.PhoneNumberUtils;
import android.telephony.TelephonyManager;
import android.util.ArrayMap;
import android.util.Slog;

@@ -36,6 +39,8 @@ import com.android.internal.messages.nano.SystemMessageProto;
import com.android.internal.util.NotificationMessagingUtil;

import java.io.PrintWriter;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.util.Date;

public class ZenModeFiltering {
@@ -64,13 +69,22 @@ public class ZenModeFiltering {
        pw.print(prefix); pw.print("RepeatCallers.mThresholdMinutes=");
        pw.println(REPEAT_CALLERS.mThresholdMinutes);
        synchronized (REPEAT_CALLERS) {
            if (!REPEAT_CALLERS.mCalls.isEmpty()) {
                pw.print(prefix); pw.println("RepeatCallers.mCalls=");
                for (int i = 0; i < REPEAT_CALLERS.mCalls.size(); i++) {
            if (!REPEAT_CALLERS.mTelCalls.isEmpty()) {
                pw.print(prefix); pw.println("RepeatCallers.mTelCalls=");
                for (int i = 0; i < REPEAT_CALLERS.mTelCalls.size(); i++) {
                    pw.print(prefix); pw.print("  ");
                    pw.print(REPEAT_CALLERS.mCalls.keyAt(i));
                    pw.print(REPEAT_CALLERS.mTelCalls.keyAt(i));
                    pw.print(" at ");
                    pw.println(ts(REPEAT_CALLERS.mCalls.valueAt(i)));
                    pw.println(ts(REPEAT_CALLERS.mTelCalls.valueAt(i)));
                }
            }
            if (!REPEAT_CALLERS.mOtherCalls.isEmpty()) {
                pw.print(prefix); pw.println("RepeatCallers.mOtherCalls=");
                for (int i = 0; i < REPEAT_CALLERS.mOtherCalls.size(); i++) {
                    pw.print(prefix); pw.print("  ");
                    pw.print(REPEAT_CALLERS.mOtherCalls.keyAt(i));
                    pw.print(" at ");
                    pw.println(ts(REPEAT_CALLERS.mOtherCalls.valueAt(i)));
                }
            }
        }
@@ -330,34 +344,39 @@ public class ZenModeFiltering {
    }

    private static class RepeatCallers {
        // Person : time
        private final ArrayMap<String, Long> mCalls = new ArrayMap<>();
        // We keep a separate map per uri scheme to do more generous number-matching
        // handling on telephone numbers specifically. For other inputs, we
        // simply match directly on the string.
        private final ArrayMap<String, Long> mTelCalls = new ArrayMap<>();
        private final ArrayMap<String, Long> mOtherCalls = new ArrayMap<>();
        private int mThresholdMinutes;

        private synchronized void recordCall(Context context, Bundle extras) {
            setThresholdMinutes(context);
            if (mThresholdMinutes <= 0 || extras == null) return;
            final String peopleString = peopleString(extras);
            if (peopleString == null) return;
            final String[] extraPeople = ValidateNotificationPeople.getExtraPeople(extras);
            if (extraPeople == null || extraPeople.length == 0) return;
            final long now = System.currentTimeMillis();
            cleanUp(mCalls, now);
            mCalls.put(peopleString, now);
            cleanUp(mTelCalls, now);
            cleanUp(mOtherCalls, now);
            recordCallers(extraPeople, now);
        }

        private synchronized boolean isRepeat(Context context, Bundle extras) {
            setThresholdMinutes(context);
            if (mThresholdMinutes <= 0 || extras == null) return false;
            final String peopleString = peopleString(extras);
            if (peopleString == null) return false;
            final String[] extraPeople = ValidateNotificationPeople.getExtraPeople(extras);
            if (extraPeople == null || extraPeople.length == 0) return false;
            final long now = System.currentTimeMillis();
            cleanUp(mCalls, now);
            return mCalls.containsKey(peopleString);
            cleanUp(mTelCalls, now);
            cleanUp(mOtherCalls, now);
            return checkCallers(context, extraPeople);
        }

        private synchronized void cleanUp(ArrayMap<String, Long> calls, long now) {
            final int N = calls.size();
            for (int i = N - 1; i >= 0; i--) {
                final long time = mCalls.valueAt(i);
                final long time = calls.valueAt(i);
                if (time > now || (now - time) > mThresholdMinutes * 1000 * 60) {
                    calls.removeAt(i);
                }
@@ -367,10 +386,16 @@ 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 = mCalls.size() - 1; i >= 0; i--) {
                final long time = mCalls.valueAt(i);
            for (int i = mTelCalls.size() - 1; i >= 0; i--) {
                final long time = mTelCalls.valueAt(i);
                if (time > timeThreshold) {
                    mCalls.removeAt(i);
                    mTelCalls.removeAt(i);
                }
            }
            for (int j = mOtherCalls.size() - 1; j >= 0; j--) {
                final long time = mOtherCalls.valueAt(j);
                if (time > timeThreshold) {
                    mOtherCalls.removeAt(j);
                }
            }
        }
@@ -382,21 +407,65 @@ public class ZenModeFiltering {
            }
        }

        private static String peopleString(Bundle extras) {
            final String[] extraPeople = ValidateNotificationPeople.getExtraPeople(extras);
            if (extraPeople == null || extraPeople.length == 0) return null;
            final StringBuilder sb = new StringBuilder();
            for (int i = 0; i < extraPeople.length; i++) {
                String extraPerson = extraPeople[i];
                if (extraPerson == null) continue;
                extraPerson = extraPerson.trim();
                if (extraPerson.isEmpty()) continue;
                if (sb.length() > 0) {
                    sb.append('|');
                }
                sb.append(extraPerson);
            }
            return sb.length() == 0 ? null : sb.toString();
        private synchronized void recordCallers(String[] people, long now) {
            for (int i = 0; i < people.length; i++) {
                String person = people[i];
                if (person == null) continue;
                final Uri uri = Uri.parse(person);
                if ("tel".equals(uri.getScheme())) {
                    String tel = uri.getSchemeSpecificPart();
                    // while ideally we should not need to do this, sometimes we have seen tel
                    // numbers given in a url-encoded format
                    try {
                        tel = URLDecoder.decode(tel, "UTF-8");
                    } catch (UnsupportedEncodingException e) {
                        // ignore, keep the original tel string
                        Slog.w(TAG, "unsupported encoding in tel: uri input");
                    }
                    mTelCalls.put(tel, now);
                } else {
                    // for non-tel calls, store the entire string, uri-component and all
                    mOtherCalls.put(person, now);
                }
            }
        }

        private synchronized boolean checkCallers(Context context, String[] people) {
            // get the default country code for checking telephone numbers
            final String defaultCountryCode =
                    context.getSystemService(TelephonyManager.class).getNetworkCountryIso();
            for (int i = 0; i < people.length; i++) {
                String person = people[i];
                if (person == null) continue;
                final Uri uri = Uri.parse(person);
                if ("tel".equals(uri.getScheme())) {
                    String number = uri.getSchemeSpecificPart();
                    if (mTelCalls.containsKey(number)) {
                        // check directly via map first
                        return true;
                    } else {
                        // see if a number that matches via areSameNumber exists
                        String numberToCheck = number;
                        try {
                            numberToCheck = URLDecoder.decode(number, "UTF-8");
                        } catch (UnsupportedEncodingException e) {
                            // ignore, continue to use the original string
                            Slog.w(TAG, "unsupported encoding in tel: uri input");
                        }
                        for (String prev : mTelCalls.keySet()) {
                            if (PhoneNumberUtils.areSamePhoneNumber(
                                    numberToCheck, prev, defaultCountryCode)) {
                                return true;
                            }
                        }
                    }
                } else {
                    if (mOtherCalls.containsKey(person)) {
                        return true;
                    }
                }
            }
            return false;
        }
    }

+132 −1
Original line number Diff line number Diff line
@@ -17,7 +17,6 @@
package com.android.server.notification;

import static android.app.Notification.CATEGORY_CALL;
import static android.app.Notification.CATEGORY_MESSAGE;
import static android.app.NotificationManager.IMPORTANCE_DEFAULT;
import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_ANYONE;
import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_IMPORTANT;
@@ -25,6 +24,7 @@ import static android.app.NotificationManager.Policy.CONVERSATION_SENDERS_NONE;
import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_CALLS;
import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_CONVERSATIONS;
import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_MESSAGES;
import static android.app.NotificationManager.Policy.PRIORITY_CATEGORY_REPEAT_CALLERS;
import static android.app.NotificationManager.Policy.PRIORITY_SENDERS_ANY;
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR;
import static android.provider.Settings.Global.ZEN_MODE_ALARMS;
@@ -43,8 +43,10 @@ import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationManager.Policy;
import android.media.AudioAttributes;
import android.os.Bundle;
import android.os.UserHandle;
import android.service.notification.StatusBarNotification;
import android.telephony.TelephonyManager;
import android.test.suitebuilder.annotation.SmallTest;
import android.testing.AndroidTestingRunner;
import android.testing.TestableLooper;
@@ -68,10 +70,15 @@ public class ZenModeFilteringTest extends UiServiceTestCase {
    private NotificationMessagingUtil mMessagingUtil;
    private ZenModeFiltering mZenModeFiltering;

    @Mock private TelephonyManager mTelephonyManager;

    @Before
    public void setUp() {
        MockitoAnnotations.initMocks(this);
        mZenModeFiltering = new ZenModeFiltering(mContext, mMessagingUtil);

        // for repeat callers / matchesCallFilter
        mContext.addMockSystemService(TelephonyManager.class, mTelephonyManager);
    }

    private NotificationRecord getNotificationRecord() {
@@ -95,6 +102,23 @@ public class ZenModeFilteringTest extends UiServiceTestCase {
        return r;
    }

    private Bundle makeExtrasBundleWithPeople(String[] people) {
        Bundle extras = new Bundle();
        extras.putObject(Notification.EXTRA_PEOPLE_LIST, people);
        return extras;
    }

    private NotificationRecord getNotificationRecordWithPeople(String[] people) {
        // set up notification record
        NotificationRecord r = mock(NotificationRecord.class);
        StatusBarNotification sbn = mock(StatusBarNotification.class);
        Notification notification = mock(Notification.class);
        notification.extras = makeExtrasBundleWithPeople(people);
        when(sbn.getNotification()).thenReturn(notification);
        when(r.getSbn()).thenReturn(sbn);
        return r;
    }

    @Test
    public void testIsMessage() {
        NotificationRecord r = getNotificationRecord();
@@ -309,4 +333,111 @@ public class ZenModeFilteringTest extends UiServiceTestCase {

        assertFalse(mZenModeFiltering.shouldIntercept(ZEN_MODE_IMPORTANT_INTERRUPTIONS, policy, r));
    }

    @Test
    public void testMatchesCallFilter_repeatCallers_directMatch() {
        // after calls given an email with an exact string match, make sure that
        // matchesCallFilter returns the right thing
        String[] mailSource = new String[]{"mailto:hello.world"};
        mZenModeFiltering.recordCall(getNotificationRecordWithPeople(mailSource));

        // set up policy to only allow repeat callers
        Policy policy = new Policy(
                PRIORITY_CATEGORY_REPEAT_CALLERS, 0, 0, 0, CONVERSATION_SENDERS_NONE);

        // check whether matchesCallFilter returns the right thing
        Bundle inputMatches = makeExtrasBundleWithPeople(new String[]{"mailto:hello.world"});
        Bundle inputWrong = makeExtrasBundleWithPeople(new String[]{"mailto:nope"});
        assertTrue(ZenModeFiltering.matchesCallFilter(mContext, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
                policy, UserHandle.SYSTEM,
                inputMatches, null, 0, 0));
        assertFalse(ZenModeFiltering.matchesCallFilter(mContext, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
                policy, UserHandle.SYSTEM,
                inputWrong, null, 0, 0));
    }

    @Test
    public void testMatchesCallFilter_repeatCallers_telephoneVariants() {
        // set up telephony manager behavior
        when(mTelephonyManager.getNetworkCountryIso()).thenReturn("us");

        String[] telSource = new String[]{"tel:+1-617-555-1212"};
        mZenModeFiltering.recordCall(getNotificationRecordWithPeople(telSource));

        // set up policy to only allow repeat callers
        Policy policy = new Policy(
                PRIORITY_CATEGORY_REPEAT_CALLERS, 0, 0, 0, CONVERSATION_SENDERS_NONE);

        // cases to test:
        //   - identical number
        //   - same number, different formatting
        //   - different number
        //   - garbage
        Bundle identical = makeExtrasBundleWithPeople(new String[]{"tel:+1-617-555-1212"});
        Bundle same = makeExtrasBundleWithPeople(new String[]{"tel:16175551212"});
        Bundle different = makeExtrasBundleWithPeople(new String[]{"tel:123-456-7890"});
        Bundle garbage = makeExtrasBundleWithPeople(new String[]{"asdfghjkl;"});

        assertTrue("identical numbers should match",
                ZenModeFiltering.matchesCallFilter(mContext, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
                policy, UserHandle.SYSTEM,
                identical, null, 0, 0));
        assertTrue("equivalent but non-identical numbers should match",
                ZenModeFiltering.matchesCallFilter(mContext, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
                policy, UserHandle.SYSTEM,
                same, null, 0, 0));
        assertFalse("non-equivalent numbers should not match",
                ZenModeFiltering.matchesCallFilter(mContext, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
                policy, UserHandle.SYSTEM,
                different, null, 0, 0));
        assertFalse("non-tel strings should not match",
                ZenModeFiltering.matchesCallFilter(mContext, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
                policy, UserHandle.SYSTEM,
                garbage, null, 0, 0));
    }

    @Test
    public void testMatchesCallFilter_repeatCallers_urlEncodedTels() {
        // this is not intended to be a supported case but is one that we have seen
        // sometimes in the wild, so make sure we handle url-encoded telephone numbers correctly
        // when somebody provides one.

        // set up telephony manager behavior
        when(mTelephonyManager.getNetworkCountryIso()).thenReturn("us");

        String[] telSource = new String[]{"tel:%2B16175551212"};
        mZenModeFiltering.recordCall(getNotificationRecordWithPeople(telSource));

        // set up policy to only allow repeat callers
        Policy policy = new Policy(
                PRIORITY_CATEGORY_REPEAT_CALLERS, 0, 0, 0, CONVERSATION_SENDERS_NONE);

        // test cases for various forms of the same phone number and different ones
        Bundle same1 = makeExtrasBundleWithPeople(new String[]{"tel:+1-617-555-1212"});
        Bundle same2 = makeExtrasBundleWithPeople(new String[]{"tel:%2B1-617-555-1212"});
        Bundle same3 = makeExtrasBundleWithPeople(new String[]{"tel:6175551212"});
        Bundle different1 = makeExtrasBundleWithPeople(new String[]{"tel:%2B16175553434"});
        Bundle different2 = makeExtrasBundleWithPeople(new String[]{"tel:+16175553434"});

        assertTrue("same number should match",
                ZenModeFiltering.matchesCallFilter(mContext, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
                        policy, UserHandle.SYSTEM,
                        same1, null, 0, 0));
        assertTrue("same number should match",
                ZenModeFiltering.matchesCallFilter(mContext, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
                        policy, UserHandle.SYSTEM,
                        same2, null, 0, 0));
        assertTrue("same number should match",
                ZenModeFiltering.matchesCallFilter(mContext, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
                        policy, UserHandle.SYSTEM,
                        same3, null, 0, 0));
        assertFalse("different number should not match",
                ZenModeFiltering.matchesCallFilter(mContext, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
                        policy, UserHandle.SYSTEM,
                        different1, null, 0, 0));
        assertFalse("different number should not match",
                ZenModeFiltering.matchesCallFilter(mContext, ZEN_MODE_IMPORTANT_INTERRUPTIONS,
                        policy, UserHandle.SYSTEM,
                        different2, null, 0, 0));
    }
}