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.app.admin.DevicePolicyManager
19 import android.content.Context
20 import android.content.Intent
21 import android.content.pm.ShortcutInfo
22 import android.os.UserHandle
23 import android.text.TextUtils
24 import android.util.Log
25 import android.util.Pair
26 import androidx.annotation.WorkerThread
27 import com.android.launcher3.celllayout.CellPosMapper
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
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.ModelDbController
37 import com.android.launcher3.model.ModelDelegate
38 import com.android.launcher3.model.ModelLauncherCallbacks
39 import com.android.launcher3.model.ModelTaskController
40 import com.android.launcher3.model.ModelWriter
41 import com.android.launcher3.model.PackageUpdatedTask
42 import com.android.launcher3.model.ReloadStringCacheTask
43 import com.android.launcher3.model.ShortcutsChangedTask
44 import com.android.launcher3.model.UserLockStateChangedTask
45 import com.android.launcher3.model.WidgetsFilterDataProvider
46 import com.android.launcher3.model.data.ItemInfo
47 import com.android.launcher3.model.data.WorkspaceItemInfo
48 import com.android.launcher3.pm.UserCache
49 import com.android.launcher3.shortcuts.ShortcutRequest
50 import com.android.launcher3.testing.shared.TestProtocol.sDebugTracing
51 import com.android.launcher3.util.Executors.MAIN_EXECUTOR
52 import com.android.launcher3.util.Executors.MODEL_EXECUTOR
53 import com.android.launcher3.util.PackageManagerHelper
54 import com.android.launcher3.util.PackageUserKey
55 import com.android.launcher3.util.Preconditions
56 import java.io.FileDescriptor
57 import java.io.PrintWriter
58 import java.util.concurrent.CancellationException
59 import java.util.function.Consumer
60 
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  */
66 class LauncherModel(
67     private val context: Context,
68     private val mApp: LauncherAppState,
69     private val iconCache: IconCache,
70     private val widgetsFilterDataProvider: WidgetsFilterDataProvider,
71     appFilter: AppFilter,
72     mPmHelper: PackageManagerHelper,
73     isPrimaryInstance: Boolean,
74 ) {
75 
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 
145     /** Returns the [WidgetsFilterDataProvider] that manages widget filters. */
146     fun getWidgetsFilterDataProvider(): WidgetsFilterDataProvider {
147         return widgetsFilterDataProvider
148     }
149 
150     /** 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))
155         ShortcutRequest(context, user).forPackage(packageName).query(ShortcutRequest.PINNED).let {
156             if (it.isNotEmpty()) {
157                 enqueueModelUpdateTask(ShortcutsChangedTask(packageName, it, user, false))
158             }
159         }
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
170         MODEL_EXECUTOR.execute {
171             modelDelegate.destroy()
172             widgetsFilterDataProvider.destroy()
173         }
174     }
175 
176     fun onBroadcastIntent(intent: Intent) {
177         if (DEBUG_RECEIVER || sDebugTracing) Log.d(TAG, "onReceive intent=$intent")
178         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))
185         }
186     }
187 
188     /**
189      * Called then there use a user event
190      *
191      * @see UserCache.addUserEventListener
192      */
193     fun onUserEvent(user: UserHandle, action: String) {
194         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             }
231         }
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         }
244         rebindCallbacks()
245     }
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()
316                     return true
317                 } else {
318                     mLoaderTask =
319                         LoaderTask(
320                             mApp,
321                             mBgAllAppsList,
322                             mBgDataModel,
323                             this.modelDelegate,
324                             launcherBinder,
325                             widgetsFilterDataProvider,
326                         )
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 
368     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 
431     /** 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 
439     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) {
469         enqueueModelUpdateTask { taskController, _, _ ->
470             si.updateFromDeepShortcutInfo(info, context)
471             iconCache.getShortcutIcon(si, info)
472             taskController.getModelWriter().updateItemInDatabase(si)
473             taskController.bindUpdatedWorkspaceItems(listOf(si))
474         }
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 }
515