• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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