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