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