blob: c3cee35d37fb7abcc9d2ce5fafa3c36f23ec5407 [file] [log] [blame]
Mady Mellor3dff9e62019-02-05 18:12:53 -08001/*
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 */
16package com.android.systemui.bubbles;
17
18
Issei Suzukicac2a502019-04-16 16:52:50 +020019import static android.view.Display.INVALID_DISPLAY;
20
Mark Renouf9ba6cea2019-04-17 11:53:50 -040021import static com.android.internal.annotations.VisibleForTesting.Visibility.PRIVATE;
22
Mady Mellor70cba7bb2019-07-02 15:06:07 -070023import android.annotation.NonNull;
Mady Mellor99a302602019-06-14 11:39:56 -070024import android.annotation.Nullable;
25import android.app.Notification;
26import android.app.PendingIntent;
Lyn Han6c40fe72019-05-08 14:06:33 -070027import android.content.Context;
Mady Mellor99a302602019-06-14 11:39:56 -070028import android.content.Intent;
Lyn Han6c40fe72019-05-08 14:06:33 -070029import android.content.pm.ApplicationInfo;
30import android.content.pm.PackageManager;
Mady Mellor99a302602019-06-14 11:39:56 -070031import android.content.res.Resources;
Lyn Han4c1731f2019-06-19 19:14:24 -070032import android.graphics.drawable.Drawable;
Mady Mellor99a302602019-06-14 11:39:56 -070033import android.os.Parcelable;
Mark Renouf71a3af62019-04-08 15:02:54 -040034import android.os.UserHandle;
Mady Mellor99a302602019-06-14 11:39:56 -070035import android.provider.Settings;
36import android.text.TextUtils;
37import android.util.Log;
Mady Mellor3dff9e62019-02-05 18:12:53 -080038import android.view.LayoutInflater;
39
Mark Renouf9ba6cea2019-04-17 11:53:50 -040040import com.android.internal.annotations.VisibleForTesting;
Mady Mellor3dff9e62019-02-05 18:12:53 -080041import com.android.systemui.R;
42import com.android.systemui.statusbar.notification.collection.NotificationEntry;
43
Mady Mellor70cba7bb2019-07-02 15:06:07 -070044import java.io.FileDescriptor;
45import java.io.PrintWriter;
Mady Mellor99a302602019-06-14 11:39:56 -070046import java.util.List;
Mark Renouf71a3af62019-04-08 15:02:54 -040047import java.util.Objects;
48
Mady Mellor3dff9e62019-02-05 18:12:53 -080049/**
50 * Encapsulates the data and UI elements of a bubble.
51 */
52class Bubble {
Mady Mellor99a302602019-06-14 11:39:56 -070053 private static final String TAG = "Bubble";
54
55 private NotificationEntry mEntry;
Mark Renouf85e0a902019-04-05 15:51:51 -040056 private final String mKey;
Mark Renouf71a3af62019-04-08 15:02:54 -040057 private final String mGroupId;
Lyn Han6c40fe72019-05-08 14:06:33 -070058 private String mAppName;
Lyn Han4c1731f2019-06-19 19:14:24 -070059 private Drawable mUserBadgedAppIcon;
Mark Renouf85e0a902019-04-05 15:51:51 -040060
61 private boolean mInflated;
Mady Mellored99c272019-06-13 15:58:30 -070062 private BubbleView mIconView;
63 private BubbleExpandedView mExpandedView;
Mady Mellor99a302602019-06-14 11:39:56 -070064
Mark Renouf9ba6cea2019-04-17 11:53:50 -040065 private long mLastUpdated;
66 private long mLastAccessed;
Mady Mellored99c272019-06-13 15:58:30 -070067 private boolean mIsRemoved;
Mark Renouf71a3af62019-04-08 15:02:54 -040068
Mady Mellorce23c462019-06-17 17:30:07 -070069 /**
70 * Whether this notification should be shown in the shade when it is also displayed as a bubble.
71 *
72 * <p>When a notification is a bubble we don't show it in the shade once the bubble has been
73 * expanded</p>
74 */
75 private boolean mShowInShadeWhenBubble = true;
76
Mady Mellordf48d0a2019-06-25 18:26:46 -070077 /**
78 * Whether the bubble should show a dot for the notification indicating updated content.
79 */
80 private boolean mShowBubbleUpdateDot = true;
81
Mark Renoufc19b4732019-06-26 12:08:33 -040082 /** Whether flyout text should be suppressed, regardless of any other flags or state. */
83 private boolean mSuppressFlyout;
84
Mark Renoufba5ab512019-05-02 15:21:01 -040085 public static String groupId(NotificationEntry entry) {
Mark Renouf71a3af62019-04-08 15:02:54 -040086 UserHandle user = entry.notification.getUser();
Mark Renouf9ba6cea2019-04-17 11:53:50 -040087 return user.getIdentifier() + "|" + entry.notification.getPackageName();
88 }
89
90 /** Used in tests when no UI is required. */
91 @VisibleForTesting(visibility = PRIVATE)
Lyn Han6c40fe72019-05-08 14:06:33 -070092 Bubble(Context context, NotificationEntry e) {
Mady Mellored99c272019-06-13 15:58:30 -070093 mEntry = e;
Mark Renouf85e0a902019-04-05 15:51:51 -040094 mKey = e.key;
Mark Renouf9ba6cea2019-04-17 11:53:50 -040095 mLastUpdated = e.notification.getPostTime();
Mark Renouf71a3af62019-04-08 15:02:54 -040096 mGroupId = groupId(e);
Lyn Han6c40fe72019-05-08 14:06:33 -070097
Mady Mellored99c272019-06-13 15:58:30 -070098 PackageManager pm = context.getPackageManager();
Lyn Han6c40fe72019-05-08 14:06:33 -070099 ApplicationInfo info;
100 try {
Mady Mellored99c272019-06-13 15:58:30 -0700101 info = pm.getApplicationInfo(
102 mEntry.notification.getPackageName(),
Lyn Han6c40fe72019-05-08 14:06:33 -0700103 PackageManager.MATCH_UNINSTALLED_PACKAGES
104 | PackageManager.MATCH_DISABLED_COMPONENTS
105 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE
106 | PackageManager.MATCH_DIRECT_BOOT_AWARE);
107 if (info != null) {
Mady Mellored99c272019-06-13 15:58:30 -0700108 mAppName = String.valueOf(pm.getApplicationLabel(info));
Lyn Han6c40fe72019-05-08 14:06:33 -0700109 }
Lyn Han4c1731f2019-06-19 19:14:24 -0700110 Drawable appIcon = pm.getApplicationIcon(mEntry.notification.getPackageName());
111 mUserBadgedAppIcon = pm.getUserBadgedIcon(appIcon, mEntry.notification.getUser());
Lyn Han6c40fe72019-05-08 14:06:33 -0700112 } catch (PackageManager.NameNotFoundException unused) {
Mady Mellored99c272019-06-13 15:58:30 -0700113 mAppName = mEntry.notification.getPackageName();
Lyn Han6c40fe72019-05-08 14:06:33 -0700114 }
Mark Renouf85e0a902019-04-05 15:51:51 -0400115 }
116
Mark Renouf85e0a902019-04-05 15:51:51 -0400117 public String getKey() {
118 return mKey;
119 }
120
Mady Mellored99c272019-06-13 15:58:30 -0700121 public NotificationEntry getEntry() {
122 return mEntry;
123 }
124
Mark Renouf71a3af62019-04-08 15:02:54 -0400125 public String getGroupId() {
126 return mGroupId;
127 }
128
129 public String getPackageName() {
Mady Mellored99c272019-06-13 15:58:30 -0700130 return mEntry.notification.getPackageName();
Mark Renouf71a3af62019-04-08 15:02:54 -0400131 }
132
Lyn Han6c40fe72019-05-08 14:06:33 -0700133 public String getAppName() {
134 return mAppName;
135 }
136
Mark Renouf85e0a902019-04-05 15:51:51 -0400137 boolean isInflated() {
138 return mInflated;
139 }
140
Mady Mellor99a302602019-06-14 11:39:56 -0700141 void updateDotVisibility() {
Mady Mellored99c272019-06-13 15:58:30 -0700142 if (mIconView != null) {
143 mIconView.updateDotVisibility(true /* animate */);
Mark Renouf71a3af62019-04-08 15:02:54 -0400144 }
145 }
146
Mady Mellorce23c462019-06-17 17:30:07 -0700147 BubbleView getIconView() {
Mady Mellored99c272019-06-13 15:58:30 -0700148 return mIconView;
149 }
150
Mady Mellorce23c462019-06-17 17:30:07 -0700151 BubbleExpandedView getExpandedView() {
Mady Mellored99c272019-06-13 15:58:30 -0700152 return mExpandedView;
153 }
154
Mark Renoufc19b4732019-06-26 12:08:33 -0400155 void cleanupExpandedState() {
156 if (mExpandedView != null) {
157 mExpandedView.cleanUpExpandedState();
158 }
159 }
160
Mark Renouf85e0a902019-04-05 15:51:51 -0400161 void inflate(LayoutInflater inflater, BubbleStackView stackView) {
162 if (mInflated) {
163 return;
164 }
Mady Mellored99c272019-06-13 15:58:30 -0700165 mIconView = (BubbleView) inflater.inflate(
Mady Mellor3dff9e62019-02-05 18:12:53 -0800166 R.layout.bubble_view, stackView, false /* attachToRoot */);
Mady Mellor99a302602019-06-14 11:39:56 -0700167 mIconView.setBubble(this);
Lyn Han4c1731f2019-06-19 19:14:24 -0700168 mIconView.setAppIcon(mUserBadgedAppIcon);
Mady Mellor3dff9e62019-02-05 18:12:53 -0800169
Mady Mellored99c272019-06-13 15:58:30 -0700170 mExpandedView = (BubbleExpandedView) inflater.inflate(
Mady Mellor3dff9e62019-02-05 18:12:53 -0800171 R.layout.bubble_expanded_view, stackView, false /* attachToRoot */);
Mady Mellor99a302602019-06-14 11:39:56 -0700172 mExpandedView.setBubble(this, stackView, mAppName);
Lyn Han6c40fe72019-05-08 14:06:33 -0700173
Mark Renouf85e0a902019-04-05 15:51:51 -0400174 mInflated = true;
Mady Mellor3dff9e62019-02-05 18:12:53 -0800175 }
176
Issei Suzukicac2a502019-04-16 16:52:50 +0200177 /**
178 * Set visibility of bubble in the expanded state.
179 *
180 * @param visibility {@code true} if the expanded bubble should be visible on the screen.
181 *
182 * Note that this contents visibility doesn't affect visibility at {@link android.view.View},
183 * and setting {@code false} actually means rendering the expanded view in transparent.
184 */
185 void setContentVisibility(boolean visibility) {
Mady Mellored99c272019-06-13 15:58:30 -0700186 if (mExpandedView != null) {
187 mExpandedView.setContentVisibility(visibility);
Issei Suzukicac2a502019-04-16 16:52:50 +0200188 }
189 }
190
Mady Mellorce23c462019-06-17 17:30:07 -0700191 void updateEntry(NotificationEntry entry) {
Mady Mellor99a302602019-06-14 11:39:56 -0700192 mEntry = entry;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400193 mLastUpdated = entry.notification.getPostTime();
Mark Renouf85e0a902019-04-05 15:51:51 -0400194 if (mInflated) {
Mady Mellor99a302602019-06-14 11:39:56 -0700195 mIconView.update(this);
196 mExpandedView.update(this);
Mark Renouf85e0a902019-04-05 15:51:51 -0400197 }
Mady Mellor3dff9e62019-02-05 18:12:53 -0800198 }
Mark Renouf71a3af62019-04-08 15:02:54 -0400199
Mark Renoufba5ab512019-05-02 15:21:01 -0400200 /**
201 * @return the newer of {@link #getLastUpdateTime()} and {@link #getLastAccessTime()}
202 */
Mady Mellor99a302602019-06-14 11:39:56 -0700203 long getLastActivity() {
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400204 return Math.max(mLastUpdated, mLastAccessed);
205 }
206
207 /**
Mark Renoufba5ab512019-05-02 15:21:01 -0400208 * @return the timestamp in milliseconds of the most recent notification entry for this bubble
209 */
Mady Mellor99a302602019-06-14 11:39:56 -0700210 long getLastUpdateTime() {
Mark Renoufba5ab512019-05-02 15:21:01 -0400211 return mLastUpdated;
212 }
213
214 /**
215 * @return the timestamp in milliseconds when this bubble was last displayed in expanded state
216 */
Mady Mellor99a302602019-06-14 11:39:56 -0700217 long getLastAccessTime() {
Mark Renoufba5ab512019-05-02 15:21:01 -0400218 return mLastAccessed;
219 }
220
221 /**
Issei Suzukicac2a502019-04-16 16:52:50 +0200222 * @return the display id of the virtual display on which bubble contents is drawn.
223 */
224 int getDisplayId() {
Mady Mellored99c272019-06-13 15:58:30 -0700225 return mExpandedView != null ? mExpandedView.getVirtualDisplayId() : INVALID_DISPLAY;
Issei Suzukicac2a502019-04-16 16:52:50 +0200226 }
227
228 /**
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400229 * Should be invoked whenever a Bubble is accessed (selected while expanded).
230 */
231 void markAsAccessedAt(long lastAccessedMillis) {
232 mLastAccessed = lastAccessedMillis;
Mady Mellor99a302602019-06-14 11:39:56 -0700233 setShowInShadeWhenBubble(false);
Mady Mellordf48d0a2019-06-25 18:26:46 -0700234 setShowBubbleDot(false);
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400235 }
236
237 /**
Mady Mellorce23c462019-06-17 17:30:07 -0700238 * Whether this notification should be shown in the shade when it is also displayed as a
239 * bubble.
240 */
241 boolean showInShadeWhenBubble() {
242 return !mEntry.isRowDismissed() && !shouldSuppressNotification()
243 && (!mEntry.isClearable() || mShowInShadeWhenBubble);
244 }
245
246 /**
247 * Sets whether this notification should be shown in the shade when it is also displayed as a
248 * bubble.
249 */
250 void setShowInShadeWhenBubble(boolean showInShade) {
251 mShowInShadeWhenBubble = showInShade;
252 }
253
254 /**
Mady Mellordf48d0a2019-06-25 18:26:46 -0700255 * Sets whether the bubble for this notification should show a dot indicating updated content.
256 */
257 void setShowBubbleDot(boolean showDot) {
258 mShowBubbleUpdateDot = showDot;
259 }
260
261 /**
262 * Whether the bubble for this notification should show a dot indicating updated content.
263 */
264 boolean showBubbleDot() {
265 return mShowBubbleUpdateDot && !mEntry.shouldSuppressNotificationDot();
266 }
267
268 /**
269 * Whether the flyout for the bubble should be shown.
270 */
271 boolean showFlyoutForBubble() {
Mark Renoufc19b4732019-06-26 12:08:33 -0400272 return !mSuppressFlyout && !mEntry.shouldSuppressPeek()
273 && !mEntry.shouldSuppressNotificationList();
274 }
275
276 /**
277 * Set whether the flyout text for the bubble should be shown when an update is received.
278 *
279 * @param suppressFlyout whether the flyout text is shown
280 */
281 void setSuppressFlyout(boolean suppressFlyout) {
282 mSuppressFlyout = suppressFlyout;
Mady Mellordf48d0a2019-06-25 18:26:46 -0700283 }
284
285 /**
Mady Mellor99a302602019-06-14 11:39:56 -0700286 * Returns whether the notification for this bubble is a foreground service. It shows that this
287 * is an ongoing bubble.
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400288 */
Mady Mellor99a302602019-06-14 11:39:56 -0700289 boolean isOngoing() {
290 int flags = mEntry.notification.getNotification().flags;
291 return (flags & Notification.FLAG_FOREGROUND_SERVICE) != 0;
292 }
293
294 float getDesiredHeight(Context context) {
295 Notification.BubbleMetadata data = mEntry.getBubbleMetadata();
296 boolean useRes = data.getDesiredHeightResId() != 0;
297 if (useRes) {
298 return getDimenForPackageUser(context, data.getDesiredHeightResId(),
299 mEntry.notification.getPackageName(),
300 mEntry.notification.getUser().getIdentifier());
301 } else {
302 return data.getDesiredHeight()
303 * context.getResources().getDisplayMetrics().density;
304 }
305 }
306
Mady Mellor70cba7bb2019-07-02 15:06:07 -0700307 String getDesiredHeightString() {
308 Notification.BubbleMetadata data = mEntry.getBubbleMetadata();
309 boolean useRes = data.getDesiredHeightResId() != 0;
310 if (useRes) {
311 return String.valueOf(data.getDesiredHeightResId());
312 } else {
313 return String.valueOf(data.getDesiredHeight());
314 }
315 }
316
Mady Mellor99a302602019-06-14 11:39:56 -0700317 @Nullable
318 PendingIntent getBubbleIntent(Context context) {
319 Notification notif = mEntry.notification.getNotification();
320 Notification.BubbleMetadata data = notif.getBubbleMetadata();
321 if (BubbleController.canLaunchInActivityView(context, mEntry) && data != null) {
322 return data.getIntent();
323 }
324 return null;
325 }
326
327 Intent getSettingsIntent() {
328 final Intent intent = new Intent(Settings.ACTION_APP_NOTIFICATION_BUBBLE_SETTINGS);
329 intent.putExtra(Settings.EXTRA_APP_PACKAGE, getPackageName());
330 intent.putExtra(Settings.EXTRA_APP_UID, mEntry.notification.getUid());
331 intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
332 intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
333 intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
334 return intent;
335 }
336
337 /**
338 * Returns our best guess for the most relevant text summary of the latest update to this
339 * notification, based on its type. Returns null if there should not be an update message.
340 */
341 CharSequence getUpdateMessage(Context context) {
342 final Notification underlyingNotif = mEntry.notification.getNotification();
343 final Class<? extends Notification.Style> style = underlyingNotif.getNotificationStyle();
344
345 try {
346 if (Notification.BigTextStyle.class.equals(style)) {
347 // Return the big text, it is big so probably important. If it's not there use the
348 // normal text.
349 CharSequence bigText =
350 underlyingNotif.extras.getCharSequence(Notification.EXTRA_BIG_TEXT);
351 return !TextUtils.isEmpty(bigText)
352 ? bigText
353 : underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT);
354 } else if (Notification.MessagingStyle.class.equals(style)) {
355 final List<Notification.MessagingStyle.Message> messages =
356 Notification.MessagingStyle.Message.getMessagesFromBundleArray(
357 (Parcelable[]) underlyingNotif.extras.get(
358 Notification.EXTRA_MESSAGES));
359
360 final Notification.MessagingStyle.Message latestMessage =
361 Notification.MessagingStyle.findLatestIncomingMessage(messages);
362
363 if (latestMessage != null) {
364 final CharSequence personName = latestMessage.getSenderPerson() != null
365 ? latestMessage.getSenderPerson().getName()
366 : null;
367
368 // Prepend the sender name if available since group chats also use messaging
369 // style.
370 if (!TextUtils.isEmpty(personName)) {
371 return context.getResources().getString(
372 R.string.notification_summary_message_format,
373 personName,
374 latestMessage.getText());
375 } else {
376 return latestMessage.getText();
377 }
378 }
379 } else if (Notification.InboxStyle.class.equals(style)) {
380 CharSequence[] lines =
381 underlyingNotif.extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES);
382
383 // Return the last line since it should be the most recent.
384 if (lines != null && lines.length > 0) {
385 return lines[lines.length - 1];
386 }
387 } else if (Notification.MediaStyle.class.equals(style)) {
388 // Return nothing, media updates aren't typically useful as a text update.
389 return null;
390 } else {
391 // Default to text extra.
392 return underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT);
393 }
394 } catch (ClassCastException | NullPointerException | ArrayIndexOutOfBoundsException e) {
395 // No use crashing, we'll just return null and the caller will assume there's no update
396 // message.
397 e.printStackTrace();
398 }
399
400 return null;
401 }
402
403 private int getDimenForPackageUser(Context context, int resId, String pkg, int userId) {
404 PackageManager pm = context.getPackageManager();
405 Resources r;
406 if (pkg != null) {
407 try {
408 if (userId == UserHandle.USER_ALL) {
409 userId = UserHandle.USER_SYSTEM;
410 }
411 r = pm.getResourcesForApplicationAsUser(pkg, userId);
412 return r.getDimensionPixelSize(resId);
413 } catch (PackageManager.NameNotFoundException ex) {
414 // Uninstalled, don't care
415 } catch (Resources.NotFoundException e) {
416 // Invalid res id, return 0 and user our default
417 Log.e(TAG, "Couldn't find desired height res id", e);
418 }
419 }
420 return 0;
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400421 }
422
Mady Mellorce23c462019-06-17 17:30:07 -0700423 private boolean shouldSuppressNotification() {
424 return mEntry.getBubbleMetadata() != null
425 && mEntry.getBubbleMetadata().isNotificationSuppressed();
426 }
427
Mady Mellor70cba7bb2019-07-02 15:06:07 -0700428 boolean shouldAutoExpand() {
429 Notification.BubbleMetadata metadata = mEntry.getBubbleMetadata();
430 return metadata != null && metadata.getAutoExpandBubble();
431 }
432
Mark Renouf9ba6cea2019-04-17 11:53:50 -0400433 @Override
434 public String toString() {
435 return "Bubble{" + mKey + '}';
436 }
437
Mady Mellor70cba7bb2019-07-02 15:06:07 -0700438 /**
439 * Description of current bubble state.
440 */
441 public void dump(
442 @NonNull FileDescriptor fd, @NonNull PrintWriter pw, @NonNull String[] args) {
443 pw.print("key: "); pw.println(mKey);
444 pw.print(" showInShade: "); pw.println(showInShadeWhenBubble());
445 pw.print(" showDot: "); pw.println(showBubbleDot());
446 pw.print(" showFlyout: "); pw.println(showFlyoutForBubble());
447 pw.print(" desiredHeight: "); pw.println(getDesiredHeightString());
448 pw.print(" suppressNotif: "); pw.println(shouldSuppressNotification());
449 pw.print(" autoExpand: "); pw.println(shouldAutoExpand());
450 }
451
Mark Renouf71a3af62019-04-08 15:02:54 -0400452 @Override
453 public boolean equals(Object o) {
454 if (this == o) return true;
455 if (!(o instanceof Bubble)) return false;
456 Bubble bubble = (Bubble) o;
457 return Objects.equals(mKey, bubble.mKey);
458 }
459
460 @Override
461 public int hashCode() {
462 return Objects.hash(mKey);
463 }
Mady Mellor3dff9e62019-02-05 18:12:53 -0800464}