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