1 /*
2  * Copyright (C) 2021 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 package com.android.launcher3.widget;
17 
18 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
19 
20 import android.content.Context;
21 import android.content.res.Resources;
22 import android.graphics.Bitmap;
23 import android.graphics.Canvas;
24 import android.graphics.Color;
25 import android.graphics.Paint;
26 import android.graphics.PorterDuff;
27 import android.graphics.PorterDuffXfermode;
28 import android.graphics.Rect;
29 import android.graphics.RectF;
30 import android.graphics.drawable.BitmapDrawable;
31 import android.graphics.drawable.Drawable;
32 import android.os.AsyncTask;
33 import android.os.Handler;
34 import android.os.Process;
35 import android.os.UserHandle;
36 import android.util.ArrayMap;
37 import android.util.Log;
38 import android.util.Size;
39 
40 import androidx.annotation.NonNull;
41 
42 import com.android.launcher3.DeviceProfile;
43 import com.android.launcher3.LauncherAppState;
44 import com.android.launcher3.R;
45 import com.android.launcher3.Utilities;
46 import com.android.launcher3.icons.BitmapRenderer;
47 import com.android.launcher3.icons.FastBitmapDrawable;
48 import com.android.launcher3.icons.LauncherIcons;
49 import com.android.launcher3.icons.ShadowGenerator;
50 import com.android.launcher3.icons.cache.HandlerRunnable;
51 import com.android.launcher3.model.WidgetItem;
52 import com.android.launcher3.pm.ShortcutConfigActivityInfo;
53 import com.android.launcher3.util.Executors;
54 import com.android.launcher3.views.ActivityContext;
55 import com.android.launcher3.widget.util.WidgetSizes;
56 
57 import java.util.concurrent.ExecutionException;
58 import java.util.function.Consumer;
59 
60 /** Utility class to load widget previews */
61 public class DatabaseWidgetPreviewLoader {
62 
63     private static final String TAG = "WidgetPreviewLoader";
64 
65     private final Context mContext;
66     private final float mPreviewBoxCornerRadius;
67 
68     private final UserHandle mMyUser = Process.myUserHandle();
69     private final ArrayMap<UserHandle, Bitmap> mUserBadges = new ArrayMap<>();
70 
DatabaseWidgetPreviewLoader(Context context)71     public DatabaseWidgetPreviewLoader(Context context) {
72         mContext = context;
73         float previewCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(context);
74         mPreviewBoxCornerRadius = previewCornerRadius > 0
75                 ? previewCornerRadius
76                 : mContext.getResources().getDimension(R.dimen.widget_preview_corner_radius);
77     }
78 
79     /**
80      * Generates the widget preview on {@link AsyncTask#THREAD_POOL_EXECUTOR}. Must be
81      * called on UI thread.
82      *
83      * @return a request id which can be used to cancel the request.
84      */
85     @NonNull
loadPreview( @onNull WidgetItem item, @NonNull Size previewSize, @NonNull Consumer<Bitmap> callback)86     public HandlerRunnable loadPreview(
87             @NonNull WidgetItem item,
88             @NonNull Size previewSize,
89             @NonNull Consumer<Bitmap> callback) {
90         Handler handler = Executors.UI_HELPER_EXECUTOR.getHandler();
91         HandlerRunnable<Bitmap> request = new HandlerRunnable<>(handler,
92                 () -> generatePreview(item, previewSize.getWidth(), previewSize.getHeight()),
93                 MAIN_EXECUTOR,
94                 callback);
95         Utilities.postAsyncCallback(handler, request);
96         return request;
97     }
98 
99     /**
100      * Returns a generated preview for a widget and if the preview should be saved in persistent
101      * storage.
102      */
generatePreview(WidgetItem item, int previewWidth, int previewHeight)103     private Bitmap generatePreview(WidgetItem item, int previewWidth, int previewHeight) {
104         if (item.widgetInfo != null) {
105             return generateWidgetPreview(item.widgetInfo, previewWidth, null);
106         } else {
107             return generateShortcutPreview(item.activityInfo, previewWidth, previewHeight);
108         }
109     }
110 
111     /**
112      * Returns a drawable that can be used as a badge for the user or null.
113      */
114    // @UiThread
getBadgeForUser(UserHandle user, int badgeSize)115     public Drawable getBadgeForUser(UserHandle user, int badgeSize) {
116         if (mMyUser.equals(user)) {
117             return null;
118         }
119 
120         Bitmap badgeBitmap = getUserBadge(user, badgeSize);
121         FastBitmapDrawable d = new FastBitmapDrawable(badgeBitmap);
122         d.setFilterBitmap(true);
123         d.setBounds(0, 0, badgeBitmap.getWidth(), badgeBitmap.getHeight());
124         return d;
125     }
126 
getUserBadge(UserHandle user, int badgeSize)127     private Bitmap getUserBadge(UserHandle user, int badgeSize) {
128         synchronized (mUserBadges) {
129             Bitmap badgeBitmap = mUserBadges.get(user);
130             if (badgeBitmap != null) {
131                 return badgeBitmap;
132             }
133 
134             final Resources res = mContext.getResources();
135             badgeBitmap = Bitmap.createBitmap(badgeSize, badgeSize, Bitmap.Config.ARGB_8888);
136 
137             Drawable drawable = mContext.getPackageManager().getUserBadgedDrawableForDensity(
138                     new BitmapDrawable(res, badgeBitmap), user,
139                     new Rect(0, 0, badgeSize, badgeSize),
140                     0);
141             if (drawable instanceof BitmapDrawable) {
142                 badgeBitmap = ((BitmapDrawable) drawable).getBitmap();
143             } else {
144                 badgeBitmap.eraseColor(Color.TRANSPARENT);
145                 Canvas c = new Canvas(badgeBitmap);
146                 drawable.setBounds(0, 0, badgeSize, badgeSize);
147                 drawable.draw(c);
148                 c.setBitmap(null);
149             }
150 
151             mUserBadges.put(user, badgeBitmap);
152             return badgeBitmap;
153         }
154     }
155 
156 
157     /**
158      * Generates the widget preview from either the {@link WidgetManagerHelper} or cache
159      * and add badge at the bottom right corner.
160      *
161      * @param info                        information about the widget
162      * @param maxPreviewWidth             width of the preview on either workspace or tray
163      * @param preScaledWidthOut           return the width of the returned bitmap
164      */
generateWidgetPreview(LauncherAppWidgetProviderInfo info, int maxPreviewWidth, int[] preScaledWidthOut)165     public Bitmap generateWidgetPreview(LauncherAppWidgetProviderInfo info,
166             int maxPreviewWidth, int[] preScaledWidthOut) {
167         // Load the preview image if possible
168         if (maxPreviewWidth < 0) maxPreviewWidth = Integer.MAX_VALUE;
169 
170         Drawable drawable = null;
171         if (info.previewImage != 0) {
172             try {
173                 drawable = info.loadPreviewImage(mContext, 0);
174             } catch (OutOfMemoryError e) {
175                 Log.w(TAG, "Error loading widget preview for: " + info.provider, e);
176                 // During OutOfMemoryError, the previous heap stack is not affected. Catching
177                 // an OOM error here should be safe & not affect other parts of launcher.
178                 drawable = null;
179             }
180             if (drawable != null) {
181                 drawable = mutateOnMainThread(drawable);
182             } else {
183                 Log.w(TAG, "Can't load widget preview drawable 0x"
184                         + Integer.toHexString(info.previewImage)
185                         + " for provider: "
186                         + info.provider);
187             }
188         }
189 
190         final boolean widgetPreviewExists = (drawable != null);
191         final int spanX = info.spanX;
192         final int spanY = info.spanY;
193 
194         int previewWidth;
195         int previewHeight;
196 
197         DeviceProfile dp = ActivityContext.lookupContext(mContext).getDeviceProfile();
198 
199         if (widgetPreviewExists && drawable.getIntrinsicWidth() > 0
200                 && drawable.getIntrinsicHeight() > 0) {
201             previewWidth = drawable.getIntrinsicWidth();
202             previewHeight = drawable.getIntrinsicHeight();
203         } else {
204             Size widgetSize = WidgetSizes.getWidgetPaddedSizePx(mContext, info.provider, dp, spanX,
205                     spanY);
206             previewWidth = widgetSize.getWidth();
207             previewHeight = widgetSize.getHeight();
208         }
209 
210         if (preScaledWidthOut != null) {
211             preScaledWidthOut[0] = previewWidth;
212         }
213         // Scale to fit width only - let the widget preview be clipped in the
214         // vertical dimension
215         final float scale = previewWidth > maxPreviewWidth
216                 ? (maxPreviewWidth / (float) (previewWidth)) : 1f;
217         if (scale != 1f) {
218             previewWidth = Math.max((int) (scale * previewWidth), 1);
219             previewHeight = Math.max((int) (scale * previewHeight), 1);
220         }
221 
222         final int previewWidthF = previewWidth;
223         final int previewHeightF = previewHeight;
224         final Drawable drawableF = drawable;
225 
226         return BitmapRenderer.createHardwareBitmap(previewWidth, previewHeight, c -> {
227             // Draw the scaled preview into the final bitmap
228             if (widgetPreviewExists) {
229                 drawableF.setBounds(0, 0, previewWidthF, previewHeightF);
230                 drawableF.draw(c);
231             } else {
232                 RectF boxRect;
233 
234                 // Draw horizontal and vertical lines to represent individual columns.
235                 final Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
236 
237                 if (Utilities.ATLEAST_S) {
238                     boxRect = new RectF(/* left= */ 0, /* top= */ 0, /* right= */
239                             previewWidthF, /* bottom= */ previewHeightF);
240 
241                     p.setStyle(Paint.Style.FILL);
242                     p.setColor(Color.WHITE);
243                     float roundedCorner = mContext.getResources().getDimension(
244                             android.R.dimen.system_app_widget_background_radius);
245                     c.drawRoundRect(boxRect, roundedCorner, roundedCorner, p);
246                 } else {
247                     boxRect = drawBoxWithShadow(c, previewWidthF, previewHeightF);
248                 }
249 
250                 p.setStyle(Paint.Style.STROKE);
251                 p.setStrokeWidth(mContext.getResources()
252                         .getDimension(R.dimen.widget_preview_cell_divider_width));
253                 p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
254 
255                 float t = boxRect.left;
256                 float tileSize = boxRect.width() / spanX;
257                 for (int i = 1; i < spanX; i++) {
258                     t += tileSize;
259                     c.drawLine(t, 0, t, previewHeightF, p);
260                 }
261 
262                 t = boxRect.top;
263                 tileSize = boxRect.height() / spanY;
264                 for (int i = 1; i < spanY; i++) {
265                     t += tileSize;
266                     c.drawLine(0, t, previewWidthF, t, p);
267                 }
268 
269                 // Draw icon in the center.
270                 try {
271                     Drawable icon = LauncherAppState.getInstance(mContext).getIconCache()
272                             .getFullResIcon(info.provider.getPackageName(), info.icon);
273                     if (icon != null) {
274                         int appIconSize = dp.iconSizePx;
275                         int iconSize = (int) Math.min(appIconSize * scale,
276                                 Math.min(boxRect.width(), boxRect.height()));
277 
278                         icon = mutateOnMainThread(icon);
279                         int hoffset = (previewWidthF - iconSize) / 2;
280                         int yoffset = (previewHeightF - iconSize) / 2;
281                         icon.setBounds(hoffset, yoffset, hoffset + iconSize, yoffset + iconSize);
282                         icon.draw(c);
283                     }
284                 } catch (Resources.NotFoundException e) {
285                 }
286             }
287         });
288     }
289 
290     private RectF drawBoxWithShadow(Canvas c, int width, int height) {
291         Resources res = mContext.getResources();
292 
293         ShadowGenerator.Builder builder = new ShadowGenerator.Builder(Color.WHITE);
294         builder.shadowBlur = res.getDimension(R.dimen.widget_preview_shadow_blur);
295         builder.radius = mPreviewBoxCornerRadius;
296         builder.keyShadowDistance = res.getDimension(R.dimen.widget_preview_key_shadow_distance);
297 
298         builder.bounds.set(builder.shadowBlur, builder.shadowBlur,
299                 width - builder.shadowBlur,
300                 height - builder.shadowBlur - builder.keyShadowDistance);
301         builder.drawShadow(c);
302         return builder.bounds;
303     }
304 
305     private Bitmap generateShortcutPreview(
306             ShortcutConfigActivityInfo info, int maxWidth, int maxHeight) {
307         int iconSize = ActivityContext.lookupContext(mContext).getDeviceProfile().allAppsIconSizePx;
308         int padding = mContext.getResources()
309                 .getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding);
310 
311         int size = iconSize + 2 * padding;
312         if (maxHeight < size || maxWidth < size) {
313             throw new RuntimeException("Max size is too small for preview");
314         }
315         return BitmapRenderer.createHardwareBitmap(size, size, c -> {
316             drawBoxWithShadow(c, size, size);
317 
318             LauncherIcons li = LauncherIcons.obtain(mContext);
319             Drawable icon = li.createBadgedIconBitmap(
320                     mutateOnMainThread(info.getFullResIcon(
321                             LauncherAppState.getInstance(mContext).getIconCache())),
322                     Process.myUserHandle(), 0).newIcon(mContext);
323             li.recycle();
324 
325             icon.setBounds(padding, padding, padding + iconSize, padding + iconSize);
326             icon.draw(c);
327         });
328     }
329 
330     private Drawable mutateOnMainThread(final Drawable drawable) {
331         try {
332             return MAIN_EXECUTOR.submit(drawable::mutate).get();
333         } catch (InterruptedException e) {
334             Thread.currentThread().interrupt();
335             throw new RuntimeException(e);
336         } catch (ExecutionException e) {
337             throw new RuntimeException(e);
338         }
339     }
340 }
341