1 /* 2 * Copyright (C) 2019 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.systemui.statusbar.notification.collection; 18 19 import static android.app.Notification.CATEGORY_ALARM; 20 import static android.app.Notification.CATEGORY_CALL; 21 import static android.app.Notification.CATEGORY_EVENT; 22 import static android.app.Notification.CATEGORY_MESSAGE; 23 import static android.app.Notification.CATEGORY_REMINDER; 24 import static android.app.Notification.FLAG_BUBBLE; 25 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT; 26 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_BADGE; 27 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_FULL_SCREEN_INTENT; 28 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST; 29 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; 30 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR; 31 32 import android.annotation.NonNull; 33 import android.app.Notification; 34 import android.app.NotificationChannel; 35 import android.app.NotificationManager.Policy; 36 import android.app.Person; 37 import android.content.Context; 38 import android.graphics.drawable.Icon; 39 import android.os.Bundle; 40 import android.os.Parcelable; 41 import android.os.SystemClock; 42 import android.service.notification.NotificationListenerService; 43 import android.service.notification.SnoozeCriterion; 44 import android.service.notification.StatusBarNotification; 45 import android.util.ArraySet; 46 import android.view.View; 47 import android.widget.ImageView; 48 49 import androidx.annotation.Nullable; 50 51 import com.android.internal.annotations.VisibleForTesting; 52 import com.android.internal.statusbar.StatusBarIcon; 53 import com.android.internal.util.ArrayUtils; 54 import com.android.internal.util.ContrastColorUtil; 55 import com.android.systemui.statusbar.InflationTask; 56 import com.android.systemui.statusbar.StatusBarIconView; 57 import com.android.systemui.statusbar.notification.InflationException; 58 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 59 import com.android.systemui.statusbar.notification.row.NotificationContentInflater.InflationFlag; 60 import com.android.systemui.statusbar.notification.row.NotificationGuts; 61 62 import java.util.ArrayList; 63 import java.util.Collections; 64 import java.util.List; 65 import java.util.Objects; 66 67 /** 68 * Represents a notification that the system UI knows about 69 * 70 * Whenever the NotificationManager tells us about the existence of a new notification, we wrap it 71 * in a NotificationEntry. Thus, every notification has an associated NotificationEntry, even if 72 * that notification is never displayed to the user (for example, if it's filtered out for some 73 * reason). 74 * 75 * Entries store information about the current state of the notification. Essentially: 76 * anything that needs to persist or be modifiable even when the notification's views don't 77 * exist. Any other state should be stored on the views/view controllers themselves. 78 * 79 * At the moment, there are many things here that shouldn't be and vice-versa. Hopefully we can 80 * clean this up in the future. 81 */ 82 public final class NotificationEntry { 83 private static final long LAUNCH_COOLDOWN = 2000; 84 private static final long REMOTE_INPUT_COOLDOWN = 500; 85 private static final long INITIALIZATION_DELAY = 400; 86 private static final long NOT_LAUNCHED_YET = -LAUNCH_COOLDOWN; 87 private static final int COLOR_INVALID = 1; 88 public final String key; 89 public StatusBarNotification notification; 90 public NotificationChannel channel; 91 public long lastAudiblyAlertedMs; 92 public boolean noisy; 93 public boolean ambient; 94 public int importance; 95 public StatusBarIconView icon; 96 public StatusBarIconView expandedIcon; 97 public StatusBarIconView centeredIcon; 98 public StatusBarIconView aodIcon; 99 private boolean interruption; 100 public boolean autoRedacted; // whether the redacted notification was generated by us 101 public int targetSdk; 102 private long lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET; 103 public CharSequence remoteInputText; 104 public List<SnoozeCriterion> snoozeCriteria; 105 public int userSentiment = NotificationListenerService.Ranking.USER_SENTIMENT_NEUTRAL; 106 /** Smart Actions provided by the NotificationAssistantService. */ 107 @NonNull 108 public List<Notification.Action> systemGeneratedSmartActions = Collections.emptyList(); 109 /** Smart replies provided by the NotificationAssistantService. */ 110 @NonNull 111 public CharSequence[] systemGeneratedSmartReplies = new CharSequence[0]; 112 113 /** 114 * If {@link android.app.RemoteInput#getEditChoicesBeforeSending} is enabled, and the user is 115 * currently editing a choice (smart reply), then this field contains the information about the 116 * suggestion being edited. Otherwise <code>null</code>. 117 */ 118 public EditedSuggestionInfo editedSuggestionInfo; 119 120 @VisibleForTesting 121 public int suppressedVisualEffects; 122 public boolean suspended; 123 124 private NotificationEntry parent; // our parent (if we're in a group) 125 private ExpandableNotificationRow row; // the outer expanded view 126 127 private int mCachedContrastColor = COLOR_INVALID; 128 private int mCachedContrastColorIsFor = COLOR_INVALID; 129 private InflationTask mRunningTask = null; 130 private Throwable mDebugThrowable; 131 public CharSequence remoteInputTextWhenReset; 132 public long lastRemoteInputSent = NOT_LAUNCHED_YET; 133 public ArraySet<Integer> mActiveAppOps = new ArraySet<>(3); 134 public CharSequence headsUpStatusBarText; 135 public CharSequence headsUpStatusBarTextPublic; 136 137 private long initializationTime = -1; 138 139 /** 140 * Whether or not this row represents a system notification. Note that if this is 141 * {@code null}, that means we were either unable to retrieve the info or have yet to 142 * retrieve the info. 143 */ 144 public Boolean mIsSystemNotification; 145 146 /** 147 * Has the user sent a reply through this Notification. 148 */ 149 private boolean hasSentReply; 150 151 /** 152 * Whether this notification has been approved globally, at the app level, and at the channel 153 * level for bubbling. 154 */ 155 public boolean canBubble; 156 157 /** 158 * Whether this notification has changed in visual appearance since the previous post. 159 * New notifications are interruptive by default. 160 */ 161 public boolean isVisuallyInterruptive; 162 163 /** 164 * Whether this notification is shown to the user as a high priority notification: visible on 165 * the lock screen/status bar and in the top section in the shade. 166 */ 167 private boolean mHighPriority; 168 169 private boolean mIsTopBucket; 170 171 private boolean mSensitive = true; 172 private Runnable mOnSensitiveChangedListener; 173 private boolean mAutoHeadsUp; 174 private boolean mPulseSupressed; 175 NotificationEntry(StatusBarNotification n)176 public NotificationEntry(StatusBarNotification n) { 177 this(n, null); 178 } 179 NotificationEntry( StatusBarNotification n, @Nullable NotificationListenerService.Ranking ranking)180 public NotificationEntry( 181 StatusBarNotification n, 182 @Nullable NotificationListenerService.Ranking ranking) { 183 this.key = n.getKey(); 184 this.notification = n; 185 if (ranking != null) { 186 populateFromRanking(ranking); 187 } 188 } 189 populateFromRanking(@onNull NotificationListenerService.Ranking ranking)190 public void populateFromRanking(@NonNull NotificationListenerService.Ranking ranking) { 191 channel = ranking.getChannel(); 192 lastAudiblyAlertedMs = ranking.getLastAudiblyAlertedMillis(); 193 importance = ranking.getImportance(); 194 ambient = ranking.isAmbient(); 195 snoozeCriteria = ranking.getSnoozeCriteria(); 196 userSentiment = ranking.getUserSentiment(); 197 systemGeneratedSmartActions = ranking.getSmartActions() == null 198 ? Collections.emptyList() : ranking.getSmartActions(); 199 systemGeneratedSmartReplies = ranking.getSmartReplies() == null 200 ? new CharSequence[0] 201 : ranking.getSmartReplies().toArray(new CharSequence[0]); 202 suppressedVisualEffects = ranking.getSuppressedVisualEffects(); 203 suspended = ranking.isSuspended(); 204 canBubble = ranking.canBubble(); 205 isVisuallyInterruptive = ranking.visuallyInterruptive(); 206 } 207 setInterruption()208 public void setInterruption() { 209 interruption = true; 210 } 211 hasInterrupted()212 public boolean hasInterrupted() { 213 return interruption; 214 } 215 isHighPriority()216 public boolean isHighPriority() { 217 return mHighPriority; 218 } 219 setIsHighPriority(boolean highPriority)220 public void setIsHighPriority(boolean highPriority) { 221 this.mHighPriority = highPriority; 222 } 223 224 /** 225 * @return True if the notif should appear in the "top" or "important" section of notifications 226 * (as opposed to the "bottom" or "silent" section). This is usually the same as 227 * {@link #isHighPriority()}, but there are certain exceptions, such as media notifs. 228 */ isTopBucket()229 public boolean isTopBucket() { 230 return mIsTopBucket; 231 } setIsTopBucket(boolean isTopBucket)232 public void setIsTopBucket(boolean isTopBucket) { 233 mIsTopBucket = isTopBucket; 234 } 235 isBubble()236 public boolean isBubble() { 237 return (notification.getNotification().flags & FLAG_BUBBLE) != 0; 238 } 239 240 /** 241 * Returns the data needed for a bubble for this notification, if it exists. 242 */ getBubbleMetadata()243 public Notification.BubbleMetadata getBubbleMetadata() { 244 return notification.getNotification().getBubbleMetadata(); 245 } 246 247 /** 248 * Resets the notification entry to be re-used. 249 */ reset()250 public void reset() { 251 if (row != null) { 252 row.reset(); 253 } 254 } 255 getRow()256 public ExpandableNotificationRow getRow() { 257 return row; 258 } 259 260 //TODO: This will go away when we have a way to bind an entry to a row setRow(ExpandableNotificationRow row)261 public void setRow(ExpandableNotificationRow row) { 262 this.row = row; 263 } 264 265 @Nullable getChildren()266 public List<NotificationEntry> getChildren() { 267 if (row == null) { 268 return null; 269 } 270 271 List<ExpandableNotificationRow> rowChildren = row.getNotificationChildren(); 272 if (rowChildren == null) { 273 return null; 274 } 275 276 ArrayList<NotificationEntry> children = new ArrayList<>(); 277 for (ExpandableNotificationRow child : rowChildren) { 278 children.add(child.getEntry()); 279 } 280 281 return children; 282 } 283 notifyFullScreenIntentLaunched()284 public void notifyFullScreenIntentLaunched() { 285 setInterruption(); 286 lastFullScreenIntentLaunchTime = SystemClock.elapsedRealtime(); 287 } 288 hasJustLaunchedFullScreenIntent()289 public boolean hasJustLaunchedFullScreenIntent() { 290 return SystemClock.elapsedRealtime() < lastFullScreenIntentLaunchTime + LAUNCH_COOLDOWN; 291 } 292 hasJustSentRemoteInput()293 public boolean hasJustSentRemoteInput() { 294 return SystemClock.elapsedRealtime() < lastRemoteInputSent + REMOTE_INPUT_COOLDOWN; 295 } 296 hasFinishedInitialization()297 public boolean hasFinishedInitialization() { 298 return initializationTime == -1 299 || SystemClock.elapsedRealtime() > initializationTime + INITIALIZATION_DELAY; 300 } 301 302 /** 303 * Create the icons for a notification 304 * @param context the context to create the icons with 305 * @param sbn the notification 306 * @throws InflationException Exception if required icons are not valid or specified 307 */ createIcons(Context context, StatusBarNotification sbn)308 public void createIcons(Context context, StatusBarNotification sbn) 309 throws InflationException { 310 Notification n = sbn.getNotification(); 311 final Icon smallIcon = n.getSmallIcon(); 312 if (smallIcon == null) { 313 throw new InflationException("No small icon in notification from " 314 + sbn.getPackageName()); 315 } 316 317 // Construct the icon. 318 icon = new StatusBarIconView(context, 319 sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn); 320 icon.setScaleType(ImageView.ScaleType.CENTER_INSIDE); 321 322 // Construct the expanded icon. 323 expandedIcon = new StatusBarIconView(context, 324 sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn); 325 expandedIcon.setScaleType(ImageView.ScaleType.CENTER_INSIDE); 326 327 // Construct the expanded icon. 328 aodIcon = new StatusBarIconView(context, 329 sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn); 330 aodIcon.setScaleType(ImageView.ScaleType.CENTER_INSIDE); 331 aodIcon.setIncreasedSize(true); 332 333 final StatusBarIcon ic = new StatusBarIcon( 334 sbn.getUser(), 335 sbn.getPackageName(), 336 smallIcon, 337 n.iconLevel, 338 n.number, 339 StatusBarIconView.contentDescForNotification(context, n)); 340 341 if (!icon.set(ic) || !expandedIcon.set(ic) || !aodIcon.set(ic)) { 342 icon = null; 343 expandedIcon = null; 344 centeredIcon = null; 345 aodIcon = null; 346 throw new InflationException("Couldn't create icon: " + ic); 347 } 348 expandedIcon.setVisibility(View.INVISIBLE); 349 expandedIcon.setOnVisibilityChangedListener( 350 newVisibility -> { 351 if (row != null) { 352 row.setIconsVisible(newVisibility != View.VISIBLE); 353 } 354 }); 355 356 // Construct the centered icon 357 if (notification.getNotification().isMediaNotification()) { 358 centeredIcon = new StatusBarIconView(context, 359 sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn); 360 centeredIcon.setScaleType(ImageView.ScaleType.CENTER_INSIDE); 361 362 if (!centeredIcon.set(ic)) { 363 centeredIcon = null; 364 throw new InflationException("Couldn't update centered icon: " + ic); 365 } 366 } 367 } 368 setIconTag(int key, Object tag)369 public void setIconTag(int key, Object tag) { 370 if (icon != null) { 371 icon.setTag(key, tag); 372 expandedIcon.setTag(key, tag); 373 } 374 375 if (centeredIcon != null) { 376 centeredIcon.setTag(key, tag); 377 } 378 379 if (aodIcon != null) { 380 aodIcon.setTag(key, tag); 381 } 382 } 383 384 /** 385 * Update the notification icons. 386 * 387 * @param context the context to create the icons with. 388 * @param sbn the notification to read the icon from. 389 * @throws InflationException Exception if required icons are not valid or specified 390 */ updateIcons(Context context, StatusBarNotification sbn)391 public void updateIcons(Context context, StatusBarNotification sbn) 392 throws InflationException { 393 if (icon != null) { 394 // Update the icon 395 Notification n = sbn.getNotification(); 396 final StatusBarIcon ic = new StatusBarIcon( 397 notification.getUser(), 398 notification.getPackageName(), 399 n.getSmallIcon(), 400 n.iconLevel, 401 n.number, 402 StatusBarIconView.contentDescForNotification(context, n)); 403 icon.setNotification(sbn); 404 expandedIcon.setNotification(sbn); 405 aodIcon.setNotification(sbn); 406 if (!icon.set(ic) || !expandedIcon.set(ic) || !aodIcon.set(ic)) { 407 throw new InflationException("Couldn't update icon: " + ic); 408 } 409 410 if (centeredIcon != null) { 411 centeredIcon.setNotification(sbn); 412 if (!centeredIcon.set(ic)) { 413 throw new InflationException("Couldn't update centered icon: " + ic); 414 } 415 } 416 } 417 } 418 getContrastedColor(Context context, boolean isLowPriority, int backgroundColor)419 public int getContrastedColor(Context context, boolean isLowPriority, 420 int backgroundColor) { 421 int rawColor = isLowPriority ? Notification.COLOR_DEFAULT : 422 notification.getNotification().color; 423 if (mCachedContrastColorIsFor == rawColor && mCachedContrastColor != COLOR_INVALID) { 424 return mCachedContrastColor; 425 } 426 final int contrasted = ContrastColorUtil.resolveContrastColor(context, rawColor, 427 backgroundColor); 428 mCachedContrastColorIsFor = rawColor; 429 mCachedContrastColor = contrasted; 430 return mCachedContrastColor; 431 } 432 433 /** 434 * Abort all existing inflation tasks 435 */ abortTask()436 public void abortTask() { 437 if (mRunningTask != null) { 438 mRunningTask.abort(); 439 mRunningTask = null; 440 } 441 } 442 setInflationTask(InflationTask abortableTask)443 public void setInflationTask(InflationTask abortableTask) { 444 // abort any existing inflation 445 InflationTask existing = mRunningTask; 446 abortTask(); 447 mRunningTask = abortableTask; 448 if (existing != null && mRunningTask != null) { 449 mRunningTask.supersedeTask(existing); 450 } 451 } 452 onInflationTaskFinished()453 public void onInflationTaskFinished() { 454 mRunningTask = null; 455 } 456 457 @VisibleForTesting getRunningTask()458 public InflationTask getRunningTask() { 459 return mRunningTask; 460 } 461 462 /** 463 * Set a throwable that is used for debugging 464 * 465 * @param debugThrowable the throwable to save 466 */ setDebugThrowable(Throwable debugThrowable)467 public void setDebugThrowable(Throwable debugThrowable) { 468 mDebugThrowable = debugThrowable; 469 } 470 getDebugThrowable()471 public Throwable getDebugThrowable() { 472 return mDebugThrowable; 473 } 474 onRemoteInputInserted()475 public void onRemoteInputInserted() { 476 lastRemoteInputSent = NOT_LAUNCHED_YET; 477 remoteInputTextWhenReset = null; 478 } 479 setHasSentReply()480 public void setHasSentReply() { 481 hasSentReply = true; 482 } 483 isLastMessageFromReply()484 public boolean isLastMessageFromReply() { 485 if (!hasSentReply) { 486 return false; 487 } 488 Bundle extras = notification.getNotification().extras; 489 CharSequence[] replyTexts = extras.getCharSequenceArray( 490 Notification.EXTRA_REMOTE_INPUT_HISTORY); 491 if (!ArrayUtils.isEmpty(replyTexts)) { 492 return true; 493 } 494 Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES); 495 if (messages != null && messages.length > 0) { 496 Parcelable message = messages[messages.length - 1]; 497 if (message instanceof Bundle) { 498 Notification.MessagingStyle.Message lastMessage = 499 Notification.MessagingStyle.Message.getMessageFromBundle( 500 (Bundle) message); 501 if (lastMessage != null) { 502 Person senderPerson = lastMessage.getSenderPerson(); 503 if (senderPerson == null) { 504 return true; 505 } 506 Person user = extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON); 507 return Objects.equals(user, senderPerson); 508 } 509 } 510 } 511 return false; 512 } 513 setInitializationTime(long time)514 public void setInitializationTime(long time) { 515 if (initializationTime == -1) { 516 initializationTime = time; 517 } 518 } 519 sendAccessibilityEvent(int eventType)520 public void sendAccessibilityEvent(int eventType) { 521 if (row != null) { 522 row.sendAccessibilityEvent(eventType); 523 } 524 } 525 526 /** 527 * Used by NotificationMediaManager to determine... things 528 * @return {@code true} if we are a media notification 529 */ isMediaNotification()530 public boolean isMediaNotification() { 531 if (row == null) return false; 532 533 return row.isMediaRow(); 534 } 535 536 /** 537 * We are a top level child if our parent is the list of notifications duh 538 * @return {@code true} if we're a top level notification 539 */ isTopLevelChild()540 public boolean isTopLevelChild() { 541 return row != null && row.isTopLevelChild(); 542 } 543 resetUserExpansion()544 public void resetUserExpansion() { 545 if (row != null) row.resetUserExpansion(); 546 } 547 freeContentViewWhenSafe(@nflationFlag int inflationFlag)548 public void freeContentViewWhenSafe(@InflationFlag int inflationFlag) { 549 if (row != null) row.freeContentViewWhenSafe(inflationFlag); 550 } 551 rowExists()552 public boolean rowExists() { 553 return row != null; 554 } 555 isRowDismissed()556 public boolean isRowDismissed() { 557 return row != null && row.isDismissed(); 558 } 559 isRowRemoved()560 public boolean isRowRemoved() { 561 return row != null && row.isRemoved(); 562 } 563 564 /** 565 * @return {@code true} if the row is null or removed 566 */ isRemoved()567 public boolean isRemoved() { 568 //TODO: recycling invalidates this 569 return row == null || row.isRemoved(); 570 } 571 isRowPinned()572 public boolean isRowPinned() { 573 return row != null && row.isPinned(); 574 } 575 setRowPinned(boolean pinned)576 public void setRowPinned(boolean pinned) { 577 if (row != null) row.setPinned(pinned); 578 } 579 isRowHeadsUp()580 public boolean isRowHeadsUp() { 581 return row != null && row.isHeadsUp(); 582 } 583 showingPulsing()584 public boolean showingPulsing() { 585 return row != null && row.showingPulsing(); 586 } 587 setHeadsUp(boolean shouldHeadsUp)588 public void setHeadsUp(boolean shouldHeadsUp) { 589 if (row != null) row.setHeadsUp(shouldHeadsUp); 590 } 591 setHeadsUpAnimatingAway(boolean animatingAway)592 public void setHeadsUpAnimatingAway(boolean animatingAway) { 593 if (row != null) row.setHeadsUpAnimatingAway(animatingAway); 594 } 595 596 /** 597 * Set that this notification was automatically heads upped. This happens for example when 598 * the user bypasses the lockscreen and media is playing. 599 */ setAutoHeadsUp(boolean autoHeadsUp)600 public void setAutoHeadsUp(boolean autoHeadsUp) { 601 mAutoHeadsUp = autoHeadsUp; 602 } 603 604 /** 605 * @return if this notification was automatically heads upped. This happens for example when 606 * * the user bypasses the lockscreen and media is playing. 607 */ isAutoHeadsUp()608 public boolean isAutoHeadsUp() { 609 return mAutoHeadsUp; 610 } 611 mustStayOnScreen()612 public boolean mustStayOnScreen() { 613 return row != null && row.mustStayOnScreen(); 614 } 615 setHeadsUpIsVisible()616 public void setHeadsUpIsVisible() { 617 if (row != null) row.setHeadsUpIsVisible(); 618 } 619 620 //TODO: i'm imagining a world where this isn't just the row, but I could be rwong getHeadsUpAnimationView()621 public ExpandableNotificationRow getHeadsUpAnimationView() { 622 return row; 623 } 624 setUserLocked(boolean userLocked)625 public void setUserLocked(boolean userLocked) { 626 if (row != null) row.setUserLocked(userLocked); 627 } 628 setUserExpanded(boolean userExpanded, boolean allowChildExpansion)629 public void setUserExpanded(boolean userExpanded, boolean allowChildExpansion) { 630 if (row != null) row.setUserExpanded(userExpanded, allowChildExpansion); 631 } 632 setGroupExpansionChanging(boolean changing)633 public void setGroupExpansionChanging(boolean changing) { 634 if (row != null) row.setGroupExpansionChanging(changing); 635 } 636 notifyHeightChanged(boolean needsAnimation)637 public void notifyHeightChanged(boolean needsAnimation) { 638 if (row != null) row.notifyHeightChanged(needsAnimation); 639 } 640 closeRemoteInput()641 public void closeRemoteInput() { 642 if (row != null) row.closeRemoteInput(); 643 } 644 areChildrenExpanded()645 public boolean areChildrenExpanded() { 646 return row != null && row.areChildrenExpanded(); 647 } 648 keepInParent()649 public boolean keepInParent() { 650 return row != null && row.keepInParent(); 651 } 652 653 //TODO: probably less confusing to say "is group fully visible" isGroupNotFullyVisible()654 public boolean isGroupNotFullyVisible() { 655 return row == null || row.isGroupNotFullyVisible(); 656 } 657 getGuts()658 public NotificationGuts getGuts() { 659 if (row != null) return row.getGuts(); 660 return null; 661 } 662 removeRow()663 public void removeRow() { 664 if (row != null) row.setRemoved(); 665 } 666 isSummaryWithChildren()667 public boolean isSummaryWithChildren() { 668 return row != null && row.isSummaryWithChildren(); 669 } 670 setKeepInParent(boolean keep)671 public void setKeepInParent(boolean keep) { 672 if (row != null) row.setKeepInParent(keep); 673 } 674 onDensityOrFontScaleChanged()675 public void onDensityOrFontScaleChanged() { 676 if (row != null) row.onDensityOrFontScaleChanged(); 677 } 678 areGutsExposed()679 public boolean areGutsExposed() { 680 return row != null && row.getGuts() != null && row.getGuts().isExposed(); 681 } 682 isChildInGroup()683 public boolean isChildInGroup() { 684 return parent == null; 685 } 686 687 /** 688 * @return Can the underlying notification be cleared? This can be different from whether the 689 * notification can be dismissed in case notifications are sensitive on the lockscreen. 690 * @see #canViewBeDismissed() 691 */ isClearable()692 public boolean isClearable() { 693 if (notification == null || !notification.isClearable()) { 694 return false; 695 } 696 697 List<NotificationEntry> children = getChildren(); 698 if (children != null && children.size() > 0) { 699 for (int i = 0; i < children.size(); i++) { 700 NotificationEntry child = children.get(i); 701 if (!child.isClearable()) { 702 return false; 703 } 704 } 705 } 706 return true; 707 } 708 canViewBeDismissed()709 public boolean canViewBeDismissed() { 710 if (row == null) return true; 711 return row.canViewBeDismissed(); 712 } 713 714 @VisibleForTesting isExemptFromDndVisualSuppression()715 boolean isExemptFromDndVisualSuppression() { 716 if (isNotificationBlockedByPolicy(notification.getNotification())) { 717 return false; 718 } 719 720 if ((notification.getNotification().flags 721 & Notification.FLAG_FOREGROUND_SERVICE) != 0) { 722 return true; 723 } 724 if (notification.getNotification().isMediaNotification()) { 725 return true; 726 } 727 if (mIsSystemNotification != null && mIsSystemNotification) { 728 return true; 729 } 730 return false; 731 } 732 shouldSuppressVisualEffect(int effect)733 private boolean shouldSuppressVisualEffect(int effect) { 734 if (isExemptFromDndVisualSuppression()) { 735 return false; 736 } 737 return (suppressedVisualEffects & effect) != 0; 738 } 739 740 /** 741 * Returns whether {@link Policy#SUPPRESSED_EFFECT_FULL_SCREEN_INTENT} 742 * is set for this entry. 743 */ shouldSuppressFullScreenIntent()744 public boolean shouldSuppressFullScreenIntent() { 745 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_FULL_SCREEN_INTENT); 746 } 747 748 /** 749 * Returns whether {@link Policy#SUPPRESSED_EFFECT_PEEK} 750 * is set for this entry. 751 */ shouldSuppressPeek()752 public boolean shouldSuppressPeek() { 753 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_PEEK); 754 } 755 756 /** 757 * Returns whether {@link Policy#SUPPRESSED_EFFECT_STATUS_BAR} 758 * is set for this entry. 759 */ shouldSuppressStatusBar()760 public boolean shouldSuppressStatusBar() { 761 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_STATUS_BAR); 762 } 763 764 /** 765 * Returns whether {@link Policy#SUPPRESSED_EFFECT_AMBIENT} 766 * is set for this entry. 767 */ shouldSuppressAmbient()768 public boolean shouldSuppressAmbient() { 769 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_AMBIENT); 770 } 771 772 /** 773 * Returns whether {@link Policy#SUPPRESSED_EFFECT_NOTIFICATION_LIST} 774 * is set for this entry. 775 */ shouldSuppressNotificationList()776 public boolean shouldSuppressNotificationList() { 777 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_NOTIFICATION_LIST); 778 } 779 780 781 /** 782 * Returns whether {@link Policy#SUPPRESSED_EFFECT_BADGE} 783 * is set for this entry. This badge is not an app badge, but rather an indicator of "unseen" 784 * content. Typically this is referred to as a "dot" internally in Launcher & SysUI code. 785 */ shouldSuppressNotificationDot()786 public boolean shouldSuppressNotificationDot() { 787 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_BADGE); 788 } 789 790 /** 791 * Categories that are explicitly called out on DND settings screens are always blocked, if 792 * DND has flagged them, even if they are foreground or system notifications that might 793 * otherwise visually bypass DND. 794 */ isNotificationBlockedByPolicy(Notification n)795 private static boolean isNotificationBlockedByPolicy(Notification n) { 796 return isCategory(CATEGORY_CALL, n) 797 || isCategory(CATEGORY_MESSAGE, n) 798 || isCategory(CATEGORY_ALARM, n) 799 || isCategory(CATEGORY_EVENT, n) 800 || isCategory(CATEGORY_REMINDER, n); 801 } 802 isCategory(String category, Notification n)803 private static boolean isCategory(String category, Notification n) { 804 return Objects.equals(n.category, category); 805 } 806 807 /** 808 * Set this notification to be sensitive. 809 * 810 * @param sensitive true if the content of this notification is sensitive right now 811 * @param deviceSensitive true if the device in general is sensitive right now 812 */ setSensitive(boolean sensitive, boolean deviceSensitive)813 public void setSensitive(boolean sensitive, boolean deviceSensitive) { 814 getRow().setSensitive(sensitive, deviceSensitive); 815 if (sensitive != mSensitive) { 816 mSensitive = sensitive; 817 if (mOnSensitiveChangedListener != null) { 818 mOnSensitiveChangedListener.run(); 819 } 820 } 821 } 822 isSensitive()823 public boolean isSensitive() { 824 return mSensitive; 825 } 826 setOnSensitiveChangedListener(Runnable listener)827 public void setOnSensitiveChangedListener(Runnable listener) { 828 mOnSensitiveChangedListener = listener; 829 } 830 isPulseSuppressed()831 public boolean isPulseSuppressed() { 832 return mPulseSupressed; 833 } 834 setPulseSuppressed(boolean suppressed)835 public void setPulseSuppressed(boolean suppressed) { 836 mPulseSupressed = suppressed; 837 } 838 839 /** Information about a suggestion that is being edited. */ 840 public static class EditedSuggestionInfo { 841 842 /** 843 * The value of the suggestion (before any user edits). 844 */ 845 public final CharSequence originalText; 846 847 /** 848 * The index of the suggestion that is being edited. 849 */ 850 public final int index; 851 EditedSuggestionInfo(CharSequence originalText, int index)852 public EditedSuggestionInfo(CharSequence originalText, int index) { 853 this.originalText = originalText; 854 this.index = index; 855 } 856 } 857 } 858