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