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