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.produceState
36 import androidx.compose.ui.Modifier
37 import androidx.compose.ui.platform.LocalContext
38 import androidx.compose.ui.res.stringResource
39 import com.android.settings.R
40 import com.android.settings.spa.app.appinfo.AppInfoSettingsProvider
41 import com.android.settings.spa.app.startUninstallActivity
42 import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
43 import com.android.settingslib.spa.framework.common.SettingsPageProvider
44 import com.android.settingslib.spa.framework.common.createSettingsPage
45 import com.android.settingslib.spa.framework.compose.navigator
46 import com.android.settingslib.spa.framework.compose.rememberContext
47 import com.android.settingslib.spa.framework.theme.SettingsDimension
48 import com.android.settingslib.spa.framework.util.asyncMap
49 import com.android.settingslib.spa.framework.util.formatString
50 import com.android.settingslib.spa.widget.preference.Preference
51 import com.android.settingslib.spa.widget.preference.PreferenceModel
52 import com.android.settingslib.spa.widget.ui.SettingsBody
53 import com.android.settingslib.spaprivileged.model.app.AppEntry
54 import com.android.settingslib.spaprivileged.model.app.AppListModel
55 import com.android.settingslib.spaprivileged.model.app.AppRecord
56 import com.android.settingslib.spaprivileged.model.app.userHandle
57 import com.android.settingslib.spaprivileged.template.app.AppList
58 import com.android.settingslib.spaprivileged.template.app.AppListButtonItem
59 import com.android.settingslib.spaprivileged.template.app.AppListInput
60 import com.android.settingslib.spaprivileged.template.app.AppListItemModel
61 import com.android.settingslib.spaprivileged.template.app.AppListPage
62 import com.google.common.annotations.VisibleForTesting
63 import kotlinx.coroutines.Dispatchers
64 import kotlinx.coroutines.flow.Flow
65 import kotlinx.coroutines.flow.combine
66 import kotlinx.coroutines.flow.flowOf
67 import kotlinx.coroutines.withContext
68
69 private const val KEY_GROUPING_MONTH = "key_grouping_by_month"
70 const val DEFAULT_GROUPING_MONTH_VALUE = 6
71 const val MONTH_IN_MILLIS = 2629800000L
72 const val KEY_BIC_UI_ENABLED = "key_bic_ui_enabled"
73 const val BACKGROUND_INSTALL_CONTROL_FLAG = PackageManager.MATCH_ALL.toLong()
74
75 object BackgroundInstalledAppsPageProvider : SettingsPageProvider {
76 override val name = "BackgroundInstalledAppsPage"
77 private val owner = createSettingsPage()
78 private var backgroundInstallService = IBackgroundInstallControlService.Stub.asInterface(
79 ServiceManager.getService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE))
80 private var featureIsDisabled = featureIsDisabled()
81
82 @Composable
83 override fun Page(arguments: Bundle?) {
84 if(featureIsDisabled) return
85 BackgroundInstalledAppList()
86 }
87
88 @Composable
89 fun EntryItem() {
90 if(featureIsDisabled) return
91 Preference(object : PreferenceModel {
92 override val title = stringResource(R.string.background_install_title)
93 override val summary = generatePreferenceSummary()
94 override val onClick = navigator(name)
95 })
96 }
97
98 fun buildInjectEntry() = SettingsEntryBuilder
99 .createInject(owner)
100 .setSearchDataFn { null }
101 .setUiLayoutFn { EntryItem() }
102
103 private fun featureIsDisabled() = !DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_SETTINGS_UI,
104 KEY_BIC_UI_ENABLED, false)
105
106 @Composable
107 private fun generatePreferenceSummary(): State<String> {
108 val context = LocalContext.current
109 return produceState(initialValue = stringResource(R.string.summary_placeholder)) {
110 withContext(Dispatchers.IO) {
111 val backgroundInstalledApps =
112 backgroundInstallService.getBackgroundInstalledPackages(
113 BACKGROUND_INSTALL_CONTROL_FLAG, context.user.identifier
114 ).list.size
115 value = context.formatString(
116 R.string.background_install_preference_summary,
117 "count" to backgroundInstalledApps
118 )
119 }
120 }
121 }
122
123 @VisibleForTesting
124 fun setDisableFeature(disableFeature : Boolean): BackgroundInstalledAppsPageProvider {
125 featureIsDisabled = disableFeature
126 return this
127 }
128
129 @VisibleForTesting
130 fun setBackgroundInstallControlService(bic: IBackgroundInstallControlService):
131 BackgroundInstalledAppsPageProvider {
132 backgroundInstallService = bic
133 return this
134 }
135 }
136
137 @Composable
BackgroundInstalledAppListnull138 fun BackgroundInstalledAppList(
139 appList: @Composable AppListInput<BackgroundInstalledAppListWithGroupingAppRecord>.() -> Unit
140 = { AppList() },
141 ) {
142 AppListPage(
143 title = stringResource(R.string.background_install_title),
144 listModel = rememberContext(::BackgroundInstalledAppsWithGroupingListModel),
145 noItemMessage = stringResource(R.string.background_install_feature_list_no_entry),
146 appList = appList,
<lambda>null147 header = {
148 Box(Modifier.padding(SettingsDimension.itemPadding)) {
149 SettingsBody(stringResource(R.string.background_install_summary))
150 }
151 }
152 )
153 }
154
155 data class BackgroundInstalledAppListWithGroupingAppRecord(
156 override val app: ApplicationInfo,
157 val dateOfInstall: Long,
158 ) : AppRecord
159
160 class BackgroundInstalledAppsWithGroupingListModel(private val context: Context)
161 : AppListModel<BackgroundInstalledAppListWithGroupingAppRecord> {
162
163 companion object {
164 private const val tag = "AppListModel<BackgroundInstalledAppListWithGroupingAppRecord>"
165 }
166
167 private var backgroundInstallService = IBackgroundInstallControlService.Stub.asInterface(
168 ServiceManager.getService(Context.BACKGROUND_INSTALL_CONTROL_SERVICE))
169
170 @VisibleForTesting
setBackgroundInstallControlServicenull171 fun setBackgroundInstallControlService(bic: IBackgroundInstallControlService) {
172 backgroundInstallService = bic
173 }
174 @Composable
AppItemnull175 override fun AppListItemModel<BackgroundInstalledAppListWithGroupingAppRecord>.AppItem() {
176 val context = LocalContext.current
177 val app = record.app
178 AppListButtonItem(
179 onClick = AppInfoSettingsProvider.navigator(app = app),
180 onButtonClick = { context.startUninstallActivity(app.packageName, app.userHandle) },
181 buttonIcon = Icons.Outlined.Delete,
182 buttonIconDescription = stringResource(
183 R.string.background_install_uninstall_button_description))
184 }
185
transformnull186 override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) =
187 userIdFlow.combine(appListFlow) { userId, appList ->
188 appList.asyncMap { app ->
189 BackgroundInstalledAppListWithGroupingAppRecord(
190 app = app,
191 dateOfInstall = context.packageManager.getPackageInfoAsUser(app.packageName,
192 PackageManager.PackageInfoFlags.of(0), userId).firstInstallTime
193 )
194 }
195 }
196
filternull197 override fun filter(
198 userIdFlow: Flow<Int>,
199 option: Int,
200 recordListFlow: Flow<List<BackgroundInstalledAppListWithGroupingAppRecord>>
201 ): Flow<List<BackgroundInstalledAppListWithGroupingAppRecord>> {
202 if(backgroundInstallService == null) {
203 Log.e(tag, "Failed to retrieve Background Install Control Service")
204 return flowOf()
205 }
206 return userIdFlow.combine(recordListFlow) { userId, recordList ->
207 @Suppress("UNCHECKED_CAST")
208 val appList = (backgroundInstallService.getBackgroundInstalledPackages(
209 PackageManager.MATCH_ALL.toLong(), userId) as ParceledListSlice<PackageInfo>).list
210 val appNameList = appList.map { it.packageName }
211 recordList.filter { record -> record.app.packageName in appNameList }
212 }
213 }
214
getComparatornull215 override fun getComparator(
216 option: Int,
217 ): Comparator<AppEntry<BackgroundInstalledAppListWithGroupingAppRecord>> =
218 compareByDescending { it.record.dateOfInstall }
219
getGroupTitlenull220 override fun getGroupTitle(option: Int, record: BackgroundInstalledAppListWithGroupingAppRecord)
221 : String {
222 val groupByMonth = getGroupSeparationByMonth()
223 return when (record.dateOfInstall > System.currentTimeMillis()
224 - (groupByMonth * MONTH_IN_MILLIS)) {
225 true -> context.formatString(R.string.background_install_before, "count" to groupByMonth)
226 else -> context.formatString(R.string.background_install_after, "count" to groupByMonth)
227 }
228 }
229 }
230
getGroupSeparationByMonthnull231 private fun getGroupSeparationByMonth(): Int {
232 val month = DeviceConfig.getProperty(DeviceConfig.NAMESPACE_SETTINGS_UI, KEY_GROUPING_MONTH)
233 return try {
234 if (month.isNullOrBlank()) {
235 DEFAULT_GROUPING_MONTH_VALUE
236 } else {
237 month.toInt()
238 }
239 } catch (e: Exception) {
240 Log.d(
241 BackgroundInstalledAppsPageProvider.name, "Error parsing list grouping value: " +
242 "${e.message} falling back to default value: $DEFAULT_GROUPING_MONTH_VALUE")
243 DEFAULT_GROUPING_MONTH_VALUE
244 }
245 }
246