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 24 import android.annotation.NonNull; 25 import android.app.Notification; 26 import android.app.Person; 27 import android.content.Context; 28 import android.content.Intent; 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; 37 import android.graphics.drawable.Icon; 38 import android.os.AsyncTask; 39 import android.os.Parcelable; 40 import android.text.TextUtils; 41 import android.util.Log; 42 import android.util.PathParser; 43 import android.view.LayoutInflater; 44 45 import androidx.annotation.Nullable; 46 47 import com.android.internal.graphics.ColorUtils; 48 import com.android.launcher3.icons.BitmapInfo; 49 import com.android.systemui.R; 50 import com.android.systemui.statusbar.notification.collection.NotificationEntry; 51 52 import java.lang.ref.WeakReference; 53 import java.util.List; 54 import java.util.Objects; 55 56 /** 57 * Simple task to inflate views & load necessary info to display a bubble. 58 */ 59 public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask.BubbleViewInfo> { 60 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleViewInfoTask" : TAG_BUBBLES; 61 62 63 /** 64 * Callback to find out when the bubble has been inflated & necessary data loaded. 65 */ 66 public interface Callback { 67 /** 68 * Called when data has been loaded for the bubble. 69 */ onBubbleViewsReady(Bubble bubble)70 void onBubbleViewsReady(Bubble bubble); 71 } 72 73 private Bubble mBubble; 74 private WeakReference<Context> mContext; 75 private WeakReference<BubbleStackView> mStackView; 76 private BubbleIconFactory mIconFactory; 77 private boolean mSkipInflation; 78 private Callback mCallback; 79 80 /** 81 * Creates a task to load information for the provided {@link Bubble}. Once all info 82 * is loaded, {@link Callback} is notified. 83 */ BubbleViewInfoTask(Bubble b, Context context, BubbleStackView stackView, BubbleIconFactory factory, boolean skipInflation, Callback c)84 BubbleViewInfoTask(Bubble b, 85 Context context, 86 BubbleStackView stackView, 87 BubbleIconFactory factory, 88 boolean skipInflation, 89 Callback c) { 90 mBubble = b; 91 mContext = new WeakReference<>(context); 92 mStackView = new WeakReference<>(stackView); 93 mIconFactory = factory; 94 mSkipInflation = skipInflation; 95 mCallback = c; 96 } 97 98 @Override doInBackground(Void... voids)99 protected BubbleViewInfo doInBackground(Void... voids) { 100 return BubbleViewInfo.populate(mContext.get(), mStackView.get(), mIconFactory, mBubble, 101 mSkipInflation); 102 } 103 104 @Override onPostExecute(BubbleViewInfo viewInfo)105 protected void onPostExecute(BubbleViewInfo viewInfo) { 106 if (viewInfo != null) { 107 mBubble.setViewInfo(viewInfo); 108 if (mCallback != null && !isCancelled()) { 109 mCallback.onBubbleViewsReady(mBubble); 110 } 111 } 112 } 113 114 /** 115 * Info necessary to render a bubble. 116 */ 117 static class BubbleViewInfo { 118 BadgedImageView imageView; 119 BubbleExpandedView expandedView; 120 ShortcutInfo shortcutInfo; 121 String appName; 122 Bitmap badgedBubbleImage; 123 Drawable badgedAppIcon; 124 int dotColor; 125 Path dotPath; 126 Bubble.FlyoutMessage flyoutMessage; 127 128 @Nullable populate(Context c, BubbleStackView stackView, BubbleIconFactory iconFactory, Bubble b, boolean skipInflation)129 static BubbleViewInfo populate(Context c, BubbleStackView stackView, 130 BubbleIconFactory iconFactory, Bubble b, boolean skipInflation) { 131 BubbleViewInfo info = new BubbleViewInfo(); 132 133 // View inflation: only should do this once per bubble 134 if (!skipInflation && !b.isInflated()) { 135 LayoutInflater inflater = LayoutInflater.from(c); 136 info.imageView = (BadgedImageView) inflater.inflate( 137 R.layout.bubble_view, stackView, false /* attachToRoot */); 138 139 info.expandedView = (BubbleExpandedView) inflater.inflate( 140 R.layout.bubble_expanded_view, stackView, false /* attachToRoot */); 141 info.expandedView.setStackView(stackView); 142 } 143 144 if (b.getShortcutInfo() != null) { 145 info.shortcutInfo = b.getShortcutInfo(); 146 } 147 148 // App name & app icon 149 PackageManager pm = c.getPackageManager(); 150 ApplicationInfo appInfo; 151 Drawable badgedIcon; 152 Drawable appIcon; 153 try { 154 appInfo = pm.getApplicationInfo( 155 b.getPackageName(), 156 PackageManager.MATCH_UNINSTALLED_PACKAGES 157 | PackageManager.MATCH_DISABLED_COMPONENTS 158 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE 159 | PackageManager.MATCH_DIRECT_BOOT_AWARE); 160 if (appInfo != null) { 161 info.appName = String.valueOf(pm.getApplicationLabel(appInfo)); 162 } 163 appIcon = pm.getApplicationIcon(b.getPackageName()); 164 badgedIcon = pm.getUserBadgedIcon(appIcon, b.getUser()); 165 } catch (PackageManager.NameNotFoundException exception) { 166 // If we can't find package... don't think we should show the bubble. 167 Log.w(TAG, "Unable to find package: " + b.getPackageName()); 168 return null; 169 } 170 171 // Badged bubble image 172 Drawable bubbleDrawable = iconFactory.getBubbleDrawable(c, info.shortcutInfo, 173 b.getIcon()); 174 if (bubbleDrawable == null) { 175 // Default to app icon 176 bubbleDrawable = appIcon; 177 } 178 179 BitmapInfo badgeBitmapInfo = iconFactory.getBadgeBitmap(badgedIcon, 180 b.isImportantConversation()); 181 info.badgedAppIcon = badgedIcon; 182 info.badgedBubbleImage = iconFactory.getBubbleBitmap(bubbleDrawable, 183 badgeBitmapInfo).icon; 184 185 // Dot color & placement 186 Path iconPath = PathParser.createPathFromPathData( 187 c.getResources().getString(com.android.internal.R.string.config_icon_mask)); 188 Matrix matrix = new Matrix(); 189 float scale = iconFactory.getNormalizer().getScale(bubbleDrawable, 190 null /* outBounds */, null /* path */, null /* outMaskShape */); 191 float radius = DEFAULT_PATH_SIZE / 2f; 192 matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */, 193 radius /* pivot y */); 194 iconPath.transform(matrix); 195 info.dotPath = iconPath; 196 info.dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color, 197 Color.WHITE, WHITE_SCRIM_ALPHA); 198 199 // Flyout 200 info.flyoutMessage = b.getFlyoutMessage(); 201 if (info.flyoutMessage != null) { 202 info.flyoutMessage.senderAvatar = 203 loadSenderAvatar(c, info.flyoutMessage.senderIcon); 204 } 205 return info; 206 } 207 } 208 209 210 /** 211 * Returns our best guess for the most relevant text summary of the latest update to this 212 * notification, based on its type. Returns null if there should not be an update message. 213 */ 214 @NonNull extractFlyoutMessage(NotificationEntry entry)215 static Bubble.FlyoutMessage extractFlyoutMessage(NotificationEntry entry) { 216 Objects.requireNonNull(entry); 217 final Notification underlyingNotif = entry.getSbn().getNotification(); 218 final Class<? extends Notification.Style> style = underlyingNotif.getNotificationStyle(); 219 220 Bubble.FlyoutMessage bubbleMessage = new Bubble.FlyoutMessage(); 221 bubbleMessage.isGroupChat = underlyingNotif.extras.getBoolean( 222 Notification.EXTRA_IS_GROUP_CONVERSATION); 223 try { 224 if (Notification.BigTextStyle.class.equals(style)) { 225 // Return the big text, it is big so probably important. If it's not there use the 226 // normal text. 227 CharSequence bigText = 228 underlyingNotif.extras.getCharSequence(Notification.EXTRA_BIG_TEXT); 229 bubbleMessage.message = !TextUtils.isEmpty(bigText) 230 ? bigText 231 : underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT); 232 return bubbleMessage; 233 } else if (Notification.MessagingStyle.class.equals(style)) { 234 final List<Notification.MessagingStyle.Message> messages = 235 Notification.MessagingStyle.Message.getMessagesFromBundleArray( 236 (Parcelable[]) underlyingNotif.extras.get( 237 Notification.EXTRA_MESSAGES)); 238 239 final Notification.MessagingStyle.Message latestMessage = 240 Notification.MessagingStyle.findLatestIncomingMessage(messages); 241 if (latestMessage != null) { 242 bubbleMessage.message = latestMessage.getText(); 243 Person sender = latestMessage.getSenderPerson(); 244 bubbleMessage.senderName = sender != null ? sender.getName() : null; 245 bubbleMessage.senderAvatar = null; 246 bubbleMessage.senderIcon = sender != null ? sender.getIcon() : null; 247 return bubbleMessage; 248 } 249 } else if (Notification.InboxStyle.class.equals(style)) { 250 CharSequence[] lines = 251 underlyingNotif.extras.getCharSequenceArray(Notification.EXTRA_TEXT_LINES); 252 253 // Return the last line since it should be the most recent. 254 if (lines != null && lines.length > 0) { 255 bubbleMessage.message = lines[lines.length - 1]; 256 return bubbleMessage; 257 } 258 } else if (Notification.MediaStyle.class.equals(style)) { 259 // Return nothing, media updates aren't typically useful as a text update. 260 return bubbleMessage; 261 } else { 262 // Default to text extra. 263 bubbleMessage.message = 264 underlyingNotif.extras.getCharSequence(Notification.EXTRA_TEXT); 265 return bubbleMessage; 266 } 267 } catch (ClassCastException | NullPointerException | ArrayIndexOutOfBoundsException e) { 268 // No use crashing, we'll just return null and the caller will assume there's no update 269 // message. 270 e.printStackTrace(); 271 } 272 273 return bubbleMessage; 274 } 275 276 @Nullable loadSenderAvatar(@onNull final Context context, @Nullable final Icon icon)277 static Drawable loadSenderAvatar(@NonNull final Context context, @Nullable final Icon icon) { 278 Objects.requireNonNull(context); 279 if (icon == null) return null; 280 if (icon.getType() == Icon.TYPE_URI || icon.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP) { 281 context.grantUriPermission(context.getPackageName(), 282 icon.getUri(), Intent.FLAG_GRANT_READ_URI_PERMISSION); 283 } 284 return icon.loadDrawable(context); 285 } 286 } 287