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 81 protected int getMetricsCategory() { 82 return MetricsEvent.ACCESSIBILITY_SERVICE; 83 } 84 85 @Override 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 101 public void onCreate(Bundle savedInstanceState) { 102 super.onCreate(savedInstanceState); 103 mLockPatternUtils = new LockPatternUtils(getActivity()); 104 } 105 106 @Override 107 public void onResume() { 108 mSettingsContentObserver.register(getContentResolver()); 109 updateSwitchBarToggleSwitch(); 110 super.onResume(); 111 } 112 113 @Override 114 public void onPause() { 115 mSettingsContentObserver.unregister(getContentResolver()); 116 super.onPause(); 117 } 118 119 @Override 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. 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 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 205 private void updateSwitchBarToggleSwitch() { 206 final boolean checked = AccessibilityUtils.getEnabledServicesFromSettings(getActivity()) 207 .contains(mComponentName); 208 mSwitchBar.setCheckedInternal(checked); 209 } 210 211 /** 212 * Return whether the device is encrypted with legacy full disk encryption. Newer devices 213 * should be using File Based Encryption. 214 * 215 * @return true if device is encrypted 216 */ 217 private boolean isFullDiskEncrypted() { 218 return StorageManager.isNonDefaultBlockEncrypted(); 219 } 220 221 private View createEnableDialogContentView(AccessibilityServiceInfo info) { 222 LayoutInflater inflater = (LayoutInflater) getSystemService( 223 Context.LAYOUT_INFLATER_SERVICE); 224 225 View content = inflater.inflate(R.layout.enable_accessibility_service_dialog_content, 226 null); 227 228 TextView encryptionWarningView = (TextView) content.findViewById( 229 R.id.encryption_warning); 230 if (isFullDiskEncrypted()) { 231 String text = getString(R.string.enable_service_encryption_warning, 232 info.getResolveInfo().loadLabel(getPackageManager())); 233 encryptionWarningView.setText(text); 234 encryptionWarningView.setVisibility(View.VISIBLE); 235 } else { 236 encryptionWarningView.setVisibility(View.GONE); 237 } 238 239 TextView capabilitiesHeaderView = (TextView) content.findViewById( 240 R.id.capabilities_header); 241 capabilitiesHeaderView.setText(getString(R.string.capabilities_list_title, 242 info.getResolveInfo().loadLabel(getPackageManager()))); 243 244 LinearLayout capabilitiesView = (LinearLayout) content.findViewById(R.id.capabilities); 245 246 // This capability is implicit for all services. 247 View capabilityView = inflater.inflate( 248 com.android.internal.R.layout.app_permission_item_old, null); 249 250 ImageView imageView = (ImageView) capabilityView.findViewById( 251 com.android.internal.R.id.perm_icon); 252 imageView.setImageDrawable(getActivity().getDrawable( 253 com.android.internal.R.drawable.ic_text_dot)); 254 255 TextView labelView = (TextView) capabilityView.findViewById( 256 com.android.internal.R.id.permission_group); 257 labelView.setText(getString(R.string.capability_title_receiveAccessibilityEvents)); 258 259 TextView descriptionView = (TextView) capabilityView.findViewById( 260 com.android.internal.R.id.permission_list); 261 descriptionView.setText(getString(R.string.capability_desc_receiveAccessibilityEvents)); 262 263 List<AccessibilityServiceInfo.CapabilityInfo> capabilities = 264 info.getCapabilityInfos(); 265 266 capabilitiesView.addView(capabilityView); 267 268 // Service specific capabilities. 269 final int capabilityCount = capabilities.size(); 270 for (int i = 0; i < capabilityCount; i++) { 271 AccessibilityServiceInfo.CapabilityInfo capability = capabilities.get(i); 272 273 capabilityView = inflater.inflate( 274 com.android.internal.R.layout.app_permission_item_old, null); 275 276 imageView = (ImageView) capabilityView.findViewById( 277 com.android.internal.R.id.perm_icon); 278 imageView.setImageDrawable(getActivity().getDrawable( 279 com.android.internal.R.drawable.ic_text_dot)); 280 281 labelView = (TextView) capabilityView.findViewById( 282 com.android.internal.R.id.permission_group); 283 labelView.setText(getString(capability.titleResId)); 284 285 descriptionView = (TextView) capabilityView.findViewById( 286 com.android.internal.R.id.permission_list); 287 descriptionView.setText(getString(capability.descResId)); 288 289 capabilitiesView.addView(capabilityView); 290 } 291 292 return content; 293 } 294 295 @Override 296 public void onActivityResult(int requestCode, int resultCode, Intent data) { 297 if (requestCode == ACTIVITY_REQUEST_CONFIRM_CREDENTIAL_FOR_WEAKER_ENCRYPTION) { 298 if (resultCode == Activity.RESULT_OK) { 299 handleConfirmServiceEnabled(true); 300 // The user confirmed that they accept weaker encryption when 301 // enabling the accessibility service, so change encryption. 302 // Since we came here asynchronously, check encryption again. 303 if (isFullDiskEncrypted()) { 304 mLockPatternUtils.clearEncryptionPassword(); 305 Settings.Global.putInt(getContentResolver(), 306 Settings.Global.REQUIRE_PASSWORD_TO_DECRYPT, 0); 307 } 308 } else { 309 handleConfirmServiceEnabled(false); 310 } 311 } 312 } 313 314 @Override 315 public void onClick(DialogInterface dialog, int which) { 316 final boolean checked; 317 switch (which) { 318 case DialogInterface.BUTTON_POSITIVE: 319 if (mShownDialogId == DIALOG_ID_ENABLE_WARNING) { 320 if (isFullDiskEncrypted()) { 321 String title = createConfirmCredentialReasonMessage(); 322 Intent intent = ConfirmDeviceCredentialActivity.createIntent(title, null); 323 startActivityForResult(intent, 324 ACTIVITY_REQUEST_CONFIRM_CREDENTIAL_FOR_WEAKER_ENCRYPTION); 325 } else { 326 handleConfirmServiceEnabled(true); 327 } 328 } else { 329 handleConfirmServiceEnabled(false); 330 } 331 break; 332 case DialogInterface.BUTTON_NEGATIVE: 333 checked = (mShownDialogId == DIALOG_ID_DISABLE_WARNING); 334 handleConfirmServiceEnabled(checked); 335 break; 336 default: 337 throw new IllegalArgumentException(); 338 } 339 } 340 341 private void handleConfirmServiceEnabled(boolean confirmed) { 342 mSwitchBar.setCheckedInternal(confirmed); 343 getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, confirmed); 344 onPreferenceToggled(mPreferenceKey, confirmed); 345 } 346 347 private String createConfirmCredentialReasonMessage() { 348 int resId = R.string.enable_service_password_reason; 349 switch (mLockPatternUtils.getKeyguardStoredPasswordQuality(UserHandle.myUserId())) { 350 case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING: { 351 resId = R.string.enable_service_pattern_reason; 352 } break; 353 case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC: 354 case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX: { 355 resId = R.string.enable_service_pin_reason; 356 } break; 357 } 358 return getString(resId, getAccessibilityServiceInfo().getResolveInfo() 359 .loadLabel(getPackageManager())); 360 } 361 362 @Override 363 protected void onInstallSwitchBarToggleSwitch() { 364 super.onInstallSwitchBarToggleSwitch(); 365 mToggleSwitch.setOnBeforeCheckedChangeListener(new OnBeforeCheckedChangeListener() { 366 @Override 367 public boolean onBeforeCheckedChanged(ToggleSwitch toggleSwitch, boolean checked) { 368 if (checked) { 369 mSwitchBar.setCheckedInternal(false); 370 getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, false); 371 showDialog(DIALOG_ID_ENABLE_WARNING); 372 } else { 373 mSwitchBar.setCheckedInternal(true); 374 getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, true); 375 showDialog(DIALOG_ID_DISABLE_WARNING); 376 } 377 return true; 378 } 379 }); 380 } 381 382 @Override 383 protected void onProcessArguments(Bundle arguments) { 384 super.onProcessArguments(arguments); 385 // Settings title and intent. 386 String settingsTitle = arguments.getString(AccessibilitySettings.EXTRA_SETTINGS_TITLE); 387 String settingsComponentName = arguments.getString( 388 AccessibilitySettings.EXTRA_SETTINGS_COMPONENT_NAME); 389 if (!TextUtils.isEmpty(settingsTitle) && !TextUtils.isEmpty(settingsComponentName)) { 390 Intent settingsIntent = new Intent(Intent.ACTION_MAIN).setComponent( 391 ComponentName.unflattenFromString(settingsComponentName.toString())); 392 if (!getPackageManager().queryIntentActivities(settingsIntent, 0).isEmpty()) { 393 mSettingsTitle = settingsTitle; 394 mSettingsIntent = settingsIntent; 395 setHasOptionsMenu(true); 396 } 397 } 398 399 mComponentName = arguments.getParcelable(AccessibilitySettings.EXTRA_COMPONENT_NAME); 400 } 401 } 402