Adding support for async view inflation
Bug: 318539160
Test: atest AsyncBindingTest; atest ItemInflaterTest
Flag: aconfig enable_workspace_inflation DEVELOPMENT
Change-Id: I77a373db7a5805f68f4b8cbfa9b586b5674252de
diff --git a/src/com/android/launcher3/Alarm.java b/src/com/android/launcher3/Alarm.java
index e4aebf6..fb8088c 100644
--- a/src/com/android/launcher3/Alarm.java
+++ b/src/com/android/launcher3/Alarm.java
@@ -17,6 +17,7 @@
package com.android.launcher3;
import android.os.Handler;
+import android.os.Looper;
import android.os.SystemClock;
public class Alarm implements Runnable{
@@ -33,7 +34,11 @@
private long mLastSetTimeout;
public Alarm() {
- mHandler = new Handler();
+ this(Looper.myLooper());
+ }
+
+ public Alarm(Looper looper) {
+ mHandler = new Handler(looper);
}
public void setOnAlarmListener(OnAlarmListener alarmListener) {
diff --git a/src/com/android/launcher3/Launcher.java b/src/com/android/launcher3/Launcher.java
index edfef5e..1ab6222 100644
--- a/src/com/android/launcher3/Launcher.java
+++ b/src/com/android/launcher3/Launcher.java
@@ -28,6 +28,7 @@
import static com.android.launcher3.AbstractFloatingView.TYPE_REBIND_SAFE;
import static com.android.launcher3.AbstractFloatingView.getTopOpenViewWithType;
import static com.android.launcher3.Flags.enableAddAppWidgetViaConfigActivityV2;
+import static com.android.launcher3.Flags.enableWorkspaceInflation;
import static com.android.launcher3.LauncherAnimUtils.HOTSEAT_SCALE_PROPERTY_FACTORY;
import static com.android.launcher3.LauncherAnimUtils.SCALE_INDEX_WIDGET_TRANSITION;
import static com.android.launcher3.LauncherAnimUtils.SPRING_LOADED_EXIT_DELAY;
@@ -1485,8 +1486,8 @@
CellPos presenterPos = getCellPosMapper().mapModelToPresenter(itemInfo);
if (showPendingWidget) {
launcherInfo.restoreStatus = LauncherAppWidgetInfo.FLAG_UI_NOT_READY;
- PendingAppWidgetHostView pendingAppWidgetHostView =
- new PendingAppWidgetHostView(this, launcherInfo, appWidgetInfo);
+ PendingAppWidgetHostView pendingAppWidgetHostView = new PendingAppWidgetHostView(
+ this, mAppWidgetHolder, launcherInfo, appWidgetInfo);
pendingAppWidgetHostView.setPreviewBitmap(widgetPreviewBitmap);
hostView = pendingAppWidgetHostView;
} else if (hostView instanceof PendingAppWidgetHostView) {
@@ -2187,17 +2188,23 @@
*/
@Override
public void bindItems(final List<ItemInfo> items, final boolean forceAnimateIcons) {
- bindItems(items.stream().map(i -> Pair.create(
+ bindInflatedItems(items.stream().map(i -> Pair.create(
i, getItemInflater().inflateItem(i, getModelWriter()))).toList(),
forceAnimateIcons ? new AnimatorSet() : null);
}
+ @Override
+ public void bindInflatedItems(List<Pair<ItemInfo, View>> items) {
+ bindInflatedItems(items, null);
+ }
+
/**
* Bind all the items in the map, ignoring any null views
*
* @param boundAnim if non-null, uses it to create and play the bounce animation for added views
*/
- public void bindItems(List<Pair<ItemInfo, View>> shortcuts, @Nullable AnimatorSet boundAnim) {
+ public void bindInflatedItems(
+ List<Pair<ItemInfo, View>> shortcuts, @Nullable AnimatorSet boundAnim) {
// Get the list of added items and intersect them with the set of items here
Workspace<?> workspace = mWorkspace;
int newItemsScreenId = -1;
@@ -2222,10 +2229,13 @@
}
}
- final View view = e.second;
+ View view = e.second;
if (view == null) {
continue;
}
+ if (enableWorkspaceInflation() && view instanceof LauncherAppWidgetHostView lv) {
+ view = getAppWidgetHolder().attachViewToHostAndGetAttachedView(lv);
+ }
workspace.addInScreenFromBind(view, item);
if (boundAnim != null) {
// Animate all the applications up now
@@ -2324,9 +2334,9 @@
@Override
public void onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks,
- int workspaceItemCount, boolean isBindSync) {
- mModelCallbacks.onInitialBindComplete(boundPages, pendingTasks, workspaceItemCount,
- isBindSync);
+ RunnableList onCompleteSignal, int workspaceItemCount, boolean isBindSync) {
+ mModelCallbacks.onInitialBindComplete(boundPages, pendingTasks, onCompleteSignal,
+ workspaceItemCount, isBindSync);
}
/**
@@ -3057,6 +3067,7 @@
return super.getStatsLogManager().withDefaultInstanceId(mAllAppsSessionLogId);
}
+ @Override
public ItemInflater<Launcher> getItemInflater() {
return mItemInflater;
}
diff --git a/src/com/android/launcher3/ModelCallbacks.kt b/src/com/android/launcher3/ModelCallbacks.kt
index 9867556..9b65a31 100644
--- a/src/com/android/launcher3/ModelCallbacks.kt
+++ b/src/com/android/launcher3/ModelCallbacks.kt
@@ -72,6 +72,7 @@
override fun onInitialBindComplete(
boundPages: LIntSet,
pendingTasks: RunnableList,
+ onCompleteSignal: RunnableList,
workspaceItemCount: Int,
isBindSync: Boolean
) {
@@ -99,7 +100,14 @@
}
}
pendingExecutor = executor
- executor.attachTo(launcher)
+
+ if (Flags.enableWorkspaceInflation()) {
+ // Finish the executor as soon as the pending inflation is completed
+ onCompleteSignal.add(executor::markCompleted)
+ } else {
+ // Pending executor is already completed, wait until first draw to run the tasks
+ executor.attachTo(launcher)
+ }
launcher.bindComplete(workspaceItemCount, isBindSync)
}
@@ -409,4 +417,6 @@
}
fun getIsFirstPagePinnedItemEnabled(): Boolean = isFirstPagePinnedItemEnabled
+
+ override fun getItemInflater() = launcher.itemInflater
}
diff --git a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
index a846e68..e861d38 100644
--- a/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
+++ b/src/com/android/launcher3/accessibility/LauncherAccessibilityDelegate.java
@@ -440,7 +440,7 @@
anim.addListener(forEndCallback(
() -> view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED)));
}
- mContext.bindItems(Collections.singletonList(Pair.create(item, view)), anim);
+ mContext.bindInflatedItems(Collections.singletonList(Pair.create(item, view)), anim);
}
/**
diff --git a/src/com/android/launcher3/folder/Folder.java b/src/com/android/launcher3/folder/Folder.java
index f013126..ec9c27d 100644
--- a/src/com/android/launcher3/folder/Folder.java
+++ b/src/com/android/launcher3/folder/Folder.java
@@ -40,6 +40,7 @@
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.drawable.GradientDrawable;
+import android.os.Looper;
import android.text.InputType;
import android.text.Selection;
import android.text.TextUtils;
@@ -165,10 +166,10 @@
private static final Rect sTempRect = new Rect();
private static final int MIN_FOLDERS_FOR_HARDWARE_OPTIMIZATION = 10;
- private final Alarm mReorderAlarm = new Alarm();
- private final Alarm mOnExitAlarm = new Alarm();
- private final Alarm mOnScrollHintAlarm = new Alarm();
- final Alarm mScrollPauseAlarm = new Alarm();
+ private final Alarm mReorderAlarm = new Alarm(Looper.getMainLooper());
+ private final Alarm mOnExitAlarm = new Alarm(Looper.getMainLooper());
+ private final Alarm mOnScrollHintAlarm = new Alarm(Looper.getMainLooper());
+ final Alarm mScrollPauseAlarm = new Alarm(Looper.getMainLooper());
final ArrayList<View> mItemsInReadingOrder = new ArrayList<View>();
diff --git a/src/com/android/launcher3/folder/FolderIcon.java b/src/com/android/launcher3/folder/FolderIcon.java
index 284b31e..ee0d5fc 100644
--- a/src/com/android/launcher3/folder/FolderIcon.java
+++ b/src/com/android/launcher3/folder/FolderIcon.java
@@ -32,6 +32,7 @@
import android.graphics.Paint;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
+import android.os.Looper;
import android.util.AttributeSet;
import android.util.Property;
import android.view.LayoutInflater;
@@ -121,7 +122,7 @@
boolean mAnimating = false;
- private Alarm mOpenAlarm = new Alarm();
+ private Alarm mOpenAlarm = new Alarm(Looper.getMainLooper());
private boolean mForceHideDot;
@ViewDebug.ExportedProperty(category = "launcher", deepExport = true)
diff --git a/src/com/android/launcher3/icons/IconCache.java b/src/com/android/launcher3/icons/IconCache.java
index ee66a60..8e73660 100644
--- a/src/com/android/launcher3/icons/IconCache.java
+++ b/src/com/android/launcher3/icons/IconCache.java
@@ -37,6 +37,7 @@
import android.database.Cursor;
import android.database.sqlite.SQLiteException;
import android.graphics.drawable.Drawable;
+import android.os.Looper;
import android.os.Process;
import android.os.Trace;
import android.os.UserHandle;
@@ -44,6 +45,7 @@
import android.util.Log;
import android.util.SparseArray;
+import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
@@ -65,7 +67,6 @@
import com.android.launcher3.util.CancellableTask;
import com.android.launcher3.util.InstantAppResolver;
import com.android.launcher3.util.PackageUserKey;
-import com.android.launcher3.util.Preconditions;
import com.android.launcher3.widget.WidgetSections;
import com.android.launcher3.widget.WidgetSections.WidgetSection;
@@ -173,9 +174,9 @@
*
* @return a request ID that can be used to cancel the request.
*/
+ @AnyThread
public CancellableTask updateIconInBackground(final ItemInfoUpdateReceiver caller,
final ItemInfoWithIcon info) {
- Preconditions.assertUIThread();
Supplier<ItemInfoWithIcon> task;
if (info instanceof AppInfo || info instanceof WorkspaceItemInfo) {
task = () -> {
@@ -193,13 +194,19 @@
return mCancelledTask;
}
- if (mPendingIconRequestCount <= 0) {
- MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND);
+ Runnable endRunnable;
+ if (Looper.myLooper() == Looper.getMainLooper()) {
+ if (mPendingIconRequestCount <= 0) {
+ MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_FOREGROUND);
+ }
+ mPendingIconRequestCount++;
+ endRunnable = this::onIconRequestEnd;
+ } else {
+ endRunnable = () -> { };
}
- mPendingIconRequestCount++;
CancellableTask<ItemInfoWithIcon> request = new CancellableTask<>(
- task, MAIN_EXECUTOR, caller::reapplyItemInfo, this::onIconRequestEnd);
+ task, MAIN_EXECUTOR, caller::reapplyItemInfo, endRunnable);
Utilities.postAsyncCallback(mWorkerHandler, request);
return request;
}
diff --git a/src/com/android/launcher3/model/BaseLauncherBinder.java b/src/com/android/launcher3/model/BaseLauncherBinder.java
index 9b2344d..fa2a1b0 100644
--- a/src/com/android/launcher3/model/BaseLauncherBinder.java
+++ b/src/com/android/launcher3/model/BaseLauncherBinder.java
@@ -16,20 +16,25 @@
package com.android.launcher3.model;
+import static com.android.launcher3.Flags.enableWorkspaceInflation;
import static com.android.launcher3.config.FeatureFlags.ENABLE_SMARTSPACE_REMOVAL;
import static com.android.launcher3.model.ItemInstallQueue.FLAG_LOADER_RUNNING;
import static com.android.launcher3.model.ModelUtils.filterCurrentWorkspaceItems;
+import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
import static com.android.launcher3.util.Executors.MODEL_EXECUTOR;
import android.os.Process;
import android.os.Trace;
import android.util.Log;
+import android.util.Pair;
+import android.view.View;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.LauncherModel.CallbackTask;
import com.android.launcher3.LauncherSettings;
import com.android.launcher3.Workspace;
+import com.android.launcher3.celllayout.CellPosMapper;
import com.android.launcher3.config.FeatureFlags;
import com.android.launcher3.model.BgDataModel.Callbacks;
import com.android.launcher3.model.BgDataModel.FixedContainerItems;
@@ -38,6 +43,7 @@
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.IntSet;
+import com.android.launcher3.util.ItemInflater;
import com.android.launcher3.util.LooperExecutor;
import com.android.launcher3.util.LooperIdleLock;
import com.android.launcher3.util.PackageUserKey;
@@ -279,8 +285,8 @@
// 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<>();
+ ArrayList<ItemInfo> currentAppWidgets = new ArrayList<>();
+ ArrayList<ItemInfo> otherAppWidgets = new ArrayList<>();
filterCurrentWorkspaceItems(currentScreenIds, mWorkspaceItems, currentWorkspaceItems,
otherWorkspaceItems);
@@ -304,8 +310,8 @@
executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor);
// Load items on the current page.
- bindWorkspaceItems(currentWorkspaceItems, mUiExecutor);
- bindAppWidgets(currentAppWidgets, mUiExecutor);
+ bindItemsInChunks(currentWorkspaceItems, ITEMS_CHUNK, mUiExecutor);
+ bindItemsInChunks(currentAppWidgets, 1, mUiExecutor);
if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) {
mExtraItems.forEach(item ->
executeCallbacksTask(c -> c.bindExtraContainerItems(item), mUiExecutor));
@@ -313,8 +319,41 @@
RunnableList pendingTasks = new RunnableList();
Executor pendingExecutor = pendingTasks::add;
- bindWorkspaceItems(otherWorkspaceItems, pendingExecutor);
- bindAppWidgets(otherAppWidgets, pendingExecutor);
+
+ RunnableList onCompleteSignal = new RunnableList();
+
+ if (enableWorkspaceInflation()) {
+ MODEL_EXECUTOR.execute(() -> {
+ setupPendingBind(otherWorkspaceItems, otherAppWidgets, currentScreenIds,
+ pendingExecutor);
+
+ // Wait for the async inflation to complete and then notify the completion
+ // signal on UI thread.
+ MAIN_EXECUTOR.execute(onCompleteSignal::executeAllAndDestroy);
+ });
+ } else {
+ setupPendingBind(
+ otherWorkspaceItems, otherAppWidgets, currentScreenIds, pendingExecutor);
+ onCompleteSignal.executeAllAndDestroy();
+ }
+
+ executeCallbacksTask(
+ c -> {
+ if (!enableWorkspaceInflation()) {
+ MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
+ }
+ c.onInitialBindComplete(currentScreenIds, pendingTasks, onCompleteSignal,
+ workspaceItemCount, isBindSync);
+ }, mUiExecutor);
+ }
+
+ private void setupPendingBind(
+ List<ItemInfo> otherWorkspaceItems,
+ List<ItemInfo> otherAppWidgets,
+ IntSet currentScreenIds,
+ Executor pendingExecutor) {
+ bindItemsInChunks(otherWorkspaceItems, ITEMS_CHUNK, pendingExecutor);
+ bindItemsInChunks(otherAppWidgets, 1, pendingExecutor);
StringCache cacheClone = mBgDataModel.stringCache.clone();
executeCallbacksTask(c -> c.bindStringCache(cacheClone), pendingExecutor);
@@ -326,38 +365,51 @@
ItemInstallQueue.INSTANCE.get(mApp.getContext())
.resumeModelPush(FLAG_LOADER_RUNNING);
});
-
- executeCallbacksTask(
- c -> {
- MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
- c.onInitialBindComplete(
- currentScreenIds, pendingTasks, workspaceItemCount, isBindSync);
- }, mUiExecutor);
}
- private void bindWorkspaceItems(
- final ArrayList<ItemInfo> workspaceItems, final Executor executor) {
+ /**
+ * Tries to inflate the items asynchronously and bind. Returns true on success or false if
+ * async-binding is not supported in this case.
+ */
+ private boolean inflateAsyncAndBind(List<ItemInfo> items, Executor executor) {
+ if (!enableWorkspaceInflation()) {
+ return false;
+ }
+ ItemInflater inflater = mCallbacks.getItemInflater();
+ if (inflater == null) {
+ return false;
+ }
+
+ if (mMyBindingId != mBgDataModel.lastBindId) {
+ Log.d(TAG, "Too many consecutive reloads, skipping obsolete view inflation");
+ return true;
+ }
+
+ ModelWriter writer = mApp.getModel()
+ .getWriter(false /* verifyChanges */, CellPosMapper.DEFAULT, null);
+ List<Pair<ItemInfo, View>> bindItems = items.stream().map(i ->
+ Pair.create(i, inflater.inflateItem(i, writer, null))).toList();
+ executeCallbacksTask(c -> c.bindInflatedItems(bindItems), executor);
+ return true;
+ }
+
+ private void bindItemsInChunks(List<ItemInfo> workspaceItems, int chunkCount,
+ Executor executor) {
+ if (inflateAsyncAndBind(workspaceItems, executor)) {
+ return;
+ }
+
// Bind the workspace items
int count = workspaceItems.size();
- for (int i = 0; i < count; i += ITEMS_CHUNK) {
+ for (int i = 0; i < count; i += chunkCount) {
final int start = i;
- final int chunkSize = (i + ITEMS_CHUNK <= count) ? ITEMS_CHUNK : (count - i);
+ final int chunkSize = (i + chunkCount <= count) ? chunkCount : (count - i);
executeCallbacksTask(
c -> c.bindItems(workspaceItems.subList(start, start + chunkSize), false),
executor);
}
}
- private void bindAppWidgets(List<LauncherAppWidgetInfo> appWidgets, Executor executor) {
- // Bind the widgets, one at a time
- int count = appWidgets.size();
- for (int i = 0; i < count; i++) {
- final ItemInfo widget = appWidgets.get(i);
- executeCallbacksTask(
- c -> c.bindItems(Collections.singletonList(widget), false), executor);
- }
- }
-
protected void executeCallbacksTask(CallbackTask task, Executor executor) {
executor.execute(() -> {
if (mMyBindingId != mBgDataModel.lastBindId) {
@@ -430,8 +482,11 @@
bindAppWidgets(appWidgets);
executeCallbacksTask(c -> {
MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
- c.onInitialBindComplete(
- mCurrentScreenIds, new RunnableList(), workspaceItemCount, isBindSync);
+
+ RunnableList onCompleteSignal = new RunnableList();
+ onCompleteSignal.executeAllAndDestroy();
+ c.onInitialBindComplete(mCurrentScreenIds, new RunnableList(), onCompleteSignal,
+ workspaceItemCount, isBindSync);
}, mUiExecutor);
}
diff --git a/src/com/android/launcher3/model/BgDataModel.java b/src/com/android/launcher3/model/BgDataModel.java
index 7f0f683..8579d1d 100644
--- a/src/com/android/launcher3/model/BgDataModel.java
+++ b/src/com/android/launcher3/model/BgDataModel.java
@@ -33,6 +33,8 @@
import android.text.TextUtils;
import android.util.ArraySet;
import android.util.Log;
+import android.util.Pair;
+import android.view.View;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@@ -54,6 +56,7 @@
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.IntSet;
import com.android.launcher3.util.IntSparseArrayMap;
+import com.android.launcher3.util.ItemInflater;
import com.android.launcher3.util.PackageUserKey;
import com.android.launcher3.util.RunnableList;
import com.android.launcher3.widget.model.WidgetsListBaseEntry;
@@ -495,7 +498,15 @@
default void clearPendingBinds() { }
default void startBinding() { }
- default void bindItems(List<ItemInfo> shortcuts, boolean forceAnimateIcons) { }
+ @Nullable
+ default ItemInflater getItemInflater() {
+ return null;
+ }
+
+ default void bindItems(@NonNull List<ItemInfo> shortcuts, boolean forceAnimateIcons) { }
+ /** Alternate method to bind preinflated views */
+ default void bindInflatedItems(@NonNull List<Pair<ItemInfo, View>> items) { }
+
default void bindScreens(IntArray orderedScreenIds) { }
default void setIsFirstPagePinnedItemEnabled(boolean isFirstPagePinnedItemEnabled) { }
default void finishBindingItems(IntSet pagesBoundFirst) { }
@@ -520,7 +531,9 @@
default void bindSmartspaceWidget() { }
/** Called when workspace has been bound. */
- default void onInitialBindComplete(IntSet boundPages, RunnableList pendingTasks,
+ default void onInitialBindComplete(@NonNull IntSet boundPages,
+ @NonNull RunnableList pendingTasks,
+ @NonNull RunnableList onCompleteSignal,
int workspaceItemCount, boolean isBindSync) {
pendingTasks.executeAllAndDestroy();
}
diff --git a/src/com/android/launcher3/model/ModelUtils.java b/src/com/android/launcher3/model/ModelUtils.java
index bc51c9b..9e72e28 100644
--- a/src/com/android/launcher3/model/ModelUtils.java
+++ b/src/com/android/launcher3/model/ModelUtils.java
@@ -20,7 +20,6 @@
import com.android.launcher3.util.IntArray;
import com.android.launcher3.util.IntSet;
-import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@@ -37,9 +36,9 @@
*/
public static <T extends ItemInfo> void filterCurrentWorkspaceItems(
final IntSet currentScreenIds,
- ArrayList<T> allWorkspaceItems,
- ArrayList<T> currentScreenItems,
- ArrayList<T> otherScreenItems) {
+ List<? extends T> allWorkspaceItems,
+ List<T> currentScreenItems,
+ List<T> otherScreenItems) {
// Purge any null ItemInfos
allWorkspaceItems.removeIf(Objects::isNull);
// Order the set of items by their containers first, this allows use to walk through the
diff --git a/src/com/android/launcher3/util/ItemInflater.kt b/src/com/android/launcher3/util/ItemInflater.kt
index 79091ca..cc66af1 100644
--- a/src/com/android/launcher3/util/ItemInflater.kt
+++ b/src/com/android/launcher3/util/ItemInflater.kt
@@ -121,7 +121,7 @@
}
val view =
if (type == WidgetInflater.TYPE_PENDING || widgetInfo == null)
- PendingAppWidgetHostView(context, item, widgetInfo)
+ PendingAppWidgetHostView(context, widgetHolder, item, widgetInfo)
else widgetHolder.createView(item.appWidgetId, widgetInfo)
prepareAppWidget(view, item)
return view
diff --git a/src/com/android/launcher3/util/RunnableList.java b/src/com/android/launcher3/util/RunnableList.java
index f6e0c57..2b8bf56 100644
--- a/src/com/android/launcher3/util/RunnableList.java
+++ b/src/com/android/launcher3/util/RunnableList.java
@@ -69,4 +69,11 @@
}
}
}
+
+ /**
+ * Returns true if the list has been destroyed
+ */
+ public boolean isDestroyed() {
+ return mDestroyed;
+ }
}
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHost.java b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
index b1c477c..40c3984 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHost.java
@@ -54,6 +54,9 @@
@Nullable
private final IntConsumer mAppWidgetRemovedCallback;
+ @Nullable
+ private ListenableHostView mViewToRecycle;
+
public LauncherAppWidgetHost(@NonNull Context context,
@Nullable IntConsumer appWidgetRemovedCallback,
List<ProviderChangedListener> providerChangeListeners) {
@@ -73,11 +76,21 @@
}
}
+ /**
+ * Sets the view to be recycled for the next widget creation.
+ */
+ public void recycleViewForNextCreation(ListenableHostView viewToRecycle) {
+ mViewToRecycle = viewToRecycle;
+ }
+
@Override
@NonNull
public LauncherAppWidgetHostView onCreateView(Context context, int appWidgetId,
AppWidgetProviderInfo appWidget) {
- return new ListenableHostView(context);
+ ListenableHostView result =
+ mViewToRecycle != null ? mViewToRecycle : new ListenableHostView(context);
+ mViewToRecycle = null;
+ return result;
}
/**
diff --git a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
index e77ec12..2259e3c 100644
--- a/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/LauncherAppWidgetHostView.java
@@ -40,12 +40,12 @@
import com.android.launcher3.CheckLongPressHelper;
import com.android.launcher3.Flags;
-import com.android.launcher3.Launcher;
import com.android.launcher3.R;
-import com.android.launcher3.dragndrop.DragLayer;
import com.android.launcher3.model.data.ItemInfo;
import com.android.launcher3.model.data.LauncherAppWidgetInfo;
import com.android.launcher3.util.Themes;
+import com.android.launcher3.views.ActivityContext;
+import com.android.launcher3.views.BaseDragLayer;
import com.android.launcher3.views.BaseDragLayer.TouchCompleteListener;
/**
@@ -72,7 +72,7 @@
private final Rect mTempRect = new Rect();
private final CheckLongPressHelper mLongPressHelper;
- protected final Launcher mLauncher;
+ protected final ActivityContext mActivityContext;
// Maintain the color manager.
private final LocalColorExtractor mColorExtractor;
@@ -94,15 +94,15 @@
public LauncherAppWidgetHostView(Context context) {
super(context);
- mLauncher = Launcher.getLauncher(context);
+ mActivityContext = ActivityContext.lookupContext(context);
mLongPressHelper = new CheckLongPressHelper(this, this);
- setAccessibilityDelegate(mLauncher.getAccessibilityDelegate());
+ setAccessibilityDelegate(mActivityContext.getAccessibilityDelegate());
setBackgroundResource(R.drawable.widget_internal_focus_bg);
if (Flags.enableFocusOutline()) {
setDefaultFocusHighlightEnabled(false);
}
- if (Themes.getAttrBoolean(mLauncher, R.attr.isWorkspaceDarkText)) {
+ if (Themes.getAttrBoolean(context, R.attr.isWorkspaceDarkText)) {
setOnLightBackground(true);
}
mColorExtractor = new LocalColorExtractor(); // no-op
@@ -120,8 +120,7 @@
@Override
public boolean onLongClick(View view) {
if (mIsScrollable) {
- DragLayer dragLayer = mLauncher.getDragLayer();
- dragLayer.requestDisallowInterceptTouchEvent(false);
+ mActivityContext.getDragLayer().requestDisallowInterceptTouchEvent(false);
}
view.performLongClick();
return true;
@@ -218,7 +217,7 @@
public boolean onInterceptTouchEvent(MotionEvent ev) {
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
- DragLayer dragLayer = mLauncher.getDragLayer();
+ BaseDragLayer dragLayer = mActivityContext.getDragLayer();
if (mIsScrollable) {
dragLayer.requestDisallowInterceptTouchEvent(true);
}
diff --git a/src/com/android/launcher3/widget/LauncherWidgetHolder.java b/src/com/android/launcher3/widget/LauncherWidgetHolder.java
index 23127b3..15bd6ed 100644
--- a/src/com/android/launcher3/widget/LauncherWidgetHolder.java
+++ b/src/com/android/launcher3/widget/LauncherWidgetHolder.java
@@ -17,7 +17,9 @@
import static android.app.Activity.RESULT_CANCELED;
+import static com.android.launcher3.Flags.enableWorkspaceInflation;
import static com.android.launcher3.util.Executors.MAIN_EXECUTOR;
+import static com.android.launcher3.widget.LauncherAppWidgetProviderInfo.fromProviderInfo;
import android.appwidget.AppWidgetHost;
import android.appwidget.AppWidgetHostView;
@@ -27,6 +29,7 @@
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
+import android.os.Looper;
import android.util.SparseArray;
import android.widget.Toast;
@@ -310,7 +313,9 @@
}
/**
- * Create a view for the specified app widget
+ * Create a view for the specified app widget. When calling this method from a background
+ * thread, the returned view will not receive ongoing updates. The caller needs to reattach
+ * the view using {@link #attachViewToHostAndGetAttachedView} on UIThread
*
* @param appWidgetId The ID of the widget
* @param appWidget The {@link LauncherAppWidgetProviderInfo} of the widget
@@ -327,7 +332,55 @@
}
LauncherAppWidgetHostView view = createViewInternal(appWidgetId, appWidget);
- mViews.put(appWidgetId, view);
+ // Do not update mViews on a background thread call, as the holder is not thread safe.
+ if (!enableWorkspaceInflation() || Looper.myLooper() == Looper.getMainLooper()) {
+ mViews.put(appWidgetId, view);
+ }
+ return view;
+ }
+
+ /**
+ * Attaches an already inflated view to the host. If the view can't be attached, creates
+ * and attaches a new view.
+ * @return the final attached view
+ */
+ @NonNull
+ public final AppWidgetHostView attachViewToHostAndGetAttachedView(
+ @NonNull LauncherAppWidgetHostView view) {
+ if (mViews.get(view.getAppWidgetId()) != view) {
+ view = recycleExistingView(view);
+ mViews.put(view.getAppWidgetId(), view);
+ }
+ return view;
+ }
+
+ /**
+ * Recycling logic:
+ * 1) If the final view should be a pendingView
+ * if the provided view is also a pendingView, return itself
+ * otherwise discard provided view and return a new pending view
+ * 2) If the recycled view is a pendingView, discard it and return a new view
+ * 3) Use the same for as creating a new view, but used the provided view in the host instead
+ * of creating a new view. This ensures that all the host callbacks are properly attached
+ * as a result of using the same flow.
+ */
+ protected LauncherAppWidgetHostView recycleExistingView(LauncherAppWidgetHostView view) {
+ if ((mFlags & FLAG_LISTENING) == 0) {
+ if (view instanceof PendingAppWidgetHostView pv && pv.isDeferredWidget()) {
+ return view;
+ } else {
+ return new PendingAppWidgetHostView(mContext, this, view.getAppWidgetId(),
+ fromProviderInfo(mContext, view.getAppWidgetInfo()));
+ }
+ }
+ LauncherAppWidgetHost host = (LauncherAppWidgetHost) mWidgetHost;
+ if (view instanceof ListenableHostView lhv) {
+ host.recycleViewForNextCreation(lhv);
+ }
+
+ view = createViewInternal(
+ view.getAppWidgetId(), fromProviderInfo(mContext, view.getAppWidgetInfo()));
+ host.recycleViewForNextCreation(null);
return view;
}
@@ -338,8 +391,15 @@
// Since the launcher hasn't started listening to widget updates, we can't simply call
// host.createView here because the later will make a binder call to retrieve
// RemoteViews from system process.
- return new PendingAppWidgetHostView(mContext, appWidgetId, appWidget);
+ return new PendingAppWidgetHostView(mContext, this, appWidgetId, appWidget);
} else {
+ if (enableWorkspaceInflation() && Looper.myLooper() != Looper.getMainLooper()) {
+ // Widget is being inflated a background thread, just create and
+ // return a placeholder view
+ ListenableHostView hostView = new ListenableHostView(mContext);
+ hostView.setAppWidget(appWidgetId, appWidget);
+ return hostView;
+ }
try {
return (LauncherAppWidgetHostView) mWidgetHost.createView(
mContext, appWidgetId, appWidget);
diff --git a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
index adf85c7..86400ba 100644
--- a/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
+++ b/src/com/android/launcher3/widget/PendingAppWidgetHostView.java
@@ -50,6 +50,7 @@
import androidx.annotation.Nullable;
import com.android.launcher3.DeviceProfile;
+import com.android.launcher3.Launcher;
import com.android.launcher3.LauncherAppState;
import com.android.launcher3.R;
import com.android.launcher3.icons.FastBitmapDrawable;
@@ -65,7 +66,7 @@
public class PendingAppWidgetHostView extends LauncherAppWidgetHostView
implements OnClickListener, ItemInfoUpdateReceiver {
private static final float SETUP_ICON_SIZE_FACTOR = 2f / 5;
- private static final float MIN_SATUNATION = 0.7f;
+ private static final float MIN_SATURATION = 0.7f;
private static final int FLAG_DRAW_SETTINGS = 1;
private static final int FLAG_DRAW_ICON = 2;
@@ -75,6 +76,7 @@
private final Rect mRect = new Rect();
+ private final LauncherWidgetHolder mWidgetHolder;
private final LauncherAppWidgetProviderInfo mAppwidget;
private final LauncherAppWidgetInfo mInfo;
private final int mStartState;
@@ -90,6 +92,7 @@
private Drawable mSettingIconDrawable;
private boolean mDrawableSizeChanged;
+ private boolean mIsDeferredWidget;
private final TextPaint mPaint;
@@ -98,13 +101,13 @@
@Nullable private Bitmap mPreviewBitmap;
- public PendingAppWidgetHostView(Context context, LauncherAppWidgetInfo info,
- @Nullable LauncherAppWidgetProviderInfo appWidget) {
- this(context, info, appWidget,
+ public PendingAppWidgetHostView(Context context, LauncherWidgetHolder widgetHolder,
+ LauncherAppWidgetInfo info, @Nullable LauncherAppWidgetProviderInfo appWidget) {
+ this(context, widgetHolder, info, appWidget,
context.getResources().getText(R.string.gadget_complete_setup_text));
super.updateAppWidget(null);
- setOnClickListener(mLauncher.getItemOnClickListener());
+ setOnClickListener(mActivityContext.getItemOnClickListener());
if (info.pendingItemInfo == null) {
info.pendingItemInfo = new PackageItemInfo(info.providerName.getPackageName(),
@@ -117,14 +120,16 @@
}
public PendingAppWidgetHostView(
- Context context, int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget) {
- this(context, new LauncherAppWidgetInfo(appWidgetId, appWidget.provider),
+ Context context, LauncherWidgetHolder widgetHolder,
+ int appWidgetId, @NonNull LauncherAppWidgetProviderInfo appWidget) {
+ this(context, widgetHolder, new LauncherAppWidgetInfo(appWidgetId, appWidget.provider),
appWidget, appWidget.label);
getBackground().mutate().setAlpha(DEFERRED_ALPHA);
mCenterDrawable = new ColorDrawable(Color.TRANSPARENT);
mDragFlags = FLAG_DRAW_LABEL;
mDrawableSizeChanged = true;
+ mIsDeferredWidget = true;
}
/** Set {@link Bitmap} of widget preview. */
@@ -136,10 +141,11 @@
invalidate();
}
- private PendingAppWidgetHostView(Context context, LauncherAppWidgetInfo info,
+ private PendingAppWidgetHostView(Context context,
+ LauncherWidgetHolder widgetHolder, LauncherAppWidgetInfo info,
LauncherAppWidgetProviderInfo appwidget, CharSequence label) {
super(new ContextThemeWrapper(context, R.style.WidgetContainerTheme));
-
+ mWidgetHolder = widgetHolder;
mAppwidget = appwidget;
mInfo = info;
mStartState = info.restoreStatus;
@@ -148,9 +154,12 @@
mPaint = new TextPaint();
mPaint.setColor(Themes.getAttrColor(getContext(), android.R.attr.textColorPrimary));
- mPaint.setTextSize(TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_PX,
- mLauncher.getDeviceProfile().iconTextSizePx, getResources().getDisplayMetrics()));
+ mPaint.setTextSize(TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_PX,
+ mActivityContext.getDeviceProfile().iconTextSizePx,
+ getResources().getDisplayMetrics()));
mPreviewPaint = new Paint(ANTI_ALIAS_FLAG | DITHER_FLAG | FILTER_BITMAP_FLAG);
+
setWillNotDraw(false);
setBackgroundResource(R.drawable.pending_widget_bg);
}
@@ -161,6 +170,11 @@
}
@Override
+ public int getAppWidgetId() {
+ return mInfo.appWidgetId;
+ }
+
+ @Override
public void updateAppWidget(RemoteViews remoteViews) {
checkIfRestored();
}
@@ -172,6 +186,10 @@
}
}
+ public boolean isDeferredWidget() {
+ return mIsDeferredWidget;
+ }
+
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
@@ -184,8 +202,8 @@
if (mOnDetachCleanup != null) {
mOnDetachCleanup.close();
}
- mOnDetachCleanup = mLauncher.getAppWidgetHolder()
- .addOnUpdateListener(mInfo.appWidgetId, mAppwidget, this::checkIfRestored);
+ mOnDetachCleanup = mWidgetHolder.addOnUpdateListener(
+ mInfo.appWidgetId, mAppwidget, this::checkIfRestored);
checkIfRestored();
}
}
@@ -211,11 +229,13 @@
// This occurs when LauncherAppWidgetHostView is used to render a preview layout.
return;
}
- // Remove and rebind the current widget (which was inflated in the wrong
- // orientation), but don't delete it from the database
- mLauncher.removeItem(this, info, false /* deleteFromDb */,
- "widget removed because of configuration change");
- mLauncher.bindAppWidget(info);
+ if (mActivityContext instanceof Launcher launcher) {
+ // Remove and rebind the current widget (which was inflated in the wrong
+ // orientation), but don't delete it from the database
+ launcher.removeItem(this, info, false /* deleteFromDb */,
+ "widget removed because of configuration change");
+ launcher.bindAppWidget(info);
+ }
}
@Override
@@ -303,7 +323,7 @@
// Make the dominant color bright.
float[] hsv = new float[3];
Color.colorToHSV(dominantColor, hsv);
- hsv[1] = Math.min(hsv[1], MIN_SATUNATION);
+ hsv[1] = Math.min(hsv[1], MIN_SATURATION);
hsv[2] = 1;
mSettingIconDrawable.setColorFilter(Color.HSVToColor(hsv), PorterDuff.Mode.SRC_IN);
}
@@ -344,7 +364,7 @@
}
private void updateDrawableBounds() {
- DeviceProfile grid = mLauncher.getDeviceProfile();
+ DeviceProfile grid = mActivityContext.getDeviceProfile();
int paddingTop = getPaddingTop();
int paddingBottom = getPaddingBottom();
int paddingLeft = getPaddingLeft();