blob: 9db715d129a532a873202d62124a25e3da8a6ad8 [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;
Selim Cinekb2c5dc52019-06-24 15:46:52 -0700176 private boolean mSensitive = true;
177 private Runnable mOnSensitiveChangedListener;
Gus Prevascaed15c2019-01-18 14:19:51 -0500178
Ned Burnsf81c4c42019-01-07 14:10:43 -0500179 public NotificationEntry(StatusBarNotification n) {
180 this(n, null);
181 }
182
183 public NotificationEntry(
184 StatusBarNotification n,
185 @Nullable NotificationListenerService.Ranking ranking) {
186 this.key = n.getKey();
187 this.notification = n;
188 if (ranking != null) {
189 populateFromRanking(ranking);
190 }
191 }
192
193 public void populateFromRanking(@NonNull NotificationListenerService.Ranking ranking) {
194 channel = ranking.getChannel();
195 lastAudiblyAlertedMs = ranking.getLastAudiblyAlertedMillis();
196 importance = ranking.getImportance();
197 ambient = ranking.isAmbient();
198 snoozeCriteria = ranking.getSnoozeCriteria();
199 userSentiment = ranking.getUserSentiment();
200 systemGeneratedSmartActions = ranking.getSmartActions() == null
201 ? Collections.emptyList() : ranking.getSmartActions();
Gustav Senntone255f902019-01-31 13:28:02 +0000202 systemGeneratedSmartReplies = ranking.getSmartReplies() == null
Ned Burnsf81c4c42019-01-07 14:10:43 -0500203 ? new CharSequence[0]
204 : ranking.getSmartReplies().toArray(new CharSequence[0]);
205 suppressedVisualEffects = ranking.getSuppressedVisualEffects();
206 suspended = ranking.isSuspended();
Julia Reynolds4509ce72019-01-31 13:12:43 -0500207 canBubble = ranking.canBubble();
Ned Burnsf81c4c42019-01-07 14:10:43 -0500208 }
209
210 public void setInterruption() {
211 interruption = true;
212 }
213
214 public boolean hasInterrupted() {
215 return interruption;
216 }
217
Gus Prevascaed15c2019-01-18 14:19:51 -0500218 public boolean isHighPriority() {
219 return mHighPriority;
220 }
221
222 public void setIsHighPriority(boolean highPriority) {
223 this.mHighPriority = highPriority;
224 }
225
Ned Burnsf81c4c42019-01-07 14:10:43 -0500226 public boolean isBubble() {
Mady Mellorfc02cc32019-04-01 14:47:55 -0700227 return (notification.getNotification().flags & FLAG_BUBBLE) != 0;
Ned Burnsf81c4c42019-01-07 14:10:43 -0500228 }
229
230 public void setBubbleDismissed(boolean userDismissed) {
231 mUserDismissedBubble = userDismissed;
232 }
233
234 public boolean isBubbleDismissed() {
235 return mUserDismissedBubble;
236 }
237
238 /**
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800239 * Sets whether this notification should be shown in the shade when it is also displayed as a
240 * bubble.
241 */
242 public void setShowInShadeWhenBubble(boolean showInShade) {
243 mShowInShadeWhenBubble = showInShade;
244 }
245
246 /**
247 * Whether this notification should be shown in the shade when it is also displayed as a
248 * bubble.
249 */
250 public boolean showInShadeWhenBubble() {
251 // We always show it in the shade if non-clearable
Mady Mellorc2ff0112019-03-28 14:18:06 -0700252 return !isRowDismissed() && (!isClearable() || mShowInShadeWhenBubble);
Mady Mellor3f2efdb2018-11-21 11:30:45 -0800253 }
254
255 /**
Mady Mellor9801e852019-01-22 14:50:28 -0800256 * Returns the data needed for a bubble for this notification, if it exists.
257 */
258 public Notification.BubbleMetadata getBubbleMetadata() {
259 return notification.getNotification().getBubbleMetadata();
260 }
261
262 /**
Ned Burnsf81c4c42019-01-07 14:10:43 -0500263 * Resets the notification entry to be re-used.
264 */
265 public void reset() {
266 if (row != null) {
267 row.reset();
268 }
269 }
270
271 public ExpandableNotificationRow getRow() {
272 return row;
273 }
274
275 //TODO: This will go away when we have a way to bind an entry to a row
276 public void setRow(ExpandableNotificationRow row) {
277 this.row = row;
278 }
279
280 @Nullable
281 public List<NotificationEntry> getChildren() {
Evan Lairdce2d1af2019-05-30 16:00:22 -0400282 if (row == null) {
Ned Burnsf81c4c42019-01-07 14:10:43 -0500283 return null;
284 }
285
Evan Lairdce2d1af2019-05-30 16:00:22 -0400286 List<ExpandableNotificationRow> rowChildren = row.getNotificationChildren();
287 if (rowChildren == null) {
288 return null;
289 }
290
291 ArrayList<NotificationEntry> children = new ArrayList<>();
292 for (ExpandableNotificationRow child : rowChildren) {
293 children.add(child.getEntry());
294 }
295
Ned Burnsf81c4c42019-01-07 14:10:43 -0500296 return children;
297 }
298
299 public void notifyFullScreenIntentLaunched() {
300 setInterruption();
301 lastFullScreenIntentLaunchTime = SystemClock.elapsedRealtime();
302 }
303
304 public boolean hasJustLaunchedFullScreenIntent() {
305 return SystemClock.elapsedRealtime() < lastFullScreenIntentLaunchTime + LAUNCH_COOLDOWN;
306 }
307
308 public boolean hasJustSentRemoteInput() {
309 return SystemClock.elapsedRealtime() < lastRemoteInputSent + REMOTE_INPUT_COOLDOWN;
310 }
311
312 public boolean hasFinishedInitialization() {
313 return initializationTime == -1
314 || SystemClock.elapsedRealtime() > initializationTime + INITIALIZATION_DELAY;
315 }
316
317 /**
318 * Create the icons for a notification
319 * @param context the context to create the icons with
320 * @param sbn the notification
321 * @throws InflationException Exception if required icons are not valid or specified
322 */
323 public void createIcons(Context context, StatusBarNotification sbn)
324 throws InflationException {
325 Notification n = sbn.getNotification();
326 final Icon smallIcon = n.getSmallIcon();
327 if (smallIcon == null) {
328 throw new InflationException("No small icon in notification from "
329 + sbn.getPackageName());
330 }
331
332 // Construct the icon.
333 icon = new StatusBarIconView(context,
334 sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn);
335 icon.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
336
337 // Construct the expanded icon.
338 expandedIcon = new StatusBarIconView(context,
339 sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn);
340 expandedIcon.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
Beverly40770652019-02-15 15:49:49 -0500341
Selim Cinek195dfc52019-05-30 19:35:05 -0700342 // Construct the expanded icon.
343 aodIcon = new StatusBarIconView(context,
344 sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn);
345 aodIcon.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
346 aodIcon.setIncreasedSize(true);
347
Ned Burnsf81c4c42019-01-07 14:10:43 -0500348 final StatusBarIcon ic = new StatusBarIcon(
349 sbn.getUser(),
350 sbn.getPackageName(),
351 smallIcon,
352 n.iconLevel,
353 n.number,
354 StatusBarIconView.contentDescForNotification(context, n));
Beverly40770652019-02-15 15:49:49 -0500355
Selim Cinek195dfc52019-05-30 19:35:05 -0700356 if (!icon.set(ic) || !expandedIcon.set(ic) || !aodIcon.set(ic)) {
Ned Burnsf81c4c42019-01-07 14:10:43 -0500357 icon = null;
358 expandedIcon = null;
Beverly40770652019-02-15 15:49:49 -0500359 centeredIcon = null;
Selim Cinek195dfc52019-05-30 19:35:05 -0700360 aodIcon = null;
Ned Burnsf81c4c42019-01-07 14:10:43 -0500361 throw new InflationException("Couldn't create icon: " + ic);
362 }
363 expandedIcon.setVisibility(View.INVISIBLE);
364 expandedIcon.setOnVisibilityChangedListener(
365 newVisibility -> {
366 if (row != null) {
367 row.setIconsVisible(newVisibility != View.VISIBLE);
368 }
369 });
Beverly40770652019-02-15 15:49:49 -0500370
371 // Construct the centered icon
372 if (notification.getNotification().isMediaNotification()) {
373 centeredIcon = new StatusBarIconView(context,
374 sbn.getPackageName() + "/0x" + Integer.toHexString(sbn.getId()), sbn);
375 centeredIcon.setScaleType(ImageView.ScaleType.CENTER_INSIDE);
376
377 if (!centeredIcon.set(ic)) {
378 centeredIcon = null;
379 throw new InflationException("Couldn't update centered icon: " + ic);
380 }
381 }
Ned Burnsf81c4c42019-01-07 14:10:43 -0500382 }
383
384 public void setIconTag(int key, Object tag) {
385 if (icon != null) {
386 icon.setTag(key, tag);
387 expandedIcon.setTag(key, tag);
388 }
Beverly40770652019-02-15 15:49:49 -0500389
390 if (centeredIcon != null) {
391 centeredIcon.setTag(key, tag);
392 }
Selim Cinek195dfc52019-05-30 19:35:05 -0700393
394 if (aodIcon != null) {
395 aodIcon.setTag(key, tag);
396 }
Ned Burnsf81c4c42019-01-07 14:10:43 -0500397 }
398
399 /**
400 * Update the notification icons.
401 *
402 * @param context the context to create the icons with.
403 * @param sbn the notification to read the icon from.
404 * @throws InflationException Exception if required icons are not valid or specified
405 */
406 public void updateIcons(Context context, StatusBarNotification sbn)
407 throws InflationException {
408 if (icon != null) {
409 // Update the icon
410 Notification n = sbn.getNotification();
411 final StatusBarIcon ic = new StatusBarIcon(
412 notification.getUser(),
413 notification.getPackageName(),
414 n.getSmallIcon(),
415 n.iconLevel,
416 n.number,
417 StatusBarIconView.contentDescForNotification(context, n));
418 icon.setNotification(sbn);
419 expandedIcon.setNotification(sbn);
Selim Cinek195dfc52019-05-30 19:35:05 -0700420 aodIcon.setNotification(sbn);
421 if (!icon.set(ic) || !expandedIcon.set(ic) || !aodIcon.set(ic)) {
Ned Burnsf81c4c42019-01-07 14:10:43 -0500422 throw new InflationException("Couldn't update icon: " + ic);
423 }
Beverly40770652019-02-15 15:49:49 -0500424
425 if (centeredIcon != null) {
426 centeredIcon.setNotification(sbn);
427 if (!centeredIcon.set(ic)) {
428 throw new InflationException("Couldn't update centered icon: " + ic);
429 }
430 }
Ned Burnsf81c4c42019-01-07 14:10:43 -0500431 }
432 }
433
434 public int getContrastedColor(Context context, boolean isLowPriority,
435 int backgroundColor) {
436 int rawColor = isLowPriority ? Notification.COLOR_DEFAULT :
437 notification.getNotification().color;
438 if (mCachedContrastColorIsFor == rawColor && mCachedContrastColor != COLOR_INVALID) {
439 return mCachedContrastColor;
440 }
441 final int contrasted = ContrastColorUtil.resolveContrastColor(context, rawColor,
442 backgroundColor);
443 mCachedContrastColorIsFor = rawColor;
444 mCachedContrastColor = contrasted;
445 return mCachedContrastColor;
446 }
447
448 /**
Joshua Tsuji614b1df2019-03-26 13:57:05 -0400449 * Returns our best guess for the most relevant text summary of the latest update to this
450 * notification, based on its type. Returns null if there should not be an update message.
451 */
452 public CharSequence getUpdateMessage(Context context) {
453 final Notification underlyingNotif = notification.getNotification();
454 final Class<? extends Notification.Style> style = underlyingNotif.getNotificationStyle();
455
456 try {
457 if (Notification.BigTextStyle.class.equals(style)) {
458 // Return the big text, it is big so probably important. If it's not there use the
459 // normal text.
460 CharSequence bigText =
461 underlyingNotif.extras.getCharSequence(Notification.EXTRA_BIG_TEXT);
462 return !TextUtils.isEmpty(bigText)
463 ? bigText
464 : underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT);
465 } else if (Notification.MessagingStyle.class.equals(style)) {
466 final List<Notification.MessagingStyle.Message> messages =
467 Notification.MessagingStyle.Message.getMessagesFromBundleArray(
468 (Parcelable[]) underlyingNotif.extras.get(
469 Notification.EXTRA_MESSAGES));
470
471 final Notification.MessagingStyle.Message latestMessage =
472 Notification.MessagingStyle.findLatestIncomingMessage(messages);
473
474 if (latestMessage != null) {
475 final CharSequence personName = latestMessage.getSenderPerson() != null
476 ? latestMessage.getSenderPerson().getName()
477 : null;
478
479 // Prepend the sender name if available since group chats also use messaging
480 // style.
481 if (!TextUtils.isEmpty(personName)) {
482 return context.getResources().getString(
483 R.string.notification_summary_message_format,
484 personName,
485 latestMessage.getText());
486 } else {
487 return latestMessage.getText();
488 }
489 }
490 } else if (Notification.InboxStyle.class.equals(style)) {
491 CharSequence[] lines =
492 underlyingNotif.extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES);
493
494 // Return the last line since it should be the most recent.
495 if (lines != null && lines.length > 0) {
496 return lines[lines.length - 1];
497 }
498 } else if (Notification.MediaStyle.class.equals(style)) {
499 // Return nothing, media updates aren't typically useful as a text update.
500 return null;
501 } else {
502 // Default to text extra.
503 return underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT);
504 }
505 } catch (ClassCastException | NullPointerException | ArrayIndexOutOfBoundsException e) {
506 // No use crashing, we'll just return null and the caller will assume there's no update
507 // message.
508 e.printStackTrace();
509 }
510
511 return null;
512 }
513
514 /**
Ned Burnsf81c4c42019-01-07 14:10:43 -0500515 * Abort all existing inflation tasks
516 */
517 public void abortTask() {
518 if (mRunningTask != null) {
519 mRunningTask.abort();
520 mRunningTask = null;
521 }
522 }
523
524 public void setInflationTask(InflationTask abortableTask) {
525 // abort any existing inflation
526 InflationTask existing = mRunningTask;
527 abortTask();
528 mRunningTask = abortableTask;
529 if (existing != null && mRunningTask != null) {
530 mRunningTask.supersedeTask(existing);
531 }
532 }
533
534 public void onInflationTaskFinished() {
535 mRunningTask = null;
536 }
537
538 @VisibleForTesting
539 public InflationTask getRunningTask() {
540 return mRunningTask;
541 }
542
543 /**
544 * Set a throwable that is used for debugging
545 *
546 * @param debugThrowable the throwable to save
547 */
548 public void setDebugThrowable(Throwable debugThrowable) {
549 mDebugThrowable = debugThrowable;
550 }
551
552 public Throwable getDebugThrowable() {
553 return mDebugThrowable;
554 }
555
556 public void onRemoteInputInserted() {
557 lastRemoteInputSent = NOT_LAUNCHED_YET;
558 remoteInputTextWhenReset = null;
559 }
560
561 public void setHasSentReply() {
562 hasSentReply = true;
563 }
564
565 public boolean isLastMessageFromReply() {
566 if (!hasSentReply) {
567 return false;
568 }
569 Bundle extras = notification.getNotification().extras;
570 CharSequence[] replyTexts = extras.getCharSequenceArray(
571 Notification.EXTRA_REMOTE_INPUT_HISTORY);
572 if (!ArrayUtils.isEmpty(replyTexts)) {
573 return true;
574 }
575 Parcelable[] messages = extras.getParcelableArray(Notification.EXTRA_MESSAGES);
576 if (messages != null && messages.length > 0) {
577 Parcelable message = messages[messages.length - 1];
578 if (message instanceof Bundle) {
579 Notification.MessagingStyle.Message lastMessage =
580 Notification.MessagingStyle.Message.getMessageFromBundle(
581 (Bundle) message);
582 if (lastMessage != null) {
583 Person senderPerson = lastMessage.getSenderPerson();
584 if (senderPerson == null) {
585 return true;
586 }
587 Person user = extras.getParcelable(Notification.EXTRA_MESSAGING_PERSON);
588 return Objects.equals(user, senderPerson);
589 }
590 }
591 }
592 return false;
593 }
594
595 public void setInitializationTime(long time) {
596 if (initializationTime == -1) {
597 initializationTime = time;
598 }
599 }
600
601 public void sendAccessibilityEvent(int eventType) {
602 if (row != null) {
603 row.sendAccessibilityEvent(eventType);
604 }
605 }
606
607 /**
608 * Used by NotificationMediaManager to determine... things
609 * @return {@code true} if we are a media notification
610 */
611 public boolean isMediaNotification() {
612 if (row == null) return false;
613
614 return row.isMediaRow();
615 }
616
617 /**
618 * We are a top level child if our parent is the list of notifications duh
619 * @return {@code true} if we're a top level notification
620 */
621 public boolean isTopLevelChild() {
622 return row != null && row.isTopLevelChild();
623 }
624
625 public void resetUserExpansion() {
626 if (row != null) row.resetUserExpansion();
627 }
628
Ned Burns1a5e22f2019-02-14 15:11:52 -0500629 public void freeContentViewWhenSafe(@InflationFlag int inflationFlag) {
Ned Burnsf81c4c42019-01-07 14:10:43 -0500630 if (row != null) row.freeContentViewWhenSafe(inflationFlag);
631 }
632
Ned Burnsf81c4c42019-01-07 14:10:43 -0500633 public boolean rowExists() {
634 return row != null;
635 }
636
637 public boolean isRowDismissed() {
638 return row != null && row.isDismissed();
639 }
640
641 public boolean isRowRemoved() {
642 return row != null && row.isRemoved();
643 }
644
645 /**
646 * @return {@code true} if the row is null or removed
647 */
648 public boolean isRemoved() {
649 //TODO: recycling invalidates this
650 return row == null || row.isRemoved();
651 }
652
Ned Burnsf81c4c42019-01-07 14:10:43 -0500653 public boolean isRowPinned() {
654 return row != null && row.isPinned();
655 }
656
657 public void setRowPinned(boolean pinned) {
658 if (row != null) row.setPinned(pinned);
659 }
660
Ned Burnsf81c4c42019-01-07 14:10:43 -0500661 public boolean isRowHeadsUp() {
662 return row != null && row.isHeadsUp();
663 }
664
Selim Cinekb57dd8a2019-06-14 12:27:58 -0700665 public boolean showingPulsing() {
666 return row != null && row.showingPulsing();
667 }
668
Ned Burnsf81c4c42019-01-07 14:10:43 -0500669 public void setHeadsUp(boolean shouldHeadsUp) {
670 if (row != null) row.setHeadsUp(shouldHeadsUp);
671 }
672
Selim Cinek459aee32019-02-20 11:18:56 -0800673
Selim Cinekc3fec682019-06-06 18:11:07 -0700674 public void setHeadsUpAnimatingAway(boolean animatingAway) {
675 if (row != null) row.setHeadsUpAnimatingAway(animatingAway);
Selim Cinek459aee32019-02-20 11:18:56 -0800676 }
677
678
Ned Burnsf81c4c42019-01-07 14:10:43 -0500679 public boolean mustStayOnScreen() {
680 return row != null && row.mustStayOnScreen();
681 }
682
683 public void setHeadsUpIsVisible() {
684 if (row != null) row.setHeadsUpIsVisible();
685 }
686
687 //TODO: i'm imagining a world where this isn't just the row, but I could be rwong
688 public ExpandableNotificationRow getHeadsUpAnimationView() {
689 return row;
690 }
691
692 public void setUserLocked(boolean userLocked) {
693 if (row != null) row.setUserLocked(userLocked);
694 }
695
696 public void setUserExpanded(boolean userExpanded, boolean allowChildExpansion) {
697 if (row != null) row.setUserExpanded(userExpanded, allowChildExpansion);
698 }
699
700 public void setGroupExpansionChanging(boolean changing) {
701 if (row != null) row.setGroupExpansionChanging(changing);
702 }
703
704 public void notifyHeightChanged(boolean needsAnimation) {
705 if (row != null) row.notifyHeightChanged(needsAnimation);
706 }
707
708 public void closeRemoteInput() {
709 if (row != null) row.closeRemoteInput();
710 }
711
712 public boolean areChildrenExpanded() {
713 return row != null && row.areChildrenExpanded();
714 }
715
716 public boolean keepInParent() {
717 return row != null && row.keepInParent();
718 }
719
720 //TODO: probably less confusing to say "is group fully visible"
721 public boolean isGroupNotFullyVisible() {
722 return row == null || row.isGroupNotFullyVisible();
723 }
724
725 public NotificationGuts getGuts() {
726 if (row != null) return row.getGuts();
727 return null;
728 }
729
Ned Burnsf81c4c42019-01-07 14:10:43 -0500730 public void removeRow() {
731 if (row != null) row.setRemoved();
732 }
733
734 public boolean isSummaryWithChildren() {
735 return row != null && row.isSummaryWithChildren();
736 }
737
738 public void setKeepInParent(boolean keep) {
739 if (row != null) row.setKeepInParent(keep);
740 }
741
742 public void onDensityOrFontScaleChanged() {
743 if (row != null) row.onDensityOrFontScaleChanged();
744 }
745
746 public boolean areGutsExposed() {
747 return row != null && row.getGuts() != null && row.getGuts().isExposed();
748 }
749
750 public boolean isChildInGroup() {
751 return parent == null;
752 }
753
Ned Burnsf81c4c42019-01-07 14:10:43 -0500754 /**
755 * @return Can the underlying notification be cleared? This can be different from whether the
756 * notification can be dismissed in case notifications are sensitive on the lockscreen.
757 * @see #canViewBeDismissed()
758 */
759 public boolean isClearable() {
760 if (notification == null || !notification.isClearable()) {
761 return false;
762 }
Evan Lairdce2d1af2019-05-30 16:00:22 -0400763
764 List<NotificationEntry> children = getChildren();
765 if (children != null && children.size() > 0) {
Ned Burnsf81c4c42019-01-07 14:10:43 -0500766 for (int i = 0; i < children.size(); i++) {
767 NotificationEntry child = children.get(i);
768 if (!child.isClearable()) {
769 return false;
770 }
771 }
772 }
773 return true;
774 }
775
776 public boolean canViewBeDismissed() {
777 if (row == null) return true;
778 return row.canViewBeDismissed();
779 }
780
781 @VisibleForTesting
782 boolean isExemptFromDndVisualSuppression() {
783 if (isNotificationBlockedByPolicy(notification.getNotification())) {
784 return false;
785 }
786
787 if ((notification.getNotification().flags
788 & Notification.FLAG_FOREGROUND_SERVICE) != 0) {
789 return true;
790 }
791 if (notification.getNotification().isMediaNotification()) {
792 return true;
793 }
794 if (mIsSystemNotification != null && mIsSystemNotification) {
795 return true;
796 }
797 return false;
798 }
799
800 private boolean shouldSuppressVisualEffect(int effect) {
801 if (isExemptFromDndVisualSuppression()) {
802 return false;
803 }
804 return (suppressedVisualEffects & effect) != 0;
805 }
806
807 /**
808 * Returns whether {@link Policy#SUPPRESSED_EFFECT_FULL_SCREEN_INTENT}
809 * is set for this entry.
810 */
811 public boolean shouldSuppressFullScreenIntent() {
812 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_FULL_SCREEN_INTENT);
813 }
814
815 /**
816 * Returns whether {@link Policy#SUPPRESSED_EFFECT_PEEK}
817 * is set for this entry.
818 */
819 public boolean shouldSuppressPeek() {
820 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_PEEK);
821 }
822
823 /**
824 * Returns whether {@link Policy#SUPPRESSED_EFFECT_STATUS_BAR}
825 * is set for this entry.
826 */
827 public boolean shouldSuppressStatusBar() {
828 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_STATUS_BAR);
829 }
830
831 /**
832 * Returns whether {@link Policy#SUPPRESSED_EFFECT_AMBIENT}
833 * is set for this entry.
834 */
835 public boolean shouldSuppressAmbient() {
836 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_AMBIENT);
837 }
838
839 /**
840 * Returns whether {@link Policy#SUPPRESSED_EFFECT_NOTIFICATION_LIST}
841 * is set for this entry.
842 */
843 public boolean shouldSuppressNotificationList() {
844 return shouldSuppressVisualEffect(SUPPRESSED_EFFECT_NOTIFICATION_LIST);
845 }
846
847 /**
848 * Categories that are explicitly called out on DND settings screens are always blocked, if
849 * DND has flagged them, even if they are foreground or system notifications that might
850 * otherwise visually bypass DND.
851 */
852 private static boolean isNotificationBlockedByPolicy(Notification n) {
853 return isCategory(CATEGORY_CALL, n)
854 || isCategory(CATEGORY_MESSAGE, n)
855 || isCategory(CATEGORY_ALARM, n)
856 || isCategory(CATEGORY_EVENT, n)
857 || isCategory(CATEGORY_REMINDER, n);
858 }
859
860 private static boolean isCategory(String category, Notification n) {
861 return Objects.equals(n.category, category);
862 }
Milo Sredkov13d88112019-02-01 12:23:24 +0000863
Selim Cinekb2c5dc52019-06-24 15:46:52 -0700864 /**
865 * Set this notification to be sensitive.
866 *
867 * @param sensitive true if the content of this notification is sensitive right now
868 * @param deviceSensitive true if the device in general is sensitive right now
869 */
870 public void setSensitive(boolean sensitive, boolean deviceSensitive) {
871 getRow().setSensitive(sensitive, deviceSensitive);
872 if (sensitive != mSensitive) {
873 mSensitive = sensitive;
874 if (mOnSensitiveChangedListener != null) {
875 mOnSensitiveChangedListener.run();
876 }
877 }
878 }
879
880 public boolean isSensitive() {
881 return mSensitive;
882 }
883
884 public void setOnSensitiveChangedListener(Runnable listener) {
885 mOnSensitiveChangedListener = listener;
886 }
887
Milo Sredkov13d88112019-02-01 12:23:24 +0000888 /** Information about a suggestion that is being edited. */
889 public static class EditedSuggestionInfo {
890
891 /**
892 * The value of the suggestion (before any user edits).
893 */
894 public final CharSequence originalText;
895
896 /**
897 * The index of the suggestion that is being edited.
898 */
899 public final int index;
900
901 public EditedSuggestionInfo(CharSequence originalText, int index) {
902 this.originalText = originalText;
903 this.index = index;
904 }
905 }
Steven Wu8ba8ca92019-04-11 10:47:42 -0400906
907 /**
908 * Returns whether the notification is a foreground service. It shows that this is an ongoing
909 * bubble.
910 */
911 public boolean isForegroundService() {
912 return (notification.getNotification().flags & Notification.FLAG_FOREGROUND_SERVICE) != 0;
913 }
Ned Burnsf81c4c42019-01-07 14:10:43 -0500914}