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