1 /* 2 * Copyright (C) 2021 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 import static com.android.settings.accessibility.ToggleFeaturePreferenceFragment.KEY_GENERAL_CATEGORY; 21 22 import android.app.Dialog; 23 import android.app.settings.SettingsEnums; 24 import android.content.ComponentName; 25 import android.content.Context; 26 import android.content.DialogInterface; 27 import android.icu.text.CaseMap; 28 import android.net.Uri; 29 import android.os.Bundle; 30 import android.os.Handler; 31 import android.provider.Settings; 32 import android.view.LayoutInflater; 33 import android.view.View; 34 import android.view.ViewGroup; 35 import android.view.accessibility.AccessibilityManager; 36 import android.widget.CheckBox; 37 38 import androidx.annotation.Nullable; 39 import androidx.annotation.VisibleForTesting; 40 import androidx.preference.PreferenceCategory; 41 import androidx.preference.PreferenceScreen; 42 43 import com.android.settings.R; 44 import com.android.settings.dashboard.DashboardFragment; 45 import com.android.settings.utils.LocaleUtils; 46 47 import com.google.android.setupcompat.util.WizardManagerHelper; 48 49 import java.util.ArrayList; 50 import java.util.List; 51 import java.util.Locale; 52 53 /** 54 * Base class for accessibility fragments shortcut functions and dialog management. 55 */ 56 public abstract class AccessibilityShortcutPreferenceFragment extends DashboardFragment 57 implements ShortcutPreference.OnClickCallback { 58 private static final String KEY_SHORTCUT_PREFERENCE = "shortcut_preference"; 59 protected static final String KEY_SAVED_USER_SHORTCUT_TYPE = "shortcut_type"; 60 protected static final int NOT_SET = -1; 61 // Save user's shortcutType value when savedInstance has value (e.g. device rotated). 62 protected int mSavedCheckBoxValue = NOT_SET; 63 64 protected ShortcutPreference mShortcutPreference; 65 private AccessibilityManager.TouchExplorationStateChangeListener 66 mTouchExplorationStateChangeListener; 67 private SettingsContentObserver mSettingsContentObserver; 68 private CheckBox mSoftwareTypeCheckBox; 69 private CheckBox mHardwareTypeCheckBox; 70 71 /** Returns the accessibility component name. */ getComponentName()72 protected abstract ComponentName getComponentName(); 73 74 /** Returns the accessibility feature name. */ getLabelName()75 protected abstract CharSequence getLabelName(); 76 77 @Override onCreate(Bundle savedInstanceState)78 public void onCreate(Bundle savedInstanceState) { 79 super.onCreate(savedInstanceState); 80 81 // Restore the user shortcut type. 82 if (savedInstanceState != null && savedInstanceState.containsKey( 83 KEY_SAVED_USER_SHORTCUT_TYPE)) { 84 mSavedCheckBoxValue = savedInstanceState.getInt(KEY_SAVED_USER_SHORTCUT_TYPE, NOT_SET); 85 } 86 87 final int resId = getPreferenceScreenResId(); 88 if (resId <= 0) { 89 final PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen( 90 getPrefContext()); 91 setPreferenceScreen(preferenceScreen); 92 } 93 94 if (showGeneralCategory()) { 95 initGeneralCategory(); 96 } 97 98 final List<String> shortcutFeatureKeys = new ArrayList<>(); 99 shortcutFeatureKeys.add(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS); 100 shortcutFeatureKeys.add(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE); 101 mSettingsContentObserver = new SettingsContentObserver(new Handler(), shortcutFeatureKeys) { 102 @Override 103 public void onChange(boolean selfChange, Uri uri) { 104 updateShortcutPreferenceData(); 105 updateShortcutPreference(); 106 } 107 }; 108 } 109 110 @Override onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)111 public View onCreateView(LayoutInflater inflater, ViewGroup container, 112 Bundle savedInstanceState) { 113 mShortcutPreference = new ShortcutPreference(getPrefContext(), /* attrs= */ null); 114 mShortcutPreference.setPersistent(false); 115 mShortcutPreference.setKey(getShortcutPreferenceKey()); 116 mShortcutPreference.setOnClickCallback(this); 117 118 updateShortcutTitle(mShortcutPreference); 119 getPreferenceScreen().addPreference(mShortcutPreference); 120 121 mTouchExplorationStateChangeListener = isTouchExplorationEnabled -> { 122 removeDialog(DialogEnums.EDIT_SHORTCUT); 123 mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext())); 124 }; 125 126 return super.onCreateView(inflater, container, savedInstanceState); 127 } 128 129 @Override onResume()130 public void onResume() { 131 super.onResume(); 132 final AccessibilityManager am = getPrefContext().getSystemService( 133 AccessibilityManager.class); 134 am.addTouchExplorationStateChangeListener(mTouchExplorationStateChangeListener); 135 mSettingsContentObserver.register(getContentResolver()); 136 updateShortcutPreferenceData(); 137 updateShortcutPreference(); 138 } 139 140 @Override onPause()141 public void onPause() { 142 final AccessibilityManager am = getPrefContext().getSystemService( 143 AccessibilityManager.class); 144 am.removeTouchExplorationStateChangeListener(mTouchExplorationStateChangeListener); 145 mSettingsContentObserver.unregister(getContentResolver()); 146 super.onPause(); 147 } 148 149 @Override onSaveInstanceState(Bundle outState)150 public void onSaveInstanceState(Bundle outState) { 151 final int value = getShortcutTypeCheckBoxValue(); 152 if (value != NOT_SET) { 153 outState.putInt(KEY_SAVED_USER_SHORTCUT_TYPE, value); 154 } 155 super.onSaveInstanceState(outState); 156 } 157 158 @Override onCreateDialog(int dialogId)159 public Dialog onCreateDialog(int dialogId) { 160 final Dialog dialog; 161 switch (dialogId) { 162 case DialogEnums.EDIT_SHORTCUT: 163 final CharSequence dialogTitle = getPrefContext().getString( 164 R.string.accessibility_shortcut_title, getLabelName()); 165 final int dialogType = WizardManagerHelper.isAnySetupWizard(getIntent()) 166 ? AccessibilityDialogUtils.DialogType.EDIT_SHORTCUT_GENERIC_SUW : 167 AccessibilityDialogUtils.DialogType.EDIT_SHORTCUT_GENERIC; 168 dialog = AccessibilityDialogUtils.showEditShortcutDialog( 169 getPrefContext(), dialogType, dialogTitle, 170 this::callOnAlertDialogCheckboxClicked); 171 setupEditShortcutDialog(dialog); 172 return dialog; 173 case DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL: 174 dialog = AccessibilityGestureNavigationTutorial 175 .createAccessibilityTutorialDialog(getPrefContext(), 176 getUserShortcutTypes()); 177 dialog.setCanceledOnTouchOutside(false); 178 return dialog; 179 default: 180 throw new IllegalArgumentException("Unsupported dialogId " + dialogId); 181 } 182 } 183 updateShortcutTitle(ShortcutPreference shortcutPreference)184 protected void updateShortcutTitle(ShortcutPreference shortcutPreference) { 185 final CharSequence title = getString(R.string.accessibility_shortcut_title, getLabelName()); 186 shortcutPreference.setTitle(title); 187 } 188 189 @Override getDialogMetricsCategory(int dialogId)190 public int getDialogMetricsCategory(int dialogId) { 191 switch (dialogId) { 192 case DialogEnums.EDIT_SHORTCUT: 193 return SettingsEnums.DIALOG_ACCESSIBILITY_SERVICE_EDIT_SHORTCUT; 194 case DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL: 195 return SettingsEnums.DIALOG_ACCESSIBILITY_TUTORIAL; 196 default: 197 return SettingsEnums.ACTION_UNKNOWN; 198 } 199 } 200 201 @Override onSettingsClicked(ShortcutPreference preference)202 public void onSettingsClicked(ShortcutPreference preference) { 203 showDialog(DialogEnums.EDIT_SHORTCUT); 204 } 205 206 @Override onToggleClicked(ShortcutPreference preference)207 public void onToggleClicked(ShortcutPreference preference) { 208 if (getComponentName() == null) { 209 return; 210 } 211 212 final int shortcutTypes = PreferredShortcuts.retrieveUserShortcutType(getPrefContext(), 213 getComponentName().flattenToString(), AccessibilityUtil.UserShortcutType.SOFTWARE); 214 if (preference.isChecked()) { 215 AccessibilityUtil.optInAllValuesToSettings(getPrefContext(), shortcutTypes, 216 getComponentName()); 217 showDialog(DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL); 218 } else { 219 AccessibilityUtil.optOutAllValuesFromSettings(getPrefContext(), shortcutTypes, 220 getComponentName()); 221 } 222 mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext())); 223 } 224 225 /** 226 * Overrides to return specific shortcut preference key 227 * 228 * @return String The specific shortcut preference key 229 */ getShortcutPreferenceKey()230 protected String getShortcutPreferenceKey() { 231 return KEY_SHORTCUT_PREFERENCE; 232 } 233 234 @VisibleForTesting setupEditShortcutDialog(Dialog dialog)235 void setupEditShortcutDialog(Dialog dialog) { 236 final View dialogSoftwareView = dialog.findViewById(R.id.software_shortcut); 237 mSoftwareTypeCheckBox = dialogSoftwareView.findViewById(R.id.checkbox); 238 setDialogTextAreaClickListener(dialogSoftwareView, mSoftwareTypeCheckBox); 239 240 final View dialogHardwareView = dialog.findViewById(R.id.hardware_shortcut); 241 mHardwareTypeCheckBox = dialogHardwareView.findViewById(R.id.checkbox); 242 setDialogTextAreaClickListener(dialogHardwareView, mHardwareTypeCheckBox); 243 244 updateEditShortcutDialogCheckBox(); 245 } 246 247 /** 248 * Returns accumulated {@link AccessibilityUtil.UserShortcutType} checkbox value or 249 * {@code NOT_SET} if checkboxes did not exist. 250 */ getShortcutTypeCheckBoxValue()251 protected int getShortcutTypeCheckBoxValue() { 252 if (mSoftwareTypeCheckBox == null || mHardwareTypeCheckBox == null) { 253 return NOT_SET; 254 } 255 256 int value = AccessibilityUtil.UserShortcutType.EMPTY; 257 if (mSoftwareTypeCheckBox.isChecked()) { 258 value |= AccessibilityUtil.UserShortcutType.SOFTWARE; 259 } 260 if (mHardwareTypeCheckBox.isChecked()) { 261 value |= AccessibilityUtil.UserShortcutType.HARDWARE; 262 } 263 return value; 264 } 265 266 /** 267 * Returns the shortcut type list which has been checked by user. 268 */ getUserShortcutTypes()269 protected int getUserShortcutTypes() { 270 return AccessibilityUtil.getUserShortcutTypesFromSettings(getPrefContext(), 271 getComponentName()); 272 }; 273 274 /** 275 * This method will be invoked when a button in the edit shortcut dialog is clicked. 276 * 277 * @param dialog The dialog that received the click 278 * @param which The button that was clicked 279 */ callOnAlertDialogCheckboxClicked(DialogInterface dialog, int which)280 protected void callOnAlertDialogCheckboxClicked(DialogInterface dialog, int which) { 281 if (getComponentName() == null) { 282 return; 283 } 284 285 final int value = getShortcutTypeCheckBoxValue(); 286 287 saveNonEmptyUserShortcutType(value); 288 AccessibilityUtil.optInAllValuesToSettings(getPrefContext(), value, getComponentName()); 289 AccessibilityUtil.optOutAllValuesFromSettings(getPrefContext(), ~value, getComponentName()); 290 mShortcutPreference.setChecked(value != AccessibilityUtil.UserShortcutType.EMPTY); 291 mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext())); 292 } 293 294 @VisibleForTesting initGeneralCategory()295 void initGeneralCategory() { 296 final PreferenceCategory generalCategory = new PreferenceCategory(getPrefContext()); 297 generalCategory.setKey(KEY_GENERAL_CATEGORY); 298 generalCategory.setTitle(getGeneralCategoryDescription(null)); 299 300 getPreferenceScreen().addPreference(generalCategory); 301 } 302 303 @VisibleForTesting saveNonEmptyUserShortcutType(int type)304 void saveNonEmptyUserShortcutType(int type) { 305 if (type == AccessibilityUtil.UserShortcutType.EMPTY) { 306 return; 307 } 308 309 final PreferredShortcut shortcut = new PreferredShortcut( 310 getComponentName().flattenToString(), type); 311 PreferredShortcuts.saveUserShortcutType(getPrefContext(), shortcut); 312 } 313 314 /** 315 * Overrides to return customized description for general category above shortcut 316 * 317 * @return CharSequence The customized description for general category 318 */ getGeneralCategoryDescription(@ullable CharSequence title)319 protected CharSequence getGeneralCategoryDescription(@Nullable CharSequence title) { 320 if (title == null || title.toString().isEmpty()) { 321 // Return default 'Options' string for category 322 return getContext().getString(R.string.accessibility_screen_option); 323 } 324 return title; 325 } 326 327 /** 328 * Overrides to determinate if showing additional category description above shortcut 329 * 330 * @return boolean true to show category, false otherwise. 331 */ showGeneralCategory()332 protected boolean showGeneralCategory() { 333 return false; 334 } 335 setDialogTextAreaClickListener(View dialogView, CheckBox checkBox)336 private void setDialogTextAreaClickListener(View dialogView, CheckBox checkBox) { 337 final View dialogTextArea = dialogView.findViewById(R.id.container); 338 dialogTextArea.setOnClickListener(v -> checkBox.toggle()); 339 } 340 getShortcutTypeSummary(Context context)341 protected CharSequence getShortcutTypeSummary(Context context) { 342 if (!mShortcutPreference.isSettingsEditable()) { 343 return context.getText(R.string.accessibility_shortcut_edit_dialog_title_hardware); 344 } 345 346 if (!mShortcutPreference.isChecked()) { 347 return context.getText(R.string.switch_off_text); 348 } 349 350 final int shortcutTypes = PreferredShortcuts.retrieveUserShortcutType(context, 351 getComponentName().flattenToString(), AccessibilityUtil.UserShortcutType.SOFTWARE); 352 353 final List<CharSequence> list = new ArrayList<>(); 354 final CharSequence softwareTitle = context.getText( 355 R.string.accessibility_shortcut_edit_summary_software); 356 357 if (hasShortcutType(shortcutTypes, AccessibilityUtil.UserShortcutType.SOFTWARE)) { 358 list.add(softwareTitle); 359 } 360 if (hasShortcutType(shortcutTypes, AccessibilityUtil.UserShortcutType.HARDWARE)) { 361 final CharSequence hardwareTitle = context.getText( 362 R.string.accessibility_shortcut_hardware_keyword); 363 list.add(hardwareTitle); 364 } 365 366 // Show software shortcut if first time to use. 367 if (list.isEmpty()) { 368 list.add(softwareTitle); 369 } 370 371 return CaseMap.toTitle().wholeString().noLowercase().apply(Locale.getDefault(), /* iter= */ 372 null, LocaleUtils.getConcatenatedString(list)); 373 } 374 updateEditShortcutDialogCheckBox()375 private void updateEditShortcutDialogCheckBox() { 376 // If it is during onConfigChanged process then restore the value, or get the saved value 377 // when shortcutPreference is checked. 378 int value = restoreOnConfigChangedValue(); 379 if (value == NOT_SET) { 380 final int lastNonEmptyUserShortcutType = PreferredShortcuts.retrieveUserShortcutType( 381 getPrefContext(), getComponentName().flattenToString(), 382 AccessibilityUtil.UserShortcutType.SOFTWARE); 383 value = mShortcutPreference.isChecked() ? lastNonEmptyUserShortcutType 384 : AccessibilityUtil.UserShortcutType.EMPTY; 385 } 386 387 mSoftwareTypeCheckBox.setChecked( 388 hasShortcutType(value, AccessibilityUtil.UserShortcutType.SOFTWARE)); 389 mHardwareTypeCheckBox.setChecked( 390 hasShortcutType(value, AccessibilityUtil.UserShortcutType.HARDWARE)); 391 } 392 restoreOnConfigChangedValue()393 private int restoreOnConfigChangedValue() { 394 final int savedValue = mSavedCheckBoxValue; 395 mSavedCheckBoxValue = NOT_SET; 396 return savedValue; 397 } 398 hasShortcutType(int value, @AccessibilityUtil.UserShortcutType int type)399 private boolean hasShortcutType(int value, @AccessibilityUtil.UserShortcutType int type) { 400 return (value & type) == type; 401 } 402 updateShortcutPreferenceData()403 protected void updateShortcutPreferenceData() { 404 if (getComponentName() == null) { 405 return; 406 } 407 408 final int shortcutTypes = AccessibilityUtil.getUserShortcutTypesFromSettings( 409 getPrefContext(), getComponentName()); 410 if (shortcutTypes != AccessibilityUtil.UserShortcutType.EMPTY) { 411 final PreferredShortcut shortcut = new PreferredShortcut( 412 getComponentName().flattenToString(), shortcutTypes); 413 PreferredShortcuts.saveUserShortcutType(getPrefContext(), shortcut); 414 } 415 } 416 updateShortcutPreference()417 protected void updateShortcutPreference() { 418 if (getComponentName() == null) { 419 return; 420 } 421 422 final int shortcutTypes = PreferredShortcuts.retrieveUserShortcutType(getPrefContext(), 423 getComponentName().flattenToString(), AccessibilityUtil.UserShortcutType.SOFTWARE); 424 mShortcutPreference.setChecked( 425 AccessibilityUtil.hasValuesInSettings(getPrefContext(), shortcutTypes, 426 getComponentName())); 427 mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext())); 428 } 429 } 430