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