1 /* 2 * Copyright (C) 2020 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.internal.accessibility.util; 18 19 import static com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType; 20 import static com.android.internal.accessibility.common.ShortcutConstants.SERVICES_SEPARATOR; 21 22 import android.accessibilityservice.AccessibilityService; 23 import android.accessibilityservice.AccessibilityServiceInfo; 24 import android.annotation.IntDef; 25 import android.annotation.NonNull; 26 import android.annotation.Nullable; 27 import android.content.ComponentName; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.pm.PackageManager; 31 import android.content.pm.ResolveInfo; 32 import android.content.pm.ServiceInfo; 33 import android.os.Build; 34 import android.os.UserHandle; 35 import android.provider.Settings; 36 import android.telecom.TelecomManager; 37 import android.telephony.Annotation; 38 import android.telephony.TelephonyManager; 39 import android.text.ParcelableSpan; 40 import android.text.Spanned; 41 import android.text.TextUtils; 42 import android.util.ArraySet; 43 import android.view.accessibility.AccessibilityManager; 44 45 import com.android.internal.annotations.VisibleForTesting; 46 47 import libcore.util.EmptyArray; 48 49 import java.lang.annotation.Retention; 50 import java.lang.annotation.RetentionPolicy; 51 import java.util.Collections; 52 import java.util.HashSet; 53 import java.util.List; 54 import java.util.Optional; 55 import java.util.Set; 56 57 /** 58 * Collection of utilities for accessibility service. 59 */ 60 public final class AccessibilityUtils { AccessibilityUtils()61 private AccessibilityUtils() { 62 } 63 64 /** @hide */ 65 @IntDef(value = { 66 NONE, 67 TEXT, 68 PARCELABLE_SPAN 69 }) 70 @Retention(RetentionPolicy.SOURCE) 71 public @interface A11yTextChangeType { 72 } 73 74 /** Denotes the accessibility enabled status */ 75 @Retention(RetentionPolicy.SOURCE) 76 public @interface State { 77 int OFF = 0; 78 int ON = 1; 79 } 80 81 /** Specifies no content has been changed for accessibility. */ 82 public static final int NONE = 0; 83 /** Specifies some readable sequence has been changed. */ 84 public static final int TEXT = 1; 85 /** Specifies some parcelable spans has been changed. */ 86 public static final int PARCELABLE_SPAN = 2; 87 88 @VisibleForTesting 89 public static final String MENU_SERVICE_RELATIVE_CLASS_NAME = ".AccessibilityMenuService"; 90 91 /** 92 * {@link ComponentName} for the Accessibility Menu {@link AccessibilityService} as provided 93 * inside the system build, used for automatic migration to this version of the service. 94 * @hide 95 */ 96 public static final ComponentName ACCESSIBILITY_MENU_IN_SYSTEM = 97 new ComponentName("com.android.systemui.accessibility.accessibilitymenu", 98 "com.android.systemui.accessibility.accessibilitymenu" 99 + MENU_SERVICE_RELATIVE_CLASS_NAME); 100 101 /** 102 * Returns the set of enabled accessibility services for userId. If there are no 103 * services, it returns the unmodifiable {@link Collections#emptySet()}. 104 */ getEnabledServicesFromSettings(Context context, int userId)105 public static Set<ComponentName> getEnabledServicesFromSettings(Context context, int userId) { 106 final String enabledServicesSetting = Settings.Secure.getStringForUser( 107 context.getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, 108 userId); 109 if (TextUtils.isEmpty(enabledServicesSetting)) { 110 return Collections.emptySet(); 111 } 112 113 final Set<ComponentName> enabledServices = new HashSet<>(); 114 final TextUtils.StringSplitter colonSplitter = 115 new TextUtils.SimpleStringSplitter(SERVICES_SEPARATOR); 116 colonSplitter.setString(enabledServicesSetting); 117 118 for (String componentNameString : colonSplitter) { 119 final ComponentName enabledService = ComponentName.unflattenFromString( 120 componentNameString); 121 if (enabledService != null) { 122 enabledServices.add(enabledService); 123 } 124 } 125 126 return enabledServices; 127 } 128 129 /** 130 * Changes an accessibility component's state for the calling process userId 131 */ setAccessibilityServiceState(Context context, ComponentName componentName, boolean enabled)132 public static void setAccessibilityServiceState(Context context, ComponentName componentName, 133 boolean enabled) { 134 setAccessibilityServiceState(context, componentName, enabled, UserHandle.myUserId()); 135 } 136 137 /** 138 * Changes an accessibility component's state for {@param userId}. 139 */ setAccessibilityServiceState(Context context, ComponentName componentName, boolean enabled, int userId)140 public static void setAccessibilityServiceState(Context context, ComponentName componentName, 141 boolean enabled, int userId) { 142 Set<ComponentName> enabledServices = getEnabledServicesFromSettings( 143 context, userId); 144 145 if (enabledServices.isEmpty()) { 146 enabledServices = new ArraySet<>(/* capacity= */ 1); 147 } 148 149 if (enabled) { 150 enabledServices.add(componentName); 151 } else { 152 enabledServices.remove(componentName); 153 } 154 155 final StringBuilder enabledServicesBuilder = new StringBuilder(); 156 for (ComponentName enabledService : enabledServices) { 157 enabledServicesBuilder.append(enabledService.flattenToString()); 158 enabledServicesBuilder.append( 159 SERVICES_SEPARATOR); 160 } 161 162 final int enabledServicesBuilderLength = enabledServicesBuilder.length(); 163 if (enabledServicesBuilderLength > 0) { 164 enabledServicesBuilder.deleteCharAt(enabledServicesBuilderLength - 1); 165 } 166 167 Settings.Secure.putStringForUser(context.getContentResolver(), 168 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, 169 enabledServicesBuilder.toString(), userId); 170 } 171 172 /** 173 * Gets the corresponding fragment type of a given accessibility service. 174 * 175 * @param accessibilityServiceInfo The accessibilityService's info. 176 * @return int from {@link AccessibilityFragmentType}. 177 */ getAccessibilityServiceFragmentType( @onNull AccessibilityServiceInfo accessibilityServiceInfo)178 public static @AccessibilityFragmentType int getAccessibilityServiceFragmentType( 179 @NonNull AccessibilityServiceInfo accessibilityServiceInfo) { 180 final int targetSdk = accessibilityServiceInfo.getResolveInfo() 181 .serviceInfo.applicationInfo.targetSdkVersion; 182 final boolean requestA11yButton = (accessibilityServiceInfo.flags 183 & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0; 184 185 if (targetSdk <= Build.VERSION_CODES.Q) { 186 return AccessibilityFragmentType.VOLUME_SHORTCUT_TOGGLE; 187 } 188 return requestA11yButton 189 ? AccessibilityFragmentType.INVISIBLE_TOGGLE 190 : AccessibilityFragmentType.TOGGLE; 191 } 192 193 /** 194 * Returns if a {@code componentId} service is enabled. 195 * 196 * @param context The current context. 197 * @param componentId The component id that need to be checked. 198 * @return {@code true} if a {@code componentId} service is enabled. 199 */ isAccessibilityServiceEnabled(Context context, @NonNull String componentId)200 public static boolean isAccessibilityServiceEnabled(Context context, 201 @NonNull String componentId) { 202 final AccessibilityManager am = (AccessibilityManager) context.getSystemService( 203 Context.ACCESSIBILITY_SERVICE); 204 final List<AccessibilityServiceInfo> enabledServices = 205 am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK); 206 207 for (AccessibilityServiceInfo info : enabledServices) { 208 final String id = info.getComponentName().flattenToString(); 209 if (id.equals(componentId)) { 210 return true; 211 } 212 } 213 214 return false; 215 } 216 217 /** 218 * Intercepts the {@link AccessibilityService#GLOBAL_ACTION_KEYCODE_HEADSETHOOK} action 219 * by directly interacting with TelecomManager if a call is incoming or in progress. 220 * 221 * <p> 222 * Provided here in shared utils to be used by both the legacy and modern (SysUI) 223 * system action implementations. 224 * </p> 225 * 226 * @return True if the action was propagated to TelecomManager, otherwise false. 227 */ interceptHeadsetHookForActiveCall(Context context)228 public static boolean interceptHeadsetHookForActiveCall(Context context) { 229 final TelecomManager telecomManager = context.getSystemService(TelecomManager.class); 230 @Annotation.CallState final int callState = 231 telecomManager != null ? telecomManager.getCallState() 232 : TelephonyManager.CALL_STATE_IDLE; 233 if (callState == TelephonyManager.CALL_STATE_RINGING) { 234 telecomManager.acceptRingingCall(); 235 return true; 236 } else if (callState == TelephonyManager.CALL_STATE_OFFHOOK) { 237 telecomManager.endCall(); 238 return true; 239 } 240 return false; 241 } 242 243 /** 244 * Indicates whether the current user has completed setup via the setup wizard. 245 * {@link android.provider.Settings.Secure#USER_SETUP_COMPLETE} 246 * 247 * @return {@code true} if the setup is completed. 248 */ isUserSetupCompleted(Context context)249 public static boolean isUserSetupCompleted(Context context) { 250 return Settings.Secure.getIntForUser(context.getContentResolver(), 251 Settings.Secure.USER_SETUP_COMPLETE, /* def= */ 0, UserHandle.USER_CURRENT) 252 != /* false */ 0; 253 } 254 255 /** 256 * Returns the text change type for accessibility. It only cares about readable sequence changes 257 * or {@link ParcelableSpan} changes which are able to pass via IPC. 258 * 259 * @param before The CharSequence before changing 260 * @param after The CharSequence after changing 261 * @return Returns {@code TEXT} for readable sequence changes or {@code PARCELABLE_SPAN} for 262 * ParcelableSpan changes. Otherwise, returns {@code NONE}. 263 */ 264 @A11yTextChangeType textOrSpanChanged(CharSequence before, CharSequence after)265 public static int textOrSpanChanged(CharSequence before, CharSequence after) { 266 if (!TextUtils.equals(before, after)) { 267 return TEXT; 268 } 269 if (before instanceof Spanned || after instanceof Spanned) { 270 if (!parcelableSpansEquals(before, after)) { 271 return PARCELABLE_SPAN; 272 } 273 } 274 return NONE; 275 } 276 parcelableSpansEquals(CharSequence before, CharSequence after)277 private static boolean parcelableSpansEquals(CharSequence before, CharSequence after) { 278 Object[] spansA = EmptyArray.OBJECT; 279 Object[] spansB = EmptyArray.OBJECT; 280 Spanned a = null; 281 Spanned b = null; 282 if (before instanceof Spanned) { 283 a = (Spanned) before; 284 spansA = a.getSpans(0, a.length(), ParcelableSpan.class); 285 } 286 if (after instanceof Spanned) { 287 b = (Spanned) after; 288 spansB = b.getSpans(0, b.length(), ParcelableSpan.class); 289 } 290 if (spansA.length != spansB.length) { 291 return false; 292 } 293 for (int i = 0; i < spansA.length; ++i) { 294 final Object thisSpan = spansA[i]; 295 final Object otherSpan = spansB[i]; 296 if ((thisSpan.getClass() != otherSpan.getClass()) 297 || (a.getSpanStart(thisSpan) != b.getSpanStart(otherSpan)) 298 || (a.getSpanEnd(thisSpan) != b.getSpanEnd(otherSpan)) 299 || (a.getSpanFlags(thisSpan) != b.getSpanFlags(otherSpan))) { 300 return false; 301 } 302 } 303 return true; 304 } 305 306 /** 307 * Finds the {@link ComponentName} of the AccessibilityMenu accessibility service that the 308 * device should be migrated off. Devices using this service should be migrated to 309 * {@link #ACCESSIBILITY_MENU_IN_SYSTEM}. 310 * 311 * <p> 312 * Requirements: 313 * <li>There are exactly two installed accessibility service components with class name 314 * {@link #MENU_SERVICE_RELATIVE_CLASS_NAME}.</li> 315 * <li>Exactly one of these components is equal to {@link #ACCESSIBILITY_MENU_IN_SYSTEM}.</li> 316 * </p> 317 * 318 * @return The {@link ComponentName} of the service that is not {@link 319 * #ACCESSIBILITY_MENU_IN_SYSTEM}, 320 * or <code>null</code> if the above requirements are not met. 321 */ 322 @Nullable getAccessibilityMenuComponentToMigrate( PackageManager packageManager, int userId)323 public static ComponentName getAccessibilityMenuComponentToMigrate( 324 PackageManager packageManager, int userId) { 325 final Set<ComponentName> menuComponentNames = findA11yMenuComponentNames(packageManager, 326 userId); 327 Optional<ComponentName> menuOutsideSystem = menuComponentNames.stream().filter( 328 name -> !name.equals(ACCESSIBILITY_MENU_IN_SYSTEM)).findFirst(); 329 final boolean shouldMigrateToMenuInSystem = menuComponentNames.size() == 2 330 && menuComponentNames.contains(ACCESSIBILITY_MENU_IN_SYSTEM) 331 && menuOutsideSystem.isPresent(); 332 return shouldMigrateToMenuInSystem ? menuOutsideSystem.get() : null; 333 } 334 335 /** 336 * Returns all {@link ComponentName}s whose class name ends in {@link 337 * #MENU_SERVICE_RELATIVE_CLASS_NAME}. 338 **/ findA11yMenuComponentNames( PackageManager packageManager, int userId)339 private static Set<ComponentName> findA11yMenuComponentNames( 340 PackageManager packageManager, int userId) { 341 Set<ComponentName> result = new ArraySet<>(); 342 final PackageManager.ResolveInfoFlags flags = PackageManager.ResolveInfoFlags.of( 343 PackageManager.MATCH_DISABLED_COMPONENTS 344 | PackageManager.MATCH_DIRECT_BOOT_AWARE 345 | PackageManager.MATCH_DIRECT_BOOT_UNAWARE); 346 for (ResolveInfo resolveInfo : packageManager.queryIntentServicesAsUser( 347 new Intent(AccessibilityService.SERVICE_INTERFACE), flags, userId)) { 348 final ComponentName componentName = resolveInfo.serviceInfo.getComponentName(); 349 if (componentName.getClassName().endsWith(MENU_SERVICE_RELATIVE_CLASS_NAME)) { 350 result.add(componentName); 351 } 352 } 353 return result; 354 } 355 356 /** Returns the {@link ComponentName} of an installed accessibility service by label. */ 357 @Nullable getInstalledAccessibilityServiceComponentNameByLabel( Context context, String label)358 public static ComponentName getInstalledAccessibilityServiceComponentNameByLabel( 359 Context context, String label) { 360 AccessibilityManager accessibilityManager = 361 context.getSystemService(AccessibilityManager.class); 362 List<AccessibilityServiceInfo> serviceInfos = 363 accessibilityManager.getInstalledAccessibilityServiceList(); 364 365 for (AccessibilityServiceInfo service : serviceInfos) { 366 final ServiceInfo serviceInfo = service.getResolveInfo().serviceInfo; 367 if (label.equals(serviceInfo.loadLabel(context.getPackageManager()).toString()) 368 && (serviceInfo.applicationInfo.isSystemApp() 369 || serviceInfo.applicationInfo.isUpdatedSystemApp())) { 370 return new ComponentName(serviceInfo.packageName, serviceInfo.name); 371 } 372 } 373 return null; 374 } 375 } 376