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