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