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