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