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