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 }