1 /*
<lambda>null2 * Copyright (C) 2023 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.appcompat
18
19 import android.app.settings.SettingsEnums
20 import android.content.Context
21 import android.content.pm.ApplicationInfo
22 import android.content.pm.PackageInfo
23 import android.content.pm.PackageManager
24 import android.content.pm.PackageManager.GET_ACTIVITIES
25 import android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_APP_DEFAULT
26 import android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_UNSET
27 import android.os.Build
28 import android.os.Bundle
29 import android.util.Log
30 import androidx.compose.foundation.layout.Box
31 import androidx.compose.foundation.layout.padding
32 import androidx.compose.runtime.Composable
33 import androidx.compose.runtime.getValue
34 import androidx.compose.runtime.remember
35 import androidx.compose.ui.Modifier
36 import androidx.compose.ui.res.stringResource
37 import androidx.lifecycle.compose.collectAsStateWithLifecycle
38 import com.android.settings.R
39 import com.android.settings.applications.appcompat.UserAspectRatioManager
40 import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
41 import com.android.settingslib.spa.framework.common.SettingsPageProvider
42 import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
43 import com.android.settingslib.spa.framework.common.createSettingsPage
44 import com.android.settingslib.spa.framework.compose.navigator
45 import com.android.settingslib.spa.framework.compose.rememberContext
46 import com.android.settingslib.spa.framework.theme.SettingsDimension
47 import com.android.settingslib.spa.framework.util.asyncMap
48 import com.android.settingslib.spa.framework.util.filterItem
49 import com.android.settingslib.spa.widget.illustration.Illustration
50 import com.android.settingslib.spa.widget.illustration.IllustrationModel
51 import com.android.settingslib.spa.widget.illustration.ResourceType
52 import com.android.settingslib.spa.widget.preference.Preference
53 import com.android.settingslib.spa.widget.preference.PreferenceModel
54 import com.android.settingslib.spa.widget.ui.SettingsBody
55 import com.android.settingslib.spa.widget.ui.SpinnerOption
56 import com.android.settingslib.spaprivileged.model.app.AppListModel
57 import com.android.settingslib.spaprivileged.model.app.AppRecord
58 import com.android.settingslib.spaprivileged.model.app.userId
59 import com.android.settingslib.spaprivileged.template.app.AppList
60 import com.android.settingslib.spaprivileged.template.app.AppListInput
61 import com.android.settingslib.spaprivileged.template.app.AppListItem
62 import com.android.settingslib.spaprivileged.template.app.AppListItemModel
63 import com.android.settingslib.spaprivileged.template.app.AppListPage
64 import com.google.common.annotations.VisibleForTesting
65 import kotlinx.coroutines.CoroutineDispatcher
66 import kotlinx.coroutines.Dispatchers
67 import kotlinx.coroutines.flow.Flow
68 import kotlinx.coroutines.flow.combine
69 import kotlinx.coroutines.flow.flow
70 import kotlinx.coroutines.flow.flowOn
71
72 object UserAspectRatioAppsPageProvider : SettingsPageProvider {
73 override val name = "UserAspectRatioAppsPage"
74 private val owner = createSettingsPage()
75
76 override fun isEnabled(arguments: Bundle?): Boolean =
77 UserAspectRatioManager.isFeatureEnabled(SpaEnvironmentFactory.instance.appContext)
78
79 @Composable
80 override fun Page(arguments: Bundle?) =
81 UserAspectRatioAppList()
82
83 @Composable
84 @VisibleForTesting
85 fun EntryItem() {
86 val summary = getSummary()
87 Preference(object : PreferenceModel {
88 override val title = stringResource(R.string.aspect_ratio_experimental_title)
89 override val summary = { summary }
90 override val onClick = navigator(name)
91 })
92 }
93
94 @VisibleForTesting
95 fun buildInjectEntry() = SettingsEntryBuilder
96 .createInject(owner)
97 .setSearchDataFn { null }
98 .setUiLayoutFn { EntryItem() }
99
100 @Composable
101 @VisibleForTesting
102 fun getSummary(): String = stringResource(R.string.aspect_ratio_summary_text, Build.MODEL)
103 }
104
105 @Composable
UserAspectRatioAppListnull106 fun UserAspectRatioAppList(
107 appList: @Composable AppListInput<UserAspectRatioAppListItemModel>.() -> Unit
108 = { AppList() },
109 ) {
110 AppListPage(
111 title = stringResource(R.string.aspect_ratio_experimental_title),
112 listModel = rememberContext(::UserAspectRatioAppListModel),
113 appList = appList,
<lambda>null114 header = {
115 Box(Modifier.padding(SettingsDimension.itemPadding)) {
116 SettingsBody(stringResource(R.string.aspect_ratio_main_summary_text, Build.MODEL))
117 }
118 Illustration(object : IllustrationModel {
119 override val resId = R.raw.user_aspect_ratio_education
120 override val resourceType = ResourceType.LOTTIE
121 })
122 },
123 noMoreOptions = true,
124 )
125 }
126
127 data class UserAspectRatioAppListItemModel(
128 override val app: ApplicationInfo,
129 val userOverride: Int,
130 val suggested: Boolean,
131 val canDisplay: Boolean,
132 ) : AppRecord
133
134 class UserAspectRatioAppListModel(
135 private val context: Context,
136 private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO
137 ) : AppListModel<UserAspectRatioAppListItemModel> {
138
139 private val packageManager = context.packageManager
140 private val userAspectRatioManager = UserAspectRatioManager(context)
141
getSpinnerOptionsnull142 override fun getSpinnerOptions(
143 recordList: List<UserAspectRatioAppListItemModel>
144 ): List<SpinnerOption> {
145 val hasSuggested = recordList.any { it.suggested }
146 val hasOverride = recordList.any {
147 userAspectRatioManager.isAppOverridden(it.app, it.userOverride)
148 }
149 val options = mutableListOf(SpinnerItem.All)
150 // Add suggested filter first as default
151 if (hasSuggested) options.add(0, SpinnerItem.Suggested)
152 if (hasOverride) options += SpinnerItem.Overridden
153 return options.map {
154 SpinnerOption(
155 id = it.ordinal,
156 text = context.getString(it.stringResId),
157 )
158 }
159 }
160
161 @Composable
AppItemnull162 override fun AppListItemModel<UserAspectRatioAppListItemModel>.AppItem() {
163 val app = record.app
164 AppListItem(
165 onClick = {
166 navigateToAppAspectRatioSettings(
167 context,
168 app,
169 SettingsEnums.USER_ASPECT_RATIO_APP_LIST_SETTINGS
170 )
171 }
172 )
173 }
174
transformnull175 override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) =
176 userIdFlow.combine(appListFlow) { uid, appList ->
177 appList.asyncMap { app ->
178 UserAspectRatioAppListItemModel(
179 app = app,
180 suggested = !app.isSystemApp && getPackageAndActivityInfo(
181 app)?.isFixedOrientationOrAspectRatio() == true,
182 userOverride = userAspectRatioManager.getUserMinAspectRatioValue(
183 app.packageName, uid),
184 canDisplay = userAspectRatioManager.canDisplayAspectRatioUi(app),
185 )
186 }
187 }
188
filternull189 override fun filter(
190 userIdFlow: Flow<Int>,
191 option: Int,
192 recordListFlow: Flow<List<UserAspectRatioAppListItemModel>>
193 ): Flow<List<UserAspectRatioAppListItemModel>> = recordListFlow.filterItem(
194 when (SpinnerItem.entries.getOrNull(option)) {
195 SpinnerItem.Suggested -> ({ it.canDisplay && it.suggested })
196 SpinnerItem.Overridden -> ({
197 userAspectRatioManager.isAppOverridden(it.app, it.userOverride)
198 })
199 else -> ({ it.canDisplay })
200 }
201 )
202
203 @Composable
getSummarynull204 override fun getSummary(option: Int, record: UserAspectRatioAppListItemModel): () -> String {
205 val summary by remember(record.userOverride) {
206 flow {
207 emit(userAspectRatioManager.getUserMinAspectRatioEntry(record.userOverride,
208 record.app.packageName, record.app.userId))
209 }.flowOn(ioDispatcher)
210 }.collectAsStateWithLifecycle(initialValue = stringResource(R.string.summary_placeholder))
211 return { summary }
212 }
213
getPackageAndActivityInfonull214 private fun getPackageAndActivityInfo(app: ApplicationInfo): PackageInfo? = try {
215 packageManager.getPackageInfoAsUser(app.packageName, GET_ACTIVITIES_FLAGS, app.userId)
216 } catch (e: Exception) {
217 // Query PackageManager.getPackageInfoAsUser() with GET_ACTIVITIES_FLAGS could cause
218 // exception sometimes. Since we reply on this flag to retrieve the Picture In Picture
219 // packages, we need to catch the exception to alleviate the impact before PackageManager
220 // fixing this issue or provide a better api.
221 Log.e(TAG, "Exception while getPackageInfoAsUser", e)
222 null
223 }
224
225 companion object {
226 private const val TAG = "AspectRatioAppsListModel"
isFixedOrientationOrAspectRationull227 private fun PackageInfo.isFixedOrientationOrAspectRatio() =
228 activities?.any { a -> a.isFixedOrientation || a.hasFixedAspectRatio() } ?: false
229 private val GET_ACTIVITIES_FLAGS =
230 PackageManager.PackageInfoFlags.of(GET_ACTIVITIES.toLong())
231 }
232 }
233
234 private enum class SpinnerItem(val stringResId: Int) {
235 Suggested(R.string.user_aspect_ratio_suggested_apps_label),
236 All(R.string.filter_all_apps),
237 Overridden(R.string.user_aspect_ratio_changed_apps_label)
238 }