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