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