1 /* <lambda>null2 * 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 package com.android.launcher3 17 18 import android.app.admin.DevicePolicyManager 19 import android.content.Context 20 import android.content.Intent 21 import android.content.pm.ShortcutInfo 22 import android.os.UserHandle 23 import android.text.TextUtils 24 import android.util.Log 25 import android.util.Pair 26 import androidx.annotation.WorkerThread 27 import com.android.launcher3.celllayout.CellPosMapper 28 import com.android.launcher3.icons.IconCache 29 import com.android.launcher3.model.AddWorkspaceItemsTask 30 import com.android.launcher3.model.AllAppsList 31 import com.android.launcher3.model.BaseLauncherBinder 32 import com.android.launcher3.model.BgDataModel 33 import com.android.launcher3.model.CacheDataUpdatedTask 34 import com.android.launcher3.model.ItemInstallQueue 35 import com.android.launcher3.model.LoaderTask 36 import com.android.launcher3.model.ModelDbController 37 import com.android.launcher3.model.ModelDelegate 38 import com.android.launcher3.model.ModelLauncherCallbacks 39 import com.android.launcher3.model.ModelTaskController 40 import com.android.launcher3.model.ModelWriter 41 import com.android.launcher3.model.PackageUpdatedTask 42 import com.android.launcher3.model.ReloadStringCacheTask 43 import com.android.launcher3.model.ShortcutsChangedTask 44 import com.android.launcher3.model.UserLockStateChangedTask 45 import com.android.launcher3.model.WidgetsFilterDataProvider 46 import com.android.launcher3.model.data.ItemInfo 47 import com.android.launcher3.model.data.WorkspaceItemInfo 48 import com.android.launcher3.pm.UserCache 49 import com.android.launcher3.shortcuts.ShortcutRequest 50 import com.android.launcher3.testing.shared.TestProtocol.sDebugTracing 51 import com.android.launcher3.util.Executors.MAIN_EXECUTOR 52 import com.android.launcher3.util.Executors.MODEL_EXECUTOR 53 import com.android.launcher3.util.PackageManagerHelper 54 import com.android.launcher3.util.PackageUserKey 55 import com.android.launcher3.util.Preconditions 56 import java.io.FileDescriptor 57 import java.io.PrintWriter 58 import java.util.concurrent.CancellationException 59 import java.util.function.Consumer 60 61 /** 62 * Maintains in-memory state of the Launcher. It is expected that there should be only one 63 * LauncherModel object held in a static. Also provide APIs for updating the database state for the 64 * Launcher. 65 */ 66 class LauncherModel( 67 private val context: Context, 68 private val mApp: LauncherAppState, 69 private val iconCache: IconCache, 70 private val widgetsFilterDataProvider: WidgetsFilterDataProvider, 71 appFilter: AppFilter, 72 mPmHelper: PackageManagerHelper, 73 isPrimaryInstance: Boolean, 74 ) { 75 76 private val mCallbacksList = ArrayList<BgDataModel.Callbacks>(1) 77 78 // < only access in worker thread > 79 private val mBgAllAppsList = AllAppsList(iconCache, appFilter) 80 81 /** 82 * All the static data should be accessed on the background thread, A lock should be acquired on 83 * this object when accessing any data from this model. 84 */ 85 private val mBgDataModel = BgDataModel() 86 87 val modelDelegate: ModelDelegate = 88 ModelDelegate.newInstance( 89 context, 90 mApp, 91 mPmHelper, 92 mBgAllAppsList, 93 mBgDataModel, 94 isPrimaryInstance, 95 ) 96 97 val modelDbController = ModelDbController(context) 98 99 private val mLock = Any() 100 101 private var mLoaderTask: LoaderTask? = null 102 private var mIsLoaderTaskRunning = false 103 104 // only allow this once per reboot to reload work apps 105 private var mShouldReloadWorkProfile = true 106 107 // Indicates whether the current model data is valid or not. 108 // We start off with everything not loaded. After that, we assume that 109 // our monitoring of the package manager provides all updates and we never 110 // need to do a requery. This is only ever touched from the loader thread. 111 private var mModelLoaded = false 112 private var mModelDestroyed = false 113 114 fun isModelLoaded() = 115 synchronized(mLock) { mModelLoaded && mLoaderTask == null && !mModelDestroyed } 116 117 /** 118 * Returns the ID for the last model load. If the load ID doesn't match for a transaction, the 119 * transaction should be ignored. 120 */ 121 var lastLoadId: Int = -1 122 private set 123 124 // Runnable to check if the shortcuts permission has changed. 125 private val mDataValidationCheck = Runnable { 126 if (mModelLoaded) { 127 modelDelegate.validateData() 128 } 129 } 130 131 fun newModelCallbacks() = ModelLauncherCallbacks(this::enqueueModelUpdateTask) 132 133 /** Adds the provided items to the workspace. */ 134 fun addAndBindAddedWorkspaceItems(itemList: List<Pair<ItemInfo?, Any?>?>) { 135 callbacks.forEach { it.preAddApps() } 136 enqueueModelUpdateTask(AddWorkspaceItemsTask(itemList)) 137 } 138 139 fun getWriter( 140 verifyChanges: Boolean, 141 cellPosMapper: CellPosMapper?, 142 owner: BgDataModel.Callbacks?, 143 ) = ModelWriter(mApp.context, this, mBgDataModel, verifyChanges, cellPosMapper, owner) 144 145 /** Returns the [WidgetsFilterDataProvider] that manages widget filters. */ 146 fun getWidgetsFilterDataProvider(): WidgetsFilterDataProvider { 147 return widgetsFilterDataProvider 148 } 149 150 /** Called when the icon for an app changes, outside of package event */ 151 @WorkerThread 152 fun onAppIconChanged(packageName: String, user: UserHandle) { 153 // Update the icon for the calendar package 154 enqueueModelUpdateTask(PackageUpdatedTask(PackageUpdatedTask.OP_UPDATE, user, packageName)) 155 ShortcutRequest(context, user).forPackage(packageName).query(ShortcutRequest.PINNED).let { 156 if (it.isNotEmpty()) { 157 enqueueModelUpdateTask(ShortcutsChangedTask(packageName, it, user, false)) 158 } 159 } 160 } 161 162 /** Called when the workspace items have drastically changed */ 163 fun onWorkspaceUiChanged() { 164 MODEL_EXECUTOR.execute(modelDelegate::workspaceLoadComplete) 165 } 166 167 /** Called when the model is destroyed */ 168 fun destroy() { 169 mModelDestroyed = true 170 MODEL_EXECUTOR.execute { 171 modelDelegate.destroy() 172 widgetsFilterDataProvider.destroy() 173 } 174 } 175 176 fun onBroadcastIntent(intent: Intent) { 177 if (DEBUG_RECEIVER || sDebugTracing) Log.d(TAG, "onReceive intent=$intent") 178 when (intent.action) { 179 Intent.ACTION_LOCALE_CHANGED, 180 LauncherAppState.ACTION_FORCE_ROLOAD -> 181 // If we have changed locale we need to clear out the labels in all apps/workspace. 182 forceReload() 183 DevicePolicyManager.ACTION_DEVICE_POLICY_RESOURCE_UPDATED -> 184 enqueueModelUpdateTask(ReloadStringCacheTask(this.modelDelegate)) 185 } 186 } 187 188 /** 189 * Called then there use a user event 190 * 191 * @see UserCache.addUserEventListener 192 */ 193 fun onUserEvent(user: UserHandle, action: String) { 194 when (action) { 195 Intent.ACTION_MANAGED_PROFILE_AVAILABLE -> { 196 if (mShouldReloadWorkProfile) { 197 forceReload() 198 } else { 199 enqueueModelUpdateTask( 200 PackageUpdatedTask(PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE, user) 201 ) 202 } 203 mShouldReloadWorkProfile = false 204 } 205 Intent.ACTION_MANAGED_PROFILE_UNAVAILABLE -> { 206 mShouldReloadWorkProfile = false 207 enqueueModelUpdateTask( 208 PackageUpdatedTask(PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE, user) 209 ) 210 } 211 UserCache.ACTION_PROFILE_LOCKED -> 212 enqueueModelUpdateTask(UserLockStateChangedTask(user, false)) 213 UserCache.ACTION_PROFILE_UNLOCKED -> 214 enqueueModelUpdateTask(UserLockStateChangedTask(user, true)) 215 Intent.ACTION_MANAGED_PROFILE_REMOVED -> { 216 LauncherPrefs.get(mApp.context).put(LauncherPrefs.WORK_EDU_STEP, 0) 217 forceReload() 218 } 219 UserCache.ACTION_PROFILE_ADDED, 220 UserCache.ACTION_PROFILE_REMOVED -> forceReload() 221 UserCache.ACTION_PROFILE_AVAILABLE, 222 UserCache.ACTION_PROFILE_UNAVAILABLE -> { 223 // This broadcast is only available when android.os.Flags.allowPrivateProfile() is 224 // set. For Work-profile this broadcast will be sent in addition to 225 // ACTION_MANAGED_PROFILE_AVAILABLE/UNAVAILABLE. So effectively, this if block only 226 // handles the non-work profile case. 227 enqueueModelUpdateTask( 228 PackageUpdatedTask(PackageUpdatedTask.OP_USER_AVAILABILITY_CHANGE, user) 229 ) 230 } 231 } 232 } 233 234 /** 235 * Reloads the workspace items from the DB and re-binds the workspace. This should generally not 236 * be called as DB updates are automatically followed by UI update 237 */ 238 fun forceReload() { 239 synchronized(mLock) { 240 // Stop any existing loaders first, so they don't set mModelLoaded to true later 241 stopLoader() 242 mModelLoaded = false 243 } 244 rebindCallbacks() 245 } 246 247 /** Rebinds all existing callbacks with already loaded model */ 248 fun rebindCallbacks() { 249 if (hasCallbacks()) { 250 startLoader() 251 } 252 } 253 254 /** Removes an existing callback */ 255 fun removeCallbacks(callbacks: BgDataModel.Callbacks) { 256 synchronized(mCallbacksList) { 257 Preconditions.assertUIThread() 258 if (mCallbacksList.remove(callbacks)) { 259 if (stopLoader()) { 260 // Rebind existing callbacks 261 startLoader() 262 } 263 } 264 } 265 } 266 267 /** 268 * Adds a callbacks to receive model updates 269 * 270 * @return true if workspace load was performed synchronously 271 */ 272 fun addCallbacksAndLoad(callbacks: BgDataModel.Callbacks): Boolean { 273 synchronized(mLock) { 274 addCallbacks(callbacks) 275 return startLoader(arrayOf(callbacks)) 276 } 277 } 278 279 /** Adds a callbacks to receive model updates */ 280 fun addCallbacks(callbacks: BgDataModel.Callbacks) { 281 Preconditions.assertUIThread() 282 synchronized(mCallbacksList) { mCallbacksList.add(callbacks) } 283 } 284 285 /** 286 * Starts the loader. Tries to bind {@params synchronousBindPage} synchronously if possible. 287 * 288 * @return true if the page could be bound synchronously. 289 */ 290 fun startLoader() = startLoader(arrayOf()) 291 292 private fun startLoader(newCallbacks: Array<BgDataModel.Callbacks>): Boolean { 293 // Enable queue before starting loader. It will get disabled in Launcher#finishBindingItems 294 ItemInstallQueue.INSTANCE.get(context).pauseModelPush(ItemInstallQueue.FLAG_LOADER_RUNNING) 295 synchronized(mLock) { 296 // If there is already one running, tell it to stop. 297 val wasRunning = stopLoader() 298 val bindDirectly = mModelLoaded && !mIsLoaderTaskRunning 299 val bindAllCallbacks = wasRunning || !bindDirectly || newCallbacks.isEmpty() 300 val callbacksList = if (bindAllCallbacks) callbacks else newCallbacks 301 if (callbacksList.isNotEmpty()) { 302 // Clear any pending bind-runnables from the synchronized load process. 303 callbacksList.forEach { MAIN_EXECUTOR.execute(it::clearPendingBinds) } 304 305 val launcherBinder = 306 BaseLauncherBinder(mApp, mBgDataModel, mBgAllAppsList, callbacksList) 307 if (bindDirectly) { 308 // Divide the set of loaded items into those that we are binding synchronously, 309 // and everything else that is to be bound normally (asynchronously). 310 launcherBinder.bindWorkspace(bindAllCallbacks, /* isBindSync= */ true) 311 // For now, continue posting the binding of AllApps as there are other 312 // issues that arise from that. 313 launcherBinder.bindAllApps() 314 launcherBinder.bindDeepShortcuts() 315 launcherBinder.bindWidgets() 316 return true 317 } else { 318 mLoaderTask = 319 LoaderTask( 320 mApp, 321 mBgAllAppsList, 322 mBgDataModel, 323 this.modelDelegate, 324 launcherBinder, 325 widgetsFilterDataProvider, 326 ) 327 328 // Always post the loader task, instead of running directly 329 // (even on same thread) so that we exit any nested synchronized blocks 330 MODEL_EXECUTOR.post(mLoaderTask) 331 } 332 } 333 } 334 return false 335 } 336 337 /** 338 * If there is already a loader task running, tell it to stop. 339 * 340 * @return true if an existing loader was stopped. 341 */ 342 private fun stopLoader(): Boolean { 343 synchronized(mLock) { 344 val oldTask: LoaderTask? = mLoaderTask 345 mLoaderTask = null 346 if (oldTask != null) { 347 oldTask.stopLocked() 348 return true 349 } 350 return false 351 } 352 } 353 354 /** 355 * Loads the model if not loaded 356 * 357 * @param callback called with the data model upon successful load or null on model thread. 358 */ 359 fun loadAsync(callback: Consumer<BgDataModel?>) { 360 synchronized(mLock) { 361 if (!mModelLoaded && !mIsLoaderTaskRunning) { 362 startLoader() 363 } 364 } 365 MODEL_EXECUTOR.post { callback.accept(if (isModelLoaded()) mBgDataModel else null) } 366 } 367 368 inner class LoaderTransaction(task: LoaderTask) : AutoCloseable { 369 private var mTask: LoaderTask? = null 370 371 init { 372 synchronized(mLock) { 373 if (mLoaderTask !== task) { 374 throw CancellationException("Loader already stopped") 375 } 376 this@LauncherModel.lastLoadId++ 377 mTask = task 378 mIsLoaderTaskRunning = true 379 mModelLoaded = false 380 } 381 } 382 383 fun commit() { 384 synchronized(mLock) { 385 // Everything loaded bind the data. 386 mModelLoaded = true 387 } 388 } 389 390 override fun close() { 391 synchronized(mLock) { 392 // If we are still the last one to be scheduled, remove ourselves. 393 if (mLoaderTask === mTask) { 394 mLoaderTask = null 395 } 396 mIsLoaderTaskRunning = false 397 } 398 } 399 } 400 401 @Throws(CancellationException::class) 402 fun beginLoader(task: LoaderTask) = LoaderTransaction(task) 403 404 /** 405 * Refreshes the cached shortcuts if the shortcut permission has changed. Current implementation 406 * simply reloads the workspace, but it can be optimized to use partial updates similar to 407 * [UserCache] 408 */ 409 fun validateModelDataOnResume() { 410 MODEL_EXECUTOR.handler.removeCallbacks(mDataValidationCheck) 411 MODEL_EXECUTOR.post(mDataValidationCheck) 412 } 413 414 /** Called when the icons for packages have been updated in the icon cache. */ 415 fun onPackageIconsUpdated(updatedPackages: HashSet<String?>, user: UserHandle) { 416 // If any package icon has changed (app was updated while launcher was dead), 417 // update the corresponding shortcuts. 418 enqueueModelUpdateTask( 419 CacheDataUpdatedTask(CacheDataUpdatedTask.OP_CACHE_UPDATE, user, updatedPackages) 420 ) 421 } 422 423 /** Called when the labels for the widgets has updated in the icon cache. */ 424 fun onWidgetLabelsUpdated(updatedPackages: HashSet<String?>, user: UserHandle) { 425 enqueueModelUpdateTask { taskController, dataModel, _ -> 426 dataModel.widgetsModel.onPackageIconsUpdated(updatedPackages, user, mApp) 427 taskController.bindUpdatedWidgets(dataModel) 428 } 429 } 430 431 /** Called when the widget filters are refreshed and available to bind to the model. */ 432 fun onWidgetFiltersLoaded() { 433 enqueueModelUpdateTask { taskController, dataModel, _ -> 434 dataModel.widgetsModel.updateWidgetFilters(widgetsFilterDataProvider) 435 taskController.bindUpdatedWidgets(dataModel) 436 } 437 } 438 439 fun enqueueModelUpdateTask(task: ModelUpdateTask) { 440 if (mModelDestroyed) { 441 return 442 } 443 MODEL_EXECUTOR.execute { 444 if (!isModelLoaded()) { 445 // Loader has not yet run. 446 return@execute 447 } 448 task.execute( 449 ModelTaskController(mApp, mBgDataModel, mBgAllAppsList, this, MAIN_EXECUTOR), 450 mBgDataModel, 451 mBgAllAppsList, 452 ) 453 } 454 } 455 456 /** 457 * A task to be executed on the current callbacks on the UI thread. If there is no current 458 * callbacks, the task is ignored. 459 */ 460 fun interface CallbackTask { 461 fun execute(callbacks: BgDataModel.Callbacks) 462 } 463 464 fun interface ModelUpdateTask { 465 fun execute(taskController: ModelTaskController, dataModel: BgDataModel, apps: AllAppsList) 466 } 467 468 fun updateAndBindWorkspaceItem(si: WorkspaceItemInfo, info: ShortcutInfo) { 469 enqueueModelUpdateTask { taskController, _, _ -> 470 si.updateFromDeepShortcutInfo(info, context) 471 iconCache.getShortcutIcon(si, info) 472 taskController.getModelWriter().updateItemInDatabase(si) 473 taskController.bindUpdatedWorkspaceItems(listOf(si)) 474 } 475 } 476 477 fun refreshAndBindWidgetsAndShortcuts(packageUser: PackageUserKey?) { 478 enqueueModelUpdateTask { taskController, dataModel, _ -> 479 dataModel.widgetsModel.update(taskController.app, packageUser) 480 taskController.bindUpdatedWidgets(dataModel) 481 } 482 } 483 484 fun dumpState(prefix: String?, fd: FileDescriptor?, writer: PrintWriter, args: Array<String?>) { 485 if (args.isNotEmpty() && TextUtils.equals(args[0], "--all")) { 486 writer.println(prefix + "All apps list: size=" + mBgAllAppsList.data.size) 487 for (info in mBgAllAppsList.data) { 488 writer.println( 489 "$prefix title=\"${info.title}\" bitmapIcon=${info.bitmap.icon} componentName=${info.targetPackage}" 490 ) 491 } 492 writer.println() 493 } 494 modelDelegate.dump(prefix, fd, writer, args) 495 mBgDataModel.dump(prefix, fd, writer, args) 496 } 497 498 /** Returns true if there are any callbacks attached to the model */ 499 fun hasCallbacks() = synchronized(mCallbacksList) { mCallbacksList.isNotEmpty() } 500 501 /** Returns an array of currently attached callbacks */ 502 val callbacks: Array<BgDataModel.Callbacks> 503 get() { 504 synchronized(mCallbacksList) { 505 return mCallbacksList.toTypedArray<BgDataModel.Callbacks>() 506 } 507 } 508 509 companion object { 510 private const val DEBUG_RECEIVER = false 511 512 const val TAG = "Launcher.Model" 513 } 514 } 515