[2/n] Add aspect ratio app list page under apps

Apps > General > Screen Size

To enable feature:
adb shell device_config put window_manager enable_app_compat_user_aspect_ratio_settings true
adb shell am force-stop com.android.settings

Fix: 287448088
Test: Manual
      atest AspectRatioAppsPageProviderTest
      atest AspectRatioUtilsTest
      All CUJs passed in go/settings-cujs
Change-Id: I4de6c3cbdbdfbc79ed839ec149fb633344dcd3a7
This commit is contained in:
Graciela Wissen Putri
2023-07-04 15:10:29 +00:00
parent a94cfc7b76
commit b4c4f6cbe6
12 changed files with 796 additions and 0 deletions

View File

@@ -20,6 +20,7 @@ import android.content.Context
import android.util.FeatureFlagUtils
import com.android.settings.spa.app.AllAppListPageProvider
import com.android.settings.spa.app.AppsMainPageProvider
import com.android.settings.spa.app.appcompat.UserAspectRatioAppsPageProvider
import com.android.settings.spa.app.appinfo.AppInfoSettingsProvider
import com.android.settings.spa.app.appinfo.CloneAppInfoSettingsProvider
import com.android.settings.spa.app.backgroundinstall.BackgroundInstalledAppsPageProvider
@@ -86,6 +87,7 @@ open class SettingsSpaEnvironment(context: Context) : SpaEnvironment(context) {
UsageStatsPageProvider,
PlatformCompatAppListPageProvider,
BackgroundInstalledAppsPageProvider,
UserAspectRatioAppsPageProvider,
CloneAppInfoSettingsProvider,
NetworkAndInternetPageProvider,
) + togglePermissionAppListTemplate.createPageProviders(),

View File

