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