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 android.accessibilityservice.AccessibilityServiceInfo;
20 import android.app.Activity;
21 import android.app.AlertDialog;
22 import android.app.Dialog;
23 import android.app.admin.DevicePolicyManager;
24 import android.content.ComponentName;
25 import android.content.Context;
26 import android.content.DialogInterface;
27 import android.content.Intent;
28 import android.content.pm.ResolveInfo;
29 import android.net.Uri;
30 import android.os.Bundle;
31 import android.os.Handler;
32 import android.os.UserHandle;
33 import android.os.storage.StorageManager;
34 import android.provider.Settings;
35 import android.text.TextUtils;
36 import android.view.LayoutInflater;
37 import android.view.MotionEvent;
38 import android.view.View;
39 import android.view.accessibility.AccessibilityManager;
40 import android.widget.ImageView;
41 import android.widget.LinearLayout;
42 import android.widget.TextView;
43 import android.widget.Toast;
44 
45 import com.android.internal.logging.MetricsProto.MetricsEvent;
46 import com.android.internal.widget.LockPatternUtils;
47 import com.android.settings.ConfirmDeviceCredentialActivity;
48 import com.android.settings.R;
49 import com.android.settings.widget.ToggleSwitch;
50 import com.android.settings.widget.ToggleSwitch.OnBeforeCheckedChangeListener;
51 import com.android.settingslib.accessibility.AccessibilityUtils;
52 
53 import java.util.List;
54 
55 public class ToggleAccessibilityServicePreferenceFragment
56         extends ToggleFeaturePreferenceFragment implements DialogInterface.OnClickListener {
57 
58     private static final int DIALOG_ID_ENABLE_WARNING = 1;
59     private static final int DIALOG_ID_DISABLE_WARNING = 2;
60 
61     public static final int ACTIVITY_REQUEST_CONFIRM_CREDENTIAL_FOR_WEAKER_ENCRYPTION = 1;
62 
63     private LockPatternUtils mLockPatternUtils;
64 
65     private final SettingsContentObserver mSettingsContentObserver =
66             new SettingsContentObserver(new Handler()) {
67             @Override
68                 public void onChange(boolean selfChange, Uri uri) {
69                     updateSwitchBarToggleSwitch();
70                 }
71             };
72 
73     private ComponentName mComponentName;
74 
75     private int mShownDialogId;
76 
77     @Override
getMetricsCategory()78     protected int getMetricsCategory() {
79         return MetricsEvent.ACCESSIBILITY_SERVICE;
80     }
81 
82     @Override
onCreate(Bundle savedInstanceState)83     public void onCreate(Bundle savedInstanceState) {
84         super.onCreate(savedInstanceState);
85         mLockPatternUtils = new LockPatternUtils(getActivity());
86     }
87 
88     @Override
onResume()89     public void onResume() {
90         mSettingsContentObserver.register(getContentResolver());
91         updateSwitchBarToggleSwitch();
92         super.onResume();
93     }
94 
95     @Override
onPause()96     public void onPause() {
97         mSettingsContentObserver.unregister(getContentResolver());
98         super.onPause();
99     }
100 
101     @Override
onPreferenceToggled(String preferenceKey, boolean enabled)102     public void onPreferenceToggled(String preferenceKey, boolean enabled) {
103         ComponentName toggledService = ComponentName.unflattenFromString(preferenceKey);
104         AccessibilityUtils.setAccessibilityServiceState(getActivity(), toggledService, enabled);
105     }
106 
107     // IMPORTANT: Refresh the info since there are dynamically changing
108     // capabilities. For
109     // example, before JellyBean MR2 the user was granting the explore by touch
110     // one.
getAccessibilityServiceInfo()111     private AccessibilityServiceInfo getAccessibilityServiceInfo() {
112         List<AccessibilityServiceInfo> serviceInfos = AccessibilityManager.getInstance(
113                 getActivity()).getInstalledAccessibilityServiceList();
114         final int serviceInfoCount = serviceInfos.size();
115         for (int i = 0; i < serviceInfoCount; i++) {
116             AccessibilityServiceInfo serviceInfo = serviceInfos.get(i);
117             ResolveInfo resolveInfo = serviceInfo.getResolveInfo();
118             if (mComponentName.getPackageName().equals(resolveInfo.serviceInfo.packageName)
119                     && mComponentName.getClassName().equals(resolveInfo.serviceInfo.name)) {
120                 return serviceInfo;
121             }
122         }
123         return null;
124     }
125 
126     @Override
onCreateDialog(int dialogId)127     public Dialog onCreateDialog(int dialogId) {
128         switch (dialogId) {
129             case DIALOG_ID_ENABLE_WARNING: {
130                 mShownDialogId = DIALOG_ID_ENABLE_WARNING;
131 
132                 final AccessibilityServiceInfo info = getAccessibilityServiceInfo();
133                 if (info == null) {
134                     return null;
135                 }
136 
137                 final AlertDialog ad = new AlertDialog.Builder(getActivity())
138                         .setTitle(getString(R.string.enable_service_title,
139                                 info.getResolveInfo().loadLabel(getPackageManager())))
140                         .setView(createEnableDialogContentView(info))
141                         .setCancelable(true)
142                         .setPositiveButton(android.R.string.ok, this)
143                         .setNegativeButton(android.R.string.cancel, this)
144                         .create();
145 
146                 final View.OnTouchListener filterTouchListener = new View.OnTouchListener() {
147                     @Override
148                     public boolean onTouch(View v, MotionEvent event) {
149                         // Filter obscured touches by consuming them.
150                         if ((event.getFlags() & MotionEvent.FLAG_WINDOW_IS_OBSCURED) != 0) {
151                             if (event.getAction() == MotionEvent.ACTION_UP) {
152                                 Toast.makeText(v.getContext(), R.string.touch_filtered_warning,
153                                         Toast.LENGTH_SHORT).show();
154                             }
155                             return true;
156                         }
157                         return false;
158                     }
159                 };
160 
161                 ad.create();
162                 ad.getButton(AlertDialog.BUTTON_POSITIVE).setOnTouchListener(filterTouchListener);
163                 return ad;
164             }
165             case DIALOG_ID_DISABLE_WARNING: {
166                 mShownDialogId = DIALOG_ID_DISABLE_WARNING;
167                 AccessibilityServiceInfo info = getAccessibilityServiceInfo();
168                 if (info == null) {
169                     return null;
170                 }
171                 return new AlertDialog.Builder(getActivity())
172                         .setTitle(getString(R.string.disable_service_title,
173                                 info.getResolveInfo().loadLabel(getPackageManager())))
174                         .setMessage(getString(R.string.disable_service_message,
175                                 info.getResolveInfo().loadLabel(getPackageManager())))
176                         .setCancelable(true)
177                         .setPositiveButton(android.R.string.ok, this)
178                         .setNegativeButton(android.R.string.cancel, this)
179                         .create();
180             }
181             default: {
182                 throw new IllegalArgumentException();
183             }
184         }
185     }
186 
updateSwitchBarToggleSwitch()187     private void updateSwitchBarToggleSwitch() {
188         final String settingValue = Settings.Secure.getString(getContentResolver(),
189                 Settings.Secure.ENABLED_ACCESSIBILITY_SERVICES);
190         final boolean checked = settingValue != null
191                 && settingValue.contains(mComponentName.flattenToString());
192         mSwitchBar.setCheckedInternal(checked);
193     }
194 
195     /**
196      * Return whether the device is encrypted with legacy full disk encryption. Newer devices
197      * should be using File Based Encryption.
198      *
199      * @return true if device is encrypted
200      */
isFullDiskEncrypted()201     private boolean isFullDiskEncrypted() {
202         return StorageManager.isNonDefaultBlockEncrypted();
203     }
204 
createEnableDialogContentView(AccessibilityServiceInfo info)205     private View createEnableDialogContentView(AccessibilityServiceInfo info) {
206         LayoutInflater inflater = (LayoutInflater) getSystemService(
207                 Context.LAYOUT_INFLATER_SERVICE);
208 
209         View content = inflater.inflate(R.layout.enable_accessibility_service_dialog_content,
210                 null);
211 
212         TextView encryptionWarningView = (TextView) content.findViewById(
213                 R.id.encryption_warning);
214         if (isFullDiskEncrypted()) {
215             String text = getString(R.string.enable_service_encryption_warning,
216                     info.getResolveInfo().loadLabel(getPackageManager()));
217             encryptionWarningView.setText(text);
218             encryptionWarningView.setVisibility(View.VISIBLE);
219         } else {
220             encryptionWarningView.setVisibility(View.GONE);
221         }
222 
223         TextView capabilitiesHeaderView = (TextView) content.findViewById(
224                 R.id.capabilities_header);
225         capabilitiesHeaderView.setText(getString(R.string.capabilities_list_title,
226                 info.getResolveInfo().loadLabel(getPackageManager())));
227 
228         LinearLayout capabilitiesView = (LinearLayout) content.findViewById(R.id.capabilities);
229 
230         // This capability is implicit for all services.
231         View capabilityView = inflater.inflate(
232                 com.android.internal.R.layout.app_permission_item_old, null);
233 
234         ImageView imageView = (ImageView) capabilityView.findViewById(
235                 com.android.internal.R.id.perm_icon);
236         imageView.setImageDrawable(getActivity().getDrawable(
237                 com.android.internal.R.drawable.ic_text_dot));
238 
239         TextView labelView = (TextView) capabilityView.findViewById(
240                 com.android.internal.R.id.permission_group);
241         labelView.setText(getString(R.string.capability_title_receiveAccessibilityEvents));
242 
243         TextView descriptionView = (TextView) capabilityView.findViewById(
244                 com.android.internal.R.id.permission_list);
245         descriptionView.setText(getString(R.string.capability_desc_receiveAccessibilityEvents));
246 
247         List<AccessibilityServiceInfo.CapabilityInfo> capabilities =
248                 info.getCapabilityInfos();
249 
250         capabilitiesView.addView(capabilityView);
251 
252         // Service specific capabilities.
253         final int capabilityCount = capabilities.size();
254         for (int i = 0; i < capabilityCount; i++) {
255             AccessibilityServiceInfo.CapabilityInfo capability = capabilities.get(i);
256 
257             capabilityView = inflater.inflate(
258                     com.android.internal.R.layout.app_permission_item_old, null);
259 
260             imageView = (ImageView) capabilityView.findViewById(
261                     com.android.internal.R.id.perm_icon);
262             imageView.setImageDrawable(getActivity().getDrawable(
263                     com.android.internal.R.drawable.ic_text_dot));
264 
265             labelView = (TextView) capabilityView.findViewById(
266                     com.android.internal.R.id.permission_group);
267             labelView.setText(getString(capability.titleResId));
268 
269             descriptionView = (TextView) capabilityView.findViewById(
270                     com.android.internal.R.id.permission_list);
271             descriptionView.setText(getString(capability.descResId));
272 
273             capabilitiesView.addView(capabilityView);
274         }
275 
276         return content;
277     }
278 
279     @Override
onActivityResult(int requestCode, int resultCode, Intent data)280     public void onActivityResult(int requestCode, int resultCode, Intent data) {
281         if (requestCode == ACTIVITY_REQUEST_CONFIRM_CREDENTIAL_FOR_WEAKER_ENCRYPTION) {
282             if (resultCode == Activity.RESULT_OK) {
283                 handleConfirmServiceEnabled(true);
284                 // The user confirmed that they accept weaker encryption when
285                 // enabling the accessibility service, so change encryption.
286                 // Since we came here asynchronously, check encryption again.
287                 if (isFullDiskEncrypted()) {
288                     mLockPatternUtils.clearEncryptionPassword();
289                     Settings.Global.putInt(getContentResolver(),
290                             Settings.Global.REQUIRE_PASSWORD_TO_DECRYPT, 0);
291                 }
292             } else {
293                 handleConfirmServiceEnabled(false);
294             }
295         }
296     }
297 
298     @Override
onClick(DialogInterface dialog, int which)299     public void onClick(DialogInterface dialog, int which) {
300         final boolean checked;
301         switch (which) {
302             case DialogInterface.BUTTON_POSITIVE:
303                 if (mShownDialogId == DIALOG_ID_ENABLE_WARNING) {
304                     if (isFullDiskEncrypted()) {
305                         String title = createConfirmCredentialReasonMessage();
306                         Intent intent = ConfirmDeviceCredentialActivity.createIntent(title, null);
307                         startActivityForResult(intent,
308                                 ACTIVITY_REQUEST_CONFIRM_CREDENTIAL_FOR_WEAKER_ENCRYPTION);
309                     } else {
310                         handleConfirmServiceEnabled(true);
311                     }
312                 } else {
313                     handleConfirmServiceEnabled(false);
314                 }
315                 break;
316             case DialogInterface.BUTTON_NEGATIVE:
317                 checked = (mShownDialogId == DIALOG_ID_DISABLE_WARNING);
318                 handleConfirmServiceEnabled(checked);
319                 break;
320             default:
321                 throw new IllegalArgumentException();
322         }
323     }
324 
handleConfirmServiceEnabled(boolean confirmed)325     private void handleConfirmServiceEnabled(boolean confirmed) {
326         mSwitchBar.setCheckedInternal(confirmed);
327         getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, confirmed);
328         onPreferenceToggled(mPreferenceKey, confirmed);
329     }
330 
createConfirmCredentialReasonMessage()331     private String createConfirmCredentialReasonMessage() {
332         int resId = R.string.enable_service_password_reason;
333         switch (mLockPatternUtils.getKeyguardStoredPasswordQuality(UserHandle.myUserId())) {
334             case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING: {
335                 resId = R.string.enable_service_pattern_reason;
336             } break;
337             case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC:
338             case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX: {
339                 resId = R.string.enable_service_pin_reason;
340             } break;
341         }
342         return getString(resId, getAccessibilityServiceInfo().getResolveInfo()
343                 .loadLabel(getPackageManager()));
344     }
345 
346     @Override
onInstallSwitchBarToggleSwitch()347     protected void onInstallSwitchBarToggleSwitch() {
348         super.onInstallSwitchBarToggleSwitch();
349         mToggleSwitch.setOnBeforeCheckedChangeListener(new OnBeforeCheckedChangeListener() {
350                 @Override
351             public boolean onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked) {
352                 if (checked) {
353                     mSwitchBar.setCheckedInternal(false);
354                     getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, false);
355                     showDialog(DIALOG_ID_ENABLE_WARNING);
356                 } else {
357                     mSwitchBar.setCheckedInternal(true);
358                     getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, true);
359                     showDialog(DIALOG_ID_DISABLE_WARNING);
360                 }
361                 return true;
362             }
363         });
364     }
365 
366     @Override
onProcessArguments(Bundle arguments)367     protected void onProcessArguments(Bundle arguments) {
368         super.onProcessArguments(arguments);
369         // Settings title and intent.
370         String settingsTitle = arguments.getString(AccessibilitySettings.EXTRA_SETTINGS_TITLE);
371         String settingsComponentName = arguments.getString(
372                 AccessibilitySettings.EXTRA_SETTINGS_COMPONENT_NAME);
373         if (!TextUtils.isEmpty(settingsTitle) && !TextUtils.isEmpty(settingsComponentName)) {
374             Intent settingsIntent = new Intent(Intent.ACTION_MAIN).setComponent(
375                     ComponentName.unflattenFromString(settingsComponentName.toString()));
376             if (!getPackageManager().queryIntentActivities(settingsIntent, 0).isEmpty()) {
377                 mSettingsTitle = settingsTitle;
378                 mSettingsIntent = settingsIntent;
379                 setHasOptionsMenu(true);
380             }
381         }
382 
383         mComponentName = arguments.getParcelable(AccessibilitySettings.EXTRA_COMPONENT_NAME);
384     }
385 }
386