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