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