1 /*
2  * Copyright (C) 2019 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.settings.accessibility;
18 
19 import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_BUTTON_COMPONENT_NAME;
20 import static com.android.internal.accessibility.AccessibilityShortcutController.ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME;
21 import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_COMPONENT_NAME;
22 
23 import android.accessibilityservice.AccessibilityServiceInfo;
24 import android.app.Activity;
25 import android.app.AppOpsManager;
26 import android.app.admin.DevicePolicyManager;
27 import android.app.settings.SettingsEnums;
28 import android.content.ComponentName;
29 import android.content.Intent;
30 import android.content.pm.ResolveInfo;
31 import android.content.pm.ServiceInfo;
32 import android.os.Bundle;
33 import android.os.UserHandle;
34 import android.text.TextUtils;
35 import android.util.Log;
36 import android.view.accessibility.AccessibilityManager;
37 
38 import androidx.annotation.NonNull;
39 import androidx.annotation.Nullable;
40 
41 import com.android.internal.annotations.VisibleForTesting;
42 import com.android.settings.R;
43 import com.android.settings.core.InstrumentedFragment;
44 import com.android.settings.core.SubSettingLauncher;
45 import com.android.settings.overlay.FeatureFactory;
46 import com.android.settingslib.RestrictedLockUtilsInternal;
47 import com.android.settingslib.accessibility.AccessibilityUtils;
48 
49 import java.util.List;
50 import java.util.Objects;
51 import java.util.Set;
52 
53 public class AccessibilityDetailsSettingsFragment extends InstrumentedFragment {
54 
55     private final static String TAG = "A11yDetailsSettings";
56     private AppOpsManager mAppOps;
57 
58     @Override
getMetricsCategory()59     public int getMetricsCategory() {
60         return SettingsEnums.ACCESSIBILITY_DETAILS_SETTINGS;
61     }
62 
63     @Override
onCreate(Bundle savedInstanceState)64     public void onCreate(Bundle savedInstanceState) {
65         super.onCreate(savedInstanceState);
66 
67         mAppOps = getActivity().getSystemService(AppOpsManager.class);
68 
69         // In case the Intent doesn't have component name, go to a11y services list.
70         final String extraComponentName = getActivity().getIntent().getStringExtra(
71                 Intent.EXTRA_COMPONENT_NAME);
72         if (extraComponentName == null) {
73             Log.w(TAG, "Open accessibility services list due to no component name.");
74             openAccessibilitySettingsAndFinish();
75             return;
76         }
77 
78         final ComponentName componentName = ComponentName.unflattenFromString(extraComponentName);
79         if (openSystemAccessibilitySettingsAndFinish(componentName)) {
80             return;
81         }
82 
83         if (openAccessibilityDetailsSettingsAndFinish(componentName)) {
84             return;
85         }
86         // Fall back to open accessibility services list.
87         openAccessibilitySettingsAndFinish();
88     }
89 
openSystemAccessibilitySettingsAndFinish( @ullable ComponentName componentName)90     private boolean openSystemAccessibilitySettingsAndFinish(
91             @Nullable ComponentName componentName) {
92         final LaunchFragmentArguments launchArguments =
93                 getSystemAccessibilitySettingsLaunchArguments(componentName);
94         if (launchArguments == null) {
95             return false;
96         }
97         openSubSettings(launchArguments.mDestination, launchArguments.mArguments);
98         finish();
99         return true;
100     }
101 
102     @Nullable
getSystemAccessibilitySettingsLaunchArguments( @ullable ComponentName componentName)103     private LaunchFragmentArguments getSystemAccessibilitySettingsLaunchArguments(
104             @Nullable ComponentName componentName) {
105         if (MAGNIFICATION_COMPONENT_NAME.equals(componentName)) {
106             final String destination = ToggleScreenMagnificationPreferenceFragment.class.getName();
107             return new LaunchFragmentArguments(destination, /* arguments= */ null);
108         }
109 
110         if (ACCESSIBILITY_BUTTON_COMPONENT_NAME.equals(componentName)) {
111             final String destination = AccessibilityButtonFragment.class.getName();
112             return new LaunchFragmentArguments(destination, /* arguments= */ null);
113         }
114 
115         if (ACCESSIBILITY_HEARING_AIDS_COMPONENT_NAME.equals(componentName)) {
116             final String destination = AccessibilityHearingAidsFragment.class.getName();
117             return new LaunchFragmentArguments(destination, /* arguments= */ null);
118         }
119 
120         return null;
121     }
122 
123 
openAccessibilitySettingsAndFinish()124     private void openAccessibilitySettingsAndFinish() {
125         openSubSettings(AccessibilitySettings.class.getName(), /* arguments= */ null);
126         finish();
127     }
128 
openAccessibilityDetailsSettingsAndFinish( @ullable ComponentName componentName)129     private boolean openAccessibilityDetailsSettingsAndFinish(
130             @Nullable ComponentName componentName) {
131         // In case the A11yServiceInfo doesn't exist, go to ally services list.
132         final AccessibilityServiceInfo info = getAccessibilityServiceInfo(componentName);
133         if (info == null) {
134             Log.w(TAG, "openAccessibilityDetailsSettingsAndFinish : invalid component name.");
135             return false;
136         }
137 
138         // In case this accessibility service isn't permitted, go to a11y services list.
139         if (!isServiceAllowed(info.getResolveInfo().serviceInfo.applicationInfo.uid,
140                 componentName.getPackageName())) {
141             Log.w(TAG,
142                     "openAccessibilityDetailsSettingsAndFinish: target accessibility service is"
143                             + "prohibited by Device Admin or App Op.");
144             return false;
145         }
146         openSubSettings(ToggleAccessibilityServicePreferenceFragment.class.getName(),
147                 buildArguments(info));
148         finish();
149         return true;
150     }
151 
openSubSettings(@onNull String destination, @Nullable Bundle arguments)152     private void openSubSettings(@NonNull String destination, @Nullable Bundle arguments) {
153         new SubSettingLauncher(getActivity())
154                 .setDestination(destination)
155                 .setSourceMetricsCategory(getMetricsCategory())
156                 .setArguments(arguments)
157                 .launch();
158     }
159 
160     @VisibleForTesting
isServiceAllowed(int uid, String packageName)161     boolean isServiceAllowed(int uid, String packageName) {
162         final DevicePolicyManager dpm = getContext().getSystemService(DevicePolicyManager.class);
163         final List<String> permittedServices = dpm.getPermittedAccessibilityServices(
164                 UserHandle.myUserId());
165         if (permittedServices != null && !permittedServices.contains(packageName)) {
166             return false;
167         }
168 
169         return !RestrictedLockUtilsInternal.isEnhancedConfirmationRestricted(getContext(),
170                 packageName, AppOpsManager.OPSTR_BIND_ACCESSIBILITY_SERVICE);
171     }
172 
getAccessibilityServiceInfo(ComponentName componentName)173     private AccessibilityServiceInfo getAccessibilityServiceInfo(ComponentName componentName) {
174         if (componentName == null) {
175             return null;
176         }
177 
178         final List<AccessibilityServiceInfo> serviceInfos = AccessibilityManager.getInstance(
179                 getActivity()).getInstalledAccessibilityServiceList();
180         final int serviceInfoCount = serviceInfos.size();
181         for (int i = 0; i < serviceInfoCount; i++) {
182             AccessibilityServiceInfo serviceInfo = serviceInfos.get(i);
183             ResolveInfo resolveInfo = serviceInfo.getResolveInfo();
184             if (componentName.getPackageName().equals(resolveInfo.serviceInfo.packageName)
185                     && componentName.getClassName().equals(resolveInfo.serviceInfo.name)) {
186                 return serviceInfo;
187             }
188         }
189         return null;
190     }
191 
buildArguments(AccessibilityServiceInfo info)192     private Bundle buildArguments(AccessibilityServiceInfo info) {
193         final ResolveInfo resolveInfo = info.getResolveInfo();
194         final String title = resolveInfo.loadLabel(getActivity().getPackageManager()).toString();
195         final ServiceInfo serviceInfo = resolveInfo.serviceInfo;
196         final String packageName = serviceInfo.packageName;
197         final ComponentName componentName = new ComponentName(packageName, serviceInfo.name);
198 
199         final Set<ComponentName> enabledServices =
200                 AccessibilityUtils.getEnabledServicesFromSettings(getActivity());
201         final boolean serviceEnabled = enabledServices.contains(componentName);
202         String description = info.loadDescription(getActivity().getPackageManager());
203 
204         if (serviceEnabled && info.crashed) {
205             // Update the summaries for services that have crashed.
206             description = getString(R.string.accessibility_description_state_stopped);
207         }
208 
209         final Bundle extras = new Bundle();
210         extras.putString(AccessibilitySettings.EXTRA_PREFERENCE_KEY,
211                 componentName.flattenToString());
212         extras.putBoolean(AccessibilitySettings.EXTRA_CHECKED, serviceEnabled);
213         extras.putString(AccessibilitySettings.EXTRA_TITLE, title);
214         extras.putParcelable(AccessibilitySettings.EXTRA_RESOLVE_INFO, resolveInfo);
215         extras.putString(AccessibilitySettings.EXTRA_SUMMARY, description);
216 
217         final String settingsClassName = info.getSettingsActivityName();
218         if (!TextUtils.isEmpty(settingsClassName)) {
219             extras.putString(AccessibilitySettings.EXTRA_SETTINGS_TITLE,
220                     getString(R.string.accessibility_menu_item_settings));
221             extras.putString(AccessibilitySettings.EXTRA_SETTINGS_COMPONENT_NAME,
222                     new ComponentName(packageName, settingsClassName).flattenToString());
223         }
224 
225         final String tileServiceClassName = info.getTileServiceName();
226         if (!TextUtils.isEmpty(tileServiceClassName)) {
227             extras.putString(AccessibilitySettings.EXTRA_TILE_SERVICE_COMPONENT_NAME,
228                     new ComponentName(packageName, tileServiceClassName).flattenToString());
229         }
230 
231         final int metricsCategory = FeatureFactory.getFeatureFactory()
232                 .getAccessibilityMetricsFeatureProvider()
233                 .getDownloadedFeatureMetricsCategory(componentName);
234         extras.putInt(AccessibilitySettings.EXTRA_METRICS_CATEGORY, metricsCategory);
235         extras.putParcelable(AccessibilitySettings.EXTRA_COMPONENT_NAME, componentName);
236         extras.putInt(AccessibilitySettings.EXTRA_ANIMATED_IMAGE_RES, info.getAnimatedImageRes());
237 
238         final String htmlDescription = info.loadHtmlDescription(getActivity().getPackageManager());
239         extras.putString(AccessibilitySettings.EXTRA_HTML_DESCRIPTION, htmlDescription);
240 
241         final CharSequence intro = info.loadIntro(getActivity().getPackageManager());
242         extras.putCharSequence(AccessibilitySettings.EXTRA_INTRO, intro);
243 
244         // We will log nonA11yTool status from PolicyWarningUIController; others none.
245         extras.putLong(AccessibilitySettings.EXTRA_TIME_FOR_LOGGING,
246                 getActivity().getIntent().getLongExtra(
247                         AccessibilitySettings.EXTRA_TIME_FOR_LOGGING, 0));
248         return extras;
249     }
250 
finish()251     private void finish() {
252         Activity activity = getActivity();
253         if (activity == null) {
254             return;
255         }
256         activity.finish();
257     }
258 
259     private static class LaunchFragmentArguments {
260         final String mDestination;
261         final Bundle mArguments;
LaunchFragmentArguments(@onNull String destination, @Nullable Bundle arguments)262         LaunchFragmentArguments(@NonNull String destination, @Nullable Bundle arguments) {
263             mDestination = Objects.requireNonNull(destination);
264             mArguments = arguments;
265         }
266     }
267 }
268