@@ -0,0 +1,214 @@
/*
* Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.settings.spa.app.appcompat
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.content.pm.PackageManager.GET_ACTIVITIES
import android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_UNSET
import android.os.Build
import android.os.Bundle
import android.util.Log
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.State
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.ExperimentalLifecycleComposeApi
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.settings.R
import com.android.settings.applications.appcompat.UserAspectRatioManager
import com.android.settings.spa.app.appinfo.AppInfoSettingsProvider
import com.android.settingslib.spa.framework.common.SettingsEntryBuilder
import com.android.settingslib.spa.framework.common.SettingsPageProvider
import com.android.settingslib.spa.framework.common.SpaEnvironmentFactory
import com.android.settingslib.spa.framework.common.createSettingsPage
import com.android.settingslib.spa.framework.compose.navigator
import com.android.settingslib.spa.framework.compose.rememberContext
import com.android.settingslib.spa.framework.compose.toState
import com.android.settingslib.spa.framework.theme.SettingsDimension
import com.android.settingslib.spa.framework.util.asyncMap
import com.android.settingslib.spa.framework.util.filterItem
import com.android.settingslib.spa.widget.preference.Preference
import com.android.settingslib.spa.widget.preference.PreferenceModel
import com.android.settingslib.spa.widget.ui.SettingsBody
import com.android.settingslib.spa.widget.ui.SpinnerOption
import com.android.settingslib.spaprivileged.model.app.AppListModel
import com.android.settingslib.spaprivileged.model.app.AppRecord
import com.android.settingslib.spaprivileged.model.app.userId
import com.android.settingslib.spaprivileged.template.app.AppList
import com.android.settingslib.spaprivileged.template.app.AppListInput
import com.android.settingslib.spaprivileged.template.app.AppListItem
import com.android.settingslib.spaprivileged.template.app.AppListItemModel
import com.android.settingslib.spaprivileged.template.app.AppListPage
import com.google.common.annotations.VisibleForTesting
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flow
import kotlinx.coroutines.flow.flowOn
object UserAspectRatioAppsPageProvider : SettingsPageProvider {
override val name = "UserAspectRatioAppsPage"
private val owner = createSettingsPage()
override fun isEnabled(arguments: Bundle?): Boolean =
UserAspectRatioManager.isFeatureEnabled(SpaEnvironmentFactory.instance.appContext)
@Composable
override fun Page(arguments: Bundle?) =
UserAspectRatioAppList()
@Composable
@VisibleForTesting
fun EntryItem() =
Preference(object : PreferenceModel {
override val title = stringResource(R.string.screen_size_title)
override val summary = getSummary().toState()
override val onClick = navigator(name)
})
@VisibleForTesting
fun buildInjectEntry() = SettingsEntryBuilder
.createInject(owner)
.setSearchDataFn { null }
.setUiLayoutFn { EntryItem() }
@Composable
@VisibleForTesting
fun getSummary(): String = stringResource(R.string.screen_size_summary, Build.MODEL)
}
@Composable
fun UserAspectRatioAppList(
appList: @Composable AppListInput<UserAspectRatioAppListItemModel>.() -> Unit
= { AppList() },
) {
AppListPage(
title = stringResource(R.string.screen_size_title),
listModel = rememberContext(::UserAspectRatioAppListModel),
appList = appList,
header = {
Box(Modifier.padding(SettingsDimension.itemPadding)) {
SettingsBody(UserAspectRatioAppsPageProvider.getSummary())
}
}
)
}
data class UserAspectRatioAppListItemModel(
override val app: ApplicationInfo,
val override: Int,
val suggested: Boolean,
val canDisplay: Boolean,
) : AppRecord
class UserAspectRatioAppListModel(private val context: Context)
: AppListModel<UserAspectRatioAppListItemModel> {
private val packageManager = context.packageManager
private val userAspectRatioManager = UserAspectRatioManager(context)
override fun getSpinnerOptions(
recordList: List<UserAspectRatioAppListItemModel>
): List<SpinnerOption> {
val hasSuggested = recordList.any { it.suggested }
val hasOverride = recordList.any { it.override != USER_MIN_ASPECT_RATIO_UNSET }
val options = mutableListOf(SpinnerItem.All)
// Add suggested filter first as default
if (hasSuggested) options.add(0, SpinnerItem.Suggested)
if (hasOverride) options += SpinnerItem.Overridden
return options.map {
SpinnerOption(
id = it.ordinal,
text = context.getString(it.stringResId),
)
}
}
@Composable
override fun AppListItemModel<UserAspectRatioAppListItemModel>.AppItem() {
val app = record.app
AppListItem(
onClick = AppInfoSettingsProvider.navigator(app)
)
}
override fun transform(userIdFlow: Flow<Int>, appListFlow: Flow<List<ApplicationInfo>>) =
userIdFlow.combine(appListFlow) { uid, appList ->
appList.asyncMap { app ->
UserAspectRatioAppListItemModel(
app = app,
suggested = !app.isSystemApp && getPackageAndActivityInfo(
app)?.isFixedOrientationOrAspectRatio() == true,
override = userAspectRatioManager.getUserMinAspectRatioValue(
app.packageName, uid),
canDisplay = userAspectRatioManager.canDisplayAspectRatioUi(app),
)
}
}
override fun filter(
userIdFlow: Flow<Int>,
option: Int,
recordListFlow: Flow<List<UserAspectRatioAppListItemModel>>
): Flow<List<UserAspectRatioAppListItemModel>> = recordListFlow.filterItem(
when (SpinnerItem.values().getOrNull(option)) {
SpinnerItem.Suggested -> ({ it.canDisplay && it.suggested })
SpinnerItem.Overridden -> ({ it.override != USER_MIN_ASPECT_RATIO_UNSET })
else -> ({ it.canDisplay })
}
)
@OptIn(ExperimentalLifecycleComposeApi::class)
@Composable
override fun getSummary(option: Int, record: UserAspectRatioAppListItemModel) : State<String> =
remember(record.override) {
flow {
emit(userAspectRatioManager.getUserMinAspectRatioEntry(record.override))
}.flowOn(Dispatchers.IO)
}.collectAsStateWithLifecycle(initialValue = stringResource(R.string.summary_placeholder))
private fun getPackageAndActivityInfo(app: ApplicationInfo): PackageInfo? = try {
packageManager.getPackageInfoAsUser(app.packageName, GET_ACTIVITIES_FLAGS, app.userId)
} catch (e: Exception) {
// Query PackageManager.getPackageInfoAsUser() with GET_ACTIVITIES_FLAGS could cause
// exception sometimes. Since we reply on this flag to retrieve the Picture In Picture
// packages, we need to catch the exception to alleviate the impact before PackageManager
// fixing this issue or provide a better api.
Log.e(TAG, "Exception while getPackageInfoAsUser", e)
null
}
companion object {
private const val TAG = "AspectRatioAppsListModel"
private fun PackageInfo.isFixedOrientationOrAspectRatio() =
activities?.any { a -> a.isFixedOrientation || a.hasFixedAspectRatio() } ?: false
private val GET_ACTIVITIES_FLAGS =
PackageManager.PackageInfoFlags.of(GET_ACTIVITIES.toLong())
}
}
private enum class SpinnerItem(val stringResId: Int) {
Suggested(R.string.user_aspect_ratio_suggested_apps_label),
All(R.string.filter_all_apps),
Overridden(R.string.user_aspect_ratio_overridden_apps_label)
}