• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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