• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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