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.Rect; 29 import android.graphics.RectF; 30 import android.graphics.drawable.BitmapDrawable; 31 import android.graphics.drawable.Drawable; 32 import android.os.AsyncTask; 33 import android.os.Handler; 34 import android.os.Process; 35 import android.os.UserHandle; 36 import android.util.ArrayMap; 37 import android.util.Log; 38 import android.util.Size; 39 40 import androidx.annotation.NonNull; 41 42 import com.android.launcher3.DeviceProfile; 43 import com.android.launcher3.LauncherAppState; 44 import com.android.launcher3.R; 45 import com.android.launcher3.Utilities; 46 import com.android.launcher3.icons.BitmapRenderer; 47 import com.android.launcher3.icons.FastBitmapDrawable; 48 import com.android.launcher3.icons.LauncherIcons; 49 import com.android.launcher3.icons.ShadowGenerator; 50 import com.android.launcher3.icons.cache.HandlerRunnable; 51 import com.android.launcher3.model.WidgetItem; 52 import com.android.launcher3.pm.ShortcutConfigActivityInfo; 53 import com.android.launcher3.util.Executors; 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 /** Utility class to load widget previews */ 61 public class DatabaseWidgetPreviewLoader { 62 63 private static final String TAG = "WidgetPreviewLoader"; 64 65 private final Context mContext; 66 private final float mPreviewBoxCornerRadius; 67 68 private final UserHandle mMyUser = Process.myUserHandle(); 69 private final ArrayMap<UserHandle, Bitmap> mUserBadges = new ArrayMap<>(); 70 DatabaseWidgetPreviewLoader(Context context)71 public DatabaseWidgetPreviewLoader(Context context) { 72 mContext = context; 73 float previewCornerRadius = RoundedCornerEnforcement.computeEnforcedRadius(context); 74 mPreviewBoxCornerRadius = previewCornerRadius > 0 75 ? previewCornerRadius 76 : mContext.getResources().getDimension(R.dimen.widget_preview_corner_radius); 77 } 78 79 /** 80 * Generates the widget preview on {@link AsyncTask#THREAD_POOL_EXECUTOR}. Must be 81 * called on UI thread. 82 * 83 * @return a request id which can be used to cancel the request. 84 */ 85 @NonNull loadPreview( @onNull WidgetItem item, @NonNull Size previewSize, @NonNull Consumer<Bitmap> callback)86 public HandlerRunnable loadPreview( 87 @NonNull WidgetItem item, 88 @NonNull Size previewSize, 89 @NonNull Consumer<Bitmap> callback) { 90 Handler handler = Executors.UI_HELPER_EXECUTOR.getHandler(); 91 HandlerRunnable<Bitmap> request = new HandlerRunnable<>(handler, 92 () -> generatePreview(item, previewSize.getWidth(), previewSize.getHeight()), 93 MAIN_EXECUTOR, 94 callback); 95 Utilities.postAsyncCallback(handler, request); 96 return request; 97 } 98 99 /** 100 * Returns a generated preview for a widget and if the preview should be saved in persistent 101 * storage. 102 */ generatePreview(WidgetItem item, int previewWidth, int previewHeight)103 private Bitmap generatePreview(WidgetItem item, int previewWidth, int previewHeight) { 104 if (item.widgetInfo != null) { 105 return generateWidgetPreview(item.widgetInfo, previewWidth, null); 106 } else { 107 return generateShortcutPreview(item.activityInfo, previewWidth, previewHeight); 108 } 109 } 110 111 /** 112 * Returns a drawable that can be used as a badge for the user or null. 113 */ 114 // @UiThread getBadgeForUser(UserHandle user, int badgeSize)115 public Drawable getBadgeForUser(UserHandle user, int badgeSize) { 116 if (mMyUser.equals(user)) { 117 return null; 118 } 119 120 Bitmap badgeBitmap = getUserBadge(user, badgeSize); 121 FastBitmapDrawable d = new FastBitmapDrawable(badgeBitmap); 122 d.setFilterBitmap(true); 123 d.setBounds(0, 0, badgeBitmap.getWidth(), badgeBitmap.getHeight()); 124 return d; 125 } 126 getUserBadge(UserHandle user, int badgeSize)127 private Bitmap getUserBadge(UserHandle user, int badgeSize) { 128 synchronized (mUserBadges) { 129 Bitmap badgeBitmap = mUserBadges.get(user); 130 if (badgeBitmap != null) { 131 return badgeBitmap; 132 } 133 134 final Resources res = mContext.getResources(); 135 badgeBitmap = Bitmap.createBitmap(badgeSize, badgeSize, Bitmap.Config.ARGB_8888); 136 137 Drawable drawable = mContext.getPackageManager().getUserBadgedDrawableForDensity( 138 new BitmapDrawable(res, badgeBitmap), user, 139 new Rect(0, 0, badgeSize, badgeSize), 140 0); 141 if (drawable instanceof BitmapDrawable) { 142 badgeBitmap = ((BitmapDrawable) drawable).getBitmap(); 143 } else { 144 badgeBitmap.eraseColor(Color.TRANSPARENT); 145 Canvas c = new Canvas(badgeBitmap); 146 drawable.setBounds(0, 0, badgeSize, badgeSize); 147 drawable.draw(c); 148 c.setBitmap(null); 149 } 150 151 mUserBadges.put(user, badgeBitmap); 152 return badgeBitmap; 153 } 154 } 155 156 157 /** 158 * Generates the widget preview from either the {@link WidgetManagerHelper} or cache 159 * and add badge at the bottom right corner. 160 * 161 * @param info information about the widget 162 * @param maxPreviewWidth width of the preview on either workspace or tray 163 * @param preScaledWidthOut return the width of the returned bitmap 164 */ generateWidgetPreview(LauncherAppWidgetProviderInfo info, int maxPreviewWidth, int[] preScaledWidthOut)165 public Bitmap generateWidgetPreview(LauncherAppWidgetProviderInfo info, 166 int maxPreviewWidth, int[] preScaledWidthOut) { 167 // Load the preview image if possible 168 if (maxPreviewWidth < 0) maxPreviewWidth = Integer.MAX_VALUE; 169 170 Drawable drawable = null; 171 if (info.previewImage != 0) { 172 try { 173 drawable = info.loadPreviewImage(mContext, 0); 174 } catch (OutOfMemoryError e) { 175 Log.w(TAG, "Error loading widget preview for: " + info.provider, e); 176 // During OutOfMemoryError, the previous heap stack is not affected. Catching 177 // an OOM error here should be safe & not affect other parts of launcher. 178 drawable = null; 179 } 180 if (drawable != null) { 181 drawable = mutateOnMainThread(drawable); 182 } else { 183 Log.w(TAG, "Can't load widget preview drawable 0x" 184 + Integer.toHexString(info.previewImage) 185 + " for provider: " 186 + info.provider); 187 } 188 } 189 190 final boolean widgetPreviewExists = (drawable != null); 191 final int spanX = info.spanX; 192 final int spanY = info.spanY; 193 194 int previewWidth; 195 int previewHeight; 196 197 DeviceProfile dp = ActivityContext.lookupContext(mContext).getDeviceProfile(); 198 199 if (widgetPreviewExists && drawable.getIntrinsicWidth() > 0 200 && drawable.getIntrinsicHeight() > 0) { 201 previewWidth = drawable.getIntrinsicWidth(); 202 previewHeight = drawable.getIntrinsicHeight(); 203 } else { 204 Size widgetSize = WidgetSizes.getWidgetPaddedSizePx(mContext, info.provider, dp, spanX, 205 spanY); 206 previewWidth = widgetSize.getWidth(); 207 previewHeight = widgetSize.getHeight(); 208 } 209 210 if (preScaledWidthOut != null) { 211 preScaledWidthOut[0] = previewWidth; 212 } 213 // Scale to fit width only - let the widget preview be clipped in the 214 // vertical dimension 215 final float scale = previewWidth > maxPreviewWidth 216 ? (maxPreviewWidth / (float) (previewWidth)) : 1f; 217 if (scale != 1f) { 218 previewWidth = Math.max((int) (scale * previewWidth), 1); 219 previewHeight = Math.max((int) (scale * previewHeight), 1); 220 } 221 222 final int previewWidthF = previewWidth; 223 final int previewHeightF = previewHeight; 224 final Drawable drawableF = drawable; 225 226 return BitmapRenderer.createHardwareBitmap(previewWidth, previewHeight, c -> { 227 // Draw the scaled preview into the final bitmap 228 if (widgetPreviewExists) { 229 drawableF.setBounds(0, 0, previewWidthF, previewHeightF); 230 drawableF.draw(c); 231 } else { 232 RectF boxRect; 233 234 // Draw horizontal and vertical lines to represent individual columns. 235 final Paint p = new Paint(Paint.ANTI_ALIAS_FLAG); 236 237 if (Utilities.ATLEAST_S) { 238 boxRect = new RectF(/* left= */ 0, /* top= */ 0, /* right= */ 239 previewWidthF, /* bottom= */ previewHeightF); 240 241 p.setStyle(Paint.Style.FILL); 242 p.setColor(Color.WHITE); 243 float roundedCorner = mContext.getResources().getDimension( 244 android.R.dimen.system_app_widget_background_radius); 245 c.drawRoundRect(boxRect, roundedCorner, roundedCorner, p); 246 } else { 247 boxRect = drawBoxWithShadow(c, previewWidthF, previewHeightF); 248 } 249 250 p.setStyle(Paint.Style.STROKE); 251 p.setStrokeWidth(mContext.getResources() 252 .getDimension(R.dimen.widget_preview_cell_divider_width)); 253 p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR)); 254 255 float t = boxRect.left; 256 float tileSize = boxRect.width() / spanX; 257 for (int i = 1; i < spanX; i++) { 258 t += tileSize; 259 c.drawLine(t, 0, t, previewHeightF, p); 260 } 261 262 t = boxRect.top; 263 tileSize = boxRect.height() / spanY; 264 for (int i = 1; i < spanY; i++) { 265 t += tileSize; 266 c.drawLine(0, t, previewWidthF, t, p); 267 } 268 269 // Draw icon in the center. 270 try { 271 Drawable icon = LauncherAppState.getInstance(mContext).getIconCache() 272 .getFullResIcon(info.provider.getPackageName(), info.icon); 273 if (icon != null) { 274 int appIconSize = dp.iconSizePx; 275 int iconSize = (int) Math.min(appIconSize * scale, 276 Math.min(boxRect.width(), boxRect.height())); 277 278 icon = mutateOnMainThread(icon); 279 int hoffset = (previewWidthF - iconSize) / 2; 280 int yoffset = (previewHeightF - iconSize) / 2; 281 icon.setBounds(hoffset, yoffset, hoffset + iconSize, yoffset + iconSize); 282 icon.draw(c); 283 } 284 } catch (Resources.NotFoundException e) { 285 } 286 } 287 }); 288 } 289 290 private RectF drawBoxWithShadow(Canvas c, int width, int height) { 291 Resources res = mContext.getResources(); 292 293 ShadowGenerator.Builder builder = new ShadowGenerator.Builder(Color.WHITE); 294 builder.shadowBlur = res.getDimension(R.dimen.widget_preview_shadow_blur); 295 builder.radius = mPreviewBoxCornerRadius; 296 builder.keyShadowDistance = res.getDimension(R.dimen.widget_preview_key_shadow_distance); 297 298 builder.bounds.set(builder.shadowBlur, builder.shadowBlur, 299 width - builder.shadowBlur, 300 height - builder.shadowBlur - builder.keyShadowDistance); 301 builder.drawShadow(c); 302 return builder.bounds; 303 } 304 305 private Bitmap generateShortcutPreview( 306 ShortcutConfigActivityInfo info, int maxWidth, int maxHeight) { 307 int iconSize = ActivityContext.lookupContext(mContext).getDeviceProfile().allAppsIconSizePx; 308 int padding = mContext.getResources() 309 .getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding); 310 311 int size = iconSize + 2 * padding; 312 if (maxHeight < size || maxWidth < size) { 313 throw new RuntimeException("Max size is too small for preview"); 314 } 315 return BitmapRenderer.createHardwareBitmap(size, size, c -> { 316 drawBoxWithShadow(c, size, size); 317 318 LauncherIcons li = LauncherIcons.obtain(mContext); 319 Drawable icon = li.createBadgedIconBitmap( 320 mutateOnMainThread(info.getFullResIcon( 321 LauncherAppState.getInstance(mContext).getIconCache())), 322 Process.myUserHandle(), 0).newIcon(mContext); 323 li.recycle(); 324 325 icon.setBounds(padding, padding, padding + iconSize, padding + iconSize); 326 icon.draw(c); 327 }); 328 } 329 330 private Drawable mutateOnMainThread(final Drawable drawable) { 331 try { 332 return MAIN_EXECUTOR.submit(drawable::mutate).get(); 333 } catch (InterruptedException e) { 334 Thread.currentThread().interrupt(); 335 throw new RuntimeException(e); 336 } catch (ExecutionException e) { 337 throw new RuntimeException(e); 338 } 339 } 340 } 341