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