| /* |
| * Copyright (C) 2017 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.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; |
| import com.android.launcher3.model.data.AppInfo; |
| import com.android.launcher3.model.data.ItemInfo; |
| 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; |
| import com.android.launcher3.util.RunnableList; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.List; |
| import java.util.Map; |
| import java.util.Objects; |
| import java.util.Set; |
| import java.util.concurrent.Executor; |
| import java.util.stream.Collectors; |
| |
| /** |
| * Binds the results of {@link com.android.launcher3.model.LoaderTask} to the Callbacks objects. |
| */ |
| public abstract class BaseLauncherBinder { |
| |
| protected static final String TAG = "LauncherBinder"; |
| private static final int ITEMS_CHUNK = 6; // batch size for the workspace icons |
| |
| protected final LooperExecutor mUiExecutor; |
| |
| protected final LauncherAppState mApp; |
| protected final BgDataModel mBgDataModel; |
| private final AllAppsList mBgAllAppsList; |
| |
| final Callbacks[] mCallbacksList; |
| |
| private int mMyBindingId; |
| |
| public BaseLauncherBinder(LauncherAppState app, BgDataModel dataModel, |
| AllAppsList allAppsList, Callbacks[] callbacksList, LooperExecutor uiExecutor) { |
| mUiExecutor = uiExecutor; |
| mApp = app; |
| mBgDataModel = dataModel; |
| mBgAllAppsList = allAppsList; |
| mCallbacksList = callbacksList; |
| } |
| |
| /** |
| * Binds all loaded data to actual views on the main thread. |
| */ |
| public void bindWorkspace(boolean incrementBindId, boolean isBindSync) { |
| Trace.beginSection("BaseLauncherBinder#bindWorkspace"); |
| try { |
| if (FeatureFlags.ENABLE_WORKSPACE_LOADING_OPTIMIZATION.get()) { |
| DisjointWorkspaceBinder workspaceBinder = |
| initWorkspaceBinder(incrementBindId, mBgDataModel.collectWorkspaceScreens()); |
| workspaceBinder.bindCurrentWorkspacePages(isBindSync); |
| workspaceBinder.bindOtherWorkspacePages(); |
| } else { |
| bindWorkspaceAllAtOnce(incrementBindId, isBindSync); |
| } |
| } finally { |
| Trace.endSection(); |
| } |
| } |
| |
| /** |
| * Initializes the WorkspaceBinder for binding. |
| * |
| * @param incrementBindId this is used to stop previously started binding tasks that are |
| * obsolete but still queued. |
| * @param workspacePages this allows the Launcher to add the correct workspace screens. |
| */ |
| public DisjointWorkspaceBinder initWorkspaceBinder(boolean incrementBindId, |
| IntArray workspacePages) { |
| |
| synchronized (mBgDataModel) { |
| if (incrementBindId) { |
| mBgDataModel.lastBindId++; |
| mBgDataModel.lastLoadId = mApp.getModel().getLastLoadId(); |
| } |
| mMyBindingId = mBgDataModel.lastBindId; |
| return new DisjointWorkspaceBinder(workspacePages); |
| } |
| } |
| |
| private void bindWorkspaceAllAtOnce(boolean incrementBindId, boolean isBindSync) { |
| // Save a copy of all the bg-thread collections |
| ArrayList<ItemInfo> workspaceItems = new ArrayList<>(); |
| ArrayList<LauncherAppWidgetInfo> appWidgets = new ArrayList<>(); |
| final IntArray orderedScreenIds = new IntArray(); |
| ArrayList<FixedContainerItems> extraItems = new ArrayList<>(); |
| final int workspaceItemCount; |
| synchronized (mBgDataModel) { |
| workspaceItems.addAll(mBgDataModel.workspaceItems); |
| appWidgets.addAll(mBgDataModel.appWidgets); |
| orderedScreenIds.addAll(mBgDataModel.collectWorkspaceScreens()); |
| mBgDataModel.extraItems.forEach(extraItems::add); |
| if (incrementBindId) { |
| mBgDataModel.lastBindId++; |
| mBgDataModel.lastLoadId = mApp.getModel().getLastLoadId(); |
| } |
| mMyBindingId = mBgDataModel.lastBindId; |
| workspaceItemCount = mBgDataModel.itemsIdMap.size(); |
| } |
| |
| for (Callbacks cb : mCallbacksList) { |
| new UnifiedWorkspaceBinder(cb, mUiExecutor, mApp, mBgDataModel, mMyBindingId, |
| workspaceItems, appWidgets, extraItems, orderedScreenIds) |
| .bind(isBindSync, workspaceItemCount); |
| } |
| } |
| |
| /** |
| * BindDeepShortcuts is abstract because it is a no-op for the go launcher. |
| */ |
| public abstract void bindDeepShortcuts(); |
| |
| /** |
| * Binds the all apps results from LoaderTask to the callbacks UX. |
| */ |
| public void bindAllApps() { |
| // shallow copy |
| AppInfo[] apps = mBgAllAppsList.copyData(); |
| int flags = mBgAllAppsList.getFlags(); |
| Map<PackageUserKey, Integer> packageUserKeytoUidMap = Arrays.stream(apps).collect( |
| Collectors.toMap( |
| appInfo -> new PackageUserKey(appInfo.componentName.getPackageName(), |
| appInfo.user), appInfo -> appInfo.uid, (a, b) -> a)); |
| executeCallbacksTask(c -> c.bindAllApplications(apps, flags, packageUserKeytoUidMap), |
| mUiExecutor); |
| } |
| |
| /** |
| * bindWidgets is abstract because it is a no-op for the go launcher. |
| */ |
| public abstract void bindWidgets(); |
| |
| /** |
| * bindWidgets is abstract because it is a no-op for the go launcher. |
| */ |
| public abstract void bindSmartspaceWidget(); |
| |
| /** |
| * Sorts the set of items by hotseat, workspace (spatially from top to bottom, left to right) |
| */ |
| protected void sortWorkspaceItemsSpatially(InvariantDeviceProfile profile, |
| ArrayList<ItemInfo> workspaceItems) { |
| final int screenCols = profile.numColumns; |
| final int screenCellCount = profile.numColumns * profile.numRows; |
| Collections.sort(workspaceItems, (lhs, rhs) -> { |
| if (lhs.container == rhs.container) { |
| // Within containers, order by their spatial position in that container |
| switch (lhs.container) { |
| case LauncherSettings.Favorites.CONTAINER_DESKTOP: { |
| int lr = (lhs.screenId * screenCellCount + lhs.cellY * screenCols |
| + lhs.cellX); |
| int rr = (rhs.screenId * screenCellCount + +rhs.cellY * screenCols |
| + rhs.cellX); |
| return Integer.compare(lr, rr); |
| } |
| case LauncherSettings.Favorites.CONTAINER_HOTSEAT: { |
| // We currently use the screen id as the rank |
| return Integer.compare(lhs.screenId, rhs.screenId); |
| } |
| default: |
| if (FeatureFlags.IS_STUDIO_BUILD) { |
| throw new RuntimeException( |
| "Unexpected container type when sorting workspace items."); |
| } |
| return 0; |
| } |
| } else { |
| // Between containers, order by hotseat, desktop |
| return Integer.compare(lhs.container, rhs.container); |
| } |
| }); |
| } |
| |
| protected void executeCallbacksTask(CallbackTask task, Executor executor) { |
| executor.execute(() -> { |
| if (mMyBindingId != mBgDataModel.lastBindId) { |
| Log.d(TAG, "Too many consecutive reloads, skipping obsolete data-bind"); |
| return; |
| } |
| for (Callbacks cb : mCallbacksList) { |
| task.execute(cb); |
| } |
| }); |
| } |
| |
| /** |
| * Only used in LoaderTask. |
| */ |
| public LooperIdleLock newIdleLock(Object lock) { |
| LooperIdleLock idleLock = new LooperIdleLock(lock, mUiExecutor.getLooper()); |
| // If we are not binding or if the main looper is already idle, there is no reason to wait |
| if (mUiExecutor.getLooper().getQueue().isIdle()) { |
| idleLock.queueIdle(); |
| } |
| return idleLock; |
| } |
| |
| private class UnifiedWorkspaceBinder { |
| |
| private final Executor mUiExecutor; |
| private final Callbacks mCallbacks; |
| |
| private final LauncherAppState mApp; |
| private final BgDataModel mBgDataModel; |
| |
| private final int mMyBindingId; |
| private final ArrayList<ItemInfo> mWorkspaceItems; |
| private final ArrayList<LauncherAppWidgetInfo> mAppWidgets; |
| private final IntArray mOrderedScreenIds; |
| private final ArrayList<FixedContainerItems> mExtraItems; |
| |
| UnifiedWorkspaceBinder(Callbacks callbacks, |
| Executor uiExecutor, |
| LauncherAppState app, |
| BgDataModel bgDataModel, |
| int myBindingId, |
| ArrayList<ItemInfo> workspaceItems, |
| ArrayList<LauncherAppWidgetInfo> appWidgets, |
| ArrayList<FixedContainerItems> extraItems, |
| IntArray orderedScreenIds) { |
| mCallbacks = callbacks; |
| mUiExecutor = uiExecutor; |
| mApp = app; |
| mBgDataModel = bgDataModel; |
| mMyBindingId = myBindingId; |
| mWorkspaceItems = workspaceItems; |
| mAppWidgets = appWidgets; |
| mExtraItems = extraItems; |
| mOrderedScreenIds = orderedScreenIds; |
| } |
| |
| private void bind(boolean isBindSync, int workspaceItemCount) { |
| final IntSet currentScreenIds = |
| mCallbacks.getPagesToBindSynchronously(mOrderedScreenIds); |
| Objects.requireNonNull(currentScreenIds, "Null screen ids provided by " + mCallbacks); |
| |
| // 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<ItemInfo> currentAppWidgets = new ArrayList<>(); |
| ArrayList<ItemInfo> otherAppWidgets = new ArrayList<>(); |
| |
| filterCurrentWorkspaceItems(currentScreenIds, mWorkspaceItems, currentWorkspaceItems, |
| otherWorkspaceItems); |
| filterCurrentWorkspaceItems(currentScreenIds, mAppWidgets, currentAppWidgets, |
| otherAppWidgets); |
| final InvariantDeviceProfile idp = mApp.getInvariantDeviceProfile(); |
| sortWorkspaceItemsSpatially(idp, currentWorkspaceItems); |
| sortWorkspaceItemsSpatially(idp, otherWorkspaceItems); |
| |
| // Tell the workspace that we're about to start binding items |
| executeCallbacksTask(c -> { |
| c.clearPendingBinds(); |
| c.startBinding(); |
| if (ENABLE_SMARTSPACE_REMOVAL.get()) { |
| c.setIsFirstPagePinnedItemEnabled( |
| mBgDataModel.isFirstPagePinnedItemEnabled); |
| } |
| }, mUiExecutor); |
| |
| // Bind workspace screens |
| executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor); |
| |
| // Load items on the current page. |
| 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)); |
| } |
| |
| RunnableList pendingTasks = new RunnableList(); |
| Executor pendingExecutor = pendingTasks::add; |
| |
| 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); |
| |
| executeCallbacksTask(c -> c.finishBindingItems(currentScreenIds), pendingExecutor); |
| pendingExecutor.execute( |
| () -> { |
| MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT); |
| ItemInstallQueue.INSTANCE.get(mApp.getContext()) |
| .resumeModelPush(FLAG_LOADER_RUNNING); |
| }); |
| } |
| |
| /** |
| * 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 += chunkCount) { |
| final int start = i; |
| final int chunkSize = (i + chunkCount <= count) ? chunkCount : (count - i); |
| executeCallbacksTask( |
| c -> c.bindItems(workspaceItems.subList(start, start + chunkSize), false), |
| executor); |
| } |
| } |
| |
| protected void executeCallbacksTask(CallbackTask task, Executor executor) { |
| executor.execute(() -> { |
| if (mMyBindingId != mBgDataModel.lastBindId) { |
| Log.d(TAG, "Too many consecutive reloads, skipping obsolete data-bind"); |
| return; |
| } |
| task.execute(mCallbacks); |
| }); |
| } |
| } |
| |
| private class DisjointWorkspaceBinder { |
| private final IntArray mOrderedScreenIds; |
| private final IntSet mCurrentScreenIds = new IntSet(); |
| private final Set<Integer> mBoundItemIds = new HashSet<>(); |
| |
| protected DisjointWorkspaceBinder(IntArray orderedScreenIds) { |
| mOrderedScreenIds = orderedScreenIds; |
| |
| for (Callbacks cb : mCallbacksList) { |
| mCurrentScreenIds.addAll(cb.getPagesToBindSynchronously(orderedScreenIds)); |
| } |
| if (mCurrentScreenIds.size() == 0) { |
| mCurrentScreenIds.add(Workspace.FIRST_SCREEN_ID); |
| } |
| } |
| |
| /** |
| * Binds the currently loaded items in the Data Model. Also signals to the Callbacks[] |
| * that these items have been bound and their respective screens are ready to be shown. |
| * |
| * If this method is called after all the items on the workspace screen have already been |
| * loaded, it will bind all workspace items immediately, and bindOtherWorkspacePages() will |
| * not bind any items. |
| */ |
| protected void bindCurrentWorkspacePages(boolean isBindSync) { |
| // Save a copy of all the bg-thread collections |
| ArrayList<ItemInfo> workspaceItems; |
| ArrayList<LauncherAppWidgetInfo> appWidgets; |
| ArrayList<FixedContainerItems> fciList = new ArrayList<>(); |
| final int workspaceItemCount; |
| synchronized (mBgDataModel) { |
| workspaceItems = new ArrayList<>(mBgDataModel.workspaceItems); |
| appWidgets = new ArrayList<>(mBgDataModel.appWidgets); |
| if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) { |
| mBgDataModel.extraItems.forEach(fciList::add); |
| } |
| workspaceItemCount = mBgDataModel.itemsIdMap.size(); |
| } |
| |
| workspaceItems.forEach(it -> mBoundItemIds.add(it.id)); |
| appWidgets.forEach(it -> mBoundItemIds.add(it.id)); |
| if (!FeatureFlags.CHANGE_MODEL_DELEGATE_LOADING_ORDER.get()) { |
| fciList.forEach(item -> |
| executeCallbacksTask(c -> c.bindExtraContainerItems(item), mUiExecutor)); |
| } |
| |
| sortWorkspaceItemsSpatially(mApp.getInvariantDeviceProfile(), workspaceItems); |
| |
| // Tell the workspace that we're about to start binding items |
| executeCallbacksTask(c -> { |
| c.clearPendingBinds(); |
| c.startBinding(); |
| }, mUiExecutor); |
| |
| // Bind workspace screens |
| executeCallbacksTask(c -> c.bindScreens(mOrderedScreenIds), mUiExecutor); |
| |
| bindWorkspaceItems(workspaceItems); |
| bindAppWidgets(appWidgets); |
| executeCallbacksTask(c -> { |
| MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND); |
| |
| RunnableList onCompleteSignal = new RunnableList(); |
| onCompleteSignal.executeAllAndDestroy(); |
| c.onInitialBindComplete(mCurrentScreenIds, new RunnableList(), onCompleteSignal, |
| workspaceItemCount, isBindSync); |
| }, mUiExecutor); |
| } |
| |
| protected void bindOtherWorkspacePages() { |
| // Save a copy of all the bg-thread collections |
| ArrayList<ItemInfo> workspaceItems; |
| ArrayList<LauncherAppWidgetInfo> appWidgets; |
| |
| synchronized (mBgDataModel) { |
| workspaceItems = new ArrayList<>(mBgDataModel.workspaceItems); |
| appWidgets = new ArrayList<>(mBgDataModel.appWidgets); |
| } |
| |
| workspaceItems.removeIf(it -> mBoundItemIds.contains(it.id)); |
| appWidgets.removeIf(it -> mBoundItemIds.contains(it.id)); |
| |
| sortWorkspaceItemsSpatially(mApp.getInvariantDeviceProfile(), workspaceItems); |
| |
| bindWorkspaceItems(workspaceItems); |
| bindAppWidgets(appWidgets); |
| |
| executeCallbacksTask(c -> c.finishBindingItems(mCurrentScreenIds), mUiExecutor); |
| mUiExecutor.execute(() -> { |
| MODEL_EXECUTOR.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT); |
| ItemInstallQueue.INSTANCE.get(mApp.getContext()) |
| .resumeModelPush(FLAG_LOADER_RUNNING); |
| }); |
| |
| StringCache cacheClone = mBgDataModel.stringCache.clone(); |
| executeCallbacksTask(c -> c.bindStringCache(cacheClone), mUiExecutor); |
| } |
| |
| private void bindWorkspaceItems(final ArrayList<ItemInfo> workspaceItems) { |
| // Bind the workspace items |
| int count = workspaceItems.size(); |
| for (int i = 0; i < count; i += ITEMS_CHUNK) { |
| final int start = i; |
| final int chunkSize = (i + ITEMS_CHUNK <= count) ? ITEMS_CHUNK : (count - i); |
| executeCallbacksTask( |
| c -> c.bindItems(workspaceItems.subList(start, start + chunkSize), false), |
| mUiExecutor); |
| } |
| } |
| |
| private void bindAppWidgets(List<LauncherAppWidgetInfo> appWidgets) { |
| // 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), |
| mUiExecutor); |
| } |
| } |
| } |
| } |