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_FULL_SCREEN_INTENT; 27 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST; 28 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK; 29 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR; 30 31 import android.annotation.NonNull; 32 import android.app.Notification; 33 import android.app.NotificationChannel; 34 import android.app.NotificationManager.Policy; 35 import android.app.Person; 36 import android.content.Context; 37 import android.graphics.drawable.Icon; 38 import android.os.Bundle; 39 import android.os.Parcelable; 40 import android.os.SystemClock; 41 import android.service.notification.NotificationListenerService; 42 import android.service.notification.SnoozeCriterion; 43 import android.service.notification.StatusBarNotification; 44 import android.text.TextUtils; 45 import android.util.ArraySet; 46 import android.view.View; 47 import android.widget.ImageView; 48 49 import androidx.annotation.Nullable; 50 51 import com.android.internal.annotations.VisibleForTesting; 52 import com.android.internal.statusbar.StatusBarIcon; 53 import com.android.internal.util.ArrayUtils; 54 import com.android.internal.util.ContrastColorUtil; 55 import com.android.systemui.R; 56 import com.android.systemui.statusbar.InflationTask; 57 import com.android.systemui.statusbar.StatusBarIconView; 58 import com.android.systemui.statusbar.notification.InflationException; 59 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow; 60 import com.android.systemui.statusbar.notification.row.NotificationContentInflater.InflationFlag; 61 import com.android.systemui.statusbar.notification.row.NotificationGuts; 62 63 import java.util.ArrayList; 64 import java.util.Collections; 65 import java.util.List; 66 import java.util.Objects; 67 68 /** 69 * Represents a notification that the system UI knows about 70 * 71 * Whenever the NotificationManager tells us about the existence of a new notification, we wrap it 72 * in a NotificationEntry. Thus, every notification has an associated NotificationEntry, even if 73 * that notification is never displayed to the user (for example, if it's filtered out for some 74 * reason). 75 * 76 * Entries store information about the current state of the notification. Essentially: 77 * anything that needs to persist or be modifiable even when the notification's views don't 78 * exist. Any other state should be stored on the views/view controllers themselves. 79 * 80 * At the moment, there are many things here that shouldn't be and vice-versa. Hopefully we can 81 * clean this up in the future. 82 */ 83 public final class NotificationEntry { 84 private static final long LAUNCH_COOLDOWN = 2000; 85 private static final long REMOTE_INPUT_COOLDOWN = 500; 86 private static final long INITIALIZATION_DELAY = 400; 87 private static final long NOT_LAUNCHED_YET = -LAUNCH_COOLDOWN; 88 private static final int COLOR_INVALID = 1; 89 public final String key; 90 public StatusBarNotification notification; 91 public NotificationChannel channel; 92 public long lastAudiblyAlertedMs; 93 public boolean noisy; 94 public boolean ambient; 95 public int importance; 96 public StatusBarIconView icon; 97 public StatusBarIconView expandedIcon; 98 public StatusBarIconView centeredIcon; 99 private boolean interruption; 100 public boolean autoRedacted; // whether the redacted notification was generated by us 101 public int targetSdk; 102 private long lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET; 103 public CharSequence remoteInputText; 104 public List<SnoozeCriterion> snoozeCriteria; 105 public int userSentiment = NotificationListenerService.Ranking.USER_SENTIMENT_NEUTRAL; 106 /** Smart Actions provided by the NotificationAssistantService. */ 107 @NonNull 108 public List<Notification.Action> systemGeneratedSmartActions = Collections.emptyList(); 109 /** Smart replies provided by the NotificationAssistantService. */ 110 @NonNull 111 public CharSequence[] systemGeneratedSmartReplies = new CharSequence[0]; 112 113 /** 114 * If {@link android.app.RemoteInput#getEditChoicesBeforeSending} is enabled, and the user is 115 * currently editing a choice (smart reply), then this field contains the information about the 116 * suggestion being edited. Otherwise <code>null</code>. 117 */ 118 public EditedSuggestionInfo editedSuggestionInfo; 119 120 @VisibleForTesting 121 public int suppressedVisualEffects; 122 public boolean suspended; 123 124 private NotificationEntry parent; // our parent (if we're in a group) 125 private ExpandableNotificationRow row; // the outer expanded view 126 127 private int mCachedContrastColor = COLOR_INVALID; 128 private int mCachedContrastColorIsFor = COLOR_INVALID; 129 private InflationTask mRunningTask = null; 130 private Throwable mDebugThrowable; 131 public CharSequence remoteInputTextWhenReset; 132 public long lastRemoteInputSent = NOT_LAUNCHED_YET; 133 public ArraySet<Integer> mActiveAppOps = new ArraySet<>(3); 134 public CharSequence headsUpStatusBarText; 135 public CharSequence headsUpStatusBarTextPublic; 136 137 private long initializationTime = -1; 138 139 /** 140 * Whether or not this row represents a system notification. Note that if this is 141 * {@code null}, that means we were either unable to retrieve the info or have yet to 142 * retrieve the info. 143 */ 144 public Boolean mIsSystemNotification; 145 146 /** 147 * Has the user sent a reply through this Notification. 148 */ 149 private boolean hasSentReply; 150 151 /** 152 * Whether this notification has been approved globally, at the app level, and at the channel 153 * level for bubbling. 154 */ 155 public boolean canBubble; 156 157 /** 158 * Whether this notification should be shown in the shade when it is also displayed as a bubble. 159 * 160 * <p>When a notification is a bubble we don't show it in the shade once the bubble has been 161 * expanded</p> 162 */ 163 private boolean mShowInShadeWhenBubble; 164 165 /** 166 * Whether the user has dismissed this notification when it was in bubble form. 167 */ 168 private boolean mUserDismissedBubble; 169 170 /** 171 * Whether this notification is shown to the user as a high priority notification: visible on 172 * the lock screen/status bar and in the top section in the shade. 173 */ 174 private boolean mHighPriority; 175 176 private boolean mIsTopBucket; 177 NotificationEntry(StatusBarNotification n)178 public NotificationEntry(StatusBarNotification n) { 179 this(n, null); 180 } 181 NotificationEntry( StatusBarNotification n, @Nullable NotificationListenerService.Ranking ranking)182 public NotificationEntry( 183 StatusBarNotification n, 184 @Nullable NotificationListenerService.Ranking ranking) { 185 this.key = n.getKey(); 186 this.notification = n; 187 if (ranking != null) { 188 populateFromRanking(ranking); 189 } 190 } 191 populateFromRanking(@onNull NotificationListenerService.Ranking ranking)192 public void populateFromRanking(@NonNull NotificationListenerService.Ranking ranking) { 193 channel = ranking.getChannel(); 194 lastAudiblyAlertedMs = ranking.getLastAudiblyAlertedMillis(); 195 importance = ranking.getImportance(); 196 ambient = ranking.isAmbient(); 197 snoozeCriteria = ranking.getSnoozeCriteria(); 198 userSentiment = ranking.getUserSentiment(); 199 systemGeneratedSmartActions = ranking.getSmartActions() == null 200 ? Collections.emptyList() : ranking.getSmartActions(); 201 systemGeneratedSmartReplies = ranking.getSmartReplies() == null 202 ? new CharSequence[0] 203 : ranking.getSmartReplies().toArray(new CharSequence[0]); 204 suppressedVisualEffects = ranking.getSuppressedVisualEffects(); 205 suspended = ranking.isSuspended(); 206 canBubble = ranking.canBubble(); 207 } 208 setInterruption()209 public void setInterruption() { 210 interruption = true; 211 } 212 hasInterrupted()213 public boolean hasInterrupted() { 214 return interruption; 215 } 216 isHighPriority()217 public boolean isHighPriority() { 218 return mHighPriority; 219 } 220 setIsHighPriority(boolean highPriority)221 public void setIsHighPriority(boolean highPriority) { 222 this.mHighPriority = highPriority; 223 } 224 225 /** 226 * @return True if the notif should appear in the "top" or "important" section of notifications 227 * (as opposed to the "bottom" or "silent" section). This is usually the same as 228 * {@link #isHighPriority()}, but there are certain exceptions, such as media notifs. 229 */ isTopBucket()230 public boolean isTopBucket() { 231 return mIsTopBucket; 232 } setIsTopBucket(boolean isTopBucket)233 public void setIsTopBucket(boolean isTopBucket) { 234 mIsTopBucket = isTopBucket; 235 } 236 isBubble()237 public boolean isBubble() { 238 return (notification.getNotification().flags & FLAG_BUBBLE) != 0; 239 } 240 setBubbleDismissed(boolean userDismissed)241 public void setBubbleDismissed(boolean userDismissed) { 242 mUserDismissedBubble = userDismissed; 243 } 244 isBubbleDismissed()245 public boolean isBubbleDismissed() { 246 return mUserDismissedBubble; 247 } 248 249 /** 250 * Sets whether this notification should be shown in the shade when it is also displayed as a 251 * bubble. 252 */ setShowInShadeWhenBubble(boolean showInShade)253 public void setShowInShadeWhenBubble(boolean showInShade) { 254 mShowInShadeWhenBubble = showInShade; 255 } 256 257 /** 258 * Whether this notification should be shown in the shade when it is also displayed as a 259 * bubble. 260 */ showInShadeWhenBubble()261 public boolean showInShadeWhenBubble() { 262 // We always show it in the shade if non-clearable 263 return !isRowDismissed() && (!isClearable() || mShowInShadeWhenBubble); 264 } 265 266 /** 267 * Returns the data needed for a bubble for this notification, if it exists. 268 */ getBubbleMetadata()269 public Notification.BubbleMetadata getBubbleMetadata() { 270 return notification.getNotification().getBubbleMetadata(); 271 } 272 273 /** 274 * Resets the notification entry to be re-used. 275 */ reset()276 public void reset() { 277 if (row != null) { 278 row.reset(); 279 } 280 } 281 getRow()282 public ExpandableNotificationRow getRow() { 283 return row; 284 } 285 286 //TODO: This will go away when we have a way to bind an entry to a row setRow(ExpandableNotificationRow row)287 public void setRow(ExpandableNotificationRow row) { 288 this.row = row; 289 } 290 291 @Nullable getChildren()292 public List<NotificationEntry> getChildren() { 293 if (row == null) { 294 return null; 295 } 296 297 List<ExpandableNotificationRow> rowChildren = row.getNotificationChildren(); 298 if (rowChildren == null) { 299 return null; 300 } 301 302 ArrayList<NotificationEntry> children = new ArrayList<>(); 303 for (ExpandableNotificationRow child : rowChildren) { 304 children.add(child.getEntry()); 305 } 306 307 return children; 308 } 309 notifyFullScreenIntentLaunched()310 public void notifyFullScreenIntentLaunched() { 311 setInterruption(); 312 lastFullScreenIntentLaunchTime = SystemClock.elapsedRealtime(); 313 } 314 hasJustLaunchedFullScreenIntent()315 public boolean hasJustLaunchedFullScreenIntent() { 316 return SystemClock.elapsedRealtime() < lastFullScreenIntentLaunchTime + LAUNCH_COOLDOWN; 317 } 318 hasJustSentRemoteInput()319 public boolean hasJustSentRemoteInput() { 320 return SystemClock.elapsedRealtime() < lastRemoteInputSent + REMOTE_INPUT_COOLDOWN; 321 } 322 hasFinishedInitialization()323 public boolean hasFinishedInitialization() { 324 return initializationTime == -1 325 || SystemClock.elapsedRealtime() > initializationTime + INITIALIZATION_DELAY; 326 } 327 328 /** 329 * Create the icons for a notification 330 * @param context the context to create the icons with 331 * @param sbn the notification 332 * @throws InflationException Exception if required icons are not valid or specified 333 */ createIcons(Context context, StatusBarNotification sbn)334 public void createIcons(Context context, StatusBarNotification sbn) 335 throws InflationException { 336 Notification n = sbn.getNotification(); 337 final Icon smallIcon = n.getSmallIcon(); 338 if (smallIcon == null) { 339 throw new InflationException("No small icon in notification from " 340 + sbn.getPackageName()); 341 } 342 343 // Construct the icon. 344 icon = new StatusBarIconView(context, 345 sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn); 346 icon.setScaleType(ImageView.ScaleType.CENTER_INSIDE); 347 348 // Construct the expanded icon. 349 expandedIcon = new StatusBarIconView(context, 350 sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn); 351 expandedIcon.setScaleType(ImageView.ScaleType.CENTER_INSIDE); 352 353 final StatusBarIcon ic = new StatusBarIcon( 354 sbn.getUser(), 355 sbn.getPackageName(), 356 smallIcon, 357 n.iconLevel, 358 n.number, 359 StatusBarIconView.contentDescForNotification(context, n)); 360 361 if (!icon.set(ic) || !expandedIcon.set(ic)) { 362 icon = null; 363 expandedIcon = null; 364 centeredIcon = null; 365 throw new InflationException("Couldn't create icon: " + ic); 366 } 367 expandedIcon.setVisibility(View.INVISIBLE); 368 expandedIcon.setOnVisibilityChangedListener( 369 newVisibility -> { 370 if (row != null) { 371 row.setIconsVisible(newVisibility != View.VISIBLE); 372 } 373 }); 374 375 // Construct the centered icon 376 if (notification.getNotification().isMediaNotification()) { 377 centeredIcon = new StatusBarIconView(context, 378 sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn); 379 centeredIcon.setScaleType(ImageView.ScaleType.CENTER_INSIDE); 380 381 if (!centeredIcon.set(ic)) { 382 centeredIcon = null; 383 throw new InflationException("Couldn't update centered icon: " + ic); 384 } 385 } 386 } 387 setIconTag(int key, Object tag)388 public void setIconTag(int key, Object tag) { 389 if (icon != null) { 390 icon.setTag(key, tag); 391 expandedIcon.setTag(key, tag); 392 } 393 394 if (centeredIcon != null) { 395 centeredIcon.setTag(key, tag); 396 } 397 } 398 399 /** 400 * Update the notification icons. 401 * 402 * @param context the context to create the icons with. 403 * @param sbn the notification to read the icon from. 404 * @throws InflationException Exception if required icons are not valid or specified 405 */ updateIcons(Context context, StatusBarNotification sbn)406 public void updateIcons(Context context, StatusBarNotification sbn) 407 throws InflationException { 408 if (icon != null) { 409 // Update the icon 410 Notification n = sbn.getNotification(); 411 final StatusBarIcon ic = new StatusBarIcon( 412 notification.getUser(), 413 notification.getPackageName(), 414 n.getSmallIcon(), 415 n.iconLevel, 416 n.number, 417 StatusBarIconView.contentDescForNotification(context, n)); 418 icon.setNotification(sbn); 419 expandedIcon.setNotification(sbn); 420 if (!icon.set(ic) || !expandedIcon.set(ic)) { 421 throw new InflationException("Couldn't update icon: " + ic); 422 } 423 424 if (centeredIcon != null) { 425 centeredIcon.setNotification(sbn); 426 if (!centeredIcon.set(ic)) { 427 throw new InflationException("Couldn't update centered icon: " + ic); 428 } 429 } 430 } 431 } 432 getContrastedColor(Context context, boolean isLowPriority, int backgroundColor)433 public int getContrastedColor(Context context, boolean isLowPriority, 434 int backgroundColor) { 435 int rawColor = isLowPriority ? Notification.COLOR_DEFAULT : 436 notification.getNotification().color; 437 if (mCachedContrastColorIsFor == rawColor && mCachedContrastColor != COLOR_INVALID) { 438 return mCachedContrastColor; 439 } 440 final int contrasted = ContrastColorUtil.resolveContrastColor(context, rawColor, 441 backgroundColor); 442 mCachedContrastColorIsFor = rawColor; 443 mCachedContrastColor = contrasted; 444 return mCachedContrastColor; 445 } 446 447 /** 448 * Returns our best guess for the most relevant text summary of the latest update to this 449 * notification, based on its type. Returns null if there should not be an update message. 450 */ getUpdateMessage(Context context)451 public CharSequence getUpdateMessage(Context context) { 452 final Notification underlyingNotif = notification.getNotification(); 453 final Class<? extends Notification.Style> style = underlyingNotif.getNotificationStyle(); 454 455 try { 456 if (Notification.BigTextStyle.class.equals(style)) { 457 // Return the big text, it is big so probably important. If it's not there use the 458 // normal text. 459 CharSequence bigText = 460 underlyingNotif.extras.getCharSequence(Notification.EXTRA_BIG_TEXT); 461 return !TextUtils.isEmpty(bigText) 462 ? bigText 463 : underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT); 464 } else if (Notification.MessagingStyle.class.equals(style)) { 465 final List<Notification.MessagingStyle.Message> messages = 466 Notification.MessagingStyle.Message.getMessagesFromBundleArray( 467 (Parcelable[]) underlyingNotif.extras.get( 468 Notification.EXTRA_MESSAGES)); 469 470 final Notification.MessagingStyle.Message latestMessage = 471 Notification.MessagingStyle.findLatestIncomingMessage(messages); 472 473 if (latestMessage != null) { 474 final CharSequence personName = latestMessage.getSenderPerson() != null 475 ? latestMessage.getSenderPerson().getName() 476 : null; 477 478 // Prepend the sender name if available since group chats also use messaging 479 // style. 480 if (!TextUtils.isEmpty(personName)) { 481 return context.getResources().getString( 482 R.string.notification_summary_message_format, 483 personName, 484 latestMessage.getText()); 485 } else { 486 return latestMessage.getText(); 487 } 488 } 489 } else if (Notification.InboxStyle.class.equals(style)) { 490 CharSequence[] lines = 491 underlyingNotif.extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES); 492 493 // Return the last line since it should be the most recent. 494 if (lines != null && lines.length > 0) { 495 return lines[lines.length - 1]; 496 } 497 } else if (Notification.MediaStyle.class.equals(style)) { 498 // Return nothing, media updates aren't typically useful as a text update. 499 return null; 500 } else { 501 // Default to text extra. 502 return underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT); 503 } 504 } catch (ClassCastException | NullPointerException | ArrayIndexOutOfBoundsException e) { 505 // No use crashing, we'll just return null and the caller will assume there's no update 506 // message. 507 e.printStackTrace(); 508 } 509 510 return null; 511 } 512 513 /** 514 * Abort all existing inflation tasks 515 */ abortTask()516 public void abortTask() { 517 if (mRunningTask != null) { 518 mRunningTask.abort(); 519 mRunningTask = null; 520 } 521 } 522 setInflationTask(InflationTask abortableTask)523 public void setInflationTask(InflationTask abortableTask) { 524 // abort any existing inflation 525 InflationTask existing = mRunningTask; 526 abortTask(); 527 mRunningTask = abortableTask; 528 if (existing != null && mRunningTask != null) { 529 mRunningTask.supersedeTask(existing); 530 } 531 } 532 onInflationTaskFinished()533 public void onInflationTaskFinished() { 534 mRunningTask = null; 535 } 536 537 @VisibleForTesting getRunningTask()538 public InflationTask getRunningTask() { 539 return mRunningTask; 540 } 541 542 /** 543 * Set a throwable that is used for debugging 544 * 545 * @param debugThrowable the throwable to save 546 */ setDebugThrowable(Throwable debugThrowable)547 public void setDebugThrowable(Throwable debugThrowable) { 548 mDebugThrowable = debugThrowable; 549 } 550 getDebugThrowable()551 public Throwable getDebugThrowable() { 552 return mDebugThrowable; 553 } 554 onRemoteInputInserted()555 public void onRemoteInputInserted() { 556 lastRemoteInputSent = NOT_LAUNCHED_YET; 557 remoteInputTextWhenReset = null; 558 } 559 setHasSentReply()560 public void setHasSentReply() { 561 hasSentReply = true; 562 } 563 isLastMessageFromReply()564 public boolean isLastMessageFromReply() { 565 if (!hasSentReply) { 566 return false; 567 } 568 Bundle extras = notification.getNotification().extras; 569 CharSequence[] replyTexts = extras.getCharSequenceArray( 570 Notification.EXTRA_REMOTE_INPUT_HISTORY); 571 if (!ArrayUtils.isEmpty(replyTexts)) { 572 return true; 573 } 574 Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES); 575 if (messages != null && messages.length > 0) { 576 Parcelable message = messages[messages.length - 1]; 577 if (message instanceof Bundle) { 578 Notification.MessagingStyle.Message lastMessage = 579 Notification.MessagingStyle.Message.getMessageFromBundle( 580 (Bundle) message); 581 if (lastMessage != null) { 582 Person senderPerson = lastMessage.getSenderPerson(); 583 if (senderPerson == null) { 584 return true; 585 } 586 Person user = extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON); 587 return Objects.equals(user, senderPerson); 588 } 589 } 590 } 591 return false; 592 } 593 setInitializationTime(long time)594 public void setInitializationTime(long time) { 595 if (initializationTime == -1) { 596 initializationTime = time; 597 } 598 } 599 sendAccessibilityEvent(int eventType)600 public void sendAccessibilityEvent(int eventType) { 601 if (row != null) { 602 row.sendAccessibilityEvent(eventType); 603 } 604 } 605 606 /** 607 * Used by NotificationMediaManager to determine... things 608 * @return {@code true} if we are a media notification 609 */ isMediaNotification()610 public boolean isMediaNotification() { 611 if (row == null) return false; 612 613 return row.isMediaRow(); 614 } 615 616 /** 617 * We are a top level child if our parent is the list of notifications duh 618 * @return {@code true} if we're a top level notification 619 */ isTopLevelChild()620 public boolean isTopLevelChild() { 621 return row != null && row.isTopLevelChild(); 622 } 623 resetUserExpansion()624 public void resetUserExpansion() { 625 if (row != null) row.resetUserExpansion(); 626 } 627 freeContentViewWhenSafe(@nflationFlag int inflationFlag)628 public void freeContentViewWhenSafe(@InflationFlag int inflationFlag) { 629 if (row != null) row.freeContentViewWhenSafe(inflationFlag); 630 } 631 setAmbientPulsing(boolean pulsing)632 public void setAmbientPulsing(boolean pulsing) { 633 if (row != null) row.setAmbientPulsing(pulsing); 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 setRowPinned(boolean pinned)660 public void setRowPinned(boolean pinned) { 661 if (row != null) row.setPinned(pinned); 662 } 663 isRowAnimatingAway()664 public boolean isRowAnimatingAway() { 665 return row != null && row.isHeadsUpAnimatingAway(); 666 } 667 isRowHeadsUp()668 public boolean isRowHeadsUp() { 669 return row != null && row.isHeadsUp(); 670 } 671 setHeadsUp(boolean shouldHeadsUp)672 public void setHeadsUp(boolean shouldHeadsUp) { 673 if (row != null) row.setHeadsUp(shouldHeadsUp); 674 } 675 676 setAmbientGoingAway(boolean goingAway)677 public void setAmbientGoingAway(boolean goingAway) { 678 if (row != null) row.setAmbientGoingAway(goingAway); 679 } 680 681 mustStayOnScreen()682 public boolean mustStayOnScreen() { 683 return row != null && row.mustStayOnScreen(); 684 } 685 setHeadsUpIsVisible()686 public void setHeadsUpIsVisible() { 687 if (row != null) row.setHeadsUpIsVisible(); 688 } 689 690 //TODO: i'm imagining a world where this isn't just the row, but I could be rwong getHeadsUpAnimationView()691 public ExpandableNotificationRow getHeadsUpAnimationView() { 692 return row; 693 } 694 setUserLocked(boolean userLocked)695 public void setUserLocked(boolean userLocked) { 696 if (row != null) row.setUserLocked(userLocked); 697 } 698 setUserExpanded(boolean userExpanded, boolean allowChildExpansion)699 public void setUserExpanded(boolean userExpanded, boolean allowChildExpansion) { 700 if (row != null) row.setUserExpanded(userExpanded, allowChildExpansion); 701 } 702 setGroupExpansionChanging(boolean changing)703 public void setGroupExpansionChanging(boolean changing) { 704 if (row != null) row.setGroupExpansionChanging(changing); 705 } 706 notifyHeightChanged(boolean needsAnimation)707 public void notifyHeightChanged(boolean needsAnimation) { 708 if (row != null) row.notifyHeightChanged(needsAnimation); 709 } 710 closeRemoteInput()711 public void closeRemoteInput() { 712 if (row != null) row.closeRemoteInput(); 713 } 714 areChildrenExpanded()715 public boolean areChildrenExpanded() { 716 return row != null && row.areChildrenExpanded(); 717 } 718 keepInParent()719 public boolean keepInParent() { 720 return row != null && row.keepInParent(); 721 } 722 723 //TODO: probably less confusing to say "is group fully visible" isGroupNotFullyVisible()724 public boolean isGroupNotFullyVisible() { 725 return row == null || row.isGroupNotFullyVisible(); 726 } 727 getGuts()728 public NotificationGuts getGuts() { 729 if (row != null) return row.getGuts(); 730 return null; 731 } 732 removeRow()733 public void removeRow() { 734 if (row != null) row.setRemoved(); 735 } 736 isSummaryWithChildren()737 public boolean isSummaryWithChildren() { 738 return row != null && row.isSummaryWithChildren(); 739 } 740 setKeepInParent(boolean keep)741 public void setKeepInParent(boolean keep) { 742 if (row != null) row.setKeepInParent(keep); 743 } 744 onDensityOrFontScaleChanged()745 public void onDensityOrFontScaleChanged() { 746 if (row != null) row.onDensityOrFontScaleChanged(); 747 } 748 areGutsExposed()749 public boolean areGutsExposed() { 750 return row != null && row.getGuts() != null && row.getGuts().isExposed(); 751 } 752 isChildInGroup()753 public boolean isChildInGroup() { 754 return parent == null; 755 } 756 757 /** 758 * @return Can the underlying notification be cleared? This can be different from whether the 759 * notification can be dismissed in case notifications are sensitive on the lockscreen. 760 * @see #canViewBeDismissed() 761 */ isClearable()762 public boolean isClearable() { 763 if (notification == null || !notification.isClearable()) { 764 return false; 765 } 766 767 List<NotificationEntry> children = getChildren(); 768 if (children != null && children.size() > 0) { 769 for (int i = 0; i < children.size(); i++) { 770 NotificationEntry child = children.get(i); 771 if (!child.isClearable()) { 772 return false; 773 } 774 } 775 } 776 return true; 777 } 778 canViewBeDismissed()779 public boolean canViewBeDismissed() { 780 if (row == null) return true; 781 return row.canViewBeDismissed(); 782 } 783 784 @VisibleForTesting isExemptFromDndVisualSuppression()785 boolean isExemptFromDndVisualSuppression() { 786 if (isNotificationBlockedByPolicy(notification.getNotification())) { 787 return false; 788 } 789 790 if ((notification.getNotification().flags 791 & Notification.FLAG_FOREGROUND_SERVICE) != 0) { 792 return true; 793 } 794 if (notification.getNotification().isMediaNotification()) { 795 return true; 796 } 797 if (mIsSystemNotification != null && mIsSystemNotification) { 798 return true; 799 } 800 return false; 801 } 802 shouldSuppressVisualEffect(int effect)803 private boolean shouldSuppressVisualEffect(int effect) { 804 if (isExemptFromDndVisualSuppression()) { 805 return false; 806 } 807 return (suppressedVisualEffects & effect) != 0; 808 } 809 810 /** 811 * Returns whether {@link Policy#SUPPRESSED_EFFECT_FULL_SCREEN_INTENT} 812 * is set for this entry. 813 */ shouldSuppressFullScreenIntent()814 public boolean shouldSuppressFullScreenIntent() { 815 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_FULL_SCREEN_INTENT); 816 } 817 818 /** 819 * Returns whether {@link Policy#SUPPRESSED_EFFECT_PEEK} 820 * is set for this entry. 821 */ shouldSuppressPeek()822 public boolean shouldSuppressPeek() { 823 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_PEEK); 824 } 825 826 /** 827 * Returns whether {@link Policy#SUPPRESSED_EFFECT_STATUS_BAR} 828 * is set for this entry. 829 */ shouldSuppressStatusBar()830 public boolean shouldSuppressStatusBar() { 831 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_STATUS_BAR); 832 } 833 834 /** 835 * Returns whether {@link Policy#SUPPRESSED_EFFECT_AMBIENT} 836 * is set for this entry. 837 */ shouldSuppressAmbient()838 public boolean shouldSuppressAmbient() { 839 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_AMBIENT); 840 } 841 842 /** 843 * Returns whether {@link Policy#SUPPRESSED_EFFECT_NOTIFICATION_LIST} 844 * is set for this entry. 845 */ shouldSuppressNotificationList()846 public boolean shouldSuppressNotificationList() { 847 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_NOTIFICATION_LIST); 848 } 849 850 /** 851 * Categories that are explicitly called out on DND settings screens are always blocked, if 852 * DND has flagged them, even if they are foreground or system notifications that might 853 * otherwise visually bypass DND. 854 */ isNotificationBlockedByPolicy(Notification n)855 private static boolean isNotificationBlockedByPolicy(Notification n) { 856 return isCategory(CATEGORY_CALL, n) 857 || isCategory(CATEGORY_MESSAGE, n) 858 || isCategory(CATEGORY_ALARM, n) 859 || isCategory(CATEGORY_EVENT, n) 860 || isCategory(CATEGORY_REMINDER, n); 861 } 862 isCategory(String category, Notification n)863 private static boolean isCategory(String category, Notification n) { 864 return Objects.equals(n.category, category); 865 } 866 867 /** Information about a suggestion that is being edited. */ 868 public static class EditedSuggestionInfo { 869 870 /** 871 * The value of the suggestion (before any user edits). 872 */ 873 public final CharSequence originalText; 874 875 /** 876 * The index of the suggestion that is being edited. 877 */ 878 public final int index; 879 EditedSuggestionInfo(CharSequence originalText, int index)880 public EditedSuggestionInfo(CharSequence originalText, int index) { 881 this.originalText = originalText; 882 this.index = index; 883 } 884 } 885 886 /** 887 * Returns whether the notification is a foreground service. It shows that this is an ongoing 888 * bubble. 889 */ isForegroundService()890 public boolean isForegroundService() { 891 return (notification.getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE) != 0; 892 } 893 } 894