[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
diff --git a/res/values/config.xml b/res/values/config.xml
index 49dcce5..432b1ca 100755
--- a/res/values/config.xml
+++ b/res/values/config.xml
@@ -611,6 +611,28 @@
<item>3</item>
</integer-array>
+ <!-- TODO(b/287448187): add USER_MIN_ASPECT_RATIO_DISPLAY_SIZE entry -->
+ <!-- App screen size settings screen, User aspect ratio override options. Must be the same
+ length and order as config_userAspectRatioOverrideValues below. -->
+ <string-array name="config_userAspectRatioOverrideEntries" translatable="false">
+ <item>@string/user_aspect_ratio_app_default</item>
+ <item>@string/user_aspect_ratio_half_screen</item>
+ <item>@string/user_aspect_ratio_16_9</item>
+ <item>@string/user_aspect_ratio_4_3</item>
+ <item>@string/user_aspect_ratio_3_2</item>
+ </string-array>
+
+ <!-- App screen size settings screen, User aspect ratio override options. Must be the same
+ length and order as config_userAspectRatioOverrideEntries above. The values must
+ correspond to PackageManager.UserMinAspectRatio -->
+ <integer-array name="config_userAspectRatioOverrideValues" translatable="false">
+ <item>0</item> <!-- USER_MIN_ASPECT_RATIO_UNSET -->
+ <item>1</item> <!-- USER_MIN_ASPECT_RATIO_SPLIT_SCREEN -->
+ <item>4</item> <!-- USER_MIN_ASPECT_RATIO_16_9 -->
+ <item>3</item> <!-- USER_MIN_ASPECT_RATIO_4_3 -->
+ <item>5</item> <!-- USER_MIN_ASPECT_RATIO_3_2 -->
+ </integer-array>
+
<!-- The settings/preference description for each settable device state defined in the array
"config_perDeviceStateRotationLockDefaults".
The item in position "i" describes the auto-rotation setting for the device state also in
diff --git a/res/values/strings.xml b/res/values/strings.xml
index ccb93e4..e7b5d13 100644
--- a/res/values/strings.xml
+++ b/res/values/strings.xml
@@ -12081,6 +12081,26 @@
other {Apps installed more than # months ago}
}</string>
+ <!-- App Screen Size (User Aspect Ratio Override) -->
+ <!-- [CHAR LIMIT=60] Screen size app list title setting to choose aspect ratio -->
+ <string name="screen_size_title">Screen size</string>
+ <!-- [CHAR LIMIT=NONE] Screen size setting summary to choose aspect ratio for apps unoptimized for device -->
+ <string name="screen_size_summary">Choose an aspect ratio for apps if they haven’t been optimized for your <xliff:g id="device_name">%1$s</xliff:g></string>
+ <!-- [CHAR LIMIT=NONE] Aspect ratio suggested apps filter label -->
+ <string name="user_aspect_ratio_suggested_apps_label">Suggested apps</string>
+ <!-- [CHAR LIMIT=NONE] Filter label for apps that have user aspect ratio override applied -->
+ <string name="user_aspect_ratio_overridden_apps_label">Apps you have overridden</string>
+ <!-- [CHAR LIMIT=NONE] App default aspect ratio entry -->
+ <string name="user_aspect_ratio_app_default">App default</string>
+ <!-- [CHAR LIMIT=NONE] Half-screen aspect ratio entry -->
+ <string name="user_aspect_ratio_half_screen">Half-screen</string>
+ <!-- [CHAR LIMIT=NONE] 16:9 aspect ratio entry -->
+ <string name="user_aspect_ratio_16_9">16:9</string>
+ <!-- [CHAR LIMIT=NONE] 3:2 aspect ratio entry -->
+ <string name="user_aspect_ratio_3_2">3:2</string>
+ <!-- [CHAR LIMIT=NONE] 4:3 aspect ratio entry -->
+ <string name="user_aspect_ratio_4_3">4:3</string>
+
<!-- Accessibility label for fingerprint sensor [CHAR LIMIT=NONE] -->
<string name="accessibility_fingerprint_label">Fingerprint sensor</string>
diff --git a/res/xml/apps.xml b/res/xml/apps.xml
index 03212c9..386a07b 100644
--- a/res/xml/apps.xml
+++ b/res/xml/apps.xml
@@ -80,6 +80,17 @@
android:order="10"/>
<Preference
+ android:key="aspect_ratio_apps"
+ android:title="@string/screen_size_title"
+ android:summary="@string/summary_placeholder"
+ android:order="14"
+ settings:controller="com.android.settings.applications.appcompat.UserAspectRatioAppsPreferenceController"
+ android:fragment="com.android.settings.applications.manageapplications.ManageApplications">
+ <extra android:name="classname"
+ android:value="com.android.settings.Settings$UserAspectRatioAppListActivity"/>
+ </Preference>
+
+ <Preference
android:key="hibernated_apps"
android:title="@string/unused_apps"
android:summary="@string/summary_placeholder"
diff --git a/src/com/android/settings/Settings.java b/src/com/android/settings/Settings.java
index 3efa18f..5249e5a 100644
--- a/src/com/android/settings/Settings.java
+++ b/src/com/android/settings/Settings.java
@@ -378,6 +378,8 @@
public static class NotificationAppListActivity extends SettingsActivity { /* empty */ }
/** Activity to manage Cloned Apps page */
public static class ClonedAppsListActivity extends SettingsActivity { /* empty */ }
+ /** Activity to manage Aspect Ratio app list page */
+ public static class UserAspectRatioAppListActivity extends SettingsActivity { /* empty */ }
public static class NotificationReviewPermissionsActivity extends SettingsActivity { /* empty */ }
public static class AppNotificationSettingsActivity extends SettingsActivity { /* empty */ }
public static class ChannelNotificationSettingsActivity extends SettingsActivity { /* empty */ }
diff --git a/src/com/android/settings/applications/appcompat/UserAspectRatioAppsPreferenceController.java b/src/com/android/settings/applications/appcompat/UserAspectRatioAppsPreferenceController.java
new file mode 100644
index 0000000..6ec2528
--- /dev/null
+++ b/src/com/android/settings/applications/appcompat/UserAspectRatioAppsPreferenceController.java
@@ -0,0 +1,48 @@
+/*
+ * 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.applications.appcompat;
+
+import android.content.Context;
+import android.os.Build;
+
+import androidx.annotation.NonNull;
+
+import com.android.settings.R;
+import com.android.settings.core.BasePreferenceController;
+
+/**
+ * Preference controller for
+ * {@link com.android.settings.spa.app.appcompat.UserAspectRatioAppsPageProvider}
+ */
+public class UserAspectRatioAppsPreferenceController extends BasePreferenceController {
+
+ public UserAspectRatioAppsPreferenceController(@NonNull Context context,
+ @NonNull String preferenceKey) {
+ super(context, preferenceKey);
+ }
+
+ @Override
+ public int getAvailabilityStatus() {
+ return UserAspectRatioManager.isFeatureEnabled(mContext)
+ ? AVAILABLE : CONDITIONALLY_UNAVAILABLE;
+ }
+
+ @Override
+ public CharSequence getSummary() {
+ return mContext.getResources().getString(R.string.screen_size_summary, Build.MODEL);
+ }
+}
diff --git a/src/com/android/settings/applications/appcompat/UserAspectRatioManager.java b/src/com/android/settings/applications/appcompat/UserAspectRatioManager.java
new file mode 100644
index 0000000..35bd7a9
--- /dev/null
+++ b/src/com/android/settings/applications/appcompat/UserAspectRatioManager.java
@@ -0,0 +1,146 @@
+/*
+ * 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.applications.appcompat;
+
+import android.app.AppGlobals;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.IPackageManager;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.RemoteException;
+import android.provider.DeviceConfig;
+import android.util.ArrayMap;
+
+import androidx.annotation.NonNull;
+
+import com.android.settings.R;
+
+import com.google.common.annotations.VisibleForTesting;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Helper class for handling app aspect ratio override
+ * {@link PackageManager.UserMinAspectRatio} set by user
+ */
+public class UserAspectRatioManager {
+ private static final Intent LAUNCHER_ENTRY_INTENT =
+ new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER);
+
+ // TODO(b/288142656): Enable user aspect ratio settings by default
+ private static final boolean DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_SETTINGS = false;
+ @VisibleForTesting
+ static final String KEY_ENABLE_USER_ASPECT_RATIO_SETTINGS =
+ "enable_app_compat_user_aspect_ratio_settings";
+
+ private final Context mContext;
+ private final IPackageManager mIPm;
+ /** Apps that have launcher entry defined in manifest */
+ private final List<ResolveInfo> mInfoHasLauncherEntryList;
+ private final Map<Integer, String> mUserAspectRatioMap;
+
+ public UserAspectRatioManager(@NonNull Context context) {
+ mContext = context;
+ mIPm = AppGlobals.getPackageManager();
+ mInfoHasLauncherEntryList = context.getPackageManager().queryIntentActivities(
+ UserAspectRatioManager.LAUNCHER_ENTRY_INTENT, PackageManager.GET_META_DATA);
+ mUserAspectRatioMap = getUserMinAspectRatioMapping();
+ }
+
+ /**
+ * Whether user aspect ratio settings is enabled for device.
+ */
+ public static boolean isFeatureEnabled(Context context) {
+ final boolean isBuildTimeFlagEnabled = context.getResources().getBoolean(
+ com.android.internal.R.bool.config_appCompatUserAppAspectRatioSettingsIsEnabled);
+ return isBuildTimeFlagEnabled && getValueFromDeviceConfig();
+ }
+
+ /**
+ * @return user-specific {@link PackageManager.UserMinAspectRatio} override for an app
+ */
+ @PackageManager.UserMinAspectRatio
+ public int getUserMinAspectRatioValue(@NonNull String packageName, int uid)
+ throws RemoteException {
+ return mIPm.getUserMinAspectRatio(packageName, uid);
+ }
+
+ /**
+ * @return corresponding string for {@link PackageManager.UserMinAspectRatio} value
+ */
+ @NonNull
+ public String getUserMinAspectRatioEntry(@PackageManager.UserMinAspectRatio int aspectRatio) {
+ return mUserAspectRatioMap.getOrDefault(
+ aspectRatio, mContext.getString(R.string.user_aspect_ratio_app_default));
+ }
+
+ /**
+ * Whether an app's aspect ratio can be overridden by user. Only apps with launcher entry
+ * will be overridable.
+ */
+ public boolean canDisplayAspectRatioUi(@NonNull ApplicationInfo app) {
+ boolean hasLauncherEntry = mInfoHasLauncherEntryList.stream()
+ .anyMatch(info -> info.activityInfo.packageName.equals(app.packageName));
+ return hasLauncherEntry;
+ }
+
+ private static boolean getValueFromDeviceConfig() {
+ return DeviceConfig.getBoolean(
+ DeviceConfig.NAMESPACE_WINDOW_MANAGER,
+ KEY_ENABLE_USER_ASPECT_RATIO_SETTINGS,
+ DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_SETTINGS);
+ }
+
+ @NonNull
+ private Map<Integer, String> getUserMinAspectRatioMapping() {
+ final String[] userMinAspectRatioStrings = mContext.getResources().getStringArray(
+ R.array.config_userAspectRatioOverrideEntries);
+ final int[] userMinAspectRatioValues = mContext.getResources().getIntArray(
+ R.array.config_userAspectRatioOverrideValues);
+ if (userMinAspectRatioStrings.length != userMinAspectRatioValues.length) {
+ throw new RuntimeException(
+ "config_userAspectRatioOverride options cannot be different length");
+ }
+
+ final Map<Integer, String> userMinAspectRatioMap = new ArrayMap<>();
+ for (int i = 0; i < userMinAspectRatioValues.length; i++) {
+ final int aspectRatioVal = userMinAspectRatioValues[i];
+ switch (aspectRatioVal) {
+ // Only map known values of UserMinAspectRatio and ignore unknown entries
+ case PackageManager.USER_MIN_ASPECT_RATIO_UNSET:
+ case PackageManager.USER_MIN_ASPECT_RATIO_SPLIT_SCREEN:
+ case PackageManager.USER_MIN_ASPECT_RATIO_4_3:
+ case PackageManager.USER_MIN_ASPECT_RATIO_16_9:
+ case PackageManager.USER_MIN_ASPECT_RATIO_3_2:
+ userMinAspectRatioMap.put(aspectRatioVal, userMinAspectRatioStrings[i]);
+ }
+ }
+ if (!userMinAspectRatioMap.containsKey(PackageManager.USER_MIN_ASPECT_RATIO_UNSET)) {
+ throw new RuntimeException("config_userAspectRatioOverrideValues options must have"
+ + " USER_MIN_ASPECT_RATIO_UNSET value");
+ }
+ return userMinAspectRatioMap;
+ }
+
+ @VisibleForTesting
+ void addInfoHasLauncherEntry(@NonNull ResolveInfo infoHasLauncherEntry) {
+ mInfoHasLauncherEntryList.add(infoHasLauncherEntry);
+ }
+}
diff --git a/src/com/android/settings/applications/manageapplications/ManageApplications.java b/src/com/android/settings/applications/manageapplications/ManageApplications.java
index 548ca55..d734a27 100644
--- a/src/com/android/settings/applications/manageapplications/ManageApplications.java
+++ b/src/com/android/settings/applications/manageapplications/ManageApplications.java
@@ -269,6 +269,7 @@
public static final int LIST_TYPE_CLONED_APPS = 17;
public static final int LIST_TYPE_NFC_TAG_APPS = 18;
public static final int LIST_TYPE_TURN_SCREEN_ON = 19;
+ public static final int LIST_TYPE_USER_ASPECT_RATIO_APPS = 20;
// List types that should show instant apps.
public static final Set<Integer> LIST_TYPES_WITH_INSTANT = new ArraySet<>(Arrays.asList(
diff --git a/src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt b/src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt
index 6574f69..8313686 100644
--- a/src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt
+++ b/src/com/android/settings/applications/manageapplications/ManageApplicationsUtil.kt
@@ -20,6 +20,7 @@
import android.util.FeatureFlagUtils
import com.android.settings.Settings.AlarmsAndRemindersActivity
import com.android.settings.Settings.AppBatteryUsageActivity
+import com.android.settings.Settings.UserAspectRatioAppListActivity
import com.android.settings.Settings.ChangeNfcTagAppsActivity
import com.android.settings.Settings.ChangeWifiStateActivity
import com.android.settings.Settings.ClonedAppsListActivity
@@ -40,6 +41,7 @@
import com.android.settings.applications.manageapplications.ManageApplications.LIST_MANAGE_EXTERNAL_STORAGE
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_ALARMS_AND_REMINDERS
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_APPS_LOCALE
+import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_USER_ASPECT_RATIO_APPS
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_BATTERY_OPTIMIZATION
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_CLONED_APPS
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_GAMES
@@ -57,6 +59,7 @@
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_WIFI_ACCESS
import com.android.settings.applications.manageapplications.ManageApplications.LIST_TYPE_WRITE_SETTINGS
import com.android.settings.spa.app.AllAppListPageProvider
+import com.android.settings.spa.app.appcompat.UserAspectRatioAppsPageProvider
import com.android.settings.spa.app.specialaccess.AlarmsAndRemindersAppListProvider
import com.android.settings.spa.app.specialaccess.AllFilesAccessAppListProvider
import com.android.settings.spa.app.specialaccess.DisplayOverOtherAppsAppListProvider
@@ -92,6 +95,7 @@
ClonedAppsListActivity::class to LIST_TYPE_CLONED_APPS,
ChangeNfcTagAppsActivity::class to LIST_TYPE_NFC_TAG_APPS,
TurnScreenOnSettingsActivity::class to LIST_TYPE_TURN_SCREEN_ON,
+ UserAspectRatioAppListActivity::class to LIST_TYPE_USER_ASPECT_RATIO_APPS,
)
@JvmField
@@ -114,6 +118,7 @@
LIST_TYPE_APPS_LOCALE -> AppLanguagesPageProvider.name
LIST_TYPE_MAIN -> AllAppListPageProvider.name
LIST_TYPE_NFC_TAG_APPS -> NfcTagAppsSettingsProvider.getAppListRoute()
+ LIST_TYPE_USER_ASPECT_RATIO_APPS -> UserAspectRatioAppsPageProvider.name
else -> null
}
}
diff --git a/src/com/android/settings/spa/SettingsSpaEnvironment.kt b/src/com/android/settings/spa/SettingsSpaEnvironment.kt
index b506005..db88784 100644
--- a/src/com/android/settings/spa/SettingsSpaEnvironment.kt
+++ b/src/com/android/settings/spa/SettingsSpaEnvironment.kt
@@ -20,6 +20,7 @@
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 @@
UsageStatsPageProvider,
PlatformCompatAppListPageProvider,
BackgroundInstalledAppsPageProvider,
+ UserAspectRatioAppsPageProvider,
CloneAppInfoSettingsProvider,
NetworkAndInternetPageProvider,
) + togglePermissionAppListTemplate.createPageProviders(),
diff --git a/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppsPageProvider.kt b/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppsPageProvider.kt
new file mode 100644
index 0000000..34b6b66
--- /dev/null
+++ b/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppsPageProvider.kt
@@ -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)
+}
\ No newline at end of file
diff --git a/tests/spa_unit/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppsPageProviderTest.kt b/tests/spa_unit/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppsPageProviderTest.kt
new file mode 100644
index 0000000..e0eb5b2
--- /dev/null
+++ b/tests/spa_unit/src/com/android/settings/spa/app/appcompat/UserAspectRatioAppsPageProviderTest.kt
@@ -0,0 +1,196 @@
+/*
+ * 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.PackageManager.USER_MIN_ASPECT_RATIO_SPLIT_SCREEN
+import android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_UNSET
+import android.os.Build
+import androidx.compose.runtime.State
+import androidx.compose.ui.test.assertIsDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.test.onNodeWithText
+import androidx.compose.ui.test.performClick
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.settings.R
+import com.android.settingslib.spa.framework.compose.stateOf
+import com.android.settingslib.spa.testutils.FakeNavControllerWrapper
+import com.android.settingslib.spa.testutils.firstWithTimeoutOrNull
+import com.android.settingslib.spaprivileged.template.app.AppListItemModel
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.runTest
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+/**
+ * To run this test: atest SettingsSpaUnitTests:UserAspectRatioAppsPageProviderTest
+ */
+@RunWith(AndroidJUnit4::class)
+class UserAspectRatioAppsPageProviderTest {
+ @get:Rule
+ val composeTestRule = createComposeRule()
+
+ private val context: Context = ApplicationProvider.getApplicationContext()
+ private val fakeNavControllerWrapper = FakeNavControllerWrapper()
+
+ @Test
+ fun aspectRatioAppsPageProvider_name() {
+ assertThat(UserAspectRatioAppsPageProvider.name).isEqualTo(EXPECTED_PROVIDER_NAME)
+ }
+
+ @Test
+ fun injectEntry_title() {
+ setInjectEntry()
+ composeTestRule.onNodeWithText(context.getString(R.string.screen_size_title))
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun injectEntry_summary() {
+ setInjectEntry()
+ composeTestRule.onNodeWithText(context.getString(R.string.screen_size_summary, Build.MODEL))
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun injectEntry_onClick_navigate() {
+ setInjectEntry()
+ composeTestRule.onNodeWithText(context.getString(R.string.screen_size_title)).performClick()
+ assertThat(fakeNavControllerWrapper.navigateCalledWith).isEqualTo("UserAspectRatioAppsPage")
+ }
+
+ private fun setInjectEntry() {
+ composeTestRule.setContent {
+ fakeNavControllerWrapper.Wrapper {
+ UserAspectRatioAppsPageProvider.buildInjectEntry().build().UiLayout()
+ }
+ }
+ }
+
+ @Test
+ fun title_displayed() {
+ composeTestRule.setContent {
+ UserAspectRatioAppList {}
+ }
+
+ composeTestRule.onNodeWithText(context.getString(R.string.screen_size_title))
+ .assertIsDisplayed()
+ }
+
+ @Test
+ fun item_labelDisplayed() {
+ setItemContent()
+
+ composeTestRule.onNodeWithText(LABEL).assertIsDisplayed()
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun aspectRatioAppListModel_transform() = runTest {
+ val listModel = UserAspectRatioAppListModel(context)
+ val recordListFlow = listModel.transform(flowOf(USER_ID), flowOf(listOf(APP)))
+ val recordList = recordListFlow.firstWithTimeoutOrNull()!!
+
+ assertThat(recordList).hasSize(1)
+ assertThat(recordList[0].app).isSameInstanceAs(APP)
+ }
+
+ @OptIn(ExperimentalCoroutinesApi::class)
+ @Test
+ fun aspectRatioAppListModel_filter() = runTest {
+ val listModel = UserAspectRatioAppListModel(context)
+
+ val recordListFlow = listModel.filter(flowOf(USER_ID), 0,
+ flowOf(listOf(APP_RECORD_NOT_DISPLAYED, APP_RECORD_SUGGESTED)))
+
+ val recordList = checkNotNull(recordListFlow.firstWithTimeoutOrNull())
+ assertThat(recordList).containsExactly(APP_RECORD_SUGGESTED)
+ }
+
+ private fun setItemContent() {
+ composeTestRule.setContent {
+ fakeNavControllerWrapper.Wrapper {
+ with(UserAspectRatioAppListModel(context)) {
+ AppListItemModel(
+ record = APP_RECORD_SUGGESTED,
+ label = LABEL,
+ summary = stateOf(SUMMARY)
+ ).AppItem()
+ }
+ }
+ }
+ }
+
+ @Test
+ fun aspectRatioAppListModel_getSummaryDefault() {
+ val summaryState = setSummaryState(USER_MIN_ASPECT_RATIO_UNSET)
+ assertThat(summaryState.value)
+ .isEqualTo(context.getString(R.string.user_aspect_ratio_app_default))
+ }
+
+ @Test
+ fun aspectRatioAppListModel_getSummaryWhenSplitScreen() {
+ val summaryState = setSummaryState(USER_MIN_ASPECT_RATIO_SPLIT_SCREEN)
+ assertThat(summaryState.value)
+ .isEqualTo(context.getString(R.string.user_aspect_ratio_half_screen))
+ }
+
+ private fun setSummaryState(override: Int): State<String> {
+ val listModel = UserAspectRatioAppListModel(context)
+ lateinit var summaryState: State<String>
+ composeTestRule.setContent {
+ summaryState = listModel.getSummary(option = 0,
+ record = UserAspectRatioAppListItemModel(
+ app = APP,
+ override = override,
+ suggested = false,
+ canDisplay = true,
+ ))
+ }
+ return summaryState
+ }
+
+
+ private companion object {
+ private const val EXPECTED_PROVIDER_NAME = "UserAspectRatioAppsPage"
+ private const val PACKAGE_NAME = "package.name"
+ private const val USER_ID = 0
+ private const val LABEL = "Label"
+ private const val SUMMARY = "Summary"
+
+ private val APP = ApplicationInfo().apply {
+ packageName = PACKAGE_NAME
+ }
+ private val APP_RECORD_SUGGESTED = UserAspectRatioAppListItemModel(
+ APP,
+ override = USER_MIN_ASPECT_RATIO_UNSET,
+ suggested = true,
+ canDisplay = true
+ )
+ private val APP_RECORD_NOT_DISPLAYED = UserAspectRatioAppListItemModel(
+ APP,
+ override = USER_MIN_ASPECT_RATIO_UNSET,
+ suggested = true,
+ canDisplay = false
+ )
+ }
+}
\ No newline at end of file
diff --git a/tests/unit/src/com/android/settings/applications/appcompat/UserAspectRatioManagerTest.java b/tests/unit/src/com/android/settings/applications/appcompat/UserAspectRatioManagerTest.java
new file mode 100644
index 0000000..36f2f54
--- /dev/null
+++ b/tests/unit/src/com/android/settings/applications/appcompat/UserAspectRatioManagerTest.java
@@ -0,0 +1,129 @@
+/*
+ * 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.applications.appcompat;
+
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_16_9;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_3_2;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_4_3;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_SPLIT_SCREEN;
+import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_UNSET;
+
+import static com.android.settings.applications.appcompat.UserAspectRatioManager.KEY_ENABLE_USER_ASPECT_RATIO_SETTINGS;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.ResolveInfo;
+import android.provider.DeviceConfig;
+
+import androidx.test.core.app.ApplicationProvider;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+
+import com.android.settings.testutils.ResourcesUtils;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+
+/**
+ * To run this test: atest SettingsUnitTests:UserAspectRatioManagerTest
+ */
+@RunWith(AndroidJUnit4.class)
+public class UserAspectRatioManagerTest {
+
+ private Context mContext;
+ private UserAspectRatioManager mUtils;
+ private String mOriginalFlag;
+
+ @Before
+ public void setUp() {
+ mContext = spy(ApplicationProvider.getApplicationContext());
+ mUtils = spy(new UserAspectRatioManager(mContext));
+ mOriginalFlag = DeviceConfig.getProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
+ KEY_ENABLE_USER_ASPECT_RATIO_SETTINGS);
+ }
+
+ @After
+ public void tearDown() {
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
+ KEY_ENABLE_USER_ASPECT_RATIO_SETTINGS, mOriginalFlag, true /* makeDefault */);
+ }
+
+ @Test
+ public void testCanDisplayAspectRatioUi() {
+ final ApplicationInfo canDisplay = new ApplicationInfo();
+ canDisplay.packageName = "com.app.candisplay";
+ addResolveInfoLauncherEntry(canDisplay.packageName);
+
+ assertTrue(mUtils.canDisplayAspectRatioUi(canDisplay));
+
+ final ApplicationInfo noLauncherEntry = new ApplicationInfo();
+ noLauncherEntry.packageName = "com.app.nolauncherentry";
+
+ assertFalse(mUtils.canDisplayAspectRatioUi(noLauncherEntry));
+ }
+
+ @Test
+ public void testIsFeatureEnabled() {
+ assertFalse(UserAspectRatioManager.isFeatureEnabled(mContext));
+
+ DeviceConfig.setProperty(DeviceConfig.NAMESPACE_WINDOW_MANAGER,
+ KEY_ENABLE_USER_ASPECT_RATIO_SETTINGS, "true", false /* makeDefault */);
+ assertTrue(UserAspectRatioManager.isFeatureEnabled(mContext));
+ }
+
+ @Test
+ public void testGetUserMinAspectRatioEntry() {
+ // R.string.user_aspect_ratio_app_default
+ final String appDefault = ResourcesUtils.getResourcesString(mContext,
+ "user_aspect_ratio_app_default");
+ assertThat(mUtils.getUserMinAspectRatioEntry(USER_MIN_ASPECT_RATIO_UNSET))
+ .isEqualTo(appDefault);
+ // should always return default if value does not correspond to anything
+ assertThat(mUtils.getUserMinAspectRatioEntry(-1))
+ .isEqualTo(appDefault);
+ // R.string.user_aspect_ratio_half_screen
+ assertThat(mUtils.getUserMinAspectRatioEntry(USER_MIN_ASPECT_RATIO_SPLIT_SCREEN))
+ .isEqualTo(ResourcesUtils.getResourcesString(mContext,
+ "user_aspect_ratio_half_screen"));
+ // R.string.user_aspect_ratio_3_2
+ assertThat(mUtils.getUserMinAspectRatioEntry(USER_MIN_ASPECT_RATIO_3_2))
+ .isEqualTo(ResourcesUtils.getResourcesString(mContext, "user_aspect_ratio_3_2"));
+ // R,string.user_aspect_ratio_4_3
+ assertThat(mUtils.getUserMinAspectRatioEntry(USER_MIN_ASPECT_RATIO_4_3))
+ .isEqualTo(ResourcesUtils.getResourcesString(mContext, "user_aspect_ratio_4_3"));
+ // R.string.user_aspect_ratio_16_9
+ assertThat(mUtils.getUserMinAspectRatioEntry(USER_MIN_ASPECT_RATIO_16_9))
+ .isEqualTo(ResourcesUtils.getResourcesString(mContext, "user_aspect_ratio_16_9"));
+ }
+
+ private void addResolveInfoLauncherEntry(String packageName) {
+ final ResolveInfo resolveInfo = mock(ResolveInfo.class);
+ final ActivityInfo activityInfo = mock(ActivityInfo.class);
+ activityInfo.packageName = packageName;
+ resolveInfo.activityInfo = activityInfo;
+ mUtils.addInfoHasLauncherEntry(resolveInfo);
+ }
+}