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