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.AccessibilityServiceInfo; 23 import android.annotation.IntDef; 24 import android.annotation.NonNull; 25 import android.content.ComponentName; 26 import android.content.Context; 27 import android.os.Build; 28 import android.os.UserHandle; 29 import android.provider.Settings; 30 import android.text.ParcelableSpan; 31 import android.text.Spanned; 32 import android.text.TextUtils; 33 import android.util.ArraySet; 34 import android.view.accessibility.AccessibilityManager; 35 36 import libcore.util.EmptyArray; 37 38 import java.lang.annotation.Retention; 39 import java.lang.annotation.RetentionPolicy; 40 import java.util.Collections; 41 import java.util.HashSet; 42 import java.util.List; 43 import java.util.Set; 44 45 /** 46 * Collection of utilities for accessibility service. 47 */ 48 public final class AccessibilityUtils { AccessibilityUtils()49 private AccessibilityUtils() { 50 } 51 52 /** @hide */ 53 @IntDef(value = { 54 NONE, 55 TEXT, 56 PARCELABLE_SPAN 57 }) 58 @Retention(RetentionPolicy.SOURCE) 59 public @interface A11yTextChangeType { 60 } 61 62 /** Specifies no content has been changed for accessibility. */ 63 public static final int NONE = 0; 64 /** Specifies some readable sequence has been changed. */ 65 public static final int TEXT = 1; 66 /** Specifies some parcelable spans has been changed. */ 67 public static final int PARCELABLE_SPAN = 2; 68 69 /** 70 * Returns the set of enabled accessibility services for userId. If there are no 71 * services, it returns the unmodifiable {@link Collections#emptySet()}. 72 */ getEnabledServicesFromSettings(Context context, int userId)73 public static Set<ComponentName> getEnabledServicesFromSettings(Context context, int userId) { 74 final String enabledServicesSetting = Settings.Secure.getStringForUser( 75 context.getContentResolver(), Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, 76 userId); 77 if (TextUtils.isEmpty(enabledServicesSetting)) { 78 return Collections.emptySet(); 79 } 80 81 final Set<ComponentName> enabledServices = new HashSet<>(); 82 final TextUtils.StringSplitter colonSplitter = 83 new TextUtils.SimpleStringSplitter(SERVICES_SEPARATOR); 84 colonSplitter.setString(enabledServicesSetting); 85 86 for (String componentNameString : colonSplitter) { 87 final ComponentName enabledService = ComponentName.unflattenFromString( 88 componentNameString); 89 if (enabledService != null) { 90 enabledServices.add(enabledService); 91 } 92 } 93 94 return enabledServices; 95 } 96 97 /** 98 * Changes an accessibility component's state. 99 */ setAccessibilityServiceState(Context context, ComponentName componentName, boolean enabled)100 public static void setAccessibilityServiceState(Context context, ComponentName componentName, 101 boolean enabled) { 102 setAccessibilityServiceState(context, componentName, enabled, UserHandle.myUserId()); 103 } 104 105 /** 106 * Changes an accessibility component's state for {@param userId}. 107 */ setAccessibilityServiceState(Context context, ComponentName componentName, boolean enabled, int userId)108 public static void setAccessibilityServiceState(Context context, ComponentName componentName, 109 boolean enabled, int userId) { 110 Set<ComponentName> enabledServices = getEnabledServicesFromSettings( 111 context, userId); 112 113 if (enabledServices.isEmpty()) { 114 enabledServices = new ArraySet<>(/* capacity= */ 1); 115 } 116 117 if (enabled) { 118 enabledServices.add(componentName); 119 } else { 120 enabledServices.remove(componentName); 121 } 122 123 final StringBuilder enabledServicesBuilder = new StringBuilder(); 124 for (ComponentName enabledService : enabledServices) { 125 enabledServicesBuilder.append(enabledService.flattenToString()); 126 enabledServicesBuilder.append( 127 SERVICES_SEPARATOR); 128 } 129 130 final int enabledServicesBuilderLength = enabledServicesBuilder.length(); 131 if (enabledServicesBuilderLength > 0) { 132 enabledServicesBuilder.deleteCharAt(enabledServicesBuilderLength - 1); 133 } 134 135 Settings.Secure.putStringForUser(context.getContentResolver(), 136 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES, 137 enabledServicesBuilder.toString(), userId); 138 } 139 140 /** 141 * Gets the corresponding fragment type of a given accessibility service. 142 * 143 * @param accessibilityServiceInfo The accessibilityService's info. 144 * @return int from {@link AccessibilityFragmentType}. 145 */ getAccessibilityServiceFragmentType( @onNull AccessibilityServiceInfo accessibilityServiceInfo)146 public static @AccessibilityFragmentType int getAccessibilityServiceFragmentType( 147 @NonNull AccessibilityServiceInfo accessibilityServiceInfo) { 148 final int targetSdk = accessibilityServiceInfo.getResolveInfo() 149 .serviceInfo.applicationInfo.targetSdkVersion; 150 final boolean requestA11yButton = (accessibilityServiceInfo.flags 151 & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0; 152 153 if (targetSdk <= Build.VERSION_CODES.Q) { 154 return AccessibilityFragmentType.VOLUME_SHORTCUT_TOGGLE; 155 } 156 return requestA11yButton 157 ? AccessibilityFragmentType.INVISIBLE_TOGGLE 158 : AccessibilityFragmentType.TOGGLE; 159 } 160 161 /** 162 * Returns if a {@code componentId} service is enabled. 163 * 164 * @param context The current context. 165 * @param componentId The component id that need to be checked. 166 * @return {@code true} if a {@code componentId} service is enabled. 167 */ isAccessibilityServiceEnabled(Context context, @NonNull String componentId)168 public static boolean isAccessibilityServiceEnabled(Context context, 169 @NonNull String componentId) { 170 final AccessibilityManager am = (AccessibilityManager) context.getSystemService( 171 Context.ACCESSIBILITY_SERVICE); 172 final List<AccessibilityServiceInfo> enabledServices = 173 am.getEnabledAccessibilityServiceList(AccessibilityServiceInfo.FEEDBACK_ALL_MASK); 174 175 for (AccessibilityServiceInfo info : enabledServices) { 176 final String id = info.getComponentName().flattenToString(); 177 if (id.equals(componentId)) { 178 return true; 179 } 180 } 181 182 return false; 183 } 184 185 /** 186 * Indicates whether the current user has completed setup via the setup wizard. 187 * {@link android.provider.Settings.Secure#USER_SETUP_COMPLETE} 188 * 189 * @return {@code true} if the setup is completed. 190 */ isUserSetupCompleted(Context context)191 public static boolean isUserSetupCompleted(Context context) { 192 return Settings.Secure.getIntForUser(context.getContentResolver(), 193 Settings.Secure.USER_SETUP_COMPLETE, /* def= */ 0, UserHandle.USER_CURRENT) 194 != /* false */ 0; 195 } 196 197 /** 198 * Returns the text change type for accessibility. It only cares about readable sequence changes 199 * or {@link ParcelableSpan} changes which are able to pass via IPC. 200 * 201 * @param before The CharSequence before changing 202 * @param after The CharSequence after changing 203 * @return Returns {@code TEXT} for readable sequence changes or {@code PARCELABLE_SPAN} for 204 * ParcelableSpan changes. Otherwise, returns {@code NONE}. 205 */ 206 @A11yTextChangeType textOrSpanChanged(CharSequence before, CharSequence after)207 public static int textOrSpanChanged(CharSequence before, CharSequence after) { 208 if (!TextUtils.equals(before, after)) { 209 return TEXT; 210 } 211 if (before instanceof Spanned || after instanceof Spanned) { 212 if (!parcelableSpansEquals(before, after)) { 213 return PARCELABLE_SPAN; 214 } 215 } 216 return NONE; 217 } 218 parcelableSpansEquals(CharSequence before, CharSequence after)219 private static boolean parcelableSpansEquals(CharSequence before, CharSequence after) { 220 Object[] spansA = EmptyArray.OBJECT; 221 Object[] spansB = EmptyArray.OBJECT; 222 Spanned a = null; 223 Spanned b = null; 224 if (before instanceof Spanned) { 225 a = (Spanned) before; 226 spansA = a.getSpans(0, a.length(), ParcelableSpan.class); 227 } 228 if (after instanceof Spanned) { 229 b = (Spanned) after; 230 spansB = b.getSpans(0, b.length(), ParcelableSpan.class); 231 } 232 if (spansA.length != spansB.length) { 233 return false; 234 } 235 for (int i = 0; i < spansA.length; ++i) { 236 final Object thisSpan = spansA[i]; 237 final Object otherSpan = spansB[i]; 238 if ((thisSpan.getClass() != otherSpan.getClass()) 239 || (a.getSpanStart(thisSpan) != b.getSpanStart(otherSpan)) 240 || (a.getSpanEnd(thisSpan) != b.getSpanEnd(otherSpan)) 241 || (a.getSpanFlags(thisSpan) != b.getSpanFlags(otherSpan))) { 242 return false; 243 } 244 } 245 return true; 246 } 247 } 248