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