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