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