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