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