1 /* 2 * Copyright (C) 2020 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.wm.shell.bubbles; 18 19 import static com.android.wm.shell.bubbles.BadgedImageView.DEFAULT_PATH_SIZE; 20 import static com.android.wm.shell.bubbles.BadgedImageView.WHITE_SCRIM_ALPHA; 21 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; 22 import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; 23 24 import android.annotation.NonNull; 25 import android.annotation.Nullable; 26 import android.content.Context; 27 import android.content.Intent; 28 import android.content.pm.ApplicationInfo; 29 import android.content.pm.PackageManager; 30 import android.content.pm.ShortcutInfo; 31 import android.graphics.Bitmap; 32 import android.graphics.Color; 33 import android.graphics.Matrix; 34 import android.graphics.Path; 35 import android.graphics.drawable.Drawable; 36 import android.graphics.drawable.Icon; 37 import android.os.AsyncTask; 38 import android.util.Log; 39 import android.util.PathParser; 40 import android.view.LayoutInflater; 41 42 import com.android.internal.annotations.VisibleForTesting; 43 import com.android.internal.graphics.ColorUtils; 44 import com.android.launcher3.icons.BitmapInfo; 45 import com.android.launcher3.icons.BubbleIconFactory; 46 import com.android.wm.shell.R; 47 import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView; 48 import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; 49 50 import java.lang.ref.WeakReference; 51 import java.util.Objects; 52 import java.util.concurrent.Executor; 53 54 /** 55 * Simple task to inflate views & load necessary info to display a bubble. 56 */ 57 public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask.BubbleViewInfo> { 58 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleViewInfoTask" : TAG_BUBBLES; 59 60 61 /** 62 * Callback to find out when the bubble has been inflated & necessary data loaded. 63 */ 64 public interface Callback { 65 /** 66 * Called when data has been loaded for the bubble. 67 */ onBubbleViewsReady(Bubble bubble)68 void onBubbleViewsReady(Bubble bubble); 69 } 70 71 private Bubble mBubble; 72 private WeakReference<Context> mContext; 73 private WeakReference<BubbleExpandedViewManager> mExpandedViewManager; 74 private WeakReference<BubbleTaskViewFactory> mTaskViewFactory; 75 private WeakReference<BubblePositioner> mPositioner; 76 private WeakReference<BubbleStackView> mStackView; 77 private WeakReference<BubbleBarLayerView> mLayerView; 78 private BubbleIconFactory mIconFactory; 79 private boolean mSkipInflation; 80 private Callback mCallback; 81 private Executor mMainExecutor; 82 83 /** 84 * Creates a task to load information for the provided {@link Bubble}. Once all info 85 * is loaded, {@link Callback} is notified. 86 */ BubbleViewInfoTask(Bubble b, Context context, BubbleExpandedViewManager expandedViewManager, BubbleTaskViewFactory taskViewFactory, BubblePositioner positioner, @Nullable BubbleStackView stackView, @Nullable BubbleBarLayerView layerView, BubbleIconFactory factory, boolean skipInflation, Callback c, Executor mainExecutor)87 BubbleViewInfoTask(Bubble b, 88 Context context, 89 BubbleExpandedViewManager expandedViewManager, 90 BubbleTaskViewFactory taskViewFactory, 91 BubblePositioner positioner, 92 @Nullable BubbleStackView stackView, 93 @Nullable BubbleBarLayerView layerView, 94 BubbleIconFactory factory, 95 boolean skipInflation, 96 Callback c, 97 Executor mainExecutor) { 98 mBubble = b; 99 mContext = new WeakReference<>(context); 100 mExpandedViewManager = new WeakReference<>(expandedViewManager); 101 mTaskViewFactory = new WeakReference<>(taskViewFactory); 102 mPositioner = new WeakReference<>(positioner); 103 mStackView = new WeakReference<>(stackView); 104 mLayerView = new WeakReference<>(layerView); 105 mIconFactory = factory; 106 mSkipInflation = skipInflation; 107 mCallback = c; 108 mMainExecutor = mainExecutor; 109 } 110 111 @Override doInBackground(Void... voids)112 protected BubbleViewInfo doInBackground(Void... voids) { 113 if (!verifyState()) { 114 // If we're in an inconsistent state, then switched modes and should just bail now. 115 return null; 116 } 117 if (mLayerView.get() != null) { 118 return BubbleViewInfo.populateForBubbleBar(mContext.get(), mExpandedViewManager.get(), 119 mTaskViewFactory.get(), mPositioner.get(), mLayerView.get(), mIconFactory, 120 mBubble, mSkipInflation); 121 } else { 122 return BubbleViewInfo.populate(mContext.get(), mExpandedViewManager.get(), 123 mTaskViewFactory.get(), mPositioner.get(), mStackView.get(), mIconFactory, 124 mBubble, mSkipInflation); 125 } 126 } 127 128 @Override onPostExecute(BubbleViewInfo viewInfo)129 protected void onPostExecute(BubbleViewInfo viewInfo) { 130 if (isCancelled() || viewInfo == null) { 131 return; 132 } 133 134 mMainExecutor.execute(() -> { 135 if (!verifyState()) { 136 return; 137 } 138 mBubble.setViewInfo(viewInfo); 139 if (mCallback != null) { 140 mCallback.onBubbleViewsReady(mBubble); 141 } 142 }); 143 } 144 verifyState()145 private boolean verifyState() { 146 if (mExpandedViewManager.get().isShowingAsBubbleBar()) { 147 return mLayerView.get() != null; 148 } else { 149 return mStackView.get() != null; 150 } 151 } 152 153 /** 154 * Info necessary to render a bubble. 155 */ 156 @VisibleForTesting 157 public static class BubbleViewInfo { 158 // TODO(b/273312602): for foldables it might make sense to populate all of the views 159 160 // Always populated 161 ShortcutInfo shortcutInfo; 162 String appName; 163 Bitmap rawBadgeBitmap; 164 165 // Only populated when showing in taskbar 166 @Nullable BubbleBarExpandedView bubbleBarExpandedView; 167 168 // These are only populated when not showing in taskbar 169 @Nullable BadgedImageView imageView; 170 @Nullable BubbleExpandedView expandedView; 171 int dotColor; 172 Path dotPath; 173 @Nullable Bubble.FlyoutMessage flyoutMessage; 174 Bitmap bubbleBitmap; 175 Bitmap badgeBitmap; 176 177 @Nullable populateForBubbleBar(Context c, BubbleExpandedViewManager expandedViewManager, BubbleTaskViewFactory taskViewFactory, BubblePositioner positioner, BubbleBarLayerView layerView, BubbleIconFactory iconFactory, Bubble b, boolean skipInflation)178 public static BubbleViewInfo populateForBubbleBar(Context c, 179 BubbleExpandedViewManager expandedViewManager, 180 BubbleTaskViewFactory taskViewFactory, 181 BubblePositioner positioner, 182 BubbleBarLayerView layerView, 183 BubbleIconFactory iconFactory, 184 Bubble b, 185 boolean skipInflation) { 186 BubbleViewInfo info = new BubbleViewInfo(); 187 188 if (!skipInflation && !b.isInflated()) { 189 BubbleTaskView bubbleTaskView = b.getOrCreateBubbleTaskView(taskViewFactory); 190 LayoutInflater inflater = LayoutInflater.from(c); 191 info.bubbleBarExpandedView = (BubbleBarExpandedView) inflater.inflate( 192 R.layout.bubble_bar_expanded_view, layerView, false /* attachToRoot */); 193 info.bubbleBarExpandedView.initialize( 194 expandedViewManager, positioner, false /* isOverflow */, bubbleTaskView); 195 } 196 197 if (!populateCommonInfo(info, c, b, iconFactory)) { 198 // if we failed to update common fields return null 199 return null; 200 } 201 202 return info; 203 } 204 205 @VisibleForTesting 206 @Nullable populate(Context c, BubbleExpandedViewManager expandedViewManager, BubbleTaskViewFactory taskViewFactory, BubblePositioner positioner, BubbleStackView stackView, BubbleIconFactory iconFactory, Bubble b, boolean skipInflation)207 public static BubbleViewInfo populate(Context c, 208 BubbleExpandedViewManager expandedViewManager, 209 BubbleTaskViewFactory taskViewFactory, 210 BubblePositioner positioner, 211 BubbleStackView stackView, 212 BubbleIconFactory iconFactory, 213 Bubble b, 214 boolean skipInflation) { 215 BubbleViewInfo info = new BubbleViewInfo(); 216 217 // View inflation: only should do this once per bubble 218 if (!skipInflation && !b.isInflated()) { 219 LayoutInflater inflater = LayoutInflater.from(c); 220 info.imageView = (BadgedImageView) inflater.inflate( 221 R.layout.bubble_view, stackView, false /* attachToRoot */); 222 info.imageView.initialize(positioner); 223 224 BubbleTaskView bubbleTaskView = b.getOrCreateBubbleTaskView(taskViewFactory); 225 info.expandedView = (BubbleExpandedView) inflater.inflate( 226 R.layout.bubble_expanded_view, stackView, false /* attachToRoot */); 227 info.expandedView.initialize( 228 expandedViewManager, stackView, positioner, false /* isOverflow */, 229 bubbleTaskView); 230 } 231 232 if (!populateCommonInfo(info, c, b, iconFactory)) { 233 // if we failed to update common fields return null 234 return null; 235 } 236 237 // Flyout 238 info.flyoutMessage = b.getFlyoutMessage(); 239 if (info.flyoutMessage != null) { 240 info.flyoutMessage.senderAvatar = 241 loadSenderAvatar(c, info.flyoutMessage.senderIcon); 242 } 243 return info; 244 } 245 } 246 247 /** 248 * Modifies the given {@code info} object and populates common fields in it. 249 * 250 * <p>This method returns {@code true} if the update was successful and {@code false} otherwise. 251 * Callers should assume that the info object is unusable if the update was unsuccessful. 252 */ populateCommonInfo( BubbleViewInfo info, Context c, Bubble b, BubbleIconFactory iconFactory)253 private static boolean populateCommonInfo( 254 BubbleViewInfo info, Context c, Bubble b, BubbleIconFactory iconFactory) { 255 if (b.getShortcutInfo() != null) { 256 info.shortcutInfo = b.getShortcutInfo(); 257 } 258 259 // App name & app icon 260 PackageManager pm = BubbleController.getPackageManagerForUser(c, 261 b.getUser().getIdentifier()); 262 ApplicationInfo appInfo; 263 Drawable badgedIcon; 264 Drawable appIcon; 265 try { 266 appInfo = pm.getApplicationInfo( 267 b.getPackageName(), 268 PackageManager.MATCH_UNINSTALLED_PACKAGES 269 | PackageManager.MATCH_DISABLED_COMPONENTS 270 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE 271 | PackageManager.MATCH_DIRECT_BOOT_AWARE); 272 if (appInfo != null) { 273 info.appName = String.valueOf(pm.getApplicationLabel(appInfo)); 274 } 275 appIcon = pm.getApplicationIcon(b.getPackageName()); 276 badgedIcon = pm.getUserBadgedIcon(appIcon, b.getUser()); 277 } catch (PackageManager.NameNotFoundException exception) { 278 // If we can't find package... don't think we should show the bubble. 279 Log.w(TAG, "Unable to find package: " + b.getPackageName()); 280 return false; 281 } 282 283 Drawable bubbleDrawable = null; 284 try { 285 // Badged bubble image 286 bubbleDrawable = iconFactory.getBubbleDrawable(c, info.shortcutInfo, 287 b.getIcon()); 288 } catch (Exception e) { 289 // If we can't create the icon we'll default to the app icon 290 Log.w(TAG, "Exception creating icon for the bubble: " + b.getKey()); 291 } 292 293 if (bubbleDrawable == null) { 294 // Default to app icon 295 bubbleDrawable = appIcon; 296 } 297 298 BitmapInfo badgeBitmapInfo = iconFactory.getBadgeBitmap(badgedIcon, 299 b.isImportantConversation()); 300 info.badgeBitmap = badgeBitmapInfo.icon; 301 // Raw badge bitmap never includes the important conversation ring 302 info.rawBadgeBitmap = b.isImportantConversation() 303 ? iconFactory.getBadgeBitmap(badgedIcon, false).icon 304 : badgeBitmapInfo.icon; 305 306 float[] bubbleBitmapScale = new float[1]; 307 info.bubbleBitmap = iconFactory.getBubbleBitmap(bubbleDrawable, bubbleBitmapScale); 308 309 // Dot color & placement 310 Path iconPath = PathParser.createPathFromPathData( 311 c.getResources().getString(com.android.internal.R.string.config_icon_mask)); 312 Matrix matrix = new Matrix(); 313 float scale = bubbleBitmapScale[0]; 314 float radius = DEFAULT_PATH_SIZE / 2f; 315 matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */, 316 radius /* pivot y */); 317 iconPath.transform(matrix); 318 info.dotPath = iconPath; 319 info.dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color, 320 Color.WHITE, WHITE_SCRIM_ALPHA); 321 return true; 322 } 323 324 @Nullable loadSenderAvatar(@onNull final Context context, @Nullable final Icon icon)325 static Drawable loadSenderAvatar(@NonNull final Context context, @Nullable final Icon icon) { 326 Objects.requireNonNull(context); 327 if (icon == null) return null; 328 try { 329 if (icon.getType() == Icon.TYPE_URI 330 || icon.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP) { 331 context.grantUriPermission(context.getPackageName(), 332 icon.getUri(), Intent.FLAG_GRANT_READ_URI_PERMISSION); 333 } 334 return icon.loadDrawable(context); 335 } catch (Exception e) { 336 Log.w(TAG, "loadSenderAvatar failed: " + e.getMessage()); 337 return null; 338 } 339 } 340 } 341