1 /* 2 * Copyright (C) 2022 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.accessibility; 18 19 import static com.android.settingslib.widget.TwoTargetPreference.ICON_SIZE_MEDIUM; 20 21 import android.accessibilityservice.AccessibilityServiceInfo; 22 import android.accessibilityservice.AccessibilityShortcutInfo; 23 import android.app.AppOpsManager; 24 import android.app.admin.DevicePolicyManager; 25 import android.content.ComponentName; 26 import android.content.Context; 27 import android.content.pm.ActivityInfo; 28 import android.content.pm.PackageManager; 29 import android.content.pm.ResolveInfo; 30 import android.graphics.Color; 31 import android.graphics.drawable.Drawable; 32 import android.os.Bundle; 33 import android.os.UserHandle; 34 import android.text.TextUtils; 35 36 import androidx.core.content.ContextCompat; 37 38 import com.android.settings.R; 39 import com.android.settings.Utils; 40 import com.android.settings.overlay.FeatureFactory; 41 import com.android.settingslib.RestrictedLockUtils; 42 import com.android.settingslib.RestrictedLockUtilsInternal; 43 import com.android.settingslib.RestrictedPreference; 44 import com.android.settingslib.accessibility.AccessibilityUtils; 45 46 import java.util.ArrayList; 47 import java.util.List; 48 import java.util.Set; 49 50 /** 51 * This class helps setup RestrictedPreference for accessibility. 52 */ 53 public class RestrictedPreferenceHelper { 54 // Index of the first preference in a preference category. 55 private static final int FIRST_PREFERENCE_IN_CATEGORY_INDEX = -1; 56 57 private final Context mContext; 58 private final DevicePolicyManager mDpm; 59 private final PackageManager mPm; 60 private final AppOpsManager mAppOps; 61 RestrictedPreferenceHelper(Context context)62 public RestrictedPreferenceHelper(Context context) { 63 mContext = context; 64 mDpm = context.getSystemService(DevicePolicyManager.class); 65 mPm = context.getPackageManager(); 66 mAppOps = context.getSystemService(AppOpsManager.class); 67 } 68 69 /** 70 * Creates the list of {@link RestrictedPreference} with the installedServices arguments. 71 * 72 * @param installedServices The list of {@link AccessibilityServiceInfo}s of the 73 * installed accessibility services 74 * @return The list of {@link RestrictedPreference} 75 */ createAccessibilityServicePreferenceList( List<AccessibilityServiceInfo> installedServices)76 public List<RestrictedPreference> createAccessibilityServicePreferenceList( 77 List<AccessibilityServiceInfo> installedServices) { 78 79 final Set<ComponentName> enabledServices = 80 AccessibilityUtils.getEnabledServicesFromSettings(mContext); 81 final List<String> permittedServices = mDpm.getPermittedAccessibilityServices( 82 UserHandle.myUserId()); 83 final int installedServicesSize = installedServices.size(); 84 85 final List<RestrictedPreference> preferenceList = new ArrayList<>( 86 installedServicesSize); 87 88 for (int i = 0; i < installedServicesSize; ++i) { 89 final AccessibilityServiceInfo info = installedServices.get(i); 90 final ResolveInfo resolveInfo = info.getResolveInfo(); 91 final String packageName = resolveInfo.serviceInfo.packageName; 92 final ComponentName componentName = new ComponentName(packageName, 93 resolveInfo.serviceInfo.name); 94 95 final String key = componentName.flattenToString(); 96 final CharSequence title = resolveInfo.loadLabel(mPm); 97 final boolean serviceEnabled = enabledServices.contains(componentName); 98 final CharSequence summary = AccessibilitySettings.getServiceSummary( 99 mContext, info, serviceEnabled); 100 final String fragment = getAccessibilityServiceFragmentTypeName(info); 101 102 Drawable icon = resolveInfo.loadIcon(mPm); 103 if (resolveInfo.getIconResource() == 0) { 104 icon = ContextCompat.getDrawable(mContext, 105 R.drawable.ic_accessibility_generic); 106 } 107 108 final RestrictedPreference preference = createRestrictedPreference(key, title, 109 summary, icon, fragment, packageName, 110 resolveInfo.serviceInfo.applicationInfo.uid); 111 112 setRestrictedPreferenceEnabled(preference, permittedServices, serviceEnabled); 113 114 final String prefKey = preference.getKey(); 115 final int imageRes = info.getAnimatedImageRes(); 116 final CharSequence intro = info.loadIntro(mPm); 117 final CharSequence description = AccessibilitySettings.getServiceDescription( 118 mContext, info, serviceEnabled); 119 final String htmlDescription = info.loadHtmlDescription(mPm); 120 final String settingsClassName = info.getSettingsActivityName(); 121 final String tileServiceClassName = info.getTileServiceName(); 122 final int metricsCategory = FeatureFactory.getFactory(mContext) 123 .getAccessibilityMetricsFeatureProvider() 124 .getDownloadedFeatureMetricsCategory(componentName); 125 126 putBasicExtras(preference, prefKey, title, intro, description, imageRes, 127 htmlDescription, componentName, metricsCategory); 128 putServiceExtras(preference, resolveInfo, serviceEnabled); 129 putSettingsExtras(preference, packageName, settingsClassName); 130 putTileServiceExtras(preference, packageName, tileServiceClassName); 131 132 preferenceList.add(preference); 133 } 134 return preferenceList; 135 } 136 137 /** 138 * Creates the list of {@link RestrictedPreference} with the installedShortcuts arguments. 139 * 140 * @param installedShortcuts The list of {@link AccessibilityShortcutInfo}s of the 141 * installed accessibility shortcuts 142 * @return The list of {@link RestrictedPreference} 143 */ createAccessibilityActivityPreferenceList( List<AccessibilityShortcutInfo> installedShortcuts)144 public List<RestrictedPreference> createAccessibilityActivityPreferenceList( 145 List<AccessibilityShortcutInfo> installedShortcuts) { 146 final Set<ComponentName> enabledServices = 147 AccessibilityUtils.getEnabledServicesFromSettings(mContext); 148 final List<String> permittedServices = mDpm.getPermittedAccessibilityServices( 149 UserHandle.myUserId()); 150 151 final int installedShortcutsSize = installedShortcuts.size(); 152 final List<RestrictedPreference> preferenceList = new ArrayList<>( 153 installedShortcutsSize); 154 155 for (int i = 0; i < installedShortcutsSize; ++i) { 156 final AccessibilityShortcutInfo info = installedShortcuts.get(i); 157 final ActivityInfo activityInfo = info.getActivityInfo(); 158 final ComponentName componentName = info.getComponentName(); 159 160 final String key = componentName.flattenToString(); 161 final CharSequence title = activityInfo.loadLabel(mPm); 162 final String summary = info.loadSummary(mPm); 163 final String fragment = 164 LaunchAccessibilityActivityPreferenceFragment.class.getName(); 165 166 Drawable icon = activityInfo.loadIcon(mPm); 167 if (activityInfo.getIconResource() == 0) { 168 icon = ContextCompat.getDrawable(mContext, R.drawable.ic_accessibility_generic); 169 } 170 171 final RestrictedPreference preference = createRestrictedPreference(key, title, 172 summary, icon, fragment, componentName.getPackageName(), 173 activityInfo.applicationInfo.uid); 174 final boolean serviceEnabled = enabledServices.contains(componentName); 175 176 setRestrictedPreferenceEnabled(preference, permittedServices, serviceEnabled); 177 178 final String prefKey = preference.getKey(); 179 final CharSequence intro = info.loadIntro(mPm); 180 final String description = info.loadDescription(mPm); 181 final int imageRes = info.getAnimatedImageRes(); 182 final String htmlDescription = info.loadHtmlDescription(mPm); 183 final String settingsClassName = info.getSettingsActivityName(); 184 final String tileServiceClassName = info.getTileServiceName(); 185 final int metricsCategory = FeatureFactory.getFactory(mContext) 186 .getAccessibilityMetricsFeatureProvider() 187 .getDownloadedFeatureMetricsCategory(componentName); 188 189 putBasicExtras(preference, prefKey, title, intro, description, imageRes, 190 htmlDescription, componentName, metricsCategory); 191 putSettingsExtras(preference, componentName.getPackageName(), settingsClassName); 192 putTileServiceExtras(preference, componentName.getPackageName(), 193 tileServiceClassName); 194 195 preferenceList.add(preference); 196 } 197 return preferenceList; 198 } 199 getAccessibilityServiceFragmentTypeName(AccessibilityServiceInfo info)200 private String getAccessibilityServiceFragmentTypeName(AccessibilityServiceInfo info) { 201 final int type = AccessibilityUtil.getAccessibilityServiceFragmentType(info); 202 switch (type) { 203 case AccessibilityUtil.AccessibilityServiceFragmentType.VOLUME_SHORTCUT_TOGGLE: 204 return VolumeShortcutToggleAccessibilityServicePreferenceFragment.class.getName(); 205 case AccessibilityUtil.AccessibilityServiceFragmentType.INVISIBLE_TOGGLE: 206 return InvisibleToggleAccessibilityServicePreferenceFragment.class.getName(); 207 case AccessibilityUtil.AccessibilityServiceFragmentType.TOGGLE: 208 return ToggleAccessibilityServicePreferenceFragment.class.getName(); 209 default: 210 throw new IllegalArgumentException( 211 "Unsupported accessibility fragment type " + type); 212 } 213 } 214 createRestrictedPreference(String key, CharSequence title, CharSequence summary, Drawable icon, String fragment, String packageName, int uid)215 private RestrictedPreference createRestrictedPreference(String key, CharSequence title, 216 CharSequence summary, Drawable icon, String fragment, String packageName, int uid) { 217 final RestrictedPreference preference = new RestrictedPreference(mContext, packageName, 218 uid); 219 220 preference.setKey(key); 221 preference.setTitle(title); 222 preference.setSummary(summary); 223 preference.setIcon(Utils.getAdaptiveIcon(mContext, icon, Color.WHITE)); 224 preference.setFragment(fragment); 225 preference.setIconSize(ICON_SIZE_MEDIUM); 226 preference.setPersistent(false); // Disable SharedPreferences. 227 preference.setOrder(FIRST_PREFERENCE_IN_CATEGORY_INDEX); 228 229 return preference; 230 } 231 setRestrictedPreferenceEnabled(RestrictedPreference preference, final List<String> permittedServices, boolean serviceEnabled)232 private void setRestrictedPreferenceEnabled(RestrictedPreference preference, 233 final List<String> permittedServices, boolean serviceEnabled) { 234 // permittedServices null means all accessibility services are allowed. 235 boolean serviceAllowed = permittedServices == null || permittedServices.contains( 236 preference.getPackageName()); 237 boolean appOpsAllowed; 238 if (serviceAllowed) { 239 try { 240 final int mode = mAppOps.noteOpNoThrow( 241 AppOpsManager.OP_ACCESS_RESTRICTED_SETTINGS, 242 preference.getUid(), preference.getPackageName()); 243 final boolean ecmEnabled = mContext.getResources().getBoolean( 244 com.android.internal.R.bool.config_enhancedConfirmationModeEnabled); 245 appOpsAllowed = !ecmEnabled || mode == AppOpsManager.MODE_ALLOWED; 246 serviceAllowed = appOpsAllowed; 247 } catch (Exception e) { 248 // Allow service in case if app ops is not available in testing. 249 appOpsAllowed = true; 250 } 251 } else { 252 appOpsAllowed = false; 253 } 254 if (serviceAllowed || serviceEnabled) { 255 preference.setEnabled(true); 256 } else { 257 // Disable accessibility service that are not permitted. 258 final RestrictedLockUtils.EnforcedAdmin admin = 259 RestrictedLockUtilsInternal.checkIfAccessibilityServiceDisallowed( 260 mContext, preference.getPackageName(), UserHandle.myUserId()); 261 262 if (admin != null) { 263 preference.setDisabledByAdmin(admin); 264 } else if (!appOpsAllowed) { 265 preference.setDisabledByAppOps(true); 266 } else { 267 preference.setEnabled(false); 268 } 269 } 270 } 271 272 /** Puts the basic extras into {@link RestrictedPreference}'s getExtras(). */ putBasicExtras(RestrictedPreference preference, String prefKey, CharSequence title, CharSequence intro, CharSequence summary, int imageRes, String htmlDescription, ComponentName componentName, int metricsCategory)273 private void putBasicExtras(RestrictedPreference preference, String prefKey, 274 CharSequence title, CharSequence intro, CharSequence summary, int imageRes, 275 String htmlDescription, ComponentName componentName, int metricsCategory) { 276 final Bundle extras = preference.getExtras(); 277 extras.putString(AccessibilitySettings.EXTRA_PREFERENCE_KEY, prefKey); 278 extras.putCharSequence(AccessibilitySettings.EXTRA_TITLE, title); 279 extras.putCharSequence(AccessibilitySettings.EXTRA_INTRO, intro); 280 extras.putCharSequence(AccessibilitySettings.EXTRA_SUMMARY, summary); 281 extras.putParcelable(AccessibilitySettings.EXTRA_COMPONENT_NAME, componentName); 282 extras.putInt(AccessibilitySettings.EXTRA_ANIMATED_IMAGE_RES, imageRes); 283 extras.putString(AccessibilitySettings.EXTRA_HTML_DESCRIPTION, htmlDescription); 284 extras.putInt(AccessibilitySettings.EXTRA_METRICS_CATEGORY, metricsCategory); 285 } 286 287 /** 288 * Puts the service extras into {@link RestrictedPreference}'s getExtras(). 289 * 290 * <p><b>Note:</b> Called by {@link AccessibilityServiceInfo}.</p> 291 * 292 * @param preference The preference we are configuring. 293 * @param resolveInfo The service resolve info. 294 * @param serviceEnabled Whether the accessibility service is enabled. 295 */ putServiceExtras(RestrictedPreference preference, ResolveInfo resolveInfo, Boolean serviceEnabled)296 private void putServiceExtras(RestrictedPreference preference, ResolveInfo resolveInfo, 297 Boolean serviceEnabled) { 298 final Bundle extras = preference.getExtras(); 299 300 extras.putParcelable(AccessibilitySettings.EXTRA_RESOLVE_INFO, resolveInfo); 301 extras.putBoolean(AccessibilitySettings.EXTRA_CHECKED, serviceEnabled); 302 } 303 304 /** 305 * Puts the settings extras into {@link RestrictedPreference}'s getExtras(). 306 * 307 * <p><b>Note:</b> Called when settings UI is needed.</p> 308 * 309 * @param preference The preference we are configuring. 310 * @param packageName Package of accessibility feature. 311 * @param settingsClassName The component name of an activity that allows the user to modify 312 * the settings for this accessibility feature. 313 */ putSettingsExtras(RestrictedPreference preference, String packageName, String settingsClassName)314 private void putSettingsExtras(RestrictedPreference preference, String packageName, 315 String settingsClassName) { 316 final Bundle extras = preference.getExtras(); 317 318 if (!TextUtils.isEmpty(settingsClassName)) { 319 extras.putString(AccessibilitySettings.EXTRA_SETTINGS_TITLE, 320 mContext.getText(R.string.accessibility_menu_item_settings).toString()); 321 extras.putString(AccessibilitySettings.EXTRA_SETTINGS_COMPONENT_NAME, 322 new ComponentName(packageName, settingsClassName).flattenToString()); 323 } 324 } 325 326 /** 327 * Puts the information about a particular application 328 * {@link android.service.quicksettings.TileService} into {@link RestrictedPreference}'s 329 * getExtras(). 330 * 331 * <p><b>Note:</b> Called when a tooltip of 332 * {@link android.service.quicksettings.TileService} is needed.</p> 333 * 334 * @param preference The preference we are configuring. 335 * @param packageName Package of accessibility feature. 336 * @param tileServiceClassName The component name of tileService is associated with this 337 * accessibility feature. 338 */ putTileServiceExtras(RestrictedPreference preference, String packageName, String tileServiceClassName)339 private void putTileServiceExtras(RestrictedPreference preference, String packageName, 340 String tileServiceClassName) { 341 final Bundle extras = preference.getExtras(); 342 if (!TextUtils.isEmpty(tileServiceClassName)) { 343 extras.putString(AccessibilitySettings.EXTRA_TILE_SERVICE_COMPONENT_NAME, 344 new ComponentName(packageName, tileServiceClassName).flattenToString()); 345 } 346 } 347 } 348