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