Merge "Adding support for generating a preview bitmap with an optional delay to allow widgets to be rendered" into main

This commit is contained in:
Treehugger Robot
2025-05-09 17:16:29 -07:00
committed by Android (Google) Code Review
6 changed files with 135 additions and 52 deletions
@@ -26,14 +26,18 @@ import android.content.pm.ActivityInfo
import android.content.pm.LauncherActivityInfo
import android.content.pm.LauncherApps
import android.content.pm.ShortcutInfo
import android.graphics.Bitmap
import android.graphics.Rect
import android.os.Bundle
import android.os.Flags.allowPrivateProfile
import android.os.IBinder
import android.os.UserHandle
import android.os.UserManager
import android.util.ArrayMap
import android.view.SurfaceControlViewHost
import android.widget.Toast
import android.window.RemoteTransition
import android.window.ScreenCapture
import com.android.launcher3.Flags.enablePrivateSpace
import com.android.launcher3.Flags.privateSpaceSysAppsSeparation
import com.android.launcher3.R
@@ -193,4 +197,15 @@ open class SystemApiWrapper @Inject constructor(@ApplicationContext context: Con
override fun isFileDrawable(shortcutInfo: ShortcutInfo) =
shortcutInfo.hasIconFile() || shortcutInfo.hasIconUri()
override fun captureSnapshot(host: SurfaceControlViewHost, width: Int, height: Int): Bitmap =
ScreenCapture.captureLayers(
ScreenCapture.LayerCaptureArgs.Builder(host.surfacePackage!!.surfaceControl)
.setSourceCrop(Rect(0, 0, width, height))
.setAllowProtected(true)
.setHintForSeamlessTransition(true)
.build()
)
.asBitmap()
.copy(Bitmap.Config.ARGB_8888, true)
}
@@ -42,8 +42,6 @@ public interface WorkspaceLayoutManager {
// The is the first screen. It is always present, even if its empty.
int FIRST_SCREEN_ID = 0;
// This is the second page. On two panel home it is always present, even if its empty.
int SECOND_SCREEN_ID = 1;
/**
* At bind time, we use the rank (screenId) to compute x and y for hotseat items.
@@ -15,19 +15,25 @@
*/
package com.android.launcher3.graphics;
import static com.android.launcher3.BuildConfig.IS_DEBUG_DEVICE;
import static com.android.launcher3.Flags.enableLauncherIconShapes;
import static com.android.launcher3.graphics.PreviewSurfaceRenderer.KEY_BITMAP_GENERATION_DELAY_MS;
import static com.android.launcher3.graphics.PreviewSurfaceRenderer.KEY_VIEW_HEIGHT;
import static com.android.launcher3.graphics.PreviewSurfaceRenderer.KEY_VIEW_WIDTH;
import static com.android.launcher3.graphics.PreviewSurfaceRenderer.MIN_BITMAP_GENERATION_DELAY_MS;
import static com.android.launcher3.graphics.ThemeManager.PREF_ICON_SHAPE;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR;
import static java.util.Objects.requireNonNullElse;
import static java.util.concurrent.CompletableFuture.delayedExecutor;
import static java.util.concurrent.TimeUnit.MILLISECONDS;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Binder;
import android.os.Bundle;
@@ -50,6 +56,7 @@ import com.android.launcher3.dagger.LauncherAppSingleton;
import com.android.launcher3.model.BgDataModel;
import com.android.launcher3.shapes.IconShapeModel;
import com.android.launcher3.shapes.ShapesProvider;
import com.android.launcher3.util.ApiWrapper;
import com.android.launcher3.util.ContentProviderProxy.ProxyProvider;
import com.android.launcher3.util.DaggerSingletonTracker;
import com.android.launcher3.util.Executors;
@@ -118,6 +125,7 @@ public class GridCustomizationsProxy implements ProxyProvider {
private static final String SET_SHAPE = "/shape";
private static final String METHOD_GET_PREVIEW = "get_preview";
public static final String METHOD_GET_PREVIEW_BITMAP = "get_preview_bitmap";
private static final String GET_ICON_THEMED = "/get_icon_themed";
private static final String SET_ICON_THEMED = "/set_icon_themed";
@@ -128,6 +136,7 @@ public class GridCustomizationsProxy implements ProxyProvider {
private static final String KEY_CALLBACK = "callback";
public static final String KEY_HIDE_BOTTOM_ROW = "hide_bottom_row";
public static final String KEY_GRID_NAME = "grid_name";
public static final String KEY_IMAGE = "image";
private static final int MESSAGE_ID_UPDATE_PREVIEW = 1337;
private static final int MESSAGE_ID_UPDATE_SHAPE = 2586;
@@ -309,18 +318,45 @@ public class GridCustomizationsProxy implements ProxyProvider {
@Override
public Bundle call(@NonNull String method, String arg, Bundle extras) {
if (METHOD_GET_PREVIEW.equals(method)) {
return getPreview(extras);
} else {
return null;
return switch (method) {
case METHOD_GET_PREVIEW -> getPreview(extras);
case METHOD_GET_PREVIEW_BITMAP -> getPreviewBitmap(extras);
default -> null;
};
}
private Bundle getPreviewBitmap(Bundle request) {
RunnableList lifeCycleTracker = new RunnableList();
try {
int width = request.getInt(KEY_VIEW_WIDTH);
int height = request.getInt(KEY_VIEW_HEIGHT);
long previewDelay = Math.max(request.getLong(KEY_BITMAP_GENERATION_DELAY_MS, 0),
MIN_BITMAP_GENERATION_DELAY_MS);
PreviewSurfaceRenderer renderer = new PreviewSurfaceRenderer(
mContext, lifeCycleTracker, request, Binder.getCallingPid(),
true /* skip animations */);
renderer.loadAsync().thenRunAsync(
() -> { }, delayedExecutor(previewDelay, MILLISECONDS, MAIN_EXECUTOR)).get();
Bitmap previewBitmap = ApiWrapper.INSTANCE.get(mContext)
.captureSnapshot(renderer.getHost(), width, height);
Bundle result = new Bundle();
result.putParcelable(KEY_IMAGE, previewBitmap);
return result;
} catch (Exception e) {
Log.e(TAG, "Unable to generate preview", e);
}
MAIN_EXECUTOR.execute(lifeCycleTracker::executeAllAndDestroy);
return null;
}
private synchronized Bundle getPreview(Bundle request) {
RunnableList lifeCycleTracker = new RunnableList();
try {
PreviewSurfaceRenderer renderer = new PreviewSurfaceRenderer(
mContext, lifeCycleTracker, request, Binder.getCallingPid());
mContext, lifeCycleTracker, request, Binder.getCallingPid(),
false /* skip animations */);
PreviewLifecycleObserver observer =
new PreviewLifecycleObserver(lifeCycleTracker, renderer);
@@ -333,7 +369,7 @@ public class GridCustomizationsProxy implements ProxyProvider {
renderer.getHostToken().linkToDeath(observer, 0);
Bundle result = new Bundle();
result.putParcelable(KEY_SURFACE_PACKAGE, renderer.getSurfacePackage());
result.putParcelable(KEY_SURFACE_PACKAGE, renderer.getHost().getSurfacePackage());
mActivePreviews.add(observer);
lifeCycleTracker.add(() -> mActivePreviews.remove(observer));
@@ -68,7 +68,6 @@ import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherPrefs;
import com.android.launcher3.ProxyPrefs;
import com.android.launcher3.R;
import com.android.launcher3.Workspace;
import com.android.launcher3.WorkspaceLayoutManager;
import com.android.launcher3.celllayout.CellLayoutLayoutParams;
import com.android.launcher3.celllayout.CellPosMapper;
@@ -165,7 +164,11 @@ public class LauncherPreviewRenderer extends BaseContext
emptyDbDir();
mDbDir.mkdirs();
builder.bindParserFactory(new XmlLayoutParserFactory(this, layoutXml))
.bindWidgetsFactory(c -> new LauncherWidgetHolder(c, widgetHostId));
.bindWidgetsFactory(c -> {
LauncherWidgetHolder holder = new LauncherWidgetHolder(c, widgetHostId);
holder.startListening();
return holder;
});
}
initDaggerComponent(builder);
@@ -212,14 +215,17 @@ public class LauncherPreviewRenderer extends BaseContext
public LauncherPreviewRenderer(Context context,
InvariantDeviceProfile idp,
WallpaperColors wallpaperColorsOverride,
int workspaceScreenId,
@Nullable final SparseArray<Size> launcherWidgetSpanInfo) {
this(context, idp, null, wallpaperColorsOverride, launcherWidgetSpanInfo);
this(context, idp, null, wallpaperColorsOverride, workspaceScreenId,
launcherWidgetSpanInfo);
}
public LauncherPreviewRenderer(Context context,
InvariantDeviceProfile idp,
SparseIntArray previewColorOverride,
WallpaperColors wallpaperColorsOverride,
int workspaceScreenId,
@Nullable final SparseArray<Size> launcherWidgetSpanInfo) {
super(context, Themes.getActivityThemeRes(context));
@@ -262,7 +268,6 @@ public class LauncherPreviewRenderer extends BaseContext
: (mDp.workspacePadding.right + mDp.cellLayoutPaddingPx.right),
mDp.workspacePadding.bottom + mDp.cellLayoutPaddingPx.bottom
);
mWorkspaceScreens.put(FIRST_SCREEN_ID, firstScreen);
if (mDp.isTwoPanels) {
CellLayout rightPanel = mRootView.findViewById(R.id.workspace_right);
@@ -272,7 +277,12 @@ public class LauncherPreviewRenderer extends BaseContext
mDp.workspacePadding.right + mDp.cellLayoutPaddingPx.right,
mDp.workspacePadding.bottom + mDp.cellLayoutPaddingPx.bottom
);
mWorkspaceScreens.put(Workspace.SECOND_SCREEN_ID, rightPanel);
int closestEvenPageId = workspaceScreenId - (workspaceScreenId % 2);
mWorkspaceScreens.put(closestEvenPageId, firstScreen);
mWorkspaceScreens.put(closestEvenPageId + 1, rightPanel);
} else {
mWorkspaceScreens.put(workspaceScreenId, firstScreen);
}
SparseIntArray wallpaperColorResources;
@@ -462,12 +472,15 @@ public class LauncherPreviewRenderer extends BaseContext
// Add first page QSB
if (BuildConfig.QSB_ON_FIRST_SCREEN) {
CellLayout firstScreen = mWorkspaceScreens.get(FIRST_SCREEN_ID);
View qsb = mHomeElementInflater.inflate(R.layout.qsb_preview, firstScreen, false);
// TODO: set bgHandler on qsb when it is BaseTemplateCard, which requires API changes.
CellLayoutLayoutParams lp = new CellLayoutLayoutParams(
0, 0, firstScreen.getCountX(), 1);
lp.canReorder = false;
firstScreen.addViewToCellLayout(qsb, 0, R.id.search_container_workspace, lp, true);
if (firstScreen != null) {
View qsb = mHomeElementInflater.inflate(R.layout.qsb_preview, firstScreen, false);
// TODO: set bgHandler on qsb when it is BaseTemplateCard, which requires API
// changes.
CellLayoutLayoutParams lp = new CellLayoutLayoutParams(
0, 0, firstScreen.getCountX(), 1);
lp.canReorder = false;
firstScreen.addViewToCellLayout(qsb, 0, R.id.search_container_workspace, lp, true);
}
}
measureView(mRootView, mDp.widthPx, mDp.heightPx);
@@ -24,7 +24,6 @@ import static com.android.launcher3.Flags.extendibleThemeManager;
import static com.android.launcher3.LauncherPrefs.GRID_NAME;
import static com.android.launcher3.LauncherPrefs.NON_FIXED_LANDSCAPE_GRID_NAME;
import static com.android.launcher3.WorkspaceLayoutManager.FIRST_SCREEN_ID;
import static com.android.launcher3.WorkspaceLayoutManager.SECOND_SCREEN_ID;
import static com.android.launcher3.graphics.ThemeManager.PREF_ICON_SHAPE;
import static com.android.launcher3.graphics.ThemeManager.THEMED_ICONS;
import static com.android.launcher3.provider.LauncherDbUtils.selectionForWorkspaceScreen;
@@ -48,7 +47,6 @@ import android.util.SparseIntArray;
import android.view.ContextThemeWrapper;
import android.view.Display;
import android.view.SurfaceControlViewHost;
import android.view.SurfaceControlViewHost.SurfacePackage;
import android.view.View;
import android.view.animation.AccelerateDecelerateInterpolator;
import android.widget.FrameLayout;
@@ -77,6 +75,7 @@ import com.android.launcher3.widget.LocalColorExtractor;
import com.android.systemui.shared.Flags;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;
/** Render preview using surface view. */
@@ -84,17 +83,21 @@ import java.util.concurrent.TimeUnit;
public class PreviewSurfaceRenderer {
private static final String TAG = "PreviewSurfaceRenderer";
private static final int FADE_IN_ANIMATION_DURATION = 200;
private static final String KEY_HOST_TOKEN = "host_token";
private static final String KEY_VIEW_WIDTH = "width";
private static final String KEY_VIEW_HEIGHT = "height";
private static final String KEY_DISPLAY_ID = "display_id";
private static final String KEY_COLORS = "wallpaper_colors";
private static final String KEY_COLOR_RESOURCE_IDS = "color_resource_ids";
private static final String KEY_COLOR_VALUES = "color_values";
private static final String KEY_DARK_MODE = "use_dark_mode";
private static final String KEY_LAYOUT_XML = "layout_xml";
public static final String KEY_SKIP_ANIMATIONS = "skip_animations";
public static final int FADE_IN_ANIMATION_DURATION = 200;
public static final String KEY_HOST_TOKEN = "host_token";
public static final String KEY_VIEW_WIDTH = "width";
public static final String KEY_VIEW_HEIGHT = "height";
public static final String KEY_DISPLAY_ID = "display_id";
public static final String KEY_COLORS = "wallpaper_colors";
public static final String KEY_COLOR_RESOURCE_IDS = "color_resource_ids";
public static final String KEY_COLOR_VALUES = "color_values";
public static final String KEY_DARK_MODE = "use_dark_mode";
public static final String KEY_LAYOUT_XML = "layout_xml";
public static final String KEY_BITMAP_GENERATION_DELAY_MS = "bitmap_delay_ms";
// Wait for some time before capturing screenshot to allow the surface to be laid out
public static final long MIN_BITMAP_GENERATION_DELAY_MS = 100L;
public static final String KEY_WORKSPACE_PAGE_ID = "workspace_page_id";
public static final String FIXED_LANDSCAPE_GRID = "fixed_landscape_mode";
private final Context mContext;
@@ -103,6 +106,7 @@ public class PreviewSurfaceRenderer {
private String mLastSelectedGridName;
private String mShapeKey;
private String mLayoutXml;
private int mWorkspacePageId;
private boolean mIsMonoThemeEnabled;
@Nullable private Boolean mDarkMode;
@@ -127,7 +131,7 @@ public class PreviewSurfaceRenderer {
@Nullable private SurfaceControlViewHost.SurfacePackage mSurfacePackage;
public PreviewSurfaceRenderer(Context context, RunnableList lifecycleTracker, Bundle bundle,
int callingPid) throws Exception {
int callingPid, boolean skipAnimations) throws Exception {
mContext = context;
mLifeCycleTracker = lifecycleTracker;
mCallingPid = callingPid;
@@ -151,11 +155,12 @@ public class PreviewSurfaceRenderer {
mHostToken = bundle.getBinder(KEY_HOST_TOKEN);
mWidth = bundle.getInt(KEY_VIEW_WIDTH);
mHeight = bundle.getInt(KEY_VIEW_HEIGHT);
mSkipAnimations = bundle.getBoolean(KEY_SKIP_ANIMATIONS, false);
mSkipAnimations = skipAnimations;
mDisplayId = bundle.getInt(KEY_DISPLAY_ID);
mDisplay = context.getSystemService(DisplayManager.class)
.getDisplay(mDisplayId);
mLayoutXml = bundle.getString(KEY_LAYOUT_XML);
mWorkspacePageId = bundle.getInt(KEY_WORKSPACE_PAGE_ID, FIRST_SCREEN_ID);
if (mDisplay == null) {
throw new IllegalArgumentException("Display ID does not match any displays.");
}
@@ -177,11 +182,8 @@ public class PreviewSurfaceRenderer {
return mHostToken;
}
public SurfacePackage getSurfacePackage() {
if (mSurfacePackage == null) {
mSurfacePackage = mSurfaceControlViewHost.getSurfacePackage();
}
return mSurfacePackage;
public SurfaceControlViewHost getHost() {
return mSurfaceControlViewHost;
}
private void setCurrentRenderer(LauncherPreviewRenderer renderer) {
@@ -241,10 +243,12 @@ public class PreviewSurfaceRenderer {
}
/**
* Generates the preview in background
* Generates the preview in background and returns the generated view
*/
public void loadAsync() {
MODEL_EXECUTOR.execute(this::loadModelData);
public CompletableFuture<View> loadAsync() {
CompletableFuture<View> result = new CompletableFuture<>();
MODEL_EXECUTOR.execute(() -> loadModelData(result));
return result;
}
/**
@@ -379,7 +383,7 @@ public class PreviewSurfaceRenderer {
}
@WorkerThread
private void loadModelData() {
private void loadModelData(CompletableFuture<View> onCompleteCallback) {
final Context inflationContext = getPreviewContext();
if (shouldReloadModelData()) {
boolean isCustomLayout = extendibleThemeManager() && !TextUtils.isEmpty(mLayoutXml);
@@ -409,21 +413,26 @@ public class PreviewSurfaceRenderer {
InvariantDeviceProfile idp = appComponent.getIDP();
DeviceProfile deviceProfile = idp.getDeviceProfile(previewContext);
int closestEvenPageId = mWorkspacePageId - (mWorkspacePageId % 2);
String query = deviceProfile.isTwoPanels
? selectionForWorkspaceScreen(FIRST_SCREEN_ID, SECOND_SCREEN_ID)
: selectionForWorkspaceScreen(FIRST_SCREEN_ID);
? selectionForWorkspaceScreen(closestEvenPageId, closestEvenPageId + 1)
: selectionForWorkspaceScreen(mWorkspacePageId);
task.loadWorkspaceForPreview(query);
final SparseArray<Size> spanInfo = getLoadedLauncherWidgetInfo();
MAIN_EXECUTOR.execute(() -> {
renderView(previewContext, appComponent.getDataModel(), spanInfo, idp);
renderView(previewContext, appComponent.getDataModel(), spanInfo, idp,
onCompleteCallback);
mLifeCycleTracker.add(previewContext::onDestroy);
});
} else {
LauncherAppState.getInstance(inflationContext).getModel().loadAsync(dataModel -> {
if (dataModel != null) {
MAIN_EXECUTOR.execute(() -> renderView(inflationContext, dataModel,
null, LauncherAppState.getIDP(inflationContext)));
null, LauncherAppState.getIDP(inflationContext), onCompleteCallback));
} else {
onCompleteCallback.completeExceptionally(
new RuntimeException("Model loading failed"));
Log.e(TAG, "Model loading failed");
}
});
@@ -433,17 +442,19 @@ public class PreviewSurfaceRenderer {
@UiThread
private void renderView(Context inflationContext, BgDataModel dataModel,
@Nullable final SparseArray<Size> launcherWidgetSpanInfo, InvariantDeviceProfile idp) {
@Nullable final SparseArray<Size> launcherWidgetSpanInfo, InvariantDeviceProfile idp,
CompletableFuture<View> onCompleteCallback) {
if (mDestroyed) {
onCompleteCallback.completeExceptionally(new RuntimeException("Renderer destroyed"));
return;
}
LauncherPreviewRenderer renderer;
if (Flags.newCustomizationPickerUi()) {
renderer = new LauncherPreviewRenderer(inflationContext, idp,
mPreviewColorOverride, mWallpaperColors, launcherWidgetSpanInfo);
renderer = new LauncherPreviewRenderer(inflationContext, idp, mPreviewColorOverride,
mWallpaperColors, mWorkspacePageId, launcherWidgetSpanInfo);
} else {
renderer = new LauncherPreviewRenderer(inflationContext, idp,
mWallpaperColors, launcherWidgetSpanInfo);
mWallpaperColors, mWorkspacePageId, launcherWidgetSpanInfo);
}
renderer.hideBottomRow(mHideQsb);
renderer.populate(dataModel);
@@ -472,6 +483,7 @@ public class PreviewSurfaceRenderer {
view.getMeasuredWidth(),
view.getMeasuredHeight()
);
onCompleteCallback.complete(view);
return;
}
@@ -497,6 +509,7 @@ public class PreviewSurfaceRenderer {
mViewRoot.removeAllViews();
mViewRoot.addView(view);
}
onCompleteCallback.complete(view);
}
private static class MySurfaceControlViewHost extends SurfaceControlViewHost {
@@ -25,12 +25,14 @@ import android.content.Context;
import android.content.Intent;
import android.content.pm.LauncherActivityInfo;
import android.content.pm.ShortcutInfo;
import android.graphics.Bitmap;
import android.graphics.drawable.ColorDrawable;
import android.net.Uri;
import android.os.Process;
import android.os.UserHandle;
import android.os.UserManager;
import android.util.ArrayMap;
import android.view.SurfaceControlViewHost;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -41,6 +43,7 @@ import com.android.launcher3.Utilities;
import com.android.launcher3.dagger.ApplicationContext;
import com.android.launcher3.dagger.LauncherAppComponent;
import com.android.launcher3.dagger.LauncherAppSingleton;
import com.android.launcher3.icons.BitmapRenderer;
import java.util.Collections;
import java.util.List;
@@ -207,6 +210,11 @@ public class ApiWrapper {
return false;
}
/** Captures a snapshot of the host content as a bitmap */
public Bitmap captureSnapshot(SurfaceControlViewHost host, int width, int height) {
return BitmapRenderer.createHardwareBitmap(width, height, host.getView()::draw);
}
private static class NoopDrawable extends ColorDrawable {
@Override
public int getIntrinsicHeight() {