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