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