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