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