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