1 /* 2 * Copyright (C) 2023 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.shortcuts; 18 19 import static android.app.Activity.RESULT_CANCELED; 20 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE; 21 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS; 22 import static android.provider.Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED; 23 import static android.provider.Settings.Secure.ACCESSIBILITY_GESTURE_TARGETS; 24 import static android.provider.Settings.Secure.ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED; 25 import static android.provider.Settings.Secure.ACCESSIBILITY_QS_TARGETS; 26 import static android.provider.Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE; 27 28 import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_COMPONENT_NAME; 29 import static com.android.internal.accessibility.AccessibilityShortcutController.MAGNIFICATION_CONTROLLER_NAME; 30 import static com.android.settings.SettingsActivity.EXTRA_SHOW_FRAGMENT_TITLE; 31 32 import android.app.Activity; 33 import android.app.settings.SettingsEnums; 34 import android.content.ComponentName; 35 import android.content.Context; 36 import android.content.Intent; 37 import android.content.res.Resources; 38 import android.database.ContentObserver; 39 import android.icu.text.ListFormatter; 40 import android.net.Uri; 41 import android.os.Bundle; 42 import android.os.Handler; 43 import android.provider.Settings; 44 import android.text.TextUtils; 45 import android.util.ArrayMap; 46 import android.util.Pair; 47 import android.view.LayoutInflater; 48 import android.view.View; 49 import android.view.ViewGroup; 50 import android.view.accessibility.AccessibilityManager; 51 52 import androidx.annotation.NonNull; 53 import androidx.annotation.Nullable; 54 import androidx.annotation.VisibleForTesting; 55 import androidx.preference.Preference; 56 import androidx.preference.PreferenceScreen; 57 import androidx.recyclerview.widget.RecyclerView; 58 59 import com.android.internal.accessibility.common.ShortcutConstants; 60 import com.android.internal.accessibility.dialog.AccessibilityTarget; 61 import com.android.internal.accessibility.dialog.AccessibilityTargetHelper; 62 import com.android.settings.R; 63 import com.android.settings.SetupWizardUtils; 64 import com.android.settings.accessibility.AccessibilitySetupWizardUtils; 65 import com.android.settings.accessibility.Flags; 66 import com.android.settings.accessibility.PreferenceAdapterInSuw; 67 import com.android.settings.accessibility.PreferredShortcuts; 68 import com.android.settings.core.SubSettingLauncher; 69 import com.android.settings.dashboard.DashboardFragment; 70 import com.android.settingslib.core.AbstractPreferenceController; 71 72 import com.google.android.setupcompat.template.FooterBarMixin; 73 import com.google.android.setupcompat.util.WizardManagerHelper; 74 import com.google.android.setupdesign.GlifPreferenceLayout; 75 import com.google.android.setupdesign.util.ThemeHelper; 76 77 import java.util.ArrayList; 78 import java.util.Collection; 79 import java.util.List; 80 import java.util.Map; 81 import java.util.Set; 82 83 /** 84 * A screen show various accessibility shortcut options for the given a11y feature 85 */ 86 public class EditShortcutsPreferenceFragment extends DashboardFragment { 87 private static final String TAG = "EditShortcutsPreferenceFragment"; 88 89 @VisibleForTesting 90 static final String ARG_KEY_SHORTCUT_TARGETS = "targets"; 91 @VisibleForTesting 92 static final String SAVED_STATE_IS_EXPANDED = "isExpanded"; 93 private ContentObserver mSettingsObserver; 94 95 private static final Uri VOLUME_KEYS_SHORTCUT_SETTING = 96 Settings.Secure.getUriFor(ACCESSIBILITY_SHORTCUT_TARGET_SERVICE); 97 private static final Uri BUTTON_SHORTCUT_MODE_SETTING = 98 Settings.Secure.getUriFor(ACCESSIBILITY_BUTTON_MODE); 99 private static final Uri BUTTON_SHORTCUT_SETTING = 100 Settings.Secure.getUriFor(ACCESSIBILITY_BUTTON_TARGETS); 101 private static final Uri GESTURE_SHORTCUT_SETTING = 102 Settings.Secure.getUriFor(ACCESSIBILITY_GESTURE_TARGETS); 103 private static final Uri TRIPLE_TAP_SHORTCUT_SETTING = 104 Settings.Secure.getUriFor(ACCESSIBILITY_DISPLAY_MAGNIFICATION_ENABLED); 105 private static final Uri TWO_FINGERS_DOUBLE_TAP_SHORTCUT_SETTING = 106 Settings.Secure.getUriFor(ACCESSIBILITY_MAGNIFICATION_TWO_FINGER_TRIPLE_TAP_ENABLED); 107 108 private static final Uri QUICK_SETTINGS_SHORTCUT_SETTING = 109 Settings.Secure.getUriFor(ACCESSIBILITY_QS_TARGETS); 110 111 @VisibleForTesting 112 static final Uri[] SHORTCUT_SETTINGS = { 113 VOLUME_KEYS_SHORTCUT_SETTING, 114 BUTTON_SHORTCUT_MODE_SETTING, 115 BUTTON_SHORTCUT_SETTING, 116 GESTURE_SHORTCUT_SETTING, 117 TRIPLE_TAP_SHORTCUT_SETTING, 118 TWO_FINGERS_DOUBLE_TAP_SHORTCUT_SETTING, 119 QUICK_SETTINGS_SHORTCUT_SETTING, 120 }; 121 122 private Set<String> mShortcutTargets; 123 124 @Nullable 125 private AccessibilityManager.TouchExplorationStateChangeListener 126 mTouchExplorationStateChangeListener; 127 128 129 /** 130 * Helper method to show the edit shortcut screen 131 */ showEditShortcutScreen( Context context, int metricsCategory, CharSequence screenTitle, ComponentName target, Intent fromIntent)132 public static void showEditShortcutScreen( 133 Context context, int metricsCategory, CharSequence screenTitle, 134 ComponentName target, Intent fromIntent) { 135 Bundle args = new Bundle(); 136 137 if (MAGNIFICATION_COMPONENT_NAME.equals(target)) { 138 // We can remove this branch once b/147990389 is completed 139 args.putStringArray( 140 ARG_KEY_SHORTCUT_TARGETS, new String[]{MAGNIFICATION_CONTROLLER_NAME}); 141 } else { 142 args.putStringArray( 143 ARG_KEY_SHORTCUT_TARGETS, new String[]{target.flattenToString()}); 144 } 145 Intent toIntent = new Intent(); 146 if (fromIntent != null) { 147 SetupWizardUtils.copySetupExtras(fromIntent, toIntent); 148 } 149 150 new SubSettingLauncher(context) 151 .setDestination(EditShortcutsPreferenceFragment.class.getName()) 152 .setExtras(toIntent.getExtras()) 153 .setArguments(args) 154 .setSourceMetricsCategory(metricsCategory) 155 .setTitleText(screenTitle) 156 .launch(); 157 } 158 159 @Override onAttach(Context context)160 public void onAttach(Context context) { 161 super.onAttach(context); 162 initializeArguments(); 163 initializePreferenceControllerArguments(); 164 } 165 166 @Override onCreate(Bundle savedInstanceState)167 public void onCreate(Bundle savedInstanceState) { 168 super.onCreate(savedInstanceState); 169 if (savedInstanceState != null) { 170 boolean isExpanded = savedInstanceState.getBoolean(SAVED_STATE_IS_EXPANDED); 171 if (isExpanded) { 172 onExpanded(); 173 } 174 } 175 mSettingsObserver = new ContentObserver(new Handler()) { 176 @Override 177 public void onChange(boolean selfChange, Uri uri) { 178 if (VOLUME_KEYS_SHORTCUT_SETTING.equals(uri)) { 179 refreshPreferenceController(VolumeKeysShortcutOptionController.class); 180 } else if (BUTTON_SHORTCUT_MODE_SETTING.equals(uri) 181 || BUTTON_SHORTCUT_SETTING.equals(uri)) { 182 refreshSoftwareShortcutControllers(); 183 } else if (GESTURE_SHORTCUT_SETTING.equals(uri)) { 184 refreshPreferenceController(GestureShortcutOptionController.class); 185 } else if (TRIPLE_TAP_SHORTCUT_SETTING.equals(uri)) { 186 refreshPreferenceController(TripleTapShortcutOptionController.class); 187 } else if (TWO_FINGERS_DOUBLE_TAP_SHORTCUT_SETTING.equals(uri)) { 188 refreshPreferenceController(TwoFingerDoubleTapShortcutOptionController.class); 189 } else if (QUICK_SETTINGS_SHORTCUT_SETTING.equals(uri)) { 190 refreshPreferenceController(QuickSettingsShortcutOptionController.class); 191 } 192 193 if (getContext() != null) { 194 PreferredShortcuts.updatePreferredShortcutsFromSettings( 195 getContext(), mShortcutTargets); 196 } 197 } 198 }; 199 200 registerSettingsObserver(); 201 } 202 203 @Override onCreatePreferences(Bundle savedInstanceState, String rootKey)204 public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { 205 super.onCreatePreferences(savedInstanceState, rootKey); 206 207 Activity activity = getActivity(); 208 final Preference descriptionPref = findPreference(getString( 209 R.string.accessibility_shortcut_description_pref)); 210 211 if (!activity.getIntent().getAction().equals( 212 Settings.ACTION_ACCESSIBILITY_SHORTCUT_SETTINGS)) { 213 if (Flags.toggleFeatureFragmentCollectionInfo()) { 214 descriptionPref.setVisible(false); 215 } 216 return; 217 } 218 219 // TODO(b/325664350): Implement shortcut type for "all shortcuts" 220 List<AccessibilityTarget> accessibilityTargets = 221 AccessibilityTargetHelper.getInstalledTargets( 222 activity.getBaseContext(), ShortcutConstants.UserShortcutType.HARDWARE); 223 224 Pair<String, String> titles = getTitlesFromAccessibilityTargetList( 225 mShortcutTargets, 226 accessibilityTargets, 227 activity.getResources() 228 ); 229 230 activity.setTitle(titles.first); 231 if (titles.second != null || !Flags.toggleFeatureFragmentCollectionInfo()) { 232 descriptionPref.setSummary(titles.second); 233 } else { 234 descriptionPref.setVisible(false); 235 } 236 } 237 238 @NonNull 239 @Override onCreateRecyclerView( @onNull LayoutInflater inflater, @NonNull ViewGroup parent, @Nullable Bundle savedInstanceState)240 public RecyclerView onCreateRecyclerView( 241 @NonNull LayoutInflater inflater, @NonNull ViewGroup parent, 242 @Nullable Bundle savedInstanceState) { 243 if (parent instanceof GlifPreferenceLayout layout) { 244 // Usually for setup wizard 245 return layout.onCreateRecyclerView(inflater, parent, savedInstanceState); 246 } else { 247 return super.onCreateRecyclerView(inflater, parent, savedInstanceState); 248 } 249 } 250 251 @Override onCreateAdapter(PreferenceScreen preferenceScreen)252 protected RecyclerView.Adapter onCreateAdapter(PreferenceScreen preferenceScreen) { 253 if (ThemeHelper.shouldApplyGlifExpressiveStyle(getContext()) 254 && WizardManagerHelper.isAnySetupWizard(getIntent())) { 255 return new PreferenceAdapterInSuw(preferenceScreen); 256 } 257 return super.onCreateAdapter(preferenceScreen); 258 } 259 260 @Override onViewCreated(@onNull View view, @Nullable Bundle savedInstanceState)261 public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { 262 super.onViewCreated(view, savedInstanceState); 263 264 if (view instanceof GlifPreferenceLayout layout) { 265 // Usually for setup wizard 266 String title = null; 267 Intent intent = getIntent(); 268 if (intent != null) { 269 title = intent.getStringExtra(EXTRA_SHOW_FRAGMENT_TITLE); 270 } 271 AccessibilitySetupWizardUtils.updateGlifPreferenceLayout(getContext(), layout, title, 272 /* description= */ null, /* icon= */ null); 273 274 FooterBarMixin mixin = layout.getMixin(FooterBarMixin.class); 275 AccessibilitySetupWizardUtils.setPrimaryButton(getContext(), mixin, R.string.done, 276 () -> { 277 setResult(RESULT_CANCELED); 278 finish(); 279 }); 280 } 281 } 282 283 @Override onResume()284 public void onResume() { 285 super.onResume(); 286 mTouchExplorationStateChangeListener = isTouchExplorationEnabled -> { 287 refreshPreferenceController(QuickSettingsShortcutOptionController.class); 288 refreshPreferenceController(GestureShortcutOptionController.class); 289 }; 290 291 final AccessibilityManager am = getSystemService( 292 AccessibilityManager.class); 293 am.addTouchExplorationStateChangeListener(mTouchExplorationStateChangeListener); 294 PreferredShortcuts.updatePreferredShortcutsFromSettings(getContext(), mShortcutTargets); 295 } 296 297 @Override onPause()298 public void onPause() { 299 super.onPause(); 300 301 if (mTouchExplorationStateChangeListener != null) { 302 final AccessibilityManager am = getSystemService( 303 AccessibilityManager.class); 304 am.removeTouchExplorationStateChangeListener(mTouchExplorationStateChangeListener); 305 } 306 } 307 308 @Override onSaveInstanceState(Bundle outState)309 public void onSaveInstanceState(Bundle outState) { 310 super.onSaveInstanceState(outState); 311 outState.putBoolean( 312 SAVED_STATE_IS_EXPANDED, 313 use(AdvancedShortcutsPreferenceController.class).isExpanded()); 314 } 315 316 @Override onDestroy()317 public void onDestroy() { 318 super.onDestroy(); 319 unregisterSettingsObserver(); 320 } 321 registerSettingsObserver()322 private void registerSettingsObserver() { 323 if (mSettingsObserver != null) { 324 for (Uri uri : SHORTCUT_SETTINGS) { 325 getContentResolver().registerContentObserver( 326 uri, /* notifyForDescendants= */ false, mSettingsObserver); 327 } 328 } 329 } 330 unregisterSettingsObserver()331 private void unregisterSettingsObserver() { 332 if (mSettingsObserver != null) { 333 getContentResolver().unregisterContentObserver(mSettingsObserver); 334 } 335 } 336 initializeArguments()337 private void initializeArguments() { 338 Bundle args = getArguments(); 339 if (args == null || args.isEmpty()) { 340 throw new IllegalArgumentException( 341 EditShortcutsPreferenceFragment.class.getSimpleName() 342 + " requires non-empty shortcut targets"); 343 } 344 345 String[] targets = args.getStringArray(ARG_KEY_SHORTCUT_TARGETS); 346 if (targets == null) { 347 throw new IllegalArgumentException( 348 EditShortcutsPreferenceFragment.class.getSimpleName() 349 + " requires non-empty shortcut targets"); 350 } 351 352 mShortcutTargets = Set.of(targets); 353 } 354 355 @Override getMetricsCategory()356 public int getMetricsCategory() { 357 return SettingsEnums.DIALOG_ACCESSIBILITY_SERVICE_EDIT_SHORTCUT; 358 } 359 360 @Override getPreferenceScreenResId()361 protected int getPreferenceScreenResId() { 362 return R.xml.accessibility_edit_shortcuts; 363 } 364 365 @Override getLogTag()366 protected String getLogTag() { 367 return TAG; 368 } 369 370 @Override onPreferenceTreeClick(Preference preference)371 public boolean onPreferenceTreeClick(Preference preference) { 372 if (getString(R.string.accessibility_shortcuts_advanced_collapsed) 373 .equals(preference.getKey())) { 374 onExpanded(); 375 // log here since calling super.onPreferenceTreeClick will be skipped 376 writePreferenceClickMetric(preference); 377 return true; 378 } 379 return super.onPreferenceTreeClick(preference); 380 } 381 382 @VisibleForTesting initializePreferenceControllerArguments()383 void initializePreferenceControllerArguments() { 384 boolean isInSuw = WizardManagerHelper.isAnySetupWizard(getIntent()); 385 386 getPreferenceControllers() 387 .stream() 388 .flatMap(Collection::stream) 389 .filter( 390 controller -> controller instanceof ShortcutOptionPreferenceController) 391 .forEach(controller -> { 392 ShortcutOptionPreferenceController shortcutOptionPreferenceController = 393 (ShortcutOptionPreferenceController) controller; 394 shortcutOptionPreferenceController.setShortcutTargets(mShortcutTargets); 395 shortcutOptionPreferenceController.setInSetupWizard(isInSuw); 396 }); 397 } 398 onExpanded()399 private void onExpanded() { 400 AdvancedShortcutsPreferenceController advanced = 401 use(AdvancedShortcutsPreferenceController.class); 402 advanced.setExpanded(true); 403 404 TripleTapShortcutOptionController tripleTapShortcutOptionController = 405 use(TripleTapShortcutOptionController.class); 406 tripleTapShortcutOptionController.setExpanded(true); 407 408 refreshPreferenceController(AdvancedShortcutsPreferenceController.class); 409 refreshPreferenceController(TripleTapShortcutOptionController.class); 410 } 411 refreshPreferenceController( Class<? extends AbstractPreferenceController> controllerClass)412 private void refreshPreferenceController( 413 Class<? extends AbstractPreferenceController> controllerClass) { 414 AbstractPreferenceController controller = use(controllerClass); 415 if (controller != null && getPreferenceScreen() != null) { 416 controller.displayPreference(getPreferenceScreen()); 417 if (!TextUtils.isEmpty(controller.getPreferenceKey())) { 418 controller.updateState(findPreference(controller.getPreferenceKey())); 419 } 420 } 421 } 422 refreshSoftwareShortcutControllers()423 private void refreshSoftwareShortcutControllers() { 424 // Gesture 425 refreshPreferenceController(GestureShortcutOptionController.class); 426 427 // FAB 428 refreshPreferenceController(FloatingButtonShortcutOptionController.class); 429 430 // A11y Nav Button 431 refreshPreferenceController(NavButtonShortcutOptionController.class); 432 } 433 434 /** 435 * Generates a title & subtitle pair describing the features whose shortcuts are being edited. 436 * 437 * @param shortcutTargets string list of component names corresponding to 438 * the relevant shortcut targets. 439 * @param accessibilityTargets list of accessibility targets 440 * to try and find corresponding labels in. 441 * @return pair of strings to be used as page title and subtitle. 442 * If there is only one shortcut label, It is displayed in the title and the subtitle is null. 443 * Otherwise, the title is a generic prompt and the subtitle lists all shortcut labels. 444 */ 445 @VisibleForTesting getTitlesFromAccessibilityTargetList( Set<String> shortcutTargets, List<AccessibilityTarget> accessibilityTargets, Resources resources)446 static Pair<String, String> getTitlesFromAccessibilityTargetList( 447 Set<String> shortcutTargets, 448 List<AccessibilityTarget> accessibilityTargets, 449 Resources resources) { 450 ArrayList<CharSequence> featureLabels = new ArrayList<>(); 451 452 Map<String, CharSequence> accessibilityTargetLabels = new ArrayMap<>(); 453 accessibilityTargets.forEach((target) -> accessibilityTargetLabels.put( 454 target.getId(), target.getLabel())); 455 456 for (String target: shortcutTargets) { 457 if (accessibilityTargetLabels.containsKey(target)) { 458 featureLabels.add(accessibilityTargetLabels.get(target)); 459 } else { 460 throw new IllegalStateException("Shortcut target does not have a label: " + target); 461 } 462 } 463 464 if (featureLabels.size() == 1) { 465 return new Pair<>( 466 resources.getString( 467 R.string.accessibility_shortcut_title, featureLabels.get(0)), 468 null 469 ); 470 } else if (featureLabels.size() == 0) { 471 throw new IllegalStateException("Found no labels for any shortcut targets."); 472 } else { 473 return new Pair<>( 474 resources.getString(R.string.accessibility_shortcut_edit_screen_title), 475 resources.getString( 476 R.string.accessibility_shortcut_edit_screen_prompt, 477 ListFormatter.getInstance().format(featureLabels)) 478 ); 479 } 480 } 481 } 482