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 static com.android.settings.accessibility.AccessibilityStatsLogUtils.logAccessibilityServiceEnabled; 20 21 import android.accessibilityservice.AccessibilityServiceInfo; 22 import android.app.Activity; 23 import android.app.Dialog; 24 import android.app.admin.DevicePolicyManager; 25 import android.app.settings.SettingsEnums; 26 import android.content.ComponentName; 27 import android.content.ContentResolver; 28 import android.content.DialogInterface; 29 import android.content.Intent; 30 import android.content.pm.ResolveInfo; 31 import android.content.pm.ServiceInfo; 32 import android.net.Uri; 33 import android.os.Bundle; 34 import android.os.Handler; 35 import android.os.UserHandle; 36 import android.os.storage.StorageManager; 37 import android.provider.Settings; 38 import android.text.TextUtils; 39 import android.view.Menu; 40 import android.view.MenuInflater; 41 import android.view.View; 42 import android.view.accessibility.AccessibilityManager; 43 44 import androidx.preference.Preference; 45 import androidx.preference.SwitchPreference; 46 47 import com.android.internal.widget.LockPatternUtils; 48 import com.android.settings.R; 49 import com.android.settings.accessibility.AccessibilityUtil.UserShortcutType; 50 import com.android.settings.password.ConfirmDeviceCredentialActivity; 51 import com.android.settingslib.accessibility.AccessibilityUtils; 52 53 import java.util.List; 54 import java.util.concurrent.atomic.AtomicBoolean; 55 56 /** Fragment for providing toggle bar and basic accessibility service setup. */ 57 public class ToggleAccessibilityServicePreferenceFragment extends 58 ToggleFeaturePreferenceFragment { 59 60 public static final int ACTIVITY_REQUEST_CONFIRM_CREDENTIAL_FOR_WEAKER_ENCRYPTION = 1; 61 private LockPatternUtils mLockPatternUtils; 62 private AtomicBoolean mIsDialogShown = new AtomicBoolean(/* initialValue= */ false); 63 64 private static final String EMPTY_STRING = ""; 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 Dialog mDialog; 75 76 @Override getMetricsCategory()77 public int getMetricsCategory() { 78 return SettingsEnums.ACCESSIBILITY_SERVICE; 79 } 80 81 @Override onCreateOptionsMenu(Menu menu, MenuInflater infalter)82 public void onCreateOptionsMenu(Menu menu, MenuInflater infalter) { 83 // Do not call super. We don't want to see the "Help & feedback" option on this page so as 84 // not to confuse users who think they might be able to send feedback about a specific 85 // accessibility service from this page. 86 } 87 88 @Override onCreate(Bundle savedInstanceState)89 public void onCreate(Bundle savedInstanceState) { 90 super.onCreate(savedInstanceState); 91 mLockPatternUtils = new LockPatternUtils(getPrefContext()); 92 } 93 94 @Override onResume()95 public void onResume() { 96 super.onResume(); 97 updateSwitchBarToggleSwitch(); 98 mSettingsContentObserver.register(getContentResolver()); 99 } 100 101 @Override onPreferenceToggled(String preferenceKey, boolean enabled)102 public void onPreferenceToggled(String preferenceKey, boolean enabled) { 103 ComponentName toggledService = ComponentName.unflattenFromString(preferenceKey); 104 logAccessibilityServiceEnabled(toggledService, enabled); 105 AccessibilityUtils.setAccessibilityServiceState(getPrefContext(), toggledService, enabled); 106 } 107 108 // IMPORTANT: Refresh the info since there are dynamically changing 109 // capabilities. For 110 // example, before JellyBean MR2 the user was granting the explore by touch 111 // one. getAccessibilityServiceInfo()112 AccessibilityServiceInfo getAccessibilityServiceInfo() { 113 final List<AccessibilityServiceInfo> infos = AccessibilityManager.getInstance( 114 getPrefContext()).getInstalledAccessibilityServiceList(); 115 116 for (int i = 0, count = infos.size(); i < count; i++) { 117 AccessibilityServiceInfo serviceInfo = infos.get(i); 118 ResolveInfo resolveInfo = serviceInfo.getResolveInfo(); 119 if (mComponentName.getPackageName().equals(resolveInfo.serviceInfo.packageName) 120 && mComponentName.getClassName().equals(resolveInfo.serviceInfo.name)) { 121 return serviceInfo; 122 } 123 } 124 return null; 125 } 126 127 @Override onCreateDialog(int dialogId)128 public Dialog onCreateDialog(int dialogId) { 129 switch (dialogId) { 130 case DialogEnums.ENABLE_WARNING_FROM_TOGGLE: { 131 final AccessibilityServiceInfo info = getAccessibilityServiceInfo(); 132 if (info == null) { 133 return null; 134 } 135 mDialog = AccessibilityServiceWarning 136 .createCapabilitiesDialog(getPrefContext(), info, 137 this::onDialogButtonFromEnableToggleClicked); 138 break; 139 } 140 case DialogEnums.ENABLE_WARNING_FROM_SHORTCUT_TOGGLE: { 141 final AccessibilityServiceInfo info = getAccessibilityServiceInfo(); 142 if (info == null) { 143 return null; 144 } 145 mDialog = AccessibilityServiceWarning 146 .createCapabilitiesDialog(getPrefContext(), info, 147 this::onDialogButtonFromShortcutToggleClicked); 148 break; 149 } 150 case DialogEnums.ENABLE_WARNING_FROM_SHORTCUT: { 151 final AccessibilityServiceInfo info = getAccessibilityServiceInfo(); 152 if (info == null) { 153 return null; 154 } 155 mDialog = AccessibilityServiceWarning 156 .createCapabilitiesDialog(getPrefContext(), info, 157 this::onDialogButtonFromShortcutClicked); 158 break; 159 } 160 case DialogEnums.DISABLE_WARNING_FROM_TOGGLE: { 161 final AccessibilityServiceInfo info = getAccessibilityServiceInfo(); 162 if (info == null) { 163 return null; 164 } 165 mDialog = AccessibilityServiceWarning 166 .createDisableDialog(getPrefContext(), info, 167 this::onDialogButtonFromDisableToggleClicked); 168 break; 169 } 170 default: { 171 mDialog = super.onCreateDialog(dialogId); 172 } 173 } 174 return mDialog; 175 } 176 177 @Override getDialogMetricsCategory(int dialogId)178 public int getDialogMetricsCategory(int dialogId) { 179 switch (dialogId) { 180 case DialogEnums.ENABLE_WARNING_FROM_TOGGLE: 181 case DialogEnums.ENABLE_WARNING_FROM_SHORTCUT: 182 case DialogEnums.ENABLE_WARNING_FROM_SHORTCUT_TOGGLE: 183 return SettingsEnums.DIALOG_ACCESSIBILITY_SERVICE_ENABLE; 184 case DialogEnums.DISABLE_WARNING_FROM_TOGGLE: 185 return SettingsEnums.DIALOG_ACCESSIBILITY_SERVICE_DISABLE; 186 case DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL: 187 return SettingsEnums.DIALOG_ACCESSIBILITY_TUTORIAL; 188 default: 189 return super.getDialogMetricsCategory(dialogId); 190 } 191 } 192 193 @Override getUserShortcutTypes()194 int getUserShortcutTypes() { 195 return AccessibilityUtil.getUserShortcutTypesFromSettings(getPrefContext(), 196 mComponentName); 197 } 198 199 @Override updateToggleServiceTitle(SwitchPreference switchPreference)200 protected void updateToggleServiceTitle(SwitchPreference switchPreference) { 201 final AccessibilityServiceInfo info = getAccessibilityServiceInfo(); 202 final String switchBarText = (info == null) ? "" : 203 getString(R.string.accessibility_service_master_switch_title, 204 info.getResolveInfo().loadLabel(getPackageManager())); 205 switchPreference.setTitle(switchBarText); 206 } 207 updateSwitchBarToggleSwitch()208 private void updateSwitchBarToggleSwitch() { 209 final boolean checked = AccessibilityUtils.getEnabledServicesFromSettings(getPrefContext()) 210 .contains(mComponentName); 211 if (mToggleServiceDividerSwitchPreference.isChecked() == checked) { 212 return; 213 } 214 mToggleServiceDividerSwitchPreference.setChecked(checked); 215 } 216 217 /** 218 * Return whether the device is encrypted with legacy full disk encryption. Newer devices 219 * should be using File Based Encryption. 220 * 221 * @return true if device is encrypted 222 */ isFullDiskEncrypted()223 private boolean isFullDiskEncrypted() { 224 return StorageManager.isNonDefaultBlockEncrypted(); 225 } 226 227 @Override onActivityResult(int requestCode, int resultCode, Intent data)228 public void onActivityResult(int requestCode, int resultCode, Intent data) { 229 if (requestCode == ACTIVITY_REQUEST_CONFIRM_CREDENTIAL_FOR_WEAKER_ENCRYPTION) { 230 if (resultCode == Activity.RESULT_OK) { 231 handleConfirmServiceEnabled(/* confirmed= */ true); 232 // The user confirmed that they accept weaker encryption when 233 // enabling the accessibility service, so change encryption. 234 // Since we came here asynchronously, check encryption again. 235 if (isFullDiskEncrypted()) { 236 mLockPatternUtils.clearEncryptionPassword(); 237 Settings.Global.putInt(getContentResolver(), 238 Settings.Global.REQUIRE_PASSWORD_TO_DECRYPT, 0); 239 } 240 } else { 241 handleConfirmServiceEnabled(/* confirmed= */ false); 242 } 243 } 244 } 245 isServiceSupportAccessibilityButton()246 private boolean isServiceSupportAccessibilityButton() { 247 final AccessibilityManager ams = getPrefContext().getSystemService( 248 AccessibilityManager.class); 249 final List<AccessibilityServiceInfo> services = ams.getInstalledAccessibilityServiceList(); 250 251 for (AccessibilityServiceInfo info : services) { 252 if ((info.flags & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0) { 253 ServiceInfo serviceInfo = info.getResolveInfo().serviceInfo; 254 if (serviceInfo != null && TextUtils.equals(serviceInfo.name, 255 getAccessibilityServiceInfo().getResolveInfo().serviceInfo.name)) { 256 return true; 257 } 258 } 259 } 260 261 return false; 262 } 263 handleConfirmServiceEnabled(boolean confirmed)264 private void handleConfirmServiceEnabled(boolean confirmed) { 265 mToggleServiceDividerSwitchPreference.setChecked(confirmed); 266 getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, confirmed); 267 onPreferenceToggled(mPreferenceKey, confirmed); 268 } 269 createConfirmCredentialReasonMessage()270 private String createConfirmCredentialReasonMessage() { 271 int resId = R.string.enable_service_password_reason; 272 switch (mLockPatternUtils.getKeyguardStoredPasswordQuality(UserHandle.myUserId())) { 273 case DevicePolicyManager.PASSWORD_QUALITY_SOMETHING: { 274 resId = R.string.enable_service_pattern_reason; 275 } 276 break; 277 case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC: 278 case DevicePolicyManager.PASSWORD_QUALITY_NUMERIC_COMPLEX: { 279 resId = R.string.enable_service_pin_reason; 280 } 281 break; 282 } 283 return getString(resId, getAccessibilityServiceInfo().getResolveInfo() 284 .loadLabel(getPackageManager())); 285 } 286 287 @Override onInstallSwitchPreferenceToggleSwitch()288 protected void onInstallSwitchPreferenceToggleSwitch() { 289 super.onInstallSwitchPreferenceToggleSwitch(); 290 mToggleServiceDividerSwitchPreference.setOnPreferenceClickListener(this::onPreferenceClick); 291 } 292 293 @Override onToggleClicked(ShortcutPreference preference)294 public void onToggleClicked(ShortcutPreference preference) { 295 final int shortcutTypes = getUserShortcutTypes(getPrefContext(), UserShortcutType.SOFTWARE); 296 if (preference.isChecked()) { 297 if (!mToggleServiceDividerSwitchPreference.isChecked()) { 298 preference.setChecked(false); 299 showPopupDialog(DialogEnums.ENABLE_WARNING_FROM_SHORTCUT_TOGGLE); 300 } else { 301 AccessibilityUtil.optInAllValuesToSettings(getPrefContext(), shortcutTypes, 302 mComponentName); 303 showPopupDialog(DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL); 304 } 305 } else { 306 AccessibilityUtil.optOutAllValuesFromSettings(getPrefContext(), shortcutTypes, 307 mComponentName); 308 } 309 mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext())); 310 } 311 312 @Override onSettingsClicked(ShortcutPreference preference)313 public void onSettingsClicked(ShortcutPreference preference) { 314 super.onSettingsClicked(preference); 315 final boolean isServiceOnOrShortcutAdded = mShortcutPreference.isChecked() 316 || mToggleServiceDividerSwitchPreference.isChecked(); 317 showPopupDialog(isServiceOnOrShortcutAdded ? DialogEnums.EDIT_SHORTCUT 318 : DialogEnums.ENABLE_WARNING_FROM_SHORTCUT); 319 } 320 321 @Override onProcessArguments(Bundle arguments)322 protected void onProcessArguments(Bundle arguments) { 323 super.onProcessArguments(arguments); 324 // Settings title and intent. 325 String settingsTitle = arguments.getString(AccessibilitySettings.EXTRA_SETTINGS_TITLE); 326 String settingsComponentName = arguments.getString( 327 AccessibilitySettings.EXTRA_SETTINGS_COMPONENT_NAME); 328 if (!TextUtils.isEmpty(settingsTitle) && !TextUtils.isEmpty(settingsComponentName)) { 329 Intent settingsIntent = new Intent(Intent.ACTION_MAIN).setComponent( 330 ComponentName.unflattenFromString(settingsComponentName.toString())); 331 if (!getPackageManager().queryIntentActivities(settingsIntent, 0).isEmpty()) { 332 mSettingsTitle = settingsTitle; 333 mSettingsIntent = settingsIntent; 334 setHasOptionsMenu(true); 335 } 336 } 337 338 mComponentName = arguments.getParcelable(AccessibilitySettings.EXTRA_COMPONENT_NAME); 339 340 // Settings animated image. 341 final int animatedImageRes = arguments.getInt( 342 AccessibilitySettings.EXTRA_ANIMATED_IMAGE_RES); 343 mImageUri = new Uri.Builder().scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) 344 .authority(mComponentName.getPackageName()) 345 .appendPath(String.valueOf(animatedImageRes)) 346 .build(); 347 348 // Get Accessibility service name. 349 mPackageName = getAccessibilityServiceInfo().getResolveInfo().loadLabel( 350 getPackageManager()); 351 } 352 onDialogButtonFromDisableToggleClicked(DialogInterface dialog, int which)353 private void onDialogButtonFromDisableToggleClicked(DialogInterface dialog, int which) { 354 switch (which) { 355 case DialogInterface.BUTTON_POSITIVE: 356 handleConfirmServiceEnabled(/* confirmed= */ false); 357 break; 358 case DialogInterface.BUTTON_NEGATIVE: 359 handleConfirmServiceEnabled(/* confirmed= */ true); 360 break; 361 default: 362 throw new IllegalArgumentException("Unexpected button identifier"); 363 } 364 } 365 onDialogButtonFromEnableToggleClicked(View view)366 private void onDialogButtonFromEnableToggleClicked(View view) { 367 final int viewId = view.getId(); 368 if (viewId == R.id.permission_enable_allow_button) { 369 onAllowButtonFromEnableToggleClicked(); 370 } else if (viewId == R.id.permission_enable_deny_button) { 371 onDenyButtonFromEnableToggleClicked(); 372 } else { 373 throw new IllegalArgumentException("Unexpected view id"); 374 } 375 } 376 onAllowButtonFromEnableToggleClicked()377 private void onAllowButtonFromEnableToggleClicked() { 378 if (isFullDiskEncrypted()) { 379 final String title = createConfirmCredentialReasonMessage(); 380 final Intent intent = ConfirmDeviceCredentialActivity.createIntent(title, /* details= */ 381 null); 382 startActivityForResult(intent, 383 ACTIVITY_REQUEST_CONFIRM_CREDENTIAL_FOR_WEAKER_ENCRYPTION); 384 } else { 385 handleConfirmServiceEnabled(/* confirmed= */ true); 386 if (isServiceSupportAccessibilityButton()) { 387 mIsDialogShown.set(false); 388 showPopupDialog(DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL); 389 } 390 } 391 392 mDialog.dismiss(); 393 } 394 onDenyButtonFromEnableToggleClicked()395 private void onDenyButtonFromEnableToggleClicked() { 396 handleConfirmServiceEnabled(/* confirmed= */ false); 397 mDialog.dismiss(); 398 } 399 onDialogButtonFromShortcutToggleClicked(View view)400 void onDialogButtonFromShortcutToggleClicked(View view) { 401 final int viewId = view.getId(); 402 if (viewId == R.id.permission_enable_allow_button) { 403 onAllowButtonFromShortcutToggleClicked(); 404 } else if (viewId == R.id.permission_enable_deny_button) { 405 onDenyButtonFromShortcutToggleClicked(); 406 } else { 407 throw new IllegalArgumentException("Unexpected view id"); 408 } 409 } 410 onAllowButtonFromShortcutToggleClicked()411 private void onAllowButtonFromShortcutToggleClicked() { 412 mShortcutPreference.setChecked(true); 413 414 final int shortcutTypes = getUserShortcutTypes(getPrefContext(), UserShortcutType.SOFTWARE); 415 AccessibilityUtil.optInAllValuesToSettings(getPrefContext(), shortcutTypes, mComponentName); 416 417 mIsDialogShown.set(false); 418 showPopupDialog(DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL); 419 420 mDialog.dismiss(); 421 422 mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext())); 423 } 424 onDenyButtonFromShortcutToggleClicked()425 private void onDenyButtonFromShortcutToggleClicked() { 426 mShortcutPreference.setChecked(false); 427 428 mDialog.dismiss(); 429 } 430 onDialogButtonFromShortcutClicked(View view)431 void onDialogButtonFromShortcutClicked(View view) { 432 final int viewId = view.getId(); 433 if (viewId == R.id.permission_enable_allow_button) { 434 onAllowButtonFromShortcutClicked(); 435 } else if (viewId == R.id.permission_enable_deny_button) { 436 onDenyButtonFromShortcutClicked(); 437 } else { 438 throw new IllegalArgumentException("Unexpected view id"); 439 } 440 } 441 onAllowButtonFromShortcutClicked()442 private void onAllowButtonFromShortcutClicked() { 443 mIsDialogShown.set(false); 444 showPopupDialog(DialogEnums.EDIT_SHORTCUT); 445 446 mDialog.dismiss(); 447 } 448 onDenyButtonFromShortcutClicked()449 private void onDenyButtonFromShortcutClicked() { 450 mDialog.dismiss(); 451 } 452 onPreferenceClick(Preference preference)453 private boolean onPreferenceClick(Preference preference) { 454 boolean checked = ((DividerSwitchPreference) preference).isChecked(); 455 if (checked) { 456 mToggleServiceDividerSwitchPreference.setChecked(false); 457 getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, 458 /* disableService */ false); 459 if (!mShortcutPreference.isChecked()) { 460 showPopupDialog(DialogEnums.ENABLE_WARNING_FROM_TOGGLE); 461 } else { 462 handleConfirmServiceEnabled(/* confirmed= */ true); 463 if (isServiceSupportAccessibilityButton()) { 464 showPopupDialog(DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL); 465 } 466 } 467 } else { 468 mToggleServiceDividerSwitchPreference.setChecked(true); 469 getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, 470 /* enableService */ true); 471 showDialog(DialogEnums.DISABLE_WARNING_FROM_TOGGLE); 472 } 473 return true; 474 } 475 showPopupDialog(int dialogId)476 private void showPopupDialog(int dialogId) { 477 if (mIsDialogShown.compareAndSet(/* expect= */ false, /* update= */ true)) { 478 showDialog(dialogId); 479 setOnDismissListener( 480 dialog -> mIsDialogShown.compareAndSet(/* expect= */ true, /* update= */ 481 false)); 482 } 483 } 484 } 485