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.Notification.FLAG_FOREGROUND_SERVICE; 26 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT; 27 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_BADGE; 28 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_FULL_SCREEN_INTENT; 29 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST; 30 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; 31 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR; 32 33 import static com.android.systemui.statusbar.notification.collection.NotifCollection.REASON_NOT_CANCELED; 34 import static com.android.systemui.statusbar.notification.stack.NotificationPriorityBucketKt.BUCKET_ALERTING; 35 36 import static java.util.Objects.requireNonNull; 37 38 import android.app.Notification; 39 import android.app.Notification.MessagingStyle.Message; 40 import android.app.NotificationChannel; 41 import android.app.NotificationManager.Policy; 42 import android.app.Person; 43 import android.app.RemoteInput; 44 import android.content.Context; 45 import android.content.pm.ShortcutInfo; 46 import android.net.Uri; 47 import android.os.Bundle; 48 import android.os.Parcelable; 49 import android.os.SystemClock; 50 import android.service.notification.NotificationListenerService.Ranking; 51 import android.service.notification.SnoozeCriterion; 52 import android.service.notification.StatusBarNotification; 53 import android.util.ArraySet; 54 import android.view.ContentInfo; 55 56 import androidx.annotation.NonNull; 57 import androidx.annotation.Nullable; 58 59 import com.android.internal.annotations.VisibleForTesting; 60 import com.android.internal.util.ArrayUtils; 61 import com.android.internal.util.ContrastColorUtil; 62 import com.android.systemui.statusbar.InflationTask; 63 import com.android.systemui.statusbar.notification.collection.NotifCollection.CancellationReason; 64 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter; 65 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter; 66 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor; 67 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender; 68 import com.android.systemui.statusbar.notification.collection.render.GroupMembershipManager; 69 import com.android.systemui.statusbar.notification.icon.IconPack; 70 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 71 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController; 72 import com.android.systemui.statusbar.notification.row.NotificationGuts; 73 import com.android.systemui.statusbar.notification.stack.PriorityBucket; 74 75 import java.util.ArrayList; 76 import java.util.List; 77 import java.util.Objects; 78 79 /** 80 * Represents a notification that the system UI knows about 81 * 82 * Whenever the NotificationManager tells us about the existence of a new notification, we wrap it 83 * in a NotificationEntry. Thus, every notification has an associated NotificationEntry, even if 84 * that notification is never displayed to the user (for example, if it's filtered out for some 85 * reason). 86 * 87 * Entries store information about the current state of the notification. Essentially: 88 * anything that needs to persist or be modifiable even when the notification's views don't 89 * exist. Any other state should be stored on the views/view controllers themselves. 90 * 91 * At the moment, there are many things here that shouldn't be and vice-versa. Hopefully we can 92 * clean this up in the future. 93 */ 94 public final class NotificationEntry extends ListEntry { 95 96 private final String mKey; 97 private StatusBarNotification mSbn; 98 private Ranking mRanking; 99 100 /* 101 * Bookkeeping members 102 */ 103 104 /** List of lifetime extenders that are extending the lifetime of this notification. */ 105 final List<NotifLifetimeExtender> mLifetimeExtenders = new ArrayList<>(); 106 107 /** List of dismiss interceptors that are intercepting the dismissal of this notification. */ 108 final List<NotifDismissInterceptor> mDismissInterceptors = new ArrayList<>(); 109 110 /** 111 * If this notification was cancelled by system server, then the reason that was supplied. 112 * Uncancelled notifications always have REASON_NOT_CANCELED. Note that lifetime-extended 113 * notifications will have this set even though they are still in the active notification set. 114 */ 115 @CancellationReason int mCancellationReason = REASON_NOT_CANCELED; 116 117 /** @see #getDismissState() */ 118 @NonNull private DismissState mDismissState = DismissState.NOT_DISMISSED; 119 120 /* 121 * Old members 122 * TODO: Remove every member beneath this line if possible 123 */ 124 125 private IconPack mIcons = IconPack.buildEmptyPack(null); 126 private boolean interruption; 127 public int targetSdk; 128 private long lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET; 129 public CharSequence remoteInputText; 130 public String remoteInputMimeType; 131 public Uri remoteInputUri; 132 public ContentInfo remoteInputAttachment; 133 private Notification.BubbleMetadata mBubbleMetadata; 134 private ShortcutInfo mShortcutInfo; 135 136 /** 137 * If {@link RemoteInput#getEditChoicesBeforeSending} is enabled, and the user is 138 * currently editing a choice (smart reply), then this field contains the information about the 139 * suggestion being edited. Otherwise <code>null</code>. 140 */ 141 public EditedSuggestionInfo editedSuggestionInfo; 142 143 private ExpandableNotificationRow row; // the outer expanded view 144 private ExpandableNotificationRowController mRowController; 145 146 private int mCachedContrastColor = COLOR_INVALID; 147 private int mCachedContrastColorIsFor = COLOR_INVALID; 148 private InflationTask mRunningTask = null; 149 private Throwable mDebugThrowable; 150 public CharSequence remoteInputTextWhenReset; 151 public long lastRemoteInputSent = NOT_LAUNCHED_YET; 152 public final ArraySet<Integer> mActiveAppOps = new ArraySet<>(3); 153 public CharSequence headsUpStatusBarText; 154 public CharSequence headsUpStatusBarTextPublic; 155 156 // indicates when this entry's view was first attached to a window 157 // this value will reset when the view is completely removed from the shade (ie: filtered out) 158 private long initializationTime = -1; 159 160 /** 161 * Has the user sent a reply through this Notification. 162 */ 163 private boolean hasSentReply; 164 165 private boolean mSensitive = true; 166 private List<OnSensitivityChangedListener> mOnSensitivityChangedListeners = new ArrayList<>(); 167 168 private boolean mAutoHeadsUp; 169 private boolean mPulseSupressed; 170 private int mBucket = BUCKET_ALERTING; 171 @Nullable private Long mPendingAnimationDuration; 172 private boolean mIsMarkedForUserTriggeredMovement; 173 private boolean mIsAlerting; 174 175 public boolean mRemoteEditImeAnimatingAway; 176 public boolean mRemoteEditImeVisible; 177 private boolean mExpandAnimationRunning; 178 /** 179 * Flag to determine if the entry is blockable by DnD filters 180 */ 181 private boolean mBlockable; 182 183 /** 184 * @param sbn the StatusBarNotification from system server 185 * @param ranking also from system server 186 * @param creationTime SystemClock.uptimeMillis of when we were created 187 */ NotificationEntry( @onNull StatusBarNotification sbn, @NonNull Ranking ranking, long creationTime )188 public NotificationEntry( 189 @NonNull StatusBarNotification sbn, 190 @NonNull Ranking ranking, 191 long creationTime 192 ) { 193 super(requireNonNull(requireNonNull(sbn).getKey()), creationTime); 194 195 requireNonNull(ranking); 196 197 mKey = sbn.getKey(); 198 setSbn(sbn); 199 setRanking(ranking); 200 } 201 202 @Override getRepresentativeEntry()203 public NotificationEntry getRepresentativeEntry() { 204 return this; 205 } 206 207 /** The key for this notification. Guaranteed to be immutable and unique */ getKey()208 @NonNull public String getKey() { 209 return mKey; 210 } 211 212 /** 213 * The StatusBarNotification that represents one half of a NotificationEntry (the other half 214 * being the Ranking). This object is swapped out whenever a notification is updated. 215 */ getSbn()216 @NonNull public StatusBarNotification getSbn() { 217 return mSbn; 218 } 219 220 /** 221 * Should only be called by NotificationEntryManager and friends. 222 * TODO: Make this package-private 223 */ setSbn(@onNull StatusBarNotification sbn)224 public void setSbn(@NonNull StatusBarNotification sbn) { 225 requireNonNull(sbn); 226 requireNonNull(sbn.getKey()); 227 228 if (!sbn.getKey().equals(mKey)) { 229 throw new IllegalArgumentException("New key " + sbn.getKey() 230 + " doesn't match existing key " + mKey); 231 } 232 233 mSbn = sbn; 234 mBubbleMetadata = mSbn.getNotification().getBubbleMetadata(); 235 } 236 237 /** 238 * The Ranking that represents one half of a NotificationEntry (the other half being the 239 * StatusBarNotification). This object is swapped out whenever a the ranking is updated (which 240 * generally occurs whenever anything changes in the notification list). 241 */ getRanking()242 public Ranking getRanking() { 243 return mRanking; 244 } 245 246 /** 247 * Should only be called by NotificationEntryManager and friends. 248 * TODO: Make this package-private 249 */ setRanking(@onNull Ranking ranking)250 public void setRanking(@NonNull Ranking ranking) { 251 requireNonNull(ranking); 252 requireNonNull(ranking.getKey()); 253 254 if (!ranking.getKey().equals(mKey)) { 255 throw new IllegalArgumentException("New key " + ranking.getKey() 256 + " doesn't match existing key " + mKey); 257 } 258 259 mRanking = ranking.withAudiblyAlertedInfo(mRanking); 260 updateIsBlockable(); 261 } 262 263 /* 264 * Bookkeeping getters and setters 265 */ 266 267 /** 268 * Set if the user has dismissed this notif but we haven't yet heard back from system server to 269 * confirm the dismissal. 270 */ getDismissState()271 @NonNull public DismissState getDismissState() { 272 return mDismissState; 273 } 274 setDismissState(@onNull DismissState dismissState)275 void setDismissState(@NonNull DismissState dismissState) { 276 mDismissState = requireNonNull(dismissState); 277 } 278 getExcludingFilter()279 @Nullable public NotifFilter getExcludingFilter() { 280 return getAttachState().getExcludingFilter(); 281 } 282 getNotifPromoter()283 @Nullable public NotifPromoter getNotifPromoter() { 284 return getAttachState().getPromoter(); 285 } 286 287 /* 288 * Convenience getters for SBN and Ranking members 289 */ 290 getChannel()291 public NotificationChannel getChannel() { 292 return mRanking.getChannel(); 293 } 294 getLastAudiblyAlertedMs()295 public long getLastAudiblyAlertedMs() { 296 return mRanking.getLastAudiblyAlertedMillis(); 297 } 298 isAmbient()299 public boolean isAmbient() { 300 return mRanking.isAmbient(); 301 } 302 getImportance()303 public int getImportance() { 304 return mRanking.getImportance(); 305 } 306 getSnoozeCriteria()307 public List<SnoozeCriterion> getSnoozeCriteria() { 308 return mRanking.getSnoozeCriteria(); 309 } 310 getUserSentiment()311 public int getUserSentiment() { 312 return mRanking.getUserSentiment(); 313 } 314 getSuppressedVisualEffects()315 public int getSuppressedVisualEffects() { 316 return mRanking.getSuppressedVisualEffects(); 317 } 318 319 /** @see Ranking#canBubble() */ canBubble()320 public boolean canBubble() { 321 return mRanking.canBubble(); 322 } 323 getSmartActions()324 public @NonNull List<Notification.Action> getSmartActions() { 325 return mRanking.getSmartActions(); 326 } 327 getSmartReplies()328 public @NonNull List<CharSequence> getSmartReplies() { 329 return mRanking.getSmartReplies(); 330 } 331 332 333 /* 334 * Old methods 335 * 336 * TODO: Remove as many of these as possible 337 */ 338 339 @NonNull getIcons()340 public IconPack getIcons() { 341 return mIcons; 342 } 343 setIcons(@onNull IconPack icons)344 public void setIcons(@NonNull IconPack icons) { 345 mIcons = icons; 346 } 347 setInterruption()348 public void setInterruption() { 349 interruption = true; 350 } 351 hasInterrupted()352 public boolean hasInterrupted() { 353 return interruption; 354 } 355 isBubble()356 public boolean isBubble() { 357 return (mSbn.getNotification().flags & FLAG_BUBBLE) != 0; 358 } 359 360 /** 361 * Returns the data needed for a bubble for this notification, if it exists. 362 */ 363 @Nullable getBubbleMetadata()364 public Notification.BubbleMetadata getBubbleMetadata() { 365 return mBubbleMetadata; 366 } 367 368 /** 369 * Sets bubble metadata for this notification. 370 */ setBubbleMetadata(@ullable Notification.BubbleMetadata metadata)371 public void setBubbleMetadata(@Nullable Notification.BubbleMetadata metadata) { 372 mBubbleMetadata = metadata; 373 } 374 375 /** 376 * Updates the {@link Notification#FLAG_BUBBLE} flag on this notification to indicate 377 * whether it is a bubble or not. If this entry is set to not bubble, or does not have 378 * the required info to bubble, the flag cannot be set to true. 379 * 380 * @param shouldBubble whether this notification should be flagged as a bubble. 381 * @return true if the value changed. 382 */ setFlagBubble(boolean shouldBubble)383 public boolean setFlagBubble(boolean shouldBubble) { 384 boolean wasBubble = isBubble(); 385 if (!shouldBubble) { 386 mSbn.getNotification().flags &= ~FLAG_BUBBLE; 387 } else if (mBubbleMetadata != null && canBubble()) { 388 // wants to be bubble & can bubble, set flag 389 mSbn.getNotification().flags |= FLAG_BUBBLE; 390 } 391 return wasBubble != isBubble(); 392 } 393 394 @PriorityBucket getBucket()395 public int getBucket() { 396 return mBucket; 397 } 398 setBucket(@riorityBucket int bucket)399 public void setBucket(@PriorityBucket int bucket) { 400 mBucket = bucket; 401 } 402 getRow()403 public ExpandableNotificationRow getRow() { 404 return row; 405 } 406 407 //TODO: This will go away when we have a way to bind an entry to a row setRow(ExpandableNotificationRow row)408 public void setRow(ExpandableNotificationRow row) { 409 this.row = row; 410 } 411 getRowController()412 public ExpandableNotificationRowController getRowController() { 413 return mRowController; 414 } 415 setRowController(ExpandableNotificationRowController controller)416 public void setRowController(ExpandableNotificationRowController controller) { 417 mRowController = controller; 418 } 419 420 /** 421 * Get the children that are actually attached to this notification's row. 422 * 423 * TODO: Seems like most callers here should probably be using 424 * {@link GroupMembershipManager#getChildren(ListEntry)} 425 */ getAttachedNotifChildren()426 public @Nullable List<NotificationEntry> getAttachedNotifChildren() { 427 if (row == null) { 428 return null; 429 } 430 431 List<ExpandableNotificationRow> rowChildren = row.getAttachedChildren(); 432 if (rowChildren == null) { 433 return null; 434 } 435 436 ArrayList<NotificationEntry> children = new ArrayList<>(); 437 for (ExpandableNotificationRow child : rowChildren) { 438 children.add(child.getEntry()); 439 } 440 441 return children; 442 } 443 notifyFullScreenIntentLaunched()444 public void notifyFullScreenIntentLaunched() { 445 setInterruption(); 446 lastFullScreenIntentLaunchTime = SystemClock.elapsedRealtime(); 447 } 448 hasJustLaunchedFullScreenIntent()449 public boolean hasJustLaunchedFullScreenIntent() { 450 return SystemClock.elapsedRealtime() < lastFullScreenIntentLaunchTime + LAUNCH_COOLDOWN; 451 } 452 hasJustSentRemoteInput()453 public boolean hasJustSentRemoteInput() { 454 return SystemClock.elapsedRealtime() < lastRemoteInputSent + REMOTE_INPUT_COOLDOWN; 455 } 456 hasFinishedInitialization()457 public boolean hasFinishedInitialization() { 458 return initializationTime != -1 459 && SystemClock.elapsedRealtime() > initializationTime + INITIALIZATION_DELAY; 460 } 461 getContrastedColor(Context context, boolean isLowPriority, int backgroundColor)462 public int getContrastedColor(Context context, boolean isLowPriority, 463 int backgroundColor) { 464 int rawColor = isLowPriority ? Notification.COLOR_DEFAULT : 465 mSbn.getNotification().color; 466 if (mCachedContrastColorIsFor == rawColor && mCachedContrastColor != COLOR_INVALID) { 467 return mCachedContrastColor; 468 } 469 final int contrasted = ContrastColorUtil.resolveContrastColor(context, rawColor, 470 backgroundColor); 471 mCachedContrastColorIsFor = rawColor; 472 mCachedContrastColor = contrasted; 473 return mCachedContrastColor; 474 } 475 476 /** 477 * Abort all existing inflation tasks 478 */ abortTask()479 public boolean abortTask() { 480 if (mRunningTask != null) { 481 mRunningTask.abort(); 482 mRunningTask = null; 483 return true; 484 } 485 return false; 486 } 487 setInflationTask(InflationTask abortableTask)488 public void setInflationTask(InflationTask abortableTask) { 489 // abort any existing inflation 490 abortTask(); 491 mRunningTask = abortableTask; 492 } 493 onInflationTaskFinished()494 public void onInflationTaskFinished() { 495 mRunningTask = null; 496 } 497 498 @VisibleForTesting getRunningTask()499 public InflationTask getRunningTask() { 500 return mRunningTask; 501 } 502 503 /** 504 * Set a throwable that is used for debugging 505 * 506 * @param debugThrowable the throwable to save 507 */ setDebugThrowable(Throwable debugThrowable)508 public void setDebugThrowable(Throwable debugThrowable) { 509 mDebugThrowable = debugThrowable; 510 } 511 getDebugThrowable()512 public Throwable getDebugThrowable() { 513 return mDebugThrowable; 514 } 515 onRemoteInputInserted()516 public void onRemoteInputInserted() { 517 lastRemoteInputSent = NOT_LAUNCHED_YET; 518 remoteInputTextWhenReset = null; 519 } 520 setHasSentReply()521 public void setHasSentReply() { 522 hasSentReply = true; 523 } 524 isLastMessageFromReply()525 public boolean isLastMessageFromReply() { 526 if (!hasSentReply) { 527 return false; 528 } 529 Bundle extras = mSbn.getNotification().extras; 530 Parcelable[] replyTexts = 531 extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS); 532 if (!ArrayUtils.isEmpty(replyTexts)) { 533 return true; 534 } 535 List<Message> messages = Message.getMessagesFromBundleArray( 536 extras.getParcelableArray(Notification.EXTRA_MESSAGES)); 537 if (messages != null && !messages.isEmpty()) { 538 Message lastMessage = messages.get(messages.size() -1); 539 540 if (lastMessage != null) { 541 Person senderPerson = lastMessage.getSenderPerson(); 542 if (senderPerson == null) { 543 return true; 544 } 545 Person user = extras.getParcelable( 546 Notification.EXTRA_MESSAGING_PERSON, Person.class); 547 return Objects.equals(user, senderPerson); 548 } 549 } 550 return false; 551 } 552 resetInitializationTime()553 public void resetInitializationTime() { 554 initializationTime = -1; 555 } 556 setInitializationTime(long time)557 public void setInitializationTime(long time) { 558 if (initializationTime == -1) { 559 initializationTime = time; 560 } 561 } 562 sendAccessibilityEvent(int eventType)563 public void sendAccessibilityEvent(int eventType) { 564 if (row != null) { 565 row.sendAccessibilityEvent(eventType); 566 } 567 } 568 569 /** 570 * Used by NotificationMediaManager to determine... things 571 * @return {@code true} if we are a media notification 572 */ isMediaNotification()573 public boolean isMediaNotification() { 574 if (row == null) return false; 575 576 return row.isMediaRow(); 577 } 578 579 /** 580 * We are a top level child if our parent is the list of notifications duh 581 * @return {@code true} if we're a top level notification 582 */ isTopLevelChild()583 public boolean isTopLevelChild() { 584 return row != null && row.isTopLevelChild(); 585 } 586 resetUserExpansion()587 public void resetUserExpansion() { 588 if (row != null) row.resetUserExpansion(); 589 } 590 rowExists()591 public boolean rowExists() { 592 return row != null; 593 } 594 isRowDismissed()595 public boolean isRowDismissed() { 596 return row != null && row.isDismissed(); 597 } 598 isRowRemoved()599 public boolean isRowRemoved() { 600 return row != null && row.isRemoved(); 601 } 602 603 /** 604 * @return {@code true} if the row is null or removed 605 */ isRemoved()606 public boolean isRemoved() { 607 //TODO: recycling invalidates this 608 return row == null || row.isRemoved(); 609 } 610 isRowPinned()611 public boolean isRowPinned() { 612 return row != null && row.isPinned(); 613 } 614 615 /** 616 * Is this entry pinned and was expanded while doing so 617 */ isPinnedAndExpanded()618 public boolean isPinnedAndExpanded() { 619 return row != null && row.isPinnedAndExpanded(); 620 } 621 setRowPinned(boolean pinned)622 public void setRowPinned(boolean pinned) { 623 if (row != null) row.setPinned(pinned); 624 } 625 isRowHeadsUp()626 public boolean isRowHeadsUp() { 627 return row != null && row.isHeadsUp(); 628 } 629 showingPulsing()630 public boolean showingPulsing() { 631 return row != null && row.showingPulsing(); 632 } 633 setHeadsUp(boolean shouldHeadsUp)634 public void setHeadsUp(boolean shouldHeadsUp) { 635 if (row != null) row.setHeadsUp(shouldHeadsUp); 636 } 637 setHeadsUpAnimatingAway(boolean animatingAway)638 public void setHeadsUpAnimatingAway(boolean animatingAway) { 639 if (row != null) row.setHeadsUpAnimatingAway(animatingAway); 640 } 641 mustStayOnScreen()642 public boolean mustStayOnScreen() { 643 return row != null && row.mustStayOnScreen(); 644 } 645 setHeadsUpIsVisible()646 public void setHeadsUpIsVisible() { 647 if (row != null) row.setHeadsUpIsVisible(); 648 } 649 650 //TODO: i'm imagining a world where this isn't just the row, but I could be rwong getHeadsUpAnimationView()651 public ExpandableNotificationRow getHeadsUpAnimationView() { 652 return row; 653 } 654 setUserLocked(boolean userLocked)655 public void setUserLocked(boolean userLocked) { 656 if (row != null) row.setUserLocked(userLocked); 657 } 658 setUserExpanded(boolean userExpanded, boolean allowChildExpansion)659 public void setUserExpanded(boolean userExpanded, boolean allowChildExpansion) { 660 if (row != null) row.setUserExpanded(userExpanded, allowChildExpansion); 661 } 662 setGroupExpansionChanging(boolean changing)663 public void setGroupExpansionChanging(boolean changing) { 664 if (row != null) row.setGroupExpansionChanging(changing); 665 } 666 notifyHeightChanged(boolean needsAnimation)667 public void notifyHeightChanged(boolean needsAnimation) { 668 if (row != null) row.notifyHeightChanged(needsAnimation); 669 } 670 closeRemoteInput()671 public void closeRemoteInput() { 672 if (row != null) row.closeRemoteInput(); 673 } 674 areChildrenExpanded()675 public boolean areChildrenExpanded() { 676 return row != null && row.areChildrenExpanded(); 677 } 678 679 680 //TODO: probably less confusing to say "is group fully visible" isGroupNotFullyVisible()681 public boolean isGroupNotFullyVisible() { 682 return row == null || row.isGroupNotFullyVisible(); 683 } 684 getGuts()685 public NotificationGuts getGuts() { 686 if (row != null) return row.getGuts(); 687 return null; 688 } 689 removeRow()690 public void removeRow() { 691 if (row != null) row.setRemoved(); 692 } 693 isSummaryWithChildren()694 public boolean isSummaryWithChildren() { 695 return row != null && row.isSummaryWithChildren(); 696 } 697 onDensityOrFontScaleChanged()698 public void onDensityOrFontScaleChanged() { 699 if (row != null) row.onDensityOrFontScaleChanged(); 700 } 701 areGutsExposed()702 public boolean areGutsExposed() { 703 return row != null && row.getGuts() != null && row.getGuts().isExposed(); 704 } 705 isChildInGroup()706 public boolean isChildInGroup() { 707 return row != null && row.isChildInGroup(); 708 } 709 710 /** 711 * @return Can the underlying notification be cleared? This can be different from whether the 712 * notification can be dismissed in case notifications are sensitive on the lockscreen. 713 */ 714 // TODO: This logic doesn't belong on NotificationEntry. It should be moved to a controller 715 // that can be added as a dependency to any class that needs to answer this question. isClearable()716 public boolean isClearable() { 717 if (!mSbn.isClearable()) { 718 return false; 719 } 720 721 List<NotificationEntry> children = getAttachedNotifChildren(); 722 if (children != null && children.size() > 0) { 723 for (int i = 0; i < children.size(); i++) { 724 NotificationEntry child = children.get(i); 725 if (!child.getSbn().isClearable()) { 726 return false; 727 } 728 } 729 } 730 return true; 731 } 732 733 /** 734 * @return Can the underlying notification be individually dismissed? 735 * @see #canViewBeDismissed() 736 */ 737 // TODO: This logic doesn't belong on NotificationEntry. It should be moved to a controller 738 // that can be added as a dependency to any class that needs to answer this question. isDismissable()739 public boolean isDismissable() { 740 if (mSbn.isOngoing()) { 741 return false; 742 } 743 List<NotificationEntry> children = getAttachedNotifChildren(); 744 if (children != null && children.size() > 0) { 745 for (int i = 0; i < children.size(); i++) { 746 NotificationEntry child = children.get(i); 747 if (child.getSbn().isOngoing()) { 748 return false; 749 } 750 } 751 } 752 return true; 753 } 754 canViewBeDismissed()755 public boolean canViewBeDismissed() { 756 if (row == null) return true; 757 return row.canViewBeDismissed(); 758 } 759 760 @VisibleForTesting isExemptFromDndVisualSuppression()761 boolean isExemptFromDndVisualSuppression() { 762 if (isNotificationBlockedByPolicy(mSbn.getNotification())) { 763 return false; 764 } 765 766 if ((mSbn.getNotification().flags 767 & FLAG_FOREGROUND_SERVICE) != 0) { 768 return true; 769 } 770 if (mSbn.getNotification().isMediaNotification()) { 771 return true; 772 } 773 if (!isBlockable()) { 774 return true; 775 } 776 return false; 777 } 778 779 /** 780 * Returns whether this row is considered blockable (i.e. it's not a system notif 781 * or is not in an allowList). 782 */ isBlockable()783 public boolean isBlockable() { 784 return mBlockable; 785 } 786 updateIsBlockable()787 private void updateIsBlockable() { 788 if (getChannel() == null) { 789 mBlockable = false; 790 return; 791 } 792 if (getChannel().isImportanceLockedByCriticalDeviceFunction() 793 && !getChannel().isBlockable()) { 794 mBlockable = false; 795 return; 796 } 797 mBlockable = true; 798 } 799 shouldSuppressVisualEffect(int effect)800 private boolean shouldSuppressVisualEffect(int effect) { 801 if (isExemptFromDndVisualSuppression()) { 802 return false; 803 } 804 return (getSuppressedVisualEffects() & effect) != 0; 805 } 806 807 /** 808 * Returns whether {@link Policy#SUPPRESSED_EFFECT_FULL_SCREEN_INTENT} 809 * is set for this entry. 810 */ shouldSuppressFullScreenIntent()811 public boolean shouldSuppressFullScreenIntent() { 812 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_FULL_SCREEN_INTENT); 813 } 814 815 /** 816 * Returns whether {@link Policy#SUPPRESSED_EFFECT_PEEK} 817 * is set for this entry. 818 */ shouldSuppressPeek()819 public boolean shouldSuppressPeek() { 820 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_PEEK); 821 } 822 823 /** 824 * Returns whether {@link Policy#SUPPRESSED_EFFECT_STATUS_BAR} 825 * is set for this entry. 826 */ shouldSuppressStatusBar()827 public boolean shouldSuppressStatusBar() { 828 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_STATUS_BAR); 829 } 830 831 /** 832 * Returns whether {@link Policy#SUPPRESSED_EFFECT_AMBIENT} 833 * is set for this entry. 834 */ shouldSuppressAmbient()835 public boolean shouldSuppressAmbient() { 836 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_AMBIENT); 837 } 838 839 /** 840 * Returns whether {@link Policy#SUPPRESSED_EFFECT_NOTIFICATION_LIST} 841 * is set for this entry. 842 */ shouldSuppressNotificationList()843 public boolean shouldSuppressNotificationList() { 844 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_NOTIFICATION_LIST); 845 } 846 847 848 /** 849 * Returns whether {@link Policy#SUPPRESSED_EFFECT_BADGE} 850 * is set for this entry. This badge is not an app badge, but rather an indicator of "unseen" 851 * content. Typically this is referred to as a "dot" internally in Launcher & SysUI code. 852 */ shouldSuppressNotificationDot()853 public boolean shouldSuppressNotificationDot() { 854 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_BADGE); 855 } 856 857 /** 858 * Categories that are explicitly called out on DND settings screens are always blocked, if 859 * DND has flagged them, even if they are foreground or system notifications that might 860 * otherwise visually bypass DND. 861 */ isNotificationBlockedByPolicy(Notification n)862 private static boolean isNotificationBlockedByPolicy(Notification n) { 863 return isCategory(CATEGORY_CALL, n) 864 || isCategory(CATEGORY_MESSAGE, n) 865 || isCategory(CATEGORY_ALARM, n) 866 || isCategory(CATEGORY_EVENT, n) 867 || isCategory(CATEGORY_REMINDER, n); 868 } 869 isCategory(String category, Notification n)870 private static boolean isCategory(String category, Notification n) { 871 return Objects.equals(n.category, category); 872 } 873 874 /** 875 * Set this notification to be sensitive. 876 * 877 * @param sensitive true if the content of this notification is sensitive right now 878 * @param deviceSensitive true if the device in general is sensitive right now 879 */ setSensitive(boolean sensitive, boolean deviceSensitive)880 public void setSensitive(boolean sensitive, boolean deviceSensitive) { 881 getRow().setSensitive(sensitive, deviceSensitive); 882 if (sensitive != mSensitive) { 883 mSensitive = sensitive; 884 for (int i = 0; i < mOnSensitivityChangedListeners.size(); i++) { 885 mOnSensitivityChangedListeners.get(i).onSensitivityChanged(this); 886 } 887 } 888 } 889 isSensitive()890 public boolean isSensitive() { 891 return mSensitive; 892 } 893 894 /** Add a listener to be notified when the entry's sensitivity changes. */ addOnSensitivityChangedListener(OnSensitivityChangedListener listener)895 public void addOnSensitivityChangedListener(OnSensitivityChangedListener listener) { 896 mOnSensitivityChangedListeners.add(listener); 897 } 898 899 /** Remove a listener that was registered above. */ removeOnSensitivityChangedListener(OnSensitivityChangedListener listener)900 public void removeOnSensitivityChangedListener(OnSensitivityChangedListener listener) { 901 mOnSensitivityChangedListeners.remove(listener); 902 } 903 isPulseSuppressed()904 public boolean isPulseSuppressed() { 905 return mPulseSupressed; 906 } 907 setPulseSuppressed(boolean suppressed)908 public void setPulseSuppressed(boolean suppressed) { 909 mPulseSupressed = suppressed; 910 } 911 912 /** Whether or not this entry has been marked for a user-triggered movement. */ isMarkedForUserTriggeredMovement()913 public boolean isMarkedForUserTriggeredMovement() { 914 return mIsMarkedForUserTriggeredMovement; 915 } 916 917 /** 918 * Mark this entry for movement triggered by a user action (ex: changing the priorirty of a 919 * conversation). This can then be used for custom animations. 920 */ markForUserTriggeredMovement(boolean marked)921 public void markForUserTriggeredMovement(boolean marked) { 922 mIsMarkedForUserTriggeredMovement = marked; 923 } 924 setIsAlerting(boolean isAlerting)925 public void setIsAlerting(boolean isAlerting) { 926 mIsAlerting = isAlerting; 927 } 928 isAlerting()929 public boolean isAlerting() { 930 return mIsAlerting; 931 } 932 933 /** Set whether this notification is currently used to animate a launch. */ setExpandAnimationRunning(boolean expandAnimationRunning)934 public void setExpandAnimationRunning(boolean expandAnimationRunning) { 935 mExpandAnimationRunning = expandAnimationRunning; 936 } 937 938 /** Whether this notification is currently used to animate a launch. */ isExpandAnimationRunning()939 public boolean isExpandAnimationRunning() { 940 return mExpandAnimationRunning; 941 } 942 943 /** Information about a suggestion that is being edited. */ 944 public static class EditedSuggestionInfo { 945 946 /** 947 * The value of the suggestion (before any user edits). 948 */ 949 public final CharSequence originalText; 950 951 /** 952 * The index of the suggestion that is being edited. 953 */ 954 public final int index; 955 EditedSuggestionInfo(CharSequence originalText, int index)956 public EditedSuggestionInfo(CharSequence originalText, int index) { 957 this.originalText = originalText; 958 this.index = index; 959 } 960 } 961 962 /** Listener interface for {@link #addOnSensitivityChangedListener} */ 963 public interface OnSensitivityChangedListener { 964 /** Called when the sensitivity changes */ onSensitivityChanged(@onNull NotificationEntry entry)965 void onSensitivityChanged(@NonNull NotificationEntry entry); 966 } 967 968 /** @see #getDismissState() */ 969 public enum DismissState { 970 /** User has not dismissed this notif or its parent */ 971 NOT_DISMISSED, 972 /** User has dismissed this notif specifically */ 973 DISMISSED, 974 /** User has dismissed this notif's parent (which implicitly dismisses this one as well) */ 975 PARENT_DISMISSED, 976 } 977 978 private static final long LAUNCH_COOLDOWN = 2000; 979 private static final long REMOTE_INPUT_COOLDOWN = 500; 980 private static final long INITIALIZATION_DELAY = 400; 981 private static final long NOT_LAUNCHED_YET = -LAUNCH_COOLDOWN; 982 private static final int COLOR_INVALID = 1; 983 } 984