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.AccessibilityDialogUtils.DialogEnums; 20 import static com.android.settings.accessibility.AccessibilityStatsLogUtils.logAccessibilityServiceEnabled; 21 import static com.android.settings.accessibility.PreferredShortcuts.retrieveUserShortcutType; 22 23 import android.accessibilityservice.AccessibilityServiceInfo; 24 import android.app.Dialog; 25 import android.app.settings.SettingsEnums; 26 import android.content.BroadcastReceiver; 27 import android.content.ComponentName; 28 import android.content.ContentResolver; 29 import android.content.Context; 30 import android.content.DialogInterface; 31 import android.content.Intent; 32 import android.content.IntentFilter; 33 import android.content.pm.ApplicationInfo; 34 import android.content.pm.ResolveInfo; 35 import android.net.Uri; 36 import android.os.Bundle; 37 import android.os.SystemClock; 38 import android.text.TextUtils; 39 import android.util.Log; 40 import android.view.Menu; 41 import android.view.MenuInflater; 42 import android.view.View; 43 import android.view.accessibility.AccessibilityManager; 44 import android.widget.Switch; 45 46 import androidx.annotation.Nullable; 47 48 import com.android.settings.R; 49 import com.android.settings.accessibility.AccessibilityUtil.QuickSettingsTooltipType; 50 import com.android.settings.accessibility.AccessibilityUtil.UserShortcutType; 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 private static final String TAG = "ToggleAccessibilityServicePreferenceFragment"; 61 private static final String KEY_HAS_LOGGED = "has_logged"; 62 private final AtomicBoolean mIsDialogShown = new AtomicBoolean(/* initialValue= */ false); 63 64 private Dialog mWarningDialog; 65 private ComponentName mTileComponentName; 66 private BroadcastReceiver mPackageRemovedReceiver; 67 private boolean mDisabledStateLogged = false; 68 private long mStartTimeMillsForLogging = 0; 69 70 @Override getMetricsCategory()71 public int getMetricsCategory() { 72 return getArguments().getInt(AccessibilitySettings.EXTRA_METRICS_CATEGORY); 73 } 74 75 @Override onCreateOptionsMenu(Menu menu, MenuInflater inflater)76 public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { 77 // Do not call super. We don't want to see the "Help & feedback" option on this page so as 78 // not to confuse users who think they might be able to send feedback about a specific 79 // accessibility service from this page. 80 } 81 82 @Override onCreate(Bundle savedInstanceState)83 public void onCreate(Bundle savedInstanceState) { 84 super.onCreate(savedInstanceState); 85 if (savedInstanceState != null) { 86 if (savedInstanceState.containsKey(KEY_HAS_LOGGED)) { 87 mDisabledStateLogged = savedInstanceState.getBoolean(KEY_HAS_LOGGED); 88 } 89 } 90 } 91 92 @Override registerKeysToObserverCallback( AccessibilitySettingsContentObserver contentObserver)93 protected void registerKeysToObserverCallback( 94 AccessibilitySettingsContentObserver contentObserver) { 95 super.registerKeysToObserverCallback(contentObserver); 96 contentObserver.registerObserverCallback(key -> updateSwitchBarToggleSwitch()); 97 } 98 99 @Override onStart()100 public void onStart() { 101 super.onStart(); 102 final AccessibilityServiceInfo serviceInfo = getAccessibilityServiceInfo(); 103 if (serviceInfo == null) { 104 getActivity().finishAndRemoveTask(); 105 } else if (!AccessibilityUtil.isSystemApp(serviceInfo)) { 106 registerPackageRemoveReceiver(); 107 } 108 } 109 110 @Override onResume()111 public void onResume() { 112 super.onResume(); 113 updateSwitchBarToggleSwitch(); 114 } 115 116 @Override onSaveInstanceState(Bundle outState)117 public void onSaveInstanceState(Bundle outState) { 118 if (mStartTimeMillsForLogging > 0) { 119 outState.putBoolean(KEY_HAS_LOGGED, mDisabledStateLogged); 120 } 121 super.onSaveInstanceState(outState); 122 } 123 124 @Override onPreferenceToggled(String preferenceKey, boolean enabled)125 public void onPreferenceToggled(String preferenceKey, boolean enabled) { 126 ComponentName toggledService = ComponentName.unflattenFromString(preferenceKey); 127 logAccessibilityServiceEnabled(toggledService, enabled); 128 if (!enabled) { 129 logDisabledState(toggledService.getPackageName()); 130 } 131 AccessibilityUtils.setAccessibilityServiceState(getPrefContext(), toggledService, enabled); 132 } 133 134 // IMPORTANT: Refresh the info since there are dynamically changing capabilities. For 135 // example, before JellyBean MR2 the user was granting the explore by touch one. 136 @Nullable getAccessibilityServiceInfo()137 AccessibilityServiceInfo getAccessibilityServiceInfo() { 138 final List<AccessibilityServiceInfo> infos = AccessibilityManager.getInstance( 139 getPrefContext()).getInstalledAccessibilityServiceList(); 140 141 for (int i = 0, count = infos.size(); i < count; i++) { 142 AccessibilityServiceInfo serviceInfo = infos.get(i); 143 ResolveInfo resolveInfo = serviceInfo.getResolveInfo(); 144 if (mComponentName.getPackageName().equals(resolveInfo.serviceInfo.packageName) 145 && mComponentName.getClassName().equals(resolveInfo.serviceInfo.name)) { 146 return serviceInfo; 147 } 148 } 149 return null; 150 } 151 152 @Override onCreateDialog(int dialogId)153 public Dialog onCreateDialog(int dialogId) { 154 final AccessibilityServiceInfo info = getAccessibilityServiceInfo(); 155 switch (dialogId) { 156 case DialogEnums.ENABLE_WARNING_FROM_TOGGLE: 157 if (info == null) { 158 return null; 159 } 160 mWarningDialog = AccessibilityServiceWarning 161 .createCapabilitiesDialog(getPrefContext(), info, 162 this::onDialogButtonFromEnableToggleClicked, 163 this::onDialogButtonFromUninstallClicked); 164 return mWarningDialog; 165 case DialogEnums.ENABLE_WARNING_FROM_SHORTCUT_TOGGLE: 166 if (info == null) { 167 return null; 168 } 169 mWarningDialog = AccessibilityServiceWarning 170 .createCapabilitiesDialog(getPrefContext(), info, 171 this::onDialogButtonFromShortcutToggleClicked, 172 this::onDialogButtonFromUninstallClicked); 173 return mWarningDialog; 174 case DialogEnums.ENABLE_WARNING_FROM_SHORTCUT: 175 if (info == null) { 176 return null; 177 } 178 mWarningDialog = AccessibilityServiceWarning 179 .createCapabilitiesDialog(getPrefContext(), info, 180 this::onDialogButtonFromShortcutClicked, 181 this::onDialogButtonFromUninstallClicked); 182 return mWarningDialog; 183 case DialogEnums.DISABLE_WARNING_FROM_TOGGLE: 184 if (info == null) { 185 return null; 186 } 187 mWarningDialog = AccessibilityServiceWarning 188 .createDisableDialog(getPrefContext(), info, 189 this::onDialogButtonFromDisableToggleClicked); 190 return mWarningDialog; 191 default: 192 return super.onCreateDialog(dialogId); 193 } 194 } 195 196 @Override getDialogMetricsCategory(int dialogId)197 public int getDialogMetricsCategory(int dialogId) { 198 switch (dialogId) { 199 case DialogEnums.ENABLE_WARNING_FROM_TOGGLE: 200 case DialogEnums.ENABLE_WARNING_FROM_SHORTCUT: 201 case DialogEnums.ENABLE_WARNING_FROM_SHORTCUT_TOGGLE: 202 return SettingsEnums.DIALOG_ACCESSIBILITY_SERVICE_ENABLE; 203 case DialogEnums.DISABLE_WARNING_FROM_TOGGLE: 204 return SettingsEnums.DIALOG_ACCESSIBILITY_SERVICE_DISABLE; 205 case DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL: 206 return SettingsEnums.DIALOG_ACCESSIBILITY_TUTORIAL; 207 default: 208 return super.getDialogMetricsCategory(dialogId); 209 } 210 } 211 212 @Override getUserShortcutTypes()213 int getUserShortcutTypes() { 214 return AccessibilityUtil.getUserShortcutTypesFromSettings(getPrefContext(), 215 mComponentName); 216 } 217 218 @Override getTileComponentName()219 ComponentName getTileComponentName() { 220 return mTileComponentName; 221 } 222 223 @Override getTileTooltipContent(@uickSettingsTooltipType int type)224 CharSequence getTileTooltipContent(@QuickSettingsTooltipType int type) { 225 final ComponentName componentName = getTileComponentName(); 226 if (componentName == null) { 227 return null; 228 } 229 230 final CharSequence tileName = loadTileLabel(getPrefContext(), componentName); 231 if (tileName == null) { 232 return null; 233 } 234 235 final int titleResId = type == QuickSettingsTooltipType.GUIDE_TO_EDIT 236 ? R.string.accessibility_service_qs_tooltip_content 237 : R.string.accessibility_service_auto_added_qs_tooltip_content; 238 return getString(titleResId, tileName); 239 } 240 241 @Override updateSwitchBarToggleSwitch()242 protected void updateSwitchBarToggleSwitch() { 243 final boolean checked = isAccessibilityServiceEnabled(); 244 if (mToggleServiceSwitchPreference.isChecked() == checked) { 245 return; 246 } 247 mToggleServiceSwitchPreference.setChecked(checked); 248 } 249 isAccessibilityServiceEnabled()250 private boolean isAccessibilityServiceEnabled() { 251 return AccessibilityUtils.getEnabledServicesFromSettings(getPrefContext()) 252 .contains(mComponentName); 253 } 254 255 @Override onActivityResult(int requestCode, int resultCode, Intent data)256 public void onActivityResult(int requestCode, int resultCode, Intent data) { 257 } 258 registerPackageRemoveReceiver()259 private void registerPackageRemoveReceiver() { 260 if (mPackageRemovedReceiver != null || getContext() == null) { 261 return; 262 } 263 mPackageRemovedReceiver = new BroadcastReceiver() { 264 @Override 265 public void onReceive(Context context, Intent intent) { 266 final String packageName = intent.getData().getSchemeSpecificPart(); 267 if (TextUtils.equals(mComponentName.getPackageName(), packageName)) { 268 getActivity().finishAndRemoveTask(); 269 } 270 } 271 }; 272 final IntentFilter filter = new IntentFilter(Intent.ACTION_PACKAGE_REMOVED); 273 filter.addDataScheme("package"); 274 getContext().registerReceiver(mPackageRemovedReceiver, filter); 275 } 276 unregisterPackageRemoveReceiver()277 private void unregisterPackageRemoveReceiver() { 278 if (mPackageRemovedReceiver == null || getContext() == null) { 279 return; 280 } 281 getContext().unregisterReceiver(mPackageRemovedReceiver); 282 mPackageRemovedReceiver = null; 283 } 284 serviceSupportsAccessibilityButton()285 boolean serviceSupportsAccessibilityButton() { 286 final AccessibilityServiceInfo info = getAccessibilityServiceInfo(); 287 return info != null 288 && (info.flags & AccessibilityServiceInfo.FLAG_REQUEST_ACCESSIBILITY_BUTTON) != 0; 289 } 290 handleConfirmServiceEnabled(boolean confirmed)291 private void handleConfirmServiceEnabled(boolean confirmed) { 292 getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, confirmed); 293 onPreferenceToggled(mPreferenceKey, confirmed); 294 } 295 296 @Override onSwitchChanged(Switch switchView, boolean isChecked)297 public void onSwitchChanged(Switch switchView, boolean isChecked) { 298 if (isChecked != isAccessibilityServiceEnabled()) { 299 onPreferenceClick(isChecked); 300 } 301 } 302 303 @Override onToggleClicked(ShortcutPreference preference)304 public void onToggleClicked(ShortcutPreference preference) { 305 final int shortcutTypes = retrieveUserShortcutType(getPrefContext(), 306 mComponentName.flattenToString(), UserShortcutType.SOFTWARE); 307 if (preference.isChecked()) { 308 if (!mToggleServiceSwitchPreference.isChecked()) { 309 preference.setChecked(false); 310 showPopupDialog(DialogEnums.ENABLE_WARNING_FROM_SHORTCUT_TOGGLE); 311 } else { 312 AccessibilityUtil.optInAllValuesToSettings(getPrefContext(), shortcutTypes, 313 mComponentName); 314 showPopupDialog(DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL); 315 } 316 } else { 317 AccessibilityUtil.optOutAllValuesFromSettings(getPrefContext(), shortcutTypes, 318 mComponentName); 319 } 320 mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext())); 321 } 322 323 @Override onSettingsClicked(ShortcutPreference preference)324 public void onSettingsClicked(ShortcutPreference preference) { 325 final boolean isServiceOnOrShortcutAdded = mShortcutPreference.isChecked() 326 || mToggleServiceSwitchPreference.isChecked(); 327 showPopupDialog(isServiceOnOrShortcutAdded ? DialogEnums.EDIT_SHORTCUT 328 : DialogEnums.ENABLE_WARNING_FROM_SHORTCUT); 329 } 330 331 @Override onProcessArguments(Bundle arguments)332 protected void onProcessArguments(Bundle arguments) { 333 super.onProcessArguments(arguments); 334 // Settings title and intent. 335 String settingsTitle = arguments.getString(AccessibilitySettings.EXTRA_SETTINGS_TITLE); 336 String settingsComponentName = arguments.getString( 337 AccessibilitySettings.EXTRA_SETTINGS_COMPONENT_NAME); 338 if (!TextUtils.isEmpty(settingsTitle) && !TextUtils.isEmpty(settingsComponentName)) { 339 Intent settingsIntent = new Intent(Intent.ACTION_MAIN).setComponent( 340 ComponentName.unflattenFromString(settingsComponentName.toString())); 341 if (!getPackageManager().queryIntentActivities(settingsIntent, 0).isEmpty()) { 342 mSettingsTitle = settingsTitle; 343 mSettingsIntent = settingsIntent; 344 setHasOptionsMenu(true); 345 } 346 } 347 348 mComponentName = arguments.getParcelable(AccessibilitySettings.EXTRA_COMPONENT_NAME); 349 350 // Settings animated image. 351 final int animatedImageRes = arguments.getInt( 352 AccessibilitySettings.EXTRA_ANIMATED_IMAGE_RES); 353 if (animatedImageRes > 0) { 354 mImageUri = new Uri.Builder().scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) 355 .authority(mComponentName.getPackageName()) 356 .appendPath(String.valueOf(animatedImageRes)) 357 .build(); 358 } 359 360 // Get Accessibility service name. 361 mPackageName = getAccessibilityServiceInfo().getResolveInfo().loadLabel( 362 getPackageManager()); 363 364 if (arguments.containsKey(AccessibilitySettings.EXTRA_TILE_SERVICE_COMPONENT_NAME)) { 365 final String tileServiceComponentName = arguments.getString( 366 AccessibilitySettings.EXTRA_TILE_SERVICE_COMPONENT_NAME); 367 mTileComponentName = ComponentName.unflattenFromString(tileServiceComponentName); 368 } 369 370 mStartTimeMillsForLogging = arguments.getLong(AccessibilitySettings.EXTRA_TIME_FOR_LOGGING); 371 } 372 onDialogButtonFromDisableToggleClicked(DialogInterface dialog, int which)373 private void onDialogButtonFromDisableToggleClicked(DialogInterface dialog, int which) { 374 switch (which) { 375 case DialogInterface.BUTTON_POSITIVE: 376 handleConfirmServiceEnabled(/* confirmed= */ false); 377 break; 378 case DialogInterface.BUTTON_NEGATIVE: 379 handleConfirmServiceEnabled(/* confirmed= */ true); 380 break; 381 default: 382 throw new IllegalArgumentException("Unexpected button identifier"); 383 } 384 } 385 onDialogButtonFromEnableToggleClicked(View view)386 private void onDialogButtonFromEnableToggleClicked(View view) { 387 final int viewId = view.getId(); 388 if (viewId == R.id.permission_enable_allow_button) { 389 onAllowButtonFromEnableToggleClicked(); 390 } else if (viewId == R.id.permission_enable_deny_button) { 391 onDenyButtonFromEnableToggleClicked(); 392 } else { 393 throw new IllegalArgumentException("Unexpected view id"); 394 } 395 } 396 onDialogButtonFromUninstallClicked()397 private void onDialogButtonFromUninstallClicked() { 398 mWarningDialog.dismiss(); 399 final Intent uninstallIntent = createUninstallPackageActivityIntent(); 400 if (uninstallIntent == null) { 401 return; 402 } 403 startActivity(uninstallIntent); 404 } 405 406 @Nullable createUninstallPackageActivityIntent()407 private Intent createUninstallPackageActivityIntent() { 408 final AccessibilityServiceInfo a11yServiceInfo = getAccessibilityServiceInfo(); 409 if (a11yServiceInfo == null) { 410 Log.w(TAG, "createUnInstallIntent -- invalid a11yServiceInfo"); 411 return null; 412 } 413 final ApplicationInfo appInfo = 414 a11yServiceInfo.getResolveInfo().serviceInfo.applicationInfo; 415 final Uri packageUri = Uri.parse("package:" + appInfo.packageName); 416 final Intent uninstallIntent = new Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri); 417 return uninstallIntent; 418 } 419 420 @Override onStop()421 public void onStop() { 422 super.onStop(); 423 unregisterPackageRemoveReceiver(); 424 } 425 426 @Override getPreferenceScreenResId()427 protected int getPreferenceScreenResId() { 428 // TODO(b/171272809): Add back when controllers move to static type 429 return 0; 430 } 431 432 @Override getLogTag()433 protected String getLogTag() { 434 return TAG; 435 } 436 onAllowButtonFromEnableToggleClicked()437 private void onAllowButtonFromEnableToggleClicked() { 438 handleConfirmServiceEnabled(/* confirmed= */ true); 439 if (serviceSupportsAccessibilityButton()) { 440 mIsDialogShown.set(false); 441 showPopupDialog(DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL); 442 } 443 mWarningDialog.dismiss(); 444 } 445 onDenyButtonFromEnableToggleClicked()446 private void onDenyButtonFromEnableToggleClicked() { 447 handleConfirmServiceEnabled(/* confirmed= */ false); 448 mWarningDialog.dismiss(); 449 } 450 onDialogButtonFromShortcutToggleClicked(View view)451 void onDialogButtonFromShortcutToggleClicked(View view) { 452 final int viewId = view.getId(); 453 if (viewId == R.id.permission_enable_allow_button) { 454 onAllowButtonFromShortcutToggleClicked(); 455 } else if (viewId == R.id.permission_enable_deny_button) { 456 onDenyButtonFromShortcutToggleClicked(); 457 } else { 458 throw new IllegalArgumentException("Unexpected view id"); 459 } 460 } 461 onAllowButtonFromShortcutToggleClicked()462 private void onAllowButtonFromShortcutToggleClicked() { 463 mShortcutPreference.setChecked(true); 464 465 final int shortcutTypes = retrieveUserShortcutType(getPrefContext(), 466 mComponentName.flattenToString(), UserShortcutType.SOFTWARE); 467 AccessibilityUtil.optInAllValuesToSettings(getPrefContext(), shortcutTypes, mComponentName); 468 469 mIsDialogShown.set(false); 470 showPopupDialog(DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL); 471 472 mWarningDialog.dismiss(); 473 474 mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext())); 475 } 476 onDenyButtonFromShortcutToggleClicked()477 private void onDenyButtonFromShortcutToggleClicked() { 478 mShortcutPreference.setChecked(false); 479 480 mWarningDialog.dismiss(); 481 } 482 onDialogButtonFromShortcutClicked(View view)483 void onDialogButtonFromShortcutClicked(View view) { 484 final int viewId = view.getId(); 485 if (viewId == R.id.permission_enable_allow_button) { 486 onAllowButtonFromShortcutClicked(); 487 } else if (viewId == R.id.permission_enable_deny_button) { 488 onDenyButtonFromShortcutClicked(); 489 } else { 490 throw new IllegalArgumentException("Unexpected view id"); 491 } 492 } 493 onAllowButtonFromShortcutClicked()494 private void onAllowButtonFromShortcutClicked() { 495 mIsDialogShown.set(false); 496 showPopupDialog(DialogEnums.EDIT_SHORTCUT); 497 498 mWarningDialog.dismiss(); 499 } 500 onDenyButtonFromShortcutClicked()501 private void onDenyButtonFromShortcutClicked() { 502 mWarningDialog.dismiss(); 503 } 504 onPreferenceClick(boolean isChecked)505 private boolean onPreferenceClick(boolean isChecked) { 506 if (isChecked) { 507 mToggleServiceSwitchPreference.setChecked(false); 508 getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, 509 /* disableService */ false); 510 if (!mShortcutPreference.isChecked()) { 511 showPopupDialog(DialogEnums.ENABLE_WARNING_FROM_TOGGLE); 512 } else { 513 handleConfirmServiceEnabled(/* confirmed= */ true); 514 if (serviceSupportsAccessibilityButton()) { 515 showPopupDialog(DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL); 516 } 517 } 518 } else { 519 mToggleServiceSwitchPreference.setChecked(true); 520 getArguments().putBoolean(AccessibilitySettings.EXTRA_CHECKED, 521 /* enableService */ true); 522 showDialog(DialogEnums.DISABLE_WARNING_FROM_TOGGLE); 523 } 524 return true; 525 } 526 showPopupDialog(int dialogId)527 private void showPopupDialog(int dialogId) { 528 if (mIsDialogShown.compareAndSet(/* expect= */ false, /* update= */ true)) { 529 showDialog(dialogId); 530 setOnDismissListener( 531 dialog -> mIsDialogShown.compareAndSet(/* expect= */ true, /* update= */ 532 false)); 533 } 534 } 535 logDisabledState(String packageName)536 private void logDisabledState(String packageName) { 537 if (mStartTimeMillsForLogging > 0 && !mDisabledStateLogged) { 538 AccessibilityStatsLogUtils.logDisableNonA11yCategoryService( 539 packageName, 540 SystemClock.elapsedRealtime() - mStartTimeMillsForLogging); 541 mDisabledStateLogged = true; 542 } 543 } 544 } 545