Files
Lawnchair/src/com/android/launcher3/widget/DatabaseWidgetPreviewLoader.java
T
Shamali P 4cb6ff3838 Pass device profile as constructor arg to DatabaseWidgetPreviewLoader
This is so, we can use it with compose, and let caller pass the needed
dependencies. May also convert to dagger in
 followup

Bug: 408283627
Flag: EXEMPT BUGFIX
Test: N/A
Change-Id: I9b04011ec2592df3bd02bd74ec4889eb05928e7f
2025-05-16 11:47:18 -07:00

318 lines
13 KiB
Java

/*
* Copyright (C) 2021 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.widget;
import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.widget.LauncherAppWidgetProviderInfo.fromProviderInfo;
import android.appwidget.AppWidgetProviderInfo;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.PorterDuff;
import android.graphics.PorterDuffXfermode;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.os.Handler;
import android.util.Log;
import android.util.Size;
import android.widget.RemoteViews;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import androidx.core.os.BuildCompat;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Flags;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.R;
import com.android.launcher3.Utilities;
import com.android.launcher3.icons.BitmapRenderer;
import com.android.launcher3.icons.LauncherIcons;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.pm.ShortcutConfigActivityInfo;
import com.android.launcher3.util.CancellableTask;
import com.android.launcher3.util.Executors;
import com.android.launcher3.util.LooperExecutor;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.widget.util.WidgetSizes;
import java.util.concurrent.ExecutionException;
import java.util.function.Consumer;
/**
* Utility class to generate widget previews
*
* Note that it no longer uses database, all previews are freshly generated
*/
public class DatabaseWidgetPreviewLoader {
private static final String TAG = "WidgetPreviewLoader";
private final Context mContext;
private final DeviceProfile mDeviceProfile;
public DatabaseWidgetPreviewLoader(Context context, DeviceProfile deviceProfile) {
mContext = context;
mDeviceProfile = deviceProfile;
}
/**
* Generates the widget preview on {@link Executors#UI_HELPER_EXECUTOR}.
*
* @return a request id which can be used to cancel the request.
*/
@NonNull
public CancellableTask loadPreview(
@NonNull WidgetItem item,
@NonNull Size previewSize,
@NonNull Consumer<WidgetPreviewInfo> callback) {
Handler handler = getLoaderExecutor().getHandler();
CancellableTask<WidgetPreviewInfo> request = new CancellableTask<>(
() -> generatePreviewInfoBg(item, previewSize.getWidth(), previewSize.getHeight()),
MAIN_EXECUTOR,
callback);
Utilities.postAsyncCallback(handler, request);
return request;
}
@VisibleForTesting
@NonNull
public static LooperExecutor getLoaderExecutor() {
return Executors.UI_HELPER_EXECUTOR;
}
/** Generated the preview object. This method must be called on a background thread */
@VisibleForTesting
@NonNull
public WidgetPreviewInfo generatePreviewInfoBg(
WidgetItem item, int previewWidth, int previewHeight) {
WidgetPreviewInfo result = new WidgetPreviewInfo();
AppWidgetProviderInfo widgetInfo = item.widgetInfo;
if (BuildCompat.isAtLeastV() && Flags.enableGeneratedPreviews() && widgetInfo != null
&& ((widgetInfo.generatedPreviewCategories & WIDGET_CATEGORY_HOME_SCREEN) != 0)) {
result.remoteViews = new WidgetManagerHelper(mContext)
.loadGeneratedPreview(widgetInfo, WIDGET_CATEGORY_HOME_SCREEN);
if (result.remoteViews != null) {
result.providerInfo = widgetInfo;
}
}
if (result.providerInfo == null && widgetInfo != null
&& widgetInfo.previewLayout != Resources.ID_NULL) {
result.providerInfo = fromProviderInfo(mContext, widgetInfo.clone());
// A hack to force the initial layout to be the preview layout since there is no API for
// rendering a preview layout for work profile apps yet. For non-work profile layout, a
// proper solution is to use RemoteViews(PackageName, LayoutId).
result.providerInfo.initialLayout = item.widgetInfo.previewLayout;
}
if (result.providerInfo == null) {
// fallback to bitmap preview
result.previewBitmap = generatePreview(item, previewWidth, previewHeight);
}
return result;
}
/**
* Returns a generated preview for a widget and if the preview should be saved in persistent
* storage.
*/
private Bitmap generatePreview(WidgetItem item, int previewWidth, int previewHeight) {
if (item.widgetInfo != null) {
return generateWidgetPreview(item.widgetInfo, previewWidth, null);
} else {
return generateShortcutPreview(item.activityInfo, previewWidth, previewHeight);
}
}
/**
* Generates the widget preview from either the {@link WidgetManagerHelper} or cache
* and add badge at the bottom right corner.
*
* @param info information about the widget
* @param maxPreviewWidth width of the preview on either workspace or tray
* @param preScaledWidthOut return the width of the returned bitmap
*/
public Bitmap generateWidgetPreview(LauncherAppWidgetProviderInfo info,
int maxPreviewWidth, int[] preScaledWidthOut) {
// Load the preview image if possible
if (maxPreviewWidth < 0) maxPreviewWidth = Integer.MAX_VALUE;
Drawable drawable = null;
if (info.previewImage != 0) {
try {
drawable = info.loadPreviewImage(mContext, 0);
} catch (OutOfMemoryError e) {
Log.w(TAG, "Error loading widget preview for: " + info.provider, e);
// During OutOfMemoryError, the previous heap stack is not affected. Catching
// an OOM error here should be safe & not affect other parts of launcher.
drawable = null;
}
if (drawable != null) {
drawable = mutateOnMainThread(drawable);
} else {
Log.w(TAG, "Can't load widget preview drawable 0x"
+ Integer.toHexString(info.previewImage)
+ " for provider: "
+ info.provider);
}
}
final boolean widgetPreviewExists = (drawable != null);
final int spanX = info.spanX;
final int spanY = info.spanY;
int previewWidth;
int previewHeight;
if (widgetPreviewExists && drawable.getIntrinsicWidth() > 0
&& drawable.getIntrinsicHeight() > 0) {
previewWidth = drawable.getIntrinsicWidth();
previewHeight = drawable.getIntrinsicHeight();
} else {
Size widgetSize = WidgetSizes.getWidgetSizePx(mDeviceProfile, spanX, spanY);
previewWidth = widgetSize.getWidth();
previewHeight = widgetSize.getHeight();
}
if (preScaledWidthOut != null) {
preScaledWidthOut[0] = previewWidth;
}
// Scale to fit width only - let the widget preview be clipped in the
// vertical dimension
final float scale = previewWidth > maxPreviewWidth
? (maxPreviewWidth / (float) (previewWidth)) : 1f;
if (scale != 1f) {
previewWidth = Math.max((int) (scale * previewWidth), 1);
previewHeight = Math.max((int) (scale * previewHeight), 1);
}
final int previewWidthF = previewWidth;
final int previewHeightF = previewHeight;
final Drawable drawableF = drawable;
return BitmapRenderer.createHardwareBitmap(previewWidth, previewHeight, c -> {
// Draw the scaled preview into the final bitmap
if (widgetPreviewExists) {
drawableF.setBounds(0, 0, previewWidthF, previewHeightF);
drawableF.draw(c);
} else {
RectF boxRect;
// Draw horizontal and vertical lines to represent individual columns.
final Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
boxRect = new RectF(/* left= */ 0, /* top= */ 0, /* right= */
previewWidthF, /* bottom= */ previewHeightF);
p.setStyle(Paint.Style.FILL);
p.setColor(Color.WHITE);
float roundedCorner = mContext.getResources().getDimension(
android.R.dimen.system_app_widget_background_radius);
c.drawRoundRect(boxRect, roundedCorner, roundedCorner, p);
p.setStyle(Paint.Style.STROKE);
p.setStrokeWidth(mContext.getResources()
.getDimension(R.dimen.widget_preview_cell_divider_width));
p.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.CLEAR));
float t = boxRect.left;
float tileSize = boxRect.width() / spanX;
for (int i = 1; i < spanX; i++) {
t += tileSize;
c.drawLine(t, 0, t, previewHeightF, p);
}
t = boxRect.top;
tileSize = boxRect.height() / spanY;
for (int i = 1; i < spanY; i++) {
t += tileSize;
c.drawLine(0, t, previewWidthF, t, p);
}
// Draw icon in the center.
try {
Drawable icon = info.getFullResIcon(
LauncherAppState.getInstance(mContext).getIconCache());
if (icon != null) {
int appIconSize = mDeviceProfile.iconSizePx;
int iconSize = (int) Math.min(appIconSize * scale,
Math.min(boxRect.width(), boxRect.height()));
icon = mutateOnMainThread(icon);
int hoffset = (previewWidthF - iconSize) / 2;
int yoffset = (previewHeightF - iconSize) / 2;
icon.setBounds(hoffset, yoffset, hoffset + iconSize, yoffset + iconSize);
icon.draw(c);
}
} catch (Resources.NotFoundException e) {
}
}
});
}
private Bitmap generateShortcutPreview(
ShortcutConfigActivityInfo info, int maxWidth, int maxHeight) {
int iconSize = ActivityContext.lookupContext(mContext).getDeviceProfile().allAppsIconSizePx;
int padding = mContext.getResources()
.getDimensionPixelSize(R.dimen.widget_preview_shortcut_padding);
int size = iconSize + 2 * padding;
if (maxHeight < size || maxWidth < size) {
throw new RuntimeException("Max size is too small for preview");
}
return BitmapRenderer.createHardwareBitmap(size, size, c -> {
LauncherIcons li = LauncherIcons.obtain(mContext);
Drawable icon = li.createBadgedIconBitmap(
mutateOnMainThread(info.getFullResIcon(
LauncherAppState.getInstance(mContext).getIconCache())))
.newIcon(mContext);
li.recycle();
icon.setBounds(padding, padding, padding + iconSize, padding + iconSize);
icon.draw(c);
});
}
private Drawable mutateOnMainThread(final Drawable drawable) {
try {
return MAIN_EXECUTOR.submit(drawable::mutate).get();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RuntimeException(e);
} catch (ExecutionException e) {
throw new RuntimeException(e);
}
}
/**
* Simple class to hold preview information
*/
public static class WidgetPreviewInfo {
public AppWidgetProviderInfo providerInfo;
public RemoteViews remoteViews;
public Bitmap previewBitmap;
}
}