• 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.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 
34 import static java.util.Objects.requireNonNull;
35 
36 import android.annotation.FlaggedApi;
37 import android.app.Flags;
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.app.RemoteInputHistoryItem;
45 import android.content.Context;
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.Log;
54 import android.view.ContentInfo;
55 
56 import androidx.annotation.NonNull;
57 import androidx.annotation.Nullable;
58 
59 import com.android.internal.annotations.VisibleForTesting;
60 import com.android.internal.util.ArrayUtils;
61 import com.android.internal.util.ContrastColorUtil;
62 import com.android.systemui.statusbar.InflationTask;
63 import com.android.systemui.statusbar.notification.collection.NotifCollection.CancellationReason;
64 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifFilter;
65 import com.android.systemui.statusbar.notification.collection.listbuilder.pluggable.NotifPromoter;
66 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifDismissInterceptor;
67 import com.android.systemui.statusbar.notification.collection.notifcollection.NotifLifetimeExtender;
68 import com.android.systemui.statusbar.notification.headsup.PinnedStatus;
69 import com.android.systemui.statusbar.notification.icon.IconPack;
70 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModel;
71 import com.android.systemui.statusbar.notification.promoted.shared.model.PromotedNotificationContentModels;
72 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
73 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRowController;
74 import com.android.systemui.statusbar.notification.row.NotificationGuts;
75 import com.android.systemui.statusbar.notification.row.shared.HeadsUpStatusBarModel;
76 import com.android.systemui.statusbar.notification.row.shared.NotificationContentModel;
77 import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor;
78 import com.android.systemui.statusbar.notification.shared.NotificationBundleUi;
79 import com.android.systemui.util.ListenerSet;
80 
81 import java.util.ArrayList;
82 import java.util.List;
83 import java.util.Objects;
84 
85 import kotlinx.coroutines.flow.MutableStateFlow;
86 import kotlinx.coroutines.flow.StateFlow;
87 import kotlinx.coroutines.flow.StateFlowKt;
88 
89 /**
90  * Represents a notification that the system UI knows about
91  *
92  * Whenever the NotificationManager tells us about the existence of a new notification, we wrap it
93  * in a NotificationEntry. Thus, every notification has an associated NotificationEntry, even if
94  * that notification is never displayed to the user (for example, if it's filtered out for some
95  * reason).
96  *
97  * Entries store information about the current state of the notification. Essentially:
98  * anything that needs to persist or be modifiable even when the notification's views don't
99  * exist. Any other state should be stored on the views/view controllers themselves.
100  *
101  * At the moment, there are many things here that shouldn't be and vice-versa. Hopefully we can
102  * clean this up in the future.
103  */
104 public final class NotificationEntry extends ListEntry {
105 
106     private final String mKey;
107     private StatusBarNotification mSbn;
108     private Ranking mRanking;
109 
110     /*
111      * Bookkeeping members
112      */
113 
114     /** List of lifetime extenders that are extending the lifetime of this notification. */
115     final List<NotifLifetimeExtender> mLifetimeExtenders = new ArrayList<>();
116 
117     /** List of dismiss interceptors that are intercepting the dismissal of this notification. */
118     final List<NotifDismissInterceptor> mDismissInterceptors = new ArrayList<>();
119 
120     /**
121      * If this notification was cancelled by system server, then the reason that was supplied.
122      * Uncancelled notifications always have REASON_NOT_CANCELED. Note that lifetime-extended
123      * notifications will have this set even though they are still in the active notification set.
124      */
125     @CancellationReason int mCancellationReason = REASON_NOT_CANCELED;
126 
127     /** @see #getDismissState() */
128     @NonNull private DismissState mDismissState = DismissState.NOT_DISMISSED;
129 
130     /*
131     * Old members
132     * TODO: Remove every member beneath this line if possible
133     */
134 
135     private IconPack mIcons = IconPack.buildEmptyPack(null);
136     private boolean interruption;
137     public int targetSdk;
138     private long lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET;
139     public CharSequence remoteInputText;
140     public List<RemoteInputHistoryItem> remoteInputs = null;
141     public String remoteInputMimeType;
142     public Uri remoteInputUri;
143     public ContentInfo remoteInputAttachment;
144     private Notification.BubbleMetadata mBubbleMetadata;
145 
146     /**
147      * If {@link RemoteInput#getEditChoicesBeforeSending} is enabled, and the user is
148      * currently editing a choice (smart reply), then this field contains the information about the
149      * suggestion being edited. Otherwise <code>null</code>.
150      */
151     public EditedSuggestionInfo editedSuggestionInfo;
152 
153     private ExpandableNotificationRow row; // the outer expanded view
154     private ExpandableNotificationRowController mRowController;
155 
156     private int mCachedContrastColor = COLOR_INVALID;
157     private int mCachedContrastColorIsFor = COLOR_INVALID;
158     private InflationTask mRunningTask = null;
159     public CharSequence remoteInputTextWhenReset;
160     public long lastRemoteInputSent = NOT_LAUNCHED_YET;
161 
162     private final MutableStateFlow<CharSequence> mHeadsUpStatusBarText =
163             StateFlowKt.MutableStateFlow(null);
164     private final MutableStateFlow<CharSequence> mHeadsUpStatusBarTextPublic =
165             StateFlowKt.MutableStateFlow(null);
166 
167     // indicates when this entry's view was first attached to a window
168     // this value will reset when the view is completely removed from the shade (ie: filtered out)
169     private long initializationTime = -1;
170 
171     /**
172      * Has the user sent a reply through this Notification.
173      */
174     private boolean hasSentReply;
175 
176     private final MutableStateFlow<Boolean> mSensitive = StateFlowKt.MutableStateFlow(true);
177     private final ListenerSet<OnSensitivityChangedListener> mOnSensitivityChangedListeners =
178             new ListenerSet<>();
179 
180     private boolean mPulseSupressed;
181     private boolean mIsMarkedForUserTriggeredMovement;
182     private boolean mIsHeadsUpEntry;
183 
184     private boolean mHasEverBeenGroupSummary;
185     private boolean mHasEverBeenGroupChild;
186 
187     public boolean mRemoteEditImeAnimatingAway;
188     public boolean mRemoteEditImeVisible;
189     private boolean mExpandAnimationRunning;
190     /**
191      * Flag to determine if the entry is blockable by DnD filters
192      */
193     private boolean mBlockable;
194 
195     /**
196      * Whether this notification has ever been a non-sticky HUN.
197      */
198     private boolean mIsDemoted = false;
199 
200     // TODO(b/377565433): Move into NotificationContentModel during/after
201     //  NotificationRowContentBinderRefactor.
202     private PromotedNotificationContentModels mPromotedNotificationContentModels;
203 
204     /**
205      * True if both
206      *  1) app provided full screen intent but does not have the permission to send it
207      *  2) this notification has never been demoted before
208      */
isStickyAndNotDemoted()209     public boolean isStickyAndNotDemoted() {
210 
211         final boolean fsiRequestedButDenied =  (getSbn().getNotification().flags
212                 & Notification.FLAG_FSI_REQUESTED_BUT_DENIED) != 0;
213 
214         if (!fsiRequestedButDenied && !mIsDemoted) {
215             demoteStickyHun();
216         }
217         return fsiRequestedButDenied && !mIsDemoted;
218     }
219 
220     @VisibleForTesting
isDemoted()221     public boolean isDemoted() {
222         return mIsDemoted;
223     }
224 
225     /**
226      * Make sticky HUN not sticky.
227      */
demoteStickyHun()228     public void demoteStickyHun() {
229         mIsDemoted = true;
230     }
231 
232     /** called when entry is currently a summary of a group */
markAsGroupSummary()233     public void markAsGroupSummary() {
234         mHasEverBeenGroupSummary = true;
235     }
236 
237     /** whether this entry has ever been marked as a summary */
hasEverBeenGroupSummary()238     public boolean hasEverBeenGroupSummary() {
239         return mHasEverBeenGroupSummary;
240     }
241 
242     /** called when entry is currently a child in a group */
markAsGroupChild()243     public void markAsGroupChild() {
244         mHasEverBeenGroupChild = true;
245     }
246 
247     /** whether this entry has ever been marked as a child */
hasEverBeenGroupChild()248     public boolean hasEverBeenGroupChild() {
249         return mHasEverBeenGroupChild;
250     }
251 
252     /**
253      * @param sbn the StatusBarNotification from system server
254      * @param ranking also from system server
255      * @param creationTime SystemClock.elapsedRealtime of when we were created
256      */
NotificationEntry( @onNull StatusBarNotification sbn, @NonNull Ranking ranking, long creationTime )257     public NotificationEntry(
258             @NonNull StatusBarNotification sbn,
259             @NonNull Ranking ranking,
260             long creationTime
261     ) {
262         super(requireNonNull(requireNonNull(sbn).getKey()), creationTime);
263 
264         requireNonNull(ranking);
265 
266         mKey = sbn.getKey();
267         setSbn(sbn);
268         setRanking(ranking);
269     }
270 
271     @Override
getRepresentativeEntry()272     public NotificationEntry getRepresentativeEntry() {
273         return this;
274     }
275 
276     /** The key for this notification. Guaranteed to be immutable and unique */
getKey()277     @NonNull public String getKey() {
278         return mKey;
279     }
280 
281     /**
282      * The StatusBarNotification that represents one half of a NotificationEntry (the other half
283      * being the Ranking). This object is swapped out whenever a notification is updated.
284      */
getSbn()285     @NonNull public StatusBarNotification getSbn() {
286         return mSbn;
287     }
288 
289     /**
290      * Should only be called by NotificationEntryManager and friends.
291      * TODO: Make this package-private
292      */
setSbn(@onNull StatusBarNotification sbn)293     public void setSbn(@NonNull StatusBarNotification sbn) {
294         requireNonNull(sbn);
295         requireNonNull(sbn.getKey());
296 
297         if (!sbn.getKey().equals(mKey)) {
298             throw new IllegalArgumentException("New key " + sbn.getKey()
299                     + " doesn't match existing key " + mKey);
300         }
301 
302         mSbn = sbn;
303         mBubbleMetadata = mSbn.getNotification().getBubbleMetadata();
304     }
305 
306     /**
307      * The Ranking that represents one half of a NotificationEntry (the other half being the
308      * StatusBarNotification). This object is swapped out whenever a the ranking is updated (which
309      * generally occurs whenever anything changes in the notification list).
310      */
getRanking()311     public Ranking getRanking() {
312         return mRanking;
313     }
314 
315     /**
316      * Should only be called by NotificationEntryManager and friends.
317      * TODO: Make this package-private
318      */
setRanking(@onNull Ranking ranking)319     public void setRanking(@NonNull Ranking ranking) {
320         requireNonNull(ranking);
321         requireNonNull(ranking.getKey());
322 
323         if (!ranking.getKey().equals(mKey)) {
324             throw new IllegalArgumentException("New key " + ranking.getKey()
325                     + " doesn't match existing key " + mKey);
326         }
327 
328         mRanking = ranking.withAudiblyAlertedInfo(mRanking);
329         updateIsBlockable();
330     }
331 
332     /*
333      * Bookkeeping getters and setters
334      */
335 
336     /**
337      * Set if the user has dismissed this notif but we haven't yet heard back from system server to
338      * confirm the dismissal.
339      */
getDismissState()340     @NonNull public DismissState getDismissState() {
341         return mDismissState;
342     }
343 
setDismissState(@onNull DismissState dismissState)344     void setDismissState(@NonNull DismissState dismissState) {
345         mDismissState = requireNonNull(dismissState);
346     }
347 
348     /**
349      * True if the notification has been canceled by system server. Usually, such notifications are
350      * immediately removed from the collection, but can sometimes stick around due to lifetime
351      * extenders.
352      */
isCanceled()353     public boolean isCanceled() {
354         return mCancellationReason != REASON_NOT_CANCELED;
355     }
356 
getExcludingFilter()357     @Nullable public NotifFilter getExcludingFilter() {
358         return getAttachState().getExcludingFilter();
359     }
360 
getNotifPromoter()361     @Nullable public NotifPromoter getNotifPromoter() {
362         return getAttachState().getPromoter();
363     }
364 
365     /*
366      * Convenience getters for SBN and Ranking members
367      */
368 
getChannel()369     public NotificationChannel getChannel() {
370         return mRanking.getChannel();
371     }
372 
getLastAudiblyAlertedMs()373     public long getLastAudiblyAlertedMs() {
374         return mRanking.getLastAudiblyAlertedMillis();
375     }
376 
isAmbient()377     public boolean isAmbient() {
378         return mRanking.isAmbient();
379     }
380 
getImportance()381     public int getImportance() {
382         return mRanking.getImportance();
383     }
384 
getSnoozeCriteria()385     public List<SnoozeCriterion> getSnoozeCriteria() {
386         return mRanking.getSnoozeCriteria();
387     }
388 
getUserSentiment()389     public int getUserSentiment() {
390         return mRanking.getUserSentiment();
391     }
392 
getSuppressedVisualEffects()393     public int getSuppressedVisualEffects() {
394         return mRanking.getSuppressedVisualEffects();
395     }
396 
397     /** @see Ranking#canBubble() */
canBubble()398     public boolean canBubble() {
399         return mRanking.canBubble();
400     }
401 
getSmartActions()402     public @NonNull List<Notification.Action> getSmartActions() {
403         return mRanking.getSmartActions();
404     }
405 
getSmartReplies()406     public @NonNull List<CharSequence> getSmartReplies() {
407         return mRanking.getSmartReplies();
408     }
409 
410 
411     /*
412      * Old methods
413      *
414      * TODO: Remove as many of these as possible
415      */
416 
417     @NonNull
getIcons()418     public IconPack getIcons() {
419         return mIcons;
420     }
421 
setIcons(@onNull IconPack icons)422     public void setIcons(@NonNull IconPack icons) {
423         mIcons = icons;
424     }
425 
setInterruption()426     public void setInterruption() {
427         interruption = true;
428     }
429 
hasInterrupted()430     public boolean hasInterrupted() {
431         return interruption;
432     }
433 
isBubble()434     public boolean isBubble() {
435         return (mSbn.getNotification().flags & FLAG_BUBBLE) != 0;
436     }
437 
438     /**
439      * Returns the data needed for a bubble for this notification, if it exists.
440      */
441     @Nullable
getBubbleMetadata()442     public Notification.BubbleMetadata getBubbleMetadata() {
443         return mBubbleMetadata;
444     }
445 
446     /**
447      * Sets bubble metadata for this notification.
448      */
setBubbleMetadata(@ullable Notification.BubbleMetadata metadata)449     public void setBubbleMetadata(@Nullable Notification.BubbleMetadata metadata) {
450         mBubbleMetadata = metadata;
451     }
452 
453     /**
454      * Updates the {@link Notification#FLAG_BUBBLE} flag on this notification to indicate
455      * whether it is a bubble or not. If this entry is set to not bubble, or does not have
456      * the required info to bubble, the flag cannot be set to true.
457      *
458      * @param shouldBubble whether this notification should be flagged as a bubble.
459      * @return true if the value changed.
460      */
setFlagBubble(boolean shouldBubble)461     public boolean setFlagBubble(boolean shouldBubble) {
462         boolean wasBubble = isBubble();
463         if (!shouldBubble) {
464             mSbn.getNotification().flags &= ~FLAG_BUBBLE;
465         } else if (mBubbleMetadata != null && canBubble()) {
466             // wants to be bubble & can bubble, set flag
467             mSbn.getNotification().flags |= FLAG_BUBBLE;
468         }
469         return wasBubble != isBubble();
470     }
471 
getRow()472     public ExpandableNotificationRow getRow() {
473         return row;
474     }
475 
476     //TODO: This will go away when we have a way to bind an entry to a row
setRow(ExpandableNotificationRow row)477     public void setRow(ExpandableNotificationRow row) {
478         this.row = row;
479     }
480 
getRowController()481     public ExpandableNotificationRowController getRowController() {
482         return mRowController;
483     }
484 
setRowController(ExpandableNotificationRowController controller)485     public void setRowController(ExpandableNotificationRowController controller) {
486         mRowController = controller;
487     }
488 
489     /**
490      * Get the children that are actually attached to this notification's row.
491      *
492      * TODO: Seems like most callers here should be asking a PipelineEntry, not a NotificationEntry
493      */
getAttachedNotifChildren()494     public @Nullable List<NotificationEntry> getAttachedNotifChildren() {
495         if (NotificationBundleUi.isEnabled()) {
496             if (isGroupSummary()) {
497                 GroupEntry parent = (GroupEntry) getParent();
498                 return parent != null ? new ArrayList<>(parent.getChildren()) : null;
499             }
500         } else {
501             if (row == null) {
502                 return null;
503             }
504 
505             List<ExpandableNotificationRow> rowChildren = row.getAttachedChildren();
506             if (rowChildren == null) {
507                 return null;
508             }
509 
510             ArrayList<NotificationEntry> children = new ArrayList<>();
511             for (ExpandableNotificationRow child : rowChildren) {
512                 children.add(child.getEntryLegacy());
513             }
514 
515             return children;
516         }
517         return null;
518     }
519 
isGroupSummary()520     private boolean isGroupSummary() {
521         if (getParent() == null) {
522             // The entry is not attached, so it doesn't count.
523             return false;
524         }
525         PipelineEntry pipelineEntry = getParent();
526         if (!(pipelineEntry instanceof GroupEntry groupEntry)) {
527             return false;
528         }
529 
530         // If entry is a summary, its parent is a GroupEntry with summary = entry.
531         return groupEntry.getSummary() == this;
532     }
533 
notifyFullScreenIntentLaunched()534     public void notifyFullScreenIntentLaunched() {
535         setInterruption();
536         lastFullScreenIntentLaunchTime = SystemClock.elapsedRealtime();
537     }
538 
hasJustLaunchedFullScreenIntent()539     public boolean hasJustLaunchedFullScreenIntent() {
540         return SystemClock.elapsedRealtime() < lastFullScreenIntentLaunchTime + LAUNCH_COOLDOWN;
541     }
542 
hasJustSentRemoteInput()543     public boolean hasJustSentRemoteInput() {
544         return SystemClock.elapsedRealtime() < lastRemoteInputSent + REMOTE_INPUT_COOLDOWN;
545     }
546 
hasFinishedInitialization()547     public boolean hasFinishedInitialization() {
548         NotificationBundleUi.assertInLegacyMode();
549         return initializationTime != -1
550                 && SystemClock.elapsedRealtime() > initializationTime + INITIALIZATION_DELAY;
551     }
552 
getContrastedColor(Context context, boolean isLowPriority, int backgroundColor)553     public int getContrastedColor(Context context, boolean isLowPriority,
554             int backgroundColor) {
555         int rawColor = isLowPriority ? Notification.COLOR_DEFAULT :
556                 mSbn.getNotification().color;
557         if (mCachedContrastColorIsFor == rawColor && mCachedContrastColor != COLOR_INVALID) {
558             return mCachedContrastColor;
559         }
560         final int contrasted = ContrastColorUtil.resolveContrastColor(context, rawColor,
561                 backgroundColor);
562         mCachedContrastColorIsFor = rawColor;
563         mCachedContrastColor = contrasted;
564         return mCachedContrastColor;
565     }
566 
567     /**
568      * Abort all existing inflation tasks
569      */
abortTask()570     public boolean abortTask() {
571         if (mRunningTask != null) {
572             mRunningTask.abort();
573             mRunningTask = null;
574             return true;
575         }
576         return false;
577     }
578 
setInflationTask(InflationTask abortableTask)579     public void setInflationTask(InflationTask abortableTask) {
580         // abort any existing inflation
581         abortTask();
582         mRunningTask = abortableTask;
583     }
584 
onInflationTaskFinished()585     public void onInflationTaskFinished() {
586         mRunningTask = null;
587     }
588 
589     @VisibleForTesting
getRunningTask()590     public InflationTask getRunningTask() {
591         return mRunningTask;
592     }
593 
onRemoteInputInserted()594     public void onRemoteInputInserted() {
595         lastRemoteInputSent = NOT_LAUNCHED_YET;
596         remoteInputTextWhenReset = null;
597     }
598 
setHasSentReply()599     public void setHasSentReply() {
600         hasSentReply = true;
601     }
602 
isLastMessageFromReply()603     public boolean isLastMessageFromReply() {
604         if (!hasSentReply) {
605             return false;
606         }
607         Bundle extras = mSbn.getNotification().extras;
608         Parcelable[] replyTexts =
609                 extras.getParcelableArray(Notification.EXTRA_REMOTE_INPUT_HISTORY_ITEMS);
610         if (!ArrayUtils.isEmpty(replyTexts)) {
611             return true;
612         }
613         List<Message> messages = Message.getMessagesFromBundleArray(
614                 extras.getParcelableArray(Notification.EXTRA_MESSAGES));
615         if (messages != null && !messages.isEmpty()) {
616             Message lastMessage = messages.get(messages.size() -1);
617 
618             if (lastMessage != null) {
619                 Person senderPerson = lastMessage.getSenderPerson();
620                 if (senderPerson == null) {
621                     return true;
622                 }
623                 Person user = extras.getParcelable(
624                         Notification.EXTRA_MESSAGING_PERSON, Person.class);
625                 return Objects.equals(user, senderPerson);
626             }
627         }
628         return false;
629     }
630 
resetInitializationTime()631     public void resetInitializationTime() {
632         NotificationBundleUi.assertInLegacyMode();
633         initializationTime = -1;
634     }
635 
setInitializationTime(long time)636     public void setInitializationTime(long time) {
637         NotificationBundleUi.assertInLegacyMode();
638         if (initializationTime == -1) {
639             initializationTime = time;
640         }
641     }
642 
sendAccessibilityEvent(int eventType)643     public void sendAccessibilityEvent(int eventType) {
644         if (row != null) {
645             row.sendAccessibilityEvent(eventType);
646         }
647     }
648 
649     /**
650      * Used by NotificationMediaManager to determine... things
651      * @return {@code true} if we are a media notification
652      */
isMediaNotification()653     public boolean isMediaNotification() {
654         if (NotificationBundleUi.isEnabled()) {
655             return getSbn().getNotification().isMediaNotification();
656         } else {
657             if (row == null) return false;
658 
659             return row.isMediaRow();
660         }
661     }
662 
containsCustomViews()663     public boolean containsCustomViews() {
664         return getSbn().getNotification().containsCustomViews();
665     }
666 
resetUserExpansion()667     public void resetUserExpansion() {
668         if (row != null) row.resetUserExpansion();
669     }
670 
rowExists()671     public boolean rowExists() {
672         return row != null;
673     }
674 
isRowDismissed()675     public boolean isRowDismissed() {
676         return row != null && row.isDismissed();
677     }
678 
isRowRemoved()679     public boolean isRowRemoved() {
680         return row != null && row.isRemoved();
681     }
682 
683     /**
684      * @return {@code true} if the row is null or removed
685      */
isRemoved()686     public boolean isRemoved() {
687         //TODO: recycling invalidates this
688         return row == null || row.isRemoved();
689     }
690 
isRowPinned()691     public boolean isRowPinned() {
692         return getPinnedStatus().isPinned();
693     }
694 
695     /** Returns this notification's current pinned status. */
getPinnedStatus()696     public PinnedStatus getPinnedStatus() {
697         if (row != null) {
698             return row.getPinnedStatus();
699         } else {
700             return PinnedStatus.NotPinned;
701         }
702     }
703 
704     /**
705      * Is this entry pinned and was expanded while doing so
706      */
isPinnedAndExpanded()707     public boolean isPinnedAndExpanded() {
708         return row != null && row.isPinnedAndExpanded();
709     }
710 
setRowPinnedStatus(PinnedStatus pinnedStatus)711     public void setRowPinnedStatus(PinnedStatus pinnedStatus) {
712         if (row != null) row.setPinnedStatus(pinnedStatus);
713     }
714 
isRowHeadsUp()715     public boolean isRowHeadsUp() {
716         return row != null && row.isHeadsUp();
717     }
718 
showingPulsing()719     public boolean showingPulsing() {
720         return row != null && row.showingPulsing();
721     }
722 
setHeadsUp(boolean shouldHeadsUp)723     public void setHeadsUp(boolean shouldHeadsUp) {
724         if (row != null) row.setHeadsUp(shouldHeadsUp);
725     }
726 
setHeadsUpAnimatingAway(boolean animatingAway)727     public void setHeadsUpAnimatingAway(boolean animatingAway) {
728         if (row != null) row.setHeadsUpAnimatingAway(animatingAway);
729     }
730 
mustStayOnScreen()731     public boolean mustStayOnScreen() {
732         return row != null && row.mustStayOnScreen();
733     }
734 
setHeadsUpIsVisible()735     public void setHeadsUpIsVisible() {
736         if (row != null) row.markHeadsUpSeen();
737     }
738 
739     //TODO: i'm imagining a world where this isn't just the row, but I could be rwong
getHeadsUpAnimationView()740     public ExpandableNotificationRow getHeadsUpAnimationView() {
741         return row;
742     }
743 
setUserLocked(boolean userLocked)744     public void setUserLocked(boolean userLocked) {
745         if (row != null) row.setUserLocked(userLocked);
746     }
747 
setUserExpanded(boolean userExpanded, boolean allowChildExpansion)748     public void setUserExpanded(boolean userExpanded, boolean allowChildExpansion) {
749         if (row != null) row.setUserExpanded(userExpanded, allowChildExpansion);
750     }
751 
setGroupExpansionChanging(boolean changing)752     public void setGroupExpansionChanging(boolean changing) {
753         if (row != null) row.setGroupExpansionChanging(changing);
754     }
755 
notifyHeightChanged(boolean needsAnimation)756     public void notifyHeightChanged(boolean needsAnimation) {
757         if (row != null) row.notifyHeightChanged(needsAnimation);
758     }
759 
closeRemoteInput()760     public void closeRemoteInput() {
761         if (row != null) row.closeRemoteInput();
762     }
763 
areChildrenExpanded()764     public boolean areChildrenExpanded() {
765         return row != null && row.areChildrenExpanded();
766     }
767 
getGuts()768     public NotificationGuts getGuts() {
769         if (row != null) return row.getGuts();
770         return null;
771     }
772 
removeRow()773     public void removeRow() {
774         if (row != null) row.setRemoved();
775     }
776 
isSummaryWithChildren()777     public boolean isSummaryWithChildren() {
778         return row != null && row.isSummaryWithChildren();
779     }
780 
onDensityOrFontScaleChanged()781     public void onDensityOrFontScaleChanged() {
782         if (row != null) row.onDensityOrFontScaleChanged();
783     }
784 
areGutsExposed()785     public boolean areGutsExposed() {
786         return row != null && row.getGuts() != null && row.getGuts().isExposed();
787     }
788 
789     /**
790      * @return Whether the notification row is a child of a group notification view; false if the
791      * row is null
792      */
rowIsChildInGroup()793     public boolean rowIsChildInGroup() {
794         return row != null && row.isChildInGroup();
795     }
796 
797     /**
798      * @return Can the underlying notification be cleared? This can be different from whether the
799      *         notification can be dismissed in case notifications are sensitive on the lockscreen.
800      */
801     // TODO: This logic doesn't belong on NotificationEntry. It should be moved to a controller
802     // that can be added as a dependency to any class that needs to answer this question.
isClearable()803     public boolean isClearable() {
804         if (!mSbn.isClearable()) {
805             return false;
806         }
807 
808         List<NotificationEntry> children = getAttachedNotifChildren();
809         if (children != null && children.size() > 0) {
810             for (int i = 0; i < children.size(); i++) {
811                 NotificationEntry child =  children.get(i);
812                 if (!child.getSbn().isClearable()) {
813                     return false;
814                 }
815             }
816         }
817         return true;
818     }
819 
820     /**
821      * Determines whether the NotificationEntry is dismissable based on the Notification flags and
822      * the given state. It doesn't recurse children or depend on the view attach state.
823      *
824      * @param isLocked if the device is locked or unlocked
825      * @return true if this NotificationEntry is dismissable.
826      */
isDismissableForState(boolean isLocked)827     public boolean isDismissableForState(boolean isLocked) {
828         if (mSbn.isNonDismissable()) {
829             // don't dismiss exempted Notifications
830             return false;
831         }
832         // don't dismiss ongoing Notifications when the device is locked
833         return !mSbn.isOngoing() || !isLocked;
834     }
835 
836     @VisibleForTesting
isExemptFromDndVisualSuppression()837     boolean isExemptFromDndVisualSuppression() {
838         if (isNotificationBlockedByPolicy(mSbn.getNotification())) {
839             return false;
840         }
841 
842         if (mSbn.getNotification().isFgsOrUij()) {
843             return true;
844         }
845         if (mSbn.getNotification().isMediaNotification()) {
846             return true;
847         }
848         if (!isBlockable()) {
849             return true;
850         }
851         return false;
852     }
853 
854     /**
855      * Returns whether the NotificationEntry is promoted ongoing.
856      */
857     @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
isOngoingPromoted()858     public boolean isOngoingPromoted() {
859         return mSbn.getNotification().isPromotedOngoing();
860     }
861 
862     /**
863      * Returns whether this row is considered blockable (i.e. it's not a system notif
864      * or is not in an allowList).
865      */
isBlockable()866     public boolean isBlockable() {
867         return mBlockable;
868     }
869 
updateIsBlockable()870     private void updateIsBlockable() {
871         if (getChannel() == null) {
872             mBlockable = false;
873             return;
874         }
875         if (getChannel().isImportanceLockedByCriticalDeviceFunction()
876                 && !getChannel().isBlockable()) {
877             mBlockable = false;
878             return;
879         }
880         mBlockable = true;
881     }
882 
shouldSuppressVisualEffect(int effect)883     private boolean shouldSuppressVisualEffect(int effect) {
884         if (isExemptFromDndVisualSuppression()) {
885             return false;
886         }
887         return (getSuppressedVisualEffects() & effect) != 0;
888     }
889 
890     /**
891      * Returns whether {@link Policy#SUPPRESSED_EFFECT_FULL_SCREEN_INTENT}
892      * is set for this entry.
893      */
shouldSuppressFullScreenIntent()894     public boolean shouldSuppressFullScreenIntent() {
895         return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_FULL_SCREEN_INTENT);
896     }
897 
898     /**
899      * Returns whether {@link Policy#SUPPRESSED_EFFECT_PEEK}
900      * is set for this entry.
901      */
shouldSuppressPeek()902     public boolean shouldSuppressPeek() {
903         return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_PEEK);
904     }
905 
906     /**
907      * Returns whether {@link Policy#SUPPRESSED_EFFECT_STATUS_BAR}
908      * is set for this entry.
909      */
shouldSuppressStatusBar()910     public boolean shouldSuppressStatusBar() {
911         return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_STATUS_BAR);
912     }
913 
914     /**
915      * Returns whether {@link Policy#SUPPRESSED_EFFECT_AMBIENT}
916      * is set for this entry.
917      */
shouldSuppressAmbient()918     public boolean shouldSuppressAmbient() {
919         return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_AMBIENT);
920     }
921 
922     /**
923      * Returns whether {@link Policy#SUPPRESSED_EFFECT_NOTIFICATION_LIST}
924      * is set for this entry.
925      */
shouldSuppressNotificationList()926     public boolean shouldSuppressNotificationList() {
927         return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_NOTIFICATION_LIST);
928     }
929 
930 
931     /**
932      * Returns whether {@link Policy#SUPPRESSED_EFFECT_BADGE}
933      * is set for this entry. This badge is not an app badge, but rather an indicator of "unseen"
934      * content. Typically this is referred to as a "dot" internally in Launcher & SysUI code.
935      */
shouldSuppressNotificationDot()936     public boolean shouldSuppressNotificationDot() {
937         return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_BADGE);
938     }
939 
940     /**
941      * Categories that are explicitly called out on DND settings screens are always blocked, if
942      * DND has flagged them, even if they are foreground or system notifications that might
943      * otherwise visually bypass DND.
944      */
isNotificationBlockedByPolicy(Notification n)945     private static boolean isNotificationBlockedByPolicy(Notification n) {
946         return isCategory(CATEGORY_CALL, n)
947                 || isCategory(CATEGORY_MESSAGE, n)
948                 || isCategory(CATEGORY_ALARM, n)
949                 || isCategory(CATEGORY_EVENT, n)
950                 || isCategory(CATEGORY_REMINDER, n);
951     }
952 
isCategory(String category, Notification n)953     private static boolean isCategory(String category, Notification n) {
954         return Objects.equals(n.category, category);
955     }
956 
957     /** @see #setSensitive(boolean, boolean)  */
isSensitive()958     public StateFlow<Boolean> isSensitive() {
959         return mSensitive;
960     }
961 
962     /**
963      * Set this notification to be sensitive.
964      *
965      * @param sensitive true if the content of this notification is sensitive right now
966      * @param deviceSensitive true if the device in general is sensitive right now
967      */
setSensitive(boolean sensitive, boolean deviceSensitive)968     public void setSensitive(boolean sensitive, boolean deviceSensitive) {
969         getRow().setSensitive(sensitive, deviceSensitive);
970         if (sensitive != mSensitive.getValue()) {
971             mSensitive.setValue(sensitive);
972             for (NotificationEntry.OnSensitivityChangedListener listener :
973                     mOnSensitivityChangedListeners) {
974                 listener.onSensitivityChanged(this);
975             }
976         }
977     }
978 
979     /** Add a listener to be notified when the entry's sensitivity changes. */
addOnSensitivityChangedListener(OnSensitivityChangedListener listener)980     public void addOnSensitivityChangedListener(OnSensitivityChangedListener listener) {
981         mOnSensitivityChangedListeners.addIfAbsent(listener);
982     }
983 
984     /** Remove a listener that was registered above. */
removeOnSensitivityChangedListener(OnSensitivityChangedListener listener)985     public void removeOnSensitivityChangedListener(OnSensitivityChangedListener listener) {
986         mOnSensitivityChangedListeners.remove(listener);
987     }
988 
989     /** @see #setHeadsUpStatusBarText(CharSequence) */
990     @NonNull
getHeadsUpStatusBarText()991     public StateFlow<CharSequence> getHeadsUpStatusBarText() {
992         return mHeadsUpStatusBarText;
993     }
994 
995     /**
996      * Sets the text to be displayed on the StatusBar, when this notification is the top pinned
997      * heads up.
998      */
setHeadsUpStatusBarText(CharSequence headsUpStatusBarText)999     public void setHeadsUpStatusBarText(CharSequence headsUpStatusBarText) {
1000         NotificationRowContentBinderRefactor.assertInLegacyMode();
1001         this.mHeadsUpStatusBarText.setValue(headsUpStatusBarText);
1002     }
1003 
1004     /** @see #setHeadsUpStatusBarTextPublic(CharSequence) */
1005     @NonNull
getHeadsUpStatusBarTextPublic()1006     public StateFlow<CharSequence> getHeadsUpStatusBarTextPublic() {
1007         return mHeadsUpStatusBarTextPublic;
1008     }
1009 
1010     /**
1011      * Sets the text to be displayed on the StatusBar, when this notification is the top pinned
1012      * heads up, and its content is sensitive right now.
1013      */
setHeadsUpStatusBarTextPublic(CharSequence headsUpStatusBarTextPublic)1014     public void setHeadsUpStatusBarTextPublic(CharSequence headsUpStatusBarTextPublic) {
1015         NotificationRowContentBinderRefactor.assertInLegacyMode();
1016         this.mHeadsUpStatusBarTextPublic.setValue(headsUpStatusBarTextPublic);
1017     }
1018 
isPulseSuppressed()1019     public boolean isPulseSuppressed() {
1020         return mPulseSupressed;
1021     }
1022 
setPulseSuppressed(boolean suppressed)1023     public void setPulseSuppressed(boolean suppressed) {
1024         mPulseSupressed = suppressed;
1025     }
1026 
1027     /** Whether or not this entry has been marked for a user-triggered movement. */
isMarkedForUserTriggeredMovement()1028     public boolean isMarkedForUserTriggeredMovement() {
1029         return mIsMarkedForUserTriggeredMovement;
1030     }
1031 
1032     /**
1033      * Mark this entry for movement triggered by a user action (ex: changing the priority of a
1034      * conversation). This can then be used for custom animations.
1035      */
markForUserTriggeredMovement(boolean marked)1036     public void markForUserTriggeredMovement(boolean marked) {
1037         mIsMarkedForUserTriggeredMovement = marked;
1038     }
1039 
1040     private boolean mSeenInShade = false;
1041 
setSeenInShade(boolean seen)1042     public void setSeenInShade(boolean seen) {
1043         mSeenInShade = seen;
1044     }
1045 
isSeenInShade()1046     public boolean isSeenInShade() {
1047         return mSeenInShade;
1048     }
1049 
setIsHeadsUpEntry(boolean isHeadsUpEntry)1050     public void setIsHeadsUpEntry(boolean isHeadsUpEntry) {
1051         mIsHeadsUpEntry = isHeadsUpEntry;
1052     }
1053 
isHeadsUpEntry()1054     public boolean isHeadsUpEntry() {
1055         return mIsHeadsUpEntry;
1056     }
1057 
1058     /** Set whether this notification is currently used to animate a launch. */
setExpandAnimationRunning(boolean expandAnimationRunning)1059     public void setExpandAnimationRunning(boolean expandAnimationRunning) {
1060         mExpandAnimationRunning = expandAnimationRunning;
1061     }
1062 
1063     /** Whether this notification is currently used to animate a launch. */
isExpandAnimationRunning()1064     public boolean isExpandAnimationRunning() {
1065         return mExpandAnimationRunning;
1066     }
1067 
1068     /**
1069      * @return NotificationStyle
1070      */
getNotificationStyle()1071     public String getNotificationStyle() {
1072         if (isSummaryWithChildren()) {
1073             return "summary";
1074         }
1075 
1076         final Class<? extends Notification.Style> style =
1077                 getSbn().getNotification().getNotificationStyle();
1078         return style == null ? "nostyle" : style.getSimpleName();
1079     }
1080 
1081     /**
1082      * Return {@code true} if notification's visibility is {@link Notification.VISIBILITY_PRIVATE}
1083      */
isNotificationVisibilityPrivate()1084     public boolean isNotificationVisibilityPrivate() {
1085         return getSbn().getNotification().visibility == Notification.VISIBILITY_PRIVATE;
1086     }
1087 
1088     /**
1089      * Return {@code true} if notification's channel lockscreen visibility is
1090      * {@link Notification.VISIBILITY_PRIVATE}
1091      */
isChannelVisibilityPrivate()1092     public boolean isChannelVisibilityPrivate() {
1093         return getRanking().getChannel() != null
1094                 && getRanking().getChannel().getLockscreenVisibility()
1095                 == Notification.VISIBILITY_PRIVATE;
1096     }
1097 
1098     /** Set the content generated by the notification inflater. */
setContentModel(NotificationContentModel contentModel)1099     public void setContentModel(NotificationContentModel contentModel) {
1100         if (NotificationRowContentBinderRefactor.isUnexpectedlyInLegacyMode()) return;
1101         HeadsUpStatusBarModel headsUpStatusBarModel = contentModel.getHeadsUpStatusBarModel();
1102         this.mHeadsUpStatusBarText.setValue(headsUpStatusBarModel.getPrivateText());
1103         this.mHeadsUpStatusBarTextPublic.setValue(headsUpStatusBarModel.getPublicText());
1104     }
1105 
1106     /**
1107      * Gets the content needed to render this notification as a promoted notification on various
1108      * surfaces (like status bar chips and AOD).
1109      */
getPromotedNotificationContentModels()1110     public PromotedNotificationContentModels getPromotedNotificationContentModels() {
1111         if (PromotedNotificationContentModel.featureFlagEnabled()) {
1112             return mPromotedNotificationContentModels;
1113         } else {
1114             Log.wtf(TAG, "getting promoted content without feature flag enabled", new Throwable());
1115             return null;
1116         }
1117     }
1118 
1119     /**
1120      * Returns whether the NotificationEntry is promoted ongoing.
1121      */
1122     @FlaggedApi(Flags.FLAG_API_RICH_ONGOING)
isPromotedOngoing()1123     public boolean isPromotedOngoing() {
1124         return PromotedNotificationContentModel.isPromotedForStatusBarChip(mSbn.getNotification());
1125     }
1126 
1127     /**
1128      * Sets the content needed to render this notification as a promoted notification on various
1129      * surfaces (like status bar chips and AOD).
1130      */
setPromotedNotificationContentModels( @ullable PromotedNotificationContentModels promotedNotificationContentModels)1131     public void setPromotedNotificationContentModels(
1132             @Nullable PromotedNotificationContentModels promotedNotificationContentModels) {
1133         if (PromotedNotificationContentModel.featureFlagEnabled()) {
1134             this.mPromotedNotificationContentModels = promotedNotificationContentModels;
1135         } else {
1136             Log.wtf(TAG, "setting promoted content without feature flag enabled", new Throwable());
1137         }
1138     }
1139 
1140     /** Information about a suggestion that is being edited. */
1141     public static class EditedSuggestionInfo {
1142 
1143         /**
1144          * The value of the suggestion (before any user edits).
1145          */
1146         public final CharSequence originalText;
1147 
1148         /**
1149          * The index of the suggestion that is being edited.
1150          */
1151         public final int index;
1152 
EditedSuggestionInfo(CharSequence originalText, int index)1153         public EditedSuggestionInfo(CharSequence originalText, int index) {
1154             this.originalText = originalText;
1155             this.index = index;
1156         }
1157     }
1158 
1159     /** Listener interface for {@link #addOnSensitivityChangedListener} */
1160     public interface OnSensitivityChangedListener {
1161         /** Called when the sensitivity changes */
onSensitivityChanged(@onNull NotificationEntry entry)1162         void onSensitivityChanged(@NonNull NotificationEntry entry);
1163     }
1164 
1165     /** @see #getDismissState() */
1166     public enum DismissState {
1167         /** User has not dismissed this notif or its parent */
1168         NOT_DISMISSED,
1169         /** User has dismissed this notif specifically */
1170         DISMISSED,
1171         /** User has dismissed this notif's parent (which implicitly dismisses this one as well) */
1172         PARENT_DISMISSED,
1173     }
1174 
1175     private static final long LAUNCH_COOLDOWN = 2000;
1176     private static final long REMOTE_INPUT_COOLDOWN = 500;
1177     private static final long INITIALIZATION_DELAY = 400;
1178     private static final long NOT_LAUNCHED_YET = -LAUNCH_COOLDOWN;
1179     private static final int COLOR_INVALID = 1;
1180 
1181     private static final String TAG = "NotificationEntry";
1182 }
1183