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