blob: b56df4624c33e9f879df88005bb02ad64afff07d [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
Sunny Goyal266f51b2024-10-08 13:02:56 -070028import com.android.launcher3.icons.IconCache
Sunny Goyal266f51b2024-10-08 13:02:56 -070029import com.android.launcher3.model.AddWorkspaceItemsTask
30import com.android.launcher3.model.AllAppsList
31import com.android.launcher3.model.BaseLauncherBinder
32import com.android.launcher3.model.BgDataModel
33import com.android.launcher3.model.CacheDataUpdatedTask
34import com.android.launcher3.model.ItemInstallQueue
35import com.android.launcher3.model.LoaderTask
36import com.android.launcher3.model.ModelDbController
37import com.android.launcher3.model.ModelDelegate
38import com.android.launcher3.model.ModelLauncherCallbacks
39import com.android.launcher3.model.ModelTaskController
40import com.android.launcher3.model.ModelWriter
Sunny Goyal266f51b2024-10-08 13:02:56 -070041import com.android.launcher3.model.PackageUpdatedTask
42import com.android.launcher3.model.ReloadStringCacheTask
43import com.android.launcher3.model.ShortcutsChangedTask
44import com.android.launcher3.model.UserLockStateChangedTask
Shamali Pea078cb2024-11-04 21:35:38 +000045import com.android.launcher3.model.WidgetsFilterDataProvider
Sunny Goyal266f51b2024-10-08 13:02:56 -070046import 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,
Shamali Pea078cb2024-11-04 21:35:38 +000070 private val widgetsFilterDataProvider: WidgetsFilterDataProvider,
Stefan Andonianbb851102024-10-25 12:48:59 -070071 appFilter: AppFilter,
72 mPmHelper: PackageManagerHelper,
Sunny Goyal266f51b2024-10-08 13:02:56 -070073 isPrimaryInstance: Boolean,
Sunny Goyalc3632952024-10-09 15:15:42 -070074) {
Sunny Goyal266f51b2024-10-08 13:02:56 -070075
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
Shamali Pea078cb2024-11-04 21:35:38 +0000145 /** Returns the [WidgetsFilterDataProvider] that manages widget filters. */
146 fun getWidgetsFilterDataProvider(): WidgetsFilterDataProvider {
147 return widgetsFilterDataProvider
148 }
149
Sunny Goyal266f51b2024-10-08 13:02:56 -0700150 /** 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))
Sunny Goyalc3632952024-10-09 15:15:42 -0700155 ShortcutRequest(context, user).forPackage(packageName).query(ShortcutRequest.PINNED).let {
156 if (it.isNotEmpty()) {
157 enqueueModelUpdateTask(ShortcutsChangedTask(packageName, it, user, false))
158 }
Sunny Goyal266f51b2024-10-08 13:02:56 -0700159 }
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
Shamali Pea078cb2024-11-04 21:35:38 +0000170 MODEL_EXECUTOR.execute {
171 modelDelegate.destroy()
172 widgetsFilterDataProvider.destroy()
173 }
Sunny Goyal266f51b2024-10-08 13:02:56 -0700174 }
175
176 fun onBroadcastIntent(intent: Intent) {
177 if (DEBUG_RECEIVER || sDebugTracing) Log.d(TAG, "onReceive intent=$intent")
Sunny Goyalc3632952024-10-09 15:15:42 -0700178 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))
Sunny Goyal266f51b2024-10-08 13:02:56 -0700185 }
186 }
187
188 /**
189 * Called then there use a user event
190 *
191 * @see UserCache.addUserEventListener
192 */
193 fun onUserEvent(user: UserHandle, action: String) {
Sunny Goyalc3632952024-10-09 15:15:42 -0700194 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 }
Sunny Goyal266f51b2024-10-08 13:02:56 -0700231 }
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 }
Sunny Goyalc3632952024-10-09 15:15:42 -0700244 rebindCallbacks()
Sunny Goyal266f51b2024-10-08 13:02:56 -0700245 }
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()
Sunny Goyal266f51b2024-10-08 13:02:56 -0700316 return true
317 } else {
Sunny Goyal266f51b2024-10-08 13:02:56 -0700318 mLoaderTask =
319 LoaderTask(
320 mApp,
321 mBgAllAppsList,
322 mBgDataModel,
323 this.modelDelegate,
324 launcherBinder,
Shamali Pea078cb2024-11-04 21:35:38 +0000325 widgetsFilterDataProvider,
Sunny Goyal266f51b2024-10-08 13:02:56 -0700326 )
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
Sunny Goyal266f51b2024-10-08 13:02:56 -0700368 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
Shamali Pea078cb2024-11-04 21:35:38 +0000431 /** 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
Sunny Goyal266f51b2024-10-08 13:02:56 -0700439 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) {
Sunny Goyal266f51b2024-10-08 13:02:56 -0700469 enqueueModelUpdateTask { taskController, _, _ ->
Sunny Goyalc3632952024-10-09 15:15:42 -0700470 si.updateFromDeepShortcutInfo(info, context)
471 iconCache.getShortcutIcon(si, info)
472 taskController.getModelWriter().updateItemInDatabase(si)
473 taskController.bindUpdatedWorkspaceItems(listOf(si))
Sunny Goyal266f51b2024-10-08 13:02:56 -0700474 }
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}