• 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.getWidgetPaddedSizePx(mContext, info.provider, dp, spanX,
150                     spanY);
151             previewWidth = widgetSize.getWidth();
152             previewHeight = widgetSize.getHeight();
153         }
154 
155         if (preScaledWidthOut != null) {
156             preScaledWidthOut[0] = previewWidth;
157         }
158         // Scale to fit width only - let the widget preview be clipped in the
159         // vertical dimension
160         final float scale = previewWidth > maxPreviewWidth
161                 ? (maxPreviewWidth / (float) (previewWidth)) : 1f;
162         if (scale != 1f) {
163             previewWidth = Math.max((int) (scale * previewWidth), 1);
164             previewHeight = Math.max((int) (scale * previewHeight), 1);
165         }
166 
167         final int previewWidthF = previewWidth;
168         final int previewHeightF = previewHeight;
169         final Drawable drawableF = drawable;
170 
171         return BitmapRenderer.createHardwareBitmap(previewWidth, previewHeight, c -> {
172             // Draw the scaled preview into the final bitmap
173             if (widgetPreviewExists) {
174                 drawableF.setBounds(0, 0, previewWidthF, previewHeightF);
175                 drawableF.draw(c);
176             } else {
177                 RectF boxRect;
178 
179                 // Draw horizontal and vertical lines to represent individual columns.
180                 final Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
181 
182                 if (Utilities.ATLEAST_S) {
183                     boxRect = new RectF(/* left= */ 0, /* top= */ 0, /* right= */
184                             previewWidthF, /* bottom= */ previewHeightF);
185 
186                     p.setStyle(Paint.Style.FILL);
187                     p.setColor(Color.WHITE);
188                     float roundedCorner = mContext.getResources().getDimension(
189                             android.R.dimen.system_app_widget_background_radius);
190                     c.drawRoundRect(boxRect, roundedCorner, roundedCorner, p);
191                 } else {
192                     boxRect = drawBoxWithShadow(c, previewWidthF, previewHeightF);
193                 }
194 
195                 p.setStyle(Paint.Style.STROKE);
196                 p.setStrokeWidth(mContext.getResources()
197                         .getDimension(R.dimen.widget_preview_cell_divider_width));
198                 p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
199 
200                 float t = boxRect.left;
201                 float tileSize = boxRect.width() / spanX;
202                 for (int i = 1; i < spanX; i++) {
203                     t += tileSize;
204                     c.drawLine(t, 0, t, previewHeightF, p);
205                 }
206 
207                 t = boxRect.top;
208                 tileSize = boxRect.height() / spanY;
209                 for (int i = 1; i < spanY; i++) {
210                     t += tileSize;
211                     c.drawLine(0, t, previewWidthF, t, p);
212                 }
213 
214                 // Draw icon in the center.
215                 try {
216                     Drawable icon = LauncherAppState.getInstance(mContext).getIconCache()
217                             .getFullResIcon(info.provider.getPackageName(), info.icon);
218                     if (icon != null) {
219                         int appIconSize = dp.iconSizePx;
220                         int iconSize = (int) Math.min(appIconSize * scale,
221                                 Math.min(boxRect.width(), boxRect.height()));
222 
223                         icon = mutateOnMainThread(icon);
224                         int hoffset = (previewWidthF - iconSize) / 2;
225                         int yoffset = (previewHeightF - iconSize) / 2;
226                         icon.setBounds(hoffset, yoffset, hoffset + iconSize, yoffset + iconSize);
227                         icon.draw(c);
228                     }
229                 } catch (Resources.NotFoundException e) {
230                 }
231             }
232         });
233     }
234 
235     private RectF drawBoxWithShadow(Canvas c, int width, int height) {
236         Resources res = mContext.getResources();
237 
238         ShadowGenerator.Builder builder = new ShadowGenerator.Builder(Color.WHITE);
239         builder.shadowBlur = res.getDimension(R.dimen.widget_preview_shadow_blur);
240         builder.radius = mPreviewBoxCornerRadius;
241         builder.keyShadowDistance = res.getDimension(R.dimen.widget_preview_key_shadow_distance);
242 
243         builder.bounds.set(builder.shadowBlur, builder.shadowBlur,
244                 width - builder.shadowBlur,
245                 height - builder.shadowBlur - builder.keyShadowDistance);
246         builder.drawShadow(c);
247         return builder.bounds;
248     }
249 
250     private Bitmap generateShortcutPreview(
251             ShortcutConfigActivityInfo info, int maxWidth, int maxHeight) {
252         int iconSize = ActivityContext.lookupContext(mContext).getDeviceProfile().allAppsIconSizePx;
253         int padding = mContext.getResources()
254                 .getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding);
255 
256         int size = iconSize + 2 * padding;
257         if (maxHeight < size || maxWidth < size) {
258             throw new RuntimeException("Max size is too small for preview");
259         }
260         return BitmapRenderer.createHardwareBitmap(size, size, c -> {
261             drawBoxWithShadow(c, size, size);
262 
263             LauncherIcons li = LauncherIcons.obtain(mContext);
264             Drawable icon = li.createBadgedIconBitmap(
265                     mutateOnMainThread(info.getFullResIcon(
266                             LauncherAppState.getInstance(mContext).getIconCache())))
267                     .newIcon(mContext);
268             li.recycle();
269 
270             icon.setBounds(padding, padding, padding + iconSize, padding + iconSize);
271             icon.draw(c);
272         });
273     }
274 
275     private Drawable mutateOnMainThread(final Drawable drawable) {
276         try {
277             return MAIN_EXECUTOR.submit(drawable::mutate).get();
278         } catch (InterruptedException e) {
279             Thread.currentThread().interrupt();
280             throw new RuntimeException(e);
281         } catch (ExecutionException e) {
282             throw new RuntimeException(e);
283         }
284     }
285 }
286