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