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