1 /*
<lambda>null2  * Copyright (C) 2024 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.model
17 
18 import android.app.PendingIntent
19 import android.content.Context
20 import android.content.Intent
21 import android.content.pm.PackageInstaller.SessionInfo
22 import android.os.Process
23 import android.util.Log
24 import androidx.annotation.AnyThread
25 import androidx.annotation.VisibleForTesting
26 import androidx.annotation.WorkerThread
27 import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
28 import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
29 import com.android.launcher3.model.data.CollectionInfo
30 import com.android.launcher3.model.data.ItemInfo
31 import com.android.launcher3.model.data.LauncherAppWidgetInfo
32 import com.android.launcher3.model.data.WorkspaceItemInfo
33 import com.android.launcher3.pm.InstallSessionHelper
34 import com.android.launcher3.util.Executors
35 import com.android.launcher3.util.PackageManagerHelper
36 import com.android.launcher3.util.PackageUserKey
37 
38 /**
39  * Helper class to send broadcasts to package installers that have:
40  * - Pending Items on first screen
41  * - Installed/Archived Items on first screen
42  * - Installed/Archived Widgets on every screen
43  *
44  * The packages are broken down by: folder items, workspace items, hotseat items, and widgets.
45  * Package installers only receive data for items that they are installing or have installed.
46  */
47 object FirstScreenBroadcastHelper {
48     @VisibleForTesting const val MAX_BROADCAST_SIZE = 70
49 
50     private const val TAG = "FirstScreenBroadcastHelper"
51     private const val DEBUG = true
52     private const val ACTION_FIRST_SCREEN_ACTIVE_INSTALLS =
53         "com.android.launcher3.action.FIRST_SCREEN_ACTIVE_INSTALLS"
54     // String retained as "folderItem" for back-compatibility reasons.
55     private const val PENDING_COLLECTION_ITEM_EXTRA = "folderItem"
56     private const val PENDING_WORKSPACE_ITEM_EXTRA = "workspaceItem"
57     private const val PENDING_HOTSEAT_ITEM_EXTRA = "hotseatItem"
58     private const val PENDING_WIDGET_ITEM_EXTRA = "widgetItem"
59     // Extras containing all installed items, including Archived Apps.
60     private const val INSTALLED_WORKSPACE_ITEMS_EXTRA = "workspaceInstalledItems"
61     private const val INSTALLED_HOTSEAT_ITEMS_EXTRA = "hotseatInstalledItems"
62     // This includes installed widgets on all screens, not just first.
63     private const val ALL_INSTALLED_WIDGETS_ITEM_EXTRA = "widgetInstalledItems"
64     private const val VERIFICATION_TOKEN_EXTRA = "verificationToken"
65 
66     /**
67      * Return list of [FirstScreenBroadcastModel] for each installer and their
68      * installing/installed/archived items. If the FirstScreenBroadcastModel data is greater in size
69      * than [MAX_BROADCAST_SIZE], then we will truncate the data until it meets the size limit to
70      * avoid overloading the broadcast.
71      *
72      * @param packageManagerHelper helper for querying PackageManager
73      * @param firstScreenItems every ItemInfo on first screen
74      * @param userKeyToSessionMap map of pending SessionInfo's for installing items
75      * @param allWidgets list of all Widgets added to every screen
76      */
77     @WorkerThread
78     @JvmStatic
79     fun createModelsForFirstScreenBroadcast(
80         packageManagerHelper: PackageManagerHelper,
81         firstScreenItems: List<ItemInfo>,
82         userKeyToSessionMap: Map<PackageUserKey, SessionInfo>,
83         allWidgets: List<LauncherAppWidgetInfo>
84     ): List<FirstScreenBroadcastModel> {
85 
86         // installers for installing items
87         val pendingItemInstallerMap: Map<String, MutableSet<String>> =
88             createPendingItemsMap(userKeyToSessionMap)
89         val installingPackages = pendingItemInstallerMap.values.flatten().toSet()
90 
91         // installers for installed items on first screen
92         val installedItemInstallerMap: Map<String, MutableSet<ItemInfo>> =
93             createInstalledItemsMap(firstScreenItems, installingPackages, packageManagerHelper)
94 
95         // installers for widgets on all screens
96         val allInstalledWidgetsMap: Map<String, MutableSet<LauncherAppWidgetInfo>> =
97             createAllInstalledWidgetsMap(allWidgets, installingPackages, packageManagerHelper)
98 
99         val allInstallers: Set<String> =
100             pendingItemInstallerMap.keys +
101                 installedItemInstallerMap.keys +
102                 allInstalledWidgetsMap.keys
103         val models = mutableListOf<FirstScreenBroadcastModel>()
104         // create broadcast for each installer, with extras for each item category
105         allInstallers.forEach { installer ->
106             val installingItems = pendingItemInstallerMap[installer]
107             val broadcastModel =
108                 FirstScreenBroadcastModel(installerPackage = installer).apply {
109                     addPendingItems(installingItems, firstScreenItems)
110                     addInstalledItems(installer, installedItemInstallerMap)
111                     addAllScreenWidgets(installer, allInstalledWidgetsMap)
112                 }
113             broadcastModel.truncateModelForBroadcast()
114             models.add(broadcastModel)
115         }
116         return models
117     }
118 
119     /** From the model data, create Intents to send broadcasts and fire them. */
120     @WorkerThread
121     @JvmStatic
122     fun sendBroadcastsForModels(context: Context, models: List<FirstScreenBroadcastModel>) {
123         for (model in models) {
124             model.printDebugInfo()
125             val intent =
126                 Intent(ACTION_FIRST_SCREEN_ACTIVE_INSTALLS)
127                     .setPackage(model.installerPackage)
128                     .putExtra(
129                         VERIFICATION_TOKEN_EXTRA,
130                         PendingIntent.getActivity(
131                             context,
132                             0 /* requestCode */,
133                             Intent(),
134                             PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
135                         )
136                     )
137                     .putStringArrayListExtra(
138                         PENDING_COLLECTION_ITEM_EXTRA,
139                         ArrayList(model.pendingCollectionItems)
140                     )
141                     .putStringArrayListExtra(
142                         PENDING_WORKSPACE_ITEM_EXTRA,
143                         ArrayList(model.pendingWorkspaceItems)
144                     )
145                     .putStringArrayListExtra(
146                         PENDING_HOTSEAT_ITEM_EXTRA,
147                         ArrayList(model.pendingHotseatItems)
148                     )
149                     .putStringArrayListExtra(
150                         PENDING_WIDGET_ITEM_EXTRA,
151                         ArrayList(model.pendingWidgetItems)
152                     )
153                     .putStringArrayListExtra(
154                         INSTALLED_WORKSPACE_ITEMS_EXTRA,
155                         ArrayList(model.installedWorkspaceItems)
156                     )
157                     .putStringArrayListExtra(
158                         INSTALLED_HOTSEAT_ITEMS_EXTRA,
159                         ArrayList(model.installedHotseatItems)
160                     )
161                     .putStringArrayListExtra(
162                         ALL_INSTALLED_WIDGETS_ITEM_EXTRA,
163                         ArrayList(
164                             model.firstScreenInstalledWidgets +
165                                 model.secondaryScreenInstalledWidgets
166                         )
167                     )
168             context.sendBroadcast(intent)
169         }
170     }
171 
172     /** Maps Installer packages to Set of app packages from install sessions */
173     private fun createPendingItemsMap(
174         userKeyToSessionMap: Map<PackageUserKey, SessionInfo>
175     ): Map<String, MutableSet<String>> {
176         val myUser = Process.myUserHandle()
177         val result = mutableMapOf<String, MutableSet<String>>()
178         userKeyToSessionMap.forEach { entry ->
179             if (!myUser.equals(InstallSessionHelper.getUserHandle(entry.value))) return@forEach
180             val installer = entry.value.installerPackageName
181             val appPackage = entry.value.appPackageName
182             if (installer.isNullOrEmpty() || appPackage.isNullOrEmpty()) return@forEach
183             result.getOrPut(installer) { mutableSetOf() }.add(appPackage)
184         }
185         return result
186     }
187 
188     /**
189      * Maps Installer packages to Set of ItemInfo from first screen. Filter out installing packages.
190      */
191     private fun createInstalledItemsMap(
192         firstScreenItems: List<ItemInfo>,
193         installingPackages: Set<String>,
194         packageManagerHelper: PackageManagerHelper
195     ): Map<String, MutableSet<ItemInfo>> {
196         val result = mutableMapOf<String, MutableSet<ItemInfo>>()
197         firstScreenItems.forEach { item ->
198             val appPackage = getPackageName(item) ?: return@forEach
199             if (installingPackages.contains(appPackage)) return@forEach
200             val installer = packageManagerHelper.getAppInstallerPackage(appPackage)
201             if (installer.isNullOrEmpty()) return@forEach
202             result.getOrPut(installer) { mutableSetOf() }.add(item)
203         }
204         return result
205     }
206 
207     /**
208      * Maps Installer packages to Set of AppWidget packages installed on all screens. Filter out
209      * installing packages.
210      */
211     private fun createAllInstalledWidgetsMap(
212         allWidgets: List<LauncherAppWidgetInfo>,
213         installingPackages: Set<String>,
214         packageManagerHelper: PackageManagerHelper
215     ): Map<String, MutableSet<LauncherAppWidgetInfo>> {
216         val result = mutableMapOf<String, MutableSet<LauncherAppWidgetInfo>>()
217         allWidgets
218             .sortedBy { widget -> widget.screenId }
219             .forEach { widget ->
220                 val appPackage = getPackageName(widget) ?: return@forEach
221                 if (installingPackages.contains(appPackage)) return@forEach
222                 val installer = packageManagerHelper.getAppInstallerPackage(appPackage)
223                 if (installer.isNullOrEmpty()) return@forEach
224                 result.getOrPut(installer) { mutableSetOf() }.add(widget)
225             }
226         return result
227     }
228 
229     /**
230      * Add first screen Pending Items from Map to [FirstScreenBroadcastModel] for given installer
231      */
232     private fun FirstScreenBroadcastModel.addPendingItems(
233         installingItems: Set<String>?,
234         firstScreenItems: List<ItemInfo>
235     ) {
236         if (installingItems == null) return
237         for (info in firstScreenItems) {
238             addCollectionItems(info, installingItems)
239             val packageName = getPackageName(info) ?: continue
240             if (!installingItems.contains(packageName)) continue
241             when {
242                 info is LauncherAppWidgetInfo -> pendingWidgetItems.add(packageName)
243                 info.container == CONTAINER_HOTSEAT -> pendingHotseatItems.add(packageName)
244                 info.container == CONTAINER_DESKTOP -> pendingWorkspaceItems.add(packageName)
245             }
246         }
247     }
248 
249     /**
250      * Add first screen installed Items from Map to [FirstScreenBroadcastModel] for given installer
251      */
252     private fun FirstScreenBroadcastModel.addInstalledItems(
253         installer: String,
254         installedItemInstallerMap: Map<String, Set<ItemInfo>>,
255     ) {
256         installedItemInstallerMap[installer]?.forEach { info ->
257             val packageName: String = getPackageName(info) ?: return@forEach
258             when (info.container) {
259                 CONTAINER_HOTSEAT -> installedHotseatItems.add(packageName)
260                 CONTAINER_DESKTOP -> installedWorkspaceItems.add(packageName)
261             }
262         }
263     }
264 
265     /** Add Widgets on every screen from Map to [FirstScreenBroadcastModel] for given installer */
266     private fun FirstScreenBroadcastModel.addAllScreenWidgets(
267         installer: String,
268         allInstalledWidgetsMap: Map<String, Set<LauncherAppWidgetInfo>>
269     ) {
270         allInstalledWidgetsMap[installer]?.forEach { widget ->
271             val packageName: String = getPackageName(widget) ?: return@forEach
272             if (widget.screenId == 0) {
273                 firstScreenInstalledWidgets.add(packageName)
274             } else {
275                 secondaryScreenInstalledWidgets.add(packageName)
276             }
277         }
278     }
279 
280     private fun FirstScreenBroadcastModel.addCollectionItems(
281         info: ItemInfo,
282         installingPackages: Set<String>
283     ) {
284         if (info !is CollectionInfo) return
285         pendingCollectionItems.addAll(
286             cloneOnMainThread(info.getAppContents())
287                 .mapNotNull { getPackageName(it) }
288                 .filter { installingPackages.contains(it) }
289         )
290     }
291 
292     /**
293      * Creates a copy of [FirstScreenBroadcastModel] with items truncated to meet
294      * [MAX_BROADCAST_SIZE] in a prioritized order.
295      */
296     @VisibleForTesting
297     fun FirstScreenBroadcastModel.truncateModelForBroadcast() {
298         val totalItemCount = getTotalItemCount()
299         if (totalItemCount <= MAX_BROADCAST_SIZE) return
300         var extraItemCount = totalItemCount - MAX_BROADCAST_SIZE
301 
302         while (extraItemCount > 0) {
303             // In this order, remove items until we meet the max size limit.
304             when {
305                 pendingCollectionItems.isNotEmpty() ->
306                     pendingCollectionItems.apply { remove(last()) }
307                 pendingHotseatItems.isNotEmpty() -> pendingHotseatItems.apply { remove(last()) }
308                 installedHotseatItems.isNotEmpty() -> installedHotseatItems.apply { remove(last()) }
309                 secondaryScreenInstalledWidgets.isNotEmpty() ->
310                     secondaryScreenInstalledWidgets.apply { remove(last()) }
311                 pendingWidgetItems.isNotEmpty() -> pendingWidgetItems.apply { remove(last()) }
312                 firstScreenInstalledWidgets.isNotEmpty() ->
313                     firstScreenInstalledWidgets.apply { remove(last()) }
314                 pendingWorkspaceItems.isNotEmpty() -> pendingWorkspaceItems.apply { remove(last()) }
315                 installedWorkspaceItems.isNotEmpty() ->
316                     installedWorkspaceItems.apply { remove(last()) }
317             }
318             extraItemCount--
319         }
320     }
321 
322     /** Returns count of all Items held by [FirstScreenBroadcastModel]. */
323     @VisibleForTesting
324     fun FirstScreenBroadcastModel.getTotalItemCount() =
325         pendingCollectionItems.size +
326             pendingWorkspaceItems.size +
327             pendingHotseatItems.size +
328             pendingWidgetItems.size +
329             installedWorkspaceItems.size +
330             installedHotseatItems.size +
331             firstScreenInstalledWidgets.size +
332             secondaryScreenInstalledWidgets.size
333 
334     private fun FirstScreenBroadcastModel.printDebugInfo() {
335         if (DEBUG) {
336             Log.d(
337                 TAG,
338                 "Sending First Screen Broadcast for installer=$installerPackage" +
339                     ", total packages=${getTotalItemCount()}"
340             )
341             pendingCollectionItems.forEach {
342                 Log.d(TAG, "$installerPackage:Pending Collection item:$it")
343             }
344             pendingWorkspaceItems.forEach {
345                 Log.d(TAG, "$installerPackage:Pending Workspace item:$it")
346             }
347             pendingHotseatItems.forEach { Log.d(TAG, "$installerPackage:Pending Hotseat item:$it") }
348             pendingWidgetItems.forEach { Log.d(TAG, "$installerPackage:Pending Widget item:$it") }
349             installedWorkspaceItems.forEach {
350                 Log.d(TAG, "$installerPackage:Installed Workspace item:$it")
351             }
352             installedHotseatItems.forEach {
353                 Log.d(TAG, "$installerPackage:Installed Hotseat item:$it")
354             }
355             firstScreenInstalledWidgets.forEach {
356                 Log.d(TAG, "$installerPackage:Installed Widget item (first screen):$it")
357             }
358             secondaryScreenInstalledWidgets.forEach {
359                 Log.d(TAG, "$installerPackage:Installed Widget item (secondary screens):$it")
360             }
361         }
362     }
363 
364     private fun getPackageName(info: ItemInfo): String? {
365         var packageName: String? = null
366         if (info is LauncherAppWidgetInfo) {
367             info.providerName?.let { packageName = info.providerName.packageName }
368         } else if (info.targetComponent != null) {
369             packageName = info.targetComponent?.packageName
370         }
371         return packageName
372     }
373 
374     /**
375      * Clone the provided list on UI thread. This is used for [FolderInfo.getContents] which is
376      * always modified on UI thread.
377      */
378     @AnyThread
379     private fun cloneOnMainThread(list: ArrayList<WorkspaceItemInfo>): List<WorkspaceItemInfo> {
380         return try {
381             return Executors.MAIN_EXECUTOR.submit<ArrayList<WorkspaceItemInfo>> { ArrayList(list) }
382                 .get()
383         } catch (e: Exception) {
384             emptyList()
385         }
386     }
387 }
388