• 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.os.UserHandle.getUserHandleForUid;
20 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE;
21 import static android.view.WindowManager.PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE;
22 
23 import static java.lang.Boolean.FALSE;
24 
25 import android.app.AppGlobals;
26 import android.content.Context;
27 import android.content.Intent;
28 import android.content.pm.ApplicationInfo;
29 import android.content.pm.IPackageManager;
30 import android.content.pm.LauncherApps;
31 import android.content.pm.PackageManager;
32 import android.os.RemoteException;
33 import android.provider.DeviceConfig;
34 import android.util.ArrayMap;
35 
36 import androidx.annotation.NonNull;
37 import androidx.annotation.Nullable;
38 
39 import com.android.settings.R;
40 import com.android.settings.Utils;
41 
42 import com.google.common.annotations.VisibleForTesting;
43 
44 import java.util.Map;
45 
46 /**
47  * Helper class for handling app aspect ratio override
48  * {@link PackageManager.UserMinAspectRatio} set by user
49  */
50 public class UserAspectRatioManager {
51     private static final Intent LAUNCHER_ENTRY_INTENT =
52             new Intent(Intent.ACTION_MAIN).addCategory(Intent.CATEGORY_LAUNCHER);
53 
54     // TODO(b/288142656): Enable user aspect ratio settings by default
55     private static final boolean DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_SETTINGS = true;
56     @VisibleForTesting
57     static final String KEY_ENABLE_USER_ASPECT_RATIO_SETTINGS =
58             "enable_app_compat_aspect_ratio_user_settings";
59     static final String KEY_ENABLE_USER_ASPECT_RATIO_FULLSCREEN =
60             "enable_app_compat_user_aspect_ratio_fullscreen";
61     private static final boolean DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_FULLSCREEN = true;
62 
63     private final Context mContext;
64     private final IPackageManager mIPm;
65     /** Apps that have launcher entry defined in manifest */
66     private final Map<Integer, String> mUserAspectRatioMap;
67     private final Map<Integer, CharSequence> mUserAspectRatioA11yMap;
68 
UserAspectRatioManager(@onNull Context context)69     public UserAspectRatioManager(@NonNull Context context) {
70         mContext = context;
71         mIPm = AppGlobals.getPackageManager();
72         mUserAspectRatioA11yMap = new ArrayMap<>();
73         mUserAspectRatioMap = getUserMinAspectRatioMapping();
74     }
75 
76     /**
77      * Whether user aspect ratio settings is enabled for device.
78      */
isFeatureEnabled(Context context)79     public static boolean isFeatureEnabled(Context context) {
80         final boolean isBuildTimeFlagEnabled = context.getResources().getBoolean(
81                 com.android.internal.R.bool.config_appCompatUserAppAspectRatioSettingsIsEnabled);
82         return getValueFromDeviceConfig(KEY_ENABLE_USER_ASPECT_RATIO_SETTINGS,
83                 DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_SETTINGS) && isBuildTimeFlagEnabled;
84     }
85 
86     /**
87      * @return user-specific {@link PackageManager.UserMinAspectRatio} override for an app
88      */
89     @PackageManager.UserMinAspectRatio
getUserMinAspectRatioValue(@onNull String packageName, int uid)90     public int getUserMinAspectRatioValue(@NonNull String packageName, int uid)
91             throws RemoteException {
92         final int aspectRatio = mIPm.getUserMinAspectRatio(packageName, uid);
93         return hasAspectRatioOption(aspectRatio, packageName)
94                 ? aspectRatio : PackageManager.USER_MIN_ASPECT_RATIO_UNSET;
95     }
96 
97     /**
98      * @return corresponding string for {@link PackageManager.UserMinAspectRatio} value
99      */
100     @NonNull
getUserMinAspectRatioEntry(@ackageManager.UserMinAspectRatio int aspectRatio, String packageName)101     public String getUserMinAspectRatioEntry(@PackageManager.UserMinAspectRatio int aspectRatio,
102             String packageName) {
103         if (!hasAspectRatioOption(aspectRatio, packageName))  {
104             return mUserAspectRatioMap.get(PackageManager.USER_MIN_ASPECT_RATIO_UNSET);
105         }
106         return mUserAspectRatioMap.get(aspectRatio);
107     }
108 
109     /**
110      * @return corresponding accessible string for {@link PackageManager.UserMinAspectRatio} value
111      */
112     @NonNull
getAccessibleEntry(@ackageManager.UserMinAspectRatio int aspectRatio, String packageName)113     public CharSequence getAccessibleEntry(@PackageManager.UserMinAspectRatio int aspectRatio,
114             String packageName) {
115         return mUserAspectRatioA11yMap.getOrDefault(aspectRatio,
116                 getUserMinAspectRatioEntry(aspectRatio, packageName));
117     }
118 
119     /**
120      * @return corresponding aspect ratio string for package name and user
121      */
122     @NonNull
getUserMinAspectRatioEntry(@onNull String packageName, int uid)123     public String getUserMinAspectRatioEntry(@NonNull String packageName, int uid)
124             throws RemoteException {
125         final int aspectRatio = getUserMinAspectRatioValue(packageName, uid);
126         return getUserMinAspectRatioEntry(aspectRatio, packageName);
127     }
128 
129     /**
130      * Whether user aspect ratio option is specified in
131      * {@link R.array.config_userAspectRatioOverrideValues}
132      * and is enabled by device config
133      */
hasAspectRatioOption(@ackageManager.UserMinAspectRatio int option, String packageName)134     public boolean hasAspectRatioOption(@PackageManager.UserMinAspectRatio int option,
135             String packageName) {
136         if (option == PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN
137                 && !isFullscreenOptionEnabled(packageName)) {
138             return false;
139         }
140         return mUserAspectRatioMap.containsKey(option);
141     }
142 
143     /**
144      * Sets user-specified {@link PackageManager.UserMinAspectRatio} override for an app
145      */
setUserMinAspectRatio(@onNull String packageName, int uid, @PackageManager.UserMinAspectRatio int aspectRatio)146     public void setUserMinAspectRatio(@NonNull String packageName, int uid,
147             @PackageManager.UserMinAspectRatio int aspectRatio) throws RemoteException {
148         mIPm.setUserMinAspectRatio(packageName, uid, aspectRatio);
149     }
150 
151     /**
152      * Whether an app's aspect ratio can be overridden by user. Only apps with launcher entry
153      * will be overridable.
154      */
canDisplayAspectRatioUi(@onNull ApplicationInfo app)155     public boolean canDisplayAspectRatioUi(@NonNull ApplicationInfo app) {
156         Boolean appAllowsUserAspectRatioOverride = readComponentProperty(
157                 mContext.getPackageManager(), app.packageName,
158                 PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_OVERRIDE);
159         return !FALSE.equals(appAllowsUserAspectRatioOverride) && hasLauncherEntry(app);
160     }
161 
162     /**
163      * Whether fullscreen option in per-app user aspect ratio settings is enabled
164      */
165     @VisibleForTesting
isFullscreenOptionEnabled(String packageName)166     boolean isFullscreenOptionEnabled(String packageName) {
167         Boolean appAllowsFullscreenOption = readComponentProperty(mContext.getPackageManager(),
168                 packageName, PROPERTY_COMPAT_ALLOW_USER_ASPECT_RATIO_FULLSCREEN_OVERRIDE);
169         final boolean isBuildTimeFlagEnabled = mContext.getResources().getBoolean(
170                 com.android.internal.R.bool.config_appCompatUserAppAspectRatioFullscreenIsEnabled);
171         return !FALSE.equals(appAllowsFullscreenOption) && isBuildTimeFlagEnabled
172                 && getValueFromDeviceConfig(KEY_ENABLE_USER_ASPECT_RATIO_FULLSCREEN,
173                     DEFAULT_VALUE_ENABLE_USER_ASPECT_RATIO_FULLSCREEN);
174     }
175 
getLauncherApps()176     LauncherApps getLauncherApps() {
177         return mContext.getSystemService(LauncherApps.class);
178     }
179 
hasLauncherEntry(@onNull ApplicationInfo app)180     private boolean hasLauncherEntry(@NonNull ApplicationInfo app) {
181         return !getLauncherApps().getActivityList(app.packageName, getUserHandleForUid(app.uid))
182                 .isEmpty();
183     }
184 
getValueFromDeviceConfig(String name, boolean defaultValue)185     private static boolean getValueFromDeviceConfig(String name, boolean defaultValue) {
186         return DeviceConfig.getBoolean(DeviceConfig.NAMESPACE_WINDOW_MANAGER, name, defaultValue);
187     }
188 
189     @NonNull
getUserMinAspectRatioMapping()190     private Map<Integer, String> getUserMinAspectRatioMapping() {
191         final String[] userMinAspectRatioStrings = mContext.getResources().getStringArray(
192                 R.array.config_userAspectRatioOverrideEntries);
193         final int[] userMinAspectRatioValues = mContext.getResources().getIntArray(
194                 R.array.config_userAspectRatioOverrideValues);
195         if (userMinAspectRatioStrings.length != userMinAspectRatioValues.length) {
196             throw new RuntimeException(
197                     "config_userAspectRatioOverride options cannot be different length");
198         }
199 
200         final Map<Integer, String> userMinAspectRatioMap = new ArrayMap<>();
201         for (int i = 0; i < userMinAspectRatioValues.length; i++) {
202             final int aspectRatioVal = userMinAspectRatioValues[i];
203             final String aspectRatioString = getAspectRatioStringOrDefault(
204                     userMinAspectRatioStrings[i], aspectRatioVal);
205             boolean containsColon = aspectRatioString.contains(":");
206             switch (aspectRatioVal) {
207                 // Only map known values of UserMinAspectRatio and ignore unknown entries
208                 case PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN:
209                 case PackageManager.USER_MIN_ASPECT_RATIO_UNSET:
210                 case PackageManager.USER_MIN_ASPECT_RATIO_SPLIT_SCREEN:
211                 case PackageManager.USER_MIN_ASPECT_RATIO_DISPLAY_SIZE:
212                 case PackageManager.USER_MIN_ASPECT_RATIO_4_3:
213                 case PackageManager.USER_MIN_ASPECT_RATIO_16_9:
214                 case PackageManager.USER_MIN_ASPECT_RATIO_3_2:
215                     if (containsColon) {
216                         String[] aspectRatioDigits = aspectRatioString.split(":");
217                         String accessibleString = getAccessibleOption(aspectRatioDigits[0],
218                                 aspectRatioDigits[1]);
219                         final CharSequence accessibleSequence = Utils.createAccessibleSequence(
220                                 aspectRatioString, accessibleString);
221                         mUserAspectRatioA11yMap.put(aspectRatioVal, accessibleSequence);
222                     }
223                     userMinAspectRatioMap.put(aspectRatioVal, aspectRatioString);
224             }
225         }
226         if (!userMinAspectRatioMap.containsKey(PackageManager.USER_MIN_ASPECT_RATIO_UNSET)) {
227             throw new RuntimeException("config_userAspectRatioOverrideValues options must have"
228                     + " USER_MIN_ASPECT_RATIO_UNSET value");
229         }
230         return userMinAspectRatioMap;
231     }
232 
233     @NonNull
getAccessibleOption(String numerator, String denominator)234     private String getAccessibleOption(String numerator, String denominator) {
235         return mContext.getResources().getString(R.string.user_aspect_ratio_option_a11y,
236                 numerator, denominator);
237     }
238 
239     @NonNull
getAspectRatioStringOrDefault(@ullable String aspectRatioString, @PackageManager.UserMinAspectRatio int aspectRatioVal)240     private String getAspectRatioStringOrDefault(@Nullable String aspectRatioString,
241             @PackageManager.UserMinAspectRatio int aspectRatioVal) {
242         if (aspectRatioString != null) {
243             return aspectRatioString;
244         }
245         // Options are customized per device and if strings are set to @null, use default
246         switch (aspectRatioVal) {
247             case PackageManager.USER_MIN_ASPECT_RATIO_FULLSCREEN:
248                 return mContext.getString(R.string.user_aspect_ratio_fullscreen);
249             case PackageManager.USER_MIN_ASPECT_RATIO_SPLIT_SCREEN:
250                 return mContext.getString(R.string.user_aspect_ratio_half_screen);
251             case PackageManager.USER_MIN_ASPECT_RATIO_DISPLAY_SIZE:
252                 return mContext.getString(R.string.user_aspect_ratio_device_size);
253             case PackageManager.USER_MIN_ASPECT_RATIO_4_3:
254                 return mContext.getString(R.string.user_aspect_ratio_4_3);
255             case PackageManager.USER_MIN_ASPECT_RATIO_16_9:
256                 return mContext.getString(R.string.user_aspect_ratio_16_9);
257             case PackageManager.USER_MIN_ASPECT_RATIO_3_2:
258                 return mContext.getString(R.string.user_aspect_ratio_3_2);
259             default:
260                 return mContext.getString(R.string.user_aspect_ratio_app_default);
261         }
262     }
263 
264     @Nullable
readComponentProperty(PackageManager pm, String packageName, String propertyName)265     private static Boolean readComponentProperty(PackageManager pm, String packageName,
266             String propertyName) {
267         try {
268             return pm.getProperty(propertyName, packageName).getBoolean();
269         } catch (PackageManager.NameNotFoundException e) {
270             // No such property name
271         }
272         return null;
273     }
274 }
275