/* * Copyright (C) 2008 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; import static com.android.launcher3.LauncherAppState.ACTION_FORCE_ROLOAD; import static com.android.launcher3.config.FeatureFlags.IS_DOGFOOD_BUILD; import android.content.BroadcastReceiver; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.ShortcutInfo; import android.os.Handler; import android.os.HandlerThread; import android.os.Looper; import android.os.Process; import android.os.UserHandle; import android.text.TextUtils; import android.util.Log; import android.util.Pair; import com.android.launcher3.compat.LauncherAppsCompat; import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo; import com.android.launcher3.compat.UserManagerCompat; import com.android.launcher3.icons.IconCache; import com.android.launcher3.icons.LauncherIcons; import com.android.launcher3.model.AddWorkspaceItemsTask; import com.android.launcher3.model.BaseModelUpdateTask; import com.android.launcher3.model.BgDataModel; import com.android.launcher3.model.CacheDataUpdatedTask; import com.android.launcher3.model.LoaderResults; import com.android.launcher3.model.LoaderTask; import com.android.launcher3.model.ModelWriter; import com.android.launcher3.model.PackageInstallStateChangedTask; import com.android.launcher3.model.PackageUpdatedTask; import com.android.launcher3.model.ShortcutsChangedTask; import com.android.launcher3.model.UserLockStateChangedTask; import com.android.launcher3.shortcuts.DeepShortcutManager; import com.android.launcher3.util.ComponentKey; import com.android.launcher3.util.IntArray; import com.android.launcher3.util.IntSparseArrayMap; import com.android.launcher3.util.ItemInfoMatcher; import com.android.launcher3.util.PackageUserKey; import com.android.launcher3.util.Preconditions; import com.android.launcher3.util.Thunk; import com.android.launcher3.util.ViewOnDrawExecutor; import com.android.launcher3.widget.WidgetListRowEntry; import java.io.FileDescriptor; import java.io.PrintWriter; import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.concurrent.CancellationException; import java.util.concurrent.Executor; import java.util.function.Supplier; import androidx.annotation.Nullable; /** * Maintains in-memory state of the Launcher. It is expected that there should be only one * LauncherModel object held in a static. Also provide APIs for updating the database state * for the Launcher. */ public class LauncherModel extends BroadcastReceiver implements LauncherAppsCompat.OnAppsChangedCallbackCompat { private static final boolean DEBUG_RECEIVER = false; static final String TAG = "Launcher.Model"; private final MainThreadExecutor mUiExecutor = new MainThreadExecutor(); @Thunk final LauncherAppState mApp; @Thunk final Object mLock = new Object(); @Thunk LoaderTask mLoaderTask; @Thunk boolean mIsLoaderTaskRunning; @Thunk static final HandlerThread sWorkerThread = new HandlerThread("launcher-loader"); private static final Looper mWorkerLooper; static { sWorkerThread.start(); mWorkerLooper = sWorkerThread.getLooper(); } @Thunk static final Handler sWorker = new Handler(mWorkerLooper); // Indicates whether the current model data is valid or not. // We start off with everything not loaded. After that, we assume that // our monitoring of the package manager provides all updates and we never // need to do a requery. This is only ever touched from the loader thread. private boolean mModelLoaded; public boolean isModelLoaded() { synchronized (mLock) { return mModelLoaded && mLoaderTask == null; } } @Thunk WeakReference mCallbacks; // < only access in worker thread > private final AllAppsList mBgAllAppsList; /** * All the static data should be accessed on the background thread, A lock should be acquired * on this object when accessing any data from this model. */ static final BgDataModel sBgDataModel = new BgDataModel(); // Runnable to check if the shortcuts permission has changed. private final Runnable mShortcutPermissionCheckRunnable = new Runnable() { @Override public void run() { if (mModelLoaded) { boolean hasShortcutHostPermission = DeepShortcutManager.getInstance(mApp.getContext()).hasHostPermission(); if (hasShortcutHostPermission != sBgDataModel.hasShortcutHostPermission) { forceReload(); } } } }; public interface Callbacks { public void rebindModel(); public int getCurrentWorkspaceScreen(); public void clearPendingBinds(); public void startBinding(); public void bindItems(List shortcuts, boolean forceAnimateIcons); public void bindScreens(IntArray orderedScreenIds); public void finishFirstPageBind(ViewOnDrawExecutor executor); public void finishBindingItems(int pageBoundFirst); public void bindAllApplications(ArrayList apps); public void bindAppsAddedOrUpdated(ArrayList apps); public void preAddApps(); public void bindAppsAdded(IntArray newScreens, ArrayList addNotAnimated, ArrayList addAnimated); public void bindPromiseAppProgressUpdated(PromiseAppInfo app); public void bindWorkspaceItemsChanged(ArrayList updated); public void bindWidgetsRestored(ArrayList widgets); public void bindRestoreItemsChange(HashSet updates); public void bindWorkspaceComponentsRemoved(ItemInfoMatcher matcher); public void bindAppInfosRemoved(ArrayList appInfos); public void bindAllWidgets(ArrayList widgets); public void onPageBoundSynchronously(int page); public void executeOnNextDraw(ViewOnDrawExecutor executor); public void bindDeepShortcutMap(HashMap deepShortcutMap); } LauncherModel(LauncherAppState app, IconCache iconCache, AppFilter appFilter) { mApp = app; mBgAllAppsList = new AllAppsList(iconCache, appFilter); } public void setPackageState(PackageInstallInfo installInfo) { enqueueModelUpdateTask(new PackageInstallStateChangedTask(installInfo)); } /** * Updates the icons and label of all pending icons for the provided package name. */ public void updateSessionDisplayInfo(final String packageName) { HashSet packages = new HashSet<>(); packages.add(packageName); enqueueModelUpdateTask(new CacheDataUpdatedTask( CacheDataUpdatedTask.OP_SESSION_UPDATE, Process.myUserHandle(), packages)); } /** * Adds the provided items to the workspace. */ public void addAndBindAddedWorkspaceItems(List> itemList) { Callbacks callbacks = getCallback(); if (callbacks != null) { callbacks.preAddApps(); } enqueueModelUpdateTask(new AddWorkspaceItemsTask(itemList)); } public ModelWriter getWriter(boolean hasVerticalHotseat, boolean verifyChanges) { return new ModelWriter(mApp.getContext(), this, sBgDataModel, hasVerticalHotseat, verifyChanges); } /** * Set this as the current Launcher activity object for the loader. */ public void initialize(Callbacks callbacks) { synchronized (mLock) { Preconditions.assertUIThread(); mCallbacks = new WeakReference<>(callbacks); } } @Override public void onPackageChanged(String packageName, UserHandle user) { int op = PackageUpdatedTask.OP_UPDATE; enqueueModelUpdateTask(new PackageUpdatedTask(op, user, packageName)); } public void onSessionFailure(String packageName, UserHandle user) { enqueueModelUpdateTask(new BaseModelUpdateTask() { @Override public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) { final IntSparseArrayMap removedIds = new IntSparseArrayMap<>(); synchronized (dataModel) { for (ItemInfo info : dataModel.itemsIdMap) { if (info instanceof WorkspaceItemInfo && ((WorkspaceItemInfo) info).hasPromiseIconUi() && user.equals(info.user) && info.getIntent() != null && TextUtils.equals(packageName, info.getIntent().getPackage())) { removedIds.put(info.id, true /* remove */); } } } if (!removedIds.isEmpty()) { deleteAndBindComponentsRemoved(ItemInfoMatcher.ofItemIds(removedIds, false)); } } }); } @Override public void onPackageRemoved(String packageName, UserHandle user) { onPackagesRemoved(user, packageName); } public void onPackagesRemoved(UserHandle user, String... packages) { int op = PackageUpdatedTask.OP_REMOVE; enqueueModelUpdateTask(new PackageUpdatedTask(op, user, packages)); } @Override public void onPackageAdded(String packageName, UserHandle user) { int op = PackageUpdatedTask.OP_ADD; enqueueModelUpdateTask(new PackageUpdatedTask(op, user, packageName)); } @Override public void onPackagesAvailable(String[] packageNames, UserHandle user, boolean replacing) { enqueueModelUpdateTask( new PackageUpdatedTask(PackageUpdatedTask.OP_UPDATE, user, packageNames)); } @Override public void onPackagesUnavailable(String[] packageNames, UserHandle user, boolean replacing) { if (!replacing) { enqueueModelUpdateTask(new PackageUpdatedTask( PackageUpdatedTask.OP_UNAVAILABLE, user, packageNames)); } } @Override public void onPackagesSuspended(String[] packageNames, UserHandle user) { enqueueModelUpdateTask(new PackageUpdatedTask( PackageUpdatedTask.OP_SUSPEND, user, packageNames)); } @Override public void onPackagesUnsuspended(String[] packageNames, UserHandle user) { enqueueModelUpdateTask(new PackageUpdatedTask( PackageUpdatedTask.OP_UNSUSPEND, user, packageNames)); } @Override public void onShortcutsChanged(String packageName, List shortcuts, UserHandle user) { enqueueModelUpdateTask(new ShortcutsChangedTask(packageName, shortcuts, user, true)); } public void updatePinnedShortcuts(String packageName, List shortcuts, UserHandle user) { enqueueModelUpdateTask(new ShortcutsChangedTask(packageName, shortcuts, user, false)); } /** * Call from the handler for ACTION_PACKAGE_ADDED, ACTION_PACKAGE_REMOVED and * ACTION_PACKAGE_CHANGED. */ @Override public void onReceive(Context context, Intent intent) { if (DEBUG_RECEIVER) Log.d(TAG, "onReceive intent=" + intent); final String action = intent.getAction(); if (Intent.ACTION_LOCALE_CHANGED.equals(action)) { // If we have changed locale we need to clear out the labels in all apps/workspace. forceReload(); } else if (Intent.ACTION_MANAGED_PROFILE_ADDED.equals(action) || Intent.ACTION_MANAGED_PROFILE_REMOVED.equals(action)) { UserManagerCompat.getInstance(context).enableAndResetCache(); forceReload(); } else if (Intent.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action) || Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action) || Intent.ACTION_MANAGED_PROFILE_UNLOCKED.equals(action)) { UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER); if (user != null) { if (Intent.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action) || Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action)) { enqueueModelUpdateTask(new PackageUpdatedTask( PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE, user)); } // ACTION_MANAGED_PROFILE_UNAVAILABLE sends the profile back to locked mode, so // we need to run the state change task again. if (Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action) || Intent.ACTION_MANAGED_PROFILE_UNLOCKED.equals(action)) { enqueueModelUpdateTask(new UserLockStateChangedTask(user)); } } } else if (IS_DOGFOOD_BUILD && ACTION_FORCE_ROLOAD.equals(action)) { Launcher l = (Launcher) getCallback(); l.reload(); } } public void forceReload() { forceReload(-1); } /** * Reloads the workspace items from the DB and re-binds the workspace. This should generally * not be called as DB updates are automatically followed by UI update * @param synchronousBindPage The page to bind first. Can pass -1 to use the current page. */ public void forceReload(int synchronousBindPage) { synchronized (mLock) { // Stop any existing loaders first, so they don't set mModelLoaded to true later stopLoader(); mModelLoaded = false; } // Start the loader if launcher is already running, otherwise the loader will run, // the next time launcher starts Callbacks callbacks = getCallback(); if (callbacks != null) { if (synchronousBindPage < 0) { synchronousBindPage = callbacks.getCurrentWorkspaceScreen(); } startLoader(synchronousBindPage); } } public boolean isCurrentCallbacks(Callbacks callbacks) { return (mCallbacks != null && mCallbacks.get() == callbacks); } /** * Starts the loader. Tries to bind {@params synchronousBindPage} synchronously if possible. * @return true if the page could be bound synchronously. */ public boolean startLoader(int synchronousBindPage) { // Enable queue before starting loader. It will get disabled in Launcher#finishBindingItems InstallShortcutReceiver.enableInstallQueue(InstallShortcutReceiver.FLAG_LOADER_RUNNING); synchronized (mLock) { // Don't bother to start the thread if we know it's not going to do anything if (mCallbacks != null && mCallbacks.get() != null) { final Callbacks oldCallbacks = mCallbacks.get(); // Clear any pending bind-runnables from the synchronized load process. mUiExecutor.execute(oldCallbacks::clearPendingBinds); // If there is already one running, tell it to stop. stopLoader(); LoaderResults loaderResults = new LoaderResults(mApp, sBgDataModel, mBgAllAppsList, synchronousBindPage, mCallbacks); if (mModelLoaded && !mIsLoaderTaskRunning) { // Divide the set of loaded items into those that we are binding synchronously, // and everything else that is to be bound normally (asynchronously). loaderResults.bindWorkspace(); // For now, continue posting the binding of AllApps as there are other // issues that arise from that. loaderResults.bindAllApps(); loaderResults.bindDeepShortcuts(); loaderResults.bindWidgets(); return true; } else { startLoaderForResults(loaderResults); } } } return false; } /** * If there is already a loader task running, tell it to stop. */ public void stopLoader() { synchronized (mLock) { LoaderTask oldTask = mLoaderTask; mLoaderTask = null; if (oldTask != null) { oldTask.stopLocked(); } } } public void startLoaderForResults(LoaderResults results) { synchronized (mLock) { stopLoader(); mLoaderTask = new LoaderTask(mApp, mBgAllAppsList, sBgDataModel, results); // Always post the loader task, instead of running directly (even on same thread) so // that we exit any nested synchronized blocks sWorker.post(mLoaderTask); } } public void startLoaderForResultsIfNotLoaded(LoaderResults results) { synchronized (mLock) { if (!isModelLoaded()) { Log.d(TAG, "Workspace not loaded, loading now"); startLoaderForResults(results); } } } public void onInstallSessionCreated(final PackageInstallInfo sessionInfo) { enqueueModelUpdateTask(new BaseModelUpdateTask() { @Override public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) { apps.addPromiseApp(app.getContext(), sessionInfo); if (!apps.added.isEmpty()) { final ArrayList arrayList = new ArrayList<>(apps.added); apps.added.clear(); scheduleCallbackTask(new CallbackTask() { @Override public void execute(Callbacks callbacks) { callbacks.bindAppsAddedOrUpdated(arrayList); } }); } } }); } public class LoaderTransaction implements AutoCloseable { private final LoaderTask mTask; private LoaderTransaction(LoaderTask task) throws CancellationException { synchronized (mLock) { if (mLoaderTask != task) { throw new CancellationException("Loader already stopped"); } mTask = task; mIsLoaderTaskRunning = true; mModelLoaded = false; } } public void commit() { synchronized (mLock) { // Everything loaded bind the data. mModelLoaded = true; } } @Override public void close() { synchronized (mLock) { // If we are still the last one to be scheduled, remove ourselves. if (mLoaderTask == mTask) { mLoaderTask = null; } mIsLoaderTaskRunning = false; } } } public LoaderTransaction beginLoader(LoaderTask task) throws CancellationException { return new LoaderTransaction(task); } /** * Refreshes the cached shortcuts if the shortcut permission has changed. * Current implementation simply reloads the workspace, but it can be optimized to * use partial updates similar to {@link UserManagerCompat} */ public void refreshShortcutsIfRequired() { sWorker.removeCallbacks(mShortcutPermissionCheckRunnable); sWorker.post(mShortcutPermissionCheckRunnable); } /** * Called when the icons for packages have been updated in the icon cache. */ public void onPackageIconsUpdated(HashSet updatedPackages, UserHandle user) { // If any package icon has changed (app was updated while launcher was dead), // update the corresponding shortcuts. enqueueModelUpdateTask(new CacheDataUpdatedTask( CacheDataUpdatedTask.OP_CACHE_UPDATE, user, updatedPackages)); } /** * Called when the labels for the widgets has updated in the icon cache. */ public void onWidgetLabelsUpdated(HashSet updatedPackages, UserHandle user) { enqueueModelUpdateTask(new BaseModelUpdateTask() { @Override public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) { dataModel.widgetsModel.onPackageIconsUpdated(updatedPackages, user, app); bindUpdatedWidgets(dataModel); } }); } public void enqueueModelUpdateTask(ModelUpdateTask task) { task.init(mApp, this, sBgDataModel, mBgAllAppsList, mUiExecutor); if (sWorkerThread.getThreadId() == Process.myTid()) { task.run(); } else { // If we are not on the worker thread, then post to the worker handler sWorker.post(task); } } /** * A task to be executed on the current callbacks on the UI thread. * If there is no current callbacks, the task is ignored. */ public interface CallbackTask { void execute(Callbacks callbacks); } /** * A runnable which changes/updates the data model of the launcher based on certain events. */ public interface ModelUpdateTask extends Runnable { /** * Called before the task is posted to initialize the internal state. */ void init(LauncherAppState app, LauncherModel model, BgDataModel dataModel, AllAppsList allAppsList, Executor uiExecutor); } public void updateAndBindWorkspaceItem(WorkspaceItemInfo si, ShortcutInfo info) { updateAndBindWorkspaceItem(() -> { si.updateFromDeepShortcutInfo(info, mApp.getContext()); LauncherIcons li = LauncherIcons.obtain(mApp.getContext()); si.applyFrom(li.createShortcutIcon(info)); li.recycle(); return si; }); } /** * Utility method to update a shortcut on the background thread. */ public void updateAndBindWorkspaceItem(final Supplier itemProvider) { enqueueModelUpdateTask(new BaseModelUpdateTask() { @Override public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) { WorkspaceItemInfo info = itemProvider.get(); getModelWriter().updateItemInDatabase(info); ArrayList update = new ArrayList<>(); update.add(info); bindUpdatedWorkspaceItems(update); } }); } public void refreshAndBindWidgetsAndShortcuts(@Nullable final PackageUserKey packageUser) { enqueueModelUpdateTask(new BaseModelUpdateTask() { @Override public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) { dataModel.widgetsModel.update(app, packageUser); bindUpdatedWidgets(dataModel); } }); } public void dumpState(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { if (args.length > 0 && TextUtils.equals(args[0], "--all")) { writer.println(prefix + "All apps list: size=" + mBgAllAppsList.data.size()); for (AppInfo info : mBgAllAppsList.data) { writer.println(prefix + " title=\"" + info.title + "\" iconBitmap=" + info.iconBitmap + " componentName=" + info.componentName.getPackageName()); } } sBgDataModel.dump(prefix, fd, writer, args); } public Callbacks getCallback() { return mCallbacks != null ? mCallbacks.get() : null; } /** * @return the looper for the worker thread which can be used to start background tasks. */ public static Looper getWorkerLooper() { return mWorkerLooper; } public static void setWorkerPriority(final int priority) { Process.setThreadPriority(sWorkerThread.getThreadId(), priority); } }