Mady Mellor | 3df7ab0 | 2019-12-09 15:07:10 -0800 | [diff] [blame] | 1 | /* |
| 2 | * Copyright (C) 2019 The Android Open Source Project |
| 3 | * |
| 4 | * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | * you may not use this file except in compliance with the License. |
| 6 | * You may obtain a copy of the License at |
| 7 | * |
| 8 | * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | * |
| 10 | * Unless required by applicable law or agreed to in writing, software |
| 11 | * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | * See the License for the specific language governing permissions and |
| 14 | * limitations under the License. |
| 15 | */ |
| 16 | |
| 17 | package com.android.systemui.bubbles; |
| 18 | |
| 19 | import static com.android.systemui.bubbles.BadgedImageView.DEFAULT_PATH_SIZE; |
| 20 | import static com.android.systemui.bubbles.BadgedImageView.WHITE_SCRIM_ALPHA; |
| 21 | import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_BUBBLES; |
| 22 | import static com.android.systemui.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; |
| 23 | |
Mady Mellor | df898fd | 2020-01-09 09:26:36 -0800 | [diff] [blame] | 24 | import android.annotation.NonNull; |
| 25 | import android.app.Notification; |
| 26 | import android.app.Person; |
Mady Mellor | 3df7ab0 | 2019-12-09 15:07:10 -0800 | [diff] [blame] | 27 | import android.content.Context; |
Lyn Han | e566b36 | 2020-03-13 17:32:19 -0700 | [diff] [blame] | 28 | import android.content.Intent; |
Mady Mellor | 3df7ab0 | 2019-12-09 15:07:10 -0800 | [diff] [blame] | 29 | import android.content.pm.ApplicationInfo; |
| 30 | import android.content.pm.PackageManager; |
| 31 | import android.content.pm.ShortcutInfo; |
| 32 | import android.graphics.Bitmap; |
| 33 | import android.graphics.Color; |
| 34 | import android.graphics.Matrix; |
| 35 | import android.graphics.Path; |
| 36 | import android.graphics.drawable.Drawable; |
Lyn Han | e566b36 | 2020-03-13 17:32:19 -0700 | [diff] [blame] | 37 | import android.graphics.drawable.Icon; |
Mady Mellor | 3df7ab0 | 2019-12-09 15:07:10 -0800 | [diff] [blame] | 38 | import android.os.AsyncTask; |
Mady Mellor | df898fd | 2020-01-09 09:26:36 -0800 | [diff] [blame] | 39 | import android.os.Parcelable; |
Pinyao Ting | 293b83d | 2020-05-06 17:10:56 -0700 | [diff] [blame] | 40 | import android.os.UserHandle; |
Mady Mellor | 3df7ab0 | 2019-12-09 15:07:10 -0800 | [diff] [blame] | 41 | import android.service.notification.StatusBarNotification; |
Mady Mellor | df898fd | 2020-01-09 09:26:36 -0800 | [diff] [blame] | 42 | import android.text.TextUtils; |
Mady Mellor | 3df7ab0 | 2019-12-09 15:07:10 -0800 | [diff] [blame] | 43 | import android.util.Log; |
| 44 | import android.util.PathParser; |
| 45 | import android.view.LayoutInflater; |
| 46 | |
| 47 | import androidx.annotation.Nullable; |
| 48 | |
| 49 | import com.android.internal.graphics.ColorUtils; |
| 50 | import com.android.launcher3.icons.BitmapInfo; |
| 51 | import com.android.systemui.R; |
Mady Mellor | df898fd | 2020-01-09 09:26:36 -0800 | [diff] [blame] | 52 | import com.android.systemui.statusbar.notification.collection.NotificationEntry; |
Mady Mellor | 3df7ab0 | 2019-12-09 15:07:10 -0800 | [diff] [blame] | 53 | |
| 54 | import java.lang.ref.WeakReference; |
Mady Mellor | df898fd | 2020-01-09 09:26:36 -0800 | [diff] [blame] | 55 | import java.util.List; |
Mady Mellor | 3df7ab0 | 2019-12-09 15:07:10 -0800 | [diff] [blame] | 56 | |
| 57 | /** |
| 58 | * Simple task to inflate views & load necessary info to display a bubble. |
| 59 | */ |
| 60 | public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask.BubbleViewInfo> { |
| 61 | private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleViewInfoTask" : TAG_BUBBLES; |
| 62 | |
| 63 | |
| 64 | /** |
| 65 | * Callback to find out when the bubble has been inflated & necessary data loaded. |
| 66 | */ |
| 67 | public interface Callback { |
| 68 | /** |
| 69 | * Called when data has been loaded for the bubble. |
| 70 | */ |
| 71 | void onBubbleViewsReady(Bubble bubble); |
| 72 | } |
| 73 | |
| 74 | private Bubble mBubble; |
| 75 | private WeakReference<Context> mContext; |
| 76 | private WeakReference<BubbleStackView> mStackView; |
| 77 | private BubbleIconFactory mIconFactory; |
Pinyao Ting | 293b83d | 2020-05-06 17:10:56 -0700 | [diff] [blame] | 78 | private boolean mSkipInflation; |
Mady Mellor | 3df7ab0 | 2019-12-09 15:07:10 -0800 | [diff] [blame] | 79 | private Callback mCallback; |
| 80 | |
| 81 | /** |
| 82 | * Creates a task to load information for the provided {@link Bubble}. Once all info |
| 83 | * is loaded, {@link Callback} is notified. |
| 84 | */ |
| 85 | BubbleViewInfoTask(Bubble b, |
| 86 | Context context, |
| 87 | BubbleStackView stackView, |
| 88 | BubbleIconFactory factory, |
Pinyao Ting | 293b83d | 2020-05-06 17:10:56 -0700 | [diff] [blame] | 89 | boolean skipInflation, |
Mady Mellor | 3df7ab0 | 2019-12-09 15:07:10 -0800 | [diff] [blame] | 90 | Callback c) { |
| 91 | mBubble = b; |
| 92 | mContext = new WeakReference<>(context); |
| 93 | mStackView = new WeakReference<>(stackView); |
| 94 | mIconFactory = factory; |
Pinyao Ting | 293b83d | 2020-05-06 17:10:56 -0700 | [diff] [blame] | 95 | mSkipInflation = skipInflation; |
Mady Mellor | 3df7ab0 | 2019-12-09 15:07:10 -0800 | [diff] [blame] | 96 | mCallback = c; |
| 97 | } |
| 98 | |
| 99 | @Override |
| 100 | protected BubbleViewInfo doInBackground(Void... voids) { |
Pinyao Ting | 293b83d | 2020-05-06 17:10:56 -0700 | [diff] [blame] | 101 | return BubbleViewInfo.populate(mContext.get(), mStackView.get(), mIconFactory, mBubble, |
| 102 | mSkipInflation); |
Mady Mellor | 3df7ab0 | 2019-12-09 15:07:10 -0800 | [diff] [blame] | 103 | } |
| 104 | |
| 105 | @Override |
| 106 | protected void onPostExecute(BubbleViewInfo viewInfo) { |
| 107 | if (viewInfo != null) { |
| 108 | mBubble.setViewInfo(viewInfo); |
| 109 | if (mCallback != null && !isCancelled()) { |
| 110 | mCallback.onBubbleViewsReady(mBubble); |
| 111 | } |
| 112 | } |
| 113 | } |
| 114 | |
Mady Mellor | df898fd | 2020-01-09 09:26:36 -0800 | [diff] [blame] | 115 | /** |
| 116 | * Info necessary to render a bubble. |
| 117 | */ |
Mady Mellor | 3df7ab0 | 2019-12-09 15:07:10 -0800 | [diff] [blame] | 118 | static class BubbleViewInfo { |
| 119 | BadgedImageView imageView; |
| 120 | BubbleExpandedView expandedView; |
| 121 | ShortcutInfo shortcutInfo; |
| 122 | String appName; |
| 123 | Bitmap badgedBubbleImage; |
Joshua Tsuji | 6855cab | 2020-04-16 01:05:39 -0400 | [diff] [blame] | 124 | Drawable badgedAppIcon; |
Mady Mellor | 3df7ab0 | 2019-12-09 15:07:10 -0800 | [diff] [blame] | 125 | int dotColor; |
| 126 | Path dotPath; |
Mady Mellor | df898fd | 2020-01-09 09:26:36 -0800 | [diff] [blame] | 127 | Bubble.FlyoutMessage flyoutMessage; |
Mady Mellor | 3df7ab0 | 2019-12-09 15:07:10 -0800 | [diff] [blame] | 128 | |
| 129 | @Nullable |
| 130 | static BubbleViewInfo populate(Context c, BubbleStackView stackView, |
Pinyao Ting | 293b83d | 2020-05-06 17:10:56 -0700 | [diff] [blame] | 131 | BubbleIconFactory iconFactory, Bubble b, boolean skipInflation) { |
| 132 | final NotificationEntry entry = b.getEntry(); |
| 133 | if (entry == null) { |
| 134 | // populate from ShortcutInfo when NotificationEntry is not available |
| 135 | final ShortcutInfo s = b.getShortcutInfo(); |
| 136 | return populate(c, stackView, iconFactory, skipInflation || b.isInflated(), |
| 137 | s.getPackage(), s.getUserHandle(), s, null); |
| 138 | } |
| 139 | final StatusBarNotification sbn = entry.getSbn(); |
| 140 | final String bubbleShortcutId = entry.getBubbleMetadata().getShortcutId(); |
| 141 | final ShortcutInfo si = bubbleShortcutId == null |
| 142 | ? null : entry.getRanking().getShortcutInfo(); |
| 143 | return populate( |
| 144 | c, stackView, iconFactory, skipInflation || b.isInflated(), |
| 145 | sbn.getPackageName(), sbn.getUser(), si, entry); |
| 146 | } |
| 147 | |
| 148 | private static BubbleViewInfo populate( |
| 149 | @NonNull final Context c, |
| 150 | @NonNull final BubbleStackView stackView, |
| 151 | @NonNull final BubbleIconFactory iconFactory, |
| 152 | final boolean isInflated, |
| 153 | @NonNull final String packageName, |
| 154 | @NonNull final UserHandle user, |
| 155 | @Nullable final ShortcutInfo shortcutInfo, |
| 156 | @Nullable final NotificationEntry entry) { |
Mady Mellor | 3df7ab0 | 2019-12-09 15:07:10 -0800 | [diff] [blame] | 157 | BubbleViewInfo info = new BubbleViewInfo(); |
| 158 | |
| 159 | // View inflation: only should do this once per bubble |
Pinyao Ting | 293b83d | 2020-05-06 17:10:56 -0700 | [diff] [blame] | 160 | if (!isInflated) { |
Mady Mellor | 3df7ab0 | 2019-12-09 15:07:10 -0800 | [diff] [blame] | 161 | LayoutInflater inflater = LayoutInflater.from(c); |
| 162 | info.imageView = (BadgedImageView) inflater.inflate( |
| 163 | R.layout.bubble_view, stackView, false /* attachToRoot */); |
| 164 | |
| 165 | info.expandedView = (BubbleExpandedView) inflater.inflate( |
| 166 | R.layout.bubble_expanded_view, stackView, false /* attachToRoot */); |
| 167 | info.expandedView.setStackView(stackView); |
| 168 | } |
| 169 | |
Pinyao Ting | 293b83d | 2020-05-06 17:10:56 -0700 | [diff] [blame] | 170 | if (shortcutInfo != null) { |
| 171 | info.shortcutInfo = shortcutInfo; |
Mady Mellor | 3df7ab0 | 2019-12-09 15:07:10 -0800 | [diff] [blame] | 172 | } |
| 173 | |
| 174 | // App name & app icon |
| 175 | PackageManager pm = c.getPackageManager(); |
| 176 | ApplicationInfo appInfo; |
| 177 | Drawable badgedIcon; |
Mady Mellor | 2dae34b | 2020-01-07 13:52:58 -0800 | [diff] [blame] | 178 | Drawable appIcon; |
Mady Mellor | 3df7ab0 | 2019-12-09 15:07:10 -0800 | [diff] [blame] | 179 | try { |
| 180 | appInfo = pm.getApplicationInfo( |
| 181 | packageName, |
| 182 | PackageManager.MATCH_UNINSTALLED_PACKAGES |
| 183 | | PackageManager.MATCH_DISABLED_COMPONENTS |
| 184 | | PackageManager.MATCH_DIRECT_BOOT_UNAWARE |
| 185 | | PackageManager.MATCH_DIRECT_BOOT_AWARE); |
| 186 | if (appInfo != null) { |
| 187 | info.appName = String.valueOf(pm.getApplicationLabel(appInfo)); |
| 188 | } |
Mady Mellor | 2dae34b | 2020-01-07 13:52:58 -0800 | [diff] [blame] | 189 | appIcon = pm.getApplicationIcon(packageName); |
Pinyao Ting | 293b83d | 2020-05-06 17:10:56 -0700 | [diff] [blame] | 190 | badgedIcon = pm.getUserBadgedIcon(appIcon, user); |
Mady Mellor | 3df7ab0 | 2019-12-09 15:07:10 -0800 | [diff] [blame] | 191 | } catch (PackageManager.NameNotFoundException exception) { |
| 192 | // If we can't find package... don't think we should show the bubble. |
| 193 | Log.w(TAG, "Unable to find package: " + packageName); |
| 194 | return null; |
| 195 | } |
| 196 | |
| 197 | // Badged bubble image |
Mady Mellor | 2ac2d3a | 2020-01-08 17:18:54 -0800 | [diff] [blame] | 198 | Drawable bubbleDrawable = iconFactory.getBubbleDrawable(c, info.shortcutInfo, |
Pinyao Ting | 293b83d | 2020-05-06 17:10:56 -0700 | [diff] [blame] | 199 | entry == null ? null : entry.getBubbleMetadata()); |
Mady Mellor | 2dae34b | 2020-01-07 13:52:58 -0800 | [diff] [blame] | 200 | if (bubbleDrawable == null) { |
| 201 | // Default to app icon |
| 202 | bubbleDrawable = appIcon; |
| 203 | } |
| 204 | |
Mady Mellor | 3df7ab0 | 2019-12-09 15:07:10 -0800 | [diff] [blame] | 205 | BitmapInfo badgeBitmapInfo = iconFactory.getBadgeBitmap(badgedIcon); |
Joshua Tsuji | 6855cab | 2020-04-16 01:05:39 -0400 | [diff] [blame] | 206 | info.badgedAppIcon = badgedIcon; |
Mady Mellor | 3df7ab0 | 2019-12-09 15:07:10 -0800 | [diff] [blame] | 207 | info.badgedBubbleImage = iconFactory.getBubbleBitmap(bubbleDrawable, |
| 208 | badgeBitmapInfo).icon; |
| 209 | |
| 210 | // Dot color & placement |
| 211 | Path iconPath = PathParser.createPathFromPathData( |
| 212 | c.getResources().getString(com.android.internal.R.string.config_icon_mask)); |
| 213 | Matrix matrix = new Matrix(); |
| 214 | float scale = iconFactory.getNormalizer().getScale(bubbleDrawable, |
| 215 | null /* outBounds */, null /* path */, null /* outMaskShape */); |
| 216 | float radius = DEFAULT_PATH_SIZE / 2f; |
| 217 | matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */, |
| 218 | radius /* pivot y */); |
| 219 | iconPath.transform(matrix); |
| 220 | info.dotPath = iconPath; |
| 221 | info.dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color, |
| 222 | Color.WHITE, WHITE_SCRIM_ALPHA); |
Mady Mellor | df898fd | 2020-01-09 09:26:36 -0800 | [diff] [blame] | 223 | |
| 224 | // Flyout |
Pinyao Ting | 293b83d | 2020-05-06 17:10:56 -0700 | [diff] [blame] | 225 | if (entry != null) { |
| 226 | info.flyoutMessage = extractFlyoutMessage(c, entry); |
| 227 | } |
Mady Mellor | 3df7ab0 | 2019-12-09 15:07:10 -0800 | [diff] [blame] | 228 | return info; |
| 229 | } |
| 230 | } |
Mady Mellor | df898fd | 2020-01-09 09:26:36 -0800 | [diff] [blame] | 231 | |
| 232 | |
| 233 | /** |
| 234 | * Returns our best guess for the most relevant text summary of the latest update to this |
| 235 | * notification, based on its type. Returns null if there should not be an update message. |
| 236 | */ |
| 237 | @NonNull |
| 238 | static Bubble.FlyoutMessage extractFlyoutMessage(Context context, |
| 239 | NotificationEntry entry) { |
| 240 | final Notification underlyingNotif = entry.getSbn().getNotification(); |
| 241 | final Class<? extends Notification.Style> style = underlyingNotif.getNotificationStyle(); |
| 242 | |
| 243 | Bubble.FlyoutMessage bubbleMessage = new Bubble.FlyoutMessage(); |
| 244 | bubbleMessage.isGroupChat = underlyingNotif.extras.getBoolean( |
| 245 | Notification.EXTRA_IS_GROUP_CONVERSATION); |
| 246 | try { |
| 247 | if (Notification.BigTextStyle.class.equals(style)) { |
| 248 | // Return the big text, it is big so probably important. If it's not there use the |
| 249 | // normal text. |
| 250 | CharSequence bigText = |
| 251 | underlyingNotif.extras.getCharSequence(Notification.EXTRA_BIG_TEXT); |
| 252 | bubbleMessage.message = !TextUtils.isEmpty(bigText) |
| 253 | ? bigText |
| 254 | : underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT); |
| 255 | return bubbleMessage; |
| 256 | } else if (Notification.MessagingStyle.class.equals(style)) { |
| 257 | final List<Notification.MessagingStyle.Message> messages = |
| 258 | Notification.MessagingStyle.Message.getMessagesFromBundleArray( |
| 259 | (Parcelable[]) underlyingNotif.extras.get( |
| 260 | Notification.EXTRA_MESSAGES)); |
| 261 | |
| 262 | final Notification.MessagingStyle.Message latestMessage = |
| 263 | Notification.MessagingStyle.findLatestIncomingMessage(messages); |
| 264 | if (latestMessage != null) { |
| 265 | bubbleMessage.message = latestMessage.getText(); |
| 266 | Person sender = latestMessage.getSenderPerson(); |
| 267 | bubbleMessage.senderName = sender != null |
| 268 | ? sender.getName() |
| 269 | : null; |
Lyn Han | e566b36 | 2020-03-13 17:32:19 -0700 | [diff] [blame] | 270 | |
| 271 | bubbleMessage.senderAvatar = null; |
| 272 | if (sender != null && sender.getIcon() != null) { |
| 273 | if (sender.getIcon().getType() == Icon.TYPE_URI |
| 274 | || sender.getIcon().getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP) { |
| 275 | context.grantUriPermission(context.getPackageName(), |
| 276 | sender.getIcon().getUri(), |
| 277 | Intent.FLAG_GRANT_READ_URI_PERMISSION); |
| 278 | } |
| 279 | bubbleMessage.senderAvatar = sender.getIcon().loadDrawable(context); |
| 280 | } |
Mady Mellor | df898fd | 2020-01-09 09:26:36 -0800 | [diff] [blame] | 281 | return bubbleMessage; |
| 282 | } |
| 283 | } else if (Notification.InboxStyle.class.equals(style)) { |
| 284 | CharSequence[] lines = |
| 285 | underlyingNotif.extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES); |
| 286 | |
| 287 | // Return the last line since it should be the most recent. |
| 288 | if (lines != null && lines.length > 0) { |
| 289 | bubbleMessage.message = lines[lines.length - 1]; |
| 290 | return bubbleMessage; |
| 291 | } |
| 292 | } else if (Notification.MediaStyle.class.equals(style)) { |
| 293 | // Return nothing, media updates aren't typically useful as a text update. |
| 294 | return bubbleMessage; |
| 295 | } else { |
| 296 | // Default to text extra. |
| 297 | bubbleMessage.message = |
| 298 | underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT); |
| 299 | return bubbleMessage; |
| 300 | } |
| 301 | } catch (ClassCastException | NullPointerException | ArrayIndexOutOfBoundsException e) { |
| 302 | // No use crashing, we'll just return null and the caller will assume there's no update |
| 303 | // message. |
| 304 | e.printStackTrace(); |
| 305 | } |
| 306 | |
| 307 | return bubbleMessage; |
| 308 | } |
Mady Mellor | 3df7ab0 | 2019-12-09 15:07:10 -0800 | [diff] [blame] | 309 | } |