blob: fe8854168288940aebcfc1bb6c127d80be4fbead [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;
Selim Cinek195dfc52019-05-30 19:35:05 -070099 public StatusBarIconView aodIcon;
Ned Burnsf81c4c42019-01-07 14:10:43 -0500100 private boolean interruption;
101 public boolean autoRedacted; // whether the redacted notification was generated by us
102 public int targetSdk;
103 private long lastFullScreenIntentLaunchTime = NOT_LAUNCHED_YET;
104 public CharSequence remoteInputText;
105 public List<SnoozeCriterion> snoozeCriteria;
106 public int userSentiment = NotificationListenerService.Ranking.USER_SENTIMENT_NEUTRAL;
107 /** Smart Actions provided by the NotificationAssistantService. */
108 @NonNull
109 public List<Notification.Action> systemGeneratedSmartActions = Collections.emptyList();
Gustav Senntone255f902019-01-31 13:28:02 +0000110 /** Smart replies provided by the NotificationAssistantService. */
111 @NonNull
112 public CharSequence[] systemGeneratedSmartReplies = new CharSequence[0];
Milo Sredkov13d88112019-02-01 12:23:24 +0000113
114 /**
115 * If {@link android.app.RemoteInput#getEditChoicesBeforeSending} is enabled, and the user is
116 * currently editing a choice (smart reply), then this field contains the information about the
117 * suggestion being edited. Otherwise <code>null</code>.
118 */
119 public EditedSuggestionInfo editedSuggestionInfo;
120
Ned Burnsf81c4c42019-01-07 14:10:43 -0500121 @VisibleForTesting
122 public int suppressedVisualEffects;
123 public boolean suspended;
124
125 private NotificationEntry parent; // our parent (if we're in a group)
Ned Burnsf81c4c42019-01-07 14:10:43 -0500126 private ExpandableNotificationRow row; // the outer expanded view
127
128 private int mCachedContrastColor = COLOR_INVALID;
129 private int mCachedContrastColorIsFor = COLOR_INVALID;
130 private InflationTask mRunningTask = null;
131 private Throwable mDebugThrowable;
132 public CharSequence remoteInputTextWhenReset;
133 public long lastRemoteInputSent = NOT_LAUNCHED_YET;
134 public ArraySet<Integer> mActiveAppOps = new ArraySet<>(3);
135 public CharSequence headsUpStatusBarText;
136 public CharSequence headsUpStatusBarTextPublic;
137
138 private long initializationTime = -1;
139
140 /**
141 * Whether or not this row represents a system notification. Note that if this is
142 * {@code null}, that means we were either unable to retrieve the info or have yet to
143 * retrieve the info.
144 */
145 public Boolean mIsSystemNotification;
146
147 /**
148 * Has the user sent a reply through this Notification.
149 */
150 private boolean hasSentReply;
151
152 /**
Julia Reynolds4509ce72019-01-31 13:12:43 -0500153 * Whether this notification has been approved globally, at the app level, and at the channel
154 * level for bubbling.
155 */
156 public boolean canBubble;
157
158 /**
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800159 * Whether this notification should be shown in the shade when it is also displayed as a bubble.
160 *
161 * <p>When a notification is a bubble we don't show it in the shade once the bubble has been
162 * expanded</p>
163 */
164 private boolean mShowInShadeWhenBubble;
165
166 /**
Ned Burnsf81c4c42019-01-07 14:10:43 -0500167 * Whether the user has dismissed this notification when it was in bubble form.
168 */
169 private boolean mUserDismissedBubble;
170
Gus Prevascaed15c2019-01-18 14:19:51 -0500171 /**
172 * Whether this notification is shown to the user as a high priority notification: visible on
173 * the lock screen/status bar and in the top section in the shade.
174 */
175 private boolean mHighPriority;
Ned Burns8c1b7632019-07-19 14:26:15 -0400176
177 private boolean mIsTopBucket;
178
Selim Cinekb2c5dc52019-06-24 15:46:52 -0700179 private boolean mSensitive = true;
180 private Runnable mOnSensitiveChangedListener;
Selim Cineke3c6e462019-06-24 19:37:06 -0700181 private boolean mAutoHeadsUp;
Selim Cinek65c96f22019-07-25 20:09:04 -0700182 private boolean mPulseSupressed;
Gus Prevascaed15c2019-01-18 14:19:51 -0500183
Ned Burnsf81c4c42019-01-07 14:10:43 -0500184 public NotificationEntry(StatusBarNotification n) {
185 this(n, null);
186 }
187
188 public NotificationEntry(
189 StatusBarNotification n,
190 @Nullable NotificationListenerService.Ranking ranking) {
191 this.key = n.getKey();
192 this.notification = n;
193 if (ranking != null) {
194 populateFromRanking(ranking);
195 }
196 }
197
198 public void populateFromRanking(@NonNull NotificationListenerService.Ranking ranking) {
199 channel = ranking.getChannel();
200 lastAudiblyAlertedMs = ranking.getLastAudiblyAlertedMillis();
201 importance = ranking.getImportance();
202 ambient = ranking.isAmbient();
203 snoozeCriteria = ranking.getSnoozeCriteria();
204 userSentiment = ranking.getUserSentiment();
205 systemGeneratedSmartActions = ranking.getSmartActions() == null
206 ? Collections.emptyList() : ranking.getSmartActions();
Gustav Senntone255f902019-01-31 13:28:02 +0000207 systemGeneratedSmartReplies = ranking.getSmartReplies() == null
Ned Burnsf81c4c42019-01-07 14:10:43 -0500208 ? new CharSequence[0]
209 : ranking.getSmartReplies().toArray(new CharSequence[0]);
210 suppressedVisualEffects = ranking.getSuppressedVisualEffects();
211 suspended = ranking.isSuspended();
Julia Reynolds4509ce72019-01-31 13:12:43 -0500212 canBubble = ranking.canBubble();
Ned Burnsf81c4c42019-01-07 14:10:43 -0500213 }
214
215 public void setInterruption() {
216 interruption = true;
217 }
218
219 public boolean hasInterrupted() {
220 return interruption;
221 }
222
Gus Prevascaed15c2019-01-18 14:19:51 -0500223 public boolean isHighPriority() {
224 return mHighPriority;
225 }
226
227 public void setIsHighPriority(boolean highPriority) {
228 this.mHighPriority = highPriority;
229 }
230
Ned Burns8c1b7632019-07-19 14:26:15 -0400231 /**
232 * @return True if the notif should appear in the "top" or "important" section of notifications
233 * (as opposed to the "bottom" or "silent" section). This is usually the same as
234 * {@link #isHighPriority()}, but there are certain exceptions, such as media notifs.
235 */
236 public boolean isTopBucket() {
237 return mIsTopBucket;
238 }
239 public void setIsTopBucket(boolean isTopBucket) {
240 mIsTopBucket = isTopBucket;
241 }
242
Ned Burnsf81c4c42019-01-07 14:10:43 -0500243 public boolean isBubble() {
Mady Mellorfc02cc32019-04-01 14:47:55 -0700244 return (notification.getNotification().flags & FLAG_BUBBLE) != 0;
Ned Burnsf81c4c42019-01-07 14:10:43 -0500245 }
246
247 public void setBubbleDismissed(boolean userDismissed) {
248 mUserDismissedBubble = userDismissed;
249 }
250
251 public boolean isBubbleDismissed() {
252 return mUserDismissedBubble;
253 }
254
255 /**
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800256 * Sets whether this notification should be shown in the shade when it is also displayed as a
257 * bubble.
258 */
259 public void setShowInShadeWhenBubble(boolean showInShade) {
260 mShowInShadeWhenBubble = showInShade;
261 }
262
263 /**
264 * Whether this notification should be shown in the shade when it is also displayed as a
265 * bubble.
266 */
267 public boolean showInShadeWhenBubble() {
268 // We always show it in the shade if non-clearable
Mady Mellorc2ff0112019-03-28 14:18:06 -0700269 return !isRowDismissed() && (!isClearable() || mShowInShadeWhenBubble);
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800270 }
271
272 /**
Mady Mellor9801e852019-01-22 14:50:28 -0800273 * Returns the data needed for a bubble for this notification, if it exists.
274 */
275 public Notification.BubbleMetadata getBubbleMetadata() {
276 return notification.getNotification().getBubbleMetadata();
277 }
278
279 /**
Ned Burnsf81c4c42019-01-07 14:10:43 -0500280 * Resets the notification entry to be re-used.
281 */
282 public void reset() {
283 if (row != null) {
284 row.reset();
285 }
286 }
287
288 public ExpandableNotificationRow getRow() {
289 return row;
290 }
291
292 //TODO: This will go away when we have a way to bind an entry to a row
293 public void setRow(ExpandableNotificationRow row) {
294 this.row = row;
295 }
296
297 @Nullable
298 public List<NotificationEntry> getChildren() {
Evan Lairdce2d1af2019-05-30 16:00:22 -0400299 if (row == null) {
Ned Burnsf81c4c42019-01-07 14:10:43 -0500300 return null;
301 }
302
Evan Lairdce2d1af2019-05-30 16:00:22 -0400303 List<ExpandableNotificationRow> rowChildren = row.getNotificationChildren();
304 if (rowChildren == null) {
305 return null;
306 }
307
308 ArrayList<NotificationEntry> children = new ArrayList<>();
309 for (ExpandableNotificationRow child : rowChildren) {
310 children.add(child.getEntry());
311 }
312
Ned Burnsf81c4c42019-01-07 14:10:43 -0500313 return children;
314 }
315
316 public void notifyFullScreenIntentLaunched() {
317 setInterruption();
318 lastFullScreenIntentLaunchTime = SystemClock.elapsedRealtime();
319 }
320
321 public boolean hasJustLaunchedFullScreenIntent() {
322 return SystemClock.elapsedRealtime() < lastFullScreenIntentLaunchTime + LAUNCH_COOLDOWN;
323 }
324
325 public boolean hasJustSentRemoteInput() {
326 return SystemClock.elapsedRealtime() < lastRemoteInputSent + REMOTE_INPUT_COOLDOWN;
327 }
328
329 public boolean hasFinishedInitialization() {
330 return initializationTime == -1
331 || SystemClock.elapsedRealtime() > initializationTime + INITIALIZATION_DELAY;
332 }
333
334 /**
335 * Create the icons for a notification
336 * @param context the context to create the icons with
337 * @param sbn the notification
338 * @throws InflationException Exception if required icons are not valid or specified
339 */
340 public void createIcons(Context context, StatusBarNotification sbn)
341 throws InflationException {
342 Notification n = sbn.getNotification();
343 final Icon smallIcon = n.getSmallIcon();
344 if (smallIcon == null) {
345 throw new InflationException("No small icon in notification from "
346 + sbn.getPackageName());
347 }
348
349 // Construct the icon.
350 icon = new StatusBarIconView(context,
351 sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn);
352 icon.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
353
354 // Construct the expanded icon.
355 expandedIcon = new StatusBarIconView(context,
356 sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn);
357 expandedIcon.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
Beverly40770652019-02-15 15:49:49 -0500358
Selim Cinek195dfc52019-05-30 19:35:05 -0700359 // Construct the expanded icon.
360 aodIcon = new StatusBarIconView(context,
361 sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn);
362 aodIcon.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
363 aodIcon.setIncreasedSize(true);
364
Ned Burnsf81c4c42019-01-07 14:10:43 -0500365 final StatusBarIcon ic = new StatusBarIcon(
366 sbn.getUser(),
367 sbn.getPackageName(),
368 smallIcon,
369 n.iconLevel,
370 n.number,
371 StatusBarIconView.contentDescForNotification(context, n));
Beverly40770652019-02-15 15:49:49 -0500372
Selim Cinek195dfc52019-05-30 19:35:05 -0700373 if (!icon.set(ic) || !expandedIcon.set(ic) || !aodIcon.set(ic)) {
Ned Burnsf81c4c42019-01-07 14:10:43 -0500374 icon = null;
375 expandedIcon = null;
Beverly40770652019-02-15 15:49:49 -0500376 centeredIcon = null;
Selim Cinek195dfc52019-05-30 19:35:05 -0700377 aodIcon = null;
Ned Burnsf81c4c42019-01-07 14:10:43 -0500378 throw new InflationException("Couldn't create icon: " + ic);
379 }
380 expandedIcon.setVisibility(View.INVISIBLE);
381 expandedIcon.setOnVisibilityChangedListener(
382 newVisibility -> {
383 if (row != null) {
384 row.setIconsVisible(newVisibility != View.VISIBLE);
385 }
386 });
Beverly40770652019-02-15 15:49:49 -0500387
388 // Construct the centered icon
389 if (notification.getNotification().isMediaNotification()) {
390 centeredIcon = new StatusBarIconView(context,
391 sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn);
392 centeredIcon.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
393
394 if (!centeredIcon.set(ic)) {
395 centeredIcon = null;
396 throw new InflationException("Couldn't update centered icon: " + ic);
397 }
398 }
Ned Burnsf81c4c42019-01-07 14:10:43 -0500399 }
400
401 public void setIconTag(int key, Object tag) {
402 if (icon != null) {
403 icon.setTag(key, tag);
404 expandedIcon.setTag(key, tag);
405 }
Beverly40770652019-02-15 15:49:49 -0500406
407 if (centeredIcon != null) {
408 centeredIcon.setTag(key, tag);
409 }
Selim Cinek195dfc52019-05-30 19:35:05 -0700410
411 if (aodIcon != null) {
412 aodIcon.setTag(key, tag);
413 }
Ned Burnsf81c4c42019-01-07 14:10:43 -0500414 }
415
416 /**
417 * Update the notification icons.
418 *
419 * @param context the context to create the icons with.
420 * @param sbn the notification to read the icon from.
421 * @throws InflationException Exception if required icons are not valid or specified
422 */
423 public void updateIcons(Context context, StatusBarNotification sbn)
424 throws InflationException {
425 if (icon != null) {
426 // Update the icon
427 Notification n = sbn.getNotification();
428 final StatusBarIcon ic = new StatusBarIcon(
429 notification.getUser(),
430 notification.getPackageName(),
431 n.getSmallIcon(),
432 n.iconLevel,
433 n.number,
434 StatusBarIconView.contentDescForNotification(context, n));
435 icon.setNotification(sbn);
436 expandedIcon.setNotification(sbn);
Selim Cinek195dfc52019-05-30 19:35:05 -0700437 aodIcon.setNotification(sbn);
438 if (!icon.set(ic) || !expandedIcon.set(ic) || !aodIcon.set(ic)) {
Ned Burnsf81c4c42019-01-07 14:10:43 -0500439 throw new InflationException("Couldn't update icon: " + ic);
440 }
Beverly40770652019-02-15 15:49:49 -0500441
442 if (centeredIcon != null) {
443 centeredIcon.setNotification(sbn);
444 if (!centeredIcon.set(ic)) {
445 throw new InflationException("Couldn't update centered icon: " + ic);
446 }
447 }
Ned Burnsf81c4c42019-01-07 14:10:43 -0500448 }
449 }
450
451 public int getContrastedColor(Context context, boolean isLowPriority,
452 int backgroundColor) {
453 int rawColor = isLowPriority ? Notification.COLOR_DEFAULT :
454 notification.getNotification().color;
455 if (mCachedContrastColorIsFor == rawColor && mCachedContrastColor != COLOR_INVALID) {
456 return mCachedContrastColor;
457 }
458 final int contrasted = ContrastColorUtil.resolveContrastColor(context, rawColor,
459 backgroundColor);
460 mCachedContrastColorIsFor = rawColor;
461 mCachedContrastColor = contrasted;
462 return mCachedContrastColor;
463 }
464
465 /**
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400466 * Returns our best guess for the most relevant text summary of the latest update to this
467 * notification, based on its type. Returns null if there should not be an update message.
468 */
469 public CharSequence getUpdateMessage(Context context) {
470 final Notification underlyingNotif = notification.getNotification();
471 final Class<? extends Notification.Style> style = underlyingNotif.getNotificationStyle();
472
473 try {
474 if (Notification.BigTextStyle.class.equals(style)) {
475 // Return the big text, it is big so probably important. If it's not there use the
476 // normal text.
477 CharSequence bigText =
478 underlyingNotif.extras.getCharSequence(Notification.EXTRA_BIG_TEXT);
479 return !TextUtils.isEmpty(bigText)
480 ? bigText
481 : underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT);
482 } else if (Notification.MessagingStyle.class.equals(style)) {
483 final List<Notification.MessagingStyle.Message> messages =
484 Notification.MessagingStyle.Message.getMessagesFromBundleArray(
485 (Parcelable[]) underlyingNotif.extras.get(
486 Notification.EXTRA_MESSAGES));
487
488 final Notification.MessagingStyle.Message latestMessage =
489 Notification.MessagingStyle.findLatestIncomingMessage(messages);
490
491 if (latestMessage != null) {
492 final CharSequence personName = latestMessage.getSenderPerson() != null
493 ? latestMessage.getSenderPerson().getName()
494 : null;
495
496 // Prepend the sender name if available since group chats also use messaging
497 // style.
498 if (!TextUtils.isEmpty(personName)) {
499 return context.getResources().getString(
500 R.string.notification_summary_message_format,
501 personName,
502 latestMessage.getText());
503 } else {
504 return latestMessage.getText();
505 }
506 }
507 } else if (Notification.InboxStyle.class.equals(style)) {
508 CharSequence[] lines =
509 underlyingNotif.extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES);
510
511 // Return the last line since it should be the most recent.
512 if (lines != null && lines.length > 0) {
513 return lines[lines.length - 1];
514 }
515 } else if (Notification.MediaStyle.class.equals(style)) {
516 // Return nothing, media updates aren't typically useful as a text update.
517 return null;
518 } else {
519 // Default to text extra.
520 return underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT);
521 }
522 } catch (ClassCastException | NullPointerException | ArrayIndexOutOfBoundsException e) {
523 // No use crashing, we'll just return null and the caller will assume there's no update
524 // message.
525 e.printStackTrace();
526 }
527
528 return null;
529 }
530
531 /**
Ned Burnsf81c4c42019-01-07 14:10:43 -0500532 * Abort all existing inflation tasks
533 */
534 public void abortTask() {
535 if (mRunningTask != null) {
536 mRunningTask.abort();
537 mRunningTask = null;
538 }
539 }
540
541 public void setInflationTask(InflationTask abortableTask) {
542 // abort any existing inflation
543 InflationTask existing = mRunningTask;
544 abortTask();
545 mRunningTask = abortableTask;
546 if (existing != null && mRunningTask != null) {
547 mRunningTask.supersedeTask(existing);
548 }
549 }
550
551 public void onInflationTaskFinished() {
552 mRunningTask = null;
553 }
554
555 @VisibleForTesting
556 public InflationTask getRunningTask() {
557 return mRunningTask;
558 }
559
560 /**
561 * Set a throwable that is used for debugging
562 *
563 * @param debugThrowable the throwable to save
564 */
565 public void setDebugThrowable(Throwable debugThrowable) {
566 mDebugThrowable = debugThrowable;
567 }
568
569 public Throwable getDebugThrowable() {
570 return mDebugThrowable;
571 }
572
573 public void onRemoteInputInserted() {
574 lastRemoteInputSent = NOT_LAUNCHED_YET;
575 remoteInputTextWhenReset = null;
576 }
577
578 public void setHasSentReply() {
579 hasSentReply = true;
580 }
581
582 public boolean isLastMessageFromReply() {
583 if (!hasSentReply) {
584 return false;
585 }
586 Bundle extras = notification.getNotification().extras;
587 CharSequence[] replyTexts = extras.getCharSequenceArray(
588 Notification.EXTRA_REMOTE_INPUT_HISTORY);
589 if (!ArrayUtils.isEmpty(replyTexts)) {
590 return true;
591 }
592 Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
593 if (messages != null && messages.length > 0) {
594 Parcelable message = messages[messages.length - 1];
595 if (message instanceof Bundle) {
596 Notification.MessagingStyle.Message lastMessage =
597 Notification.MessagingStyle.Message.getMessageFromBundle(
598 (Bundle) message);
599 if (lastMessage != null) {
600 Person senderPerson = lastMessage.getSenderPerson();
601 if (senderPerson == null) {
602 return true;
603 }
604 Person user = extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON);
605 return Objects.equals(user, senderPerson);
606 }
607 }
608 }
609 return false;
610 }
611
612 public void setInitializationTime(long time) {
613 if (initializationTime == -1) {
614 initializationTime = time;
615 }
616 }
617
618 public void sendAccessibilityEvent(int eventType) {
619 if (row != null) {
620 row.sendAccessibilityEvent(eventType);
621 }
622 }
623
624 /**
625 * Used by NotificationMediaManager to determine... things
626 * @return {@code true} if we are a media notification
627 */
628 public boolean isMediaNotification() {
629 if (row == null) return false;
630
631 return row.isMediaRow();
632 }
633
634 /**
635 * We are a top level child if our parent is the list of notifications duh
636 * @return {@code true} if we're a top level notification
637 */
638 public boolean isTopLevelChild() {
639 return row != null && row.isTopLevelChild();
640 }
641
642 public void resetUserExpansion() {
643 if (row != null) row.resetUserExpansion();
644 }
645
Ned Burns1a5e22f2019-02-14 15:11:52 -0500646 public void freeContentViewWhenSafe(@InflationFlag int inflationFlag) {
Ned Burnsf81c4c42019-01-07 14:10:43 -0500647 if (row != null) row.freeContentViewWhenSafe(inflationFlag);
648 }
649
Ned Burnsf81c4c42019-01-07 14:10:43 -0500650 public boolean rowExists() {
651 return row != null;
652 }
653
654 public boolean isRowDismissed() {
655 return row != null && row.isDismissed();
656 }
657
658 public boolean isRowRemoved() {
659 return row != null && row.isRemoved();
660 }
661
662 /**
663 * @return {@code true} if the row is null or removed
664 */
665 public boolean isRemoved() {
666 //TODO: recycling invalidates this
667 return row == null || row.isRemoved();
668 }
669
Ned Burnsf81c4c42019-01-07 14:10:43 -0500670 public boolean isRowPinned() {
671 return row != null && row.isPinned();
672 }
673
674 public void setRowPinned(boolean pinned) {
675 if (row != null) row.setPinned(pinned);
676 }
677
Ned Burnsf81c4c42019-01-07 14:10:43 -0500678 public boolean isRowHeadsUp() {
679 return row != null && row.isHeadsUp();
680 }
681
Selim Cinekb57dd8a2019-06-14 12:27:58 -0700682 public boolean showingPulsing() {
683 return row != null && row.showingPulsing();
684 }
685
Ned Burnsf81c4c42019-01-07 14:10:43 -0500686 public void setHeadsUp(boolean shouldHeadsUp) {
687 if (row != null) row.setHeadsUp(shouldHeadsUp);
688 }
689
Selim Cinekc3fec682019-06-06 18:11:07 -0700690 public void setHeadsUpAnimatingAway(boolean animatingAway) {
691 if (row != null) row.setHeadsUpAnimatingAway(animatingAway);
Selim Cinek459aee32019-02-20 11:18:56 -0800692 }
693
Selim Cineke3c6e462019-06-24 19:37:06 -0700694 /**
695 * Set that this notification was automatically heads upped. This happens for example when
696 * the user bypasses the lockscreen and media is playing.
697 */
698 public void setAutoHeadsUp(boolean autoHeadsUp) {
699 mAutoHeadsUp = autoHeadsUp;
700 }
701
702 /**
703 * @return if this notification was automatically heads upped. This happens for example when
704 * * the user bypasses the lockscreen and media is playing.
705 */
706 public boolean isAutoHeadsUp() {
707 return mAutoHeadsUp;
708 }
Selim Cinek459aee32019-02-20 11:18:56 -0800709
Ned Burnsf81c4c42019-01-07 14:10:43 -0500710 public boolean mustStayOnScreen() {
711 return row != null && row.mustStayOnScreen();
712 }
713
714 public void setHeadsUpIsVisible() {
715 if (row != null) row.setHeadsUpIsVisible();
716 }
717
718 //TODO: i'm imagining a world where this isn't just the row, but I could be rwong
719 public ExpandableNotificationRow getHeadsUpAnimationView() {
720 return row;
721 }
722
723 public void setUserLocked(boolean userLocked) {
724 if (row != null) row.setUserLocked(userLocked);
725 }
726
727 public void setUserExpanded(boolean userExpanded, boolean allowChildExpansion) {
728 if (row != null) row.setUserExpanded(userExpanded, allowChildExpansion);
729 }
730
731 public void setGroupExpansionChanging(boolean changing) {
732 if (row != null) row.setGroupExpansionChanging(changing);
733 }
734
735 public void notifyHeightChanged(boolean needsAnimation) {
736 if (row != null) row.notifyHeightChanged(needsAnimation);
737 }
738
739 public void closeRemoteInput() {
740 if (row != null) row.closeRemoteInput();
741 }
742
743 public boolean areChildrenExpanded() {
744 return row != null && row.areChildrenExpanded();
745 }
746
747 public boolean keepInParent() {
748 return row != null && row.keepInParent();
749 }
750
751 //TODO: probably less confusing to say "is group fully visible"
752 public boolean isGroupNotFullyVisible() {
753 return row == null || row.isGroupNotFullyVisible();
754 }
755
756 public NotificationGuts getGuts() {
757 if (row != null) return row.getGuts();
758 return null;
759 }
760
Ned Burnsf81c4c42019-01-07 14:10:43 -0500761 public void removeRow() {
762 if (row != null) row.setRemoved();
763 }
764
765 public boolean isSummaryWithChildren() {
766 return row != null && row.isSummaryWithChildren();
767 }
768
769 public void setKeepInParent(boolean keep) {
770 if (row != null) row.setKeepInParent(keep);
771 }
772
773 public void onDensityOrFontScaleChanged() {
774 if (row != null) row.onDensityOrFontScaleChanged();
775 }
776
777 public boolean areGutsExposed() {
778 return row != null && row.getGuts() != null && row.getGuts().isExposed();
779 }
780
781 public boolean isChildInGroup() {
782 return parent == null;
783 }
784
Ned Burnsf81c4c42019-01-07 14:10:43 -0500785 /**
786 * @return Can the underlying notification be cleared? This can be different from whether the
787 * notification can be dismissed in case notifications are sensitive on the lockscreen.
788 * @see #canViewBeDismissed()
789 */
790 public boolean isClearable() {
791 if (notification == null || !notification.isClearable()) {
792 return false;
793 }
Evan Lairdce2d1af2019-05-30 16:00:22 -0400794
795 List<NotificationEntry> children = getChildren();
796 if (children != null && children.size() > 0) {
Ned Burnsf81c4c42019-01-07 14:10:43 -0500797 for (int i = 0; i < children.size(); i++) {
798 NotificationEntry child = children.get(i);
799 if (!child.isClearable()) {
800 return false;
801 }
802 }
803 }
804 return true;
805 }
806
807 public boolean canViewBeDismissed() {
808 if (row == null) return true;
809 return row.canViewBeDismissed();
810 }
811
812 @VisibleForTesting
813 boolean isExemptFromDndVisualSuppression() {
814 if (isNotificationBlockedByPolicy(notification.getNotification())) {
815 return false;
816 }
817
818 if ((notification.getNotification().flags
819 & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
820 return true;
821 }
822 if (notification.getNotification().isMediaNotification()) {
823 return true;
824 }
825 if (mIsSystemNotification != null && mIsSystemNotification) {
826 return true;
827 }
828 return false;
829 }
830
831 private boolean shouldSuppressVisualEffect(int effect) {
832 if (isExemptFromDndVisualSuppression()) {
833 return false;
834 }
835 return (suppressedVisualEffects & effect) != 0;
836 }
837
838 /**
839 * Returns whether {@link Policy#SUPPRESSED_EFFECT_FULL_SCREEN_INTENT}
840 * is set for this entry.
841 */
842 public boolean shouldSuppressFullScreenIntent() {
843 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_FULL_SCREEN_INTENT);
844 }
845
846 /**
847 * Returns whether {@link Policy#SUPPRESSED_EFFECT_PEEK}
848 * is set for this entry.
849 */
850 public boolean shouldSuppressPeek() {
851 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_PEEK);
852 }
853
854 /**
855 * Returns whether {@link Policy#SUPPRESSED_EFFECT_STATUS_BAR}
856 * is set for this entry.
857 */
858 public boolean shouldSuppressStatusBar() {
859 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_STATUS_BAR);
860 }
861
862 /**
863 * Returns whether {@link Policy#SUPPRESSED_EFFECT_AMBIENT}
864 * is set for this entry.
865 */
866 public boolean shouldSuppressAmbient() {
867 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_AMBIENT);
868 }
869
870 /**
871 * Returns whether {@link Policy#SUPPRESSED_EFFECT_NOTIFICATION_LIST}
872 * is set for this entry.
873 */
874 public boolean shouldSuppressNotificationList() {
875 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_NOTIFICATION_LIST);
876 }
877
878 /**
879 * Categories that are explicitly called out on DND settings screens are always blocked, if
880 * DND has flagged them, even if they are foreground or system notifications that might
881 * otherwise visually bypass DND.
882 */
883 private static boolean isNotificationBlockedByPolicy(Notification n) {
884 return isCategory(CATEGORY_CALL, n)
885 || isCategory(CATEGORY_MESSAGE, n)
886 || isCategory(CATEGORY_ALARM, n)
887 || isCategory(CATEGORY_EVENT, n)
888 || isCategory(CATEGORY_REMINDER, n);
889 }
890
891 private static boolean isCategory(String category, Notification n) {
892 return Objects.equals(n.category, category);
893 }
Milo Sredkov13d88112019-02-01 12:23:24 +0000894
Selim Cinekb2c5dc52019-06-24 15:46:52 -0700895 /**
896 * Set this notification to be sensitive.
897 *
898 * @param sensitive true if the content of this notification is sensitive right now
899 * @param deviceSensitive true if the device in general is sensitive right now
900 */
901 public void setSensitive(boolean sensitive, boolean deviceSensitive) {
902 getRow().setSensitive(sensitive, deviceSensitive);
903 if (sensitive != mSensitive) {
904 mSensitive = sensitive;
905 if (mOnSensitiveChangedListener != null) {
906 mOnSensitiveChangedListener.run();
907 }
908 }
909 }
910
911 public boolean isSensitive() {
912 return mSensitive;
913 }
914
915 public void setOnSensitiveChangedListener(Runnable listener) {
916 mOnSensitiveChangedListener = listener;
917 }
918
Selim Cinek65c96f22019-07-25 20:09:04 -0700919 public boolean isPulseSuppressed() {
920 return mPulseSupressed;
921 }
922
923 public void setPulseSuppressed(boolean suppressed) {
924 mPulseSupressed = suppressed;
925 }
926
Milo Sredkov13d88112019-02-01 12:23:24 +0000927 /** Information about a suggestion that is being edited. */
928 public static class EditedSuggestionInfo {
929
930 /**
931 * The value of the suggestion (before any user edits).
932 */
933 public final CharSequence originalText;
934
935 /**
936 * The index of the suggestion that is being edited.
937 */
938 public final int index;
939
940 public EditedSuggestionInfo(CharSequence originalText, int index) {
941 this.originalText = originalText;
942 this.index = index;
943 }
944 }
Steven Wu8ba8ca92019-04-11 10:47:42 -0400945
946 /**
947 * Returns whether the notification is a foreground service. It shows that this is an ongoing
948 * bubble.
949 */
950 public boolean isForegroundService() {
951 return (notification.getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE) != 0;
952 }
Ned Burnsf81c4c42019-01-07 14:10:43 -0500953}