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