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 static com.android.systemui.statusbar.notification.collection.NotifCollection.REASON_NOT_CANCELED; 33 34 import static java.util.Objects.requireNonNull; 35 36 import android.annotation.FlaggedApi; 37 import android.app.Flags; 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.app.RemoteInputHistoryItem; 45 import android.content.Context; 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.Log; 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.headsup.PinnedStatus; 69 import com.android.systemui.statusbar.notification.icon.IconPack; 70 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel; 71 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels; 72 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 73 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController; 74 import com.android.systemui.statusbar.notification.row.NotificationGuts; 75 import com.android.systemui.statusbar.notification.row.shared.HeadsUpStatusBarModel; 76 import com.android.systemui.statusbar.notification.row.shared.NotificationContentModel; 77 import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor; 78 import com.android.systemui.statusbar.notification.shared.NotificationBundleUi; 79 import com.android.systemui.util.ListenerSet; 80 81 import java.util.ArrayList; 82 import java.util.List; 83 import java.util.Objects; 84 85 import kotlinx.coroutines.flow.MutableStateFlow; 86 import kotlinx.coroutines.flow.StateFlow; 87 import kotlinx.coroutines.flow.StateFlowKt; 88 89 /** 90 * Represents a notification that the system UI knows about 91 * 92 * Whenever the NotificationManager tells us about the existence of a new notification, we wrap it 93 * in a NotificationEntry. Thus, every notification has an associated NotificationEntry, even if 94 * that notification is never displayed to the user (for example, if it's filtered out for some 95 * reason). 96 * 97 * Entries store information about the current state of the notification. Essentially: 98 * anything that needs to persist or be modifiable even when the notification's views don't 99 * exist. Any other state should be stored on the views/view controllers themselves. 100 * 101 * At the moment, there are many things here that shouldn't be and vice-versa. Hopefully we can 102 * clean this up in the future. 103 */ 104 public final class NotificationEntry extends ListEntry { 105 106 private final String mKey; 107 private StatusBarNotification mSbn; 108 private Ranking mRanking; 109 110 /* 111 * Bookkeeping members 112 */ 113 114 /** List of lifetime extenders that are extending the lifetime of this notification. */ 115 final List<NotifLifetimeExtender> mLifetimeExtenders = new ArrayList<>(); 116 117 /** List of dismiss interceptors that are intercepting the dismissal of this notification. */ 118 final List<NotifDismissInterceptor> mDismissInterceptors = new ArrayList<>(); 119 120 /** 121 * If this notification was cancelled by system server, then the reason that was supplied. 122 * Uncancelled notifications always have REASON_NOT_CANCELED. Note that lifetime-extended 123 * notifications will have this set even though they are still in the active notification set. 124 */ 125 @CancellationReason int mCancellationReason = REASON_NOT_CANCELED; 126 127 /** @see #getDismissState() */ 128 @NonNull private DismissState mDismissState = DismissState.NOT_DISMISSED; 129 130 /* 131 * Old members 132 * TODO: Remove every member beneath this line if possible 133 */ 134 135 private IconPack mIcons = IconPack.buildEmptyPack(null); 136 private boolean interruption; 137 public int targetSdk; 138 private long lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET; 139 public CharSequence remoteInputText; 140 public List<RemoteInputHistoryItem> remoteInputs = null; 141 public String remoteInputMimeType; 142 public Uri remoteInputUri; 143 public ContentInfo remoteInputAttachment; 144 private Notification.BubbleMetadata mBubbleMetadata; 145 146 /** 147 * If {@link RemoteInput#getEditChoicesBeforeSending} is enabled, and the user is 148 * currently editing a choice (smart reply), then this field contains the information about the 149 * suggestion being edited. Otherwise <code>null</code>. 150 */ 151 public EditedSuggestionInfo editedSuggestionInfo; 152 153 private ExpandableNotificationRow row; // the outer expanded view 154 private ExpandableNotificationRowController mRowController; 155 156 private int mCachedContrastColor = COLOR_INVALID; 157 private int mCachedContrastColorIsFor = COLOR_INVALID; 158 private InflationTask mRunningTask = null; 159 public CharSequence remoteInputTextWhenReset; 160 public long lastRemoteInputSent = NOT_LAUNCHED_YET; 161 162 private final MutableStateFlow<CharSequence> mHeadsUpStatusBarText = 163 StateFlowKt.MutableStateFlow(null); 164 private final MutableStateFlow<CharSequence> mHeadsUpStatusBarTextPublic = 165 StateFlowKt.MutableStateFlow(null); 166 167 // indicates when this entry's view was first attached to a window 168 // this value will reset when the view is completely removed from the shade (ie: filtered out) 169 private long initializationTime = -1; 170 171 /** 172 * Has the user sent a reply through this Notification. 173 */ 174 private boolean hasSentReply; 175 176 private final MutableStateFlow<Boolean> mSensitive = StateFlowKt.MutableStateFlow(true); 177 private final ListenerSet<OnSensitivityChangedListener> mOnSensitivityChangedListeners = 178 new ListenerSet<>(); 179 180 private boolean mPulseSupressed; 181 private boolean mIsMarkedForUserTriggeredMovement; 182 private boolean mIsHeadsUpEntry; 183 184 private boolean mHasEverBeenGroupSummary; 185 private boolean mHasEverBeenGroupChild; 186 187 public boolean mRemoteEditImeAnimatingAway; 188 public boolean mRemoteEditImeVisible; 189 private boolean mExpandAnimationRunning; 190 /** 191 * Flag to determine if the entry is blockable by DnD filters 192 */ 193 private boolean mBlockable; 194 195 /** 196 * Whether this notification has ever been a non-sticky HUN. 197 */ 198 private boolean mIsDemoted = false; 199 200 // TODO(b/377565433): Move into NotificationContentModel during/after 201 // NotificationRowContentBinderRefactor. 202 private PromotedNotificationContentModels mPromotedNotificationContentModels; 203 204 /** 205 * True if both 206 * 1) app provided full screen intent but does not have the permission to send it 207 * 2) this notification has never been demoted before 208 */ isStickyAndNotDemoted()209 public boolean isStickyAndNotDemoted() { 210 211 final boolean fsiRequestedButDenied = (getSbn().getNotification().flags 212 & Notification.FLAG_FSI_REQUESTED_BUT_DENIED) != 0; 213 214 if (!fsiRequestedButDenied && !mIsDemoted) { 215 demoteStickyHun(); 216 } 217 return fsiRequestedButDenied && !mIsDemoted; 218 } 219 220 @VisibleForTesting isDemoted()221 public boolean isDemoted() { 222 return mIsDemoted; 223 } 224 225 /** 226 * Make sticky HUN not sticky. 227 */ demoteStickyHun()228 public void demoteStickyHun() { 229 mIsDemoted = true; 230 } 231 232 /** called when entry is currently a summary of a group */ markAsGroupSummary()233 public void markAsGroupSummary() { 234 mHasEverBeenGroupSummary = true; 235 } 236 237 /** whether this entry has ever been marked as a summary */ hasEverBeenGroupSummary()238 public boolean hasEverBeenGroupSummary() { 239 return mHasEverBeenGroupSummary; 240 } 241 242 /** called when entry is currently a child in a group */ markAsGroupChild()243 public void markAsGroupChild() { 244 mHasEverBeenGroupChild = true; 245 } 246 247 /** whether this entry has ever been marked as a child */ hasEverBeenGroupChild()248 public boolean hasEverBeenGroupChild() { 249 return mHasEverBeenGroupChild; 250 } 251 252 /** 253 * @param sbn the StatusBarNotification from system server 254 * @param ranking also from system server 255 * @param creationTime SystemClock.elapsedRealtime of when we were created 256 */ NotificationEntry( @onNull StatusBarNotification sbn, @NonNull Ranking ranking, long creationTime )257 public NotificationEntry( 258 @NonNull StatusBarNotification sbn, 259 @NonNull Ranking ranking, 260 long creationTime 261 ) { 262 super(requireNonNull(requireNonNull(sbn).getKey()), creationTime); 263 264 requireNonNull(ranking); 265 266 mKey = sbn.getKey(); 267 setSbn(sbn); 268 setRanking(ranking); 269 } 270 271 @Override getRepresentativeEntry()272 public NotificationEntry getRepresentativeEntry() { 273 return this; 274 } 275 276 /** The key for this notification. Guaranteed to be immutable and unique */ getKey()277 @NonNull public String getKey() { 278 return mKey; 279 } 280 281 /** 282 * The StatusBarNotification that represents one half of a NotificationEntry (the other half 283 * being the Ranking). This object is swapped out whenever a notification is updated. 284 */ getSbn()285 @NonNull public StatusBarNotification getSbn() { 286 return mSbn; 287 } 288 289 /** 290 * Should only be called by NotificationEntryManager and friends. 291 * TODO: Make this package-private 292 */ setSbn(@onNull StatusBarNotification sbn)293 public void setSbn(@NonNull StatusBarNotification sbn) { 294 requireNonNull(sbn); 295 requireNonNull(sbn.getKey()); 296 297 if (!sbn.getKey().equals(mKey)) { 298 throw new IllegalArgumentException("New key " + sbn.getKey() 299 + " doesn't match existing key " + mKey); 300 } 301 302 mSbn = sbn; 303 mBubbleMetadata = mSbn.getNotification().getBubbleMetadata(); 304 } 305 306 /** 307 * The Ranking that represents one half of a NotificationEntry (the other half being the 308 * StatusBarNotification). This object is swapped out whenever a the ranking is updated (which 309 * generally occurs whenever anything changes in the notification list). 310 */ getRanking()311 public Ranking getRanking() { 312 return mRanking; 313 } 314 315 /** 316 * Should only be called by NotificationEntryManager and friends. 317 * TODO: Make this package-private 318 */ setRanking(@onNull Ranking ranking)319 public void setRanking(@NonNull Ranking ranking) { 320 requireNonNull(ranking); 321 requireNonNull(ranking.getKey()); 322 323 if (!ranking.getKey().equals(mKey)) { 324 throw new IllegalArgumentException("New key " + ranking.getKey() 325 + " doesn't match existing key " + mKey); 326 } 327 328 mRanking = ranking.withAudiblyAlertedInfo(mRanking); 329 updateIsBlockable(); 330 } 331 332 /* 333 * Bookkeeping getters and setters 334 */ 335 336 /** 337 * Set if the user has dismissed this notif but we haven't yet heard back from system server to 338 * confirm the dismissal. 339 */ getDismissState()340 @NonNull public DismissState getDismissState() { 341 return mDismissState; 342 } 343 setDismissState(@onNull DismissState dismissState)344 void setDismissState(@NonNull DismissState dismissState) { 345 mDismissState = requireNonNull(dismissState); 346 } 347 348 /** 349 * True if the notification has been canceled by system server. Usually, such notifications are 350 * immediately removed from the collection, but can sometimes stick around due to lifetime 351 * extenders. 352 */ isCanceled()353 public boolean isCanceled() { 354 return mCancellationReason != REASON_NOT_CANCELED; 355 } 356 getExcludingFilter()357 @Nullable public NotifFilter getExcludingFilter() { 358 return getAttachState().getExcludingFilter(); 359 } 360 getNotifPromoter()361 @Nullable public NotifPromoter getNotifPromoter() { 362 return getAttachState().getPromoter(); 363 } 364 365 /* 366 * Convenience getters for SBN and Ranking members 367 */ 368 getChannel()369 public NotificationChannel getChannel() { 370 return mRanking.getChannel(); 371 } 372 getLastAudiblyAlertedMs()373 public long getLastAudiblyAlertedMs() { 374 return mRanking.getLastAudiblyAlertedMillis(); 375 } 376 isAmbient()377 public boolean isAmbient() { 378 return mRanking.isAmbient(); 379 } 380 getImportance()381 public int getImportance() { 382 return mRanking.getImportance(); 383 } 384 getSnoozeCriteria()385 public List<SnoozeCriterion> getSnoozeCriteria() { 386 return mRanking.getSnoozeCriteria(); 387 } 388 getUserSentiment()389 public int getUserSentiment() { 390 return mRanking.getUserSentiment(); 391 } 392 getSuppressedVisualEffects()393 public int getSuppressedVisualEffects() { 394 return mRanking.getSuppressedVisualEffects(); 395 } 396 397 /** @see Ranking#canBubble() */ canBubble()398 public boolean canBubble() { 399 return mRanking.canBubble(); 400 } 401 getSmartActions()402 public @NonNull List<Notification.Action> getSmartActions() { 403 return mRanking.getSmartActions(); 404 } 405 getSmartReplies()406 public @NonNull List<CharSequence> getSmartReplies() { 407 return mRanking.getSmartReplies(); 408 } 409 410 411 /* 412 * Old methods 413 * 414 * TODO: Remove as many of these as possible 415 */ 416 417 @NonNull getIcons()418 public IconPack getIcons() { 419 return mIcons; 420 } 421 setIcons(@onNull IconPack icons)422 public void setIcons(@NonNull IconPack icons) { 423 mIcons = icons; 424 } 425 setInterruption()426 public void setInterruption() { 427 interruption = true; 428 } 429 hasInterrupted()430 public boolean hasInterrupted() { 431 return interruption; 432 } 433 isBubble()434 public boolean isBubble() { 435 return (mSbn.getNotification().flags & FLAG_BUBBLE) != 0; 436 } 437 438 /** 439 * Returns the data needed for a bubble for this notification, if it exists. 440 */ 441 @Nullable getBubbleMetadata()442 public Notification.BubbleMetadata getBubbleMetadata() { 443 return mBubbleMetadata; 444 } 445 446 /** 447 * Sets bubble metadata for this notification. 448 */ setBubbleMetadata(@ullable Notification.BubbleMetadata metadata)449 public void setBubbleMetadata(@Nullable Notification.BubbleMetadata metadata) { 450 mBubbleMetadata = metadata; 451 } 452 453 /** 454 * Updates the {@link Notification#FLAG_BUBBLE} flag on this notification to indicate 455 * whether it is a bubble or not. If this entry is set to not bubble, or does not have 456 * the required info to bubble, the flag cannot be set to true. 457 * 458 * @param shouldBubble whether this notification should be flagged as a bubble. 459 * @return true if the value changed. 460 */ setFlagBubble(boolean shouldBubble)461 public boolean setFlagBubble(boolean shouldBubble) { 462 boolean wasBubble = isBubble(); 463 if (!shouldBubble) { 464 mSbn.getNotification().flags &= ~FLAG_BUBBLE; 465 } else if (mBubbleMetadata != null && canBubble()) { 466 // wants to be bubble & can bubble, set flag 467 mSbn.getNotification().flags |= FLAG_BUBBLE; 468 } 469 return wasBubble != isBubble(); 470 } 471 getRow()472 public ExpandableNotificationRow getRow() { 473 return row; 474 } 475 476 //TODO: This will go away when we have a way to bind an entry to a row setRow(ExpandableNotificationRow row)477 public void setRow(ExpandableNotificationRow row) { 478 this.row = row; 479 } 480 getRowController()481 public ExpandableNotificationRowController getRowController() { 482 return mRowController; 483 } 484 setRowController(ExpandableNotificationRowController controller)485 public void setRowController(ExpandableNotificationRowController controller) { 486 mRowController = controller; 487 } 488 489 /** 490 * Get the children that are actually attached to this notification's row. 491 * 492 * TODO: Seems like most callers here should be asking a PipelineEntry, not a NotificationEntry 493 */ getAttachedNotifChildren()494 public @Nullable List<NotificationEntry> getAttachedNotifChildren() { 495 if (NotificationBundleUi.isEnabled()) { 496 if (isGroupSummary()) { 497 GroupEntry parent = (GroupEntry) getParent(); 498 return parent != null ? new ArrayList<>(parent.getChildren()) : null; 499 } 500 } else { 501 if (row == null) { 502 return null; 503 } 504 505 List<ExpandableNotificationRow> rowChildren = row.getAttachedChildren(); 506 if (rowChildren == null) { 507 return null; 508 } 509 510 ArrayList<NotificationEntry> children = new ArrayList<>(); 511 for (ExpandableNotificationRow child : rowChildren) { 512 children.add(child.getEntryLegacy()); 513 } 514 515 return children; 516 } 517 return null; 518 } 519 isGroupSummary()520 private boolean isGroupSummary() { 521 if (getParent() == null) { 522 // The entry is not attached, so it doesn't count. 523 return false; 524 } 525 PipelineEntry pipelineEntry = getParent(); 526 if (!(pipelineEntry instanceof GroupEntry groupEntry)) { 527 return false; 528 } 529 530 // If entry is a summary, its parent is a GroupEntry with summary = entry. 531 return groupEntry.getSummary() == this; 532 } 533 notifyFullScreenIntentLaunched()534 public void notifyFullScreenIntentLaunched() { 535 setInterruption(); 536 lastFullScreenIntentLaunchTime = SystemClock.elapsedRealtime(); 537 } 538 hasJustLaunchedFullScreenIntent()539 public boolean hasJustLaunchedFullScreenIntent() { 540 return SystemClock.elapsedRealtime() < lastFullScreenIntentLaunchTime + LAUNCH_COOLDOWN; 541 } 542 hasJustSentRemoteInput()543 public boolean hasJustSentRemoteInput() { 544 return SystemClock.elapsedRealtime() < lastRemoteInputSent + REMOTE_INPUT_COOLDOWN; 545 } 546 hasFinishedInitialization()547 public boolean hasFinishedInitialization() { 548 NotificationBundleUi.assertInLegacyMode(); 549 return initializationTime != -1 550 && SystemClock.elapsedRealtime() > initializationTime + INITIALIZATION_DELAY; 551 } 552 getContrastedColor(Context context, boolean isLowPriority, int backgroundColor)553 public int getContrastedColor(Context context, boolean isLowPriority, 554 int backgroundColor) { 555 int rawColor = isLowPriority ? Notification.COLOR_DEFAULT : 556 mSbn.getNotification().color; 557 if (mCachedContrastColorIsFor == rawColor && mCachedContrastColor != COLOR_INVALID) { 558 return mCachedContrastColor; 559 } 560 final int contrasted = ContrastColorUtil.resolveContrastColor(context, rawColor, 561 backgroundColor); 562 mCachedContrastColorIsFor = rawColor; 563 mCachedContrastColor = contrasted; 564 return mCachedContrastColor; 565 } 566 567 /** 568 * Abort all existing inflation tasks 569 */ abortTask()570 public boolean abortTask() { 571 if (mRunningTask != null) { 572 mRunningTask.abort(); 573 mRunningTask = null; 574 return true; 575 } 576 return false; 577 } 578 setInflationTask(InflationTask abortableTask)579 public void setInflationTask(InflationTask abortableTask) { 580 // abort any existing inflation 581 abortTask(); 582 mRunningTask = abortableTask; 583 } 584 onInflationTaskFinished()585 public void onInflationTaskFinished() { 586 mRunningTask = null; 587 } 588 589 @VisibleForTesting getRunningTask()590 public InflationTask getRunningTask() { 591 return mRunningTask; 592 } 593 onRemoteInputInserted()594 public void onRemoteInputInserted() { 595 lastRemoteInputSent = NOT_LAUNCHED_YET; 596 remoteInputTextWhenReset = null; 597 } 598 setHasSentReply()599 public void setHasSentReply() { 600 hasSentReply = true; 601 } 602 isLastMessageFromReply()603 public boolean isLastMessageFromReply() { 604 if (!hasSentReply) { 605 return false; 606 } 607 Bundle extras = mSbn.getNotification().extras; 608 Parcelable[] replyTexts = 609 extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS); 610 if (!ArrayUtils.isEmpty(replyTexts)) { 611 return true; 612 } 613 List<Message> messages = Message.getMessagesFromBundleArray( 614 extras.getParcelableArray(Notification.EXTRA_MESSAGES)); 615 if (messages != null && !messages.isEmpty()) { 616 Message lastMessage = messages.get(messages.size() -1); 617 618 if (lastMessage != null) { 619 Person senderPerson = lastMessage.getSenderPerson(); 620 if (senderPerson == null) { 621 return true; 622 } 623 Person user = extras.getParcelable( 624 Notification.EXTRA_MESSAGING_PERSON, Person.class); 625 return Objects.equals(user, senderPerson); 626 } 627 } 628 return false; 629 } 630 resetInitializationTime()631 public void resetInitializationTime() { 632 NotificationBundleUi.assertInLegacyMode(); 633 initializationTime = -1; 634 } 635 setInitializationTime(long time)636 public void setInitializationTime(long time) { 637 NotificationBundleUi.assertInLegacyMode(); 638 if (initializationTime == -1) { 639 initializationTime = time; 640 } 641 } 642 sendAccessibilityEvent(int eventType)643 public void sendAccessibilityEvent(int eventType) { 644 if (row != null) { 645 row.sendAccessibilityEvent(eventType); 646 } 647 } 648 649 /** 650 * Used by NotificationMediaManager to determine... things 651 * @return {@code true} if we are a media notification 652 */ isMediaNotification()653 public boolean isMediaNotification() { 654 if (NotificationBundleUi.isEnabled()) { 655 return getSbn().getNotification().isMediaNotification(); 656 } else { 657 if (row == null) return false; 658 659 return row.isMediaRow(); 660 } 661 } 662 containsCustomViews()663 public boolean containsCustomViews() { 664 return getSbn().getNotification().containsCustomViews(); 665 } 666 resetUserExpansion()667 public void resetUserExpansion() { 668 if (row != null) row.resetUserExpansion(); 669 } 670 rowExists()671 public boolean rowExists() { 672 return row != null; 673 } 674 isRowDismissed()675 public boolean isRowDismissed() { 676 return row != null && row.isDismissed(); 677 } 678 isRowRemoved()679 public boolean isRowRemoved() { 680 return row != null && row.isRemoved(); 681 } 682 683 /** 684 * @return {@code true} if the row is null or removed 685 */ isRemoved()686 public boolean isRemoved() { 687 //TODO: recycling invalidates this 688 return row == null || row.isRemoved(); 689 } 690 isRowPinned()691 public boolean isRowPinned() { 692 return getPinnedStatus().isPinned(); 693 } 694 695 /** Returns this notification's current pinned status. */ getPinnedStatus()696 public PinnedStatus getPinnedStatus() { 697 if (row != null) { 698 return row.getPinnedStatus(); 699 } else { 700 return PinnedStatus.NotPinned; 701 } 702 } 703 704 /** 705 * Is this entry pinned and was expanded while doing so 706 */ isPinnedAndExpanded()707 public boolean isPinnedAndExpanded() { 708 return row != null && row.isPinnedAndExpanded(); 709 } 710 setRowPinnedStatus(PinnedStatus pinnedStatus)711 public void setRowPinnedStatus(PinnedStatus pinnedStatus) { 712 if (row != null) row.setPinnedStatus(pinnedStatus); 713 } 714 isRowHeadsUp()715 public boolean isRowHeadsUp() { 716 return row != null && row.isHeadsUp(); 717 } 718 showingPulsing()719 public boolean showingPulsing() { 720 return row != null && row.showingPulsing(); 721 } 722 setHeadsUp(boolean shouldHeadsUp)723 public void setHeadsUp(boolean shouldHeadsUp) { 724 if (row != null) row.setHeadsUp(shouldHeadsUp); 725 } 726 setHeadsUpAnimatingAway(boolean animatingAway)727 public void setHeadsUpAnimatingAway(boolean animatingAway) { 728 if (row != null) row.setHeadsUpAnimatingAway(animatingAway); 729 } 730 mustStayOnScreen()731 public boolean mustStayOnScreen() { 732 return row != null && row.mustStayOnScreen(); 733 } 734 setHeadsUpIsVisible()735 public void setHeadsUpIsVisible() { 736 if (row != null) row.markHeadsUpSeen(); 737 } 738 739 //TODO: i'm imagining a world where this isn't just the row, but I could be rwong getHeadsUpAnimationView()740 public ExpandableNotificationRow getHeadsUpAnimationView() { 741 return row; 742 } 743 setUserLocked(boolean userLocked)744 public void setUserLocked(boolean userLocked) { 745 if (row != null) row.setUserLocked(userLocked); 746 } 747 setUserExpanded(boolean userExpanded, boolean allowChildExpansion)748 public void setUserExpanded(boolean userExpanded, boolean allowChildExpansion) { 749 if (row != null) row.setUserExpanded(userExpanded, allowChildExpansion); 750 } 751 setGroupExpansionChanging(boolean changing)752 public void setGroupExpansionChanging(boolean changing) { 753 if (row != null) row.setGroupExpansionChanging(changing); 754 } 755 notifyHeightChanged(boolean needsAnimation)756 public void notifyHeightChanged(boolean needsAnimation) { 757 if (row != null) row.notifyHeightChanged(needsAnimation); 758 } 759 closeRemoteInput()760 public void closeRemoteInput() { 761 if (row != null) row.closeRemoteInput(); 762 } 763 areChildrenExpanded()764 public boolean areChildrenExpanded() { 765 return row != null && row.areChildrenExpanded(); 766 } 767 getGuts()768 public NotificationGuts getGuts() { 769 if (row != null) return row.getGuts(); 770 return null; 771 } 772 removeRow()773 public void removeRow() { 774 if (row != null) row.setRemoved(); 775 } 776 isSummaryWithChildren()777 public boolean isSummaryWithChildren() { 778 return row != null && row.isSummaryWithChildren(); 779 } 780 onDensityOrFontScaleChanged()781 public void onDensityOrFontScaleChanged() { 782 if (row != null) row.onDensityOrFontScaleChanged(); 783 } 784 areGutsExposed()785 public boolean areGutsExposed() { 786 return row != null && row.getGuts() != null && row.getGuts().isExposed(); 787 } 788 789 /** 790 * @return Whether the notification row is a child of a group notification view; false if the 791 * row is null 792 */ rowIsChildInGroup()793 public boolean rowIsChildInGroup() { 794 return row != null && row.isChildInGroup(); 795 } 796 797 /** 798 * @return Can the underlying notification be cleared? This can be different from whether the 799 * notification can be dismissed in case notifications are sensitive on the lockscreen. 800 */ 801 // TODO: This logic doesn't belong on NotificationEntry. It should be moved to a controller 802 // that can be added as a dependency to any class that needs to answer this question. isClearable()803 public boolean isClearable() { 804 if (!mSbn.isClearable()) { 805 return false; 806 } 807 808 List<NotificationEntry> children = getAttachedNotifChildren(); 809 if (children != null && children.size() > 0) { 810 for (int i = 0; i < children.size(); i++) { 811 NotificationEntry child = children.get(i); 812 if (!child.getSbn().isClearable()) { 813 return false; 814 } 815 } 816 } 817 return true; 818 } 819 820 /** 821 * Determines whether the NotificationEntry is dismissable based on the Notification flags and 822 * the given state. It doesn't recurse children or depend on the view attach state. 823 * 824 * @param isLocked if the device is locked or unlocked 825 * @return true if this NotificationEntry is dismissable. 826 */ isDismissableForState(boolean isLocked)827 public boolean isDismissableForState(boolean isLocked) { 828 if (mSbn.isNonDismissable()) { 829 // don't dismiss exempted Notifications 830 return false; 831 } 832 // don't dismiss ongoing Notifications when the device is locked 833 return !mSbn.isOngoing() || !isLocked; 834 } 835 836 @VisibleForTesting isExemptFromDndVisualSuppression()837 boolean isExemptFromDndVisualSuppression() { 838 if (isNotificationBlockedByPolicy(mSbn.getNotification())) { 839 return false; 840 } 841 842 if (mSbn.getNotification().isFgsOrUij()) { 843 return true; 844 } 845 if (mSbn.getNotification().isMediaNotification()) { 846 return true; 847 } 848 if (!isBlockable()) { 849 return true; 850 } 851 return false; 852 } 853 854 /** 855 * Returns whether the NotificationEntry is promoted ongoing. 856 */ 857 @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) isOngoingPromoted()858 public boolean isOngoingPromoted() { 859 return mSbn.getNotification().isPromotedOngoing(); 860 } 861 862 /** 863 * Returns whether this row is considered blockable (i.e. it's not a system notif 864 * or is not in an allowList). 865 */ isBlockable()866 public boolean isBlockable() { 867 return mBlockable; 868 } 869 updateIsBlockable()870 private void updateIsBlockable() { 871 if (getChannel() == null) { 872 mBlockable = false; 873 return; 874 } 875 if (getChannel().isImportanceLockedByCriticalDeviceFunction() 876 && !getChannel().isBlockable()) { 877 mBlockable = false; 878 return; 879 } 880 mBlockable = true; 881 } 882 shouldSuppressVisualEffect(int effect)883 private boolean shouldSuppressVisualEffect(int effect) { 884 if (isExemptFromDndVisualSuppression()) { 885 return false; 886 } 887 return (getSuppressedVisualEffects() & effect) != 0; 888 } 889 890 /** 891 * Returns whether {@link Policy#SUPPRESSED_EFFECT_FULL_SCREEN_INTENT} 892 * is set for this entry. 893 */ shouldSuppressFullScreenIntent()894 public boolean shouldSuppressFullScreenIntent() { 895 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_FULL_SCREEN_INTENT); 896 } 897 898 /** 899 * Returns whether {@link Policy#SUPPRESSED_EFFECT_PEEK} 900 * is set for this entry. 901 */ shouldSuppressPeek()902 public boolean shouldSuppressPeek() { 903 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_PEEK); 904 } 905 906 /** 907 * Returns whether {@link Policy#SUPPRESSED_EFFECT_STATUS_BAR} 908 * is set for this entry. 909 */ shouldSuppressStatusBar()910 public boolean shouldSuppressStatusBar() { 911 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_STATUS_BAR); 912 } 913 914 /** 915 * Returns whether {@link Policy#SUPPRESSED_EFFECT_AMBIENT} 916 * is set for this entry. 917 */ shouldSuppressAmbient()918 public boolean shouldSuppressAmbient() { 919 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_AMBIENT); 920 } 921 922 /** 923 * Returns whether {@link Policy#SUPPRESSED_EFFECT_NOTIFICATION_LIST} 924 * is set for this entry. 925 */ shouldSuppressNotificationList()926 public boolean shouldSuppressNotificationList() { 927 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_NOTIFICATION_LIST); 928 } 929 930 931 /** 932 * Returns whether {@link Policy#SUPPRESSED_EFFECT_BADGE} 933 * is set for this entry. This badge is not an app badge, but rather an indicator of "unseen" 934 * content. Typically this is referred to as a "dot" internally in Launcher & SysUI code. 935 */ shouldSuppressNotificationDot()936 public boolean shouldSuppressNotificationDot() { 937 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_BADGE); 938 } 939 940 /** 941 * Categories that are explicitly called out on DND settings screens are always blocked, if 942 * DND has flagged them, even if they are foreground or system notifications that might 943 * otherwise visually bypass DND. 944 */ isNotificationBlockedByPolicy(Notification n)945 private static boolean isNotificationBlockedByPolicy(Notification n) { 946 return isCategory(CATEGORY_CALL, n) 947 || isCategory(CATEGORY_MESSAGE, n) 948 || isCategory(CATEGORY_ALARM, n) 949 || isCategory(CATEGORY_EVENT, n) 950 || isCategory(CATEGORY_REMINDER, n); 951 } 952 isCategory(String category, Notification n)953 private static boolean isCategory(String category, Notification n) { 954 return Objects.equals(n.category, category); 955 } 956 957 /** @see #setSensitive(boolean, boolean) */ isSensitive()958 public StateFlow<Boolean> isSensitive() { 959 return mSensitive; 960 } 961 962 /** 963 * Set this notification to be sensitive. 964 * 965 * @param sensitive true if the content of this notification is sensitive right now 966 * @param deviceSensitive true if the device in general is sensitive right now 967 */ setSensitive(boolean sensitive, boolean deviceSensitive)968 public void setSensitive(boolean sensitive, boolean deviceSensitive) { 969 getRow().setSensitive(sensitive, deviceSensitive); 970 if (sensitive != mSensitive.getValue()) { 971 mSensitive.setValue(sensitive); 972 for (NotificationEntry.OnSensitivityChangedListener listener : 973 mOnSensitivityChangedListeners) { 974 listener.onSensitivityChanged(this); 975 } 976 } 977 } 978 979 /** Add a listener to be notified when the entry's sensitivity changes. */ addOnSensitivityChangedListener(OnSensitivityChangedListener listener)980 public void addOnSensitivityChangedListener(OnSensitivityChangedListener listener) { 981 mOnSensitivityChangedListeners.addIfAbsent(listener); 982 } 983 984 /** Remove a listener that was registered above. */ removeOnSensitivityChangedListener(OnSensitivityChangedListener listener)985 public void removeOnSensitivityChangedListener(OnSensitivityChangedListener listener) { 986 mOnSensitivityChangedListeners.remove(listener); 987 } 988 989 /** @see #setHeadsUpStatusBarText(CharSequence) */ 990 @NonNull getHeadsUpStatusBarText()991 public StateFlow<CharSequence> getHeadsUpStatusBarText() { 992 return mHeadsUpStatusBarText; 993 } 994 995 /** 996 * Sets the text to be displayed on the StatusBar, when this notification is the top pinned 997 * heads up. 998 */ setHeadsUpStatusBarText(CharSequence headsUpStatusBarText)999 public void setHeadsUpStatusBarText(CharSequence headsUpStatusBarText) { 1000 NotificationRowContentBinderRefactor.assertInLegacyMode(); 1001 this.mHeadsUpStatusBarText.setValue(headsUpStatusBarText); 1002 } 1003 1004 /** @see #setHeadsUpStatusBarTextPublic(CharSequence) */ 1005 @NonNull getHeadsUpStatusBarTextPublic()1006 public StateFlow<CharSequence> getHeadsUpStatusBarTextPublic() { 1007 return mHeadsUpStatusBarTextPublic; 1008 } 1009 1010 /** 1011 * Sets the text to be displayed on the StatusBar, when this notification is the top pinned 1012 * heads up, and its content is sensitive right now. 1013 */ setHeadsUpStatusBarTextPublic(CharSequence headsUpStatusBarTextPublic)1014 public void setHeadsUpStatusBarTextPublic(CharSequence headsUpStatusBarTextPublic) { 1015 NotificationRowContentBinderRefactor.assertInLegacyMode(); 1016 this.mHeadsUpStatusBarTextPublic.setValue(headsUpStatusBarTextPublic); 1017 } 1018 isPulseSuppressed()1019 public boolean isPulseSuppressed() { 1020 return mPulseSupressed; 1021 } 1022 setPulseSuppressed(boolean suppressed)1023 public void setPulseSuppressed(boolean suppressed) { 1024 mPulseSupressed = suppressed; 1025 } 1026 1027 /** Whether or not this entry has been marked for a user-triggered movement. */ isMarkedForUserTriggeredMovement()1028 public boolean isMarkedForUserTriggeredMovement() { 1029 return mIsMarkedForUserTriggeredMovement; 1030 } 1031 1032 /** 1033 * Mark this entry for movement triggered by a user action (ex: changing the priority of a 1034 * conversation). This can then be used for custom animations. 1035 */ markForUserTriggeredMovement(boolean marked)1036 public void markForUserTriggeredMovement(boolean marked) { 1037 mIsMarkedForUserTriggeredMovement = marked; 1038 } 1039 1040 private boolean mSeenInShade = false; 1041 setSeenInShade(boolean seen)1042 public void setSeenInShade(boolean seen) { 1043 mSeenInShade = seen; 1044 } 1045 isSeenInShade()1046 public boolean isSeenInShade() { 1047 return mSeenInShade; 1048 } 1049 setIsHeadsUpEntry(boolean isHeadsUpEntry)1050 public void setIsHeadsUpEntry(boolean isHeadsUpEntry) { 1051 mIsHeadsUpEntry = isHeadsUpEntry; 1052 } 1053 isHeadsUpEntry()1054 public boolean isHeadsUpEntry() { 1055 return mIsHeadsUpEntry; 1056 } 1057 1058 /** Set whether this notification is currently used to animate a launch. */ setExpandAnimationRunning(boolean expandAnimationRunning)1059 public void setExpandAnimationRunning(boolean expandAnimationRunning) { 1060 mExpandAnimationRunning = expandAnimationRunning; 1061 } 1062 1063 /** Whether this notification is currently used to animate a launch. */ isExpandAnimationRunning()1064 public boolean isExpandAnimationRunning() { 1065 return mExpandAnimationRunning; 1066 } 1067 1068 /** 1069 * @return NotificationStyle 1070 */ getNotificationStyle()1071 public String getNotificationStyle() { 1072 if (isSummaryWithChildren()) { 1073 return "summary"; 1074 } 1075 1076 final Class<? extends Notification.Style> style = 1077 getSbn().getNotification().getNotificationStyle(); 1078 return style == null ? "nostyle" : style.getSimpleName(); 1079 } 1080 1081 /** 1082 * Return {@code true} if notification's visibility is {@link Notification.VISIBILITY_PRIVATE} 1083 */ isNotificationVisibilityPrivate()1084 public boolean isNotificationVisibilityPrivate() { 1085 return getSbn().getNotification().visibility == Notification.VISIBILITY_PRIVATE; 1086 } 1087 1088 /** 1089 * Return {@code true} if notification's channel lockscreen visibility is 1090 * {@link Notification.VISIBILITY_PRIVATE} 1091 */ isChannelVisibilityPrivate()1092 public boolean isChannelVisibilityPrivate() { 1093 return getRanking().getChannel() != null 1094 && getRanking().getChannel().getLockscreenVisibility() 1095 == Notification.VISIBILITY_PRIVATE; 1096 } 1097 1098 /** Set the content generated by the notification inflater. */ setContentModel(NotificationContentModel contentModel)1099 public void setContentModel(NotificationContentModel contentModel) { 1100 if (NotificationRowContentBinderRefactor.isUnexpectedlyInLegacyMode()) return; 1101 HeadsUpStatusBarModel headsUpStatusBarModel = contentModel.getHeadsUpStatusBarModel(); 1102 this.mHeadsUpStatusBarText.setValue(headsUpStatusBarModel.getPrivateText()); 1103 this.mHeadsUpStatusBarTextPublic.setValue(headsUpStatusBarModel.getPublicText()); 1104 } 1105 1106 /** 1107 * Gets the content needed to render this notification as a promoted notification on various 1108 * surfaces (like status bar chips and AOD). 1109 */ getPromotedNotificationContentModels()1110 public PromotedNotificationContentModels getPromotedNotificationContentModels() { 1111 if (PromotedNotificationContentModel.featureFlagEnabled()) { 1112 return mPromotedNotificationContentModels; 1113 } else { 1114 Log.wtf(TAG, "getting promoted content without feature flag enabled", new Throwable()); 1115 return null; 1116 } 1117 } 1118 1119 /** 1120 * Returns whether the NotificationEntry is promoted ongoing. 1121 */ 1122 @FlaggedApi(Flags.FLAG_API_RICH_ONGOING) isPromotedOngoing()1123 public boolean isPromotedOngoing() { 1124 return PromotedNotificationContentModel.isPromotedForStatusBarChip(mSbn.getNotification()); 1125 } 1126 1127 /** 1128 * Sets the content needed to render this notification as a promoted notification on various 1129 * surfaces (like status bar chips and AOD). 1130 */ setPromotedNotificationContentModels( @ullable PromotedNotificationContentModels promotedNotificationContentModels)1131 public void setPromotedNotificationContentModels( 1132 @Nullable PromotedNotificationContentModels promotedNotificationContentModels) { 1133 if (PromotedNotificationContentModel.featureFlagEnabled()) { 1134 this.mPromotedNotificationContentModels = promotedNotificationContentModels; 1135 } else { 1136 Log.wtf(TAG, "setting promoted content without feature flag enabled", new Throwable()); 1137 } 1138 } 1139 1140 /** Information about a suggestion that is being edited. */ 1141 public static class EditedSuggestionInfo { 1142 1143 /** 1144 * The value of the suggestion (before any user edits). 1145 */ 1146 public final CharSequence originalText; 1147 1148 /** 1149 * The index of the suggestion that is being edited. 1150 */ 1151 public final int index; 1152 EditedSuggestionInfo(CharSequence originalText, int index)1153 public EditedSuggestionInfo(CharSequence originalText, int index) { 1154 this.originalText = originalText; 1155 this.index = index; 1156 } 1157 } 1158 1159 /** Listener interface for {@link #addOnSensitivityChangedListener} */ 1160 public interface OnSensitivityChangedListener { 1161 /** Called when the sensitivity changes */ onSensitivityChanged(@onNull NotificationEntry entry)1162 void onSensitivityChanged(@NonNull NotificationEntry entry); 1163 } 1164 1165 /** @see #getDismissState() */ 1166 public enum DismissState { 1167 /** User has not dismissed this notif or its parent */ 1168 NOT_DISMISSED, 1169 /** User has dismissed this notif specifically */ 1170 DISMISSED, 1171 /** User has dismissed this notif's parent (which implicitly dismisses this one as well) */ 1172 PARENT_DISMISSED, 1173 } 1174 1175 private static final long LAUNCH_COOLDOWN = 2000; 1176 private static final long REMOTE_INPUT_COOLDOWN = 500; 1177 private static final long INITIALIZATION_DELAY = 400; 1178 private static final long NOT_LAUNCHED_YET = -LAUNCH_COOLDOWN; 1179 private static final int COLOR_INVALID = 1; 1180 1181 private static final String TAG = "NotificationEntry"; 1182 } 1183