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