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 boolean mSkipInflation; 75 private Callback mCallback; 76 private Executor mMainExecutor; 77 78 /** 79 * Creates a task to load information for the provided {@link Bubble}. Once all info 80 * is loaded, {@link Callback} is notified. 81 */ BubbleViewInfoTask(Bubble b, Context context, BubbleController controller, BubbleStackView stackView, BubbleIconFactory factory, boolean skipInflation, Callback c, Executor mainExecutor)82 BubbleViewInfoTask(Bubble b, 83 Context context, 84 BubbleController controller, 85 BubbleStackView stackView, 86 BubbleIconFactory factory, 87 boolean skipInflation, 88 Callback c, 89 Executor mainExecutor) { 90 mBubble = b; 91 mContext = new WeakReference<>(context); 92 mController = new WeakReference<>(controller); 93 mStackView = new WeakReference<>(stackView); 94 mIconFactory = factory; 95 mSkipInflation = skipInflation; 96 mCallback = c; 97 mMainExecutor = mainExecutor; 98 } 99 100 @Override doInBackground(Void... voids)101 protected BubbleViewInfo doInBackground(Void... voids) { 102 return BubbleViewInfo.populate(mContext.get(), mController.get(), mStackView.get(), 103 mIconFactory, mBubble, mSkipInflation); 104 } 105 106 @Override onPostExecute(BubbleViewInfo viewInfo)107 protected void onPostExecute(BubbleViewInfo viewInfo) { 108 if (isCancelled() || viewInfo == null) { 109 return; 110 } 111 mMainExecutor.execute(() -> { 112 mBubble.setViewInfo(viewInfo); 113 if (mCallback != null) { 114 mCallback.onBubbleViewsReady(mBubble); 115 } 116 }); 117 } 118 119 /** 120 * Info necessary to render a bubble. 121 */ 122 @VisibleForTesting 123 public static class BubbleViewInfo { 124 BadgedImageView imageView; 125 BubbleExpandedView expandedView; 126 ShortcutInfo shortcutInfo; 127 String appName; 128 Bitmap bubbleBitmap; 129 Bitmap badgeBitmap; 130 int dotColor; 131 Path dotPath; 132 Bubble.FlyoutMessage flyoutMessage; 133 134 @VisibleForTesting 135 @Nullable populate(Context c, BubbleController controller, BubbleStackView stackView, BubbleIconFactory iconFactory, Bubble b, boolean skipInflation)136 public static BubbleViewInfo populate(Context c, BubbleController controller, 137 BubbleStackView stackView, BubbleIconFactory iconFactory, Bubble b, 138 boolean skipInflation) { 139 BubbleViewInfo info = new BubbleViewInfo(); 140 141 // View inflation: only should do this once per bubble 142 if (!skipInflation && !b.isInflated()) { 143 LayoutInflater inflater = LayoutInflater.from(c); 144 info.imageView = (BadgedImageView) inflater.inflate( 145 R.layout.bubble_view, stackView, false /* attachToRoot */); 146 info.imageView.initialize(controller.getPositioner()); 147 148 info.expandedView = (BubbleExpandedView) inflater.inflate( 149 R.layout.bubble_expanded_view, stackView, false /* attachToRoot */); 150 info.expandedView.initialize(controller, stackView, false /* isOverflow */); 151 } 152 153 if (b.getShortcutInfo() != null) { 154 info.shortcutInfo = b.getShortcutInfo(); 155 } 156 157 // App name & app icon 158 PackageManager pm = BubbleController.getPackageManagerForUser(c, 159 b.getUser().getIdentifier()); 160 ApplicationInfo appInfo; 161 Drawable badgedIcon; 162 Drawable appIcon; 163 try { 164 appInfo = pm.getApplicationInfo( 165 b.getPackageName(), 166 PackageManager.MATCH_UNINSTALLED_PACKAGES 167 | PackageManager.MATCH_DISABLED_COMPONENTS 168 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE 169 | PackageManager.MATCH_DIRECT_BOOT_AWARE); 170 if (appInfo != null) { 171 info.appName = String.valueOf(pm.getApplicationLabel(appInfo)); 172 } 173 appIcon = pm.getApplicationIcon(b.getPackageName()); 174 badgedIcon = pm.getUserBadgedIcon(appIcon, b.getUser()); 175 } catch (PackageManager.NameNotFoundException exception) { 176 // If we can't find package... don't think we should show the bubble. 177 Log.w(TAG, "Unable to find package: " + b.getPackageName()); 178 return null; 179 } 180 181 // Badged bubble image 182 Drawable bubbleDrawable = iconFactory.getBubbleDrawable(c, info.shortcutInfo, 183 b.getIcon()); 184 if (bubbleDrawable == null) { 185 // Default to app icon 186 bubbleDrawable = appIcon; 187 } 188 189 BitmapInfo badgeBitmapInfo = iconFactory.getBadgeBitmap(badgedIcon, 190 b.isImportantConversation()); 191 info.badgeBitmap = badgeBitmapInfo.icon; 192 info.bubbleBitmap = iconFactory.createBadgedIconBitmap(bubbleDrawable, 193 null /* user */, 194 true /* shrinkNonAdaptiveIcons */).icon; 195 196 // Dot color & placement 197 Path iconPath = PathParser.createPathFromPathData( 198 c.getResources().getString(com.android.internal.R.string.config_icon_mask)); 199 Matrix matrix = new Matrix(); 200 float scale = iconFactory.getNormalizer().getScale(bubbleDrawable, 201 null /* outBounds */, null /* path */, null /* outMaskShape */); 202 float radius = DEFAULT_PATH_SIZE / 2f; 203 matrix.setScale(scale /* x scale */, scale /* y scale */, radius /* pivot x */, 204 radius /* pivot y */); 205 iconPath.transform(matrix); 206 info.dotPath = iconPath; 207 info.dotColor = ColorUtils.blendARGB(badgeBitmapInfo.color, 208 Color.WHITE, WHITE_SCRIM_ALPHA); 209 210 // Flyout 211 info.flyoutMessage = b.getFlyoutMessage(); 212 if (info.flyoutMessage != null) { 213 info.flyoutMessage.senderAvatar = 214 loadSenderAvatar(c, info.flyoutMessage.senderIcon); 215 } 216 return info; 217 } 218 } 219 220 @Nullable loadSenderAvatar(@onNull final Context context, @Nullable final Icon icon)221 static Drawable loadSenderAvatar(@NonNull final Context context, @Nullable final Icon icon) { 222 Objects.requireNonNull(context); 223 if (icon == null) return null; 224 try { 225 if (icon.getType() == Icon.TYPE_URI 226 || icon.getType() == Icon.TYPE_URI_ADAPTIVE_BITMAP) { 227 context.grantUriPermission(context.getPackageName(), 228 icon.getUri(), Intent.FLAG_GRANT_READ_URI_PERMISSION); 229 } 230 return icon.loadDrawable(context); 231 } catch (Exception e) { 232 Log.w(TAG, "loadSenderAvatar failed: " + e.getMessage()); 233 return null; 234 } 235 } 236 } 237