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