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