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.dialog;
18 
19 import static android.view.accessibility.AccessibilityManager.ACCESSIBILITY_BUTTON;
20 
21 import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME;
22 import static com.android.internal.accessibility.AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME;
23 import static com.android.internal.accessibility.AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME;
24 import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME;
25 import static com.android.internal.accessibility.AccessibilityShortcutController.ONE_HANDED_COMPONENT_NAME;
26 import static com.android.internal.accessibility.AccessibilityShortcutController.REDUCE_BRIGHT_COLORS_COMPONENT_NAME;
27 import static com.android.internal.accessibility.util.AccessibilityUtils.getAccessibilityServiceFragmentType;
28 import static com.android.internal.accessibility.util.ShortcutUtils.isShortcutContained;
29 import static com.android.internal.os.RoSystemProperties.SUPPORT_ONE_HANDED_MODE;
30 
31 import android.accessibilityservice.AccessibilityServiceInfo;
32 import android.accessibilityservice.AccessibilityShortcutInfo;
33 import android.annotation.NonNull;
34 import android.app.ActivityManager;
35 import android.content.ComponentName;
36 import android.content.Context;
37 import android.os.Build;
38 import android.os.UserHandle;
39 import android.provider.Settings;
40 import android.text.BidiFormatter;
41 import android.view.LayoutInflater;
42 import android.view.View;
43 import android.view.accessibility.AccessibilityManager;
44 import android.view.accessibility.AccessibilityManager.ShortcutType;
45 import android.widget.Button;
46 import android.widget.ImageView;
47 import android.widget.TextView;
48 
49 import com.android.internal.R;
50 import com.android.internal.accessibility.common.ShortcutConstants.AccessibilityFragmentType;
51 
52 import java.util.ArrayList;
53 import java.util.Collections;
54 import java.util.List;
55 import java.util.Locale;
56 
57 /**
58  * Collection of utilities for accessibility target.
59  */
60 public final class AccessibilityTargetHelper {
AccessibilityTargetHelper()61     private AccessibilityTargetHelper() {}
62 
63     /**
64      * Returns list of {@link AccessibilityTarget} of assigned accessibility shortcuts from
65      * {@link AccessibilityManager#getAccessibilityShortcutTargets} including accessibility
66      * feature's package name, component id, etc.
67      *
68      * @param context The context of the application.
69      * @param shortcutType The shortcut type.
70      * @return The list of {@link AccessibilityTarget}.
71      * @hide
72      */
getTargets(Context context, @ShortcutType int shortcutType)73     public static List<AccessibilityTarget> getTargets(Context context,
74             @ShortcutType int shortcutType) {
75         // List all accessibility target
76         final List<AccessibilityTarget> installedTargets = getInstalledTargets(context,
77                 shortcutType);
78 
79         // List accessibility shortcut target
80         final AccessibilityManager am = (AccessibilityManager) context.getSystemService(
81                 Context.ACCESSIBILITY_SERVICE);
82         final List<String> assignedTargets = am.getAccessibilityShortcutTargets(shortcutType);
83 
84         // Get the list of accessibility shortcut target in all accessibility target
85         final List<AccessibilityTarget> results = new ArrayList<>();
86         for (String assignedTarget : assignedTargets) {
87             for (AccessibilityTarget installedTarget : installedTargets) {
88                 if (!MAGNIFICATION_CONTROLLER_NAME.contentEquals(assignedTarget)) {
89                     final ComponentName assignedTargetComponentName =
90                             ComponentName.unflattenFromString(assignedTarget);
91                     final ComponentName targetComponentName = ComponentName.unflattenFromString(
92                             installedTarget.getId());
93                     if (assignedTargetComponentName.equals(targetComponentName)) {
94                         results.add(installedTarget);
95                         continue;
96                     }
97                 }
98                 if (assignedTarget.contentEquals(installedTarget.getId())) {
99                     results.add(installedTarget);
100                 }
101             }
102         }
103         return results;
104     }
105 
106     /**
107      * Returns list of {@link AccessibilityTarget} of the installed accessibility service,
108      * accessibility activity, and allowlisting feature including accessibility feature's package
109      * name, component id, etc.
110      *
111      * @param context The context of the application.
112      * @param shortcutType The shortcut type.
113      * @return The list of {@link AccessibilityTarget}.
114      */
getInstalledTargets(Context context, @ShortcutType int shortcutType)115     static List<AccessibilityTarget> getInstalledTargets(Context context,
116             @ShortcutType int shortcutType) {
117         final List<AccessibilityTarget> targets = new ArrayList<>();
118         targets.addAll(getAccessibilityFilteredTargets(context, shortcutType));
119         targets.addAll(getAllowListingFeatureTargets(context, shortcutType));
120 
121         return targets;
122     }
123 
getAccessibilityFilteredTargets(Context context, @ShortcutType int shortcutType)124     private static List<AccessibilityTarget> getAccessibilityFilteredTargets(Context context,
125             @ShortcutType int shortcutType) {
126         final List<AccessibilityTarget> serviceTargets =
127                 getAccessibilityServiceTargets(context, shortcutType);
128         final List<AccessibilityTarget> activityTargets =
129                 getAccessibilityActivityTargets(context, shortcutType);
130 
131         for (AccessibilityTarget activityTarget : activityTargets) {
132             serviceTargets.removeIf(
133                     serviceTarget -> arePackageNameAndLabelTheSame(serviceTarget, activityTarget));
134         }
135 
136         final List<AccessibilityTarget> targets = new ArrayList<>();
137         targets.addAll(serviceTargets);
138         targets.addAll(activityTargets);
139 
140         return targets;
141     }
142 
arePackageNameAndLabelTheSame(@onNull AccessibilityTarget serviceTarget, @NonNull AccessibilityTarget activityTarget)143     private static boolean arePackageNameAndLabelTheSame(@NonNull AccessibilityTarget serviceTarget,
144             @NonNull AccessibilityTarget activityTarget) {
145         final ComponentName serviceComponentName =
146                 ComponentName.unflattenFromString(serviceTarget.getId());
147         final ComponentName activityComponentName =
148                 ComponentName.unflattenFromString(activityTarget.getId());
149         final boolean isSamePackageName = activityComponentName.getPackageName().equals(
150                 serviceComponentName.getPackageName());
151         final boolean isSameLabel = activityTarget.getLabel().equals(
152                 serviceTarget.getLabel());
153 
154         return isSamePackageName && isSameLabel;
155     }
156 
getAccessibilityServiceTargets(Context context, @ShortcutType int shortcutType)157     private static List<AccessibilityTarget> getAccessibilityServiceTargets(Context context,
158             @ShortcutType int shortcutType) {
159         final AccessibilityManager am = (AccessibilityManager) context.getSystemService(
160                 Context.ACCESSIBILITY_SERVICE);
161         final List<AccessibilityServiceInfo> installedServices =
162                 am.getInstalledAccessibilityServiceList();
163         if (installedServices == null) {
164             return Collections.emptyList();
165         }
166 
167         final List<AccessibilityTarget> targets = new ArrayList<>(installedServices.size());
168         for (AccessibilityServiceInfo info : installedServices) {
169             final int targetSdk =
170                     info.getResolveInfo().serviceInfo.applicationInfo.targetSdkVersion;
171             final boolean hasRequestAccessibilityButtonFlag =
172                     (info.flags & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0;
173             if ((targetSdk <= Build.VERSION_CODES.Q) && !hasRequestAccessibilityButtonFlag
174                     && (shortcutType == ACCESSIBILITY_BUTTON)) {
175                 continue;
176             }
177 
178             targets.add(createAccessibilityServiceTarget(context, shortcutType, info));
179         }
180 
181         return targets;
182     }
183 
getAccessibilityActivityTargets(Context context, @ShortcutType int shortcutType)184     private static List<AccessibilityTarget> getAccessibilityActivityTargets(Context context,
185             @ShortcutType int shortcutType) {
186         final AccessibilityManager am = (AccessibilityManager) context.getSystemService(
187                 Context.ACCESSIBILITY_SERVICE);
188         final List<AccessibilityShortcutInfo> installedServices =
189                 am.getInstalledAccessibilityShortcutListAsUser(context,
190                         ActivityManager.getCurrentUser());
191         if (installedServices == null) {
192             return Collections.emptyList();
193         }
194 
195         final List<AccessibilityTarget> targets = new ArrayList<>(installedServices.size());
196         for (AccessibilityShortcutInfo info : installedServices) {
197             targets.add(new AccessibilityActivityTarget(context, shortcutType, info));
198         }
199 
200         return targets;
201     }
202 
getAllowListingFeatureTargets(Context context, @ShortcutType int shortcutType)203     private static List<AccessibilityTarget> getAllowListingFeatureTargets(Context context,
204             @ShortcutType int shortcutType) {
205         final List<AccessibilityTarget> targets = new ArrayList<>();
206         final int uid = context.getApplicationInfo().uid;
207 
208         final InvisibleToggleAllowListingFeatureTarget magnification =
209                 new InvisibleToggleAllowListingFeatureTarget(context,
210                         shortcutType,
211                         isShortcutContained(context, shortcutType, MAGNIFICATION_CONTROLLER_NAME),
212                         MAGNIFICATION_CONTROLLER_NAME,
213                         uid,
214                         context.getString(R.string.accessibility_magnification_chooser_text),
215                         context.getDrawable(R.drawable.ic_accessibility_magnification),
216                         Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED);
217         targets.add(magnification);
218 
219         final ToggleAllowListingFeatureTarget daltonizer =
220                 new ToggleAllowListingFeatureTarget(context,
221                         shortcutType,
222                         isShortcutContained(context, shortcutType,
223                                 DALTONIZER_COMPONENT_NAME.flattenToString()),
224                         DALTONIZER_COMPONENT_NAME.flattenToString(),
225                         uid,
226                         context.getString(R.string.color_correction_feature_name),
227                         context.getDrawable(R.drawable.ic_accessibility_color_correction),
228                         Settings.Secure.ACCESSIBILITY_DISPLAY_DALTONIZER_ENABLED);
229         targets.add(daltonizer);
230 
231         final ToggleAllowListingFeatureTarget colorInversion =
232                 new ToggleAllowListingFeatureTarget(context,
233                         shortcutType,
234                         isShortcutContained(context, shortcutType,
235                                 COLOR_INVERSION_COMPONENT_NAME.flattenToString()),
236                         COLOR_INVERSION_COMPONENT_NAME.flattenToString(),
237                         uid,
238                         context.getString(R.string.color_inversion_feature_name),
239                         context.getDrawable(R.drawable.ic_accessibility_color_inversion),
240                         Settings.Secure.ACCESSIBILITY_DISPLAY_INVERSION_ENABLED);
241         targets.add(colorInversion);
242 
243         if (SUPPORT_ONE_HANDED_MODE) {
244             final ToggleAllowListingFeatureTarget oneHandedMode =
245                     new ToggleAllowListingFeatureTarget(context,
246                             shortcutType,
247                             isShortcutContained(context, shortcutType,
248                                     ONE_HANDED_COMPONENT_NAME.flattenToString()),
249                             ONE_HANDED_COMPONENT_NAME.flattenToString(),
250                             uid,
251                             context.getString(R.string.one_handed_mode_feature_name),
252                             context.getDrawable(R.drawable.ic_accessibility_one_handed),
253                             Settings.Secure.ONE_HANDED_MODE_ACTIVATED);
254             targets.add(oneHandedMode);
255         }
256 
257         final ToggleAllowListingFeatureTarget reduceBrightColors =
258                 new ToggleAllowListingFeatureTarget(context,
259                         shortcutType,
260                         isShortcutContained(context, shortcutType,
261                                 REDUCE_BRIGHT_COLORS_COMPONENT_NAME.flattenToString()),
262                         REDUCE_BRIGHT_COLORS_COMPONENT_NAME.flattenToString(),
263                         uid,
264                         context.getString(R.string.reduce_bright_colors_feature_name),
265                         context.getDrawable(R.drawable.ic_accessibility_reduce_bright_colors),
266                         Settings.Secure.REDUCE_BRIGHT_COLORS_ACTIVATED);
267         targets.add(reduceBrightColors);
268 
269         final InvisibleToggleAllowListingFeatureTarget hearingAids =
270                 new InvisibleToggleAllowListingFeatureTarget(context,
271                         shortcutType,
272                         isShortcutContained(context, shortcutType,
273                                 ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME.flattenToString()),
274                         ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME.flattenToString(),
275                         uid,
276                         context.getString(R.string.hearing_aids_feature_name),
277                         context.getDrawable(R.drawable.ic_accessibility_hearing_aid),
278                         /* key= */ null);
279         targets.add(hearingAids);
280 
281         return targets;
282     }
283 
createAccessibilityServiceTarget(Context context, @ShortcutType int shortcutType, @NonNull AccessibilityServiceInfo info)284     private static AccessibilityTarget createAccessibilityServiceTarget(Context context,
285             @ShortcutType int shortcutType, @NonNull AccessibilityServiceInfo info) {
286         switch (getAccessibilityServiceFragmentType(info)) {
287             case AccessibilityFragmentType.VOLUME_SHORTCUT_TOGGLE:
288                 return new VolumeShortcutToggleAccessibilityServiceTarget(context, shortcutType,
289                         info);
290             case AccessibilityFragmentType.INVISIBLE_TOGGLE:
291                 return new InvisibleToggleAccessibilityServiceTarget(context, shortcutType, info);
292             case AccessibilityFragmentType.TOGGLE:
293                 return new ToggleAccessibilityServiceTarget(context, shortcutType, info);
294             default:
295                 throw new IllegalStateException("Unexpected fragment type");
296         }
297     }
298 
createEnableDialogContentView(Context context, AccessibilityServiceTarget target, View.OnClickListener allowListener, View.OnClickListener denyListener)299     static View createEnableDialogContentView(Context context,
300             AccessibilityServiceTarget target, View.OnClickListener allowListener,
301             View.OnClickListener denyListener) {
302         final LayoutInflater inflater = (LayoutInflater) context.getSystemService(
303                 Context.LAYOUT_INFLATER_SERVICE);
304 
305         final View content = inflater.inflate(
306                 R.layout.accessibility_enable_service_warning, /* root= */ null);
307 
308         final ImageView dialogIcon = content.findViewById(
309                 R.id.accessibility_permissionDialog_icon);
310         dialogIcon.setImageDrawable(target.getIcon());
311 
312         final TextView dialogTitle = content.findViewById(
313                 R.id.accessibility_permissionDialog_title);
314         dialogTitle.setText(context.getString(R.string.accessibility_enable_service_title,
315                 getServiceName(context, target.getLabel())));
316 
317         final Button allowButton = content.findViewById(
318                 R.id.accessibility_permission_enable_allow_button);
319         final Button denyButton = content.findViewById(
320                 R.id.accessibility_permission_enable_deny_button);
321         allowButton.setOnClickListener((view) -> {
322             target.onCheckedChanged(/* isChecked= */ true);
323             allowListener.onClick(view);
324         });
325         denyButton.setOnClickListener((view) -> {
326             target.onCheckedChanged(/* isChecked= */ false);
327             denyListener.onClick(view);
328         });
329 
330         return content;
331     }
332 
333     // Gets the service name and bidi wrap it to protect from bidi side effects.
getServiceName(Context context, CharSequence label)334     private static CharSequence getServiceName(Context context, CharSequence label) {
335         final Locale locale = context.getResources().getConfiguration().getLocales().get(0);
336         return BidiFormatter.getInstance(locale).unicodeWrap(label);
337     }
338 
339     /**
340      * Determines if the{@link AccessibilityTarget} is allowed.
341      */
isAccessibilityTargetAllowed(Context context, String packageName, int uid)342     public static boolean isAccessibilityTargetAllowed(Context context, String packageName,
343             int uid) {
344         final AccessibilityManager am = context.getSystemService(AccessibilityManager.class);
345         return am.isAccessibilityTargetAllowed(packageName, uid, UserHandle.myUserId());
346     }
347 
348     /**
349      * Sends restricted dialog intent if the accessibility target is disallowed.
350      */
sendRestrictedDialogIntent(Context context, String packageName, int uid)351     public static boolean sendRestrictedDialogIntent(Context context, String packageName, int uid) {
352         final AccessibilityManager am = context.getSystemService(AccessibilityManager.class);
353         return am.sendRestrictedDialogIntent(packageName, uid, UserHandle.myUserId());
354     }
355 }
356