From d18b422c51b11b258c6f941621698f546cd00bd5 Mon Sep 17 00:00:00 2001 From: George Chan Date: Thu, 1 Dec 2022 18:19:40 +0000 Subject: [PATCH] Added Background install control UI code. Change-Id: I1b629fdc04d1df1b08998c9aaae3df3446fab3fe Bug: 238451991 Test: Manually with settings, atest --- res/values/strings.xml | 25 ++ .../settings/spa/SettingsSpaEnvironment.kt | 2 + src/com/android/settings/spa/app/AppsMain.kt | 4 + .../BackgroundInstalledAppsPageProvider.kt | 264 +++++++++++++++ ...BackgroundInstalledAppsPageProviderTest.kt | 319 ++++++++++++++++++ 5 files changed, 614 insertions(+) create mode 100644 src/com/android/settings/spa/app/backgroundinstall/BackgroundInstalledAppsPageProvider.kt create mode 100644 tests/spa_unit/src/com/android/settings/spa/app/backgroundinstall/BackgroundInstalledAppsPageProviderTest.kt diff --git a/res/values/strings.xml b/res/values/strings.xml index 36b8ed6a9dc..af830c64b8d 100644 --- a/res/values/strings.xml +++ b/res/values/strings.xml @@ -11369,4 +11369,29 @@ Transfer eSIM to another device + + + {count, plural, + =1 {# app} + other {# apps} + } + + + Apps installed in the background + + Your device manufacturer may install apps on your device in the background, or allow your carrier and other partners to do so.\u000a\u000aAny apps listed here aren\u0027t required for your device to function normally. You can uninstall apps you don\u0027t want. + + No apps installed in the background + + Uninstall app + + {count, plural, + =1 {Apps installed in the last # month} + other {Apps installed in the last # months} + } + + {count, plural, + =1 {Apps installed more than # month ago} + other {Apps installed more than # months ago} + } diff --git a/src/com/android/settings/spa/SettingsSpaEnvironment.kt b/src/com/android/settings/spa/SettingsSpaEnvironment.kt index bf15ddfa7f3..9eab400326a 100644 --- a/src/com/android/settings/spa/SettingsSpaEnvironment.kt +++ b/src/com/android/settings/spa/SettingsSpaEnvironment.kt @@ -20,6 +20,7 @@ import android.content.Context import com.android.settings.spa.app.AllAppListPageProvider import com.android.settings.spa.app.AppsMainPageProvider import com.android.settings.spa.app.appinfo.AppInfoSettingsProvider +import com.android.settings.spa.app.backgroundinstall.BackgroundInstalledAppsPageProvider import com.android.settings.spa.app.specialaccess.AlarmsAndRemindersAppListProvider import com.android.settings.spa.app.specialaccess.AllFilesAccessAppListProvider import com.android.settings.spa.app.specialaccess.DisplayOverOtherAppsAppListProvider @@ -66,6 +67,7 @@ open class SettingsSpaEnvironment(context: Context) : SpaEnvironment(context) { LanguageAndInputPageProvider, AppLanguagesPageProvider, UsageStatsPageProvider, + BackgroundInstalledAppsPageProvider, ) + togglePermissionAppListTemplate.createPageProviders(), rootPages = listOf( SettingsPage.create(HomePageProvider.name), diff --git a/src/com/android/settings/spa/app/AppsMain.kt b/src/com/android/settings/spa/app/AppsMain.kt index 4b472784e55..e9eca1fd015 100644 --- a/src/com/android/settings/spa/app/AppsMain.kt +++ b/src/com/android/settings/spa/app/AppsMain.kt @@ -22,7 +22,9 @@ import androidx.compose.material.icons.outlined.Apps import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import com.android.settings.R +import com.android.settings.spa.app.backgroundinstall.BackgroundInstalledAppsPageProvider import com.android.settings.spa.app.specialaccess.SpecialAppAccessPageProvider +import com.android.settings.spa.home.HomePageProvider import com.android.settingslib.spa.framework.common.SettingsEntry import com.android.settingslib.spa.framework.common.SettingsEntryBuilder import com.android.settingslib.spa.framework.common.SettingsPage @@ -45,6 +47,7 @@ object AppsMainPageProvider : SettingsPageProvider { RegularScaffold(title = getTitle(arguments)) { AllAppListPageProvider.buildInjectEntry().build().UiLayout() SpecialAppAccessPageProvider.EntryItem() + BackgroundInstalledAppsPageProvider.EntryItem() } } @@ -70,6 +73,7 @@ object AppsMainPageProvider : SettingsPageProvider { return listOf( AllAppListPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), SpecialAppAccessPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), + BackgroundInstalledAppsPageProvider.buildInjectEntry().setLink(fromPage = owner).build(), ) } } diff --git a/src/com/android/settings/spa/app/backgroundinstall/BackgroundInstalledAppsPageProvider.kt b/src/com/android/settings/spa/app/backgroundinstall/BackgroundInstalledAppsPageProvider.kt new file mode 100644 index 00000000000..9cf95169d42 --- /dev/null +++ b/src/com/android/settings/spa/app/backgroundinstall/BackgroundInstalledAppsPageProvider.kt @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2022 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.settings.spa.app.backgroundinstall + +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.IBackgroundInstallControlService +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.content.pm.ParceledListSlice +import android.net.Uri +import android.os.Bundle +import android.os.ServiceManager +import android.provider.DeviceConfig +import android.util.Log +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.padding +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.runtime.Composable +import androidx.compose.runtime.State +import androidx.compose.runtime.produceState +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import com.android.settings.R +import com.android.settings.spa.app.appinfo.AppInfoSettingsProvider +import com.android.settingslib.spa.framework.common.SettingsEntryBuilder +import com.android.settingslib.spa.framework.common.SettingsPage +import com.android.settingslib.spa.framework.common.SettingsPageProvider +import com.android.settingslib.spa.framework.compose.navigator +import com.android.settingslib.spa.framework.compose.rememberContext +import com.android.settingslib.spa.framework.theme.SettingsDimension +import com.android.settingslib.spa.framework.util.asyncMap +import com.android.settingslib.spa.framework.util.formatString +import com.android.settingslib.spa.widget.preference.Preference +import com.android.settingslib.spa.widget.preference.PreferenceModel +import com.android.settingslib.spa.widget.ui.SettingsBody +import com.android.settingslib.spaprivileged.model.app.AppEntry +import com.android.settingslib.spaprivileged.model.app.AppListModel +import com.android.settingslib.spaprivileged.model.app.AppRecord +import com.android.settingslib.spaprivileged.template.app.AppList +import com.android.settingslib.spaprivileged.template.app.AppListButtonItem +import com.android.settingslib.spaprivileged.template.app.AppListInput +import com.android.settingslib.spaprivileged.template.app.AppListItemModel +import com.android.settingslib.spaprivileged.template.app.AppListPage +import com.google.common.annotations.VisibleForTesting +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.withContext + +private const val KEY_GROUPING_MONTH = "key_grouping_by_month" +const val DEFAULT_GROUPING_MONTH_VALUE = 6 +const val MONTH_IN_MILLIS = 2629800000L +const val KEY_BIC_UI_ENABLED = "key_bic_ui_enabled" +const val BACKGROUND_INSTALL_CONTROL_FLAG = PackageManager.MATCH_ALL.toLong() + +object BackgroundInstalledAppsPageProvider : SettingsPageProvider { + override val name = "BackgroundInstalledAppsPage" + private var backgroundInstallService = IBackgroundInstallControlService.Stub.asInterface( + ServiceManager.getService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE)) + private var featureIsDisabled = featureIsDisabled() + + @Composable + override fun Page(arguments: Bundle?) { + if(featureIsDisabled) return + BackgroundInstalledAppList() + } + + @Composable + fun EntryItem() { + if(featureIsDisabled) return + Preference(object : PreferenceModel { + override val title = stringResource(R.string.background_install_title) + override val summary = generatePreferenceSummary() + override val onClick = navigator(name) + }) + } + + fun buildInjectEntry() = SettingsEntryBuilder + .createInject(owner = SettingsPage.create(name)) + .setSearchDataFn { null } + .setUiLayoutFn { EntryItem() } + + private fun featureIsDisabled() = !DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI, + KEY_BIC_UI_ENABLED, false) + + @Composable + private fun generatePreferenceSummary(): State { + val context = LocalContext.current + return produceState(initialValue = stringResource(R.string.summary_placeholder)) { + withContext(Dispatchers.IO) { + val backgroundInstalledApps = + backgroundInstallService.getBackgroundInstalledPackages( + BACKGROUND_INSTALL_CONTROL_FLAG, context.user.identifier + ).list.size + value = context.formatString( + R.string.background_install_preference_summary, + "count" to backgroundInstalledApps + ) + } + } + } + + @VisibleForTesting + fun setDisableFeature(disableFeature : Boolean): BackgroundInstalledAppsPageProvider { + featureIsDisabled = disableFeature + return this + } + + @VisibleForTesting + fun setBackgroundInstallControlService(bic: IBackgroundInstallControlService): + BackgroundInstalledAppsPageProvider { + backgroundInstallService = bic + return this + } +} + +@Composable +fun BackgroundInstalledAppList( + appList: @Composable AppListInput.() -> Unit + = { AppList() }, +) { + AppListPage( + title = stringResource(R.string.background_install_title), + listModel = rememberContext(::BackgroundInstalledAppsWithGroupingListModel), + noItemMessage = stringResource(R.string.background_install_feature_list_no_entry), + appList = appList, + header = { + Box(Modifier.padding(SettingsDimension.itemPadding)) { + SettingsBody(stringResource(R.string.background_install_summary)) + } + } + ) +} +/* +Based on PackageManagerService design, and it looks like the suggested replacement in the deprecate +notes suggest that we use PackageInstaller.uninstall which does not guarantee a pop up would open +and depends on the calling application. Seems like further investigation is needed before we can +move over to the new API. + */ +@Suppress +@VisibleForTesting +fun startUninstallActivity(context: Context, + packageName: String, + forAllUsers: Boolean = false) { + val packageUri = Uri.parse("package:${packageName}") + val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri).apply { + putExtra(Intent.EXTRA_UNINSTALL_ALL_USERS, forAllUsers) + } + context.startActivityAsUser(intent, context.user) +} + +data class BackgroundInstalledAppListWithGroupingAppRecord( + override val app: ApplicationInfo, + val dateOfInstall: Long, +) : AppRecord + +class BackgroundInstalledAppsWithGroupingListModel(private val context: Context) + : AppListModel { + + companion object { + private const val tag = "AppListModel" + } + + private var backgroundInstallService = IBackgroundInstallControlService.Stub.asInterface( + ServiceManager.getService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE)) + + @VisibleForTesting + fun setBackgroundInstallControlService(bic: IBackgroundInstallControlService) { + backgroundInstallService = bic + } + @Composable + override fun AppListItemModel.AppItem() { + val context = LocalContext.current + AppListButtonItem( + onClick = AppInfoSettingsProvider.navigator(app = record.app), + onButtonClick = { startUninstallActivity(context, record.app.packageName) }, + buttonIcon = Icons.Outlined.Delete, + buttonIconDescription = stringResource( + R.string.background_install_uninstall_button_description)) + } + + override fun transform(userIdFlow: Flow, appListFlow: Flow>) = + userIdFlow.combine(appListFlow) { userId, appList -> + appList.asyncMap { app -> + BackgroundInstalledAppListWithGroupingAppRecord( + app = app, + dateOfInstall = context.packageManager.getPackageInfoAsUser(app.packageName, + PackageManager.PackageInfoFlags.of(0), userId).firstInstallTime + ) + } + } + + @Composable + override fun getSummary(option: Int, record: BackgroundInstalledAppListWithGroupingAppRecord) + = null + + @Suppress + override fun filter( + userIdFlow: Flow, + option: Int, + recordListFlow: Flow> + ): Flow> { + if(backgroundInstallService == null) { + Log.e(tag, "Failed to retrieve Background Install Control Service") + return flowOf() + } + return userIdFlow.combine(recordListFlow) { userId, recordList -> + val appList = (backgroundInstallService.getBackgroundInstalledPackages( + PackageManager.MATCH_ALL.toLong(), userId) as ParceledListSlice).list + val appNameList = appList.map { it.packageName } + recordList.filter { record -> record.app.packageName in appNameList } + } + } + + override fun getComparator( + option: Int, + ): Comparator> = + compareByDescending { it.record.dateOfInstall } + + override fun getGroupTitle(option: Int, record: BackgroundInstalledAppListWithGroupingAppRecord) + : String { + val groupByMonth = getGroupSeparationByMonth() + return when (record.dateOfInstall > System.currentTimeMillis() + - (groupByMonth * MONTH_IN_MILLIS)) { + true -> context.formatString(R.string.background_install_before, "count" to groupByMonth) + else -> context.formatString(R.string.background_install_after, "count" to groupByMonth) + } + } +} + +private fun getGroupSeparationByMonth(): Int { + val month = DeviceConfig.getProperty(DeviceConfig.NAMESPACE_SETTINGS_UI, KEY_GROUPING_MONTH) + return try { + if (month.isNullOrBlank()) { + DEFAULT_GROUPING_MONTH_VALUE + } else { + month.toInt() + } + } catch (e: Exception) { + Log.d( + BackgroundInstalledAppsPageProvider.name, "Error parsing list grouping value: " + + "${e.message} falling back to default value: $DEFAULT_GROUPING_MONTH_VALUE") + DEFAULT_GROUPING_MONTH_VALUE + } +} diff --git a/tests/spa_unit/src/com/android/settings/spa/app/backgroundinstall/BackgroundInstalledAppsPageProviderTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/backgroundinstall/BackgroundInstalledAppsPageProviderTest.kt new file mode 100644 index 00000000000..8e1757f5d1e --- /dev/null +++ b/tests/spa_unit/src/com/android/settings/spa/app/backgroundinstall/BackgroundInstalledAppsPageProviderTest.kt @@ -0,0 +1,319 @@ +/* + * Copyright (C) 2022 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.settings.spa.app.backgroundinstall + +import android.content.Context +import android.content.Intent +import android.content.pm.ApplicationInfo +import android.content.pm.IBackgroundInstallControlService +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.content.pm.ParceledListSlice +import android.net.Uri +import android.os.UserHandle +import androidx.compose.ui.test.assertIsDisplayed +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.compose.ui.test.onNodeWithText +import androidx.compose.ui.test.performClick +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.settings.R +import com.android.settingslib.spa.framework.compose.stateOf +import com.android.settingslib.spa.testutils.FakeNavControllerWrapper +import com.android.settingslib.spa.testutils.any +import com.android.settingslib.spaprivileged.template.app.AppListItemModel +import com.google.common.truth.Truth +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.junit.MockitoJUnit +import org.mockito.junit.MockitoRule + +@RunWith(AndroidJUnit4::class) +class BackgroundInstalledAppsPageProviderTest { + @get:Rule + val composeTestRule = createComposeRule() + + @get:Rule + val mockito: MockitoRule = MockitoJUnit.rule() + + private val context: Context = ApplicationProvider.getApplicationContext() + + @Mock + private lateinit var mockContext: Context + + @Mock + private lateinit var mockPackageManager: PackageManager + + @Mock + private lateinit var mockBackgroundInstallControlService: IBackgroundInstallControlService + + private var packageInfoFlagsCaptor = + ArgumentCaptor.forClass(PackageManager.PackageInfoFlags::class.java) + + private var intentCaptor = + ArgumentCaptor.forClass(Intent::class.java) + + private val fakeNavControllerWrapper = FakeNavControllerWrapper() + + @Before + fun setup() { + Mockito.`when`(mockContext.packageManager).thenReturn(mockPackageManager) + } + @Test + fun allAppListPageProvider_name() { + Truth.assertThat(BackgroundInstalledAppsPageProvider.name) + .isEqualTo(EXPECTED_PROVIDER_NAME) + } + + @Test + fun injectEntry_title() { + Mockito.`when`(mockBackgroundInstallControlService + .getBackgroundInstalledPackages(any(Long::class.java), any(Int::class.java))) + .thenReturn(ParceledListSlice(listOf())) + setInjectEntry(false) + + composeTestRule.onNodeWithText( + context.getString(R.string.background_install_title)).assertIsDisplayed() + } + + @Test + fun injectEntry_title_disabled() { + setInjectEntry(true) + + composeTestRule.onNodeWithText( + context.getString(R.string.background_install_title)).assertDoesNotExist() + } + + @Test + fun injectEntry_summary() { + Mockito.`when`(mockBackgroundInstallControlService + .getBackgroundInstalledPackages(any(Long::class.java), any(Int::class.java))) + .thenReturn(ParceledListSlice(listOf())) + setInjectEntry(false) + + composeTestRule.onNodeWithText("0 apps").assertIsDisplayed() + } + + @Test + fun injectEntry_summary_disabled() { + setInjectEntry(true) + + composeTestRule.onNodeWithText("0 apps").assertDoesNotExist() + } + + @Test + fun injectEntry_onClick_navigate() { + Mockito.`when`(mockBackgroundInstallControlService + .getBackgroundInstalledPackages(any(Long::class.java), any(Int::class.java))) + .thenReturn(ParceledListSlice(listOf())) + setInjectEntry(false) + + composeTestRule.onNodeWithText( + context.getString(R.string.background_install_title)).performClick() + + Truth.assertThat(fakeNavControllerWrapper.navigateCalledWith) + .isEqualTo(EXPECTED_PROVIDER_NAME) + } + + private fun setInjectEntry(disableFeature: Boolean = false) { + composeTestRule.setContent { + fakeNavControllerWrapper.Wrapper { + BackgroundInstalledAppsPageProvider + .setBackgroundInstallControlService(mockBackgroundInstallControlService) + .setDisableFeature(disableFeature) + .buildInjectEntry().build().UiLayout() + } + } + } + + @Test + fun title_displayed() { + composeTestRule.setContent { + BackgroundInstalledAppList() + } + + composeTestRule.onNodeWithText( + context.getString(R.string.background_install_title)).assertIsDisplayed() + } + + @Test + fun item_labelDisplayed() { + setItemContent() + + composeTestRule.onNodeWithText(TEST_LABEL).assertIsDisplayed() + } + + @Test + fun item_onClick_navigate() { + setItemContent() + + composeTestRule.onNodeWithText(TEST_LABEL).performClick() + + Truth.assertThat(fakeNavControllerWrapper.navigateCalledWith) + .isEqualTo("AppInfoSettings/package.name/0") + } + + @Suppress + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun startUninstallActivity_success() = runTest { + val expectedPackageUri = Uri.parse("package:package.name") + val mockUserHandle = UserHandle(0) + Mockito.`when`(mockContext.user).thenReturn(mockUserHandle) + Mockito.`when`(mockContext.startActivityAsUser( + intentCaptor.capture(), + eq(mockUserHandle) + )).then { } + + startUninstallActivity(mockContext, TEST_PACKAGE_NAME) + + Truth.assertThat(intentCaptor.value.action).isEqualTo(Intent.ACTION_UNINSTALL_PACKAGE) + Truth.assertThat(intentCaptor.value.data).isEqualTo(expectedPackageUri) + Truth.assertThat(intentCaptor.value.extras?.getBoolean(Intent.EXTRA_UNINSTALL_ALL_USERS)) + .isEqualTo(false) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun backgroundInstalledAppsWithGroupingListModel_getGroupTitleOne() = runTest { + val listModel = BackgroundInstalledAppsWithGroupingListModel(context) + + val actualGroupTitle = listModel + .getGroupTitle(0, + BackgroundInstalledAppListWithGroupingAppRecord( + APP, + System.currentTimeMillis() + )) + + Truth.assertThat(actualGroupTitle).isEqualTo("Apps installed in the last 6 months") + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun backgroundInstalledAppsWithGroupingListModel_getGroupTitleTwo() = runTest { + val listModel = BackgroundInstalledAppsWithGroupingListModel(context) + + val actualGroupTitle = listModel + .getGroupTitle(0, + BackgroundInstalledAppListWithGroupingAppRecord( + APP, + 0L + )) + + Truth.assertThat(actualGroupTitle).isEqualTo("Apps installed more than 6 months ago") + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun backgroundInstalledAppsWithGroupingListModel_transform() = runTest { + val listModel = BackgroundInstalledAppsWithGroupingListModel(mockContext) + Mockito.`when`(mockPackageManager.getPackageInfoAsUser( + eq(TEST_PACKAGE_NAME), + packageInfoFlagsCaptor.capture(), + eq(TEST_USER_ID)) + ) + .thenReturn(PACKAGE_INFO) + val recordListFlow = listModel.transform(flowOf(TEST_USER_ID), flowOf(listOf(APP))) + + val recordList = recordListFlow.first() + + Truth.assertThat(recordList).hasSize(1) + Truth.assertThat(recordList[0].app).isSameInstanceAs(APP) + Truth.assertThat(packageInfoFlagsCaptor.value.value).isEqualTo(EXPECTED_PACKAGE_INFO_FLAG) + } + + @OptIn(ExperimentalCoroutinesApi::class) + @Test + fun backgroundInstalledAppsWithGroupingListModel_filter() = runTest { + val listModel = BackgroundInstalledAppsWithGroupingListModel(mockContext) + listModel.setBackgroundInstallControlService(mockBackgroundInstallControlService) + Mockito.`when`(mockBackgroundInstallControlService.getBackgroundInstalledPackages( + PackageManager.MATCH_ALL.toLong(), + TEST_USER_ID + )).thenReturn(ParceledListSlice(listOf(PACKAGE_INFO))) + + val recordListFlow = listModel.filter( + flowOf(TEST_USER_ID), + 0, + flowOf(listOf(APP_RECORD_WITH_PACKAGE_MATCH, APP_RECORD_WITHOUT_PACKAGE_MATCH)) + ) + + + val recordList = recordListFlow.first() + Truth.assertThat(recordList).hasSize(1) + Truth.assertThat(recordList[0]).isSameInstanceAs(APP_RECORD_WITH_PACKAGE_MATCH) + } + + private fun setItemContent() { + composeTestRule.setContent { + BackgroundInstalledAppList { + fakeNavControllerWrapper.Wrapper { + with(BackgroundInstalledAppsWithGroupingListModel(context)) { + AppListItemModel( + record = BackgroundInstalledAppListWithGroupingAppRecord( + app = APP, + dateOfInstall = TEST_FIRST_INSTALL_TIME), + label = TEST_LABEL, + summary = stateOf(TEST_SUMMARY), + ).AppItem() + } + } + } + } + } + + private companion object { + private const val TEST_USER_ID = 0 + private const val TEST_PACKAGE_NAME = "package.name" + private const val TEST_NO_MATCH_PACKAGE_NAME = "no.match" + private const val TEST_LABEL = "Label" + private const val TEST_SUMMARY = "Summary" + private const val TEST_FIRST_INSTALL_TIME = 0L + private const val EXPECTED_PROVIDER_NAME = "BackgroundInstalledAppsPage" + private const val EXPECTED_PACKAGE_INFO_FLAG = 0L + + val APP = ApplicationInfo().apply { + packageName = TEST_PACKAGE_NAME + } + val APP_NO_RECORD = ApplicationInfo().apply { + packageName = TEST_NO_MATCH_PACKAGE_NAME + } + val APP_RECORD_WITH_PACKAGE_MATCH = BackgroundInstalledAppListWithGroupingAppRecord( + APP, + TEST_FIRST_INSTALL_TIME + ) + val APP_RECORD_WITHOUT_PACKAGE_MATCH = BackgroundInstalledAppListWithGroupingAppRecord( + APP_NO_RECORD, + TEST_FIRST_INSTALL_TIME + ) + val PACKAGE_INFO = PackageInfo().apply { + packageName = TEST_PACKAGE_NAME + applicationInfo = APP + firstInstallTime = TEST_FIRST_INSTALL_TIME + } + } +} \ No newline at end of file