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