1 /*
<lambda>null2 * Copyright (C) 2022 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17 package com.android.settings.spa.app.backgroundinstall
18
19 import android.content.Context
20 import android.content.pm.ApplicationInfo
21 import android.content.pm.IBackgroundInstallControlService
22 import android.content.pm.PackageInfo
23 import android.content.pm.PackageManager
24 import android.content.pm.ParceledListSlice
25 import android.os.Bundle
26 import android.os.ServiceManager
27 import android.provider.DeviceConfig
28 import android.util.Log
29 import androidx.compose.foundation.layout.Box
30 import androidx.compose.foundation.layout.padding
31 import androidx.compose.material.icons.Icons
32 import androidx.compose.material.icons.outlined.Delete
33 import androidx.compose.runtime.Composable
34 import androidx.compose.runtime.State
35 import androidx.compose.runtime.getValue
36 import androidx.compose.runtime.produceState
37 import androidx.compose.ui.Modifier
38 import androidx.compose.ui.platform.LocalContext
39 import androidx.compose.ui.res.stringResource
40 import com.android.settings.R
41 import com.android.settings.spa.app.appinfo.AppInfoSettingsProvider
42 import com.android.settings.spa.app.startUninstallActivity
43 import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
44 import com.android.settingslib.spa.framework.common.SettingsPageProvider
45 import com.android.settingslib.spa.framework.common.createSettingsPage
46 import com.android.settingslib.spa.framework.compose.navigator
47 import com.android.settingslib.spa.framework.compose.rememberContext
48 import com.android.settingslib.spa.framework.theme.SettingsDimension
49 import com.android.settingslib.spa.framework.util.asyncMap
50 import com.android.settingslib.spa.framework.util.formatString
51 import com.android.settingslib.spa.widget.preference.Preference
52 import com.android.settingslib.spa.widget.preference.PreferenceModel
53 import com.android.settingslib.spa.widget.ui.SettingsBody
54 import com.android.settingslib.spaprivileged.model.app.AppEntry
55 import com.android.settingslib.spaprivileged.model.app.AppListModel
56 import com.android.settingslib.spaprivileged.model.app.AppRecord
57 import com.android.settingslib.spaprivileged.model.app.userHandle
58 import com.android.settingslib.spaprivileged.template.app.AppList
59 import com.android.settingslib.spaprivileged.template.app.AppListButtonItem
60 import com.android.settingslib.spaprivileged.template.app.AppListInput
61 import com.android.settingslib.spaprivileged.template.app.AppListItemModel
62 import com.android.settingslib.spaprivileged.template.app.AppListPage
63 import com.google.common.annotations.VisibleForTesting
64 import kotlinx.coroutines.Dispatchers
65 import kotlinx.coroutines.flow.Flow
66 import kotlinx.coroutines.flow.combine
67 import kotlinx.coroutines.flow.flowOf
68 import kotlinx.coroutines.withContext
69
70 private const val KEY_GROUPING_MONTH = "key_grouping_by_month"
71 const val DEFAULT_GROUPING_MONTH_VALUE = 6
72 const val MONTH_IN_MILLIS = 2629800000L
73 const val KEY_BIC_UI_ENABLED = "key_bic_ui_enabled"
74 const val BACKGROUND_INSTALL_CONTROL_FLAG = PackageManager.MATCH_ALL.toLong()
75
76 object BackgroundInstalledAppsPageProvider : SettingsPageProvider {
77 override val name = "BackgroundInstalledAppsPage"
78 private val owner = createSettingsPage()
79 private var backgroundInstallService = IBackgroundInstallControlService.Stub.asInterface(
80 ServiceManager.getService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE))
81 private var featureIsDisabled = featureIsDisabled()
82
83 @Composable
84 override fun Page(arguments: Bundle?) {
85 if(featureIsDisabled) return
86 BackgroundInstalledAppList()
87 }
88
89 @Composable
90 fun EntryItem() {
91 if(featureIsDisabled) return
92 val summary by generatePreferenceSummary()
93 Preference(object : PreferenceModel {
94 override val title = stringResource(R.string.background_install_title)
95 override val summary = { summary }
96 override val onClick = navigator(name)
97 })
98 }
99
100 fun buildInjectEntry() = SettingsEntryBuilder
101 .createInject(owner)
102 .setSearchDataFn { null }
103 .setUiLayoutFn { EntryItem() }
104
105 private fun featureIsDisabled() = !DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI,
106 KEY_BIC_UI_ENABLED, false)
107
108 @Composable
109 private fun generatePreferenceSummary(): State<String> {
110 val context = LocalContext.current
111 return produceState(initialValue = stringResource(R.string.summary_placeholder)) {
112 withContext(Dispatchers.IO) {
113 val backgroundInstalledApps =
114 backgroundInstallService.getBackgroundInstalledPackages(
115 BACKGROUND_INSTALL_CONTROL_FLAG, context.user.identifier
116 ).list.size
117 value = context.formatString(
118 R.string.background_install_preference_summary,
119 "count" to backgroundInstalledApps
120 )
121 }
122 }
123 }
124
125 @VisibleForTesting
126 fun setDisableFeature(disableFeature : Boolean): BackgroundInstalledAppsPageProvider {
127 featureIsDisabled = disableFeature
128 return this
129 }
130
131 @VisibleForTesting
132 fun setBackgroundInstallControlService(bic: IBackgroundInstallControlService):
133 BackgroundInstalledAppsPageProvider {
134 backgroundInstallService = bic
135 return this
136 }
137 }
138
139 @Composable
BackgroundInstalledAppListnull140 fun BackgroundInstalledAppList(
141 appList: @Composable AppListInput<BackgroundInstalledAppListWithGroupingAppRecord>.() -> Unit
142 = { AppList() },
143 ) {
144 AppListPage(
145 title = stringResource(R.string.background_install_title),
146 listModel = rememberContext(::BackgroundInstalledAppsWithGroupingListModel),
147 noItemMessage = stringResource(R.string.background_install_feature_list_no_entry),
148 appList = appList,
<lambda>null149 header = {
150 Box(Modifier.padding(SettingsDimension.itemPadding)) {
151 SettingsBody(stringResource(R.string.background_install_summary))
152 }
153 }
154 )
155 }
156
157 data class BackgroundInstalledAppListWithGroupingAppRecord(
158 override val app: ApplicationInfo,
159 val dateOfInstall: Long,
160 ) : AppRecord
161
162 class BackgroundInstalledAppsWithGroupingListModel(private val context: Context)
163 : AppListModel<BackgroundInstalledAppListWithGroupingAppRecord> {
164
165 companion object {
166 private const val tag = "AppListModel<BackgroundInstalledAppListWithGroupingAppRecord>"
167 }
168
169 private var backgroundInstallService = IBackgroundInstallControlService.Stub.asInterface(
170 ServiceManager.getService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE))
171
172 @VisibleForTesting
setBackgroundInstallControlServicenull173 fun setBackgroundInstallControlService(bic: IBackgroundInstallControlService) {
174 backgroundInstallService = bic
175 }
176 @Composable
AppItemnull177 override fun AppListItemModel<BackgroundInstalledAppListWithGroupingAppRecord>.AppItem() {
178 val context = LocalContext.current
179 val app = record.app
180 AppListButtonItem(
181 onClick = AppInfoSettingsProvider.navigator(app = app),
182 onButtonClick = { context.startUninstallActivity(app.packageName, app.userHandle) },
183 buttonIcon = Icons.Outlined.Delete,
184 buttonIconDescription = stringResource(
185 R.string.background_install_uninstall_button_description))
186 }
187
transformnull188 override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) =
189 userIdFlow.combine(appListFlow) { userId, appList ->
190 appList.asyncMap { app ->
191 BackgroundInstalledAppListWithGroupingAppRecord(
192 app = app,
193 dateOfInstall = context.packageManager.getPackageInfoAsUser(app.packageName,
194 PackageManager.PackageInfoFlags.of(0), userId).firstInstallTime
195 )
196 }
197 }
198
filternull199 override fun filter(
200 userIdFlow: Flow<Int>,
201 option: Int,
202 recordListFlow: Flow<List<BackgroundInstalledAppListWithGroupingAppRecord>>
203 ): Flow<List<BackgroundInstalledAppListWithGroupingAppRecord>> {
204 if(backgroundInstallService == null) {
205 Log.e(tag, "Failed to retrieve Background Install Control Service")
206 return flowOf()
207 }
208 return userIdFlow.combine(recordListFlow) { userId, recordList ->
209 @Suppress("UNCHECKED_CAST")
210 val appList = (backgroundInstallService.getBackgroundInstalledPackages(
211 PackageManager.MATCH_ALL.toLong(), userId) as ParceledListSlice<PackageInfo>).list
212 val appNameList = appList.map { it.packageName }
213 recordList.filter { record -> record.app.packageName in appNameList }
214 }
215 }
216
getComparatornull217 override fun getComparator(
218 option: Int,
219 ): Comparator<AppEntry<BackgroundInstalledAppListWithGroupingAppRecord>> =
220 compareByDescending { it.record.dateOfInstall }
221
getGroupTitlenull222 override fun getGroupTitle(option: Int, record: BackgroundInstalledAppListWithGroupingAppRecord)
223 : String {
224 val groupByMonth = getGroupSeparationByMonth()
225 return when (record.dateOfInstall > System.currentTimeMillis()
226 - (groupByMonth * MONTH_IN_MILLIS)) {
227 true -> context.formatString(R.string.background_install_before, "count" to groupByMonth)
228 else -> context.formatString(R.string.background_install_after, "count" to groupByMonth)
229 }
230 }
231 }
232
getGroupSeparationByMonthnull233 private fun getGroupSeparationByMonth(): Int {
234 val month = DeviceConfig.getProperty(DeviceConfig.NAMESPACE_SETTINGS_UI, KEY_GROUPING_MONTH)
235 return try {
236 if (month.isNullOrBlank()) {
237 DEFAULT_GROUPING_MONTH_VALUE
238 } else {
239 month.toInt()
240 }
241 } catch (e: Exception) {
242 Log.d(
243 BackgroundInstalledAppsPageProvider.name, "Error parsing list grouping value: " +
244 "${e.message} falling back to default value: $DEFAULT_GROUPING_MONTH_VALUE")
245 DEFAULT_GROUPING_MONTH_VALUE
246 }
247 }
248