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