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