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