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.content.Context; 26 import android.content.Intent; 27 import android.content.pm.ApplicationInfo; 28 import android.content.pm.PackageManager; 29 import android.content.pm.ShortcutInfo; 30 import android.graphics.Bitmap; 31 import android.graphics.Color; 32 import android.graphics.Matrix; 33 import android.graphics.Path; 34 import android.graphics.drawable.Drawable; 35 import android.graphics.drawable.Icon; 36 import android.os.AsyncTask; 37 import android.util.Log; 38 import android.util.PathParser; 39 import android.view.LayoutInflater; 40 41 import androidx.annotation.Nullable; 42 import androidx.annotation.VisibleForTesting; 43 44 import com.android.internal.graphics.ColorUtils; 45 import com.android.launcher3.icons.BitmapInfo; 46 import com.android.wm.shell.R; 47 48 import java.lang.ref.WeakReference; 49 import java.util.Objects; 50 import java.util.concurrent.Executor; 51 52 /** 53 * Simple task to inflate views & load necessary info to display a bubble. 54 */ 55 public class BubbleViewInfoTask extends AsyncTask<Void, Void, BubbleViewInfoTask.BubbleViewInfo> { 56 private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleViewInfoTask" : TAG_BUBBLES; 57 58 59 /** 60 * Callback to find out when the bubble has been inflated & necessary data loaded. 61 */ 62 public interface Callback { 63 /** 64 * Called when data has been loaded for the bubble. 65 */ onBubbleViewsReady(Bubble bubble)66 void onBubbleViewsReady(Bubble bubble); 67 } 68 69 private Bubble mBubble; 70 private WeakReference<Context> mContext; 71 private WeakReference<BubbleController> mController; 72 private WeakReference<BubbleStackView> mStackView; 73 private BubbleIconFactory mIconFactory; 74 private BubbleBadgeIconFactory mBadgeIconFactory; 75 private boolean mSkipInflation; 76 private Callback mCallback; 77 private Executor mMainExecutor; 78 79 /** 80 * Creates a task to load information for the provided {@link Bubble}. Once all info 81 * is loaded, {@link Callback} is notified. 82 */ BubbleViewInfoTask(Bubble b, Context context, BubbleController controller, BubbleStackView stackView, BubbleIconFactory factory, BubbleBadgeIconFactory badgeFactory, boolean skipInflation, Callback c, Executor mainExecutor)83 BubbleViewInfoTask(Bubble b, 84 Context context, 85 BubbleController controller, 86 BubbleStackView stackView, 87 BubbleIconFactory factory, 88 BubbleBadgeIconFactory badgeFactory, 89 boolean skipInflation, 90 Callback c, 91 Executor mainExecutor) { 92 mBubble = b; 93 mContext = new WeakReference<>(context); 94 mController = new WeakReference<>(controller); 95 mStackView = new WeakReference<>(stackView); 96 mIconFactory = factory; 97 mBadgeIconFactory = badgeFactory; 98 mSkipInflation = skipInflation; 99 mCallback = c; 100 mMainExecutor = mainExecutor; 101 } 102 103 @Override doInBackground(Void... voids)104 protected BubbleViewInfo doInBackground(Void... voids) { 105 return BubbleViewInfo.populate(mContext.get(), mController.get(), mStackView.get(), 106 mIconFactory, mBadgeIconFactory, mBubble, mSkipInflation); 107 } 108 109 @Override onPostExecute(BubbleViewInfo viewInfo)110 protected void onPostExecute(BubbleViewInfo viewInfo) { 111 if (isCancelled() || viewInfo == null) { 112 return; 113 } 114 mMainExecutor.execute(() -> { 115 mBubble.setViewInfo(viewInfo); 116 if (mCallback != null) { 117 mCallback.onBubbleViewsReady(mBubble); 118 } 119 }); 120 } 121 122 /** 123 * Info necessary to render a bubble. 124 */ 125 @VisibleForTesting 126 public static class BubbleViewInfo { 127 BadgedImageView imageView; 128 BubbleExpandedView expandedView; 129 ShortcutInfo shortcutInfo; 130 String appName; 131 Bitmap bubbleBitmap; 132 Bitmap badgeBitmap; 133 Bitmap mRawBadgeBitmap; 134 int dotColor; 135 Path dotPath; 136 Bubble.FlyoutMessage flyoutMessage; 137 138 @VisibleForTesting 139 @Nullable populate(Context c, BubbleController controller, BubbleStackView stackView, BubbleIconFactory iconFactory, BubbleBadgeIconFactory badgeIconFactory, Bubble b, boolean skipInflation)140 public static BubbleViewInfo populate(Context c, BubbleController controller, 141 BubbleStackView stackView, BubbleIconFactory iconFactory, 142 BubbleBadgeIconFactory badgeIconFactory, Bubble b, 143 boolean skipInflation) { 144 BubbleViewInfo info = new BubbleViewInfo(); 145 146 // View inflation: only should do this once per bubble 147 if (!skipInflation && !b.isInflated()) { 148 LayoutInflater inflater = LayoutInflater.from(c); 149 info.imageView = (BadgedImageView) inflater.inflate( 150 R.layout.bubble_view, stackView, false /* attachToRoot */); 151 info.imageView.initialize(controller.getPositioner()); 152 153 info.expandedView = (BubbleExpandedView) inflater.inflate( 154 R.layout.bubble_expanded_view, stackView, false /* attachToRoot */); 155 info.expandedView.initialize(controller, stackView, false /* isOverflow */); 156 } 157 158 if (b.getShortcutInfo() != null) { 159 info.shortcutInfo = b.getShortcutInfo(); 160 } 161 162 // App name & app icon 163 PackageManager pm = BubbleController.getPackageManagerForUser(c, 164 b.getUser().getIdentifier()); 165 ApplicationInfo appInfo; 166 Drawable badgedIcon; 167 Drawable appIcon; 168 try { 169 appInfo = pm.getApplicationInfo( 170 b.getPackageName(), 171 PackageManager.MATCH_UNINSTALLED_PACKAGES 172 | PackageManager.MATCH_DISABLED_COMPONENTS 173 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE 174 | PackageManager.MATCH_DIRECT_BOOT_AWARE); 175 if (appInfo != null) { 176 info.appName = String.valueOf(pm.getApplicationLabel(appInfo)); 177 } 178 appIcon = pm.getApplicationIcon(b.getPackageName()); 179 badgedIcon = pm.getUserBadgedIcon(appIcon, b.getUser()); 180 } catch (PackageManager.NameNotFoundException exception) { 181 // If we can't find package... don't think we should show the bubble. 182 Log.w(TAG, "Unable to find package: " + b.getPackageName()); 183 return null; 184 } 185 186 // Badged bubble image 187 Drawable bubbleDrawable = iconFactory.getBubbleDrawable(c, info.shortcutInfo, 188 b.getIcon()); 189 if (bubbleDrawable == null) { 190 // Default to app icon 191 bubbleDrawable = appIcon; 192 } 193 194 BitmapInfo badgeBitmapInfo = badgeIconFactory.getBadgeBitmap(badgedIcon, 195 b.isImportantConversation()); 196 info.badgeBitmap = badgeBitmapInfo.icon; 197 // Raw badge bitmap never includes the important conversation ring 198 info.mRawBadgeBitmap = b.isImportantConversation() 199 ? badgeIconFactory.getBadgeBitmap(badgedIcon, false).icon 200 : badgeBitmapInfo.icon; 201 202 float[] bubbleBitmapScale = new float[1]; 203 info.bubbleBitmap = iconFactory.createIconBitmap(bubbleDrawable, bubbleBitmapScale); 204 205 // Dot color & placement 206 Path iconPath = PathParser.createPathFromPathData( 207 c.getResources().getString(com.android.internal.R.string.config_icon_mask)); 208 Matrix matrix = new Matrix(); 209 float scale = bubbleBitmapScale[0]; 210 float radius = DEFAULT_PATH_SIZE / 2f; 211 matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */, 212 radius /* pivot y */); 213 iconPath.transform(matrix); 214 info.dotPath = iconPath; 215 info.dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color, 216 Color.WHITE, WHITE_SCRIM_ALPHA); 217 218 // Flyout 219 info.flyoutMessage = b.getFlyoutMessage(); 220 if (info.flyoutMessage != null) { 221 info.flyoutMessage.senderAvatar = 222 loadSenderAvatar(c, info.flyoutMessage.senderIcon); 223 } 224 return info; 225 } 226 } 227 228 @Nullable loadSenderAvatar(@onNull final Context context, @Nullable final Icon icon)229 static Drawable loadSenderAvatar(@NonNull final Context context, @Nullable final Icon icon) { 230 Objects.requireNonNull(context); 231 if (icon == null) return null; 232 try { 233 if (icon.getType() == Icon.TYPE_URI 234 || icon.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP) { 235 context.grantUriPermission(context.getPackageName(), 236 icon.getUri(), Intent.FLAG_GRANT_READ_URI_PERMISSION); 237 } 238 return icon.loadDrawable(context); 239 } catch (Exception e) { 240 Log.w(TAG, "loadSenderAvatar failed: " + e.getMessage()); 241 return null; 242 } 243 } 244 } 245