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