Added Background install control UI code.

Change-Id: I1b629fdc04d1df1b08998c9aaae3df3446fab3fe
Bug: 238451991
Test: Manually with settings, atest
diff --git a/res/values/strings.xml b/res/values/strings.xml
index 36b8ed6..af830c6 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -11369,4 +11369,29 @@
     <!-- [CHAR LIMIT=NONE] Title for preference: Transfer eSIM to another device -->
     <string name="transfer_esim_to_another_device_title">Transfer eSIM to another device</string>
 
+    <!-- Background Install Control UI -->
+    <!-- [CHAR LIMIT=NONE] Preference Feature Summary -->
+    <string name="background_install_preference_summary">{count, plural,
+    =1    {# app}
+    other {# apps}
+    }</string>
+
+    <!-- [CHAR LIMIT=NONE] Feature Title -->
+    <string name="background_install_title">Apps installed in the background</string>
+    <!-- [CHAR LIMIT=NONE] Feature summary -->
+    <string name="background_install_summary">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. </string>
+    <!-- [CHAR LIMIT=NONE] Group list no entry -->
+    <string name="background_install_feature_list_no_entry">No apps installed in the background</string>
+    <!-- [CHAR LIMIT=NONE] Uninstall app button content description -->
+    <string name="background_install_uninstall_button_description">Uninstall app</string>
+    <!-- [CHAR LIMIT=NONE] Before time period group list title -->
+    <string name="background_install_before">{count, plural,
+    =1    {Apps installed in the last # month}
+    other {Apps installed in the last # months}
+    }</string>
+    <!-- [CHAR LIMIT=NONE] After time period group list title -->
+    <string name="background_install_after">{count, plural,
+    =1    {Apps installed more than # month ago}
+    other {Apps installed more than # months ago}
+    }</string>
 </resources>
diff --git a/src/com/android/settings/spa/SettingsSpaEnvironment.kt b/src/com/android/settings/spa/SettingsSpaEnvironment.kt
index bf15ddf..9eab400 100644
--- a/src/com/android/settings/spa/SettingsSpaEnvironment.kt
+++ b/src/com/android/settings/spa/SettingsSpaEnvironment.kt
@@ -20,6 +20,7 @@
 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 @@
                 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 4b47278..e9eca1f 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.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 @@
         RegularScaffold(title = getTitle(arguments)) {
             AllAppListPageProvider.buildInjectEntry().build().UiLayout()
             SpecialAppAccessPageProvider.EntryItem()
+            BackgroundInstalledAppsPageProvider.EntryItem()
         }
     }
 
@@ -70,6 +73,7 @@
         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 0000000..9cf9516
--- /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<String> {
+        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<BackgroundInstalledAppListWithGroupingAppRecord>.() -> 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<BackgroundInstalledAppListWithGroupingAppRecord> {
+
+    companion object {
+        private const val tag = "AppListModel<BackgroundInstalledAppListWithGroupingAppRecord>"
+    }
+
+    private var backgroundInstallService = IBackgroundInstallControlService.Stub.asInterface(
+        ServiceManager.getService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE))
+
+    @VisibleForTesting
+    fun setBackgroundInstallControlService(bic: IBackgroundInstallControlService) {
+        backgroundInstallService = bic
+    }
+    @Composable
+    override fun AppListItemModel<BackgroundInstalledAppListWithGroupingAppRecord>.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<Int>, appListFlow: Flow<List<ApplicationInfo>>) =
+        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<Int>,
+            option: Int,
+            recordListFlow: Flow<List<BackgroundInstalledAppListWithGroupingAppRecord>>
+    ): Flow<List<BackgroundInstalledAppListWithGroupingAppRecord>> {
+        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<PackageInfo>).list
+            val appNameList = appList.map { it.packageName }
+            recordList.filter { record -> record.app.packageName in appNameList }
+        }
+    }
+
+    override fun getComparator(
+            option: Int,
+    ): Comparator<AppEntry<BackgroundInstalledAppListWithGroupingAppRecord>> =
+            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 0000000..8e1757f
--- /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