1 /*
2  * 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.applications.appcompat;
18 
19 import static android.content.pm.ActivityInfo.OVERRIDE_ANY_ORIENTATION_TO_USER;
20 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_16_9;
21 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_3_2;
22 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_4_3;
23 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_APP_DEFAULT;
24 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_DISPLAY_SIZE;
25 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN;
26 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_SPLIT_SCREEN;
27 import static android.content.pm.PackageManager.USER_MIN_ASPECT_RATIO_UNSET;
28 import static android.os.UserHandle.getUserHandleForUid;
29 import static android.os.UserHandle.getUserId;
30 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE;
31 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE;
32 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE;
33 
34 import static java.lang.Boolean.FALSE;
35 
36 import android.app.AppGlobals;
37 import android.app.compat.CompatChanges;
38 import android.content.Context;
39 import android.content.pm.ApplicationInfo;
40 import android.content.pm.IPackageManager;
41 import android.content.pm.LauncherApps;
42 import android.content.pm.PackageManager;
43 import android.os.RemoteException;
44 import android.os.UserHandle;
45 import android.provider.DeviceConfig;
46 import android.util.ArrayMap;
47 import android.util.SparseIntArray;
48 
49 import androidx.annotation.NonNull;
50 import androidx.annotation.Nullable;
51 
52 import com.android.settings.R;
53 import com.android.settings.Utils;
54 import com.android.window.flags.Flags;
55 
56 import com.google.common.annotations.VisibleForTesting;
57 
58 import java.util.Map;
59 
60 /**
61  * Helper class for handling app aspect ratio override
62  * {@link PackageManager.UserMinAspectRatio} set by user
63  */
64 public class UserAspectRatioManager {
65     private static final boolean DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_SETTINGS = true;
66     @VisibleForTesting
67     static final String KEY_ENABLE_USER_ASPECT_RATIO_SETTINGS =
68             "enable_app_compat_aspect_ratio_user_settings";
69     static final String KEY_ENABLE_USER_ASPECT_RATIO_FULLSCREEN =
70             "enable_app_compat_user_aspect_ratio_fullscreen";
71     private static final boolean DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_FULLSCREEN = true;
72 
73     final boolean mIsUserMinAspectRatioAppDefaultFlagEnabled = Flags.userMinAspectRatioAppDefault();
74 
75     private final Context mContext;
76     private final IPackageManager mIPm;
77     /** Apps that have launcher entry defined in manifest */
78     private final Map<Integer, String> mUserAspectRatioMap;
79     private final Map<Integer, CharSequence> mUserAspectRatioA11yMap;
80     private final SparseIntArray mUserAspectRatioOrder;
81 
UserAspectRatioManager(@onNull Context context)82     public UserAspectRatioManager(@NonNull Context context) {
83         this(context, AppGlobals.getPackageManager());
84     }
85 
86     @VisibleForTesting
UserAspectRatioManager(@onNull Context context, @NonNull IPackageManager pm)87     UserAspectRatioManager(@NonNull Context context, @NonNull IPackageManager pm) {
88         mContext = context;
89         mIPm = pm;
90         mUserAspectRatioA11yMap = new ArrayMap<>();
91         mUserAspectRatioOrder = new SparseIntArray();
92         mUserAspectRatioMap = getUserMinAspectRatioMapping();
93     }
94 
95     /**
96      * Whether user aspect ratio settings is enabled for device.
97      */
isFeatureEnabled(Context context)98     public static boolean isFeatureEnabled(Context context) {
99         final boolean isBuildTimeFlagEnabled = context.getResources().getBoolean(
100                 com.android.internal.R.bool.config_appCompatUserAppAspectRatioSettingsIsEnabled);
101         return getValueFromDeviceConfig(KEY_ENABLE_USER_ASPECT_RATIO_SETTINGS,
102                 DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_SETTINGS) && isBuildTimeFlagEnabled;
103     }
104 
105     /**
106      * @return user-specific {@link PackageManager.UserMinAspectRatio} override for an app
107      */
108     @PackageManager.UserMinAspectRatio
getUserMinAspectRatioValue(@onNull String packageName, int uid)109     public int getUserMinAspectRatioValue(@NonNull String packageName, int uid)
110             throws RemoteException {
111         final int aspectRatio = mIPm.getUserMinAspectRatio(packageName, uid);
112         return hasAspectRatioOption(aspectRatio, packageName)
113                 ? aspectRatio : USER_MIN_ASPECT_RATIO_UNSET;
114     }
115 
116     /**
117      * @return corresponding string for {@link PackageManager.UserMinAspectRatio} value
118      */
119     @NonNull
getUserMinAspectRatioEntry(@ackageManager.UserMinAspectRatio int aspectRatio, @NonNull String packageName, int userId)120     public String getUserMinAspectRatioEntry(@PackageManager.UserMinAspectRatio int aspectRatio,
121             @NonNull String packageName, int userId) {
122         final String appDefault = getAspectRatioStringOrDefault(
123                 mUserAspectRatioMap.get(USER_MIN_ASPECT_RATIO_UNSET),
124                 USER_MIN_ASPECT_RATIO_UNSET);
125 
126         if (!hasAspectRatioOption(aspectRatio, packageName)) {
127             return appDefault;
128         }
129 
130         return isCurrentSelectionFromManufacturerOverride(packageName, userId, aspectRatio)
131                 ? getUserMinAspectRatioEntry(USER_MIN_ASPECT_RATIO_FULLSCREEN, packageName, userId)
132                 : mUserAspectRatioMap.getOrDefault(aspectRatio, appDefault);
133     }
134 
135     /**
136      * @return corresponding accessible string for {@link PackageManager.UserMinAspectRatio} value
137      */
138     @NonNull
getAccessibleEntry(@ackageManager.UserMinAspectRatio int aspectRatio, @NonNull String packageName)139     public CharSequence getAccessibleEntry(@PackageManager.UserMinAspectRatio int aspectRatio,
140             @NonNull String packageName) {
141         final int userId = mContext.getUserId();
142         return isCurrentSelectionFromManufacturerOverride(packageName, userId, aspectRatio)
143                 ? getAccessibleEntry(USER_MIN_ASPECT_RATIO_FULLSCREEN, packageName)
144                 : mUserAspectRatioA11yMap.getOrDefault(aspectRatio,
145                         getUserMinAspectRatioEntry(aspectRatio, packageName, userId));
146     }
147 
148     /**
149      * @return corresponding aspect ratio string for package name and user
150      */
151     @NonNull
getUserMinAspectRatioEntry(@onNull String packageName, int userId)152     public String getUserMinAspectRatioEntry(@NonNull String packageName, int userId)
153             throws RemoteException {
154         final int aspectRatio = getUserMinAspectRatioValue(packageName, userId);
155         return getUserMinAspectRatioEntry(aspectRatio, packageName, userId);
156     }
157 
158     /**
159      * @return the order of the aspect ratio option corresponding to
160      * config_userAspectRatioOverrideValues
161      */
getUserMinAspectRatioOrder(@ackageManager.UserMinAspectRatio int option)162     int getUserMinAspectRatioOrder(@PackageManager.UserMinAspectRatio int option) {
163         return mUserAspectRatioOrder.get(option);
164     }
165 
166     /**
167      * Whether user aspect ratio option is specified in
168      * {@link R.array.config_userAspectRatioOverrideValues}
169      * and is enabled by device config
170      */
hasAspectRatioOption(@ackageManager.UserMinAspectRatio int option, String packageName)171     public boolean hasAspectRatioOption(@PackageManager.UserMinAspectRatio int option,
172             String packageName) {
173         if (option == USER_MIN_ASPECT_RATIO_FULLSCREEN && !isFullscreenOptionEnabled(packageName)) {
174             return false;
175         }
176         return mUserAspectRatioMap.containsKey(option);
177     }
178 
179     /**
180      * Sets user-specified {@link PackageManager.UserMinAspectRatio} override for an app
181      */
setUserMinAspectRatio(@onNull String packageName, int uid, @PackageManager.UserMinAspectRatio int aspectRatio)182     public void setUserMinAspectRatio(@NonNull String packageName, int uid,
183             @PackageManager.UserMinAspectRatio int aspectRatio) throws RemoteException {
184         mIPm.setUserMinAspectRatio(packageName, uid, aspectRatio);
185     }
186 
187     /**
188      * Whether an app's aspect ratio can be overridden by user. Only apps with launcher entry
189      * will be overridable.
190      */
canDisplayAspectRatioUi(@onNull ApplicationInfo app)191     public boolean canDisplayAspectRatioUi(@NonNull ApplicationInfo app) {
192         Boolean appAllowsUserAspectRatioOverride = readComponentProperty(
193                 mContext.getPackageManager(), app.packageName,
194                 PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE);
195         return !FALSE.equals(appAllowsUserAspectRatioOverride) && hasLauncherEntry(app);
196     }
197 
198     /**
199      * Whether the app has been overridden to fullscreen by device manufacturer or
200      * whether the app's aspect ratio has been overridden by the user.
201      */
isAppOverridden(@onNull ApplicationInfo app, @PackageManager.UserMinAspectRatio int userOverride)202     public boolean isAppOverridden(@NonNull ApplicationInfo app,
203             @PackageManager.UserMinAspectRatio int userOverride) {
204         return (userOverride != USER_MIN_ASPECT_RATIO_UNSET
205                     && userOverride != USER_MIN_ASPECT_RATIO_APP_DEFAULT)
206                 || isCurrentSelectionFromManufacturerOverride(app.packageName, getUserId(app.uid),
207                     userOverride);
208     }
209 
210     /**
211      * Whether fullscreen option in per-app user aspect ratio settings is enabled
212      */
213     @VisibleForTesting
isFullscreenOptionEnabled(String packageName)214     boolean isFullscreenOptionEnabled(String packageName) {
215         Boolean appAllowsFullscreenOption = readComponentProperty(mContext.getPackageManager(),
216                 packageName, PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE);
217         final boolean isBuildTimeFlagEnabled = mContext.getResources().getBoolean(
218                 com.android.internal.R.bool.config_appCompatUserAppAspectRatioFullscreenIsEnabled);
219         return !FALSE.equals(appAllowsFullscreenOption) && isBuildTimeFlagEnabled
220                 && getValueFromDeviceConfig(KEY_ENABLE_USER_ASPECT_RATIO_FULLSCREEN,
221                     DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_FULLSCREEN);
222     }
223 
224     /**
225      * Whether the device manufacturer has overridden app's orientation to
226      * {@link android.content.pm.ActivityInfo#SCREEN_ORIENTATION_USER} to force app to fullscreen
227      * and app has not opted-out from the treatment
228      */
isOverrideToFullscreenEnabled(String pkgName, int userId)229     boolean isOverrideToFullscreenEnabled(String pkgName, int userId) {
230         Boolean appAllowsOrientationOverride = readComponentProperty(mContext.getPackageManager(),
231                 pkgName, PROPERTY_COMPAT_ALLOW_ORIENTATION_OVERRIDE);
232         return mIsUserMinAspectRatioAppDefaultFlagEnabled
233                 && hasAspectRatioOption(USER_MIN_ASPECT_RATIO_FULLSCREEN, pkgName)
234                 && !FALSE.equals(appAllowsOrientationOverride)
235                 && isFullscreenCompatChangeEnabled(pkgName, userId);
236     }
237 
isFullscreenCompatChangeEnabled(String pkgName, int userId)238     boolean isFullscreenCompatChangeEnabled(String pkgName, int userId) {
239         return CompatChanges.isChangeEnabled(
240                 OVERRIDE_ANY_ORIENTATION_TO_USER, pkgName, UserHandle.of(userId));
241     }
242 
isCurrentSelectionFromManufacturerOverride(String pkgName, int userId, @PackageManager.UserMinAspectRatio int aspectRatio)243     private boolean isCurrentSelectionFromManufacturerOverride(String pkgName, int userId,
244             @PackageManager.UserMinAspectRatio int aspectRatio) {
245         return aspectRatio == USER_MIN_ASPECT_RATIO_UNSET
246                 && isOverrideToFullscreenEnabled(pkgName, userId);
247     }
248 
hasLauncherEntry(@onNull ApplicationInfo app)249     private boolean hasLauncherEntry(@NonNull ApplicationInfo app) {
250         return !mContext.getSystemService(LauncherApps.class)
251                 .getActivityList(app.packageName, getUserHandleForUid(app.uid))
252                 .isEmpty();
253     }
254 
getValueFromDeviceConfig(String name, boolean defaultValue)255     private static boolean getValueFromDeviceConfig(String name, boolean defaultValue) {
256         return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER, name, defaultValue);
257     }
258 
259     @NonNull
getUserMinAspectRatioMapping()260     private Map<Integer, String> getUserMinAspectRatioMapping() {
261         final String[] userMinAspectRatioStrings = mContext.getResources().getStringArray(
262                 R.array.config_userAspectRatioOverrideEntries);
263         final int[] userMinAspectRatioValues = mContext.getResources().getIntArray(
264                 R.array.config_userAspectRatioOverrideValues);
265         if (userMinAspectRatioStrings.length != userMinAspectRatioValues.length) {
266             throw new RuntimeException(
267                     "config_userAspectRatioOverride options cannot be different length");
268         }
269 
270         final Map<Integer, String> userMinAspectRatioMap = new ArrayMap<>();
271         for (int i = 0; i < userMinAspectRatioValues.length; i++) {
272             final int aspectRatioVal = userMinAspectRatioValues[i];
273             final String aspectRatioString = getAspectRatioStringOrDefault(
274                     userMinAspectRatioStrings[i], aspectRatioVal);
275             boolean containsColon = aspectRatioString.contains(":");
276             switch (aspectRatioVal) {
277                 // Only map known values of UserMinAspectRatio and ignore unknown entries
278                 case USER_MIN_ASPECT_RATIO_FULLSCREEN:
279                 case USER_MIN_ASPECT_RATIO_UNSET:
280                 case USER_MIN_ASPECT_RATIO_SPLIT_SCREEN:
281                 case USER_MIN_ASPECT_RATIO_DISPLAY_SIZE:
282                 case USER_MIN_ASPECT_RATIO_4_3:
283                 case USER_MIN_ASPECT_RATIO_16_9:
284                 case USER_MIN_ASPECT_RATIO_3_2:
285                     if (containsColon) {
286                         String[] aspectRatioDigits = aspectRatioString.split(":");
287                         String accessibleString = getAccessibleOption(aspectRatioDigits[0],
288                                 aspectRatioDigits[1]);
289                         final CharSequence accessibleSequence = Utils.createAccessibleSequence(
290                                 aspectRatioString, accessibleString);
291                         mUserAspectRatioA11yMap.put(aspectRatioVal, accessibleSequence);
292                     }
293                     userMinAspectRatioMap.put(aspectRatioVal, aspectRatioString);
294                     mUserAspectRatioOrder.put(aspectRatioVal, i);
295             }
296         }
297         if (!userMinAspectRatioMap.containsKey(USER_MIN_ASPECT_RATIO_UNSET)) {
298             throw new RuntimeException("config_userAspectRatioOverrideValues options must have"
299                     + " USER_MIN_ASPECT_RATIO_UNSET value");
300         }
301         if (mIsUserMinAspectRatioAppDefaultFlagEnabled) {
302             userMinAspectRatioMap.put(USER_MIN_ASPECT_RATIO_APP_DEFAULT,
303                     userMinAspectRatioMap.get(USER_MIN_ASPECT_RATIO_UNSET));
304             mUserAspectRatioOrder.put(USER_MIN_ASPECT_RATIO_APP_DEFAULT,
305                     mUserAspectRatioOrder.get(USER_MIN_ASPECT_RATIO_UNSET));
306             if (mUserAspectRatioA11yMap.containsKey(USER_MIN_ASPECT_RATIO_UNSET)) {
307                 mUserAspectRatioA11yMap.put(USER_MIN_ASPECT_RATIO_APP_DEFAULT,
308                         mUserAspectRatioA11yMap.get(USER_MIN_ASPECT_RATIO_UNSET));
309             }
310         }
311         return userMinAspectRatioMap;
312     }
313 
314     @NonNull
getAccessibleOption(String numerator, String denominator)315     private String getAccessibleOption(String numerator, String denominator) {
316         return mContext.getString(R.string.user_aspect_ratio_option_a11y,
317                 numerator, denominator);
318     }
319 
320     @NonNull
getAspectRatioStringOrDefault(@ullable String aspectRatioString, @PackageManager.UserMinAspectRatio int aspectRatioVal)321     private String getAspectRatioStringOrDefault(@Nullable String aspectRatioString,
322             @PackageManager.UserMinAspectRatio int aspectRatioVal) {
323         if (aspectRatioString != null) {
324             return aspectRatioString;
325         }
326         // Options are customized per device and if strings are set to @null, use default
327         switch (aspectRatioVal) {
328             case USER_MIN_ASPECT_RATIO_FULLSCREEN:
329                 return mContext.getString(R.string.user_aspect_ratio_fullscreen);
330             case USER_MIN_ASPECT_RATIO_SPLIT_SCREEN:
331                 return mContext.getString(R.string.user_aspect_ratio_half_screen);
332             case USER_MIN_ASPECT_RATIO_DISPLAY_SIZE:
333                 return mContext.getString(R.string.user_aspect_ratio_device_size);
334             case USER_MIN_ASPECT_RATIO_4_3:
335                 return mContext.getString(R.string.user_aspect_ratio_4_3);
336             case USER_MIN_ASPECT_RATIO_16_9:
337                 return mContext.getString(R.string.user_aspect_ratio_16_9);
338             case USER_MIN_ASPECT_RATIO_3_2:
339                 return mContext.getString(R.string.user_aspect_ratio_3_2);
340             default:
341                 return mContext.getString(R.string.user_aspect_ratio_app_default);
342         }
343     }
344 
345     @Nullable
readComponentProperty(PackageManager pm, String packageName, String propertyName)346     private static Boolean readComponentProperty(PackageManager pm, String packageName,
347             String propertyName) {
348         try {
349             return pm.getProperty(propertyName, packageName).getBoolean();
350         } catch (PackageManager.NameNotFoundException e) {
351             // No such property name
352         }
353         return null;
354     }
355 }
356