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