blob: 92c261c4cad7c83959208678dc119653e48058d7 [file] [log] [blame]
Ned Burnsf81c4c42019-01-07 14:10:43 -05001/*
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
17package com.android.systemui.statusbar.notification.collection;
18
19import static android.app.Notification.CATEGORY_ALARM;
20import static android.app.Notification.CATEGORY_CALL;
21import static android.app.Notification.CATEGORY_EVENT;
22import static android.app.Notification.CATEGORY_MESSAGE;
23import static android.app.Notification.CATEGORY_REMINDER;
Mady Mellorfc02cc32019-04-01 14:47:55 -070024import static android.app.Notification.FLAG_BUBBLE;
Ned Burnsf81c4c42019-01-07 14:10:43 -050025import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_AMBIENT;
26import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_FULL_SCREEN_INTENT;
27import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_NOTIFICATION_LIST;
28import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK;
29import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_STATUS_BAR;
30
31import android.annotation.NonNull;
32import android.app.Notification;
33import android.app.NotificationChannel;
34import android.app.NotificationManager.Policy;
35import android.app.Person;
36import android.content.Context;
37import android.graphics.drawable.Icon;
38import android.os.Bundle;
39import android.os.Parcelable;
40import android.os.SystemClock;
41import android.service.notification.NotificationListenerService;
42import android.service.notification.SnoozeCriterion;
43import android.service.notification.StatusBarNotification;
Joshua Tsuji614b1df2019-03-26 13:57:05 -040044import android.text.TextUtils;
Ned Burnsf81c4c42019-01-07 14:10:43 -050045import android.util.ArraySet;
46import android.view.View;
47import android.widget.ImageView;
48
49import androidx.annotation.Nullable;
50
51import com.android.internal.annotations.VisibleForTesting;
52import com.android.internal.statusbar.StatusBarIcon;
53import com.android.internal.util.ArrayUtils;
54import com.android.internal.util.ContrastColorUtil;
Joshua Tsuji614b1df2019-03-26 13:57:05 -040055import com.android.systemui.R;
Ned Burnsf81c4c42019-01-07 14:10:43 -050056import com.android.systemui.statusbar.InflationTask;
57import com.android.systemui.statusbar.StatusBarIconView;
58import com.android.systemui.statusbar.notification.InflationException;
59import com.android.systemui.statusbar.notification.row.ExpandableNotificationRow;
Ned Burns1a5e22f2019-02-14 15:11:52 -050060import com.android.systemui.statusbar.notification.row.NotificationContentInflater.InflationFlag;
Ned Burnsf81c4c42019-01-07 14:10:43 -050061import com.android.systemui.statusbar.notification.row.NotificationGuts;
Ned Burnsf81c4c42019-01-07 14:10:43 -050062
63import java.util.ArrayList;
64import java.util.Collections;
65import java.util.List;
66import 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 */
83public 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;
Beverly40770652019-02-15 15:49:49 -050098 public StatusBarIconView centeredIcon;
Ned Burnsf81c4c42019-01-07 14:10:43 -050099 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();
Gustav Senntone255f902019-01-31 13:28:02 +0000109 /** Smart replies provided by the NotificationAssistantService. */
110 @NonNull
111 public CharSequence[] systemGeneratedSmartReplies = new CharSequence[0];
Milo Sredkov13d88112019-02-01 12:23:24 +0000112
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
Ned Burnsf81c4c42019-01-07 14:10:43 -0500120 @VisibleForTesting
121 public int suppressedVisualEffects;
122 public boolean suspended;
123
124 private NotificationEntry parent; // our parent (if we're in a group)
Ned Burnsf81c4c42019-01-07 14:10:43 -0500125 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 /**
Julia Reynolds4509ce72019-01-31 13:12:43 -0500152 * 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 /**
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800158 * 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 /**
Ned Burnsf81c4c42019-01-07 14:10:43 -0500166 * Whether the user has dismissed this notification when it was in bubble form.
167 */
168 private boolean mUserDismissedBubble;
169
Gus Prevascaed15c2019-01-18 14:19:51 -0500170 /**
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
Ned Burnsf81c4c42019-01-07 14:10:43 -0500176 public NotificationEntry(StatusBarNotification n) {
177 this(n, null);
178 }
179
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
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();
Gustav Senntone255f902019-01-31 13:28:02 +0000199 systemGeneratedSmartReplies = ranking.getSmartReplies() == null
Ned Burnsf81c4c42019-01-07 14:10:43 -0500200 ? new CharSequence[0]
201 : ranking.getSmartReplies().toArray(new CharSequence[0]);
202 suppressedVisualEffects = ranking.getSuppressedVisualEffects();
203 suspended = ranking.isSuspended();
Julia Reynolds4509ce72019-01-31 13:12:43 -0500204 canBubble = ranking.canBubble();
Ned Burnsf81c4c42019-01-07 14:10:43 -0500205 }
206
207 public void setInterruption() {
208 interruption = true;
209 }
210
211 public boolean hasInterrupted() {
212 return interruption;
213 }
214
Gus Prevascaed15c2019-01-18 14:19:51 -0500215 public boolean isHighPriority() {
216 return mHighPriority;
217 }
218
219 public void setIsHighPriority(boolean highPriority) {
220 this.mHighPriority = highPriority;
221 }
222
Ned Burnsf81c4c42019-01-07 14:10:43 -0500223 public boolean isBubble() {
Mady Mellorfc02cc32019-04-01 14:47:55 -0700224 return (notification.getNotification().flags & FLAG_BUBBLE) != 0;
Ned Burnsf81c4c42019-01-07 14:10:43 -0500225 }
226
227 public void setBubbleDismissed(boolean userDismissed) {
228 mUserDismissedBubble = userDismissed;
229 }
230
231 public boolean isBubbleDismissed() {
232 return mUserDismissedBubble;
233 }
234
235 /**
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800236 * Sets whether this notification should be shown in the shade when it is also displayed as a
237 * bubble.
238 */
239 public void setShowInShadeWhenBubble(boolean showInShade) {
240 mShowInShadeWhenBubble = showInShade;
241 }
242
243 /**
244 * Whether this notification should be shown in the shade when it is also displayed as a
245 * bubble.
246 */
247 public boolean showInShadeWhenBubble() {
248 // We always show it in the shade if non-clearable
Mady Mellorc2ff0112019-03-28 14:18:06 -0700249 return !isRowDismissed() && (!isClearable() || mShowInShadeWhenBubble);
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800250 }
251
252 /**
Mady Mellor9801e852019-01-22 14:50:28 -0800253 * Returns the data needed for a bubble for this notification, if it exists.
254 */
255 public Notification.BubbleMetadata getBubbleMetadata() {
256 return notification.getNotification().getBubbleMetadata();
257 }
258
259 /**
Ned Burnsf81c4c42019-01-07 14:10:43 -0500260 * Resets the notification entry to be re-used.
261 */
262 public void reset() {
263 if (row != null) {
264 row.reset();
265 }
266 }
267
268 public ExpandableNotificationRow getRow() {
269 return row;
270 }
271
272 //TODO: This will go away when we have a way to bind an entry to a row
273 public void setRow(ExpandableNotificationRow row) {
274 this.row = row;
275 }
276
277 @Nullable
278 public List<NotificationEntry> getChildren() {
Evan Lairdce2d1af2019-05-30 16:00:22 -0400279 if (row == null) {
Ned Burnsf81c4c42019-01-07 14:10:43 -0500280 return null;
281 }
282
Evan Lairdce2d1af2019-05-30 16:00:22 -0400283 List<ExpandableNotificationRow> rowChildren = row.getNotificationChildren();
284 if (rowChildren == null) {
285 return null;
286 }
287
288 ArrayList<NotificationEntry> children = new ArrayList<>();
289 for (ExpandableNotificationRow child : rowChildren) {
290 children.add(child.getEntry());
291 }
292
Ned Burnsf81c4c42019-01-07 14:10:43 -0500293 return children;
294 }
295
296 public void notifyFullScreenIntentLaunched() {
297 setInterruption();
298 lastFullScreenIntentLaunchTime = SystemClock.elapsedRealtime();
299 }
300
301 public boolean hasJustLaunchedFullScreenIntent() {
302 return SystemClock.elapsedRealtime() < lastFullScreenIntentLaunchTime + LAUNCH_COOLDOWN;
303 }
304
305 public boolean hasJustSentRemoteInput() {
306 return SystemClock.elapsedRealtime() < lastRemoteInputSent + REMOTE_INPUT_COOLDOWN;
307 }
308
309 public boolean hasFinishedInitialization() {
310 return initializationTime == -1
311 || SystemClock.elapsedRealtime() > initializationTime + INITIALIZATION_DELAY;
312 }
313
314 /**
315 * Create the icons for a notification
316 * @param context the context to create the icons with
317 * @param sbn the notification
318 * @throws InflationException Exception if required icons are not valid or specified
319 */
320 public void createIcons(Context context, StatusBarNotification sbn)
321 throws InflationException {
322 Notification n = sbn.getNotification();
323 final Icon smallIcon = n.getSmallIcon();
324 if (smallIcon == null) {
325 throw new InflationException("No small icon in notification from "
326 + sbn.getPackageName());
327 }
328
329 // Construct the icon.
330 icon = new StatusBarIconView(context,
331 sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn);
332 icon.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
333
334 // Construct the expanded icon.
335 expandedIcon = new StatusBarIconView(context,
336 sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn);
337 expandedIcon.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
Beverly40770652019-02-15 15:49:49 -0500338
Ned Burnsf81c4c42019-01-07 14:10:43 -0500339 final StatusBarIcon ic = new StatusBarIcon(
340 sbn.getUser(),
341 sbn.getPackageName(),
342 smallIcon,
343 n.iconLevel,
344 n.number,
345 StatusBarIconView.contentDescForNotification(context, n));
Beverly40770652019-02-15 15:49:49 -0500346
Ned Burnsf81c4c42019-01-07 14:10:43 -0500347 if (!icon.set(ic) || !expandedIcon.set(ic)) {
348 icon = null;
349 expandedIcon = null;
Beverly40770652019-02-15 15:49:49 -0500350 centeredIcon = null;
Ned Burnsf81c4c42019-01-07 14:10:43 -0500351 throw new InflationException("Couldn't create icon: " + ic);
352 }
353 expandedIcon.setVisibility(View.INVISIBLE);
354 expandedIcon.setOnVisibilityChangedListener(
355 newVisibility -> {
356 if (row != null) {
357 row.setIconsVisible(newVisibility != View.VISIBLE);
358 }
359 });
Beverly40770652019-02-15 15:49:49 -0500360
361 // Construct the centered icon
362 if (notification.getNotification().isMediaNotification()) {
363 centeredIcon = new StatusBarIconView(context,
364 sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn);
365 centeredIcon.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
366
367 if (!centeredIcon.set(ic)) {
368 centeredIcon = null;
369 throw new InflationException("Couldn't update centered icon: " + ic);
370 }
371 }
Ned Burnsf81c4c42019-01-07 14:10:43 -0500372 }
373
374 public void setIconTag(int key, Object tag) {
375 if (icon != null) {
376 icon.setTag(key, tag);
377 expandedIcon.setTag(key, tag);
378 }
Beverly40770652019-02-15 15:49:49 -0500379
380 if (centeredIcon != null) {
381 centeredIcon.setTag(key, tag);
382 }
Ned Burnsf81c4c42019-01-07 14:10:43 -0500383 }
384
385 /**
386 * Update the notification icons.
387 *
388 * @param context the context to create the icons with.
389 * @param sbn the notification to read the icon from.
390 * @throws InflationException Exception if required icons are not valid or specified
391 */
392 public void updateIcons(Context context, StatusBarNotification sbn)
393 throws InflationException {
394 if (icon != null) {
395 // Update the icon
396 Notification n = sbn.getNotification();
397 final StatusBarIcon ic = new StatusBarIcon(
398 notification.getUser(),
399 notification.getPackageName(),
400 n.getSmallIcon(),
401 n.iconLevel,
402 n.number,
403 StatusBarIconView.contentDescForNotification(context, n));
404 icon.setNotification(sbn);
405 expandedIcon.setNotification(sbn);
406 if (!icon.set(ic) || !expandedIcon.set(ic)) {
407 throw new InflationException("Couldn't update icon: " + ic);
408 }
Beverly40770652019-02-15 15:49:49 -0500409
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 }
Ned Burnsf81c4c42019-01-07 14:10:43 -0500416 }
417 }
418
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 /**
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400434 * Returns our best guess for the most relevant text summary of the latest update to this
435 * notification, based on its type. Returns null if there should not be an update message.
436 */
437 public CharSequence getUpdateMessage(Context context) {
438 final Notification underlyingNotif = notification.getNotification();
439 final Class<? extends Notification.Style> style = underlyingNotif.getNotificationStyle();
440
441 try {
442 if (Notification.BigTextStyle.class.equals(style)) {
443 // Return the big text, it is big so probably important. If it's not there use the
444 // normal text.
445 CharSequence bigText =
446 underlyingNotif.extras.getCharSequence(Notification.EXTRA_BIG_TEXT);
447 return !TextUtils.isEmpty(bigText)
448 ? bigText
449 : underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT);
450 } else if (Notification.MessagingStyle.class.equals(style)) {
451 final List<Notification.MessagingStyle.Message> messages =
452 Notification.MessagingStyle.Message.getMessagesFromBundleArray(
453 (Parcelable[]) underlyingNotif.extras.get(
454 Notification.EXTRA_MESSAGES));
455
456 final Notification.MessagingStyle.Message latestMessage =
457 Notification.MessagingStyle.findLatestIncomingMessage(messages);
458
459 if (latestMessage != null) {
460 final CharSequence personName = latestMessage.getSenderPerson() != null
461 ? latestMessage.getSenderPerson().getName()
462 : null;
463
464 // Prepend the sender name if available since group chats also use messaging
465 // style.
466 if (!TextUtils.isEmpty(personName)) {
467 return context.getResources().getString(
468 R.string.notification_summary_message_format,
469 personName,
470 latestMessage.getText());
471 } else {
472 return latestMessage.getText();
473 }
474 }
475 } else if (Notification.InboxStyle.class.equals(style)) {
476 CharSequence[] lines =
477 underlyingNotif.extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES);
478
479 // Return the last line since it should be the most recent.
480 if (lines != null && lines.length > 0) {
481 return lines[lines.length - 1];
482 }
483 } else if (Notification.MediaStyle.class.equals(style)) {
484 // Return nothing, media updates aren't typically useful as a text update.
485 return null;
486 } else {
487 // Default to text extra.
488 return underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT);
489 }
490 } catch (ClassCastException | NullPointerException | ArrayIndexOutOfBoundsException e) {
491 // No use crashing, we'll just return null and the caller will assume there's no update
492 // message.
493 e.printStackTrace();
494 }
495
496 return null;
497 }
498
499 /**
Ned Burnsf81c4c42019-01-07 14:10:43 -0500500 * Abort all existing inflation tasks
501 */
502 public void abortTask() {
503 if (mRunningTask != null) {
504 mRunningTask.abort();
505 mRunningTask = null;
506 }
507 }
508
509 public void setInflationTask(InflationTask abortableTask) {
510 // abort any existing inflation
511 InflationTask existing = mRunningTask;
512 abortTask();
513 mRunningTask = abortableTask;
514 if (existing != null && mRunningTask != null) {
515 mRunningTask.supersedeTask(existing);
516 }
517 }
518
519 public void onInflationTaskFinished() {
520 mRunningTask = null;
521 }
522
523 @VisibleForTesting
524 public InflationTask getRunningTask() {
525 return mRunningTask;
526 }
527
528 /**
529 * Set a throwable that is used for debugging
530 *
531 * @param debugThrowable the throwable to save
532 */
533 public void setDebugThrowable(Throwable debugThrowable) {
534 mDebugThrowable = debugThrowable;
535 }
536
537 public Throwable getDebugThrowable() {
538 return mDebugThrowable;
539 }
540
541 public void onRemoteInputInserted() {
542 lastRemoteInputSent = NOT_LAUNCHED_YET;
543 remoteInputTextWhenReset = null;
544 }
545
546 public void setHasSentReply() {
547 hasSentReply = true;
548 }
549
550 public boolean isLastMessageFromReply() {
551 if (!hasSentReply) {
552 return false;
553 }
554 Bundle extras = notification.getNotification().extras;
555 CharSequence[] replyTexts = extras.getCharSequenceArray(
556 Notification.EXTRA_REMOTE_INPUT_HISTORY);
557 if (!ArrayUtils.isEmpty(replyTexts)) {
558 return true;
559 }
560 Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
561 if (messages != null && messages.length > 0) {
562 Parcelable message = messages[messages.length - 1];
563 if (message instanceof Bundle) {
564 Notification.MessagingStyle.Message lastMessage =
565 Notification.MessagingStyle.Message.getMessageFromBundle(
566 (Bundle) message);
567 if (lastMessage != null) {
568 Person senderPerson = lastMessage.getSenderPerson();
569 if (senderPerson == null) {
570 return true;
571 }
572 Person user = extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON);
573 return Objects.equals(user, senderPerson);
574 }
575 }
576 }
577 return false;
578 }
579
580 public void setInitializationTime(long time) {
581 if (initializationTime == -1) {
582 initializationTime = time;
583 }
584 }
585
586 public void sendAccessibilityEvent(int eventType) {
587 if (row != null) {
588 row.sendAccessibilityEvent(eventType);
589 }
590 }
591
592 /**
593 * Used by NotificationMediaManager to determine... things
594 * @return {@code true} if we are a media notification
595 */
596 public boolean isMediaNotification() {
597 if (row == null) return false;
598
599 return row.isMediaRow();
600 }
601
602 /**
603 * We are a top level child if our parent is the list of notifications duh
604 * @return {@code true} if we're a top level notification
605 */
606 public boolean isTopLevelChild() {
607 return row != null && row.isTopLevelChild();
608 }
609
610 public void resetUserExpansion() {
611 if (row != null) row.resetUserExpansion();
612 }
613
Ned Burns1a5e22f2019-02-14 15:11:52 -0500614 public void freeContentViewWhenSafe(@InflationFlag int inflationFlag) {
Ned Burnsf81c4c42019-01-07 14:10:43 -0500615 if (row != null) row.freeContentViewWhenSafe(inflationFlag);
616 }
617
618 public void setAmbientPulsing(boolean pulsing) {
619 if (row != null) row.setAmbientPulsing(pulsing);
620 }
621
622 public boolean rowExists() {
623 return row != null;
624 }
625
626 public boolean isRowDismissed() {
627 return row != null && row.isDismissed();
628 }
629
630 public boolean isRowRemoved() {
631 return row != null && row.isRemoved();
632 }
633
634 /**
635 * @return {@code true} if the row is null or removed
636 */
637 public boolean isRemoved() {
638 //TODO: recycling invalidates this
639 return row == null || row.isRemoved();
640 }
641
Ned Burnsf81c4c42019-01-07 14:10:43 -0500642 public boolean isRowPinned() {
643 return row != null && row.isPinned();
644 }
645
646 public void setRowPinned(boolean pinned) {
647 if (row != null) row.setPinned(pinned);
648 }
649
650 public boolean isRowAnimatingAway() {
651 return row != null && row.isHeadsUpAnimatingAway();
652 }
653
654 public boolean isRowHeadsUp() {
655 return row != null && row.isHeadsUp();
656 }
657
658 public void setHeadsUp(boolean shouldHeadsUp) {
659 if (row != null) row.setHeadsUp(shouldHeadsUp);
660 }
661
Selim Cinek459aee32019-02-20 11:18:56 -0800662
663 public void setAmbientGoingAway(boolean goingAway) {
664 if (row != null) row.setAmbientGoingAway(goingAway);
665 }
666
667
Ned Burnsf81c4c42019-01-07 14:10:43 -0500668 public boolean mustStayOnScreen() {
669 return row != null && row.mustStayOnScreen();
670 }
671
672 public void setHeadsUpIsVisible() {
673 if (row != null) row.setHeadsUpIsVisible();
674 }
675
676 //TODO: i'm imagining a world where this isn't just the row, but I could be rwong
677 public ExpandableNotificationRow getHeadsUpAnimationView() {
678 return row;
679 }
680
681 public void setUserLocked(boolean userLocked) {
682 if (row != null) row.setUserLocked(userLocked);
683 }
684
685 public void setUserExpanded(boolean userExpanded, boolean allowChildExpansion) {
686 if (row != null) row.setUserExpanded(userExpanded, allowChildExpansion);
687 }
688
689 public void setGroupExpansionChanging(boolean changing) {
690 if (row != null) row.setGroupExpansionChanging(changing);
691 }
692
693 public void notifyHeightChanged(boolean needsAnimation) {
694 if (row != null) row.notifyHeightChanged(needsAnimation);
695 }
696
697 public void closeRemoteInput() {
698 if (row != null) row.closeRemoteInput();
699 }
700
701 public boolean areChildrenExpanded() {
702 return row != null && row.areChildrenExpanded();
703 }
704
705 public boolean keepInParent() {
706 return row != null && row.keepInParent();
707 }
708
709 //TODO: probably less confusing to say "is group fully visible"
710 public boolean isGroupNotFullyVisible() {
711 return row == null || row.isGroupNotFullyVisible();
712 }
713
714 public NotificationGuts getGuts() {
715 if (row != null) return row.getGuts();
716 return null;
717 }
718
Ned Burnsf81c4c42019-01-07 14:10:43 -0500719 public void removeRow() {
720 if (row != null) row.setRemoved();
721 }
722
723 public boolean isSummaryWithChildren() {
724 return row != null && row.isSummaryWithChildren();
725 }
726
727 public void setKeepInParent(boolean keep) {
728 if (row != null) row.setKeepInParent(keep);
729 }
730
731 public void onDensityOrFontScaleChanged() {
732 if (row != null) row.onDensityOrFontScaleChanged();
733 }
734
735 public boolean areGutsExposed() {
736 return row != null && row.getGuts() != null && row.getGuts().isExposed();
737 }
738
739 public boolean isChildInGroup() {
740 return parent == null;
741 }
742
Ned Burnsf81c4c42019-01-07 14:10:43 -0500743 /**
744 * @return Can the underlying notification be cleared? This can be different from whether the
745 * notification can be dismissed in case notifications are sensitive on the lockscreen.
746 * @see #canViewBeDismissed()
747 */
748 public boolean isClearable() {
749 if (notification == null || !notification.isClearable()) {
750 return false;
751 }
Evan Lairdce2d1af2019-05-30 16:00:22 -0400752
753 List<NotificationEntry> children = getChildren();
754 if (children != null && children.size() > 0) {
Ned Burnsf81c4c42019-01-07 14:10:43 -0500755 for (int i = 0; i < children.size(); i++) {
756 NotificationEntry child = children.get(i);
757 if (!child.isClearable()) {
758 return false;
759 }
760 }
761 }
762 return true;
763 }
764
765 public boolean canViewBeDismissed() {
766 if (row == null) return true;
767 return row.canViewBeDismissed();
768 }
769
770 @VisibleForTesting
771 boolean isExemptFromDndVisualSuppression() {
772 if (isNotificationBlockedByPolicy(notification.getNotification())) {
773 return false;
774 }
775
776 if ((notification.getNotification().flags
777 & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
778 return true;
779 }
780 if (notification.getNotification().isMediaNotification()) {
781 return true;
782 }
783 if (mIsSystemNotification != null && mIsSystemNotification) {
784 return true;
785 }
786 return false;
787 }
788
789 private boolean shouldSuppressVisualEffect(int effect) {
790 if (isExemptFromDndVisualSuppression()) {
791 return false;
792 }
793 return (suppressedVisualEffects & effect) != 0;
794 }
795
796 /**
797 * Returns whether {@link Policy#SUPPRESSED_EFFECT_FULL_SCREEN_INTENT}
798 * is set for this entry.
799 */
800 public boolean shouldSuppressFullScreenIntent() {
801 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_FULL_SCREEN_INTENT);
802 }
803
804 /**
805 * Returns whether {@link Policy#SUPPRESSED_EFFECT_PEEK}
806 * is set for this entry.
807 */
808 public boolean shouldSuppressPeek() {
809 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_PEEK);
810 }
811
812 /**
813 * Returns whether {@link Policy#SUPPRESSED_EFFECT_STATUS_BAR}
814 * is set for this entry.
815 */
816 public boolean shouldSuppressStatusBar() {
817 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_STATUS_BAR);
818 }
819
820 /**
821 * Returns whether {@link Policy#SUPPRESSED_EFFECT_AMBIENT}
822 * is set for this entry.
823 */
824 public boolean shouldSuppressAmbient() {
825 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_AMBIENT);
826 }
827
828 /**
829 * Returns whether {@link Policy#SUPPRESSED_EFFECT_NOTIFICATION_LIST}
830 * is set for this entry.
831 */
832 public boolean shouldSuppressNotificationList() {
833 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_NOTIFICATION_LIST);
834 }
835
836 /**
837 * Categories that are explicitly called out on DND settings screens are always blocked, if
838 * DND has flagged them, even if they are foreground or system notifications that might
839 * otherwise visually bypass DND.
840 */
841 private static boolean isNotificationBlockedByPolicy(Notification n) {
842 return isCategory(CATEGORY_CALL, n)
843 || isCategory(CATEGORY_MESSAGE, n)
844 || isCategory(CATEGORY_ALARM, n)
845 || isCategory(CATEGORY_EVENT, n)
846 || isCategory(CATEGORY_REMINDER, n);
847 }
848
849 private static boolean isCategory(String category, Notification n) {
850 return Objects.equals(n.category, category);
851 }
Milo Sredkov13d88112019-02-01 12:23:24 +0000852
853 /** Information about a suggestion that is being edited. */
854 public static class EditedSuggestionInfo {
855
856 /**
857 * The value of the suggestion (before any user edits).
858 */
859 public final CharSequence originalText;
860
861 /**
862 * The index of the suggestion that is being edited.
863 */
864 public final int index;
865
866 public EditedSuggestionInfo(CharSequence originalText, int index) {
867 this.originalText = originalText;
868 this.index = index;
869 }
870 }
Steven Wu8ba8ca92019-04-11 10:47:42 -0400871
872 /**
873 * Returns whether the notification is a foreground service. It shows that this is an ongoing
874 * bubble.
875 */
876 public boolean isForegroundService() {
877 return (notification.getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE) != 0;
878 }
Ned Burnsf81c4c42019-01-07 14:10:43 -0500879}