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