Files
lawnchair/src/com/android/launcher3/graphics/LauncherPreviewRenderer.java
Tracy Zhou b023873bc3 Improve grid preview performance
After breaking down the time we spend in rendering preview under a different grid setting, I found out that we spend a huge amount of time loading the workspace. It takes a long time in non preview case (launcher workspace init), so to optimize we should try to cut down things that are not necessary for preview. Widget model loading (widget and shortcut updates) takes half of the time, and can be optimized with minimal risks / code changes.

Bug: 160662425
Test: Manual

Change-Id: I89029d0ddf6e2517077a0ba3fbbcfdcd60b268d9
2020-07-14 23:30:03 +00:00

636 lines
26 KiB
Java

/*
* Copyright (C) 2018 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.graphics;
import static android.view.View.MeasureSpec.EXACTLY;
import static android.view.View.MeasureSpec.makeMeasureSpec;
import static android.view.View.VISIBLE;
import static com.android.launcher3.config.FeatureFlags.ENABLE_LAUNCHER_PREVIEW_IN_GRID_PICKER;
import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems;
import static com.android.launcher3.model.ModelUtils.getMissingHotseatRanks;
import static com.android.launcher3.model.ModelUtils.sortWorkspaceItemsSpatially;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import android.annotation.TargetApi;
import android.app.Fragment;
import android.appwidget.AppWidgetHostView;
import android.appwidget.AppWidgetProviderInfo;
import android.content.Context;
import android.content.ContextWrapper;
import android.content.Intent;
import android.content.pm.ShortcutInfo;
import android.content.res.TypedArray;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.AdaptiveIconDrawable;
import android.graphics.drawable.ColorDrawable;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.os.Process;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ContextThemeWrapper;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextClock;
import com.android.launcher3.BubbleTextView;
import com.android.launcher3.CellLayout;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.Hotseat;
import com.android.launcher3.InsettableFrameLayout;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherAppWidgetProviderInfo;
import com.android.launcher3.LauncherModel;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.LauncherSettings.Favorites;
import com.android.launcher3.R;
import com.android.launcher3.WorkspaceLayoutManager;
import com.android.launcher3.allapps.SearchUiManager;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.folder.FolderIcon;
import com.android.launcher3.icons.BaseIconFactory;
import com.android.launcher3.icons.BitmapInfo;
import com.android.launcher3.icons.LauncherIcons;
import com.android.launcher3.model.AllAppsList;
import com.android.launcher3.model.BgDataModel;
import com.android.launcher3.model.BgDataModel.Callbacks;
import com.android.launcher3.model.LoaderResults;
import com.android.launcher3.model.LoaderTask;
import com.android.launcher3.model.WidgetItem;
import com.android.launcher3.model.WidgetsModel;
import com.android.launcher3.model.data.AppInfo;
import com.android.launcher3.model.data.FolderInfo;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.model.data.WorkspaceItemInfo;
import com.android.launcher3.pm.InstallSessionHelper;
import com.android.launcher3.pm.UserCache;
import com.android.launcher3.uioverrides.PredictedAppIconInflater;
import com.android.launcher3.uioverrides.plugins.PluginManagerWrapper;
import com.android.launcher3.util.ComponentKey;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.MainThreadInitializedObject;
import com.android.launcher3.views.ActivityContext;
import com.android.launcher3.views.BaseDragLayer;
import com.android.launcher3.widget.custom.CustomWidgetManager;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* Utility class for generating the preview of Launcher for a given InvariantDeviceProfile.
* Steps:
* 1) Create a dummy icon info with just white icon
* 2) Inflate a strip down layout definition for Launcher
* 3) Place appropriate elements like icons and first-page qsb
* 4) Measure and draw the view on a canvas
*/
@TargetApi(Build.VERSION_CODES.O)
public class LauncherPreviewRenderer {
private static final String TAG = "LauncherPreviewRenderer";
/**
* Context used just for preview. It also provides a few objects (e.g. UserCache) just for
* preview purposes.
*/
public static class PreviewContext extends ContextWrapper {
private static final Set<MainThreadInitializedObject> WHITELIST = new HashSet<>(
Arrays.asList(UserCache.INSTANCE, InstallSessionHelper.INSTANCE,
LauncherAppState.INSTANCE, InvariantDeviceProfile.INSTANCE,
CustomWidgetManager.INSTANCE, PluginManagerWrapper.INSTANCE));
private final InvariantDeviceProfile mIdp;
private final Map<MainThreadInitializedObject, Object> mObjectMap = new HashMap<>();
private final ConcurrentLinkedQueue<LauncherIconsForPreview> mIconPool =
new ConcurrentLinkedQueue<>();
public PreviewContext(Context base, InvariantDeviceProfile idp) {
super(base);
mIdp = idp;
}
@Override
public Context getApplicationContext() {
return this;
}
public void onDestroy() {
CustomWidgetManager customWidgetManager = (CustomWidgetManager) mObjectMap.get(
CustomWidgetManager.INSTANCE);
if (customWidgetManager != null) {
customWidgetManager.onDestroy();
}
}
/**
* Find a cached object from mObjectMap if we have already created one. If not, generate
* an object using the provider.
*/
public <T> T getObject(MainThreadInitializedObject<T> mainThreadInitializedObject,
MainThreadInitializedObject.ObjectProvider<T> provider) {
if (!WHITELIST.contains(mainThreadInitializedObject)) {
throw new IllegalStateException("Leaking unknown objects");
}
if (mainThreadInitializedObject == LauncherAppState.INSTANCE) {
throw new IllegalStateException(
"Should not use MainThreadInitializedObject to initialize this with "
+ "PreviewContext");
}
if (mainThreadInitializedObject == InvariantDeviceProfile.INSTANCE) {
return (T) mIdp;
}
if (mObjectMap.containsKey(mainThreadInitializedObject)) {
return (T) mObjectMap.get(mainThreadInitializedObject);
}
T t = provider.get(this);
mObjectMap.put(mainThreadInitializedObject, t);
return t;
}
public LauncherIcons newLauncherIcons(Context context, boolean shapeDetection) {
LauncherIconsForPreview launcherIconsForPreview = mIconPool.poll();
if (launcherIconsForPreview != null) {
return launcherIconsForPreview;
}
return new LauncherIconsForPreview(context, mIdp.fillResIconDpi, mIdp.iconBitmapSize,
-1 /* poolId */, shapeDetection);
}
private final class LauncherIconsForPreview extends LauncherIcons {
private LauncherIconsForPreview(Context context, int fillResIconDpi, int iconBitmapSize,
int poolId, boolean shapeDetection) {
super(context, fillResIconDpi, iconBitmapSize, poolId, shapeDetection);
}
@Override
public void recycle() {
// Clear any temporary state variables
clear();
mIconPool.offer(this);
}
}
}
private final Handler mUiHandler;
private final Context mContext;
private final InvariantDeviceProfile mIdp;
private final DeviceProfile mDp;
private final boolean mMigrated;
private final Rect mInsets;
private final WorkspaceItemInfo mWorkspaceItemInfo;
public LauncherPreviewRenderer(Context context, InvariantDeviceProfile idp, boolean migrated) {
mUiHandler = new Handler(Looper.getMainLooper());
mContext = context;
mIdp = idp;
mDp = idp.portraitProfile.copy(context);
mMigrated = migrated;
// TODO: get correct insets once display cutout API is available.
mInsets = new Rect();
mInsets.left = mInsets.right = (mDp.widthPx - mDp.availableWidthPx) / 2;
mInsets.top = mInsets.bottom = (mDp.heightPx - mDp.availableHeightPx) / 2;
mDp.updateInsets(mInsets);
BaseIconFactory iconFactory =
new BaseIconFactory(context, mIdp.fillResIconDpi, mIdp.iconBitmapSize) { };
BitmapInfo iconInfo = iconFactory.createBadgedIconBitmap(new AdaptiveIconDrawable(
new ColorDrawable(Color.WHITE), new ColorDrawable(Color.WHITE)),
Process.myUserHandle(),
Build.VERSION.SDK_INT);
mWorkspaceItemInfo = new WorkspaceItemInfo();
mWorkspaceItemInfo.bitmap = iconInfo;
mWorkspaceItemInfo.intent = new Intent();
mWorkspaceItemInfo.contentDescription = mWorkspaceItemInfo.title =
context.getString(R.string.label_application);
}
/** Populate preview and render it. */
public View getRenderedView() {
MainThreadRenderer renderer = new MainThreadRenderer(mContext);
renderer.populate();
return renderer.mRootView;
}
private class MainThreadRenderer extends ContextThemeWrapper
implements ActivityContext, WorkspaceLayoutManager, LayoutInflater.Factory2 {
private final LayoutInflater mHomeElementInflater;
private final InsettableFrameLayout mRootView;
private final Hotseat mHotseat;
private final CellLayout mWorkspace;
MainThreadRenderer(Context context) {
super(context, R.style.AppTheme);
mHomeElementInflater = LayoutInflater.from(
new ContextThemeWrapper(this, R.style.HomeScreenElementTheme));
mHomeElementInflater.setFactory2(this);
mRootView = (InsettableFrameLayout) mHomeElementInflater.inflate(
R.layout.launcher_preview_layout, null, false);
mRootView.setInsets(mInsets);
measureView(mRootView, mDp.widthPx, mDp.heightPx);
mHotseat = mRootView.findViewById(R.id.hotseat);
mHotseat.resetLayout(false);
mWorkspace = mRootView.findViewById(R.id.workspace);
mWorkspace.setPadding(mDp.workspacePadding.left + mDp.cellLayoutPaddingLeftRightPx,
mDp.workspacePadding.top,
mDp.workspacePadding.right + mDp.cellLayoutPaddingLeftRightPx,
mDp.workspacePadding.bottom);
}
@Override
public View onCreateView(View parent, String name, Context context, AttributeSet attrs) {
if ("TextClock".equals(name)) {
// Workaround for TextClock accessing handler for unregistering ticker.
return new TextClock(context, attrs) {
@Override
public Handler getHandler() {
return mUiHandler;
}
};
} else if (!"fragment".equals(name)) {
return null;
}
TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.PreviewFragment);
FragmentWithPreview f = (FragmentWithPreview) Fragment.instantiate(
context, ta.getString(R.styleable.PreviewFragment_android_name));
f.enterPreviewMode(context);
f.onInit(null);
View view = f.onCreateView(LayoutInflater.from(context), (ViewGroup) parent, null);
view.setId(ta.getInt(R.styleable.PreviewFragment_android_id, View.NO_ID));
return view;
}
@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
return onCreateView(null, name, context, attrs);
}
@Override
public BaseDragLayer getDragLayer() {
throw new UnsupportedOperationException();
}
@Override
public DeviceProfile getDeviceProfile() {
return mDp;
}
@Override
public Hotseat getHotseat() {
return mHotseat;
}
@Override
public CellLayout getScreenWithId(int screenId) {
return mWorkspace;
}
private void inflateAndAddIcon(WorkspaceItemInfo info) {
BubbleTextView icon = (BubbleTextView) mHomeElementInflater.inflate(
R.layout.app_icon, mWorkspace, false);
icon.applyFromWorkspaceItem(info);
addInScreenFromBind(icon, info);
}
private void inflateAndAddFolder(FolderInfo info) {
FolderIcon folderIcon = FolderIcon.inflateIcon(R.layout.folder_icon, this, mWorkspace,
info);
addInScreenFromBind(folderIcon, info);
}
private void inflateAndAddWidgets(LauncherAppWidgetInfo info,
Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap) {
if (widgetProviderInfoMap == null) {
return;
}
AppWidgetProviderInfo providerInfo = widgetProviderInfoMap.get(
new ComponentKey(info.providerName, info.user));
if (providerInfo == null) {
return;
}
inflateAndAddWidgets(info, LauncherAppWidgetProviderInfo.fromProviderInfo(
getApplicationContext(), providerInfo));
}
private void inflateAndAddWidgets(LauncherAppWidgetInfo info, WidgetsModel widgetsModel) {
WidgetItem widgetItem = widgetsModel.getWidgetProviderInfoByProviderName(
info.providerName);
if (widgetItem == null) {
return;
}
inflateAndAddWidgets(info, widgetItem.widgetInfo);
}
private void inflateAndAddWidgets(LauncherAppWidgetInfo info,
LauncherAppWidgetProviderInfo providerInfo) {
AppWidgetHostView view = new AppWidgetHostView(mContext);
view.setAppWidget(-1, providerInfo);
view.updateAppWidget(null);
view.setTag(info);
addInScreenFromBind(view, info);
}
private void inflateAndAddPredictedIcon(WorkspaceItemInfo info) {
View view = PredictedAppIconInflater.inflate(mHomeElementInflater, mWorkspace, info);
if (view != null) {
addInScreenFromBind(view, info);
}
}
private void dispatchVisibilityAggregated(View view, boolean isVisible) {
// Similar to View.dispatchVisibilityAggregated implementation.
final boolean thisVisible = view.getVisibility() == VISIBLE;
if (thisVisible || !isVisible) {
view.onVisibilityAggregated(isVisible);
}
if (view instanceof ViewGroup) {
isVisible = thisVisible && isVisible;
ViewGroup vg = (ViewGroup) view;
int count = vg.getChildCount();
for (int i = 0; i < count; i++) {
dispatchVisibilityAggregated(vg.getChildAt(i), isVisible);
}
}
}
private void populate() {
if (ENABLE_LAUNCHER_PREVIEW_IN_GRID_PICKER.get()) {
WorkspaceFetcher fetcher;
PreviewContext previewContext = null;
if (mMigrated) {
previewContext = new PreviewContext(mContext, mIdp);
LauncherAppState appForPreview = new LauncherAppState(
previewContext, null /* iconCacheFileName */);
fetcher = new WorkspaceItemsInfoFromPreviewFetcher(appForPreview);
MODEL_EXECUTOR.execute(fetcher);
} else {
fetcher = new WorkspaceItemsInfoFetcher();
LauncherAppState.getInstance(mContext).getModel().enqueueModelUpdateTask(
(LauncherModel.ModelUpdateTask) fetcher);
}
WorkspaceResult workspaceResult = fetcher.get();
if (previewContext != null) {
previewContext.onDestroy();
}
if (workspaceResult == null) {
return;
}
// Separate the items that are on the current screen, and all the other remaining
// items
ArrayList<ItemInfo> currentWorkspaceItems = new ArrayList<>();
ArrayList<ItemInfo> otherWorkspaceItems = new ArrayList<>();
ArrayList<LauncherAppWidgetInfo> currentAppWidgets = new ArrayList<>();
ArrayList<LauncherAppWidgetInfo> otherAppWidgets = new ArrayList<>();
filterCurrentWorkspaceItems(0 /* currentScreenId */,
workspaceResult.mWorkspaceItems, currentWorkspaceItems,
otherWorkspaceItems);
filterCurrentWorkspaceItems(0 /* currentScreenId */, workspaceResult.mAppWidgets,
currentAppWidgets, otherAppWidgets);
sortWorkspaceItemsSpatially(mIdp, currentWorkspaceItems);
for (ItemInfo itemInfo : currentWorkspaceItems) {
switch (itemInfo.itemType) {
case Favorites.ITEM_TYPE_APPLICATION:
case Favorites.ITEM_TYPE_SHORTCUT:
case Favorites.ITEM_TYPE_DEEP_SHORTCUT:
inflateAndAddIcon((WorkspaceItemInfo) itemInfo);
break;
case LauncherSettings.Favorites.ITEM_TYPE_FOLDER:
inflateAndAddFolder((FolderInfo) itemInfo);
break;
default:
break;
}
}
for (ItemInfo itemInfo : currentAppWidgets) {
switch (itemInfo.itemType) {
case Favorites.ITEM_TYPE_APPWIDGET:
case Favorites.ITEM_TYPE_CUSTOM_APPWIDGET:
if (mMigrated) {
inflateAndAddWidgets((LauncherAppWidgetInfo) itemInfo,
workspaceResult.mWidgetProvidersMap);
} else {
inflateAndAddWidgets((LauncherAppWidgetInfo) itemInfo,
workspaceResult.mWidgetsModel);
}
break;
default:
break;
}
}
IntArray ranks = getMissingHotseatRanks(currentWorkspaceItems,
mIdp.numHotseatIcons);
int count = Math.min(ranks.size(), workspaceResult.mCachedPredictedItems.size());
for (int i = 0; i < count; i++) {
AppInfo appInfo = workspaceResult.mCachedPredictedItems.get(i);
int rank = ranks.get(i);
WorkspaceItemInfo itemInfo = new WorkspaceItemInfo(appInfo);
itemInfo.container = LauncherSettings.Favorites.CONTAINER_HOTSEAT_PREDICTION;
itemInfo.rank = rank;
itemInfo.cellX = mHotseat.getCellXFromOrder(rank);
itemInfo.cellY = mHotseat.getCellYFromOrder(rank);
itemInfo.screenId = rank;
inflateAndAddPredictedIcon(itemInfo);
}
} else {
// Add hotseat icons
for (int i = 0; i < mIdp.numHotseatIcons; i++) {
WorkspaceItemInfo info = new WorkspaceItemInfo(mWorkspaceItemInfo);
info.container = Favorites.CONTAINER_HOTSEAT;
info.screenId = i;
inflateAndAddIcon(info);
}
// Add workspace icons
for (int i = 0; i < mIdp.numColumns; i++) {
WorkspaceItemInfo info = new WorkspaceItemInfo(mWorkspaceItemInfo);
info.container = Favorites.CONTAINER_DESKTOP;
info.screenId = 0;
info.cellX = i;
info.cellY = mIdp.numRows - 1;
inflateAndAddIcon(info);
}
}
// Add first page QSB
if (FeatureFlags.QSB_ON_FIRST_SCREEN) {
View qsb = mHomeElementInflater.inflate(
R.layout.search_container_workspace, mWorkspace, false);
CellLayout.LayoutParams lp =
new CellLayout.LayoutParams(0, 0, mWorkspace.getCountX(), 1);
lp.canReorder = false;
mWorkspace.addViewToCellLayout(qsb, 0, R.id.search_container_workspace, lp, true);
}
// Setup search view
SearchUiManager searchUiManager =
mRootView.findViewById(R.id.search_container_all_apps);
mRootView.findViewById(R.id.apps_view).setTranslationY(
mDp.heightPx - searchUiManager.getScrollRangeDelta(mInsets));
measureView(mRootView, mDp.widthPx, mDp.heightPx);
dispatchVisibilityAggregated(mRootView, true);
measureView(mRootView, mDp.widthPx, mDp.heightPx);
// Additional measure for views which use auto text size API
measureView(mRootView, mDp.widthPx, mDp.heightPx);
}
}
private static void measureView(View view, int width, int height) {
view.measure(makeMeasureSpec(width, EXACTLY), makeMeasureSpec(height, EXACTLY));
view.layout(0, 0, width, height);
}
private static class WorkspaceItemsInfoFetcher implements LauncherModel.ModelUpdateTask,
WorkspaceFetcher {
private final FutureTask<WorkspaceResult> mTask = new FutureTask<>(this);
private LauncherAppState mApp;
private LauncherModel mModel;
private BgDataModel mBgDataModel;
private AllAppsList mAllAppsList;
@Override
public void init(LauncherAppState app, LauncherModel model, BgDataModel dataModel,
AllAppsList allAppsList, Executor uiExecutor) {
mApp = app;
mModel = model;
mBgDataModel = dataModel;
mAllAppsList = allAppsList;
}
@Override
public FutureTask<WorkspaceResult> getTask() {
return mTask;
}
@Override
public void run() {
mTask.run();
}
@Override
public WorkspaceResult call() throws Exception {
if (!mModel.isModelLoaded()) {
Log.d(TAG, "Workspace not loaded, loading now");
mModel.startLoaderForResults(
new LoaderResults(mApp, mBgDataModel, mAllAppsList, new Callbacks[0]));
return null;
}
return new WorkspaceResult(mBgDataModel.workspaceItems, mBgDataModel.appWidgets,
mBgDataModel.cachedPredictedItems, mBgDataModel.widgetsModel, null);
}
}
private static class WorkspaceItemsInfoFromPreviewFetcher extends LoaderTask implements
WorkspaceFetcher {
private final FutureTask<WorkspaceResult> mTask = new FutureTask<>(this);
WorkspaceItemsInfoFromPreviewFetcher(LauncherAppState app) {
super(app, null, new BgDataModel(), null);
}
@Override
public FutureTask<WorkspaceResult> getTask() {
return mTask;
}
@Override
public void run() {
mTask.run();
}
@Override
public WorkspaceResult call() throws Exception {
List<ShortcutInfo> allShortcuts = new ArrayList<>();
loadWorkspace(allShortcuts, LauncherSettings.Favorites.PREVIEW_CONTENT_URI);
return new WorkspaceResult(mBgDataModel.workspaceItems, mBgDataModel.appWidgets,
mBgDataModel.cachedPredictedItems, null, mWidgetProvidersMap);
}
}
private interface WorkspaceFetcher extends Runnable, Callable<WorkspaceResult> {
FutureTask<WorkspaceResult> getTask();
default WorkspaceResult get() {
try {
return getTask().get(5, TimeUnit.SECONDS);
} catch (InterruptedException | ExecutionException | TimeoutException e) {
Log.d(TAG, "Error fetching workspace items info", e);
return null;
}
}
}
private static class WorkspaceResult {
private final ArrayList<ItemInfo> mWorkspaceItems;
private final ArrayList<LauncherAppWidgetInfo> mAppWidgets;
private final ArrayList<AppInfo> mCachedPredictedItems;
private final WidgetsModel mWidgetsModel;
private final Map<ComponentKey, AppWidgetProviderInfo> mWidgetProvidersMap;
private WorkspaceResult(ArrayList<ItemInfo> workspaceItems,
ArrayList<LauncherAppWidgetInfo> appWidgets,
ArrayList<AppInfo> cachedPredictedItems, WidgetsModel widgetsModel,
Map<ComponentKey, AppWidgetProviderInfo> widgetProviderInfoMap) {
mWorkspaceItems = workspaceItems;
mAppWidgets = appWidgets;
mCachedPredictedItems = cachedPredictedItems;
mWidgetsModel = widgetsModel;
mWidgetProvidersMap = widgetProviderInfoMap;
}
}
}