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