• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.util.Executors
34 import com.android.launcher3.util.PackageManagerHelper
35 import com.android.launcher3.util.PackageUserKey
36 
37 /**
38  * Helper class to send broadcasts to package installers that have:
39  * - Pending Items on first screen
40  * - Installed/Archived Items on first screen
41  * - Installed/Archived Widgets on every screen
42  *
43  * The packages are broken down by: folder items, workspace items, hotseat items, and widgets.
44  * Package installers only receive data for items that they are installing or have installed.
45  */
46 object FirstScreenBroadcastHelper {
47     @VisibleForTesting const val MAX_BROADCAST_SIZE = 70
48 
49     private const val TAG = "FirstScreenBroadcastHelper"
50     private const val DEBUG = true
51     private const val ACTION_FIRST_SCREEN_ACTIVE_INSTALLS =
52         "com.android.launcher3.action.FIRST_SCREEN_ACTIVE_INSTALLS"
53     // String retained as "folderItem" for back-compatibility reasons.
54     private const val PENDING_COLLECTION_ITEM_EXTRA = "folderItem"
55     private const val PENDING_WORKSPACE_ITEM_EXTRA = "workspaceItem"
56     private const val PENDING_HOTSEAT_ITEM_EXTRA = "hotseatItem"
57     private const val PENDING_WIDGET_ITEM_EXTRA = "widgetItem"
58     // Extras containing all installed items, including Archived Apps.
59     private const val INSTALLED_WORKSPACE_ITEMS_EXTRA = "workspaceInstalledItems"
60     private const val INSTALLED_HOTSEAT_ITEMS_EXTRA = "hotseatInstalledItems"
61     // This includes installed widgets on all screens, not just first.
62     private const val ALL_INSTALLED_WIDGETS_ITEM_EXTRA = "widgetInstalledItems"
63     private const val VERIFICATION_TOKEN_EXTRA = "verificationToken"
64 
65     /**
66      * Return list of [FirstScreenBroadcastModel] for each installer and their
67      * installing/installed/archived items. If the FirstScreenBroadcastModel data is greater in size
68      * than [MAX_BROADCAST_SIZE], then we will truncate the data until it meets the size limit to
69      * avoid overloading the broadcast.
70      *
71      * @param packageManagerHelper helper for querying PackageManager
72      * @param firstScreenItems every ItemInfo on first screen
73      * @param userKeyToSessionMap map of pending SessionInfo's for installing items
74      * @param allWidgets list of all Widgets added to every screen
75      */
76     @WorkerThread
77     @JvmStatic
78     fun createModelsForFirstScreenBroadcast(
79         packageManagerHelper: PackageManagerHelper,
80         firstScreenItems: List<ItemInfo>,
81         userKeyToSessionMap: Map<PackageUserKey, SessionInfo>,
82         allWidgets: List<ItemInfo>,
83     ): List<FirstScreenBroadcastModel> {
84 
85         // installers for installing items
86         val pendingItemInstallerMap: Map<String, Set<String>> =
87             createPendingItemsMap(userKeyToSessionMap)
88 
89         val installingPackages = pendingItemInstallerMap.values.flatten().toSet()
90 
91         // installers for installed items on first screen
92         val installedItemInstallerMap: Map<String, List<ItemInfo>> =
93             createInstalledItemsMap(firstScreenItems, installingPackages, packageManagerHelper)
94 
95         // installers for widgets on all screens
96         val allInstalledWidgetsMap: Map<String, List<ItemInfo>> =
97             createInstalledItemsMap(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, Set<String>> {
176         val myUser = Process.myUserHandle()
177         return userKeyToSessionMap.values
178             .filter {
179                 it.user == myUser &&
180                     !it.installerPackageName.isNullOrEmpty() &&
181                     !it.appPackageName.isNullOrEmpty()
182             }
183             .groupBy(
184                 keySelector = { it.installerPackageName },
185                 valueTransform = { it.appPackageName },
186             )
187             .mapValues { it.value.filterNotNull().toSet() } as Map<String, Set<String>>
188     }
189 
190     /** Maps Installer packages to Set of ItemInfos. Filter out installing packages. */
191     private fun createInstalledItemsMap(
192         allItems: Iterable<ItemInfo>,
193         installingPackages: Set<String>,
194         packageManagerHelper: PackageManagerHelper,
195     ): Map<String, List<ItemInfo>> =
196         allItems
197             .sortedBy { it.screenId }
198             .groupByTo(mutableMapOf()) {
199                 getPackageName(it)?.let { pkg ->
200                     if (installingPackages.contains(pkg)) {
201                         null
202                     } else {
203                         packageManagerHelper.getAppInstallerPackage(pkg)
204                     }
205                 }
206             }
207             .apply { remove(null) } as Map<String, List<ItemInfo>>
208 
209     /**
210      * Add first screen Pending Items from Map to [FirstScreenBroadcastModel] for given installer
211      */
212     private fun FirstScreenBroadcastModel.addPendingItems(
213         installingItems: Set<String>?,
214         firstScreenItems: List<ItemInfo>,
215     ) {
216         if (installingItems == null) return
217         for (info in firstScreenItems) {
218             addCollectionItems(info, installingItems)
219             val packageName = getPackageName(info) ?: continue
220             if (!installingItems.contains(packageName)) continue
221             when {
222                 info is LauncherAppWidgetInfo -> pendingWidgetItems.add(packageName)
223                 info.container == CONTAINER_HOTSEAT -> pendingHotseatItems.add(packageName)
224                 info.container == CONTAINER_DESKTOP -> pendingWorkspaceItems.add(packageName)
225             }
226         }
227     }
228 
229     /**
230      * Add first screen installed Items from Map to [FirstScreenBroadcastModel] for given installer
231      */
232     private fun FirstScreenBroadcastModel.addInstalledItems(
233         installer: String,
234         installedItemInstallerMap: Map<String, List<ItemInfo>>,
235     ) {
236         installedItemInstallerMap[installer]?.forEach { info ->
237             val packageName: String = getPackageName(info) ?: return@forEach
238             when (info.container) {
239                 CONTAINER_HOTSEAT -> installedHotseatItems.add(packageName)
240                 CONTAINER_DESKTOP -> installedWorkspaceItems.add(packageName)
241             }
242         }
243     }
244 
245     /** Add Widgets on every screen from Map to [FirstScreenBroadcastModel] for given installer */
246     private fun FirstScreenBroadcastModel.addAllScreenWidgets(
247         installer: String,
248         allInstalledWidgetsMap: Map<String, List<ItemInfo>>,
249     ) {
250         allInstalledWidgetsMap[installer]?.forEach { widget ->
251             val packageName: String = getPackageName(widget) ?: return@forEach
252             if (widget.screenId == 0) {
253                 firstScreenInstalledWidgets.add(packageName)
254             } else {
255                 secondaryScreenInstalledWidgets.add(packageName)
256             }
257         }
258     }
259 
260     private fun FirstScreenBroadcastModel.addCollectionItems(
261         info: ItemInfo,
262         installingPackages: Set<String>,
263     ) {
264         if (info !is CollectionInfo) return
265         pendingCollectionItems.addAll(
266             cloneOnMainThread(info.getAppContents())
267                 .mapNotNull { getPackageName(it) }
268                 .filter { installingPackages.contains(it) }
269         )
270     }
271 
272     /**
273      * Creates a copy of [FirstScreenBroadcastModel] with items truncated to meet
274      * [MAX_BROADCAST_SIZE] in a prioritized order.
275      */
276     @VisibleForTesting
277     fun FirstScreenBroadcastModel.truncateModelForBroadcast() {
278         val totalItemCount = getTotalItemCount()
279         if (totalItemCount <= MAX_BROADCAST_SIZE) return
280         var extraItemCount = totalItemCount - MAX_BROADCAST_SIZE
281 
282         while (extraItemCount > 0) {
283             // In this order, remove items until we meet the max size limit.
284             when {
285                 pendingCollectionItems.isNotEmpty() ->
286                     pendingCollectionItems.apply { remove(last()) }
287                 pendingHotseatItems.isNotEmpty() -> pendingHotseatItems.apply { remove(last()) }
288                 installedHotseatItems.isNotEmpty() -> installedHotseatItems.apply { remove(last()) }
289                 secondaryScreenInstalledWidgets.isNotEmpty() ->
290                     secondaryScreenInstalledWidgets.apply { remove(last()) }
291                 pendingWidgetItems.isNotEmpty() -> pendingWidgetItems.apply { remove(last()) }
292                 firstScreenInstalledWidgets.isNotEmpty() ->
293                     firstScreenInstalledWidgets.apply { remove(last()) }
294                 pendingWorkspaceItems.isNotEmpty() -> pendingWorkspaceItems.apply { remove(last()) }
295                 installedWorkspaceItems.isNotEmpty() ->
296                     installedWorkspaceItems.apply { remove(last()) }
297             }
298             extraItemCount--
299         }
300     }
301 
302     /** Returns count of all Items held by [FirstScreenBroadcastModel]. */
303     @VisibleForTesting
304     fun FirstScreenBroadcastModel.getTotalItemCount() =
305         pendingCollectionItems.size +
306             pendingWorkspaceItems.size +
307             pendingHotseatItems.size +
308             pendingWidgetItems.size +
309             installedWorkspaceItems.size +
310             installedHotseatItems.size +
311             firstScreenInstalledWidgets.size +
312             secondaryScreenInstalledWidgets.size
313 
314     private fun FirstScreenBroadcastModel.printDebugInfo() {
315         if (DEBUG) {
316             Log.d(
317                 TAG,
318                 "Sending First Screen Broadcast for installer=$installerPackage" +
319                     ", total packages=${getTotalItemCount()}",
320             )
321             pendingCollectionItems.forEach {
322                 Log.d(TAG, "$installerPackage:Pending Collection item:$it")
323             }
324             pendingWorkspaceItems.forEach {
325                 Log.d(TAG, "$installerPackage:Pending Workspace item:$it")
326             }
327             pendingHotseatItems.forEach { Log.d(TAG, "$installerPackage:Pending Hotseat item:$it") }
328             pendingWidgetItems.forEach { Log.d(TAG, "$installerPackage:Pending Widget item:$it") }
329             installedWorkspaceItems.forEach {
330                 Log.d(TAG, "$installerPackage:Installed Workspace item:$it")
331             }
332             installedHotseatItems.forEach {
333                 Log.d(TAG, "$installerPackage:Installed Hotseat item:$it")
334             }
335             firstScreenInstalledWidgets.forEach {
336                 Log.d(TAG, "$installerPackage:Installed Widget item (first screen):$it")
337             }
338             secondaryScreenInstalledWidgets.forEach {
339                 Log.d(TAG, "$installerPackage:Installed Widget item (secondary screens):$it")
340             }
341         }
342     }
343 
344     private fun getPackageName(info: ItemInfo): String? = info.targetComponent?.packageName
345 
346     /**
347      * Clone the provided list on UI thread. This is used for [FolderInfo.getContents] which is
348      * always modified on UI thread.
349      */
350     @AnyThread
351     private fun cloneOnMainThread(list: ArrayList<WorkspaceItemInfo>): List<WorkspaceItemInfo> {
352         return try {
353             return Executors.MAIN_EXECUTOR.submit<ArrayList<WorkspaceItemInfo>> { ArrayList(list) }
354                 .get()
355         } catch (e: Exception) {
356             emptyList()
357         }
358     }
359 }
360