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.Activity; 22 import android.app.Dialog; 23 import android.app.settings.SettingsEnums; 24 import android.content.ComponentName; 25 import android.content.ContentResolver; 26 import android.content.Context; 27 import android.content.DialogInterface; 28 import android.content.Intent; 29 import android.content.pm.PackageManager; 30 import android.content.pm.ResolveInfo; 31 import android.content.pm.ServiceInfo; 32 import android.graphics.drawable.Drawable; 33 import android.icu.text.CaseMap; 34 import android.net.Uri; 35 import android.os.Bundle; 36 import android.os.Handler; 37 import android.provider.Settings; 38 import android.service.quicksettings.TileService; 39 import android.text.Html; 40 import android.text.TextUtils; 41 import android.view.LayoutInflater; 42 import android.view.View; 43 import android.view.ViewGroup; 44 import android.view.accessibility.AccessibilityManager; 45 import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener; 46 import android.widget.CheckBox; 47 import android.widget.ImageView; 48 import android.widget.Switch; 49 50 import androidx.annotation.VisibleForTesting; 51 import androidx.preference.Preference; 52 import androidx.preference.PreferenceCategory; 53 import androidx.preference.PreferenceScreen; 54 55 import com.android.settings.R; 56 import com.android.settings.SettingsActivity; 57 import com.android.settings.accessibility.AccessibilityDialogUtils.DialogType; 58 import com.android.settings.accessibility.AccessibilityUtil.QuickSettingsTooltipType; 59 import com.android.settings.accessibility.AccessibilityUtil.UserShortcutType; 60 import com.android.settings.dashboard.DashboardFragment; 61 import com.android.settings.utils.LocaleUtils; 62 import com.android.settings.widget.SettingsMainSwitchBar; 63 import com.android.settings.widget.SettingsMainSwitchPreference; 64 import com.android.settingslib.widget.IllustrationPreference; 65 import com.android.settingslib.widget.OnMainSwitchChangeListener; 66 import com.android.settingslib.widget.TopIntroPreference; 67 68 import com.google.android.setupcompat.util.WizardManagerHelper; 69 70 import java.util.ArrayList; 71 import java.util.List; 72 import java.util.Locale; 73 74 /** 75 * Base class for accessibility fragments with toggle, shortcut, some helper functions 76 * and dialog management. 77 */ 78 public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment 79 implements ShortcutPreference.OnClickCallback, OnMainSwitchChangeListener { 80 81 public static final String KEY_GENERAL_CATEGORY = "general_categories"; 82 public static final String KEY_SHORTCUT_PREFERENCE = "shortcut_preference"; 83 public static final int NOT_SET = -1; 84 protected static final String KEY_TOP_INTRO_PREFERENCE = "top_intro"; 85 protected static final String KEY_USE_SERVICE_PREFERENCE = "use_service"; 86 protected static final String KEY_HTML_DESCRIPTION_PREFERENCE = "html_description"; 87 protected static final String KEY_SAVED_USER_SHORTCUT_TYPE = "shortcut_type"; 88 protected static final String KEY_SAVED_QS_TOOLTIP_RESHOW = "qs_tooltip_reshow"; 89 protected static final String KEY_SAVED_QS_TOOLTIP_TYPE = "qs_tooltip_type"; 90 protected static final String KEY_ANIMATED_IMAGE = "animated_image"; 91 // For html description of accessibility service, must follow the rule, such as 92 // <img src="R.drawable.fileName"/>, a11y settings will get the resources successfully. 93 private static final String IMG_PREFIX = "R.drawable."; 94 private static final String DRAWABLE_FOLDER = "drawable"; 95 96 protected TopIntroPreference mTopIntroPreference; 97 protected SettingsMainSwitchPreference mToggleServiceSwitchPreference; 98 protected ShortcutPreference mShortcutPreference; 99 protected Preference mSettingsPreference; 100 protected AccessibilityFooterPreferenceController mFooterPreferenceController; 101 protected String mPreferenceKey; 102 protected Dialog mDialog; 103 protected CharSequence mSettingsTitle; 104 protected Intent mSettingsIntent; 105 // The mComponentName maybe null, such as Magnify 106 protected ComponentName mComponentName; 107 protected CharSequence mPackageName; 108 protected Uri mImageUri; 109 protected CharSequence mHtmlDescription; 110 protected CharSequence mTopIntroTitle; 111 // Save user's shortcutType value when savedInstance has value (e.g. device rotated). 112 protected int mSavedCheckBoxValue = NOT_SET; 113 private CharSequence mDescription; 114 private TouchExplorationStateChangeListener mTouchExplorationStateChangeListener; 115 private AccessibilitySettingsContentObserver mSettingsContentObserver; 116 117 private CheckBox mSoftwareTypeCheckBox; 118 private CheckBox mHardwareTypeCheckBox; 119 120 private AccessibilityQuickSettingsTooltipWindow mTooltipWindow; 121 private boolean mNeedsQSTooltipReshow = false; 122 private int mNeedsQSTooltipType = QuickSettingsTooltipType.GUIDE_TO_EDIT; 123 private ImageView mImageGetterCacheView; 124 protected final Html.ImageGetter mImageGetter = (String str) -> { 125 if (str != null && str.startsWith(IMG_PREFIX)) { 126 final String fileName = str.substring(IMG_PREFIX.length()); 127 return getDrawableFromUri(Uri.parse( 128 ContentResolver.SCHEME_ANDROID_RESOURCE + "://" 129 + mComponentName.getPackageName() + "/" + DRAWABLE_FOLDER + "/" 130 + fileName)); 131 } 132 return null; 133 }; 134 135 @Override onCreate(Bundle savedInstanceState)136 public void onCreate(Bundle savedInstanceState) { 137 super.onCreate(savedInstanceState); 138 139 onProcessArguments(getArguments()); 140 // Restore the user shortcut type and tooltip. 141 if (savedInstanceState != null) { 142 if (savedInstanceState.containsKey(KEY_SAVED_USER_SHORTCUT_TYPE)) { 143 mSavedCheckBoxValue = savedInstanceState.getInt(KEY_SAVED_USER_SHORTCUT_TYPE, 144 NOT_SET); 145 } 146 if (savedInstanceState.containsKey(KEY_SAVED_QS_TOOLTIP_RESHOW)) { 147 mNeedsQSTooltipReshow = savedInstanceState.getBoolean(KEY_SAVED_QS_TOOLTIP_RESHOW); 148 } 149 if (savedInstanceState.containsKey(KEY_SAVED_QS_TOOLTIP_TYPE)) { 150 mNeedsQSTooltipType = savedInstanceState.getInt(KEY_SAVED_QS_TOOLTIP_TYPE); 151 } 152 } 153 154 final int resId = getPreferenceScreenResId(); 155 if (resId <= 0) { 156 final PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen( 157 getPrefContext()); 158 setPreferenceScreen(preferenceScreen); 159 } 160 161 mSettingsContentObserver = new AccessibilitySettingsContentObserver(new Handler()); 162 registerKeysToObserverCallback(mSettingsContentObserver); 163 } 164 registerKeysToObserverCallback( AccessibilitySettingsContentObserver contentObserver)165 protected void registerKeysToObserverCallback( 166 AccessibilitySettingsContentObserver contentObserver) { 167 final List<String> shortcutFeatureKeys = getShortcutFeatureSettingsKeys(); 168 169 contentObserver.registerKeysToObserverCallback(shortcutFeatureKeys, key -> { 170 updateShortcutPreferenceData(); 171 updateShortcutPreference(); 172 }); 173 } 174 getShortcutFeatureSettingsKeys()175 protected List<String> getShortcutFeatureSettingsKeys() { 176 final List<String> shortcutFeatureKeys = new ArrayList<>(); 177 shortcutFeatureKeys.add(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS); 178 shortcutFeatureKeys.add(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE); 179 return shortcutFeatureKeys; 180 } 181 182 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)183 public View onCreateView(LayoutInflater inflater, ViewGroup container, 184 Bundle savedInstanceState) { 185 initTopIntroPreference(); 186 initAnimatedImagePreference(); 187 initToggleServiceSwitchPreference(); 188 initGeneralCategory(); 189 initShortcutPreference(); 190 initSettingsPreference(); 191 initHtmlTextPreference(); 192 initFooterPreference(); 193 194 installActionBarToggleSwitch(); 195 196 updateToggleServiceTitle(mToggleServiceSwitchPreference); 197 198 mTouchExplorationStateChangeListener = isTouchExplorationEnabled -> { 199 removeDialog(DialogEnums.EDIT_SHORTCUT); 200 mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext())); 201 }; 202 203 updatePreferenceOrder(); 204 return super.onCreateView(inflater, container, savedInstanceState); 205 } 206 207 @Override onCreateDialog(int dialogId)208 public Dialog onCreateDialog(int dialogId) { 209 switch (dialogId) { 210 case DialogEnums.EDIT_SHORTCUT: 211 final int dialogType = WizardManagerHelper.isAnySetupWizard(getIntent()) 212 ? DialogType.EDIT_SHORTCUT_GENERIC_SUW : DialogType.EDIT_SHORTCUT_GENERIC; 213 mDialog = AccessibilityDialogUtils.showEditShortcutDialog( 214 getPrefContext(), dialogType, getShortcutTitle(), 215 this::callOnAlertDialogCheckboxClicked); 216 setupEditShortcutDialog(mDialog); 217 return mDialog; 218 case DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL: 219 if (WizardManagerHelper.isAnySetupWizard(getIntent())) { 220 mDialog = AccessibilityGestureNavigationTutorial 221 .createAccessibilityTutorialDialogForSetupWizard( 222 getPrefContext(), getUserShortcutTypes(), 223 this::callOnTutorialDialogButtonClicked); 224 } else { 225 mDialog = AccessibilityGestureNavigationTutorial 226 .createAccessibilityTutorialDialog( 227 getPrefContext(), getUserShortcutTypes(), 228 this::callOnTutorialDialogButtonClicked); 229 } 230 mDialog.setCanceledOnTouchOutside(false); 231 return mDialog; 232 default: 233 throw new IllegalArgumentException("Unsupported dialogId " + dialogId); 234 } 235 } 236 237 @Override onViewCreated(View view, Bundle savedInstanceState)238 public void onViewCreated(View view, Bundle savedInstanceState) { 239 super.onViewCreated(view, savedInstanceState); 240 241 final SettingsActivity settingsActivity = (SettingsActivity) getActivity(); 242 final SettingsMainSwitchBar switchBar = settingsActivity.getSwitchBar(); 243 switchBar.hide(); 244 245 // Reshow tooltip when activity recreate, such as rotate device. 246 if (mNeedsQSTooltipReshow) { 247 view.post(() -> { 248 final Activity activity = getActivity(); 249 if (activity != null && !activity.isFinishing()) { 250 showQuickSettingsTooltipIfNeeded(); 251 } 252 }); 253 } 254 255 writeConfigDefaultAccessibilityServiceIntoShortcutTargetServiceIfNeeded(getContext()); 256 } 257 258 @Override onResume()259 public void onResume() { 260 super.onResume(); 261 262 final AccessibilityManager am = getPrefContext().getSystemService( 263 AccessibilityManager.class); 264 am.addTouchExplorationStateChangeListener(mTouchExplorationStateChangeListener); 265 mSettingsContentObserver.register(getContentResolver()); 266 updateShortcutPreferenceData(); 267 updateShortcutPreference(); 268 269 updateEditShortcutDialogIfNeeded(); 270 } 271 272 @Override onPause()273 public void onPause() { 274 final AccessibilityManager am = getPrefContext().getSystemService( 275 AccessibilityManager.class); 276 am.removeTouchExplorationStateChangeListener(mTouchExplorationStateChangeListener); 277 mSettingsContentObserver.unregister(getContentResolver()); 278 super.onPause(); 279 } 280 281 @Override onSaveInstanceState(Bundle outState)282 public void onSaveInstanceState(Bundle outState) { 283 final int value = getShortcutTypeCheckBoxValue(); 284 if (value != NOT_SET) { 285 outState.putInt(KEY_SAVED_USER_SHORTCUT_TYPE, value); 286 } 287 final boolean isTooltipWindowShowing = mTooltipWindow != null && mTooltipWindow.isShowing(); 288 if (mNeedsQSTooltipReshow || isTooltipWindowShowing) { 289 outState.putBoolean(KEY_SAVED_QS_TOOLTIP_RESHOW, /* value= */ true); 290 outState.putInt(KEY_SAVED_QS_TOOLTIP_TYPE, mNeedsQSTooltipType); 291 } 292 super.onSaveInstanceState(outState); 293 } 294 295 @Override onDestroyView()296 public void onDestroyView() { 297 super.onDestroyView(); 298 removeActionBarToggleSwitch(); 299 final boolean isTooltipWindowShowing = mTooltipWindow != null && mTooltipWindow.isShowing(); 300 if (isTooltipWindowShowing) { 301 mTooltipWindow.dismiss(); 302 } 303 } 304 305 @Override getDialogMetricsCategory(int dialogId)306 public int getDialogMetricsCategory(int dialogId) { 307 switch (dialogId) { 308 case DialogEnums.EDIT_SHORTCUT: 309 return SettingsEnums.DIALOG_ACCESSIBILITY_SERVICE_EDIT_SHORTCUT; 310 case DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL: 311 return SettingsEnums.DIALOG_ACCESSIBILITY_TUTORIAL; 312 default: 313 return SettingsEnums.ACTION_UNKNOWN; 314 } 315 } 316 317 @Override getMetricsCategory()318 public int getMetricsCategory() { 319 return SettingsEnums.ACCESSIBILITY_SERVICE; 320 } 321 322 @Override getHelpResource()323 public int getHelpResource() { 324 return 0; 325 } 326 327 @Override onSwitchChanged(Switch switchView, boolean isChecked)328 public void onSwitchChanged(Switch switchView, boolean isChecked) { 329 onPreferenceToggled(mPreferenceKey, isChecked); 330 } 331 332 /** 333 * Returns the shortcut type list which has been checked by user. 334 */ getUserShortcutTypes()335 abstract int getUserShortcutTypes(); 336 337 /** Returns the accessibility tile component name. */ getTileComponentName()338 abstract ComponentName getTileComponentName(); 339 340 /** Returns the accessibility tile tooltip content. */ getTileTooltipContent(@uickSettingsTooltipType int type)341 abstract CharSequence getTileTooltipContent(@QuickSettingsTooltipType int type); 342 updateToggleServiceTitle(SettingsMainSwitchPreference switchPreference)343 protected void updateToggleServiceTitle(SettingsMainSwitchPreference switchPreference) { 344 final CharSequence title = 345 getString(R.string.accessibility_service_primary_switch_title, mPackageName); 346 switchPreference.setTitle(title); 347 } 348 getShortcutTitle()349 protected CharSequence getShortcutTitle() { 350 return getString(R.string.accessibility_shortcut_title, mPackageName); 351 } 352 onPreferenceToggled(String preferenceKey, boolean enabled)353 protected void onPreferenceToggled(String preferenceKey, boolean enabled) { 354 if (enabled) { 355 showQuickSettingsTooltipIfNeeded(); 356 } 357 } 358 onInstallSwitchPreferenceToggleSwitch()359 protected void onInstallSwitchPreferenceToggleSwitch() { 360 // Implement this to set a checked listener. 361 updateSwitchBarToggleSwitch(); 362 mToggleServiceSwitchPreference.addOnSwitchChangeListener(this); 363 } 364 onRemoveSwitchPreferenceToggleSwitch()365 protected void onRemoveSwitchPreferenceToggleSwitch() { 366 // Implement this to reset a checked listener. 367 } 368 updateSwitchBarToggleSwitch()369 protected void updateSwitchBarToggleSwitch() { 370 // Implement this to update the state of switch. 371 } 372 setTitle(String title)373 public void setTitle(String title) { 374 getActivity().setTitle(title); 375 } 376 onProcessArguments(Bundle arguments)377 protected void onProcessArguments(Bundle arguments) { 378 // Key. 379 mPreferenceKey = arguments.getString(AccessibilitySettings.EXTRA_PREFERENCE_KEY); 380 381 // Title. 382 if (arguments.containsKey(AccessibilitySettings.EXTRA_RESOLVE_INFO)) { 383 ResolveInfo info = arguments.getParcelable(AccessibilitySettings.EXTRA_RESOLVE_INFO); 384 getActivity().setTitle(info.loadLabel(getPackageManager()).toString()); 385 } else if (arguments.containsKey(AccessibilitySettings.EXTRA_TITLE)) { 386 setTitle(arguments.getString(AccessibilitySettings.EXTRA_TITLE)); 387 } 388 389 // Summary. 390 if (arguments.containsKey(AccessibilitySettings.EXTRA_SUMMARY)) { 391 mDescription = arguments.getCharSequence(AccessibilitySettings.EXTRA_SUMMARY); 392 } 393 394 // Settings html description. 395 if (arguments.containsKey(AccessibilitySettings.EXTRA_HTML_DESCRIPTION)) { 396 mHtmlDescription = arguments.getCharSequence( 397 AccessibilitySettings.EXTRA_HTML_DESCRIPTION); 398 } 399 400 // Intro. 401 if (arguments.containsKey(AccessibilitySettings.EXTRA_INTRO)) { 402 mTopIntroTitle = arguments.getCharSequence(AccessibilitySettings.EXTRA_INTRO); 403 } 404 } 405 installActionBarToggleSwitch()406 private void installActionBarToggleSwitch() { 407 onInstallSwitchPreferenceToggleSwitch(); 408 } 409 removeActionBarToggleSwitch()410 private void removeActionBarToggleSwitch() { 411 mToggleServiceSwitchPreference.setOnPreferenceClickListener(null); 412 onRemoveSwitchPreferenceToggleSwitch(); 413 } 414 updatePreferenceOrder()415 private void updatePreferenceOrder() { 416 final List<String> lists = getPreferenceOrderList(); 417 418 final PreferenceScreen preferenceScreen = getPreferenceScreen(); 419 preferenceScreen.setOrderingAsAdded(false); 420 421 final int size = lists.size(); 422 for (int i = 0; i < size; i++) { 423 final Preference preference = preferenceScreen.findPreference(lists.get(i)); 424 if (preference != null) { 425 preference.setOrder(i); 426 } 427 } 428 } 429 430 /** Customizes the order by preference key. */ getPreferenceOrderList()431 protected List<String> getPreferenceOrderList() { 432 final List<String> lists = new ArrayList<>(); 433 lists.add(KEY_TOP_INTRO_PREFERENCE); 434 lists.add(KEY_ANIMATED_IMAGE); 435 lists.add(KEY_USE_SERVICE_PREFERENCE); 436 lists.add(KEY_GENERAL_CATEGORY); 437 lists.add(KEY_HTML_DESCRIPTION_PREFERENCE); 438 return lists; 439 } 440 getDrawableFromUri(Uri imageUri)441 private Drawable getDrawableFromUri(Uri imageUri) { 442 if (mImageGetterCacheView == null) { 443 mImageGetterCacheView = new ImageView(getPrefContext()); 444 } 445 446 mImageGetterCacheView.setAdjustViewBounds(true); 447 mImageGetterCacheView.setImageURI(imageUri); 448 449 if (mImageGetterCacheView.getDrawable() == null) { 450 return null; 451 } 452 453 final Drawable drawable = 454 mImageGetterCacheView.getDrawable().mutate().getConstantState().newDrawable(); 455 mImageGetterCacheView.setImageURI(null); 456 final int imageWidth = drawable.getIntrinsicWidth(); 457 final int imageHeight = drawable.getIntrinsicHeight(); 458 final int screenHalfHeight = AccessibilityUtil.getScreenHeightPixels(getPrefContext()) / 2; 459 if ((imageWidth > AccessibilityUtil.getScreenWidthPixels(getPrefContext())) 460 || (imageHeight > screenHalfHeight)) { 461 return null; 462 } 463 464 drawable.setBounds(/* left= */0, /* top= */0, drawable.getIntrinsicWidth(), 465 drawable.getIntrinsicHeight()); 466 467 return drawable; 468 } 469 initAnimatedImagePreference()470 private void initAnimatedImagePreference() { 471 if (mImageUri == null) { 472 return; 473 } 474 475 final int displayHalfHeight = 476 AccessibilityUtil.getDisplayBounds(getPrefContext()).height() / 2; 477 final IllustrationPreference illustrationPreference = 478 new IllustrationPreference(getPrefContext()); 479 illustrationPreference.setImageUri(mImageUri); 480 illustrationPreference.setSelectable(false); 481 illustrationPreference.setMaxHeight(displayHalfHeight); 482 illustrationPreference.setKey(KEY_ANIMATED_IMAGE); 483 484 getPreferenceScreen().addPreference(illustrationPreference); 485 } 486 487 @VisibleForTesting initTopIntroPreference()488 void initTopIntroPreference() { 489 if (TextUtils.isEmpty(mTopIntroTitle)) { 490 return; 491 } 492 mTopIntroPreference = new TopIntroPreference(getPrefContext()); 493 mTopIntroPreference.setKey(KEY_TOP_INTRO_PREFERENCE); 494 mTopIntroPreference.setTitle(mTopIntroTitle); 495 getPreferenceScreen().addPreference(mTopIntroPreference); 496 } 497 initToggleServiceSwitchPreference()498 private void initToggleServiceSwitchPreference() { 499 mToggleServiceSwitchPreference = new SettingsMainSwitchPreference(getPrefContext()); 500 mToggleServiceSwitchPreference.setKey(KEY_USE_SERVICE_PREFERENCE); 501 if (getArguments().containsKey(AccessibilitySettings.EXTRA_CHECKED)) { 502 final boolean enabled = getArguments().getBoolean(AccessibilitySettings.EXTRA_CHECKED); 503 mToggleServiceSwitchPreference.setChecked(enabled); 504 } 505 506 getPreferenceScreen().addPreference(mToggleServiceSwitchPreference); 507 } 508 initGeneralCategory()509 private void initGeneralCategory() { 510 final PreferenceCategory generalCategory = new PreferenceCategory(getPrefContext()); 511 generalCategory.setKey(KEY_GENERAL_CATEGORY); 512 generalCategory.setTitle(R.string.accessibility_screen_option); 513 514 getPreferenceScreen().addPreference(generalCategory); 515 } 516 initShortcutPreference()517 protected void initShortcutPreference() { 518 // Initial the shortcut preference. 519 mShortcutPreference = new ShortcutPreference(getPrefContext(), /* attrs= */ null); 520 mShortcutPreference.setPersistent(false); 521 mShortcutPreference.setKey(getShortcutPreferenceKey()); 522 mShortcutPreference.setOnClickCallback(this); 523 mShortcutPreference.setTitle(getShortcutTitle()); 524 525 final PreferenceCategory generalCategory = findPreference(KEY_GENERAL_CATEGORY); 526 generalCategory.addPreference(mShortcutPreference); 527 } 528 initSettingsPreference()529 protected void initSettingsPreference() { 530 if (mSettingsTitle == null || mSettingsIntent == null) { 531 return; 532 } 533 534 // Show the "Settings" menu as if it were a preference screen. 535 mSettingsPreference = new Preference(getPrefContext()); 536 mSettingsPreference.setTitle(mSettingsTitle); 537 mSettingsPreference.setIconSpaceReserved(false); 538 mSettingsPreference.setIntent(mSettingsIntent); 539 540 final PreferenceCategory generalCategory = findPreference(KEY_GENERAL_CATEGORY); 541 generalCategory.addPreference(mSettingsPreference); 542 } 543 initHtmlTextPreference()544 private void initHtmlTextPreference() { 545 if (TextUtils.isEmpty(mHtmlDescription)) { 546 return; 547 } 548 final PreferenceScreen screen = getPreferenceScreen(); 549 final CharSequence htmlDescription = Html.fromHtml(mHtmlDescription.toString(), 550 Html.FROM_HTML_MODE_COMPACT, mImageGetter, /* tagHandler= */ null); 551 552 final AccessibilityFooterPreference htmlFooterPreference = 553 new AccessibilityFooterPreference(screen.getContext()); 554 htmlFooterPreference.setKey(KEY_HTML_DESCRIPTION_PREFERENCE); 555 htmlFooterPreference.setSummary(htmlDescription); 556 screen.addPreference(htmlFooterPreference); 557 558 // TODO(b/171272809): Migrate to DashboardFragment. 559 final String title = getString(R.string.accessibility_introduction_title, mPackageName); 560 mFooterPreferenceController = new AccessibilityFooterPreferenceController( 561 screen.getContext(), htmlFooterPreference.getKey()); 562 mFooterPreferenceController.setIntroductionTitle(title); 563 mFooterPreferenceController.displayPreference(screen); 564 } 565 initFooterPreference()566 private void initFooterPreference() { 567 if (!TextUtils.isEmpty(mDescription)) { 568 createFooterPreference(getPreferenceScreen(), mDescription, 569 getString(R.string.accessibility_introduction_title, mPackageName)); 570 } 571 } 572 573 574 /** 575 * Creates {@link AccessibilityFooterPreference} and append into {@link PreferenceScreen} 576 * 577 * @param screen The preference screen to add the footer preference 578 * @param summary The summary of the preference summary 579 * @param introductionTitle The title of introduction in the footer 580 */ 581 @VisibleForTesting createFooterPreference(PreferenceScreen screen, CharSequence summary, String introductionTitle)582 void createFooterPreference(PreferenceScreen screen, CharSequence summary, 583 String introductionTitle) { 584 final AccessibilityFooterPreference footerPreference = 585 new AccessibilityFooterPreference(screen.getContext()); 586 footerPreference.setSummary(summary); 587 screen.addPreference(footerPreference); 588 589 mFooterPreferenceController = new AccessibilityFooterPreferenceController( 590 screen.getContext(), footerPreference.getKey()); 591 mFooterPreferenceController.setIntroductionTitle(introductionTitle); 592 mFooterPreferenceController.displayPreference(screen); 593 } 594 595 @VisibleForTesting setupEditShortcutDialog(Dialog dialog)596 void setupEditShortcutDialog(Dialog dialog) { 597 final View dialogSoftwareView = dialog.findViewById(R.id.software_shortcut); 598 mSoftwareTypeCheckBox = dialogSoftwareView.findViewById(R.id.checkbox); 599 setDialogTextAreaClickListener(dialogSoftwareView, mSoftwareTypeCheckBox); 600 601 final View dialogHardwareView = dialog.findViewById(R.id.hardware_shortcut); 602 mHardwareTypeCheckBox = dialogHardwareView.findViewById(R.id.checkbox); 603 setDialogTextAreaClickListener(dialogHardwareView, mHardwareTypeCheckBox); 604 605 updateEditShortcutDialogCheckBox(); 606 } 607 setDialogTextAreaClickListener(View dialogView, CheckBox checkBox)608 private void setDialogTextAreaClickListener(View dialogView, CheckBox checkBox) { 609 final View dialogTextArea = dialogView.findViewById(R.id.container); 610 dialogTextArea.setOnClickListener(v -> checkBox.toggle()); 611 } 612 updateEditShortcutDialogCheckBox()613 private void updateEditShortcutDialogCheckBox() { 614 // If it is during onConfigChanged process then restore the value, or get the saved value 615 // when shortcutPreference is checked. 616 int value = restoreOnConfigChangedValue(); 617 if (value == NOT_SET) { 618 final int lastNonEmptyUserShortcutType = PreferredShortcuts.retrieveUserShortcutType( 619 getPrefContext(), mComponentName.flattenToString(), UserShortcutType.SOFTWARE); 620 value = mShortcutPreference.isChecked() ? lastNonEmptyUserShortcutType 621 : UserShortcutType.EMPTY; 622 } 623 624 mSoftwareTypeCheckBox.setChecked( 625 hasShortcutType(value, UserShortcutType.SOFTWARE)); 626 mHardwareTypeCheckBox.setChecked( 627 hasShortcutType(value, UserShortcutType.HARDWARE)); 628 } 629 restoreOnConfigChangedValue()630 private int restoreOnConfigChangedValue() { 631 final int savedValue = mSavedCheckBoxValue; 632 mSavedCheckBoxValue = NOT_SET; 633 return savedValue; 634 } 635 hasShortcutType(int value, @UserShortcutType int type)636 private boolean hasShortcutType(int value, @UserShortcutType int type) { 637 return (value & type) == type; 638 } 639 640 /** 641 * Returns accumulated {@link UserShortcutType} checkbox value or {@code NOT_SET} if checkboxes 642 * did not exist. 643 */ getShortcutTypeCheckBoxValue()644 protected int getShortcutTypeCheckBoxValue() { 645 if (mSoftwareTypeCheckBox == null || mHardwareTypeCheckBox == null) { 646 return NOT_SET; 647 } 648 649 int value = UserShortcutType.EMPTY; 650 if (mSoftwareTypeCheckBox.isChecked()) { 651 value |= UserShortcutType.SOFTWARE; 652 } 653 if (mHardwareTypeCheckBox.isChecked()) { 654 value |= UserShortcutType.HARDWARE; 655 } 656 return value; 657 } 658 getShortcutTypeSummary(Context context)659 protected CharSequence getShortcutTypeSummary(Context context) { 660 if (!mShortcutPreference.isSettingsEditable()) { 661 return context.getText(R.string.accessibility_shortcut_edit_dialog_title_hardware); 662 } 663 664 if (!mShortcutPreference.isChecked()) { 665 return context.getText(R.string.switch_off_text); 666 } 667 668 final int shortcutTypes = PreferredShortcuts.retrieveUserShortcutType(context, 669 mComponentName.flattenToString(), UserShortcutType.SOFTWARE); 670 671 final List<CharSequence> list = new ArrayList<>(); 672 if (hasShortcutType(shortcutTypes, UserShortcutType.SOFTWARE)) { 673 list.add(getSoftwareShortcutTypeSummary(context)); 674 } 675 if (hasShortcutType(shortcutTypes, UserShortcutType.HARDWARE)) { 676 final CharSequence hardwareTitle = context.getText( 677 R.string.accessibility_shortcut_hardware_keyword); 678 list.add(hardwareTitle); 679 } 680 681 // Show software shortcut if first time to use. 682 if (list.isEmpty()) { 683 list.add(getSoftwareShortcutTypeSummary(context)); 684 } 685 686 return CaseMap.toTitle().wholeString().noLowercase().apply(Locale.getDefault(), /* iter= */ 687 null, LocaleUtils.getConcatenatedString(list)); 688 } 689 getSoftwareShortcutTypeSummary(Context context)690 private static CharSequence getSoftwareShortcutTypeSummary(Context context) { 691 int resId; 692 if (AccessibilityUtil.isFloatingMenuEnabled(context)) { 693 resId = R.string.accessibility_shortcut_edit_summary_software; 694 } else if (AccessibilityUtil.isGestureNavigateEnabled(context)) { 695 resId = R.string.accessibility_shortcut_edit_summary_software_gesture; 696 } else { 697 resId = R.string.accessibility_shortcut_edit_summary_software; 698 } 699 return context.getText(resId); 700 } 701 702 /** 703 * This method will be invoked when a button in the tutorial dialog is clicked. 704 * 705 * @param dialog The dialog that received the click 706 * @param which The button that was clicked 707 */ callOnTutorialDialogButtonClicked(DialogInterface dialog, int which)708 private void callOnTutorialDialogButtonClicked(DialogInterface dialog, int which) { 709 dialog.dismiss(); 710 showQuickSettingsTooltipIfNeeded(); 711 } 712 713 /** 714 * This method will be invoked when a button in the edit shortcut dialog is clicked. 715 * 716 * @param dialog The dialog that received the click 717 * @param which The button that was clicked 718 */ callOnAlertDialogCheckboxClicked(DialogInterface dialog, int which)719 protected void callOnAlertDialogCheckboxClicked(DialogInterface dialog, int which) { 720 if (mComponentName == null) { 721 return; 722 } 723 724 final int value = getShortcutTypeCheckBoxValue(); 725 saveNonEmptyUserShortcutType(value); 726 AccessibilityUtil.optInAllValuesToSettings(getPrefContext(), value, mComponentName); 727 AccessibilityUtil.optOutAllValuesFromSettings(getPrefContext(), ~value, mComponentName); 728 final boolean shortcutAssigned = value != UserShortcutType.EMPTY; 729 mShortcutPreference.setChecked(shortcutAssigned); 730 mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext())); 731 732 if (mHardwareTypeCheckBox.isChecked()) { 733 AccessibilityUtil.skipVolumeShortcutDialogTimeoutRestriction(getPrefContext()); 734 } 735 736 // Show the quick setting tooltip if the shortcut assigned in the first time 737 if (shortcutAssigned) { 738 showQuickSettingsTooltipIfNeeded(); 739 } 740 } 741 updateShortcutPreferenceData()742 protected void updateShortcutPreferenceData() { 743 if (mComponentName == null) { 744 return; 745 } 746 747 final int shortcutTypes = AccessibilityUtil.getUserShortcutTypesFromSettings( 748 getPrefContext(), mComponentName); 749 if (shortcutTypes != UserShortcutType.EMPTY) { 750 final PreferredShortcut shortcut = new PreferredShortcut( 751 mComponentName.flattenToString(), shortcutTypes); 752 PreferredShortcuts.saveUserShortcutType(getPrefContext(), shortcut); 753 } 754 } 755 updateShortcutPreference()756 protected void updateShortcutPreference() { 757 if (mComponentName == null) { 758 return; 759 } 760 761 final int shortcutTypes = PreferredShortcuts.retrieveUserShortcutType(getPrefContext(), 762 mComponentName.flattenToString(), UserShortcutType.SOFTWARE); 763 mShortcutPreference.setChecked( 764 AccessibilityUtil.hasValuesInSettings(getPrefContext(), shortcutTypes, 765 mComponentName)); 766 mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext())); 767 } 768 getShortcutPreferenceKey()769 protected String getShortcutPreferenceKey() { 770 return KEY_SHORTCUT_PREFERENCE; 771 } 772 773 @Override onToggleClicked(ShortcutPreference preference)774 public void onToggleClicked(ShortcutPreference preference) { 775 if (mComponentName == null) { 776 return; 777 } 778 779 final int shortcutTypes = PreferredShortcuts.retrieveUserShortcutType(getPrefContext(), 780 mComponentName.flattenToString(), UserShortcutType.SOFTWARE); 781 if (preference.isChecked()) { 782 AccessibilityUtil.optInAllValuesToSettings(getPrefContext(), shortcutTypes, 783 mComponentName); 784 showDialog(DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL); 785 } else { 786 AccessibilityUtil.optOutAllValuesFromSettings(getPrefContext(), shortcutTypes, 787 mComponentName); 788 } 789 mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext())); 790 } 791 792 @Override onSettingsClicked(ShortcutPreference preference)793 public void onSettingsClicked(ShortcutPreference preference) { 794 showDialog(DialogEnums.EDIT_SHORTCUT); 795 } 796 797 /** 798 * Setups {@link com.android.internal.R.string#config_defaultAccessibilityService} into 799 * {@link Settings.Secure#ACCESSIBILITY_SHORTCUT_TARGET_SERVICE} if that settings key has never 800 * been set and only write the key when user enter into corresponding page. 801 */ 802 @VisibleForTesting writeConfigDefaultAccessibilityServiceIntoShortcutTargetServiceIfNeeded(Context context)803 void writeConfigDefaultAccessibilityServiceIntoShortcutTargetServiceIfNeeded(Context context) { 804 if (mComponentName == null) { 805 return; 806 } 807 808 // It might be shortened form (with a leading '.'). Need to unflatten back to ComponentName 809 // first, or it will encounter errors when getting service from 810 // `ACCESSIBILITY_SHORTCUT_TARGET_SERVICE`. 811 final ComponentName configDefaultService = ComponentName.unflattenFromString( 812 getString(com.android.internal.R.string.config_defaultAccessibilityService)); 813 814 if (!mComponentName.equals(configDefaultService)) { 815 return; 816 } 817 818 final String targetKey = Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE; 819 final String targetString = Settings.Secure.getString(context.getContentResolver(), 820 targetKey); 821 822 // By intentional, we only need to write the config string when the Settings key has never 823 // been set (== null). Empty string also means someone already wrote it before, so we need 824 // to respect the value. 825 if (targetString == null) { 826 Settings.Secure.putString(context.getContentResolver(), targetKey, 827 configDefaultService.flattenToString()); 828 } 829 } 830 updateEditShortcutDialogIfNeeded()831 private void updateEditShortcutDialogIfNeeded() { 832 if (mDialog == null || !mDialog.isShowing()) { 833 return; 834 } 835 AccessibilityDialogUtils.updateShortcutInDialog(getContext(), mDialog); 836 } 837 838 @VisibleForTesting saveNonEmptyUserShortcutType(int type)839 void saveNonEmptyUserShortcutType(int type) { 840 if (type == UserShortcutType.EMPTY) { 841 return; 842 } 843 844 final PreferredShortcut shortcut = new PreferredShortcut( 845 mComponentName.flattenToString(), type); 846 PreferredShortcuts.saveUserShortcutType(getPrefContext(), shortcut); 847 } 848 849 /** 850 * Shows the quick settings tooltip if the quick settings feature is assigned. The tooltip only 851 * shows once. 852 * 853 * @param type The quick settings tooltip type 854 */ showQuickSettingsTooltipIfNeeded(@uickSettingsTooltipType int type)855 protected void showQuickSettingsTooltipIfNeeded(@QuickSettingsTooltipType int type) { 856 mNeedsQSTooltipType = type; 857 showQuickSettingsTooltipIfNeeded(); 858 } 859 showQuickSettingsTooltipIfNeeded()860 private void showQuickSettingsTooltipIfNeeded() { 861 final ComponentName tileComponentName = getTileComponentName(); 862 if (tileComponentName == null) { 863 // Returns if no tile service assigned. 864 return; 865 } 866 867 if (!mNeedsQSTooltipReshow && AccessibilityQuickSettingUtils.hasValueInSharedPreferences( 868 getContext(), tileComponentName)) { 869 // Returns if quick settings tooltip only show once. 870 return; 871 } 872 873 final CharSequence content = getTileTooltipContent(mNeedsQSTooltipType); 874 if (TextUtils.isEmpty(content)) { 875 // Returns if no content of tile tooltip assigned. 876 return; 877 } 878 879 final int imageResId = mNeedsQSTooltipType == QuickSettingsTooltipType.GUIDE_TO_EDIT 880 ? R.drawable.accessibility_qs_tooltip_illustration 881 : R.drawable.accessibility_auto_added_qs_tooltip_illustration; 882 mTooltipWindow = new AccessibilityQuickSettingsTooltipWindow(getContext()); 883 mTooltipWindow.setup(content, imageResId); 884 mTooltipWindow.showAtTopCenter(getView()); 885 AccessibilityQuickSettingUtils.optInValueToSharedPreferences(getContext(), 886 tileComponentName); 887 mNeedsQSTooltipReshow = false; 888 } 889 890 /** Returns user visible name of the tile by given {@link ComponentName}. */ loadTileLabel(Context context, ComponentName componentName)891 protected CharSequence loadTileLabel(Context context, ComponentName componentName) { 892 final PackageManager packageManager = context.getPackageManager(); 893 final Intent queryIntent = new Intent(TileService.ACTION_QS_TILE); 894 final List<ResolveInfo> resolveInfos = 895 packageManager.queryIntentServices(queryIntent, PackageManager.GET_META_DATA); 896 for (ResolveInfo info : resolveInfos) { 897 final ServiceInfo serviceInfo = info.serviceInfo; 898 if (TextUtils.equals(componentName.getPackageName(), serviceInfo.packageName) 899 && TextUtils.equals(componentName.getClassName(), serviceInfo.name)) { 900 return serviceInfo.loadLabel(packageManager); 901 } 902 } 903 return null; 904 } 905 } 906