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<BubbleController> mController; 74 private WeakReference<BubbleStackView> mStackView; 75 private WeakReference<BubbleBarLayerView> mLayerView; 76 private BubbleIconFactory mIconFactory; 77 private boolean mSkipInflation; 78 private Callback mCallback; 79 private Executor mMainExecutor; 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, BubbleController controller, @Nullable BubbleStackView stackView, @Nullable BubbleBarLayerView layerView, BubbleIconFactory factory, boolean skipInflation, Callback c, Executor mainExecutor)85 BubbleViewInfoTask(Bubble b, 86 Context context, 87 BubbleController controller, 88 @Nullable BubbleStackView stackView, 89 @Nullable BubbleBarLayerView layerView, 90 BubbleIconFactory factory, 91 boolean skipInflation, 92 Callback c, 93 Executor mainExecutor) { 94 mBubble = b; 95 mContext = new WeakReference<>(context); 96 mController = new WeakReference<>(controller); 97 mStackView = new WeakReference<>(stackView); 98 mLayerView = new WeakReference<>(layerView); 99 mIconFactory = factory; 100 mSkipInflation = skipInflation; 101 mCallback = c; 102 mMainExecutor = mainExecutor; 103 } 104 105 @Override doInBackground(Void... voids)106 protected BubbleViewInfo doInBackground(Void... voids) { 107 if (!verifyState()) { 108 // If we're in an inconsistent state, then switched modes and should just bail now. 109 return null; 110 } 111 if (mLayerView.get() != null) { 112 return BubbleViewInfo.populateForBubbleBar(mContext.get(), mController.get(), 113 mLayerView.get(), mIconFactory, mBubble, mSkipInflation); 114 } else { 115 return BubbleViewInfo.populate(mContext.get(), mController.get(), mStackView.get(), 116 mIconFactory, mBubble, mSkipInflation); 117 } 118 } 119 120 @Override onPostExecute(BubbleViewInfo viewInfo)121 protected void onPostExecute(BubbleViewInfo viewInfo) { 122 if (isCancelled() || viewInfo == null) { 123 return; 124 } 125 126 mMainExecutor.execute(() -> { 127 if (!verifyState()) { 128 return; 129 } 130 mBubble.setViewInfo(viewInfo); 131 if (mCallback != null) { 132 mCallback.onBubbleViewsReady(mBubble); 133 } 134 }); 135 } 136 verifyState()137 private boolean verifyState() { 138 if (mController.get().isShowingAsBubbleBar()) { 139 return mLayerView.get() != null; 140 } else { 141 return mStackView.get() != null; 142 } 143 } 144 145 /** 146 * Info necessary to render a bubble. 147 */ 148 @VisibleForTesting 149 public static class BubbleViewInfo { 150 // TODO(b/273312602): for foldables it might make sense to populate all of the views 151 152 // Always populated 153 ShortcutInfo shortcutInfo; 154 String appName; 155 Bitmap rawBadgeBitmap; 156 157 // Only populated when showing in taskbar 158 BubbleBarExpandedView bubbleBarExpandedView; 159 160 // These are only populated when not showing in taskbar 161 BadgedImageView imageView; 162 BubbleExpandedView expandedView; 163 int dotColor; 164 Path dotPath; 165 Bubble.FlyoutMessage flyoutMessage; 166 Bitmap bubbleBitmap; 167 Bitmap badgeBitmap; 168 169 @Nullable populateForBubbleBar(Context c, BubbleController controller, BubbleBarLayerView layerView, BubbleIconFactory iconFactory, Bubble b, boolean skipInflation)170 public static BubbleViewInfo populateForBubbleBar(Context c, BubbleController controller, 171 BubbleBarLayerView layerView, BubbleIconFactory iconFactory, Bubble b, 172 boolean skipInflation) { 173 BubbleViewInfo info = new BubbleViewInfo(); 174 175 if (!skipInflation && !b.isInflated()) { 176 LayoutInflater inflater = LayoutInflater.from(c); 177 info.bubbleBarExpandedView = (BubbleBarExpandedView) inflater.inflate( 178 R.layout.bubble_bar_expanded_view, layerView, false /* attachToRoot */); 179 info.bubbleBarExpandedView.initialize(controller, false /* isOverflow */); 180 } 181 182 if (!populateCommonInfo(info, c, b, iconFactory)) { 183 // if we failed to update common fields return null 184 return null; 185 } 186 187 return info; 188 } 189 190 @VisibleForTesting 191 @Nullable populate(Context c, BubbleController controller, BubbleStackView stackView, BubbleIconFactory iconFactory, Bubble b, boolean skipInflation)192 public static BubbleViewInfo populate(Context c, BubbleController controller, 193 BubbleStackView stackView, BubbleIconFactory iconFactory, Bubble b, 194 boolean skipInflation) { 195 BubbleViewInfo info = new BubbleViewInfo(); 196 197 // View inflation: only should do this once per bubble 198 if (!skipInflation && !b.isInflated()) { 199 LayoutInflater inflater = LayoutInflater.from(c); 200 info.imageView = (BadgedImageView) inflater.inflate( 201 R.layout.bubble_view, stackView, false /* attachToRoot */); 202 info.imageView.initialize(controller.getPositioner()); 203 204 info.expandedView = (BubbleExpandedView) inflater.inflate( 205 R.layout.bubble_expanded_view, stackView, false /* attachToRoot */); 206 info.expandedView.initialize(controller, stackView, false /* isOverflow */); 207 } 208 209 if (!populateCommonInfo(info, c, b, iconFactory)) { 210 // if we failed to update common fields return null 211 return null; 212 } 213 214 // Flyout 215 info.flyoutMessage = b.getFlyoutMessage(); 216 if (info.flyoutMessage != null) { 217 info.flyoutMessage.senderAvatar = 218 loadSenderAvatar(c, info.flyoutMessage.senderIcon); 219 } 220 return info; 221 } 222 } 223 224 /** 225 * Modifies the given {@code info} object and populates common fields in it. 226 * 227 * <p>This method returns {@code true} if the update was successful and {@code false} otherwise. 228 * Callers should assume that the info object is unusable if the update was unsuccessful. 229 */ populateCommonInfo( BubbleViewInfo info, Context c, Bubble b, BubbleIconFactory iconFactory)230 private static boolean populateCommonInfo( 231 BubbleViewInfo info, Context c, Bubble b, BubbleIconFactory iconFactory) { 232 if (b.getShortcutInfo() != null) { 233 info.shortcutInfo = b.getShortcutInfo(); 234 } 235 236 // App name & app icon 237 PackageManager pm = BubbleController.getPackageManagerForUser(c, 238 b.getUser().getIdentifier()); 239 ApplicationInfo appInfo; 240 Drawable badgedIcon; 241 Drawable appIcon; 242 try { 243 appInfo = pm.getApplicationInfo( 244 b.getPackageName(), 245 PackageManager.MATCH_UNINSTALLED_PACKAGES 246 | PackageManager.MATCH_DISABLED_COMPONENTS 247 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE 248 | PackageManager.MATCH_DIRECT_BOOT_AWARE); 249 if (appInfo != null) { 250 info.appName = String.valueOf(pm.getApplicationLabel(appInfo)); 251 } 252 appIcon = pm.getApplicationIcon(b.getPackageName()); 253 badgedIcon = pm.getUserBadgedIcon(appIcon, b.getUser()); 254 } catch (PackageManager.NameNotFoundException exception) { 255 // If we can't find package... don't think we should show the bubble. 256 Log.w(TAG, "Unable to find package: " + b.getPackageName()); 257 return false; 258 } 259 260 Drawable bubbleDrawable = null; 261 try { 262 // Badged bubble image 263 bubbleDrawable = iconFactory.getBubbleDrawable(c, info.shortcutInfo, 264 b.getIcon()); 265 } catch (Exception e) { 266 // If we can't create the icon we'll default to the app icon 267 Log.w(TAG, "Exception creating icon for the bubble: " + b.getKey()); 268 } 269 270 if (bubbleDrawable == null) { 271 // Default to app icon 272 bubbleDrawable = appIcon; 273 } 274 275 BitmapInfo badgeBitmapInfo = iconFactory.getBadgeBitmap(badgedIcon, 276 b.isImportantConversation()); 277 info.badgeBitmap = badgeBitmapInfo.icon; 278 // Raw badge bitmap never includes the important conversation ring 279 info.rawBadgeBitmap = b.isImportantConversation() 280 ? iconFactory.getBadgeBitmap(badgedIcon, false).icon 281 : badgeBitmapInfo.icon; 282 283 float[] bubbleBitmapScale = new float[1]; 284 info.bubbleBitmap = iconFactory.getBubbleBitmap(bubbleDrawable, bubbleBitmapScale); 285 286 // Dot color & placement 287 Path iconPath = PathParser.createPathFromPathData( 288 c.getResources().getString(com.android.internal.R.string.config_icon_mask)); 289 Matrix matrix = new Matrix(); 290 float scale = bubbleBitmapScale[0]; 291 float radius = DEFAULT_PATH_SIZE / 2f; 292 matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */, 293 radius /* pivot y */); 294 iconPath.transform(matrix); 295 info.dotPath = iconPath; 296 info.dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color, 297 Color.WHITE, WHITE_SCRIM_ALPHA); 298 return true; 299 } 300 301 @Nullable loadSenderAvatar(@onNull final Context context, @Nullable final Icon icon)302 static Drawable loadSenderAvatar(@NonNull final Context context, @Nullable final Icon icon) { 303 Objects.requireNonNull(context); 304 if (icon == null) return null; 305 try { 306 if (icon.getType() == Icon.TYPE_URI 307 || icon.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP) { 308 context.grantUriPermission(context.getPackageName(), 309 icon.getUri(), Intent.FLAG_GRANT_READ_URI_PERMISSION); 310 } 311 return icon.loadDrawable(context); 312 } catch (Exception e) { 313 Log.w(TAG, "loadSenderAvatar failed: " + e.getMessage()); 314 return null; 315 } 316 } 317 } 318