• 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_FULL_SCREEN_INTENT;
27 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST;
28 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK;
29 import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR;
30 
31 import android.annotation.NonNull;
32 import android.app.Notification;
33 import android.app.NotificationChannel;
34 import android.app.NotificationManager.Policy;
35 import android.app.Person;
36 import android.content.Context;
37 import android.graphics.drawable.Icon;
38 import android.os.Bundle;
39 import android.os.Parcelable;
40 import android.os.SystemClock;
41 import android.service.notification.NotificationListenerService;
42 import android.service.notification.SnoozeCriterion;
43 import android.service.notification.StatusBarNotification;
44 import android.text.TextUtils;
45 import android.util.ArraySet;
46 import android.view.View;
47 import android.widget.ImageView;
48 
49 import androidx.annotation.Nullable;
50 
51 import com.android.internal.annotations.VisibleForTesting;
52 import com.android.internal.statusbar.StatusBarIcon;
53 import com.android.internal.util.ArrayUtils;
54 import com.android.internal.util.ContrastColorUtil;
55 import com.android.systemui.R;
56 import com.android.systemui.statusbar.InflationTask;
57 import com.android.systemui.statusbar.StatusBarIconView;
58 import com.android.systemui.statusbar.notification.InflationException;
59 import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
60 import com.android.systemui.statusbar.notification.row.NotificationContentInflater.InflationFlag;
61 import com.android.systemui.statusbar.notification.row.NotificationGuts;
62 
63 import java.util.ArrayList;
64 import java.util.Collections;
65 import java.util.List;
66 import java.util.Objects;
67 
68 /**
69  * Represents a notification that the system UI knows about
70  *
71  * Whenever the NotificationManager tells us about the existence of a new notification, we wrap it
72  * in a NotificationEntry. Thus, every notification has an associated NotificationEntry, even if
73  * that notification is never displayed to the user (for example, if it's filtered out for some
74  * reason).
75  *
76  * Entries store information about the current state of the notification. Essentially:
77  * anything that needs to persist or be modifiable even when the notification's views don't
78  * exist. Any other state should be stored on the views/view controllers themselves.
79  *
80  * At the moment, there are many things here that shouldn't be and vice-versa. Hopefully we can
81  * clean this up in the future.
82  */
83 public final class NotificationEntry {
84     private static final long LAUNCH_COOLDOWN = 2000;
85     private static final long REMOTE_INPUT_COOLDOWN = 500;
86     private static final long INITIALIZATION_DELAY = 400;
87     private static final long NOT_LAUNCHED_YET = -LAUNCH_COOLDOWN;
88     private static final int COLOR_INVALID = 1;
89     public final String key;
90     public StatusBarNotification notification;
91     public NotificationChannel channel;
92     public long lastAudiblyAlertedMs;
93     public boolean noisy;
94     public boolean ambient;
95     public int importance;
96     public StatusBarIconView icon;
97     public StatusBarIconView expandedIcon;
98     public StatusBarIconView centeredIcon;
99     private boolean interruption;
100     public boolean autoRedacted; // whether the redacted notification was generated by us
101     public int targetSdk;
102     private long lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET;
103     public CharSequence remoteInputText;
104     public List<SnoozeCriterion> snoozeCriteria;
105     public int userSentiment = NotificationListenerService.Ranking.USER_SENTIMENT_NEUTRAL;
106     /** Smart Actions provided by the NotificationAssistantService. */
107     @NonNull
108     public List<Notification.Action> systemGeneratedSmartActions = Collections.emptyList();
109     /** Smart replies provided by the NotificationAssistantService. */
110     @NonNull
111     public CharSequence[] systemGeneratedSmartReplies = new CharSequence[0];
112 
113     /**
114      * If {@link android.app.RemoteInput#getEditChoicesBeforeSending} is enabled, and the user is
115      * currently editing a choice (smart reply), then this field contains the information about the
116      * suggestion being edited. Otherwise <code>null</code>.
117      */
118     public EditedSuggestionInfo editedSuggestionInfo;
119 
120     @VisibleForTesting
121     public int suppressedVisualEffects;
122     public boolean suspended;
123 
124     private NotificationEntry parent; // our parent (if we're in a group)
125     private ExpandableNotificationRow row; // the outer expanded view
126 
127     private int mCachedContrastColor = COLOR_INVALID;
128     private int mCachedContrastColorIsFor = COLOR_INVALID;
129     private InflationTask mRunningTask = null;
130     private Throwable mDebugThrowable;
131     public CharSequence remoteInputTextWhenReset;
132     public long lastRemoteInputSent = NOT_LAUNCHED_YET;
133     public ArraySet<Integer> mActiveAppOps = new ArraySet<>(3);
134     public CharSequence headsUpStatusBarText;
135     public CharSequence headsUpStatusBarTextPublic;
136 
137     private long initializationTime = -1;
138 
139     /**
140      * Whether or not this row represents a system notification. Note that if this is
141      * {@code null}, that means we were either unable to retrieve the info or have yet to
142      * retrieve the info.
143      */
144     public Boolean mIsSystemNotification;
145 
146     /**
147      * Has the user sent a reply through this Notification.
148      */
149     private boolean hasSentReply;
150 
151     /**
152      * Whether this notification has been approved globally, at the app level, and at the channel
153      * level for bubbling.
154      */
155     public boolean canBubble;
156 
157     /**
158      * Whether this notification should be shown in the shade when it is also displayed as a bubble.
159      *
160      * <p>When a notification is a bubble we don't show it in the shade once the bubble has been
161      * expanded</p>
162      */
163     private boolean mShowInShadeWhenBubble;
164 
165     /**
166      * Whether the user has dismissed this notification when it was in bubble form.
167      */
168     private boolean mUserDismissedBubble;
169 
170     /**
171      * Whether this notification is shown to the user as a high priority notification: visible on
172      * the lock screen/status bar and in the top section in the shade.
173      */
174     private boolean mHighPriority;
175 
176     private boolean mIsTopBucket;
177 
NotificationEntry(StatusBarNotification n)178     public NotificationEntry(StatusBarNotification n) {
179         this(n, null);
180     }
181 
NotificationEntry( StatusBarNotification n, @Nullable NotificationListenerService.Ranking ranking)182     public NotificationEntry(
183             StatusBarNotification n,
184             @Nullable NotificationListenerService.Ranking ranking) {
185         this.key = n.getKey();
186         this.notification = n;
187         if (ranking != null) {
188             populateFromRanking(ranking);
189         }
190     }
191 
populateFromRanking(@onNull NotificationListenerService.Ranking ranking)192     public void populateFromRanking(@NonNull NotificationListenerService.Ranking ranking) {
193         channel = ranking.getChannel();
194         lastAudiblyAlertedMs = ranking.getLastAudiblyAlertedMillis();
195         importance = ranking.getImportance();
196         ambient = ranking.isAmbient();
197         snoozeCriteria = ranking.getSnoozeCriteria();
198         userSentiment = ranking.getUserSentiment();
199         systemGeneratedSmartActions = ranking.getSmartActions() == null
200                 ? Collections.emptyList() : ranking.getSmartActions();
201         systemGeneratedSmartReplies = ranking.getSmartReplies() == null
202                 ? new CharSequence[0]
203                 : ranking.getSmartReplies().toArray(new CharSequence[0]);
204         suppressedVisualEffects = ranking.getSuppressedVisualEffects();
205         suspended = ranking.isSuspended();
206         canBubble = ranking.canBubble();
207     }
208 
setInterruption()209     public void setInterruption() {
210         interruption = true;
211     }
212 
hasInterrupted()213     public boolean hasInterrupted() {
214         return interruption;
215     }
216 
isHighPriority()217     public boolean isHighPriority() {
218         return mHighPriority;
219     }
220 
setIsHighPriority(boolean highPriority)221     public void setIsHighPriority(boolean highPriority) {
222         this.mHighPriority = highPriority;
223     }
224 
225     /**
226      * @return True if the notif should appear in the "top" or "important" section of notifications
227      * (as opposed to the "bottom" or "silent" section). This is usually the same as
228      * {@link #isHighPriority()}, but there are certain exceptions, such as media notifs.
229      */
isTopBucket()230     public boolean isTopBucket() {
231         return mIsTopBucket;
232     }
setIsTopBucket(boolean isTopBucket)233     public void setIsTopBucket(boolean isTopBucket) {
234         mIsTopBucket = isTopBucket;
235     }
236 
isBubble()237     public boolean isBubble() {
238         return (notification.getNotification().flags & FLAG_BUBBLE) != 0;
239     }
240 
setBubbleDismissed(boolean userDismissed)241     public void setBubbleDismissed(boolean userDismissed) {
242         mUserDismissedBubble = userDismissed;
243     }
244 
isBubbleDismissed()245     public boolean isBubbleDismissed() {
246         return mUserDismissedBubble;
247     }
248 
249     /**
250      * Sets whether this notification should be shown in the shade when it is also displayed as a
251      * bubble.
252      */
setShowInShadeWhenBubble(boolean showInShade)253     public void setShowInShadeWhenBubble(boolean showInShade) {
254         mShowInShadeWhenBubble = showInShade;
255     }
256 
257     /**
258      * Whether this notification should be shown in the shade when it is also displayed as a
259      * bubble.
260      */
showInShadeWhenBubble()261     public boolean showInShadeWhenBubble() {
262         // We always show it in the shade if non-clearable
263         return !isRowDismissed() && (!isClearable() || mShowInShadeWhenBubble);
264     }
265 
266     /**
267      * Returns the data needed for a bubble for this notification, if it exists.
268      */
getBubbleMetadata()269     public Notification.BubbleMetadata getBubbleMetadata() {
270         return notification.getNotification().getBubbleMetadata();
271     }
272 
273     /**
274      * Resets the notification entry to be re-used.
275      */
reset()276     public void reset() {
277         if (row != null) {
278             row.reset();
279         }
280     }
281 
getRow()282     public ExpandableNotificationRow getRow() {
283         return row;
284     }
285 
286     //TODO: This will go away when we have a way to bind an entry to a row
setRow(ExpandableNotificationRow row)287     public void setRow(ExpandableNotificationRow row) {
288         this.row = row;
289     }
290 
291     @Nullable
getChildren()292     public List<NotificationEntry> getChildren() {
293         if (row == null) {
294             return null;
295         }
296 
297         List<ExpandableNotificationRow> rowChildren = row.getNotificationChildren();
298         if (rowChildren == null) {
299             return null;
300         }
301 
302         ArrayList<NotificationEntry> children = new ArrayList<>();
303         for (ExpandableNotificationRow child : rowChildren) {
304             children.add(child.getEntry());
305         }
306 
307         return children;
308     }
309 
notifyFullScreenIntentLaunched()310     public void notifyFullScreenIntentLaunched() {
311         setInterruption();
312         lastFullScreenIntentLaunchTime = SystemClock.elapsedRealtime();
313     }
314 
hasJustLaunchedFullScreenIntent()315     public boolean hasJustLaunchedFullScreenIntent() {
316         return SystemClock.elapsedRealtime() < lastFullScreenIntentLaunchTime + LAUNCH_COOLDOWN;
317     }
318 
hasJustSentRemoteInput()319     public boolean hasJustSentRemoteInput() {
320         return SystemClock.elapsedRealtime() < lastRemoteInputSent + REMOTE_INPUT_COOLDOWN;
321     }
322 
hasFinishedInitialization()323     public boolean hasFinishedInitialization() {
324         return initializationTime == -1
325                 || SystemClock.elapsedRealtime() > initializationTime + INITIALIZATION_DELAY;
326     }
327 
328     /**
329      * Create the icons for a notification
330      * @param context the context to create the icons with
331      * @param sbn the notification
332      * @throws InflationException Exception if required icons are not valid or specified
333      */
createIcons(Context context, StatusBarNotification sbn)334     public void createIcons(Context context, StatusBarNotification sbn)
335             throws InflationException {
336         Notification n = sbn.getNotification();
337         final Icon smallIcon = n.getSmallIcon();
338         if (smallIcon == null) {
339             throw new InflationException("No small icon in notification from "
340                     + sbn.getPackageName());
341         }
342 
343         // Construct the icon.
344         icon = new StatusBarIconView(context,
345                 sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn);
346         icon.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
347 
348         // Construct the expanded icon.
349         expandedIcon = new StatusBarIconView(context,
350                 sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn);
351         expandedIcon.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
352 
353         final StatusBarIcon ic = new StatusBarIcon(
354                 sbn.getUser(),
355                 sbn.getPackageName(),
356                 smallIcon,
357                 n.iconLevel,
358                 n.number,
359                 StatusBarIconView.contentDescForNotification(context, n));
360 
361         if (!icon.set(ic) || !expandedIcon.set(ic)) {
362             icon = null;
363             expandedIcon = null;
364             centeredIcon = null;
365             throw new InflationException("Couldn't create icon: " + ic);
366         }
367         expandedIcon.setVisibility(View.INVISIBLE);
368         expandedIcon.setOnVisibilityChangedListener(
369                 newVisibility -> {
370                     if (row != null) {
371                         row.setIconsVisible(newVisibility != View.VISIBLE);
372                     }
373                 });
374 
375         // Construct the centered icon
376         if (notification.getNotification().isMediaNotification()) {
377             centeredIcon = new StatusBarIconView(context,
378                     sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn);
379             centeredIcon.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
380 
381             if (!centeredIcon.set(ic)) {
382                 centeredIcon = null;
383                 throw new InflationException("Couldn't update centered icon: " + ic);
384             }
385         }
386     }
387 
setIconTag(int key, Object tag)388     public void setIconTag(int key, Object tag) {
389         if (icon != null) {
390             icon.setTag(key, tag);
391             expandedIcon.setTag(key, tag);
392         }
393 
394         if (centeredIcon != null) {
395             centeredIcon.setTag(key, tag);
396         }
397     }
398 
399     /**
400      * Update the notification icons.
401      *
402      * @param context the context to create the icons with.
403      * @param sbn the notification to read the icon from.
404      * @throws InflationException Exception if required icons are not valid or specified
405      */
updateIcons(Context context, StatusBarNotification sbn)406     public void updateIcons(Context context, StatusBarNotification sbn)
407             throws InflationException {
408         if (icon != null) {
409             // Update the icon
410             Notification n = sbn.getNotification();
411             final StatusBarIcon ic = new StatusBarIcon(
412                     notification.getUser(),
413                     notification.getPackageName(),
414                     n.getSmallIcon(),
415                     n.iconLevel,
416                     n.number,
417                     StatusBarIconView.contentDescForNotification(context, n));
418             icon.setNotification(sbn);
419             expandedIcon.setNotification(sbn);
420             if (!icon.set(ic) || !expandedIcon.set(ic)) {
421                 throw new InflationException("Couldn't update icon: " + ic);
422             }
423 
424             if (centeredIcon != null) {
425                 centeredIcon.setNotification(sbn);
426                 if (!centeredIcon.set(ic)) {
427                     throw new InflationException("Couldn't update centered icon: " + ic);
428                 }
429             }
430         }
431     }
432 
getContrastedColor(Context context, boolean isLowPriority, int backgroundColor)433     public int getContrastedColor(Context context, boolean isLowPriority,
434             int backgroundColor) {
435         int rawColor = isLowPriority ? Notification.COLOR_DEFAULT :
436                 notification.getNotification().color;
437         if (mCachedContrastColorIsFor == rawColor && mCachedContrastColor != COLOR_INVALID) {
438             return mCachedContrastColor;
439         }
440         final int contrasted = ContrastColorUtil.resolveContrastColor(context, rawColor,
441                 backgroundColor);
442         mCachedContrastColorIsFor = rawColor;
443         mCachedContrastColor = contrasted;
444         return mCachedContrastColor;
445     }
446 
447     /**
448      * Returns our best guess for the most relevant text summary of the latest update to this
449      * notification, based on its type. Returns null if there should not be an update message.
450      */
getUpdateMessage(Context context)451     public CharSequence getUpdateMessage(Context context) {
452         final Notification underlyingNotif = notification.getNotification();
453         final Class<? extends Notification.Style> style = underlyingNotif.getNotificationStyle();
454 
455         try {
456             if (Notification.BigTextStyle.class.equals(style)) {
457                 // Return the big text, it is big so probably important. If it's not there use the
458                 // normal text.
459                 CharSequence bigText =
460                         underlyingNotif.extras.getCharSequence(Notification.EXTRA_BIG_TEXT);
461                 return !TextUtils.isEmpty(bigText)
462                         ? bigText
463                         : underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT);
464             } else if (Notification.MessagingStyle.class.equals(style)) {
465                 final List<Notification.MessagingStyle.Message> messages =
466                         Notification.MessagingStyle.Message.getMessagesFromBundleArray(
467                                 (Parcelable[]) underlyingNotif.extras.get(
468                                         Notification.EXTRA_MESSAGES));
469 
470                 final Notification.MessagingStyle.Message latestMessage =
471                         Notification.MessagingStyle.findLatestIncomingMessage(messages);
472 
473                 if (latestMessage != null) {
474                     final CharSequence personName = latestMessage.getSenderPerson() != null
475                             ? latestMessage.getSenderPerson().getName()
476                             : null;
477 
478                     // Prepend the sender name if available since group chats also use messaging
479                     // style.
480                     if (!TextUtils.isEmpty(personName)) {
481                         return context.getResources().getString(
482                                 R.string.notification_summary_message_format,
483                                 personName,
484                                 latestMessage.getText());
485                     } else {
486                         return latestMessage.getText();
487                     }
488                 }
489             } else if (Notification.InboxStyle.class.equals(style)) {
490                 CharSequence[] lines =
491                         underlyingNotif.extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES);
492 
493                 // Return the last line since it should be the most recent.
494                 if (lines != null && lines.length > 0) {
495                     return lines[lines.length - 1];
496                 }
497             } else if (Notification.MediaStyle.class.equals(style)) {
498                 // Return nothing, media updates aren't typically useful as a text update.
499                 return null;
500             } else {
501                 // Default to text extra.
502                 return underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT);
503             }
504         } catch (ClassCastException | NullPointerException | ArrayIndexOutOfBoundsException e) {
505             // No use crashing, we'll just return null and the caller will assume there's no update
506             // message.
507             e.printStackTrace();
508         }
509 
510         return null;
511     }
512 
513     /**
514      * Abort all existing inflation tasks
515      */
abortTask()516     public void abortTask() {
517         if (mRunningTask != null) {
518             mRunningTask.abort();
519             mRunningTask = null;
520         }
521     }
522 
setInflationTask(InflationTask abortableTask)523     public void setInflationTask(InflationTask abortableTask) {
524         // abort any existing inflation
525         InflationTask existing = mRunningTask;
526         abortTask();
527         mRunningTask = abortableTask;
528         if (existing != null && mRunningTask != null) {
529             mRunningTask.supersedeTask(existing);
530         }
531     }
532 
onInflationTaskFinished()533     public void onInflationTaskFinished() {
534         mRunningTask = null;
535     }
536 
537     @VisibleForTesting
getRunningTask()538     public InflationTask getRunningTask() {
539         return mRunningTask;
540     }
541 
542     /**
543      * Set a throwable that is used for debugging
544      *
545      * @param debugThrowable the throwable to save
546      */
setDebugThrowable(Throwable debugThrowable)547     public void setDebugThrowable(Throwable debugThrowable) {
548         mDebugThrowable = debugThrowable;
549     }
550 
getDebugThrowable()551     public Throwable getDebugThrowable() {
552         return mDebugThrowable;
553     }
554 
onRemoteInputInserted()555     public void onRemoteInputInserted() {
556         lastRemoteInputSent = NOT_LAUNCHED_YET;
557         remoteInputTextWhenReset = null;
558     }
559 
setHasSentReply()560     public void setHasSentReply() {
561         hasSentReply = true;
562     }
563 
isLastMessageFromReply()564     public boolean isLastMessageFromReply() {
565         if (!hasSentReply) {
566             return false;
567         }
568         Bundle extras = notification.getNotification().extras;
569         CharSequence[] replyTexts = extras.getCharSequenceArray(
570                 Notification.EXTRA_REMOTE_INPUT_HISTORY);
571         if (!ArrayUtils.isEmpty(replyTexts)) {
572             return true;
573         }
574         Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
575         if (messages != null && messages.length > 0) {
576             Parcelable message = messages[messages.length - 1];
577             if (message instanceof Bundle) {
578                 Notification.MessagingStyle.Message lastMessage =
579                         Notification.MessagingStyle.Message.getMessageFromBundle(
580                                 (Bundle) message);
581                 if (lastMessage != null) {
582                     Person senderPerson = lastMessage.getSenderPerson();
583                     if (senderPerson == null) {
584                         return true;
585                     }
586                     Person user = extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON);
587                     return Objects.equals(user, senderPerson);
588                 }
589             }
590         }
591         return false;
592     }
593 
setInitializationTime(long time)594     public void setInitializationTime(long time) {
595         if (initializationTime == -1) {
596             initializationTime = time;
597         }
598     }
599 
sendAccessibilityEvent(int eventType)600     public void sendAccessibilityEvent(int eventType) {
601         if (row != null) {
602             row.sendAccessibilityEvent(eventType);
603         }
604     }
605 
606     /**
607      * Used by NotificationMediaManager to determine... things
608      * @return {@code true} if we are a media notification
609      */
isMediaNotification()610     public boolean isMediaNotification() {
611         if (row == null) return false;
612 
613         return row.isMediaRow();
614     }
615 
616     /**
617      * We are a top level child if our parent is the list of notifications duh
618      * @return {@code true} if we're a top level notification
619      */
isTopLevelChild()620     public boolean isTopLevelChild() {
621         return row != null && row.isTopLevelChild();
622     }
623 
resetUserExpansion()624     public void resetUserExpansion() {
625         if (row != null) row.resetUserExpansion();
626     }
627 
freeContentViewWhenSafe(@nflationFlag int inflationFlag)628     public void freeContentViewWhenSafe(@InflationFlag int inflationFlag) {
629         if (row != null) row.freeContentViewWhenSafe(inflationFlag);
630     }
631 
setAmbientPulsing(boolean pulsing)632     public void setAmbientPulsing(boolean pulsing) {
633         if (row != null) row.setAmbientPulsing(pulsing);
634     }
635 
rowExists()636     public boolean rowExists() {
637         return row != null;
638     }
639 
isRowDismissed()640     public boolean isRowDismissed() {
641         return row != null && row.isDismissed();
642     }
643 
isRowRemoved()644     public boolean isRowRemoved() {
645         return row != null && row.isRemoved();
646     }
647 
648     /**
649      * @return {@code true} if the row is null or removed
650      */
isRemoved()651     public boolean isRemoved() {
652         //TODO: recycling invalidates this
653         return row == null || row.isRemoved();
654     }
655 
isRowPinned()656     public boolean isRowPinned() {
657         return row != null && row.isPinned();
658     }
659 
setRowPinned(boolean pinned)660     public void setRowPinned(boolean pinned) {
661         if (row != null) row.setPinned(pinned);
662     }
663 
isRowAnimatingAway()664     public boolean isRowAnimatingAway() {
665         return row != null && row.isHeadsUpAnimatingAway();
666     }
667 
isRowHeadsUp()668     public boolean isRowHeadsUp() {
669         return row != null && row.isHeadsUp();
670     }
671 
setHeadsUp(boolean shouldHeadsUp)672     public void setHeadsUp(boolean shouldHeadsUp) {
673         if (row != null) row.setHeadsUp(shouldHeadsUp);
674     }
675 
676 
setAmbientGoingAway(boolean goingAway)677     public void setAmbientGoingAway(boolean goingAway) {
678         if (row != null) row.setAmbientGoingAway(goingAway);
679     }
680 
681 
mustStayOnScreen()682     public boolean mustStayOnScreen() {
683         return row != null && row.mustStayOnScreen();
684     }
685 
setHeadsUpIsVisible()686     public void setHeadsUpIsVisible() {
687         if (row != null) row.setHeadsUpIsVisible();
688     }
689 
690     //TODO: i'm imagining a world where this isn't just the row, but I could be rwong
getHeadsUpAnimationView()691     public ExpandableNotificationRow getHeadsUpAnimationView() {
692         return row;
693     }
694 
setUserLocked(boolean userLocked)695     public void setUserLocked(boolean userLocked) {
696         if (row != null) row.setUserLocked(userLocked);
697     }
698 
setUserExpanded(boolean userExpanded, boolean allowChildExpansion)699     public void setUserExpanded(boolean userExpanded, boolean allowChildExpansion) {
700         if (row != null) row.setUserExpanded(userExpanded, allowChildExpansion);
701     }
702 
setGroupExpansionChanging(boolean changing)703     public void setGroupExpansionChanging(boolean changing) {
704         if (row != null) row.setGroupExpansionChanging(changing);
705     }
706 
notifyHeightChanged(boolean needsAnimation)707     public void notifyHeightChanged(boolean needsAnimation) {
708         if (row != null) row.notifyHeightChanged(needsAnimation);
709     }
710 
closeRemoteInput()711     public void closeRemoteInput() {
712         if (row != null) row.closeRemoteInput();
713     }
714 
areChildrenExpanded()715     public boolean areChildrenExpanded() {
716         return row != null && row.areChildrenExpanded();
717     }
718 
keepInParent()719     public boolean keepInParent() {
720         return row != null && row.keepInParent();
721     }
722 
723     //TODO: probably less confusing to say "is group fully visible"
isGroupNotFullyVisible()724     public boolean isGroupNotFullyVisible() {
725         return row == null || row.isGroupNotFullyVisible();
726     }
727 
getGuts()728     public NotificationGuts getGuts() {
729         if (row != null) return row.getGuts();
730         return null;
731     }
732 
removeRow()733     public void removeRow() {
734         if (row != null) row.setRemoved();
735     }
736 
isSummaryWithChildren()737     public boolean isSummaryWithChildren() {
738         return row != null && row.isSummaryWithChildren();
739     }
740 
setKeepInParent(boolean keep)741     public void setKeepInParent(boolean keep) {
742         if (row != null) row.setKeepInParent(keep);
743     }
744 
onDensityOrFontScaleChanged()745     public void onDensityOrFontScaleChanged() {
746         if (row != null) row.onDensityOrFontScaleChanged();
747     }
748 
areGutsExposed()749     public boolean areGutsExposed() {
750         return row != null && row.getGuts() != null && row.getGuts().isExposed();
751     }
752 
isChildInGroup()753     public boolean isChildInGroup() {
754         return parent == null;
755     }
756 
757     /**
758      * @return Can the underlying notification be cleared? This can be different from whether the
759      *         notification can be dismissed in case notifications are sensitive on the lockscreen.
760      * @see #canViewBeDismissed()
761      */
isClearable()762     public boolean isClearable() {
763         if (notification == null || !notification.isClearable()) {
764             return false;
765         }
766 
767         List<NotificationEntry> children = getChildren();
768         if (children != null && children.size() > 0) {
769             for (int i = 0; i < children.size(); i++) {
770                 NotificationEntry child =  children.get(i);
771                 if (!child.isClearable()) {
772                     return false;
773                 }
774             }
775         }
776         return true;
777     }
778 
canViewBeDismissed()779     public boolean canViewBeDismissed() {
780         if (row == null) return true;
781         return row.canViewBeDismissed();
782     }
783 
784     @VisibleForTesting
isExemptFromDndVisualSuppression()785     boolean isExemptFromDndVisualSuppression() {
786         if (isNotificationBlockedByPolicy(notification.getNotification())) {
787             return false;
788         }
789 
790         if ((notification.getNotification().flags
791                 & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
792             return true;
793         }
794         if (notification.getNotification().isMediaNotification()) {
795             return true;
796         }
797         if (mIsSystemNotification != null && mIsSystemNotification) {
798             return true;
799         }
800         return false;
801     }
802 
shouldSuppressVisualEffect(int effect)803     private boolean shouldSuppressVisualEffect(int effect) {
804         if (isExemptFromDndVisualSuppression()) {
805             return false;
806         }
807         return (suppressedVisualEffects & effect) != 0;
808     }
809 
810     /**
811      * Returns whether {@link Policy#SUPPRESSED_EFFECT_FULL_SCREEN_INTENT}
812      * is set for this entry.
813      */
shouldSuppressFullScreenIntent()814     public boolean shouldSuppressFullScreenIntent() {
815         return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_FULL_SCREEN_INTENT);
816     }
817 
818     /**
819      * Returns whether {@link Policy#SUPPRESSED_EFFECT_PEEK}
820      * is set for this entry.
821      */
shouldSuppressPeek()822     public boolean shouldSuppressPeek() {
823         return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_PEEK);
824     }
825 
826     /**
827      * Returns whether {@link Policy#SUPPRESSED_EFFECT_STATUS_BAR}
828      * is set for this entry.
829      */
shouldSuppressStatusBar()830     public boolean shouldSuppressStatusBar() {
831         return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_STATUS_BAR);
832     }
833 
834     /**
835      * Returns whether {@link Policy#SUPPRESSED_EFFECT_AMBIENT}
836      * is set for this entry.
837      */
shouldSuppressAmbient()838     public boolean shouldSuppressAmbient() {
839         return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_AMBIENT);
840     }
841 
842     /**
843      * Returns whether {@link Policy#SUPPRESSED_EFFECT_NOTIFICATION_LIST}
844      * is set for this entry.
845      */
shouldSuppressNotificationList()846     public boolean shouldSuppressNotificationList() {
847         return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_NOTIFICATION_LIST);
848     }
849 
850     /**
851      * Categories that are explicitly called out on DND settings screens are always blocked, if
852      * DND has flagged them, even if they are foreground or system notifications that might
853      * otherwise visually bypass DND.
854      */
isNotificationBlockedByPolicy(Notification n)855     private static boolean isNotificationBlockedByPolicy(Notification n) {
856         return isCategory(CATEGORY_CALL, n)
857                 || isCategory(CATEGORY_MESSAGE, n)
858                 || isCategory(CATEGORY_ALARM, n)
859                 || isCategory(CATEGORY_EVENT, n)
860                 || isCategory(CATEGORY_REMINDER, n);
861     }
862 
isCategory(String category, Notification n)863     private static boolean isCategory(String category, Notification n) {
864         return Objects.equals(n.category, category);
865     }
866 
867     /** Information about a suggestion that is being edited. */
868     public static class EditedSuggestionInfo {
869 
870         /**
871          * The value of the suggestion (before any user edits).
872          */
873         public final CharSequence originalText;
874 
875         /**
876          * The index of the suggestion that is being edited.
877          */
878         public final int index;
879 
EditedSuggestionInfo(CharSequence originalText, int index)880         public EditedSuggestionInfo(CharSequence originalText, int index) {
881             this.originalText = originalText;
882             this.index = index;
883         }
884     }
885 
886     /**
887      * Returns whether the notification is a foreground service. It shows that this is an ongoing
888      * bubble.
889      */
isForegroundService()890     public boolean isForegroundService() {
891         return (notification.getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE) != 0;
892     }
893 }
894