• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2013 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.settings.accessibility.AccessibilityDialogUtils.DialogEnums;
20 import static com.android.settings.accessibility.AccessibilityStatsLogUtils.logAccessibilityServiceEnabled;
21 
22 import android.accessibilityservice.AccessibilityServiceInfo;
23 import android.annotation.SuppressLint;
24 import android.app.AlertDialog;
25 import android.app.Dialog;
26 import android.app.settings.SettingsEnums;
27 import android.content.BroadcastReceiver;
28 import android.content.ComponentName;
29 import android.content.ContentResolver;
30 import android.content.Context;
31 import android.content.DialogInterface;
32 import android.content.Intent;
33 import android.content.IntentFilter;
34 import android.content.pm.ApplicationInfo;
35 import android.content.pm.ResolveInfo;
36 import android.net.Uri;
37 import android.os.Bundle;
38 import android.os.SystemClock;
39 import android.text.BidiFormatter;
40 import android.text.TextUtils;
41 import android.util.Log;
42 import android.view.accessibility.AccessibilityManager;
43 import android.widget.CompoundButton;
44 
45 import androidx.annotation.Nullable;
46 
47 import com.android.internal.accessibility.common.ShortcutConstants;
48 import com.android.settings.R;
49 import com.android.settings.accessibility.shortcuts.EditShortcutsPreferenceFragment;
50 import com.android.settingslib.accessibility.AccessibilityUtils;
51 
52 import java.util.List;
53 import java.util.Locale;
54 import java.util.Set;
55 import java.util.concurrent.atomic.AtomicBoolean;
56 
57 /** Fragment for providing toggle bar and basic accessibility service setup. */
58 public class ToggleAccessibilityServicePreferenceFragment extends
59         ToggleFeaturePreferenceFragment {
60 
61     private static final String TAG = "ToggleAccessibilityServicePreferenceFragment";
62     private static final String KEY_HAS_LOGGED = "has_logged";
63     private final AtomicBoolean mIsDialogShown = new AtomicBoolean(/* initialValue= */ false);
64 
65     private Dialog mWarningDialog;
66     private ComponentName mTileComponentName;
67     private BroadcastReceiver mPackageRemovedReceiver;
68     private boolean mDisabledStateLogged = false;
69     private long mStartTimeMillsForLogging = 0;
70 
71     @Override
getMetricsCategory()72     public int getMetricsCategory() {
73         return getArguments().getInt(AccessibilitySettings.EXTRA_METRICS_CATEGORY);
74     }
75 
76     @Override
getFeedbackCategory()77     public int getFeedbackCategory() {
78         return getArguments().getInt(AccessibilitySettings.EXTRA_FEEDBACK_CATEGORY);
79     }
80 
81     @Override
onCreate(Bundle savedInstanceState)82     public void onCreate(Bundle savedInstanceState) {
83         super.onCreate(savedInstanceState);
84         if (savedInstanceState != null) {
85             if (savedInstanceState.containsKey(KEY_HAS_LOGGED)) {
86                 mDisabledStateLogged = savedInstanceState.getBoolean(KEY_HAS_LOGGED);
87             }
88         }
89     }
90 
91     @Override
registerKeysToObserverCallback( AccessibilitySettingsContentObserver contentObserver)92     protected void registerKeysToObserverCallback(
93             AccessibilitySettingsContentObserver contentObserver) {
94         super.registerKeysToObserverCallback(contentObserver);
95         contentObserver.registerObserverCallback(key -> updateSwitchBarToggleSwitch());
96     }
97 
98     @Override
onStart()99     public void onStart() {
100         super.onStart();
101         final AccessibilityServiceInfo serviceInfo = getAccessibilityServiceInfo();
102         if (serviceInfo == null) {
103             getActivity().finishAndRemoveTask();
104         } else if (!AccessibilityUtil.isSystemApp(serviceInfo)) {
105             registerPackageRemoveReceiver();
106         }
107     }
108 
109     @Override
onResume()110     public void onResume() {
111         super.onResume();
112         updateSwitchBarToggleSwitch();
113     }
114 
115     @Override
onSaveInstanceState(Bundle outState)116     public void onSaveInstanceState(Bundle outState) {
117         if (mStartTimeMillsForLogging > 0) {
118             outState.putBoolean(KEY_HAS_LOGGED, mDisabledStateLogged);
119         }
120         super.onSaveInstanceState(outState);
121     }
122 
123     @Override
onPreferenceToggled(String preferenceKey, boolean enabled)124     public void onPreferenceToggled(String preferenceKey, boolean enabled) {
125         ComponentName toggledService = ComponentName.unflattenFromString(preferenceKey);
126         logAccessibilityServiceEnabled(toggledService, enabled);
127         if (!enabled) {
128             logDisabledState(toggledService.getPackageName());
129         }
130         AccessibilityUtils.setAccessibilityServiceState(getPrefContext(), toggledService, enabled);
131     }
132 
133     // IMPORTANT: Refresh the info since there are dynamically changing capabilities. For
134     // example, before JellyBean MR2 the user was granting the explore by touch one.
135     @Nullable
getAccessibilityServiceInfo()136     AccessibilityServiceInfo getAccessibilityServiceInfo() {
137         final List<AccessibilityServiceInfo> infos = AccessibilityManager.getInstance(
138                 getPrefContext()).getInstalledAccessibilityServiceList();
139 
140         for (int i = 0, count = infos.size(); i < count; i++) {
141             AccessibilityServiceInfo serviceInfo = infos.get(i);
142             ResolveInfo resolveInfo = serviceInfo.getResolveInfo();
143             if (mComponentName.getPackageName().equals(resolveInfo.serviceInfo.packageName)
144                     && mComponentName.getClassName().equals(resolveInfo.serviceInfo.name)) {
145                 return serviceInfo;
146             }
147         }
148         return null;
149     }
150 
151     @Override
onCreateDialog(int dialogId)152     public Dialog onCreateDialog(int dialogId) {
153         final AccessibilityServiceInfo info = getAccessibilityServiceInfo();
154         switch (dialogId) {
155             case DialogEnums.ENABLE_WARNING_FROM_TOGGLE:
156                 if (info == null) {
157                     return null;
158                 }
159                 mWarningDialog =
160                         com.android.internal.accessibility.dialog.AccessibilityServiceWarning
161                                 .createAccessibilityServiceWarningDialog(getPrefContext(), info,
162                                         v -> onAllowButtonFromEnableToggleClicked(),
163                                         v -> onDenyButtonFromEnableToggleClicked(),
164                                         v -> onDialogButtonFromUninstallClicked());
165                 return mWarningDialog;
166             case DialogEnums.ENABLE_WARNING_FROM_SHORTCUT_TOGGLE:
167                 if (info == null) {
168                     return null;
169                 }
170                 mWarningDialog =
171                         com.android.internal.accessibility.dialog.AccessibilityServiceWarning
172                                 .createAccessibilityServiceWarningDialog(getPrefContext(), info,
173                                         v -> onAllowButtonFromShortcutToggleClicked(),
174                                         v -> onDenyButtonFromShortcutToggleClicked(),
175                                         v -> onDialogButtonFromUninstallClicked());
176                 return mWarningDialog;
177             case DialogEnums.ENABLE_WARNING_FROM_SHORTCUT:
178                 if (info == null) {
179                     return null;
180                 }
181                 mWarningDialog =
182                         com.android.internal.accessibility.dialog.AccessibilityServiceWarning
183                                 .createAccessibilityServiceWarningDialog(getPrefContext(), info,
184                                         v -> onAllowButtonFromShortcutClicked(),
185                                         v -> onDenyButtonFromShortcutClicked(),
186                                         v -> onDialogButtonFromUninstallClicked());
187                 return mWarningDialog;
188             case DialogEnums.DISABLE_WARNING_FROM_TOGGLE:
189                 if (info == null) {
190                     return null;
191                 }
192                 mWarningDialog = createDisableDialog(
193                         getPrefContext(), info, this::onDialogButtonFromDisableToggleClicked);
194                 return mWarningDialog;
195             default:
196                 return super.onCreateDialog(dialogId);
197         }
198     }
199 
200     /** Returns a {@link Dialog} to be shown to confirm that they want to disable a service. */
createDisableDialog(Context context, AccessibilityServiceInfo info, DialogInterface.OnClickListener listener)201     private static Dialog createDisableDialog(Context context,
202             AccessibilityServiceInfo info, DialogInterface.OnClickListener listener) {
203         final Locale locale = context.getResources().getConfiguration().getLocales().get(0);
204         final CharSequence label =
205                 info.getResolveInfo().loadLabel(context.getPackageManager());
206         CharSequence serviceName = BidiFormatter.getInstance(locale).unicodeWrap(label);
207 
208         return new AlertDialog.Builder(context)
209                 .setTitle(context.getString(R.string.disable_service_title, serviceName))
210                 .setCancelable(true)
211                 .setPositiveButton(R.string.accessibility_dialog_button_stop, listener)
212                 .setNegativeButton(R.string.accessibility_dialog_button_cancel, listener)
213                 .create();
214     }
215 
216     @Override
getDialogMetricsCategory(int dialogId)217     public int getDialogMetricsCategory(int dialogId) {
218         switch (dialogId) {
219             case DialogEnums.ENABLE_WARNING_FROM_TOGGLE:
220             case DialogEnums.ENABLE_WARNING_FROM_SHORTCUT:
221             case DialogEnums.ENABLE_WARNING_FROM_SHORTCUT_TOGGLE:
222                 return SettingsEnums.DIALOG_ACCESSIBILITY_SERVICE_ENABLE;
223             case DialogEnums.DISABLE_WARNING_FROM_TOGGLE:
224                 return SettingsEnums.DIALOG_ACCESSIBILITY_SERVICE_DISABLE;
225             case DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL:
226                 return SettingsEnums.DIALOG_ACCESSIBILITY_TUTORIAL;
227             default:
228                 return super.getDialogMetricsCategory(dialogId);
229         }
230     }
231 
232     @Override
getUserShortcutTypes()233     int getUserShortcutTypes() {
234         return AccessibilityUtil.getUserShortcutTypesFromSettings(getPrefContext(),
235                 mComponentName);
236     }
237 
238     @Override
getTileComponentName()239     ComponentName getTileComponentName() {
240         return mTileComponentName;
241     }
242 
243     @Override
updateSwitchBarToggleSwitch()244     protected void updateSwitchBarToggleSwitch() {
245         final boolean checked = isAccessibilityServiceEnabled();
246         if (mToggleServiceSwitchPreference.isChecked() == checked) {
247             return;
248         }
249         mToggleServiceSwitchPreference.setChecked(checked);
250     }
251 
isAccessibilityServiceEnabled()252     private boolean isAccessibilityServiceEnabled() {
253         return AccessibilityUtils.getEnabledServicesFromSettings(getPrefContext())
254                 .contains(mComponentName);
255     }
256 
257     @Override
onActivityResult(int requestCode, int resultCode, Intent data)258     public void onActivityResult(int requestCode, int resultCode, Intent data) {
259     }
260 
registerPackageRemoveReceiver()261     private void registerPackageRemoveReceiver() {
262         if (mPackageRemovedReceiver != null || getContext() == null) {
263             return;
264         }
265         mPackageRemovedReceiver = new BroadcastReceiver() {
266             @Override
267             public void onReceive(Context context, Intent intent) {
268                 final String packageName = intent.getData().getSchemeSpecificPart();
269                 if (TextUtils.equals(mComponentName.getPackageName(), packageName)) {
270                     getActivity().finishAndRemoveTask();
271                 }
272             }
273         };
274         final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_REMOVED);
275         filter.addDataScheme("package");
276         getContext().registerReceiver(mPackageRemovedReceiver, filter);
277     }
278 
unregisterPackageRemoveReceiver()279     private void unregisterPackageRemoveReceiver() {
280         if (mPackageRemovedReceiver == null || getContext() == null) {
281             return;
282         }
283         getContext().unregisterReceiver(mPackageRemovedReceiver);
284         mPackageRemovedReceiver = null;
285     }
286 
serviceSupportsAccessibilityButton()287     boolean serviceSupportsAccessibilityButton() {
288         final AccessibilityServiceInfo info = getAccessibilityServiceInfo();
289         return info != null
290                 && (info.flags & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0;
291     }
292 
handleConfirmServiceEnabled(boolean confirmed)293     private void handleConfirmServiceEnabled(boolean confirmed) {
294         getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, confirmed);
295         onPreferenceToggled(mPreferenceKey, confirmed);
296     }
297 
298     @Override
onCheckedChanged(CompoundButton buttonView, boolean isChecked)299     public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
300         if (isChecked != isAccessibilityServiceEnabled()) {
301             onPreferenceClick(isChecked);
302         }
303     }
304 
305     @SuppressLint("MissingPermission")
306     @Override
onToggleClicked(ShortcutPreference preference)307     public void onToggleClicked(ShortcutPreference preference) {
308         final int shortcutTypes = getUserPreferredShortcutTypes();
309         if (preference.isChecked()) {
310             final boolean isWarningRequired =
311                     getPrefContext().getSystemService(AccessibilityManager.class)
312                             .isAccessibilityServiceWarningRequired(getAccessibilityServiceInfo());
313             if (isWarningRequired) {
314                 preference.setChecked(false);
315                 showPopupDialog(DialogEnums.ENABLE_WARNING_FROM_SHORTCUT_TOGGLE);
316             } else {
317                 onAllowButtonFromShortcutToggleClicked();
318             }
319         } else {
320             getPrefContext().getSystemService(AccessibilityManager.class)
321                             .enableShortcutsForTargets(false, shortcutTypes,
322                                     Set.of(mComponentName.flattenToString()),
323                                     getPrefContext().getUserId());
324         }
325         mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext()));
326     }
327 
328     @Override
onSettingsClicked(ShortcutPreference preference)329     public void onSettingsClicked(ShortcutPreference preference) {
330         final boolean isWarningRequired =
331                 getPrefContext().getSystemService(AccessibilityManager.class)
332                         .isAccessibilityServiceWarningRequired(getAccessibilityServiceInfo());
333         if (isWarningRequired) {
334             showPopupDialog(DialogEnums.ENABLE_WARNING_FROM_SHORTCUT);
335         } else {
336             onAllowButtonFromShortcutClicked();
337         }
338     }
339 
340     @Override
onProcessArguments(Bundle arguments)341     protected void onProcessArguments(Bundle arguments) {
342         super.onProcessArguments(arguments);
343         // Settings title and intent.
344         String settingsTitle = arguments.getString(AccessibilitySettings.EXTRA_SETTINGS_TITLE);
345         String settingsComponentName = arguments.getString(
346                 AccessibilitySettings.EXTRA_SETTINGS_COMPONENT_NAME);
347         if (!TextUtils.isEmpty(settingsTitle) && !TextUtils.isEmpty(settingsComponentName)) {
348             Intent settingsIntent = new Intent(Intent.ACTION_MAIN).setComponent(
349                     ComponentName.unflattenFromString(settingsComponentName.toString()));
350             if (!getPackageManager().queryIntentActivities(settingsIntent, 0).isEmpty()) {
351                 mSettingsTitle = settingsTitle;
352                 mSettingsIntent = settingsIntent;
353                 setHasOptionsMenu(true);
354             }
355         }
356 
357         mComponentName = arguments.getParcelable(AccessibilitySettings.EXTRA_COMPONENT_NAME);
358 
359         // Settings animated image.
360         final int animatedImageRes = arguments.getInt(
361                 AccessibilitySettings.EXTRA_ANIMATED_IMAGE_RES);
362         if (animatedImageRes > 0) {
363             mImageUri = new Uri.Builder().scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
364                     .authority(mComponentName.getPackageName())
365                     .appendPath(String.valueOf(animatedImageRes))
366                     .build();
367         }
368 
369         // Get Accessibility service name.
370         AccessibilityServiceInfo info = getAccessibilityServiceInfo();
371         mFeatureName = info == null ? "" : info.getResolveInfo().loadLabel(getPackageManager());
372 
373         if (arguments.containsKey(AccessibilitySettings.EXTRA_TILE_SERVICE_COMPONENT_NAME)) {
374             final String tileServiceComponentName = arguments.getString(
375                     AccessibilitySettings.EXTRA_TILE_SERVICE_COMPONENT_NAME);
376             mTileComponentName = ComponentName.unflattenFromString(tileServiceComponentName);
377         }
378 
379         mStartTimeMillsForLogging = arguments.getLong(AccessibilitySettings.EXTRA_TIME_FOR_LOGGING);
380     }
381 
onDialogButtonFromDisableToggleClicked(DialogInterface dialog, int which)382     private void onDialogButtonFromDisableToggleClicked(DialogInterface dialog, int which) {
383         switch (which) {
384             case DialogInterface.BUTTON_POSITIVE:
385                 handleConfirmServiceEnabled(/* confirmed= */ false);
386                 break;
387             case DialogInterface.BUTTON_NEGATIVE:
388                 handleConfirmServiceEnabled(/* confirmed= */ true);
389                 break;
390             default:
391                 throw new IllegalArgumentException("Unexpected button identifier");
392         }
393     }
394 
onDialogButtonFromUninstallClicked()395     private void onDialogButtonFromUninstallClicked() {
396         mWarningDialog.dismiss();
397         final Intent uninstallIntent = createUninstallPackageActivityIntent();
398         if (uninstallIntent == null) {
399             return;
400         }
401         startActivity(uninstallIntent);
402     }
403 
404     @Nullable
createUninstallPackageActivityIntent()405     private Intent createUninstallPackageActivityIntent() {
406         final AccessibilityServiceInfo a11yServiceInfo = getAccessibilityServiceInfo();
407         if (a11yServiceInfo == null) {
408             Log.w(TAG, "createUnInstallIntent -- invalid a11yServiceInfo");
409             return null;
410         }
411         final ApplicationInfo appInfo =
412                 a11yServiceInfo.getResolveInfo().serviceInfo.applicationInfo;
413         final Uri packageUri = Uri.parse("package:" + appInfo.packageName);
414         final Intent uninstallIntent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri);
415         return uninstallIntent;
416     }
417 
418     @Override
onStop()419     public void onStop() {
420         super.onStop();
421         unregisterPackageRemoveReceiver();
422     }
423 
424     @Override
getPreferenceScreenResId()425     protected int getPreferenceScreenResId() {
426         // TODO(b/171272809): Add back when controllers move to static type
427         return 0;
428     }
429 
430     @Override
getLogTag()431     protected String getLogTag() {
432         return TAG;
433     }
434 
435     @Override
getDefaultShortcutTypes()436     protected int getDefaultShortcutTypes() {
437         AccessibilityServiceInfo info = getAccessibilityServiceInfo();
438         boolean isAccessibilityTool = info != null && info.isAccessibilityTool();
439         return !isAccessibilityTool || getTileComponentName() == null
440                 ? super.getDefaultShortcutTypes()
441                 : ShortcutConstants.UserShortcutType.QUICK_SETTINGS;
442     }
443 
onAllowButtonFromEnableToggleClicked()444     private void onAllowButtonFromEnableToggleClicked() {
445         handleConfirmServiceEnabled(/* confirmed= */ true);
446         if (serviceSupportsAccessibilityButton()) {
447             mIsDialogShown.set(false);
448             showPopupDialog(DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL);
449         }
450         if (mWarningDialog != null) {
451             mWarningDialog.dismiss();
452         }
453     }
454 
onDenyButtonFromEnableToggleClicked()455     private void onDenyButtonFromEnableToggleClicked() {
456         handleConfirmServiceEnabled(/* confirmed= */ false);
457         mWarningDialog.dismiss();
458     }
459 
460     @SuppressLint("MissingPermission")
onAllowButtonFromShortcutToggleClicked()461     void onAllowButtonFromShortcutToggleClicked() {
462         mShortcutPreference.setChecked(true);
463 
464         final int shortcutTypes = getUserPreferredShortcutTypes();
465         getPrefContext().getSystemService(AccessibilityManager.class)
466                 .enableShortcutsForTargets(true, shortcutTypes,
467                         Set.of(mComponentName.flattenToString()), getPrefContext().getUserId());
468 
469         mIsDialogShown.set(false);
470         showPopupDialog(DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL);
471 
472         if (mWarningDialog != null) {
473             mWarningDialog.dismiss();
474         }
475 
476         mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext()));
477     }
478 
onDenyButtonFromShortcutToggleClicked()479     private void onDenyButtonFromShortcutToggleClicked() {
480         mShortcutPreference.setChecked(false);
481 
482         mWarningDialog.dismiss();
483     }
484 
onAllowButtonFromShortcutClicked()485     private void onAllowButtonFromShortcutClicked() {
486         mIsDialogShown.set(false);
487         EditShortcutsPreferenceFragment.showEditShortcutScreen(
488                 getContext(),
489                 getMetricsCategory(),
490                 getShortcutTitle(),
491                 mComponentName,
492                 getIntent()
493         );
494 
495         if (mWarningDialog != null) {
496             mWarningDialog.dismiss();
497         }
498     }
499 
onDenyButtonFromShortcutClicked()500     private void onDenyButtonFromShortcutClicked() {
501         mWarningDialog.dismiss();
502     }
503 
onPreferenceClick(boolean isChecked)504     private boolean onPreferenceClick(boolean isChecked) {
505         if (isChecked) {
506             mToggleServiceSwitchPreference.setChecked(false);
507             getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED,
508                     /* disableService */ false);
509             final boolean isWarningRequired =
510                     getPrefContext().getSystemService(AccessibilityManager.class)
511                             .isAccessibilityServiceWarningRequired(getAccessibilityServiceInfo());
512             if (isWarningRequired) {
513                 showPopupDialog(DialogEnums.ENABLE_WARNING_FROM_TOGGLE);
514             } else {
515                 onAllowButtonFromEnableToggleClicked();
516             }
517         } else {
518             mToggleServiceSwitchPreference.setChecked(true);
519             getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED,
520                     /* enableService */ true);
521             showDialog(DialogEnums.DISABLE_WARNING_FROM_TOGGLE);
522         }
523         return true;
524     }
525 
showPopupDialog(int dialogId)526     private void showPopupDialog(int dialogId) {
527         if (mIsDialogShown.compareAndSet(/* expect= */ false, /* update= */ true)) {
528             showDialog(dialogId);
529             setOnDismissListener(
530                     dialog -> mIsDialogShown.compareAndSet(/* expect= */ true, /* update= */
531                             false));
532         }
533     }
534 
logDisabledState(String packageName)535     private void logDisabledState(String packageName) {
536         if (mStartTimeMillsForLogging > 0 && !mDisabledStateLogged) {
537             AccessibilityStatsLogUtils.logDisableNonA11yCategoryService(
538                     packageName,
539                     SystemClock.elapsedRealtime() - mStartTimeMillsForLogging);
540             mDisabledStateLogged = true;
541         }
542     }
543 }
544