Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 4f894679 authored by George Chan's avatar George Chan Committed by Android (Google) Code Review
Browse files

Merge "Added Background install control UI code."

parents e0fa3d33 d18b422c
Loading
Loading
Loading
Loading
+25 −0
Original line number Diff line number Diff line
@@ -11416,4 +11416,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>
+2 −0
Original line number Diff line number Diff line
@@ -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),
+4 −0
Original line number Diff line number Diff line
@@ -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(),
        )
    }
}
+264 −0
Original line number Diff line number Diff line
/*
 * 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
    }
}
+319 −0

File added.

Preview size limit exceeded, changes collapsed.