1 /* 2 * Copyright (C) 2008 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package com.android.launcher3; 18 19 import static com.android.launcher3.LauncherAppState.ACTION_FORCE_ROLOAD; 20 import static com.android.launcher3.config.FeatureFlags.IS_DOGFOOD_BUILD; 21 import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; 22 import static com.android.launcher3.util.Executors.MODEL_EXECUTOR; 23 24 import android.content.BroadcastReceiver; 25 import android.content.Context; 26 import android.content.Intent; 27 import android.content.pm.ShortcutInfo; 28 import android.os.Process; 29 import android.os.UserHandle; 30 import android.text.TextUtils; 31 import android.util.Log; 32 import android.util.Pair; 33 34 import androidx.annotation.Nullable; 35 36 import com.android.launcher3.compat.LauncherAppsCompat; 37 import com.android.launcher3.compat.PackageInstallerCompat.PackageInstallInfo; 38 import com.android.launcher3.compat.UserManagerCompat; 39 import com.android.launcher3.icons.IconCache; 40 import com.android.launcher3.icons.LauncherIcons; 41 import com.android.launcher3.logging.FileLog; 42 import com.android.launcher3.model.AddWorkspaceItemsTask; 43 import com.android.launcher3.model.AllAppsList; 44 import com.android.launcher3.model.BaseModelUpdateTask; 45 import com.android.launcher3.model.BgDataModel; 46 import com.android.launcher3.model.BgDataModel.Callbacks; 47 import com.android.launcher3.model.CacheDataUpdatedTask; 48 import com.android.launcher3.model.LoaderResults; 49 import com.android.launcher3.model.LoaderTask; 50 import com.android.launcher3.model.ModelWriter; 51 import com.android.launcher3.model.PackageInstallStateChangedTask; 52 import com.android.launcher3.model.PackageUpdatedTask; 53 import com.android.launcher3.model.ShortcutsChangedTask; 54 import com.android.launcher3.model.UserLockStateChangedTask; 55 import com.android.launcher3.shortcuts.DeepShortcutManager; 56 import com.android.launcher3.util.IntSparseArrayMap; 57 import com.android.launcher3.util.ItemInfoMatcher; 58 import com.android.launcher3.util.PackageUserKey; 59 import com.android.launcher3.util.Preconditions; 60 import com.android.launcher3.util.Thunk; 61 62 import java.io.FileDescriptor; 63 import java.io.PrintWriter; 64 import java.lang.ref.WeakReference; 65 import java.util.ArrayList; 66 import java.util.HashSet; 67 import java.util.List; 68 import java.util.concurrent.CancellationException; 69 import java.util.concurrent.Executor; 70 import java.util.function.Supplier; 71 72 /** 73 * Maintains in-memory state of the Launcher. It is expected that there should be only one 74 * LauncherModel object held in a static. Also provide APIs for updating the database state 75 * for the Launcher. 76 */ 77 public class LauncherModel extends BroadcastReceiver 78 implements LauncherAppsCompat.OnAppsChangedCallbackCompat { 79 private static final boolean DEBUG_RECEIVER = false; 80 81 static final String TAG = "Launcher.Model"; 82 83 @Thunk final LauncherAppState mApp; 84 @Thunk final Object mLock = new Object(); 85 @Thunk 86 LoaderTask mLoaderTask; 87 @Thunk boolean mIsLoaderTaskRunning; 88 89 // Indicates whether the current model data is valid or not. 90 // We start off with everything not loaded. After that, we assume that 91 // our monitoring of the package manager provides all updates and we never 92 // need to do a requery. This is only ever touched from the loader thread. 93 private boolean mModelLoaded; isModelLoaded()94 public boolean isModelLoaded() { 95 synchronized (mLock) { 96 return mModelLoaded && mLoaderTask == null; 97 } 98 } 99 100 @Thunk WeakReference<Callbacks> mCallbacks; 101 102 // < only access in worker thread > 103 private final AllAppsList mBgAllAppsList; 104 105 /** 106 * All the static data should be accessed on the background thread, A lock should be acquired 107 * on this object when accessing any data from this model. 108 */ 109 static final BgDataModel sBgDataModel = new BgDataModel(); 110 111 // Runnable to check if the shortcuts permission has changed. 112 private final Runnable mShortcutPermissionCheckRunnable = new Runnable() { 113 @Override 114 public void run() { 115 if (mModelLoaded) { 116 boolean hasShortcutHostPermission = 117 DeepShortcutManager.getInstance(mApp.getContext()).hasHostPermission(); 118 if (hasShortcutHostPermission != sBgDataModel.hasShortcutHostPermission) { 119 forceReload(); 120 } 121 } 122 } 123 }; 124 LauncherModel(LauncherAppState app, IconCache iconCache, AppFilter appFilter)125 LauncherModel(LauncherAppState app, IconCache iconCache, AppFilter appFilter) { 126 mApp = app; 127 mBgAllAppsList = new AllAppsList(iconCache, appFilter); 128 } 129 setPackageState(PackageInstallInfo installInfo)130 public void setPackageState(PackageInstallInfo installInfo) { 131 enqueueModelUpdateTask(new PackageInstallStateChangedTask(installInfo)); 132 } 133 134 /** 135 * Updates the icons and label of all pending icons for the provided package name. 136 */ updateSessionDisplayInfo(final String packageName, final UserHandle user)137 public void updateSessionDisplayInfo(final String packageName, final UserHandle user) { 138 HashSet<String> packages = new HashSet<>(); 139 packages.add(packageName); 140 enqueueModelUpdateTask(new CacheDataUpdatedTask( 141 CacheDataUpdatedTask.OP_SESSION_UPDATE, user, packages)); 142 } 143 144 /** 145 * Adds the provided items to the workspace. 146 */ addAndBindAddedWorkspaceItems(List<Pair<ItemInfo, Object>> itemList)147 public void addAndBindAddedWorkspaceItems(List<Pair<ItemInfo, Object>> itemList) { 148 Callbacks callbacks = getCallback(); 149 if (callbacks != null) { 150 callbacks.preAddApps(); 151 } 152 enqueueModelUpdateTask(new AddWorkspaceItemsTask(itemList)); 153 } 154 getWriter(boolean hasVerticalHotseat, boolean verifyChanges)155 public ModelWriter getWriter(boolean hasVerticalHotseat, boolean verifyChanges) { 156 return new ModelWriter(mApp.getContext(), this, sBgDataModel, 157 hasVerticalHotseat, verifyChanges); 158 } 159 160 /** 161 * Set this as the current Launcher activity object for the loader. 162 */ initialize(Callbacks callbacks)163 public void initialize(Callbacks callbacks) { 164 synchronized (mLock) { 165 Preconditions.assertUIThread(); 166 mCallbacks = new WeakReference<>(callbacks); 167 } 168 } 169 170 @Override onPackageChanged(String packageName, UserHandle user)171 public void onPackageChanged(String packageName, UserHandle user) { 172 int op = PackageUpdatedTask.OP_UPDATE; 173 enqueueModelUpdateTask(new PackageUpdatedTask(op, user, packageName)); 174 } 175 onSessionFailure(String packageName, UserHandle user)176 public void onSessionFailure(String packageName, UserHandle user) { 177 enqueueModelUpdateTask(new BaseModelUpdateTask() { 178 @Override 179 public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) { 180 final IntSparseArrayMap<Boolean> removedIds = new IntSparseArrayMap<>(); 181 synchronized (dataModel) { 182 for (ItemInfo info : dataModel.itemsIdMap) { 183 if (info instanceof WorkspaceItemInfo 184 && ((WorkspaceItemInfo) info).hasPromiseIconUi() 185 && user.equals(info.user) 186 && info.getIntent() != null 187 && TextUtils.equals(packageName, info.getIntent().getPackage())) { 188 removedIds.put(info.id, true /* remove */); 189 } 190 } 191 } 192 193 if (!removedIds.isEmpty()) { 194 deleteAndBindComponentsRemoved(ItemInfoMatcher.ofItemIds(removedIds, false)); 195 } 196 } 197 }); 198 } 199 200 @Override onPackageRemoved(String packageName, UserHandle user)201 public void onPackageRemoved(String packageName, UserHandle user) { 202 onPackagesRemoved(user, packageName); 203 } 204 onPackagesRemoved(UserHandle user, String... packages)205 public void onPackagesRemoved(UserHandle user, String... packages) { 206 int op = PackageUpdatedTask.OP_REMOVE; 207 FileLog.d(TAG, "package removed received " + String.join("," + packages)); 208 enqueueModelUpdateTask(new PackageUpdatedTask(op, user, packages)); 209 } 210 211 @Override onPackageAdded(String packageName, UserHandle user)212 public void onPackageAdded(String packageName, UserHandle user) { 213 int op = PackageUpdatedTask.OP_ADD; 214 enqueueModelUpdateTask(new PackageUpdatedTask(op, user, packageName)); 215 } 216 217 @Override onPackagesAvailable(String[] packageNames, UserHandle user, boolean replacing)218 public void onPackagesAvailable(String[] packageNames, UserHandle user, 219 boolean replacing) { 220 enqueueModelUpdateTask( 221 new PackageUpdatedTask(PackageUpdatedTask.OP_UPDATE, user, packageNames)); 222 } 223 224 @Override onPackagesUnavailable(String[] packageNames, UserHandle user, boolean replacing)225 public void onPackagesUnavailable(String[] packageNames, UserHandle user, 226 boolean replacing) { 227 if (!replacing) { 228 enqueueModelUpdateTask(new PackageUpdatedTask( 229 PackageUpdatedTask.OP_UNAVAILABLE, user, packageNames)); 230 } 231 } 232 233 @Override onPackagesSuspended(String[] packageNames, UserHandle user)234 public void onPackagesSuspended(String[] packageNames, UserHandle user) { 235 enqueueModelUpdateTask(new PackageUpdatedTask( 236 PackageUpdatedTask.OP_SUSPEND, user, packageNames)); 237 } 238 239 @Override onPackagesUnsuspended(String[] packageNames, UserHandle user)240 public void onPackagesUnsuspended(String[] packageNames, UserHandle user) { 241 enqueueModelUpdateTask(new PackageUpdatedTask( 242 PackageUpdatedTask.OP_UNSUSPEND, user, packageNames)); 243 } 244 245 @Override onShortcutsChanged(String packageName, List<ShortcutInfo> shortcuts, UserHandle user)246 public void onShortcutsChanged(String packageName, List<ShortcutInfo> shortcuts, 247 UserHandle user) { 248 enqueueModelUpdateTask(new ShortcutsChangedTask(packageName, shortcuts, user, true)); 249 } 250 updatePinnedShortcuts(String packageName, List<ShortcutInfo> shortcuts, UserHandle user)251 public void updatePinnedShortcuts(String packageName, List<ShortcutInfo> shortcuts, 252 UserHandle user) { 253 enqueueModelUpdateTask(new ShortcutsChangedTask(packageName, shortcuts, user, false)); 254 } 255 256 /** 257 * Call from the handler for ACTION_PACKAGE_ADDED, ACTION_PACKAGE_REMOVED and 258 * ACTION_PACKAGE_CHANGED. 259 */ 260 @Override onReceive(Context context, Intent intent)261 public void onReceive(Context context, Intent intent) { 262 if (DEBUG_RECEIVER) Log.d(TAG, "onReceive intent=" + intent); 263 final String action = intent.getAction(); 264 if (Intent.ACTION_LOCALE_CHANGED.equals(action)) { 265 // If we have changed locale we need to clear out the labels in all apps/workspace. 266 forceReload(); 267 } else if (Intent.ACTION_MANAGED_PROFILE_ADDED.equals(action) 268 || Intent.ACTION_MANAGED_PROFILE_REMOVED.equals(action)) { 269 UserManagerCompat.getInstance(context).enableAndResetCache(); 270 forceReload(); 271 } else if (Intent.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action) || 272 Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action) || 273 Intent.ACTION_MANAGED_PROFILE_UNLOCKED.equals(action)) { 274 UserHandle user = intent.getParcelableExtra(Intent.EXTRA_USER); 275 if (user != null) { 276 if (Intent.ACTION_MANAGED_PROFILE_AVAILABLE.equals(action) || 277 Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action)) { 278 enqueueModelUpdateTask(new PackageUpdatedTask( 279 PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE, user)); 280 } 281 282 // ACTION_MANAGED_PROFILE_UNAVAILABLE sends the profile back to locked mode, so 283 // we need to run the state change task again. 284 if (Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE.equals(action) || 285 Intent.ACTION_MANAGED_PROFILE_UNLOCKED.equals(action)) { 286 enqueueModelUpdateTask(new UserLockStateChangedTask(user)); 287 } 288 } 289 } else if (IS_DOGFOOD_BUILD && ACTION_FORCE_ROLOAD.equals(action)) { 290 Launcher l = (Launcher) getCallback(); 291 l.reload(); 292 } 293 } 294 forceReload()295 public void forceReload() { 296 forceReload(-1); 297 } 298 299 /** 300 * Reloads the workspace items from the DB and re-binds the workspace. This should generally 301 * not be called as DB updates are automatically followed by UI update 302 * @param synchronousBindPage The page to bind first. Can pass -1 to use the current page. 303 */ forceReload(int synchronousBindPage)304 public void forceReload(int synchronousBindPage) { 305 synchronized (mLock) { 306 // Stop any existing loaders first, so they don't set mModelLoaded to true later 307 stopLoader(); 308 mModelLoaded = false; 309 } 310 311 // Start the loader if launcher is already running, otherwise the loader will run, 312 // the next time launcher starts 313 Callbacks callbacks = getCallback(); 314 if (callbacks != null) { 315 if (synchronousBindPage < 0) { 316 synchronousBindPage = callbacks.getCurrentWorkspaceScreen(); 317 } 318 startLoader(synchronousBindPage); 319 } 320 } 321 isCurrentCallbacks(Callbacks callbacks)322 public boolean isCurrentCallbacks(Callbacks callbacks) { 323 return (mCallbacks != null && mCallbacks.get() == callbacks); 324 } 325 326 /** 327 * Starts the loader. Tries to bind {@params synchronousBindPage} synchronously if possible. 328 * @return true if the page could be bound synchronously. 329 */ startLoader(int synchronousBindPage)330 public boolean startLoader(int synchronousBindPage) { 331 // Enable queue before starting loader. It will get disabled in Launcher#finishBindingItems 332 InstallShortcutReceiver.enableInstallQueue(InstallShortcutReceiver.FLAG_LOADER_RUNNING); 333 synchronized (mLock) { 334 // Don't bother to start the thread if we know it's not going to do anything 335 if (mCallbacks != null && mCallbacks.get() != null) { 336 final Callbacks oldCallbacks = mCallbacks.get(); 337 // Clear any pending bind-runnables from the synchronized load process. 338 MAIN_EXECUTOR.execute(oldCallbacks::clearPendingBinds); 339 340 // If there is already one running, tell it to stop. 341 stopLoader(); 342 LoaderResults loaderResults = new LoaderResults(mApp, sBgDataModel, 343 mBgAllAppsList, synchronousBindPage, mCallbacks); 344 if (mModelLoaded && !mIsLoaderTaskRunning) { 345 // Divide the set of loaded items into those that we are binding synchronously, 346 // and everything else that is to be bound normally (asynchronously). 347 loaderResults.bindWorkspace(); 348 // For now, continue posting the binding of AllApps as there are other 349 // issues that arise from that. 350 loaderResults.bindAllApps(); 351 loaderResults.bindDeepShortcuts(); 352 loaderResults.bindWidgets(); 353 return true; 354 } else { 355 startLoaderForResults(loaderResults); 356 } 357 } 358 } 359 return false; 360 } 361 362 /** 363 * If there is already a loader task running, tell it to stop. 364 */ stopLoader()365 public void stopLoader() { 366 synchronized (mLock) { 367 LoaderTask oldTask = mLoaderTask; 368 mLoaderTask = null; 369 if (oldTask != null) { 370 oldTask.stopLocked(); 371 } 372 } 373 } 374 startLoaderForResults(LoaderResults results)375 public void startLoaderForResults(LoaderResults results) { 376 synchronized (mLock) { 377 stopLoader(); 378 mLoaderTask = new LoaderTask(mApp, mBgAllAppsList, sBgDataModel, results); 379 380 // Always post the loader task, instead of running directly (even on same thread) so 381 // that we exit any nested synchronized blocks 382 MODEL_EXECUTOR.post(mLoaderTask); 383 } 384 } 385 startLoaderForResultsIfNotLoaded(LoaderResults results)386 public void startLoaderForResultsIfNotLoaded(LoaderResults results) { 387 synchronized (mLock) { 388 if (!isModelLoaded()) { 389 Log.d(TAG, "Workspace not loaded, loading now"); 390 startLoaderForResults(results); 391 } 392 } 393 } 394 onInstallSessionCreated(final PackageInstallInfo sessionInfo)395 public void onInstallSessionCreated(final PackageInstallInfo sessionInfo) { 396 enqueueModelUpdateTask(new BaseModelUpdateTask() { 397 @Override 398 public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) { 399 apps.addPromiseApp(app.getContext(), sessionInfo); 400 bindApplicationsIfNeeded(); 401 } 402 }); 403 } 404 405 public class LoaderTransaction implements AutoCloseable { 406 407 private final LoaderTask mTask; 408 LoaderTransaction(LoaderTask task)409 private LoaderTransaction(LoaderTask task) throws CancellationException { 410 synchronized (mLock) { 411 if (mLoaderTask != task) { 412 throw new CancellationException("Loader already stopped"); 413 } 414 mTask = task; 415 mIsLoaderTaskRunning = true; 416 mModelLoaded = false; 417 } 418 } 419 commit()420 public void commit() { 421 synchronized (mLock) { 422 // Everything loaded bind the data. 423 mModelLoaded = true; 424 } 425 } 426 427 @Override close()428 public void close() { 429 synchronized (mLock) { 430 // If we are still the last one to be scheduled, remove ourselves. 431 if (mLoaderTask == mTask) { 432 mLoaderTask = null; 433 } 434 mIsLoaderTaskRunning = false; 435 } 436 } 437 } 438 beginLoader(LoaderTask task)439 public LoaderTransaction beginLoader(LoaderTask task) throws CancellationException { 440 return new LoaderTransaction(task); 441 } 442 443 /** 444 * Refreshes the cached shortcuts if the shortcut permission has changed. 445 * Current implementation simply reloads the workspace, but it can be optimized to 446 * use partial updates similar to {@link UserManagerCompat} 447 */ refreshShortcutsIfRequired()448 public void refreshShortcutsIfRequired() { 449 MODEL_EXECUTOR.getHandler().removeCallbacks(mShortcutPermissionCheckRunnable); 450 MODEL_EXECUTOR.post(mShortcutPermissionCheckRunnable); 451 } 452 453 /** 454 * Called when the icons for packages have been updated in the icon cache. 455 */ onPackageIconsUpdated(HashSet<String> updatedPackages, UserHandle user)456 public void onPackageIconsUpdated(HashSet<String> updatedPackages, UserHandle user) { 457 // If any package icon has changed (app was updated while launcher was dead), 458 // update the corresponding shortcuts. 459 enqueueModelUpdateTask(new CacheDataUpdatedTask( 460 CacheDataUpdatedTask.OP_CACHE_UPDATE, user, updatedPackages)); 461 } 462 463 /** 464 * Called when the labels for the widgets has updated in the icon cache. 465 */ onWidgetLabelsUpdated(HashSet<String> updatedPackages, UserHandle user)466 public void onWidgetLabelsUpdated(HashSet<String> updatedPackages, UserHandle user) { 467 enqueueModelUpdateTask(new BaseModelUpdateTask() { 468 @Override 469 public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) { 470 dataModel.widgetsModel.onPackageIconsUpdated(updatedPackages, user, app); 471 bindUpdatedWidgets(dataModel); 472 } 473 }); 474 } 475 enqueueModelUpdateTask(ModelUpdateTask task)476 public void enqueueModelUpdateTask(ModelUpdateTask task) { 477 task.init(mApp, this, sBgDataModel, mBgAllAppsList, MAIN_EXECUTOR); 478 MODEL_EXECUTOR.execute(task); 479 } 480 481 /** 482 * A task to be executed on the current callbacks on the UI thread. 483 * If there is no current callbacks, the task is ignored. 484 */ 485 public interface CallbackTask { 486 execute(Callbacks callbacks)487 void execute(Callbacks callbacks); 488 } 489 490 /** 491 * A runnable which changes/updates the data model of the launcher based on certain events. 492 */ 493 public interface ModelUpdateTask extends Runnable { 494 495 /** 496 * Called before the task is posted to initialize the internal state. 497 */ init(LauncherAppState app, LauncherModel model, BgDataModel dataModel, AllAppsList allAppsList, Executor uiExecutor)498 void init(LauncherAppState app, LauncherModel model, 499 BgDataModel dataModel, AllAppsList allAppsList, Executor uiExecutor); 500 501 } 502 updateAndBindWorkspaceItem(WorkspaceItemInfo si, ShortcutInfo info)503 public void updateAndBindWorkspaceItem(WorkspaceItemInfo si, ShortcutInfo info) { 504 updateAndBindWorkspaceItem(() -> { 505 si.updateFromDeepShortcutInfo(info, mApp.getContext()); 506 LauncherIcons li = LauncherIcons.obtain(mApp.getContext()); 507 si.applyFrom(li.createShortcutIcon(info)); 508 li.recycle(); 509 return si; 510 }); 511 } 512 513 /** 514 * Utility method to update a shortcut on the background thread. 515 */ updateAndBindWorkspaceItem(final Supplier<WorkspaceItemInfo> itemProvider)516 public void updateAndBindWorkspaceItem(final Supplier<WorkspaceItemInfo> itemProvider) { 517 enqueueModelUpdateTask(new BaseModelUpdateTask() { 518 @Override 519 public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) { 520 WorkspaceItemInfo info = itemProvider.get(); 521 getModelWriter().updateItemInDatabase(info); 522 ArrayList<WorkspaceItemInfo> update = new ArrayList<>(); 523 update.add(info); 524 bindUpdatedWorkspaceItems(update); 525 } 526 }); 527 } 528 refreshAndBindWidgetsAndShortcuts(@ullable final PackageUserKey packageUser)529 public void refreshAndBindWidgetsAndShortcuts(@Nullable final PackageUserKey packageUser) { 530 enqueueModelUpdateTask(new BaseModelUpdateTask() { 531 @Override 532 public void execute(LauncherAppState app, BgDataModel dataModel, AllAppsList apps) { 533 dataModel.widgetsModel.update(app, packageUser); 534 bindUpdatedWidgets(dataModel); 535 } 536 }); 537 } 538 dumpState(String prefix, FileDescriptor fd, PrintWriter writer, String[] args)539 public void dumpState(String prefix, FileDescriptor fd, PrintWriter writer, String[] args) { 540 if (args.length > 0 && TextUtils.equals(args[0], "--all")) { 541 writer.println(prefix + "All apps list: size=" + mBgAllAppsList.data.size()); 542 for (AppInfo info : mBgAllAppsList.data) { 543 writer.println(prefix + " title=\"" + info.title + "\" iconBitmap=" + info.iconBitmap 544 + " componentName=" + info.componentName.getPackageName()); 545 } 546 } 547 sBgDataModel.dump(prefix, fd, writer, args); 548 } 549 getCallback()550 public Callbacks getCallback() { 551 return mCallbacks != null ? mCallbacks.get() : null; 552 } 553 } 554