blob: aa62c32e3318e7ba750c17c89562d73b5910d9b7 [file] [log] [blame]
/*
* Copyright (C) 2024 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.launcher3.model
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageInstaller.SessionInfo
import android.os.Process
import android.util.Log
import androidx.annotation.AnyThread
import androidx.annotation.VisibleForTesting
import androidx.annotation.WorkerThread
import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP
import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT
import com.android.launcher3.model.data.CollectionInfo
import com.android.launcher3.model.data.ItemInfo
import com.android.launcher3.model.data.LauncherAppWidgetInfo
import com.android.launcher3.model.data.WorkspaceItemInfo
import com.android.launcher3.pm.InstallSessionHelper
import com.android.launcher3.util.Executors
import com.android.launcher3.util.PackageManagerHelper
import com.android.launcher3.util.PackageUserKey
/**
* Helper class to send broadcasts to package installers that have:
* - Pending Items on first screen
* - Installed/Archived Items on first screen
* - Installed/Archived Widgets on every screen
*
* The packages are broken down by: folder items, workspace items, hotseat items, and widgets.
* Package installers only receive data for items that they are installing or have installed.
*/
object FirstScreenBroadcastHelper {
@VisibleForTesting const val MAX_BROADCAST_SIZE = 70
private const val TAG = "FirstScreenBroadcastHelper"
private const val DEBUG = true
private const val ACTION_FIRST_SCREEN_ACTIVE_INSTALLS =
"com.android.launcher3.action.FIRST_SCREEN_ACTIVE_INSTALLS"
// String retained as "folderItem" for back-compatibility reasons.
private const val PENDING_COLLECTION_ITEM_EXTRA = "folderItem"
private const val PENDING_WORKSPACE_ITEM_EXTRA = "workspaceItem"
private const val PENDING_HOTSEAT_ITEM_EXTRA = "hotseatItem"
private const val PENDING_WIDGET_ITEM_EXTRA = "widgetItem"
// Extras containing all installed items, including Archived Apps.
private const val INSTALLED_WORKSPACE_ITEMS_EXTRA = "workspaceInstalledItems"
private const val INSTALLED_HOTSEAT_ITEMS_EXTRA = "hotseatInstalledItems"
// This includes installed widgets on all screens, not just first.
private const val ALL_INSTALLED_WIDGETS_ITEM_EXTRA = "widgetInstalledItems"
private const val VERIFICATION_TOKEN_EXTRA = "verificationToken"
/**
* Return list of [FirstScreenBroadcastModel] for each installer and their
* installing/installed/archived items. If the FirstScreenBroadcastModel data is greater in size
* than [MAX_BROADCAST_SIZE], then we will truncate the data until it meets the size limit to
* avoid overloading the broadcast.
*
* @param packageManagerHelper helper for querying PackageManager
* @param firstScreenItems every ItemInfo on first screen
* @param userKeyToSessionMap map of pending SessionInfo's for installing items
* @param allWidgets list of all Widgets added to every screen
*/
@WorkerThread
@JvmStatic
fun createModelsForFirstScreenBroadcast(
packageManagerHelper: PackageManagerHelper,
firstScreenItems: List<ItemInfo>,
userKeyToSessionMap: Map<PackageUserKey, SessionInfo>,
allWidgets: List<LauncherAppWidgetInfo>
): List<FirstScreenBroadcastModel> {
// installers for installing items
val pendingItemInstallerMap: Map<String, MutableSet<String>> =
createPendingItemsMap(userKeyToSessionMap)
val installingPackages = pendingItemInstallerMap.values.flatten().toSet()
// installers for installed items on first screen
val installedItemInstallerMap: Map<String, MutableSet<ItemInfo>> =
createInstalledItemsMap(firstScreenItems, installingPackages, packageManagerHelper)
// installers for widgets on all screens
val allInstalledWidgetsMap: Map<String, MutableSet<LauncherAppWidgetInfo>> =
createAllInstalledWidgetsMap(allWidgets, installingPackages, packageManagerHelper)
val allInstallers: Set<String> =
pendingItemInstallerMap.keys +
installedItemInstallerMap.keys +
allInstalledWidgetsMap.keys
val models = mutableListOf<FirstScreenBroadcastModel>()
// create broadcast for each installer, with extras for each item category
allInstallers.forEach { installer ->
val installingItems = pendingItemInstallerMap[installer]
val broadcastModel =
FirstScreenBroadcastModel(installerPackage = installer).apply {
addPendingItems(installingItems, firstScreenItems)
addInstalledItems(installer, installedItemInstallerMap)
addAllScreenWidgets(installer, allInstalledWidgetsMap)
}
broadcastModel.truncateModelForBroadcast()
models.add(broadcastModel)
}
return models
}
/** From the model data, create Intents to send broadcasts and fire them. */
@WorkerThread
@JvmStatic
fun sendBroadcastsForModels(context: Context, models: List<FirstScreenBroadcastModel>) {
for (model in models) {
model.printDebugInfo()
val intent =
Intent(ACTION_FIRST_SCREEN_ACTIVE_INSTALLS)
.setPackage(model.installerPackage)
.putExtra(
VERIFICATION_TOKEN_EXTRA,
PendingIntent.getActivity(
context,
0 /* requestCode */,
Intent(),
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
)
)
.putStringArrayListExtra(
PENDING_COLLECTION_ITEM_EXTRA,
ArrayList(model.pendingCollectionItems)
)
.putStringArrayListExtra(
PENDING_WORKSPACE_ITEM_EXTRA,
ArrayList(model.pendingWorkspaceItems)
)
.putStringArrayListExtra(
PENDING_HOTSEAT_ITEM_EXTRA,
ArrayList(model.pendingHotseatItems)
)
.putStringArrayListExtra(
PENDING_WIDGET_ITEM_EXTRA,
ArrayList(model.pendingWidgetItems)
)
.putStringArrayListExtra(
INSTALLED_WORKSPACE_ITEMS_EXTRA,
ArrayList(model.installedWorkspaceItems)
)
.putStringArrayListExtra(
INSTALLED_HOTSEAT_ITEMS_EXTRA,
ArrayList(model.installedHotseatItems)
)
.putStringArrayListExtra(
ALL_INSTALLED_WIDGETS_ITEM_EXTRA,
ArrayList(
model.firstScreenInstalledWidgets +
model.secondaryScreenInstalledWidgets
)
)
context.sendBroadcast(intent)
}
}
/** Maps Installer packages to Set of app packages from install sessions */
private fun createPendingItemsMap(
userKeyToSessionMap: Map<PackageUserKey, SessionInfo>
): Map<String, MutableSet<String>> {
val myUser = Process.myUserHandle()
val result = mutableMapOf<String, MutableSet<String>>()
userKeyToSessionMap.forEach { entry ->
if (!myUser.equals(InstallSessionHelper.getUserHandle(entry.value))) return@forEach
val installer = entry.value.installerPackageName
val appPackage = entry.value.appPackageName
if (installer.isNullOrEmpty() || appPackage.isNullOrEmpty()) return@forEach
result.getOrPut(installer) { mutableSetOf() }.add(appPackage)
}
return result
}
/**
* Maps Installer packages to Set of ItemInfo from first screen. Filter out installing packages.
*/
private fun createInstalledItemsMap(
firstScreenItems: List<ItemInfo>,
installingPackages: Set<String>,
packageManagerHelper: PackageManagerHelper
): Map<String, MutableSet<ItemInfo>> {
val result = mutableMapOf<String, MutableSet<ItemInfo>>()
firstScreenItems.forEach { item ->
val appPackage = getPackageName(item) ?: return@forEach
if (installingPackages.contains(appPackage)) return@forEach
val installer = packageManagerHelper.getAppInstallerPackage(appPackage)
if (installer.isNullOrEmpty()) return@forEach
result.getOrPut(installer) { mutableSetOf() }.add(item)
}
return result
}
/**
* Maps Installer packages to Set of AppWidget packages installed on all screens. Filter out
* installing packages.
*/
private fun createAllInstalledWidgetsMap(
allWidgets: List<LauncherAppWidgetInfo>,
installingPackages: Set<String>,
packageManagerHelper: PackageManagerHelper
): Map<String, MutableSet<LauncherAppWidgetInfo>> {
val result = mutableMapOf<String, MutableSet<LauncherAppWidgetInfo>>()
allWidgets
.sortedBy { widget -> widget.screenId }
.forEach { widget ->
val appPackage = getPackageName(widget) ?: return@forEach
if (installingPackages.contains(appPackage)) return@forEach
val installer = packageManagerHelper.getAppInstallerPackage(appPackage)
if (installer.isNullOrEmpty()) return@forEach
result.getOrPut(installer) { mutableSetOf() }.add(widget)
}
return result
}
/**
* Add first screen Pending Items from Map to [FirstScreenBroadcastModel] for given installer
*/
private fun FirstScreenBroadcastModel.addPendingItems(
installingItems: Set<String>?,
firstScreenItems: List<ItemInfo>
) {
if (installingItems == null) return
for (info in firstScreenItems) {
addCollectionItems(info, installingItems)
val packageName = getPackageName(info) ?: continue
if (!installingItems.contains(packageName)) continue
when {
info is LauncherAppWidgetInfo -> pendingWidgetItems.add(packageName)
info.container == CONTAINER_HOTSEAT -> pendingHotseatItems.add(packageName)
info.container == CONTAINER_DESKTOP -> pendingWorkspaceItems.add(packageName)
}
}
}
/**
* Add first screen installed Items from Map to [FirstScreenBroadcastModel] for given installer
*/
private fun FirstScreenBroadcastModel.addInstalledItems(
installer: String,
installedItemInstallerMap: Map<String, Set<ItemInfo>>,
) {
installedItemInstallerMap[installer]?.forEach { info ->
val packageName: String = getPackageName(info) ?: return@forEach
when (info.container) {
CONTAINER_HOTSEAT -> installedHotseatItems.add(packageName)
CONTAINER_DESKTOP -> installedWorkspaceItems.add(packageName)
}
}
}
/** Add Widgets on every screen from Map to [FirstScreenBroadcastModel] for given installer */
private fun FirstScreenBroadcastModel.addAllScreenWidgets(
installer: String,
allInstalledWidgetsMap: Map<String, Set<LauncherAppWidgetInfo>>
) {
allInstalledWidgetsMap[installer]?.forEach { widget ->
val packageName: String = getPackageName(widget) ?: return@forEach
if (widget.screenId == 0) {
firstScreenInstalledWidgets.add(packageName)
} else {
secondaryScreenInstalledWidgets.add(packageName)
}
}
}
private fun FirstScreenBroadcastModel.addCollectionItems(
info: ItemInfo,
installingPackages: Set<String>
) {
if (info !is CollectionInfo) return
pendingCollectionItems.addAll(
cloneOnMainThread(info.getAppContents())
.mapNotNull { getPackageName(it) }
.filter { installingPackages.contains(it) }
)
}
/**
* Creates a copy of [FirstScreenBroadcastModel] with items truncated to meet
* [MAX_BROADCAST_SIZE] in a prioritized order.
*/
@VisibleForTesting
fun FirstScreenBroadcastModel.truncateModelForBroadcast() {
val totalItemCount = getTotalItemCount()
if (totalItemCount <= MAX_BROADCAST_SIZE) return
var extraItemCount = totalItemCount - MAX_BROADCAST_SIZE
while (extraItemCount > 0) {
// In this order, remove items until we meet the max size limit.
when {
pendingCollectionItems.isNotEmpty() ->
pendingCollectionItems.apply { remove(last()) }
pendingHotseatItems.isNotEmpty() -> pendingHotseatItems.apply { remove(last()) }
installedHotseatItems.isNotEmpty() -> installedHotseatItems.apply { remove(last()) }
secondaryScreenInstalledWidgets.isNotEmpty() ->
secondaryScreenInstalledWidgets.apply { remove(last()) }
pendingWidgetItems.isNotEmpty() -> pendingWidgetItems.apply { remove(last()) }
firstScreenInstalledWidgets.isNotEmpty() ->
firstScreenInstalledWidgets.apply { remove(last()) }
pendingWorkspaceItems.isNotEmpty() -> pendingWorkspaceItems.apply { remove(last()) }
installedWorkspaceItems.isNotEmpty() ->
installedWorkspaceItems.apply { remove(last()) }
}
extraItemCount--
}
}
/** Returns count of all Items held by [FirstScreenBroadcastModel]. */
@VisibleForTesting
fun FirstScreenBroadcastModel.getTotalItemCount() =
pendingCollectionItems.size +
pendingWorkspaceItems.size +
pendingHotseatItems.size +
pendingWidgetItems.size +
installedWorkspaceItems.size +
installedHotseatItems.size +
firstScreenInstalledWidgets.size +
secondaryScreenInstalledWidgets.size
private fun FirstScreenBroadcastModel.printDebugInfo() {
if (DEBUG) {
Log.d(
TAG,
"Sending First Screen Broadcast for installer=$installerPackage" +
", total packages=${getTotalItemCount()}"
)
pendingCollectionItems.forEach {
Log.d(TAG, "$installerPackage:Pending Collection item:$it")
}
pendingWorkspaceItems.forEach {
Log.d(TAG, "$installerPackage:Pending Workspace item:$it")
}
pendingHotseatItems.forEach { Log.d(TAG, "$installerPackage:Pending Hotseat item:$it") }
pendingWidgetItems.forEach { Log.d(TAG, "$installerPackage:Pending Widget item:$it") }
installedWorkspaceItems.forEach {
Log.d(TAG, "$installerPackage:Installed Workspace item:$it")
}
installedHotseatItems.forEach {
Log.d(TAG, "$installerPackage:Installed Hotseat item:$it")
}
firstScreenInstalledWidgets.forEach {
Log.d(TAG, "$installerPackage:Installed Widget item (first screen):$it")
}
secondaryScreenInstalledWidgets.forEach {
Log.d(TAG, "$installerPackage:Installed Widget item (secondary screens):$it")
}
}
}
private fun getPackageName(info: ItemInfo): String? {
var packageName: String? = null
if (info is LauncherAppWidgetInfo) {
info.providerName?.let { packageName = info.providerName.packageName }
} else if (info.targetComponent != null) {
packageName = info.targetComponent?.packageName
}
return packageName
}
/**
* Clone the provided list on UI thread. This is used for [FolderInfo.getContents] which is
* always modified on UI thread.
*/
@AnyThread
private fun cloneOnMainThread(list: ArrayList<WorkspaceItemInfo>): List<WorkspaceItemInfo> {
return try {
return Executors.MAIN_EXECUTOR.submit<ArrayList<WorkspaceItemInfo>> { ArrayList(list) }
.get()
} catch (e: Exception) {
emptyList()
}
}
}