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 21 import android.app.Dialog; 22 import android.app.settings.SettingsEnums; 23 import android.content.ComponentName; 24 import android.content.ContentResolver; 25 import android.content.Context; 26 import android.content.DialogInterface; 27 import android.content.Intent; 28 import android.content.pm.ResolveInfo; 29 import android.graphics.drawable.Drawable; 30 import android.icu.text.CaseMap; 31 import android.net.Uri; 32 import android.os.Bundle; 33 import android.os.Handler; 34 import android.os.UserHandle; 35 import android.provider.Settings; 36 import android.text.Html; 37 import android.text.TextUtils; 38 import android.view.LayoutInflater; 39 import android.view.View; 40 import android.view.ViewGroup; 41 import android.view.accessibility.AccessibilityManager; 42 import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener; 43 import android.widget.CheckBox; 44 import android.widget.ImageView; 45 import android.widget.Switch; 46 47 import androidx.annotation.VisibleForTesting; 48 import androidx.preference.Preference; 49 import androidx.preference.PreferenceCategory; 50 import androidx.preference.PreferenceScreen; 51 52 import com.android.settings.R; 53 import com.android.settings.SettingsActivity; 54 import com.android.settings.SettingsPreferenceFragment; 55 import com.android.settings.accessibility.AccessibilityDialogUtils.DialogType; 56 import com.android.settings.accessibility.AccessibilityUtil.UserShortcutType; 57 import com.android.settings.utils.LocaleUtils; 58 import com.android.settings.widget.SettingsMainSwitchBar; 59 import com.android.settings.widget.SettingsMainSwitchPreference; 60 import com.android.settingslib.HelpUtils; 61 import com.android.settingslib.accessibility.AccessibilityUtils; 62 import com.android.settingslib.widget.IllustrationPreference; 63 import com.android.settingslib.widget.OnMainSwitchChangeListener; 64 65 import com.google.android.setupcompat.util.WizardManagerHelper; 66 67 import java.util.ArrayList; 68 import java.util.List; 69 import java.util.Locale; 70 71 /** 72 * Base class for accessibility fragments with toggle, shortcut, some helper functions 73 * and dialog management. 74 */ 75 public abstract class ToggleFeaturePreferenceFragment extends SettingsPreferenceFragment 76 implements ShortcutPreference.OnClickCallback, OnMainSwitchChangeListener { 77 78 protected SettingsMainSwitchPreference mToggleServiceSwitchPreference; 79 protected ShortcutPreference mShortcutPreference; 80 protected Preference mSettingsPreference; 81 protected String mPreferenceKey; 82 83 protected CharSequence mSettingsTitle; 84 protected Intent mSettingsIntent; 85 // The mComponentName maybe null, such as Magnify 86 protected ComponentName mComponentName; 87 protected CharSequence mPackageName; 88 protected Uri mImageUri; 89 private CharSequence mDescription; 90 protected CharSequence mHtmlDescription; 91 92 private static final String DRAWABLE_FOLDER = "drawable"; 93 protected static final String KEY_USE_SERVICE_PREFERENCE = "use_service"; 94 public static final String KEY_GENERAL_CATEGORY = "general_categories"; 95 protected static final String KEY_HTML_DESCRIPTION_PREFERENCE = "html_description"; 96 private static final String KEY_SHORTCUT_PREFERENCE = "shortcut_preference"; 97 protected static final String KEY_SAVED_USER_SHORTCUT_TYPE = "shortcut_type"; 98 protected static final String KEY_ANIMATED_IMAGE = "animated_image"; 99 100 private TouchExplorationStateChangeListener mTouchExplorationStateChangeListener; 101 private SettingsContentObserver mSettingsContentObserver; 102 103 private CheckBox mSoftwareTypeCheckBox; 104 private CheckBox mHardwareTypeCheckBox; 105 106 public static final int NOT_SET = -1; 107 // Save user's shortcutType value when savedInstance has value (e.g. device rotated). 108 protected int mSavedCheckBoxValue = NOT_SET; 109 110 // For html description of accessibility service, must follow the rule, such as 111 // <img src="R.drawable.fileName"/>, a11y settings will get the resources successfully. 112 private static final String IMG_PREFIX = "R.drawable."; 113 114 private ImageView mImageGetterCacheView; 115 116 private final Html.ImageGetter mImageGetter = (String str) -> { 117 if (str != null && str.startsWith(IMG_PREFIX)) { 118 final String fileName = str.substring(IMG_PREFIX.length()); 119 return getDrawableFromUri(Uri.parse( 120 ContentResolver.SCHEME_ANDROID_RESOURCE + "://" 121 + mComponentName.getPackageName() + "/" + DRAWABLE_FOLDER + "/" 122 + fileName)); 123 } 124 return null; 125 }; 126 127 @Override onCreate(Bundle savedInstanceState)128 public void onCreate(Bundle savedInstanceState) { 129 super.onCreate(savedInstanceState); 130 131 // Restore the user shortcut type. 132 if (savedInstanceState != null && savedInstanceState.containsKey( 133 KEY_SAVED_USER_SHORTCUT_TYPE)) { 134 mSavedCheckBoxValue = savedInstanceState.getInt(KEY_SAVED_USER_SHORTCUT_TYPE, NOT_SET); 135 } 136 137 setupDefaultShortcutIfNecessary(getPrefContext()); 138 final int resId = getPreferenceScreenResId(); 139 if (resId <= 0) { 140 final PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen( 141 getPrefContext()); 142 setPreferenceScreen(preferenceScreen); 143 } 144 145 final List<String> shortcutFeatureKeys = new ArrayList<>(); 146 shortcutFeatureKeys.add(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS); 147 shortcutFeatureKeys.add(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE); 148 mSettingsContentObserver = new SettingsContentObserver(new Handler(), shortcutFeatureKeys) { 149 @Override 150 public void onChange(boolean selfChange, Uri uri) { 151 updateShortcutPreferenceData(); 152 updateShortcutPreference(); 153 } 154 }; 155 } 156 157 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)158 public View onCreateView(LayoutInflater inflater, ViewGroup container, 159 Bundle savedInstanceState) { 160 // Need to be called as early as possible. Protected variables will be assigned here. 161 onProcessArguments(getArguments()); 162 163 initAnimatedImagePreference(); 164 initToggleServiceSwitchPreference(); 165 initGeneralCategory(); 166 initShortcutPreference(); 167 initSettingsPreference(); 168 initHtmlTextPreference(); 169 initFooterPreference(); 170 171 installActionBarToggleSwitch(); 172 173 updateToggleServiceTitle(mToggleServiceSwitchPreference); 174 175 mTouchExplorationStateChangeListener = isTouchExplorationEnabled -> { 176 removeDialog(DialogEnums.EDIT_SHORTCUT); 177 mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext())); 178 }; 179 return super.onCreateView(inflater, container, savedInstanceState); 180 } 181 182 @Override onViewCreated(View view, Bundle savedInstanceState)183 public void onViewCreated(View view, Bundle savedInstanceState) { 184 super.onViewCreated(view, savedInstanceState); 185 186 final SettingsActivity activity = (SettingsActivity) getActivity(); 187 final SettingsMainSwitchBar switchBar = activity.getSwitchBar(); 188 switchBar.hide(); 189 190 updatePreferenceOrder(); 191 } 192 193 @Override onResume()194 public void onResume() { 195 super.onResume(); 196 197 final AccessibilityManager am = getPrefContext().getSystemService( 198 AccessibilityManager.class); 199 am.addTouchExplorationStateChangeListener(mTouchExplorationStateChangeListener); 200 mSettingsContentObserver.register(getContentResolver()); 201 updateShortcutPreferenceData(); 202 updateShortcutPreference(); 203 } 204 205 @Override onPause()206 public void onPause() { 207 final AccessibilityManager am = getPrefContext().getSystemService( 208 AccessibilityManager.class); 209 am.removeTouchExplorationStateChangeListener(mTouchExplorationStateChangeListener); 210 mSettingsContentObserver.unregister(getContentResolver()); 211 super.onPause(); 212 } 213 214 @Override onSaveInstanceState(Bundle outState)215 public void onSaveInstanceState(Bundle outState) { 216 final int value = getShortcutTypeCheckBoxValue(); 217 if (value != NOT_SET) { 218 outState.putInt(KEY_SAVED_USER_SHORTCUT_TYPE, value); 219 } 220 super.onSaveInstanceState(outState); 221 } 222 223 @Override onCreateDialog(int dialogId)224 public Dialog onCreateDialog(int dialogId) { 225 Dialog dialog; 226 switch (dialogId) { 227 case DialogEnums.EDIT_SHORTCUT: 228 final CharSequence dialogTitle = getPrefContext().getString( 229 R.string.accessibility_shortcut_title, mPackageName); 230 final int dialogType = WizardManagerHelper.isAnySetupWizard(getIntent()) 231 ? DialogType.EDIT_SHORTCUT_GENERIC_SUW : DialogType.EDIT_SHORTCUT_GENERIC; 232 dialog = AccessibilityDialogUtils.showEditShortcutDialog( 233 getPrefContext(), dialogType, dialogTitle, 234 this::callOnAlertDialogCheckboxClicked); 235 setupEditShortcutDialog(dialog); 236 return dialog; 237 case DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL: 238 dialog = AccessibilityGestureNavigationTutorial 239 .createAccessibilityTutorialDialog(getPrefContext(), 240 getUserShortcutTypes()); 241 dialog.setCanceledOnTouchOutside(false); 242 return dialog; 243 default: 244 throw new IllegalArgumentException("Unsupported dialogId " + dialogId); 245 } 246 } 247 248 @Override getDialogMetricsCategory(int dialogId)249 public int getDialogMetricsCategory(int dialogId) { 250 switch (dialogId) { 251 case DialogEnums.EDIT_SHORTCUT: 252 return SettingsEnums.DIALOG_ACCESSIBILITY_SERVICE_EDIT_SHORTCUT; 253 case DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL: 254 return SettingsEnums.DIALOG_ACCESSIBILITY_TUTORIAL; 255 default: 256 return SettingsEnums.ACTION_UNKNOWN; 257 } 258 } 259 260 @Override getMetricsCategory()261 public int getMetricsCategory() { 262 return SettingsEnums.ACCESSIBILITY_SERVICE; 263 } 264 265 @Override getHelpResource()266 public int getHelpResource() { 267 return 0; 268 } 269 270 @Override onDestroyView()271 public void onDestroyView() { 272 super.onDestroyView(); 273 removeActionBarToggleSwitch(); 274 } 275 276 @Override onSwitchChanged(Switch switchView, boolean isChecked)277 public void onSwitchChanged(Switch switchView, boolean isChecked) { 278 onPreferenceToggled(mPreferenceKey, isChecked); 279 } 280 281 /** 282 * Returns the shortcut type list which has been checked by user. 283 */ getUserShortcutTypes()284 abstract int getUserShortcutTypes(); 285 updateToggleServiceTitle(SettingsMainSwitchPreference switchPreference)286 protected void updateToggleServiceTitle(SettingsMainSwitchPreference switchPreference) { 287 switchPreference.setTitle(R.string.accessibility_service_primary_switch_title); 288 } 289 onPreferenceToggled(String preferenceKey, boolean enabled)290 protected abstract void onPreferenceToggled(String preferenceKey, boolean enabled); 291 onInstallSwitchPreferenceToggleSwitch()292 protected void onInstallSwitchPreferenceToggleSwitch() { 293 // Implement this to set a checked listener. 294 updateSwitchBarToggleSwitch(); 295 mToggleServiceSwitchPreference.addOnSwitchChangeListener(this); 296 } 297 onRemoveSwitchPreferenceToggleSwitch()298 protected void onRemoveSwitchPreferenceToggleSwitch() { 299 // Implement this to reset a checked listener. 300 } 301 updateSwitchBarToggleSwitch()302 protected void updateSwitchBarToggleSwitch() { 303 // Implement this to update the state of switch. 304 } 305 installActionBarToggleSwitch()306 private void installActionBarToggleSwitch() { 307 onInstallSwitchPreferenceToggleSwitch(); 308 } 309 removeActionBarToggleSwitch()310 private void removeActionBarToggleSwitch() { 311 mToggleServiceSwitchPreference.setOnPreferenceClickListener(null); 312 onRemoveSwitchPreferenceToggleSwitch(); 313 } 314 setTitle(String title)315 public void setTitle(String title) { 316 getActivity().setTitle(title); 317 } 318 onProcessArguments(Bundle arguments)319 protected void onProcessArguments(Bundle arguments) { 320 // Key. 321 mPreferenceKey = arguments.getString(AccessibilitySettings.EXTRA_PREFERENCE_KEY); 322 323 // Title. 324 if (arguments.containsKey(AccessibilitySettings.EXTRA_RESOLVE_INFO)) { 325 ResolveInfo info = arguments.getParcelable(AccessibilitySettings.EXTRA_RESOLVE_INFO); 326 getActivity().setTitle(info.loadLabel(getPackageManager()).toString()); 327 } else if (arguments.containsKey(AccessibilitySettings.EXTRA_TITLE)) { 328 setTitle(arguments.getString(AccessibilitySettings.EXTRA_TITLE)); 329 } 330 331 // Summary. 332 if (arguments.containsKey(AccessibilitySettings.EXTRA_SUMMARY)) { 333 mDescription = arguments.getCharSequence(AccessibilitySettings.EXTRA_SUMMARY); 334 } 335 336 // Settings html description. 337 if (arguments.containsKey(AccessibilitySettings.EXTRA_HTML_DESCRIPTION)) { 338 mHtmlDescription = arguments.getCharSequence( 339 AccessibilitySettings.EXTRA_HTML_DESCRIPTION); 340 } 341 } 342 343 /** Customizes the order by preference key. */ getPreferenceOrderList()344 protected List<String> getPreferenceOrderList() { 345 final List<String> lists = new ArrayList<>(); 346 lists.add(KEY_ANIMATED_IMAGE); 347 lists.add(KEY_USE_SERVICE_PREFERENCE); 348 lists.add(KEY_GENERAL_CATEGORY); 349 lists.add(KEY_HTML_DESCRIPTION_PREFERENCE); 350 return lists; 351 } 352 updatePreferenceOrder()353 private void updatePreferenceOrder() { 354 final List<String> lists = getPreferenceOrderList(); 355 356 final PreferenceScreen preferenceScreen = getPreferenceScreen(); 357 preferenceScreen.setOrderingAsAdded(false); 358 359 final int size = lists.size(); 360 for (int i = 0; i < size; i++) { 361 final Preference preference = preferenceScreen.findPreference(lists.get(i)); 362 if (preference != null) { 363 preference.setOrder(i); 364 } 365 } 366 } 367 getDrawableFromUri(Uri imageUri)368 private Drawable getDrawableFromUri(Uri imageUri) { 369 if (mImageGetterCacheView == null) { 370 mImageGetterCacheView = new ImageView(getPrefContext()); 371 } 372 373 mImageGetterCacheView.setAdjustViewBounds(true); 374 mImageGetterCacheView.setImageURI(imageUri); 375 376 if (mImageGetterCacheView.getDrawable() == null) { 377 return null; 378 } 379 380 final Drawable drawable = 381 mImageGetterCacheView.getDrawable().mutate().getConstantState().newDrawable(); 382 mImageGetterCacheView.setImageURI(null); 383 final int imageWidth = drawable.getIntrinsicWidth(); 384 final int imageHeight = drawable.getIntrinsicHeight(); 385 final int screenHalfHeight = AccessibilityUtil.getScreenHeightPixels(getPrefContext()) / 2; 386 if ((imageWidth > AccessibilityUtil.getScreenWidthPixels(getPrefContext())) 387 || (imageHeight > screenHalfHeight)) { 388 return null; 389 } 390 391 drawable.setBounds(/* left= */0, /* top= */0, drawable.getIntrinsicWidth(), 392 drawable.getIntrinsicHeight()); 393 394 return drawable; 395 } 396 initAnimatedImagePreference()397 private void initAnimatedImagePreference() { 398 if (mImageUri == null) { 399 return; 400 } 401 402 final IllustrationPreference illustrationPreference = 403 new IllustrationPreference(getPrefContext()); 404 illustrationPreference.setImageUri(mImageUri); 405 illustrationPreference.setSelectable(false); 406 illustrationPreference.setKey(KEY_ANIMATED_IMAGE); 407 408 getPreferenceScreen().addPreference(illustrationPreference); 409 } 410 initToggleServiceSwitchPreference()411 private void initToggleServiceSwitchPreference() { 412 mToggleServiceSwitchPreference = new SettingsMainSwitchPreference(getPrefContext()); 413 mToggleServiceSwitchPreference.setKey(KEY_USE_SERVICE_PREFERENCE); 414 if (getArguments().containsKey(AccessibilitySettings.EXTRA_CHECKED)) { 415 final boolean enabled = getArguments().getBoolean(AccessibilitySettings.EXTRA_CHECKED); 416 mToggleServiceSwitchPreference.setChecked(enabled); 417 } 418 419 getPreferenceScreen().addPreference(mToggleServiceSwitchPreference); 420 } 421 initGeneralCategory()422 private void initGeneralCategory() { 423 final PreferenceCategory generalCategory = new PreferenceCategory(getPrefContext()); 424 generalCategory.setKey(KEY_GENERAL_CATEGORY); 425 generalCategory.setTitle(R.string.accessibility_screen_option); 426 427 getPreferenceScreen().addPreference(generalCategory); 428 } 429 initShortcutPreference()430 protected void initShortcutPreference() { 431 // Initial the shortcut preference. 432 mShortcutPreference = new ShortcutPreference(getPrefContext(), /* attrs= */ null); 433 mShortcutPreference.setPersistent(false); 434 mShortcutPreference.setKey(getShortcutPreferenceKey()); 435 mShortcutPreference.setOnClickCallback(this); 436 437 final CharSequence title = getString(R.string.accessibility_shortcut_title, mPackageName); 438 mShortcutPreference.setTitle(title); 439 440 final PreferenceCategory generalCategory = findPreference(KEY_GENERAL_CATEGORY); 441 generalCategory.addPreference(mShortcutPreference); 442 } 443 initSettingsPreference()444 protected void initSettingsPreference() { 445 if (mSettingsTitle == null || mSettingsIntent == null) { 446 return; 447 } 448 449 // Show the "Settings" menu as if it were a preference screen. 450 mSettingsPreference = new Preference(getPrefContext()); 451 mSettingsPreference.setTitle(mSettingsTitle); 452 mSettingsPreference.setIconSpaceReserved(false); 453 mSettingsPreference.setIntent(mSettingsIntent); 454 455 final PreferenceCategory generalCategory = findPreference(KEY_GENERAL_CATEGORY); 456 generalCategory.addPreference(mSettingsPreference); 457 } 458 initHtmlTextPreference()459 private void initHtmlTextPreference() { 460 if (TextUtils.isEmpty(mHtmlDescription)) { 461 return; 462 } 463 final PreferenceScreen screen = getPreferenceScreen(); 464 final CharSequence htmlDescription = Html.fromHtml(mHtmlDescription.toString(), 465 Html.FROM_HTML_MODE_COMPACT, mImageGetter, /* tagHandler= */ null); 466 final String iconContentDescription = 467 getString(R.string.accessibility_introduction_title, mPackageName); 468 469 final AccessibilityFooterPreference htmlFooterPreference = 470 new AccessibilityFooterPreference(screen.getContext()); 471 htmlFooterPreference.setKey(KEY_HTML_DESCRIPTION_PREFERENCE); 472 htmlFooterPreference.setSummary(htmlDescription); 473 htmlFooterPreference.setContentDescription( 474 generateFooterContentDescription(htmlDescription)); 475 476 // Only framework tools support help link 477 if (getHelpResource() != 0) { 478 htmlFooterPreference.setLearnMoreAction(view -> { 479 final Intent helpIntent = HelpUtils.getHelpIntent( 480 getContext(), getContext().getString(getHelpResource()), 481 getContext().getClass().getName()); 482 view.startActivityForResult(helpIntent, 0); 483 }); 484 485 final String learnMoreContentDescription = getPrefContext().getString( 486 R.string.footer_learn_more_content_description, mPackageName); 487 htmlFooterPreference.setLearnMoreContentDescription(learnMoreContentDescription); 488 htmlFooterPreference.setLinkEnabled(true); 489 } else { 490 htmlFooterPreference.setLinkEnabled(false); 491 } 492 screen.addPreference(htmlFooterPreference); 493 } 494 initFooterPreference()495 private void initFooterPreference() { 496 if (!TextUtils.isEmpty(mDescription)) { 497 createFooterPreference(getPreferenceScreen(), mDescription, 498 getString(R.string.accessibility_introduction_title, mPackageName)); 499 } 500 501 if (TextUtils.isEmpty(mHtmlDescription) && TextUtils.isEmpty(mDescription)) { 502 final CharSequence defaultDescription = 503 getText(R.string.accessibility_service_default_description); 504 createFooterPreference(getPreferenceScreen(), defaultDescription, 505 getString(R.string.accessibility_introduction_title, mPackageName)); 506 } 507 } 508 509 510 /** 511 * Creates {@link AccessibilityFooterPreference} and append into {@link PreferenceScreen} 512 * 513 * @param screen The preference screen to add the footer preference 514 * @param summary The summary of the preference summary. 515 * @param iconContentDescription The content description of icon in the footer. 516 */ 517 @VisibleForTesting createFooterPreference(PreferenceScreen screen, CharSequence summary, String iconContentDescription)518 void createFooterPreference(PreferenceScreen screen, CharSequence summary, 519 String iconContentDescription) { 520 final AccessibilityFooterPreference footerPreference = 521 new AccessibilityFooterPreference(screen.getContext()); 522 footerPreference.setSummary(summary); 523 footerPreference.setContentDescription( 524 generateFooterContentDescription(summary)); 525 526 // Only framework tools support help link 527 if (getHelpResource() != 0) { 528 footerPreference.setLearnMoreAction(view -> { 529 final Intent helpIntent = HelpUtils.getHelpIntent( 530 getContext(), getContext().getString(getHelpResource()), 531 getContext().getClass().getName()); 532 view.startActivityForResult(helpIntent, 0); 533 }); 534 535 final String learnMoreContentDescription = getPrefContext().getString( 536 R.string.footer_learn_more_content_description, mPackageName); 537 footerPreference.setLearnMoreContentDescription(learnMoreContentDescription); 538 } 539 screen.addPreference(footerPreference); 540 } 541 generateFooterContentDescription(CharSequence footerContent)542 private CharSequence generateFooterContentDescription(CharSequence footerContent) { 543 final StringBuffer sb = new StringBuffer(); 544 sb.append(getPrefContext().getString( 545 R.string.accessibility_introduction_title, mPackageName)) 546 .append("\n\n") 547 .append(footerContent); 548 return sb; 549 } 550 @VisibleForTesting setupEditShortcutDialog(Dialog dialog)551 void setupEditShortcutDialog(Dialog dialog) { 552 final View dialogSoftwareView = dialog.findViewById(R.id.software_shortcut); 553 mSoftwareTypeCheckBox = dialogSoftwareView.findViewById(R.id.checkbox); 554 setDialogTextAreaClickListener(dialogSoftwareView, mSoftwareTypeCheckBox); 555 556 final View dialogHardwareView = dialog.findViewById(R.id.hardware_shortcut); 557 mHardwareTypeCheckBox = dialogHardwareView.findViewById(R.id.checkbox); 558 setDialogTextAreaClickListener(dialogHardwareView, mHardwareTypeCheckBox); 559 560 updateEditShortcutDialogCheckBox(); 561 } 562 setDialogTextAreaClickListener(View dialogView, CheckBox checkBox)563 private void setDialogTextAreaClickListener(View dialogView, CheckBox checkBox) { 564 final View dialogTextArea = dialogView.findViewById(R.id.container); 565 dialogTextArea.setOnClickListener(v -> checkBox.toggle()); 566 } 567 updateEditShortcutDialogCheckBox()568 private void updateEditShortcutDialogCheckBox() { 569 // If it is during onConfigChanged process then restore the value, or get the saved value 570 // when shortcutPreference is checked. 571 int value = restoreOnConfigChangedValue(); 572 if (value == NOT_SET) { 573 final int lastNonEmptyUserShortcutType = PreferredShortcuts.retrieveUserShortcutType( 574 getPrefContext(), mComponentName.flattenToString(), UserShortcutType.SOFTWARE); 575 value = mShortcutPreference.isChecked() ? lastNonEmptyUserShortcutType 576 : UserShortcutType.EMPTY; 577 } 578 579 mSoftwareTypeCheckBox.setChecked( 580 hasShortcutType(value, UserShortcutType.SOFTWARE)); 581 mHardwareTypeCheckBox.setChecked( 582 hasShortcutType(value, UserShortcutType.HARDWARE)); 583 } 584 restoreOnConfigChangedValue()585 private int restoreOnConfigChangedValue() { 586 final int savedValue = mSavedCheckBoxValue; 587 mSavedCheckBoxValue = NOT_SET; 588 return savedValue; 589 } 590 hasShortcutType(int value, @UserShortcutType int type)591 private boolean hasShortcutType(int value, @UserShortcutType int type) { 592 return (value & type) == type; 593 } 594 595 /** 596 * Returns accumulated {@link UserShortcutType} checkbox value or {@code NOT_SET} if checkboxes 597 * did not exist. 598 */ getShortcutTypeCheckBoxValue()599 protected int getShortcutTypeCheckBoxValue() { 600 if (mSoftwareTypeCheckBox == null || mHardwareTypeCheckBox == null) { 601 return NOT_SET; 602 } 603 604 int value = UserShortcutType.EMPTY; 605 if (mSoftwareTypeCheckBox.isChecked()) { 606 value |= UserShortcutType.SOFTWARE; 607 } 608 if (mHardwareTypeCheckBox.isChecked()) { 609 value |= UserShortcutType.HARDWARE; 610 } 611 return value; 612 } 613 getShortcutTypeSummary(Context context)614 protected CharSequence getShortcutTypeSummary(Context context) { 615 if (!mShortcutPreference.isSettingsEditable()) { 616 return context.getText(R.string.accessibility_shortcut_edit_dialog_title_hardware); 617 } 618 619 if (!mShortcutPreference.isChecked()) { 620 return context.getText(R.string.switch_off_text); 621 } 622 623 final int shortcutTypes = PreferredShortcuts.retrieveUserShortcutType(context, 624 mComponentName.flattenToString(), UserShortcutType.SOFTWARE); 625 626 final List<CharSequence> list = new ArrayList<>(); 627 final CharSequence softwareTitle = context.getText( 628 R.string.accessibility_shortcut_edit_summary_software); 629 630 if (hasShortcutType(shortcutTypes, UserShortcutType.SOFTWARE)) { 631 list.add(softwareTitle); 632 } 633 if (hasShortcutType(shortcutTypes, UserShortcutType.HARDWARE)) { 634 final CharSequence hardwareTitle = context.getText( 635 R.string.accessibility_shortcut_hardware_keyword); 636 list.add(hardwareTitle); 637 } 638 639 // Show software shortcut if first time to use. 640 if (list.isEmpty()) { 641 list.add(softwareTitle); 642 } 643 644 return CaseMap.toTitle().wholeString().noLowercase().apply(Locale.getDefault(), /* iter= */ 645 null, LocaleUtils.getConcatenatedString(list)); 646 } 647 648 /** 649 * This method will be invoked when a button in the edit shortcut dialog is clicked. 650 * 651 * @param dialog The dialog that received the click 652 * @param which The button that was clicked 653 */ callOnAlertDialogCheckboxClicked(DialogInterface dialog, int which)654 protected void callOnAlertDialogCheckboxClicked(DialogInterface dialog, int which) { 655 if (mComponentName == null) { 656 return; 657 } 658 659 final int value = getShortcutTypeCheckBoxValue(); 660 661 saveNonEmptyUserShortcutType(value); 662 AccessibilityUtil.optInAllValuesToSettings(getPrefContext(), value, mComponentName); 663 AccessibilityUtil.optOutAllValuesFromSettings(getPrefContext(), ~value, mComponentName); 664 mShortcutPreference.setChecked(value != UserShortcutType.EMPTY); 665 mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext())); 666 } 667 updateShortcutPreferenceData()668 protected void updateShortcutPreferenceData() { 669 if (mComponentName == null) { 670 return; 671 } 672 673 final int shortcutTypes = AccessibilityUtil.getUserShortcutTypesFromSettings( 674 getPrefContext(), mComponentName); 675 if (shortcutTypes != UserShortcutType.EMPTY) { 676 final PreferredShortcut shortcut = new PreferredShortcut( 677 mComponentName.flattenToString(), shortcutTypes); 678 PreferredShortcuts.saveUserShortcutType(getPrefContext(), shortcut); 679 } 680 } 681 updateShortcutPreference()682 protected void updateShortcutPreference() { 683 if (mComponentName == null) { 684 return; 685 } 686 687 final int shortcutTypes = PreferredShortcuts.retrieveUserShortcutType(getPrefContext(), 688 mComponentName.flattenToString(), UserShortcutType.SOFTWARE); 689 mShortcutPreference.setChecked( 690 AccessibilityUtil.hasValuesInSettings(getPrefContext(), shortcutTypes, 691 mComponentName)); 692 mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext())); 693 } 694 getShortcutPreferenceKey()695 protected String getShortcutPreferenceKey() { 696 return KEY_SHORTCUT_PREFERENCE; 697 } 698 699 @Override onToggleClicked(ShortcutPreference preference)700 public void onToggleClicked(ShortcutPreference preference) { 701 if (mComponentName == null) { 702 return; 703 } 704 705 final int shortcutTypes = PreferredShortcuts.retrieveUserShortcutType(getPrefContext(), 706 mComponentName.flattenToString(), UserShortcutType.SOFTWARE); 707 if (preference.isChecked()) { 708 AccessibilityUtil.optInAllValuesToSettings(getPrefContext(), shortcutTypes, 709 mComponentName); 710 showDialog(DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL); 711 } else { 712 AccessibilityUtil.optOutAllValuesFromSettings(getPrefContext(), shortcutTypes, 713 mComponentName); 714 } 715 mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext())); 716 } 717 718 @Override onSettingsClicked(ShortcutPreference preference)719 public void onSettingsClicked(ShortcutPreference preference) { 720 showDialog(DialogEnums.EDIT_SHORTCUT); 721 } 722 723 /** 724 * Setups a configurable default if the setting has never been set. 725 */ setupDefaultShortcutIfNecessary(Context context)726 private static void setupDefaultShortcutIfNecessary(Context context) { 727 final String targetKey = Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE; 728 String targetString = Settings.Secure.getString(context.getContentResolver(), targetKey); 729 if (!TextUtils.isEmpty(targetString)) { 730 // The shortcut setting has been set 731 return; 732 } 733 734 // AccessibilityManager#getAccessibilityShortcutTargets may not return correct shortcut 735 // targets during boot. Needs to read settings directly here. 736 targetString = AccessibilityUtils.getShortcutTargetServiceComponentNameString(context, 737 UserHandle.myUserId()); 738 if (TextUtils.isEmpty(targetString)) { 739 // No configurable default accessibility service 740 return; 741 } 742 743 // Only fallback to default accessibility service when setting is never updated. 744 final ComponentName shortcutName = ComponentName.unflattenFromString(targetString); 745 if (shortcutName != null) { 746 Settings.Secure.putString(context.getContentResolver(), targetKey, 747 shortcutName.flattenToString()); 748 } 749 } 750 751 @VisibleForTesting saveNonEmptyUserShortcutType(int type)752 void saveNonEmptyUserShortcutType(int type) { 753 if (type == UserShortcutType.EMPTY) { 754 return; 755 } 756 757 final PreferredShortcut shortcut = new PreferredShortcut( 758 mComponentName.flattenToString(), type); 759 PreferredShortcuts.saveUserShortcutType(getPrefContext(), shortcut); 760 } 761 } 762