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