[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);
+    }
+}