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.AccessibilityStatsLogUtils.logAccessibilityServiceEnabled;
20 
21 import android.accessibilityservice.AccessibilityServiceInfo;
22 import android.app.Activity;
23 import android.app.Dialog;
24 import android.app.admin.DevicePolicyManager;
25 import android.app.settings.SettingsEnums;
26 import android.content.ComponentName;
27 import android.content.ContentResolver;
28 import android.content.DialogInterface;
29 import android.content.Intent;
30 import android.content.pm.ResolveInfo;
31 import android.content.pm.ServiceInfo;
32 import android.net.Uri;
33 import android.os.Bundle;
34 import android.os.Handler;
35 import android.os.UserHandle;
36 import android.os.storage.StorageManager;
37 import android.provider.Settings;
38 import android.text.TextUtils;
39 import android.view.Menu;
40 import android.view.MenuInflater;
41 import android.view.View;
42 import android.view.accessibility.AccessibilityManager;
43 
44 import androidx.preference.Preference;
45 import androidx.preference.SwitchPreference;
46 
47 import com.android.internal.widget.LockPatternUtils;
48 import com.android.settings.R;
49 import com.android.settings.accessibility.AccessibilityUtil.UserShortcutType;
50 import com.android.settings.password.ConfirmDeviceCredentialActivity;
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     public static final int ACTIVITY_REQUEST_CONFIRM_CREDENTIAL_FOR_WEAKER_ENCRYPTION = 1;
61     private LockPatternUtils mLockPatternUtils;
62     private AtomicBoolean mIsDialogShown = new AtomicBoolean(/* initialValue= */ false);
63 
64     private static final String EMPTY_STRING = "";
65 
66     private final SettingsContentObserver mSettingsContentObserver =
67             new SettingsContentObserver(new Handler()) {
68                 @Override
69                 public void onChange(boolean selfChange, Uri uri) {
70                     updateSwitchBarToggleSwitch();
71                 }
72             };
73 
74     private Dialog mDialog;
75 
76     @Override
getMetricsCategory()77     public int getMetricsCategory() {
78         return SettingsEnums.ACCESSIBILITY_SERVICE;
79     }
80 
81     @Override
onCreateOptionsMenu(Menu menu, MenuInflater infalter)82     public void onCreateOptionsMenu(Menu menu, MenuInflater infalter) {
83         // Do not call super. We don't want to see the "Help & feedback" option on this page so as
84         // not to confuse users who think they might be able to send feedback about a specific
85         // accessibility service from this page.
86     }
87 
88     @Override
onCreate(Bundle savedInstanceState)89     public void onCreate(Bundle savedInstanceState) {
90         super.onCreate(savedInstanceState);
91         mLockPatternUtils = new LockPatternUtils(getPrefContext());
92     }
93 
94     @Override
onResume()95     public void onResume() {
96         super.onResume();
97         updateSwitchBarToggleSwitch();
98         mSettingsContentObserver.register(getContentResolver());
99     }
100 
101     @Override
onPreferenceToggled(String preferenceKey, boolean enabled)102     public void onPreferenceToggled(String preferenceKey, boolean enabled) {
103         ComponentName toggledService = ComponentName.unflattenFromString(preferenceKey);
104         logAccessibilityServiceEnabled(toggledService, enabled);
105         AccessibilityUtils.setAccessibilityServiceState(getPrefContext(), toggledService, enabled);
106     }
107 
108     // IMPORTANT: Refresh the info since there are dynamically changing
109     // capabilities. For
110     // example, before JellyBean MR2 the user was granting the explore by touch
111     // one.
getAccessibilityServiceInfo()112     AccessibilityServiceInfo getAccessibilityServiceInfo() {
113         final List<AccessibilityServiceInfo> infos = AccessibilityManager.getInstance(
114                 getPrefContext()).getInstalledAccessibilityServiceList();
115 
116         for (int i = 0, count = infos.size(); i < count; i++) {
117             AccessibilityServiceInfo serviceInfo = infos.get(i);
118             ResolveInfo resolveInfo = serviceInfo.getResolveInfo();
119             if (mComponentName.getPackageName().equals(resolveInfo.serviceInfo.packageName)
120                     && mComponentName.getClassName().equals(resolveInfo.serviceInfo.name)) {
121                 return serviceInfo;
122             }
123         }
124         return null;
125     }
126 
127     @Override
onCreateDialog(int dialogId)128     public Dialog onCreateDialog(int dialogId) {
129         switch (dialogId) {
130             case DialogEnums.ENABLE_WARNING_FROM_TOGGLE: {
131                 final AccessibilityServiceInfo info = getAccessibilityServiceInfo();
132                 if (info == null) {
133                     return null;
134                 }
135                 mDialog = AccessibilityServiceWarning
136                         .createCapabilitiesDialog(getPrefContext(), info,
137                                 this::onDialogButtonFromEnableToggleClicked);
138                 break;
139             }
140             case DialogEnums.ENABLE_WARNING_FROM_SHORTCUT_TOGGLE: {
141                 final AccessibilityServiceInfo info = getAccessibilityServiceInfo();
142                 if (info == null) {
143                     return null;
144                 }
145                 mDialog = AccessibilityServiceWarning
146                         .createCapabilitiesDialog(getPrefContext(), info,
147                                 this::onDialogButtonFromShortcutToggleClicked);
148                 break;
149             }
150             case DialogEnums.ENABLE_WARNING_FROM_SHORTCUT: {
151                 final AccessibilityServiceInfo info = getAccessibilityServiceInfo();
152                 if (info == null) {
153                     return null;
154                 }
155                 mDialog = AccessibilityServiceWarning
156                         .createCapabilitiesDialog(getPrefContext(), info,
157                                 this::onDialogButtonFromShortcutClicked);
158                 break;
159             }
160             case DialogEnums.DISABLE_WARNING_FROM_TOGGLE: {
161                 final AccessibilityServiceInfo info = getAccessibilityServiceInfo();
162                 if (info == null) {
163                     return null;
164                 }
165                 mDialog = AccessibilityServiceWarning
166                         .createDisableDialog(getPrefContext(), info,
167                                 this::onDialogButtonFromDisableToggleClicked);
168                 break;
169             }
170             default: {
171                 mDialog = super.onCreateDialog(dialogId);
172             }
173         }
174         return mDialog;
175     }
176 
177     @Override
getDialogMetricsCategory(int dialogId)178     public int getDialogMetricsCategory(int dialogId) {
179         switch (dialogId) {
180             case DialogEnums.ENABLE_WARNING_FROM_TOGGLE:
181             case DialogEnums.ENABLE_WARNING_FROM_SHORTCUT:
182             case DialogEnums.ENABLE_WARNING_FROM_SHORTCUT_TOGGLE:
183                 return SettingsEnums.DIALOG_ACCESSIBILITY_SERVICE_ENABLE;
184             case DialogEnums.DISABLE_WARNING_FROM_TOGGLE:
185                 return SettingsEnums.DIALOG_ACCESSIBILITY_SERVICE_DISABLE;
186             case DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL:
187                 return SettingsEnums.DIALOG_ACCESSIBILITY_TUTORIAL;
188             default:
189                 return super.getDialogMetricsCategory(dialogId);
190         }
191     }
192 
193     @Override
getUserShortcutTypes()194     int getUserShortcutTypes() {
195         return AccessibilityUtil.getUserShortcutTypesFromSettings(getPrefContext(),
196                 mComponentName);
197     }
198 
199     @Override
updateToggleServiceTitle(SwitchPreference switchPreference)200     protected void updateToggleServiceTitle(SwitchPreference switchPreference) {
201         final AccessibilityServiceInfo info = getAccessibilityServiceInfo();
202         final String switchBarText = (info == null) ? "" :
203                 getString(R.string.accessibility_service_master_switch_title,
204                         info.getResolveInfo().loadLabel(getPackageManager()));
205         switchPreference.setTitle(switchBarText);
206     }
207 
updateSwitchBarToggleSwitch()208     private void updateSwitchBarToggleSwitch() {
209         final boolean checked = AccessibilityUtils.getEnabledServicesFromSettings(getPrefContext())
210                 .contains(mComponentName);
211         if (mToggleServiceDividerSwitchPreference.isChecked() == checked) {
212             return;
213         }
214         mToggleServiceDividerSwitchPreference.setChecked(checked);
215     }
216 
217     /**
218      * Return whether the device is encrypted with legacy full disk encryption. Newer devices
219      * should be using File Based Encryption.
220      *
221      * @return true if device is encrypted
222      */
isFullDiskEncrypted()223     private boolean isFullDiskEncrypted() {
224         return StorageManager.isNonDefaultBlockEncrypted();
225     }
226 
227     @Override
onActivityResult(int requestCode, int resultCode, Intent data)228     public void onActivityResult(int requestCode, int resultCode, Intent data) {
229         if (requestCode == ACTIVITY_REQUEST_CONFIRM_CREDENTIAL_FOR_WEAKER_ENCRYPTION) {
230             if (resultCode == Activity.RESULT_OK) {
231                 handleConfirmServiceEnabled(/* confirmed= */ true);
232                 // The user confirmed that they accept weaker encryption when
233                 // enabling the accessibility service, so change encryption.
234                 // Since we came here asynchronously, check encryption again.
235                 if (isFullDiskEncrypted()) {
236                     mLockPatternUtils.clearEncryptionPassword();
237                     Settings.Global.putInt(getContentResolver(),
238                             Settings.Global.REQUIRE_PASSWORD_TO_DECRYPT, 0);
239                 }
240             } else {
241                 handleConfirmServiceEnabled(/* confirmed= */ false);
242             }
243         }
244     }
245 
isServiceSupportAccessibilityButton()246     private boolean isServiceSupportAccessibilityButton() {
247         final AccessibilityManager ams = getPrefContext().getSystemService(
248                 AccessibilityManager.class);
249         final List<AccessibilityServiceInfo> services = ams.getInstalledAccessibilityServiceList();
250 
251         for (AccessibilityServiceInfo info : services) {
252             if ((info.flags & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0) {
253                 ServiceInfo serviceInfo = info.getResolveInfo().serviceInfo;
254                 if (serviceInfo != null && TextUtils.equals(serviceInfo.name,
255                         getAccessibilityServiceInfo().getResolveInfo().serviceInfo.name)) {
256                     return true;
257                 }
258             }
259         }
260 
261         return false;
262     }
263 
handleConfirmServiceEnabled(boolean confirmed)264     private void handleConfirmServiceEnabled(boolean confirmed) {
265         mToggleServiceDividerSwitchPreference.setChecked(confirmed);
266         getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, confirmed);
267         onPreferenceToggled(mPreferenceKey, confirmed);
268     }
269 
createConfirmCredentialReasonMessage()270     private String createConfirmCredentialReasonMessage() {
271         int resId = R.string.enable_service_password_reason;
272         switch (mLockPatternUtils.getKeyguardStoredPasswordQuality(UserHandle.myUserId())) {
273             case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING: {
274                 resId = R.string.enable_service_pattern_reason;
275             }
276             break;
277             case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC:
278             case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX: {
279                 resId = R.string.enable_service_pin_reason;
280             }
281             break;
282         }
283         return getString(resId, getAccessibilityServiceInfo().getResolveInfo()
284                 .loadLabel(getPackageManager()));
285     }
286 
287     @Override
onInstallSwitchPreferenceToggleSwitch()288     protected void onInstallSwitchPreferenceToggleSwitch() {
289         super.onInstallSwitchPreferenceToggleSwitch();
290         mToggleServiceDividerSwitchPreference.setOnPreferenceClickListener(this::onPreferenceClick);
291     }
292 
293     @Override
onToggleClicked(ShortcutPreference preference)294     public void onToggleClicked(ShortcutPreference preference) {
295         final int shortcutTypes = getUserShortcutTypes(getPrefContext(), UserShortcutType.SOFTWARE);
296         if (preference.isChecked()) {
297             if (!mToggleServiceDividerSwitchPreference.isChecked()) {
298                 preference.setChecked(false);
299                 showPopupDialog(DialogEnums.ENABLE_WARNING_FROM_SHORTCUT_TOGGLE);
300             } else {
301                 AccessibilityUtil.optInAllValuesToSettings(getPrefContext(), shortcutTypes,
302                         mComponentName);
303                 showPopupDialog(DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL);
304             }
305         } else {
306             AccessibilityUtil.optOutAllValuesFromSettings(getPrefContext(), shortcutTypes,
307                     mComponentName);
308         }
309         mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext()));
310     }
311 
312     @Override
onSettingsClicked(ShortcutPreference preference)313     public void onSettingsClicked(ShortcutPreference preference) {
314         super.onSettingsClicked(preference);
315         final boolean isServiceOnOrShortcutAdded = mShortcutPreference.isChecked()
316                 || mToggleServiceDividerSwitchPreference.isChecked();
317         showPopupDialog(isServiceOnOrShortcutAdded ? DialogEnums.EDIT_SHORTCUT
318                 : DialogEnums.ENABLE_WARNING_FROM_SHORTCUT);
319     }
320 
321     @Override
onProcessArguments(Bundle arguments)322     protected void onProcessArguments(Bundle arguments) {
323         super.onProcessArguments(arguments);
324         // Settings title and intent.
325         String settingsTitle = arguments.getString(AccessibilitySettings.EXTRA_SETTINGS_TITLE);
326         String settingsComponentName = arguments.getString(
327                 AccessibilitySettings.EXTRA_SETTINGS_COMPONENT_NAME);
328         if (!TextUtils.isEmpty(settingsTitle) && !TextUtils.isEmpty(settingsComponentName)) {
329             Intent settingsIntent = new Intent(Intent.ACTION_MAIN).setComponent(
330                     ComponentName.unflattenFromString(settingsComponentName.toString()));
331             if (!getPackageManager().queryIntentActivities(settingsIntent, 0).isEmpty()) {
332                 mSettingsTitle = settingsTitle;
333                 mSettingsIntent = settingsIntent;
334                 setHasOptionsMenu(true);
335             }
336         }
337 
338         mComponentName = arguments.getParcelable(AccessibilitySettings.EXTRA_COMPONENT_NAME);
339 
340         // Settings animated image.
341         final int animatedImageRes = arguments.getInt(
342                 AccessibilitySettings.EXTRA_ANIMATED_IMAGE_RES);
343         mImageUri = new Uri.Builder().scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
344                 .authority(mComponentName.getPackageName())
345                 .appendPath(String.valueOf(animatedImageRes))
346                 .build();
347 
348         // Get Accessibility service name.
349         mPackageName = getAccessibilityServiceInfo().getResolveInfo().loadLabel(
350                 getPackageManager());
351     }
352 
onDialogButtonFromDisableToggleClicked(DialogInterface dialog, int which)353     private void onDialogButtonFromDisableToggleClicked(DialogInterface dialog, int which) {
354         switch (which) {
355             case DialogInterface.BUTTON_POSITIVE:
356                 handleConfirmServiceEnabled(/* confirmed= */ false);
357                 break;
358             case DialogInterface.BUTTON_NEGATIVE:
359                 handleConfirmServiceEnabled(/* confirmed= */ true);
360                 break;
361             default:
362                 throw new IllegalArgumentException("Unexpected button identifier");
363         }
364     }
365 
onDialogButtonFromEnableToggleClicked(View view)366     private void onDialogButtonFromEnableToggleClicked(View view) {
367         final int viewId = view.getId();
368         if (viewId == R.id.permission_enable_allow_button) {
369             onAllowButtonFromEnableToggleClicked();
370         } else if (viewId == R.id.permission_enable_deny_button) {
371             onDenyButtonFromEnableToggleClicked();
372         } else {
373             throw new IllegalArgumentException("Unexpected view id");
374         }
375     }
376 
onAllowButtonFromEnableToggleClicked()377     private void onAllowButtonFromEnableToggleClicked() {
378         if (isFullDiskEncrypted()) {
379             final String title = createConfirmCredentialReasonMessage();
380             final Intent intent = ConfirmDeviceCredentialActivity.createIntent(title, /* details= */
381                     null);
382             startActivityForResult(intent,
383                     ACTIVITY_REQUEST_CONFIRM_CREDENTIAL_FOR_WEAKER_ENCRYPTION);
384         } else {
385             handleConfirmServiceEnabled(/* confirmed= */ true);
386             if (isServiceSupportAccessibilityButton()) {
387                 mIsDialogShown.set(false);
388                 showPopupDialog(DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL);
389             }
390         }
391 
392         mDialog.dismiss();
393     }
394 
onDenyButtonFromEnableToggleClicked()395     private void onDenyButtonFromEnableToggleClicked() {
396         handleConfirmServiceEnabled(/* confirmed= */ false);
397         mDialog.dismiss();
398     }
399 
onDialogButtonFromShortcutToggleClicked(View view)400     void onDialogButtonFromShortcutToggleClicked(View view) {
401         final int viewId = view.getId();
402         if (viewId == R.id.permission_enable_allow_button) {
403             onAllowButtonFromShortcutToggleClicked();
404         } else if (viewId == R.id.permission_enable_deny_button) {
405             onDenyButtonFromShortcutToggleClicked();
406         } else {
407             throw new IllegalArgumentException("Unexpected view id");
408         }
409     }
410 
onAllowButtonFromShortcutToggleClicked()411     private void onAllowButtonFromShortcutToggleClicked() {
412         mShortcutPreference.setChecked(true);
413 
414         final int shortcutTypes = getUserShortcutTypes(getPrefContext(), UserShortcutType.SOFTWARE);
415         AccessibilityUtil.optInAllValuesToSettings(getPrefContext(), shortcutTypes, mComponentName);
416 
417         mIsDialogShown.set(false);
418         showPopupDialog(DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL);
419 
420         mDialog.dismiss();
421 
422         mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext()));
423     }
424 
onDenyButtonFromShortcutToggleClicked()425     private void onDenyButtonFromShortcutToggleClicked() {
426         mShortcutPreference.setChecked(false);
427 
428         mDialog.dismiss();
429     }
430 
onDialogButtonFromShortcutClicked(View view)431     void onDialogButtonFromShortcutClicked(View view) {
432         final int viewId = view.getId();
433         if (viewId == R.id.permission_enable_allow_button) {
434             onAllowButtonFromShortcutClicked();
435         } else if (viewId == R.id.permission_enable_deny_button) {
436             onDenyButtonFromShortcutClicked();
437         } else {
438             throw new IllegalArgumentException("Unexpected view id");
439         }
440     }
441 
onAllowButtonFromShortcutClicked()442     private void onAllowButtonFromShortcutClicked() {
443         mIsDialogShown.set(false);
444         showPopupDialog(DialogEnums.EDIT_SHORTCUT);
445 
446         mDialog.dismiss();
447     }
448 
onDenyButtonFromShortcutClicked()449     private void onDenyButtonFromShortcutClicked() {
450         mDialog.dismiss();
451     }
452 
onPreferenceClick(Preference preference)453     private boolean onPreferenceClick(Preference preference) {
454         boolean checked = ((DividerSwitchPreference) preference).isChecked();
455         if (checked) {
456             mToggleServiceDividerSwitchPreference.setChecked(false);
457             getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED,
458                     /* disableService */ false);
459             if (!mShortcutPreference.isChecked()) {
460                 showPopupDialog(DialogEnums.ENABLE_WARNING_FROM_TOGGLE);
461             } else {
462                 handleConfirmServiceEnabled(/* confirmed= */ true);
463                 if (isServiceSupportAccessibilityButton()) {
464                     showPopupDialog(DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL);
465                 }
466             }
467         } else {
468             mToggleServiceDividerSwitchPreference.setChecked(true);
469             getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED,
470                     /* enableService */ true);
471             showDialog(DialogEnums.DISABLE_WARNING_FROM_TOGGLE);
472         }
473         return true;
474     }
475 
showPopupDialog(int dialogId)476     private void showPopupDialog(int dialogId) {
477         if (mIsDialogShown.compareAndSet(/* expect= */ false, /* update= */ true)) {
478             showDialog(dialogId);
479             setOnDismissListener(
480                     dialog -> mIsDialogShown.compareAndSet(/* expect= */ true, /* update= */
481                             false));
482         }
483     }
484 }
485