1 /**
2  * Copyright (c) 2015, The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package com.android.server.notification;
18 
19 import static android.provider.Settings.Global.ZEN_MODE_OFF;
20 import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_ANYONE;
21 
22 import android.app.Notification;
23 import android.app.NotificationManager;
24 import android.content.ComponentName;
25 import android.content.Context;
26 import android.media.AudioAttributes;
27 import android.net.Uri;
28 import android.os.Bundle;
29 import android.os.UserHandle;
30 import android.provider.Settings.Global;
31 import android.service.notification.ZenModeConfig;
32 import android.telecom.TelecomManager;
33 import android.telephony.PhoneNumberUtils;
34 import android.telephony.TelephonyManager;
35 import android.util.ArrayMap;
36 import android.util.ArraySet;
37 import android.util.Slog;
38 
39 import com.android.internal.messages.nano.SystemMessageProto;
40 import com.android.internal.util.NotificationMessagingUtil;
41 
42 import java.io.PrintWriter;
43 import java.util.Date;
44 
45 public class ZenModeFiltering {
46     private static final String TAG = ZenModeHelper.TAG;
47     private static final boolean DEBUG = ZenModeHelper.DEBUG;
48 
49     static final RepeatCallers REPEAT_CALLERS = new RepeatCallers();
50 
51     private final Context mContext;
52 
53     private ComponentName mDefaultPhoneApp;
54     private final NotificationMessagingUtil mMessagingUtil;
55 
ZenModeFiltering(Context context)56     public ZenModeFiltering(Context context) {
57         mContext = context;
58         mMessagingUtil = new NotificationMessagingUtil(mContext, null);
59     }
60 
ZenModeFiltering(Context context, NotificationMessagingUtil messagingUtil)61     public ZenModeFiltering(Context context, NotificationMessagingUtil messagingUtil) {
62         mContext = context;
63         mMessagingUtil = messagingUtil;
64     }
65 
dump(PrintWriter pw, String prefix)66     public void dump(PrintWriter pw, String prefix) {
67         pw.print(prefix); pw.print("mDefaultPhoneApp="); pw.println(mDefaultPhoneApp);
68         pw.print(prefix); pw.print("RepeatCallers.mThresholdMinutes=");
69         pw.println(REPEAT_CALLERS.mThresholdMinutes);
70         synchronized (REPEAT_CALLERS) {
71             if (!REPEAT_CALLERS.mTelCalls.isEmpty()) {
72                 pw.print(prefix); pw.println("RepeatCallers.mTelCalls=");
73                 for (int i = 0; i < REPEAT_CALLERS.mTelCalls.size(); i++) {
74                     pw.print(prefix); pw.print("  ");
75                     pw.print(REPEAT_CALLERS.mTelCalls.keyAt(i));
76                     pw.print(" at ");
77                     pw.println(ts(REPEAT_CALLERS.mTelCalls.valueAt(i)));
78                 }
79             }
80             if (!REPEAT_CALLERS.mOtherCalls.isEmpty()) {
81                 pw.print(prefix); pw.println("RepeatCallers.mOtherCalls=");
82                 for (int i = 0; i < REPEAT_CALLERS.mOtherCalls.size(); i++) {
83                     pw.print(prefix); pw.print("  ");
84                     pw.print(REPEAT_CALLERS.mOtherCalls.keyAt(i));
85                     pw.print(" at ");
86                     pw.println(ts(REPEAT_CALLERS.mOtherCalls.valueAt(i)));
87                 }
88             }
89         }
90     }
91 
ts(long time)92     private static String ts(long time) {
93         return new Date(time) + " (" + time + ")";
94     }
95 
96     /**
97      * @param extras extras of the notification with EXTRA_PEOPLE populated
98      * @param contactsTimeoutMs timeout in milliseconds to wait for contacts response
99      * @param timeoutAffinity affinity to return when the timeout specified via
100      *                        <code>contactsTimeoutMs</code> is hit
101      */
matchesCallFilter(Context context, int zen, NotificationManager.Policy consolidatedPolicy, UserHandle userHandle, Bundle extras, ValidateNotificationPeople validator, int contactsTimeoutMs, float timeoutAffinity, int callingUid)102     public static boolean matchesCallFilter(Context context, int zen, NotificationManager.Policy
103             consolidatedPolicy, UserHandle userHandle, Bundle extras,
104             ValidateNotificationPeople validator, int contactsTimeoutMs, float timeoutAffinity,
105             int callingUid) {
106         if (zen == Global.ZEN_MODE_NO_INTERRUPTIONS) {
107             ZenLog.traceMatchesCallFilter(false, "no interruptions", callingUid);
108             return false; // nothing gets through
109         }
110         if (zen == Global.ZEN_MODE_ALARMS) {
111             ZenLog.traceMatchesCallFilter(false, "alarms only", callingUid);
112             return false; // not an alarm
113         }
114         if (zen == Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS) {
115             if (consolidatedPolicy.allowRepeatCallers()
116                     && REPEAT_CALLERS.isRepeat(context, extras, null)) {
117                 ZenLog.traceMatchesCallFilter(true, "repeat caller", callingUid);
118                 return true;
119             }
120             if (!consolidatedPolicy.allowCalls()) {
121                 ZenLog.traceMatchesCallFilter(false, "calls not allowed", callingUid);
122                 return false; // no other calls get through
123             }
124             if (validator != null) {
125                 final float contactAffinity = validator.getContactAffinity(userHandle, extras,
126                         contactsTimeoutMs, timeoutAffinity);
127                 boolean match =
128                         audienceMatches(consolidatedPolicy.allowCallsFrom(), contactAffinity);
129                 ZenLog.traceMatchesCallFilter(match, "contact affinity " + contactAffinity,
130                         callingUid);
131                 return match;
132             }
133         }
134         ZenLog.traceMatchesCallFilter(true, "no restrictions", callingUid);
135         return true;
136     }
137 
extras(NotificationRecord record)138     private static Bundle extras(NotificationRecord record) {
139         return record != null && record.getSbn() != null && record.getSbn().getNotification() != null
140                 ? record.getSbn().getNotification().extras : null;
141     }
142 
recordCall(NotificationRecord record)143     protected void recordCall(NotificationRecord record) {
144         REPEAT_CALLERS.recordCall(mContext, extras(record), record.getPhoneNumbers());
145     }
146 
147     /**
148      * Whether to intercept the notification based on the policy
149      */
shouldIntercept(int zen, NotificationManager.Policy policy, NotificationRecord record)150     public boolean shouldIntercept(int zen, NotificationManager.Policy policy,
151             NotificationRecord record) {
152         if (zen == ZEN_MODE_OFF) {
153             return false;
154         }
155 
156         if (isCritical(record)) {
157             // Zen mode is ignored for critical notifications.
158             maybeLogInterceptDecision(record, false, "criticalNotification");
159             return false;
160         }
161         // Make an exception to policy for the notification saying that policy has changed
162         if (NotificationManager.Policy.areAllVisualEffectsSuppressed(policy.suppressedVisualEffects)
163                 && "android".equals(record.getSbn().getPackageName())
164                 && SystemMessageProto.SystemMessage.NOTE_ZEN_UPGRADE == record.getSbn().getId()) {
165             maybeLogInterceptDecision(record, false, "systemDndChangedNotification");
166             return false;
167         }
168         switch (zen) {
169             case Global.ZEN_MODE_NO_INTERRUPTIONS:
170                 // #notevenalarms
171                 maybeLogInterceptDecision(record, true, "none");
172                 return true;
173             case Global.ZEN_MODE_ALARMS:
174                 if (isAlarm(record)) {
175                     // Alarms only
176                     maybeLogInterceptDecision(record, false, "alarm");
177                     return false;
178                 }
179                 maybeLogInterceptDecision(record, true, "alarmsOnly");
180                 return true;
181             case Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS:
182                 // allow user-prioritized packages through in priority mode
183                 if (record.getPackagePriority() == Notification.PRIORITY_MAX) {
184                     maybeLogInterceptDecision(record, false, "priorityApp");
185                     return false;
186                 }
187 
188                 if (isAlarm(record)) {
189                     if (!policy.allowAlarms()) {
190                         maybeLogInterceptDecision(record, true, "!allowAlarms");
191                         return true;
192                     }
193                     maybeLogInterceptDecision(record, false, "allowedAlarm");
194                     return false;
195                 }
196                 if (isEvent(record)) {
197                     if (!policy.allowEvents()) {
198                         maybeLogInterceptDecision(record, true, "!allowEvents");
199                         return true;
200                     }
201                     maybeLogInterceptDecision(record, false, "allowedEvent");
202                     return false;
203                 }
204                 if (isReminder(record)) {
205                     if (!policy.allowReminders()) {
206                         maybeLogInterceptDecision(record, true, "!allowReminders");
207                         return true;
208                     }
209                     maybeLogInterceptDecision(record, false, "allowedReminder");
210                     return false;
211                 }
212                 if (isMedia(record)) {
213                     if (!policy.allowMedia()) {
214                         maybeLogInterceptDecision(record, true, "!allowMedia");
215                         return true;
216                     }
217                     maybeLogInterceptDecision(record, false, "allowedMedia");
218                     return false;
219                 }
220                 if (isSystem(record)) {
221                     if (!policy.allowSystem()) {
222                         maybeLogInterceptDecision(record, true, "!allowSystem");
223                         return true;
224                     }
225                     maybeLogInterceptDecision(record, false, "allowedSystem");
226                     return false;
227                 }
228                 if (isConversation(record)) {
229                     if (policy.allowConversations()) {
230                         if (policy.priorityConversationSenders == CONVERSATION_SENDERS_ANYONE) {
231                             maybeLogInterceptDecision(record, false, "conversationAnyone");
232                             return false;
233                         } else if (policy.priorityConversationSenders
234                                 == NotificationManager.Policy.CONVERSATION_SENDERS_IMPORTANT
235                                 && record.getChannel().isImportantConversation()) {
236                             maybeLogInterceptDecision(record, false, "conversationMatches");
237                             return false;
238                         }
239                     }
240                     // if conversations aren't allowed record might still be allowed thanks
241                     // to call or message metadata, so don't return yet
242                 }
243                 if (isCall(record)) {
244                     if (policy.allowRepeatCallers()
245                             && REPEAT_CALLERS.isRepeat(
246                                     mContext, extras(record), record.getPhoneNumbers())) {
247                         maybeLogInterceptDecision(record, false, "repeatCaller");
248                         return false;
249                     }
250                     if (!policy.allowCalls()) {
251                         maybeLogInterceptDecision(record, true, "!allowCalls");
252                         return true;
253                     }
254                     return shouldInterceptAudience(policy.allowCallsFrom(), record);
255                 }
256                 if (isMessage(record)) {
257                     if (!policy.allowMessages()) {
258                         maybeLogInterceptDecision(record, true, "!allowMessages");
259                         return true;
260                     }
261                     return shouldInterceptAudience(policy.allowMessagesFrom(), record);
262                 }
263 
264                 maybeLogInterceptDecision(record, true, "!priority");
265                 return true;
266             default:
267                 maybeLogInterceptDecision(record, false, "unknownZenMode");
268                 return false;
269         }
270     }
271 
272     // Consider logging the decision of shouldIntercept for the given record.
273     // This will log the outcome if one of the following is true:
274     //   - it's the first time the intercept decision is set for the record
275     //   - OR it's not the first time, but the intercept decision changed
maybeLogInterceptDecision(NotificationRecord record, boolean intercept, String reason)276     private static void maybeLogInterceptDecision(NotificationRecord record, boolean intercept,
277             String reason) {
278         boolean interceptBefore = record.isIntercepted();
279         if (record.hasInterceptBeenSet() && (interceptBefore == intercept)) {
280             // this record has already been evaluated for whether it should be intercepted, and
281             // the decision has not changed.
282             return;
283         }
284 
285         // add a note to the reason indicating whether it's new or updated
286         String annotatedReason = reason;
287         if (!record.hasInterceptBeenSet()) {
288             annotatedReason = "new:" + reason;
289         } else if (interceptBefore != intercept) {
290             annotatedReason = "updated:" + reason;
291         }
292 
293         if (intercept) {
294             ZenLog.traceIntercepted(record, annotatedReason);
295         } else {
296             ZenLog.traceNotIntercepted(record, annotatedReason);
297         }
298     }
299 
300     /**
301      * Check if the notification is too critical to be suppressed.
302      *
303      * @param record the record to test for criticality
304      * @return {@code true} if notification is considered critical
305      *
306      * @see CriticalNotificationExtractor for criteria
307      */
isCritical(NotificationRecord record)308     private boolean isCritical(NotificationRecord record) {
309         // 0 is the most critical
310         return record.getCriticality() < CriticalNotificationExtractor.NORMAL;
311     }
312 
shouldInterceptAudience(int source, NotificationRecord record)313     private static boolean shouldInterceptAudience(int source, NotificationRecord record) {
314         float affinity = record.getContactAffinity();
315         if (!audienceMatches(source, affinity)) {
316             maybeLogInterceptDecision(record, true, "!audienceMatches,affinity=" + affinity);
317             return true;
318         }
319         maybeLogInterceptDecision(record, false, "affinity=" + affinity);
320         return false;
321     }
322 
isAlarm(NotificationRecord record)323     protected static boolean isAlarm(NotificationRecord record) {
324         return record.isCategory(Notification.CATEGORY_ALARM)
325                 || record.isAudioAttributesUsage(AudioAttributes.USAGE_ALARM);
326     }
327 
isEvent(NotificationRecord record)328     private static boolean isEvent(NotificationRecord record) {
329         return record.isCategory(Notification.CATEGORY_EVENT);
330     }
331 
isReminder(NotificationRecord record)332     private static boolean isReminder(NotificationRecord record) {
333         return record.isCategory(Notification.CATEGORY_REMINDER);
334     }
335 
isCall(NotificationRecord record)336     public boolean isCall(NotificationRecord record) {
337         return record != null && (isDefaultPhoneApp(record.getSbn().getPackageName())
338                 || record.isCategory(Notification.CATEGORY_CALL));
339     }
340 
isMedia(NotificationRecord record)341     public boolean isMedia(NotificationRecord record) {
342         AudioAttributes aa = record.getAudioAttributes();
343         return aa != null && AudioAttributes.SUPPRESSIBLE_USAGES.get(aa.getUsage()) ==
344                 AudioAttributes.SUPPRESSIBLE_MEDIA;
345     }
346 
isSystem(NotificationRecord record)347     public boolean isSystem(NotificationRecord record) {
348         AudioAttributes aa = record.getAudioAttributes();
349         return aa != null && AudioAttributes.SUPPRESSIBLE_USAGES.get(aa.getUsage()) ==
350                 AudioAttributes.SUPPRESSIBLE_SYSTEM;
351     }
352 
isDefaultPhoneApp(String pkg)353     private boolean isDefaultPhoneApp(String pkg) {
354         if (mDefaultPhoneApp == null) {
355             final TelecomManager telecomm =
356                     (TelecomManager) mContext.getSystemService(Context.TELECOM_SERVICE);
357             mDefaultPhoneApp = telecomm != null ? telecomm.getDefaultPhoneApp() : null;
358             if (DEBUG) Slog.d(TAG, "Default phone app: " + mDefaultPhoneApp);
359         }
360         return pkg != null && mDefaultPhoneApp != null
361                 && pkg.equals(mDefaultPhoneApp.getPackageName());
362     }
363 
isMessage(NotificationRecord record)364     protected boolean isMessage(NotificationRecord record) {
365         return mMessagingUtil.isMessaging(record.getSbn());
366     }
367 
isConversation(NotificationRecord record)368     protected boolean isConversation(NotificationRecord record) {
369         return record.isConversation();
370     }
371 
audienceMatches(int source, float contactAffinity)372     private static boolean audienceMatches(int source, float contactAffinity) {
373         switch (source) {
374             case ZenModeConfig.SOURCE_ANYONE:
375                 return true;
376             case ZenModeConfig.SOURCE_CONTACT:
377                 return contactAffinity >= ValidateNotificationPeople.VALID_CONTACT;
378             case ZenModeConfig.SOURCE_STAR:
379                 return contactAffinity >= ValidateNotificationPeople.STARRED_CONTACT;
380             default:
381                 Slog.w(TAG, "Encountered unknown source: " + source);
382                 return true;
383         }
384     }
385 
cleanUpCallersAfter(long timeThreshold)386     protected void cleanUpCallersAfter(long timeThreshold) {
387         REPEAT_CALLERS.cleanUpCallsAfter(timeThreshold);
388     }
389 
390     private static class RepeatCallers {
391         // We keep a separate map per uri scheme to do more generous number-matching
392         // handling on telephone numbers specifically. For other inputs, we
393         // simply match directly on the string.
394         private final ArrayMap<String, Long> mTelCalls = new ArrayMap<>();
395         private final ArrayMap<String, Long> mOtherCalls = new ArrayMap<>();
396         private int mThresholdMinutes;
397 
398         // Record all people URIs in the extras bundle as well as the provided phoneNumbers set
399         // as callers. The phoneNumbers set is used to pass in any additional phone numbers
400         // associated with the people URIs as separately retrieved from contacts.
recordCall(Context context, Bundle extras, ArraySet<String> phoneNumbers)401         private synchronized void recordCall(Context context, Bundle extras,
402                 ArraySet<String> phoneNumbers) {
403             setThresholdMinutes(context);
404             if (mThresholdMinutes <= 0 || extras == null) return;
405             final String[] extraPeople = ValidateNotificationPeople.getExtraPeople(extras);
406             if (extraPeople == null || extraPeople.length == 0) return;
407             final long now = System.currentTimeMillis();
408             cleanUp(mTelCalls, now);
409             cleanUp(mOtherCalls, now);
410             recordCallers(extraPeople, phoneNumbers, now);
411         }
412 
413         // Determine whether any people in the provided extras bundle or phone number set is
414         // a repeat caller. The extras bundle contains the people associated with a specific
415         // notification, and will suffice for most callers; the phoneNumbers array may be used
416         // to additionally check any specific phone numbers previously retrieved from contacts
417         // associated with the people in the extras bundle.
isRepeat(Context context, Bundle extras, ArraySet<String> phoneNumbers)418         private synchronized boolean isRepeat(Context context, Bundle extras,
419                 ArraySet<String> phoneNumbers) {
420             setThresholdMinutes(context);
421             if (mThresholdMinutes <= 0 || extras == null) return false;
422             final String[] extraPeople = ValidateNotificationPeople.getExtraPeople(extras);
423             if (extraPeople == null || extraPeople.length == 0) return false;
424             final long now = System.currentTimeMillis();
425             cleanUp(mTelCalls, now);
426             cleanUp(mOtherCalls, now);
427             return checkCallers(context, extraPeople, phoneNumbers);
428         }
429 
cleanUp(ArrayMap<String, Long> calls, long now)430         private synchronized void cleanUp(ArrayMap<String, Long> calls, long now) {
431             final int N = calls.size();
432             for (int i = N - 1; i >= 0; i--) {
433                 final long time = calls.valueAt(i);
434                 if (time > now || (now - time) > mThresholdMinutes * 1000 * 60) {
435                     calls.removeAt(i);
436                 }
437             }
438         }
439 
440         // Clean up all calls that occurred after the given time.
441         // Used only for tests, to clean up after testing.
cleanUpCallsAfter(long timeThreshold)442         private synchronized void cleanUpCallsAfter(long timeThreshold) {
443             for (int i = mTelCalls.size() - 1; i >= 0; i--) {
444                 final long time = mTelCalls.valueAt(i);
445                 if (time > timeThreshold) {
446                     mTelCalls.removeAt(i);
447                 }
448             }
449             for (int j = mOtherCalls.size() - 1; j >= 0; j--) {
450                 final long time = mOtherCalls.valueAt(j);
451                 if (time > timeThreshold) {
452                     mOtherCalls.removeAt(j);
453                 }
454             }
455         }
456 
setThresholdMinutes(Context context)457         private void setThresholdMinutes(Context context) {
458             if (mThresholdMinutes <= 0) {
459                 mThresholdMinutes = context.getResources().getInteger(com.android.internal.R.integer
460                         .config_zen_repeat_callers_threshold);
461             }
462         }
463 
recordCallers(String[] people, ArraySet<String> phoneNumbers, long now)464         private synchronized void recordCallers(String[] people, ArraySet<String> phoneNumbers,
465                 long now) {
466             boolean recorded = false, hasTel = false, hasOther = false;
467             for (int i = 0; i < people.length; i++) {
468                 String person = people[i];
469                 if (person == null) continue;
470                 final Uri uri = Uri.parse(person);
471                 if ("tel".equals(uri.getScheme())) {
472                     // while ideally we should not need to decode this, sometimes we have seen tel
473                     // numbers given in an encoded format
474                     String tel = Uri.decode(uri.getSchemeSpecificPart());
475                     if (tel != null) {
476                         mTelCalls.put(tel, now);
477                         recorded = true;
478                         hasTel = true;
479                     }
480                 } else {
481                     // for non-tel calls, store the entire string, uri-component and all
482                     mOtherCalls.put(person, now);
483                     recorded = true;
484                     hasOther = true;
485                 }
486             }
487 
488             // record any additional numbers from the notification record if
489             // provided; these are in the format of just a phone number string
490             if (phoneNumbers != null) {
491                 for (String num : phoneNumbers) {
492                     if (num != null) {
493                         mTelCalls.put(num, now);
494                         recorded = true;
495                         hasTel = true;
496                     }
497                 }
498             }
499             if (recorded) {
500                 ZenLog.traceRecordCaller(hasTel, hasOther);
501             }
502         }
503 
504         // helper function to check mTelCalls array for a number, and also check its decoded
505         // version
checkForNumber(String number, String defaultCountryCode)506         private synchronized boolean checkForNumber(String number, String defaultCountryCode) {
507             if (mTelCalls.containsKey(number)) {
508                 // check directly via map first
509                 return true;
510             } else {
511                 // see if a number that matches via areSameNumber exists
512                 String numberToCheck = Uri.decode(number);
513                 if (numberToCheck != null) {
514                     for (String prev : mTelCalls.keySet()) {
515                         if (PhoneNumberUtils.areSamePhoneNumber(
516                                 numberToCheck, prev, defaultCountryCode)) {
517                             return true;
518                         }
519                     }
520                 }
521             }
522             return false;
523         }
524 
525         // Check whether anyone in the provided array of people URIs or phone number set matches a
526         // previously recorded phone call.
checkCallers(Context context, String[] people, ArraySet<String> phoneNumbers)527         private synchronized boolean checkCallers(Context context, String[] people,
528                 ArraySet<String> phoneNumbers) {
529             boolean found = false, checkedTel = false, checkedOther = false;
530 
531             // get the default country code for checking telephone numbers
532             final String defaultCountryCode =
533                     context.getSystemService(TelephonyManager.class).getNetworkCountryIso();
534             for (int i = 0; i < people.length; i++) {
535                 String person = people[i];
536                 if (person == null) continue;
537                 final Uri uri = Uri.parse(person);
538                 if ("tel".equals(uri.getScheme())) {
539                     String number = uri.getSchemeSpecificPart();
540                     checkedTel = true;
541                     if (checkForNumber(number, defaultCountryCode)) {
542                         found = true;
543                     }
544                 } else {
545                     checkedOther = true;
546                     if (mOtherCalls.containsKey(person)) {
547                         found = true;
548                     }
549                 }
550             }
551 
552             // also check any passed-in phone numbers
553             if (phoneNumbers != null) {
554                 for (String num : phoneNumbers) {
555                     checkedTel = true;
556                     if (checkForNumber(num, defaultCountryCode)) {
557                         found = true;
558                     }
559                 }
560             }
561 
562             // no matches
563             ZenLog.traceCheckRepeatCaller(found, checkedTel, checkedOther);
564             return found;
565         }
566     }
567 
568 }
569