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