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