diff --git a/src/com/android/launcher3/model/FirstScreenBroadcast.java b/src/com/android/launcher3/model/FirstScreenBroadcast.java index cc20cd194c..f443f8142e 100644 --- a/src/com/android/launcher3/model/FirstScreenBroadcast.java +++ b/src/com/android/launcher3/model/FirstScreenBroadcast.java @@ -96,7 +96,7 @@ public class FirstScreenBroadcast { .collect(groupingBy(SessionInfo::getInstallerPackageName, mapping(SessionInfo::getAppPackageName, Collectors.toSet()))) .forEach((installer, packages) -> - sendBroadcastToInstaller(context, installer, packages, firstScreenItems)); + sendBroadcastToInstaller(context, installer, packages, firstScreenItems)); } /** diff --git a/src/com/android/launcher3/model/FirstScreenBroadcastHelper.kt b/src/com/android/launcher3/model/FirstScreenBroadcastHelper.kt new file mode 100644 index 0000000000..aa62c32e33 --- /dev/null +++ b/src/com/android/launcher3/model/FirstScreenBroadcastHelper.kt @@ -0,0 +1,387 @@ +/* + * 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, + userKeyToSessionMap: Map, + allWidgets: List + ): List { + + // installers for installing items + val pendingItemInstallerMap: Map> = + createPendingItemsMap(userKeyToSessionMap) + val installingPackages = pendingItemInstallerMap.values.flatten().toSet() + + // installers for installed items on first screen + val installedItemInstallerMap: Map> = + createInstalledItemsMap(firstScreenItems, installingPackages, packageManagerHelper) + + // installers for widgets on all screens + val allInstalledWidgetsMap: Map> = + createAllInstalledWidgetsMap(allWidgets, installingPackages, packageManagerHelper) + + val allInstallers: Set = + pendingItemInstallerMap.keys + + installedItemInstallerMap.keys + + allInstalledWidgetsMap.keys + val models = mutableListOf() + // 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) { + 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 + ): Map> { + val myUser = Process.myUserHandle() + val result = mutableMapOf>() + 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, + installingPackages: Set, + packageManagerHelper: PackageManagerHelper + ): Map> { + val result = mutableMapOf>() + 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, + installingPackages: Set, + packageManagerHelper: PackageManagerHelper + ): Map> { + val result = mutableMapOf>() + 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?, + firstScreenItems: List + ) { + 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>, + ) { + 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> + ) { + 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 + ) { + 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): List { + return try { + return Executors.MAIN_EXECUTOR.submit> { ArrayList(list) } + .get() + } catch (e: Exception) { + emptyList() + } + } +} diff --git a/src/com/android/launcher3/model/FirstScreenBroadcastModel.kt b/src/com/android/launcher3/model/FirstScreenBroadcastModel.kt new file mode 100644 index 0000000000..ba5c526144 --- /dev/null +++ b/src/com/android/launcher3/model/FirstScreenBroadcastModel.kt @@ -0,0 +1,39 @@ +/* + * 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 + +/** Data model for the information used for [FirstScreenBroadcastHelper] Broadcast Extras */ +data class FirstScreenBroadcastModel( + // Package name of the installer for all items + val installerPackage: String, + // Installing items in Folders + val pendingCollectionItems: MutableSet = mutableSetOf(), + // Installing items on first screen + val pendingWorkspaceItems: MutableSet = mutableSetOf(), + // Installing items on hotseat + val pendingHotseatItems: MutableSet = mutableSetOf(), + // Installing widgets on first screen + val pendingWidgetItems: MutableSet = mutableSetOf(), + // Installed/Archived Items on first screen + val installedWorkspaceItems: MutableSet = mutableSetOf(), + // Installed/Archived items on hotseat + val installedHotseatItems: MutableSet = mutableSetOf(), + // Installed/Archived Widgets on first screen + val firstScreenInstalledWidgets: MutableSet = mutableSetOf(), + // Installed Archived Widgets on secondary screens + val secondaryScreenInstalledWidgets: MutableSet = mutableSetOf() +) diff --git a/src/com/android/launcher3/model/LoaderTask.java b/src/com/android/launcher3/model/LoaderTask.java index 0d40a2432e..dc6968c7d9 100644 --- a/src/com/android/launcher3/model/LoaderTask.java +++ b/src/com/android/launcher3/model/LoaderTask.java @@ -47,6 +47,7 @@ import android.os.Bundle; import android.os.Trace; import android.os.UserHandle; import android.os.UserManager; +import android.provider.Settings; import android.util.ArrayMap; import android.util.Log; import android.util.LongSparseArray; @@ -195,17 +196,32 @@ public class LoaderTask implements Runnable { } private void sendFirstScreenActiveInstallsBroadcast() { - ArrayList firstScreenItems = new ArrayList<>(); - ArrayList allItems = mBgDataModel.getAllWorkspaceItems(); - // Screen set is never empty IntArray allScreens = mBgDataModel.collectWorkspaceScreens(); final int firstScreen = allScreens.get(0); IntSet firstScreens = IntSet.wrap(firstScreen); + ArrayList allItems = mBgDataModel.getAllWorkspaceItems(); + ArrayList firstScreenItems = new ArrayList<>(); filterCurrentWorkspaceItems(firstScreens, allItems, firstScreenItems, new ArrayList<>() /* otherScreenItems are ignored */); - mFirstScreenBroadcast.sendBroadcasts(mApp.getContext(), firstScreenItems); + final int launcherBroadcastInstalledApps = Settings.Secure.getInt( + mApp.getContext().getContentResolver(), + "launcher_broadcast_installed_apps", + /* def= */ 0); + if (launcherBroadcastInstalledApps == 1) { + List broadcastModels = + FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast( + mPmHelper, + firstScreenItems, + mInstallingPkgsCached, + mBgDataModel.appWidgets + ); + logASplit("Sending first screen broadcast with additional archiving Extras"); + FirstScreenBroadcastHelper.sendBroadcastsForModels(mApp.getContext(), broadcastModels); + } else { + mFirstScreenBroadcast.sendBroadcasts(mApp.getContext(), firstScreenItems); + } } public void run() { @@ -249,7 +265,7 @@ public class LoaderTask implements Runnable { mModelDelegate.workspaceLoadComplete(); // Notify the installer packages of packages with active installs on the first screen. sendFirstScreenActiveInstallsBroadcast(); - logASplit("sendFirstScreenActiveInstallsBroadcast"); + logASplit("sendFirstScreenBroadcast"); // Take a break waitForIdle(); diff --git a/src/com/android/launcher3/provider/RestoreDbTask.java b/src/com/android/launcher3/provider/RestoreDbTask.java index a4ff29f33a..21897bffb8 100644 --- a/src/com/android/launcher3/provider/RestoreDbTask.java +++ b/src/com/android/launcher3/provider/RestoreDbTask.java @@ -418,9 +418,7 @@ public class RestoreDbTask { DeviceGridState deviceGridState = new DeviceGridState(context); FileLog.d(TAG, "restore initiated from backup: DeviceGridState=" + deviceGridState); LauncherPrefs.get(context).putSync(RESTORE_DEVICE.to(deviceGridState.getDeviceType())); - if (enableLauncherBrMetricsFixed()) { - LauncherPrefs.get(context).putSync(IS_FIRST_LOAD_AFTER_RESTORE.to(true)); - } + LauncherPrefs.get(context).putSync(IS_FIRST_LOAD_AFTER_RESTORE.to(true)); } @WorkerThread diff --git a/src/com/android/launcher3/util/PackageManagerHelper.java b/src/com/android/launcher3/util/PackageManagerHelper.java index f7c4df4527..8c5a76e665 100644 --- a/src/com/android/launcher3/util/PackageManagerHelper.java +++ b/src/com/android/launcher3/util/PackageManagerHelper.java @@ -16,9 +16,10 @@ package com.android.launcher3.util; -import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE; import static android.view.WindowManager.PROPERTY_SUPPORTS_MULTI_INSTANCE_SYSTEM_UI; +import static com.android.launcher3.model.data.ItemInfoWithIcon.FLAG_INSTALL_SESSION_ACTIVE; + import android.content.ActivityNotFoundException; import android.content.ComponentName; import android.content.Context; @@ -150,6 +151,18 @@ public class PackageManagerHelper implements SafeCloseable{ } } + /** + * Returns the installing app package for the given package + */ + public String getAppInstallerPackage(@NonNull final String packageName) { + try { + return mPm.getInstallSourceInfo(packageName).getInstallingPackageName(); + } catch (NameNotFoundException e) { + Log.e(TAG, "Failed to get installer package for app package:" + packageName, e); + return null; + } + } + /** * Returns the application info for the provided package or null */ diff --git a/tests/src/com/android/launcher3/model/FirstScreenBroadcastHelperTest.kt b/tests/src/com/android/launcher3/model/FirstScreenBroadcastHelperTest.kt new file mode 100644 index 0000000000..aadf72e801 --- /dev/null +++ b/tests/src/com/android/launcher3/model/FirstScreenBroadcastHelperTest.kt @@ -0,0 +1,386 @@ +/* + * 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.ComponentName +import android.content.Intent +import android.content.pm.PackageInstaller.SessionInfo +import android.os.UserHandle +import androidx.test.platform.app.InstrumentationRegistry +import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_DESKTOP +import com.android.launcher3.LauncherSettings.Favorites.CONTAINER_HOTSEAT +import com.android.launcher3.model.FirstScreenBroadcastHelper.MAX_BROADCAST_SIZE +import com.android.launcher3.model.FirstScreenBroadcastHelper.getTotalItemCount +import com.android.launcher3.model.FirstScreenBroadcastHelper.truncateModelForBroadcast +import com.android.launcher3.model.data.FolderInfo +import com.android.launcher3.model.data.LauncherAppWidgetInfo +import com.android.launcher3.model.data.WorkspaceItemInfo +import com.android.launcher3.util.PackageManagerHelper +import com.android.launcher3.util.PackageUserKey +import junit.framework.Assert.assertEquals +import org.junit.Test +import org.mockito.ArgumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +class FirstScreenBroadcastHelperTest { + private val context = spy(InstrumentationRegistry.getInstrumentation().targetContext) + private val mockPmHelper = mock() + private val expectedAppPackage = "appPackageExpected" + private val expectedComponentName = ComponentName(expectedAppPackage, "expectedClass") + private val expectedInstallerPackage = "installerPackage" + private val expectedIntent = + Intent().apply { + component = expectedComponentName + setPackage(expectedAppPackage) + } + private val unexpectedAppPackage = "appPackageUnexpected" + private val unexpectedComponentName = ComponentName(expectedAppPackage, "unexpectedClass") + private val firstScreenItems = + listOf( + WorkspaceItemInfo().apply { + container = CONTAINER_DESKTOP + intent = expectedIntent + }, + WorkspaceItemInfo().apply { + container = CONTAINER_HOTSEAT + intent = expectedIntent + }, + LauncherAppWidgetInfo().apply { providerName = expectedComponentName } + ) + + @Test + fun `Broadcast Models are created with Pending Items from first screen`() { + // Given + val sessionInfoExpected = + SessionInfo().apply { + installerPackageName = expectedInstallerPackage + appPackageName = expectedAppPackage + } + val sessionInfoUnexpected = + SessionInfo().apply { + installerPackageName = expectedInstallerPackage + appPackageName = unexpectedAppPackage + } + val sessionInfoMap: HashMap = + hashMapOf( + PackageUserKey(unexpectedAppPackage, UserHandle(0)) to sessionInfoExpected, + PackageUserKey(expectedAppPackage, UserHandle(0)) to sessionInfoUnexpected + ) + + // When + val actualResult = + FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast( + packageManagerHelper = mockPmHelper, + firstScreenItems = firstScreenItems, + userKeyToSessionMap = sessionInfoMap, + allWidgets = listOf() + ) + + // Then + val expectedResult = + listOf( + FirstScreenBroadcastModel( + installerPackage = expectedInstallerPackage, + pendingWorkspaceItems = mutableSetOf(expectedAppPackage), + pendingHotseatItems = mutableSetOf(expectedAppPackage), + pendingWidgetItems = mutableSetOf(expectedAppPackage) + ) + ) + + assertEquals(expectedResult, actualResult) + } + + @Test + fun `Broadcast Models are created with Installed Items from first screen`() { + // Given + whenever(mockPmHelper.getAppInstallerPackage(expectedAppPackage)) + .thenReturn(expectedInstallerPackage) + + // When + val actualResult = + FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast( + packageManagerHelper = mockPmHelper, + firstScreenItems = firstScreenItems, + userKeyToSessionMap = hashMapOf(), + allWidgets = + listOf( + LauncherAppWidgetInfo().apply { + providerName = expectedComponentName + screenId = 0 + } + ) + ) + + // Then + val expectedResult = + listOf( + FirstScreenBroadcastModel( + installerPackage = expectedInstallerPackage, + installedHotseatItems = mutableSetOf(expectedAppPackage), + installedWorkspaceItems = mutableSetOf(expectedAppPackage), + firstScreenInstalledWidgets = mutableSetOf(expectedAppPackage) + ) + ) + assertEquals(expectedResult, actualResult) + } + + @Test + fun `Broadcast Models are created with Installed Widgets from every screen`() { + // Given + val expectedAppPackage2 = "appPackageExpected2" + val expectedComponentName2 = ComponentName(expectedAppPackage2, "expectedClass2") + whenever(mockPmHelper.getAppInstallerPackage(expectedAppPackage)) + .thenReturn(expectedInstallerPackage) + whenever(mockPmHelper.getAppInstallerPackage(expectedAppPackage2)) + .thenReturn(expectedInstallerPackage) + + // When + val actualResult = + FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast( + packageManagerHelper = mockPmHelper, + firstScreenItems = listOf(), + userKeyToSessionMap = hashMapOf(), + allWidgets = + listOf( + LauncherAppWidgetInfo().apply { + providerName = expectedComponentName + screenId = 0 + }, + LauncherAppWidgetInfo().apply { + providerName = expectedComponentName2 + screenId = 1 + }, + LauncherAppWidgetInfo().apply { + providerName = unexpectedComponentName + screenId = 0 + } + ) + ) + + // Then + val expectedResult = + listOf( + FirstScreenBroadcastModel( + installerPackage = expectedInstallerPackage, + installedHotseatItems = mutableSetOf(), + installedWorkspaceItems = mutableSetOf(), + firstScreenInstalledWidgets = mutableSetOf(expectedAppPackage), + secondaryScreenInstalledWidgets = mutableSetOf(expectedAppPackage2) + ) + ) + assertEquals(expectedResult, actualResult) + } + + @Test + fun `Broadcast Models are created with Pending Items in Collections from the first screen`() { + // Given + val sessionInfoExpected = + SessionInfo().apply { + installerPackageName = expectedInstallerPackage + appPackageName = expectedAppPackage + } + val sessionInfoUnexpected = + SessionInfo().apply { + installerPackageName = expectedInstallerPackage + appPackageName = unexpectedAppPackage + } + val sessionInfoMap: HashMap = + hashMapOf( + PackageUserKey(unexpectedAppPackage, UserHandle(0)) to sessionInfoExpected, + PackageUserKey(expectedAppPackage, UserHandle(0)) to sessionInfoUnexpected, + ) + val expectedItemInfo = WorkspaceItemInfo().apply { intent = expectedIntent } + val expectedFolderInfo = FolderInfo().apply { add(expectedItemInfo) } + val firstScreenItems = listOf(expectedFolderInfo) + + // When + val actualResult = + FirstScreenBroadcastHelper.createModelsForFirstScreenBroadcast( + packageManagerHelper = mockPmHelper, + firstScreenItems = firstScreenItems, + userKeyToSessionMap = sessionInfoMap, + allWidgets = listOf() + ) + + // Then + val expectedResult = + listOf( + FirstScreenBroadcastModel( + installerPackage = expectedInstallerPackage, + pendingCollectionItems = mutableSetOf(expectedAppPackage) + ) + ) + assertEquals(expectedResult, actualResult) + } + + @Test + fun `Models with too many items get truncated to max Broadcast size`() { + // given + val broadcastModel = + FirstScreenBroadcastModel( + installerPackage = expectedInstallerPackage, + pendingCollectionItems = + mutableSetOf().apply { repeat(20) { add(it.toString()) } }, + pendingWorkspaceItems = + mutableSetOf().apply { repeat(20) { add(it.toString()) } }, + pendingHotseatItems = + mutableSetOf().apply { repeat(20) { add(it.toString()) } }, + pendingWidgetItems = + mutableSetOf().apply { repeat(20) { add(it.toString()) } }, + installedWorkspaceItems = + mutableSetOf().apply { repeat(20) { add(it.toString()) } }, + installedHotseatItems = + mutableSetOf().apply { repeat(20) { add(it.toString()) } }, + firstScreenInstalledWidgets = + mutableSetOf().apply { repeat(20) { add(it.toString()) } }, + secondaryScreenInstalledWidgets = + mutableSetOf().apply { repeat(20) { add(it.toString()) } } + ) + + // When + broadcastModel.truncateModelForBroadcast() + + // Then + assertEquals(MAX_BROADCAST_SIZE, broadcastModel.getTotalItemCount()) + } + + @Test + fun `Broadcast truncates installed Hotseat items before other installed items`() { + // Given + val broadcastModel = + FirstScreenBroadcastModel( + installerPackage = expectedInstallerPackage, + installedWorkspaceItems = + mutableSetOf().apply { repeat(50) { add(it.toString()) } }, + firstScreenInstalledWidgets = + mutableSetOf().apply { repeat(10) { add(it.toString()) } }, + secondaryScreenInstalledWidgets = + mutableSetOf().apply { repeat(10) { add((it + 10).toString()) } }, + installedHotseatItems = + mutableSetOf().apply { repeat(10) { add(it.toString()) } }, + ) + + // When + broadcastModel.truncateModelForBroadcast() + + // Then + assertEquals(MAX_BROADCAST_SIZE, broadcastModel.getTotalItemCount()) + assertEquals(50, broadcastModel.installedWorkspaceItems.size) + assertEquals(10, broadcastModel.firstScreenInstalledWidgets.size) + assertEquals(10, broadcastModel.secondaryScreenInstalledWidgets.size) + assertEquals(0, broadcastModel.installedHotseatItems.size) + } + + @Test + fun `Broadcast truncates Widgets before the rest of the first screen items`() { + // Given + val broadcastModel = + FirstScreenBroadcastModel( + installerPackage = expectedInstallerPackage, + installedWorkspaceItems = + mutableSetOf().apply { repeat(70) { add(it.toString()) } }, + firstScreenInstalledWidgets = + mutableSetOf().apply { repeat(20) { add(it.toString()) } }, + secondaryScreenInstalledWidgets = + mutableSetOf().apply { repeat(20) { add(it.toString()) } }, + ) + + // When + broadcastModel.truncateModelForBroadcast() + + // Then + assertEquals(MAX_BROADCAST_SIZE, broadcastModel.getTotalItemCount()) + assertEquals(70, broadcastModel.installedWorkspaceItems.size) + assertEquals(0, broadcastModel.firstScreenInstalledWidgets.size) + assertEquals(0, broadcastModel.secondaryScreenInstalledWidgets.size) + } + + @Test + fun `Broadcasts are correctly formed with Extras for each Installer`() { + // Given + val broadcastModels: List = + listOf( + FirstScreenBroadcastModel( + installerPackage = expectedInstallerPackage, + pendingCollectionItems = mutableSetOf("pendingCollectionItem"), + pendingWorkspaceItems = mutableSetOf("pendingWorkspaceItem"), + pendingHotseatItems = mutableSetOf("pendingHotseatItems"), + pendingWidgetItems = mutableSetOf("pendingWidgetItems"), + installedWorkspaceItems = mutableSetOf("installedWorkspaceItems"), + installedHotseatItems = mutableSetOf("installedHotseatItems"), + firstScreenInstalledWidgets = mutableSetOf("firstScreenInstalledWidgetItems"), + secondaryScreenInstalledWidgets = mutableSetOf("secondaryInstalledWidgetItems") + ) + ) + val expectedPendingIntent = + PendingIntent.getActivity( + context, + 0 /* requestCode */, + Intent(), + PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE + ) + + // When + FirstScreenBroadcastHelper.sendBroadcastsForModels(context, broadcastModels) + + // Then + val argumentCaptor = ArgumentCaptor.forClass(Intent::class.java) + verify(context).sendBroadcast(argumentCaptor.capture()) + + assertEquals( + "com.android.launcher3.action.FIRST_SCREEN_ACTIVE_INSTALLS", + argumentCaptor.value.action + ) + assertEquals(expectedInstallerPackage, argumentCaptor.value.`package`) + assertEquals( + expectedPendingIntent, + argumentCaptor.value.getParcelableExtra("verificationToken") + ) + assertEquals( + arrayListOf("pendingCollectionItem"), + argumentCaptor.value.getStringArrayListExtra("folderItem") + ) + assertEquals( + arrayListOf("pendingWorkspaceItem"), + argumentCaptor.value.getStringArrayListExtra("workspaceItem") + ) + assertEquals( + arrayListOf("pendingHotseatItems"), + argumentCaptor.value.getStringArrayListExtra("hotseatItem") + ) + assertEquals( + arrayListOf("pendingWidgetItems"), + argumentCaptor.value.getStringArrayListExtra("widgetItem") + ) + assertEquals( + arrayListOf("installedWorkspaceItems"), + argumentCaptor.value.getStringArrayListExtra("workspaceInstalledItems") + ) + assertEquals( + arrayListOf("installedHotseatItems"), + argumentCaptor.value.getStringArrayListExtra("hotseatInstalledItems") + ) + assertEquals( + arrayListOf("firstScreenInstalledWidgetItems", "secondaryInstalledWidgetItems"), + argumentCaptor.value.getStringArrayListExtra("widgetInstalledItems") + ) + } +}