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