1 /*
2  * Copyright (C) 2013 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 
21 import android.app.Dialog;
22 import android.app.settings.SettingsEnums;
23 import android.content.ComponentName;
24 import android.content.ContentResolver;
25 import android.content.Context;
26 import android.content.DialogInterface;
27 import android.content.Intent;
28 import android.content.pm.ResolveInfo;
29 import android.graphics.drawable.Drawable;
30 import android.icu.text.CaseMap;
31 import android.net.Uri;
32 import android.os.Bundle;
33 import android.os.Handler;
34 import android.os.UserHandle;
35 import android.provider.Settings;
36 import android.text.Html;
37 import android.text.TextUtils;
38 import android.view.LayoutInflater;
39 import android.view.View;
40 import android.view.ViewGroup;
41 import android.view.accessibility.AccessibilityManager;
42 import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener;
43 import android.widget.CheckBox;
44 import android.widget.ImageView;
45 import android.widget.Switch;
46 
47 import androidx.annotation.VisibleForTesting;
48 import androidx.preference.Preference;
49 import androidx.preference.PreferenceCategory;
50 import androidx.preference.PreferenceScreen;
51 
52 import com.android.settings.R;
53 import com.android.settings.SettingsActivity;
54 import com.android.settings.SettingsPreferenceFragment;
55 import com.android.settings.accessibility.AccessibilityDialogUtils.DialogType;
56 import com.android.settings.accessibility.AccessibilityUtil.UserShortcutType;
57 import com.android.settings.utils.LocaleUtils;
58 import com.android.settings.widget.SettingsMainSwitchBar;
59 import com.android.settings.widget.SettingsMainSwitchPreference;
60 import com.android.settingslib.accessibility.AccessibilityUtils;
61 import com.android.settingslib.widget.IllustrationPreference;
62 import com.android.settingslib.widget.OnMainSwitchChangeListener;
63 
64 import com.google.android.setupcompat.util.WizardManagerHelper;
65 
66 import java.util.ArrayList;
67 import java.util.List;
68 import java.util.Locale;
69 
70 /**
71  * Base class for accessibility fragments with toggle, shortcut, some helper functions
72  * and dialog management.
73  */
74 public abstract class ToggleFeaturePreferenceFragment extends SettingsPreferenceFragment
75         implements ShortcutPreference.OnClickCallback, OnMainSwitchChangeListener {
76 
77     protected SettingsMainSwitchPreference mToggleServiceSwitchPreference;
78     protected ShortcutPreference mShortcutPreference;
79     protected Preference mSettingsPreference;
80     protected AccessibilityFooterPreferenceController mFooterPreferenceController;
81     protected String mPreferenceKey;
82     protected Dialog mDialog;
83 
84     protected CharSequence mSettingsTitle;
85     protected Intent mSettingsIntent;
86     // The mComponentName maybe null, such as Magnify
87     protected ComponentName mComponentName;
88     protected CharSequence mPackageName;
89     protected Uri mImageUri;
90     private CharSequence mDescription;
91     protected CharSequence mHtmlDescription;
92 
93     private static final String DRAWABLE_FOLDER = "drawable";
94     protected static final String KEY_USE_SERVICE_PREFERENCE = "use_service";
95     public static final String KEY_GENERAL_CATEGORY = "general_categories";
96     protected static final String KEY_HTML_DESCRIPTION_PREFERENCE = "html_description";
97     private static final String KEY_SHORTCUT_PREFERENCE = "shortcut_preference";
98     protected static final String KEY_SAVED_USER_SHORTCUT_TYPE = "shortcut_type";
99     protected static final String KEY_ANIMATED_IMAGE = "animated_image";
100 
101     private TouchExplorationStateChangeListener mTouchExplorationStateChangeListener;
102     private SettingsContentObserver mSettingsContentObserver;
103 
104     private CheckBox mSoftwareTypeCheckBox;
105     private CheckBox mHardwareTypeCheckBox;
106 
107     public static final int NOT_SET = -1;
108     // Save user's shortcutType value when savedInstance has value (e.g. device rotated).
109     protected int mSavedCheckBoxValue = NOT_SET;
110     private boolean mSavedAccessibilityFloatingMenuEnabled;
111 
112     // For html description of accessibility service, must follow the rule, such as
113     // <img src="R.drawable.fileName"/>, a11y settings will get the resources successfully.
114     private static final String IMG_PREFIX = "R.drawable.";
115 
116     private ImageView mImageGetterCacheView;
117 
118     private final Html.ImageGetter mImageGetter = (String str) -> {
119         if (str != null && str.startsWith(IMG_PREFIX)) {
120             final String fileName = str.substring(IMG_PREFIX.length());
121             return getDrawableFromUri(Uri.parse(
122                     ContentResolver.SCHEME_ANDROID_RESOURCE + "://"
123                             + mComponentName.getPackageName() + "/" + DRAWABLE_FOLDER + "/"
124                             + fileName));
125         }
126         return null;
127     };
128 
129     @Override
onCreate(Bundle savedInstanceState)130     public void onCreate(Bundle savedInstanceState) {
131         super.onCreate(savedInstanceState);
132         // Restore the user shortcut type.
133         if (savedInstanceState != null && savedInstanceState.containsKey(
134                 KEY_SAVED_USER_SHORTCUT_TYPE)) {
135             mSavedCheckBoxValue = savedInstanceState.getInt(KEY_SAVED_USER_SHORTCUT_TYPE, NOT_SET);
136         }
137 
138         setupDefaultShortcutIfNecessary(getPrefContext());
139         final int resId = getPreferenceScreenResId();
140         if (resId <= 0) {
141             final PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(
142                     getPrefContext());
143             setPreferenceScreen(preferenceScreen);
144         }
145 
146         final List<String> shortcutFeatureKeys = getFeatureSettingsKeys();
147         mSettingsContentObserver = new SettingsContentObserver(new Handler(), shortcutFeatureKeys) {
148             @Override
149             public void onChange(boolean selfChange, Uri uri) {
150                 updateShortcutPreferenceData();
151                 updateShortcutPreference();
152             }
153         };
154     }
155 
getFeatureSettingsKeys()156     protected List<String> getFeatureSettingsKeys() {
157         final List<String> shortcutFeatureKeys = new ArrayList<>();
158         shortcutFeatureKeys.add(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS);
159         shortcutFeatureKeys.add(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE);
160         return shortcutFeatureKeys;
161     }
162 
163     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)164     public View onCreateView(LayoutInflater inflater, ViewGroup container,
165             Bundle savedInstanceState) {
166         // Need to be called as early as possible. Protected variables will be assigned here.
167         onProcessArguments(getArguments());
168 
169         initAnimatedImagePreference();
170         initToggleServiceSwitchPreference();
171         initGeneralCategory();
172         initShortcutPreference();
173         initSettingsPreference();
174         initHtmlTextPreference();
175         initFooterPreference();
176 
177         installActionBarToggleSwitch();
178 
179         updateToggleServiceTitle(mToggleServiceSwitchPreference);
180 
181         mTouchExplorationStateChangeListener = isTouchExplorationEnabled -> {
182             removeDialog(DialogEnums.EDIT_SHORTCUT);
183             mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext()));
184         };
185         return super.onCreateView(inflater, container, savedInstanceState);
186     }
187 
188     @Override
onViewCreated(View view, Bundle savedInstanceState)189     public void onViewCreated(View view, Bundle savedInstanceState) {
190         super.onViewCreated(view, savedInstanceState);
191 
192         final SettingsActivity activity = (SettingsActivity) getActivity();
193         final SettingsMainSwitchBar switchBar = activity.getSwitchBar();
194         switchBar.hide();
195 
196         updatePreferenceOrder();
197     }
198 
199     @Override
onResume()200     public void onResume() {
201         super.onResume();
202 
203         final AccessibilityManager am = getPrefContext().getSystemService(
204                 AccessibilityManager.class);
205         am.addTouchExplorationStateChangeListener(mTouchExplorationStateChangeListener);
206         mSettingsContentObserver.register(getContentResolver());
207         updateShortcutPreferenceData();
208         updateShortcutPreference();
209 
210         updateEditShortcutDialogIfNeeded();
211     }
212 
213     @Override
onPause()214     public void onPause() {
215         final AccessibilityManager am = getPrefContext().getSystemService(
216                 AccessibilityManager.class);
217         am.removeTouchExplorationStateChangeListener(mTouchExplorationStateChangeListener);
218         mSettingsContentObserver.unregister(getContentResolver());
219         mSavedAccessibilityFloatingMenuEnabled = AccessibilityUtil.isFloatingMenuEnabled(
220                 getContext());
221         super.onPause();
222     }
223 
224     @Override
onSaveInstanceState(Bundle outState)225     public void onSaveInstanceState(Bundle outState) {
226         final int value = getShortcutTypeCheckBoxValue();
227         if (value != NOT_SET) {
228             outState.putInt(KEY_SAVED_USER_SHORTCUT_TYPE, value);
229         }
230         super.onSaveInstanceState(outState);
231     }
232 
233     @Override
onCreateDialog(int dialogId)234     public Dialog onCreateDialog(int dialogId) {
235         switch (dialogId) {
236             case DialogEnums.EDIT_SHORTCUT:
237                 final CharSequence dialogTitle = getPrefContext().getString(
238                         R.string.accessibility_shortcut_title, mPackageName);
239                 final int dialogType = WizardManagerHelper.isAnySetupWizard(getIntent())
240                         ? DialogType.EDIT_SHORTCUT_GENERIC_SUW : DialogType.EDIT_SHORTCUT_GENERIC;
241                 mDialog = AccessibilityDialogUtils.showEditShortcutDialog(
242                         getPrefContext(), dialogType, dialogTitle,
243                         this::callOnAlertDialogCheckboxClicked);
244                 setupEditShortcutDialog(mDialog);
245                 return mDialog;
246             case DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL:
247                 mDialog = AccessibilityGestureNavigationTutorial
248                         .createAccessibilityTutorialDialog(getPrefContext(),
249                                 getUserShortcutTypes());
250                 mDialog.setCanceledOnTouchOutside(false);
251                 return mDialog;
252             default:
253                 throw new IllegalArgumentException("Unsupported dialogId " + dialogId);
254         }
255     }
256 
257     @Override
getDialogMetricsCategory(int dialogId)258     public int getDialogMetricsCategory(int dialogId) {
259         switch (dialogId) {
260             case DialogEnums.EDIT_SHORTCUT:
261                 return SettingsEnums.DIALOG_ACCESSIBILITY_SERVICE_EDIT_SHORTCUT;
262             case DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL:
263                 return SettingsEnums.DIALOG_ACCESSIBILITY_TUTORIAL;
264             default:
265                 return SettingsEnums.ACTION_UNKNOWN;
266         }
267     }
268 
269     @Override
getMetricsCategory()270     public int getMetricsCategory() {
271         return SettingsEnums.ACCESSIBILITY_SERVICE;
272     }
273 
274     @Override
getHelpResource()275     public int getHelpResource() {
276         return 0;
277     }
278 
279     @Override
onDestroyView()280     public void onDestroyView() {
281         super.onDestroyView();
282         removeActionBarToggleSwitch();
283     }
284 
285     @Override
onSwitchChanged(Switch switchView, boolean isChecked)286     public void onSwitchChanged(Switch switchView, boolean isChecked) {
287         onPreferenceToggled(mPreferenceKey, isChecked);
288     }
289 
290     /**
291      * Returns the shortcut type list which has been checked by user.
292      */
getUserShortcutTypes()293     abstract int getUserShortcutTypes();
294 
updateToggleServiceTitle(SettingsMainSwitchPreference switchPreference)295     protected void updateToggleServiceTitle(SettingsMainSwitchPreference switchPreference) {
296         final CharSequence title =
297             getString(R.string.accessibility_service_primary_switch_title, mPackageName);
298         switchPreference.setTitle(title);
299     }
300 
updateShortcutTitle(ShortcutPreference shortcutPreference)301     protected void updateShortcutTitle(ShortcutPreference shortcutPreference) {
302         final CharSequence title = getString(R.string.accessibility_shortcut_title, mPackageName);
303         shortcutPreference.setTitle(title);
304     }
305 
onPreferenceToggled(String preferenceKey, boolean enabled)306     protected abstract void onPreferenceToggled(String preferenceKey, boolean enabled);
307 
onInstallSwitchPreferenceToggleSwitch()308     protected void onInstallSwitchPreferenceToggleSwitch() {
309         // Implement this to set a checked listener.
310         updateSwitchBarToggleSwitch();
311         mToggleServiceSwitchPreference.addOnSwitchChangeListener(this);
312     }
313 
onRemoveSwitchPreferenceToggleSwitch()314     protected void onRemoveSwitchPreferenceToggleSwitch() {
315         // Implement this to reset a checked listener.
316     }
317 
updateSwitchBarToggleSwitch()318     protected void updateSwitchBarToggleSwitch() {
319         // Implement this to update the state of switch.
320     }
321 
installActionBarToggleSwitch()322     private void installActionBarToggleSwitch() {
323         onInstallSwitchPreferenceToggleSwitch();
324     }
325 
removeActionBarToggleSwitch()326     private void removeActionBarToggleSwitch() {
327         mToggleServiceSwitchPreference.setOnPreferenceClickListener(null);
328         onRemoveSwitchPreferenceToggleSwitch();
329     }
330 
setTitle(String title)331     public void setTitle(String title) {
332         getActivity().setTitle(title);
333     }
334 
onProcessArguments(Bundle arguments)335     protected void onProcessArguments(Bundle arguments) {
336         // Key.
337         mPreferenceKey = arguments.getString(AccessibilitySettings.EXTRA_PREFERENCE_KEY);
338 
339         // Title.
340         if (arguments.containsKey(AccessibilitySettings.EXTRA_RESOLVE_INFO)) {
341             ResolveInfo info = arguments.getParcelable(AccessibilitySettings.EXTRA_RESOLVE_INFO);
342             getActivity().setTitle(info.loadLabel(getPackageManager()).toString());
343         } else if (arguments.containsKey(AccessibilitySettings.EXTRA_TITLE)) {
344             setTitle(arguments.getString(AccessibilitySettings.EXTRA_TITLE));
345         }
346 
347         // Summary.
348         if (arguments.containsKey(AccessibilitySettings.EXTRA_SUMMARY)) {
349             mDescription = arguments.getCharSequence(AccessibilitySettings.EXTRA_SUMMARY);
350         }
351 
352         // Settings html description.
353         if (arguments.containsKey(AccessibilitySettings.EXTRA_HTML_DESCRIPTION)) {
354             mHtmlDescription = arguments.getCharSequence(
355                     AccessibilitySettings.EXTRA_HTML_DESCRIPTION);
356         }
357     }
358 
359     /** Customizes the order by preference key. */
getPreferenceOrderList()360     protected List<String> getPreferenceOrderList() {
361         final List<String> lists = new ArrayList<>();
362         lists.add(KEY_ANIMATED_IMAGE);
363         lists.add(KEY_USE_SERVICE_PREFERENCE);
364         lists.add(KEY_GENERAL_CATEGORY);
365         lists.add(KEY_HTML_DESCRIPTION_PREFERENCE);
366         return lists;
367     }
368 
updatePreferenceOrder()369     private void updatePreferenceOrder() {
370         final List<String> lists = getPreferenceOrderList();
371 
372         final PreferenceScreen preferenceScreen = getPreferenceScreen();
373         preferenceScreen.setOrderingAsAdded(false);
374 
375         final int size = lists.size();
376         for (int i = 0; i < size; i++) {
377             final Preference preference = preferenceScreen.findPreference(lists.get(i));
378             if (preference != null) {
379                 preference.setOrder(i);
380             }
381         }
382     }
383 
getDrawableFromUri(Uri imageUri)384     private Drawable getDrawableFromUri(Uri imageUri) {
385         if (mImageGetterCacheView == null) {
386             mImageGetterCacheView = new ImageView(getPrefContext());
387         }
388 
389         mImageGetterCacheView.setAdjustViewBounds(true);
390         mImageGetterCacheView.setImageURI(imageUri);
391 
392         if (mImageGetterCacheView.getDrawable() == null) {
393             return null;
394         }
395 
396         final Drawable drawable =
397                 mImageGetterCacheView.getDrawable().mutate().getConstantState().newDrawable();
398         mImageGetterCacheView.setImageURI(null);
399         final int imageWidth = drawable.getIntrinsicWidth();
400         final int imageHeight = drawable.getIntrinsicHeight();
401         final int screenHalfHeight = AccessibilityUtil.getScreenHeightPixels(getPrefContext()) / 2;
402         if ((imageWidth > AccessibilityUtil.getScreenWidthPixels(getPrefContext()))
403                 || (imageHeight > screenHalfHeight)) {
404             return null;
405         }
406 
407         drawable.setBounds(/* left= */0, /* top= */0, drawable.getIntrinsicWidth(),
408                 drawable.getIntrinsicHeight());
409 
410         return drawable;
411     }
412 
initAnimatedImagePreference()413     private void initAnimatedImagePreference() {
414         if (mImageUri == null) {
415             return;
416         }
417 
418         final int displayHalfHeight =
419                 AccessibilityUtil.getDisplayBounds(getPrefContext()).height() / 2;
420         final IllustrationPreference illustrationPreference =
421                 new IllustrationPreference(getPrefContext());
422         illustrationPreference.setImageUri(mImageUri);
423         illustrationPreference.setSelectable(false);
424         illustrationPreference.setMaxHeight(displayHalfHeight);
425         illustrationPreference.setKey(KEY_ANIMATED_IMAGE);
426 
427         getPreferenceScreen().addPreference(illustrationPreference);
428     }
429 
initToggleServiceSwitchPreference()430     private void initToggleServiceSwitchPreference() {
431         mToggleServiceSwitchPreference = new SettingsMainSwitchPreference(getPrefContext());
432         mToggleServiceSwitchPreference.setKey(KEY_USE_SERVICE_PREFERENCE);
433         if (getArguments().containsKey(AccessibilitySettings.EXTRA_CHECKED)) {
434             final boolean enabled = getArguments().getBoolean(AccessibilitySettings.EXTRA_CHECKED);
435             mToggleServiceSwitchPreference.setChecked(enabled);
436         }
437 
438         getPreferenceScreen().addPreference(mToggleServiceSwitchPreference);
439     }
440 
initGeneralCategory()441     private void initGeneralCategory() {
442         final PreferenceCategory generalCategory = new PreferenceCategory(getPrefContext());
443         generalCategory.setKey(KEY_GENERAL_CATEGORY);
444         generalCategory.setTitle(R.string.accessibility_screen_option);
445 
446         getPreferenceScreen().addPreference(generalCategory);
447     }
448 
initShortcutPreference()449     protected void initShortcutPreference() {
450         // Initial the shortcut preference.
451         mShortcutPreference = new ShortcutPreference(getPrefContext(), /* attrs= */ null);
452         mShortcutPreference.setPersistent(false);
453         mShortcutPreference.setKey(getShortcutPreferenceKey());
454         mShortcutPreference.setOnClickCallback(this);
455 
456         updateShortcutTitle(mShortcutPreference);
457 
458         final PreferenceCategory generalCategory = findPreference(KEY_GENERAL_CATEGORY);
459         generalCategory.addPreference(mShortcutPreference);
460     }
461 
initSettingsPreference()462     protected void initSettingsPreference() {
463         if (mSettingsTitle == null || mSettingsIntent == null) {
464             return;
465         }
466 
467         // Show the "Settings" menu as if it were a preference screen.
468         mSettingsPreference = new Preference(getPrefContext());
469         mSettingsPreference.setTitle(mSettingsTitle);
470         mSettingsPreference.setIconSpaceReserved(false);
471         mSettingsPreference.setIntent(mSettingsIntent);
472 
473         final PreferenceCategory generalCategory = findPreference(KEY_GENERAL_CATEGORY);
474         generalCategory.addPreference(mSettingsPreference);
475     }
476 
initHtmlTextPreference()477     private void initHtmlTextPreference() {
478         if (TextUtils.isEmpty(mHtmlDescription)) {
479             return;
480         }
481         final PreferenceScreen screen = getPreferenceScreen();
482         final CharSequence htmlDescription = Html.fromHtml(mHtmlDescription.toString(),
483                 Html.FROM_HTML_MODE_COMPACT, mImageGetter, /* tagHandler= */ null);
484 
485         final AccessibilityFooterPreference htmlFooterPreference =
486                 new AccessibilityFooterPreference(screen.getContext());
487         htmlFooterPreference.setKey(KEY_HTML_DESCRIPTION_PREFERENCE);
488         htmlFooterPreference.setSummary(htmlDescription);
489         screen.addPreference(htmlFooterPreference);
490 
491         // TODO(b/171272809): Migrate to DashboardFragment.
492         final String title = getString(R.string.accessibility_introduction_title, mPackageName);
493         mFooterPreferenceController = new AccessibilityFooterPreferenceController(
494             screen.getContext(), htmlFooterPreference.getKey());
495         mFooterPreferenceController.setIntroductionTitle(title);
496         mFooterPreferenceController.displayPreference(screen);
497     }
498 
initFooterPreference()499     private void initFooterPreference() {
500         if (!TextUtils.isEmpty(mDescription)) {
501             createFooterPreference(getPreferenceScreen(), mDescription,
502                     getString(R.string.accessibility_introduction_title, mPackageName));
503         }
504 
505         if (TextUtils.isEmpty(mHtmlDescription) && TextUtils.isEmpty(mDescription)) {
506             final CharSequence defaultDescription =
507                     getText(R.string.accessibility_service_default_description);
508             createFooterPreference(getPreferenceScreen(), defaultDescription,
509                     getString(R.string.accessibility_introduction_title, mPackageName));
510         }
511     }
512 
513 
514     /**
515      * Creates {@link AccessibilityFooterPreference} and append into {@link PreferenceScreen}
516      *
517      * @param screen The preference screen to add the footer preference
518      * @param summary The summary of the preference summary.
519      * @param introductionTitle The title of introduction in the footer.
520      */
521     @VisibleForTesting
createFooterPreference(PreferenceScreen screen, CharSequence summary, String introductionTitle)522     void createFooterPreference(PreferenceScreen screen, CharSequence summary,
523             String introductionTitle) {
524         final AccessibilityFooterPreference footerPreference =
525                 new AccessibilityFooterPreference(screen.getContext());
526         footerPreference.setSummary(summary);
527         screen.addPreference(footerPreference);
528 
529         mFooterPreferenceController = new AccessibilityFooterPreferenceController(
530             screen.getContext(), footerPreference.getKey());
531         mFooterPreferenceController.setIntroductionTitle(introductionTitle);
532         mFooterPreferenceController.displayPreference(screen);
533     }
534 
535     @VisibleForTesting
setupEditShortcutDialog(Dialog dialog)536     void setupEditShortcutDialog(Dialog dialog) {
537         final View dialogSoftwareView = dialog.findViewById(R.id.software_shortcut);
538         mSoftwareTypeCheckBox = dialogSoftwareView.findViewById(R.id.checkbox);
539         setDialogTextAreaClickListener(dialogSoftwareView, mSoftwareTypeCheckBox);
540 
541         final View dialogHardwareView = dialog.findViewById(R.id.hardware_shortcut);
542         mHardwareTypeCheckBox = dialogHardwareView.findViewById(R.id.checkbox);
543         setDialogTextAreaClickListener(dialogHardwareView, mHardwareTypeCheckBox);
544 
545         updateEditShortcutDialogCheckBox();
546     }
547 
setDialogTextAreaClickListener(View dialogView, CheckBox checkBox)548     private void setDialogTextAreaClickListener(View dialogView, CheckBox checkBox) {
549         final View dialogTextArea = dialogView.findViewById(R.id.container);
550         dialogTextArea.setOnClickListener(v -> checkBox.toggle());
551     }
552 
updateEditShortcutDialogCheckBox()553     private void updateEditShortcutDialogCheckBox() {
554         // If it is during onConfigChanged process then restore the value, or get the saved value
555         // when shortcutPreference is checked.
556         int value = restoreOnConfigChangedValue();
557         if (value == NOT_SET) {
558             final int lastNonEmptyUserShortcutType = PreferredShortcuts.retrieveUserShortcutType(
559                     getPrefContext(), mComponentName.flattenToString(), UserShortcutType.SOFTWARE);
560             value = mShortcutPreference.isChecked() ? lastNonEmptyUserShortcutType
561                     : UserShortcutType.EMPTY;
562         }
563 
564         mSoftwareTypeCheckBox.setChecked(
565                 hasShortcutType(value, UserShortcutType.SOFTWARE));
566         mHardwareTypeCheckBox.setChecked(
567                 hasShortcutType(value, UserShortcutType.HARDWARE));
568     }
569 
restoreOnConfigChangedValue()570     private int restoreOnConfigChangedValue() {
571         final int savedValue = mSavedCheckBoxValue;
572         mSavedCheckBoxValue = NOT_SET;
573         return savedValue;
574     }
575 
hasShortcutType(int value, @UserShortcutType int type)576     private boolean hasShortcutType(int value, @UserShortcutType int type) {
577         return (value & type) == type;
578     }
579 
580     /**
581      * Returns accumulated {@link UserShortcutType} checkbox value or {@code NOT_SET} if checkboxes
582      * did not exist.
583      */
getShortcutTypeCheckBoxValue()584     protected int getShortcutTypeCheckBoxValue() {
585         if (mSoftwareTypeCheckBox == null || mHardwareTypeCheckBox == null) {
586             return NOT_SET;
587         }
588 
589         int value = UserShortcutType.EMPTY;
590         if (mSoftwareTypeCheckBox.isChecked()) {
591             value |= UserShortcutType.SOFTWARE;
592         }
593         if (mHardwareTypeCheckBox.isChecked()) {
594             value |= UserShortcutType.HARDWARE;
595         }
596         return value;
597     }
598 
getSoftwareShortcutTypeSummary(Context context)599     private static CharSequence getSoftwareShortcutTypeSummary(Context context) {
600         int resId;
601         if (AccessibilityUtil.isFloatingMenuEnabled(context)) {
602             resId = R.string.accessibility_shortcut_edit_summary_software;
603         } else if (AccessibilityUtil.isGestureNavigateEnabled(context)) {
604             resId = R.string.accessibility_shortcut_edit_summary_software_gesture;
605         } else {
606             resId = R.string.accessibility_shortcut_edit_summary_software;
607         }
608         return context.getText(resId);
609     }
610 
getShortcutTypeSummary(Context context)611     protected CharSequence getShortcutTypeSummary(Context context) {
612         if (!mShortcutPreference.isSettingsEditable()) {
613             return context.getText(R.string.accessibility_shortcut_edit_dialog_title_hardware);
614         }
615 
616         if (!mShortcutPreference.isChecked()) {
617             return context.getText(R.string.switch_off_text);
618         }
619 
620         final int shortcutTypes = PreferredShortcuts.retrieveUserShortcutType(context,
621                 mComponentName.flattenToString(), UserShortcutType.SOFTWARE);
622 
623         final List<CharSequence> list = new ArrayList<>();
624         if (hasShortcutType(shortcutTypes, UserShortcutType.SOFTWARE)) {
625             list.add(getSoftwareShortcutTypeSummary(context));
626         }
627         if (hasShortcutType(shortcutTypes, UserShortcutType.HARDWARE)) {
628             final CharSequence hardwareTitle = context.getText(
629                     R.string.accessibility_shortcut_hardware_keyword);
630             list.add(hardwareTitle);
631         }
632 
633         // Show software shortcut if first time to use.
634         if (list.isEmpty()) {
635             list.add(getSoftwareShortcutTypeSummary(context));
636         }
637 
638         return CaseMap.toTitle().wholeString().noLowercase().apply(Locale.getDefault(), /* iter= */
639                 null, LocaleUtils.getConcatenatedString(list));
640     }
641 
642     /**
643      * This method will be invoked when a button in the edit shortcut dialog is clicked.
644      *
645      * @param dialog The dialog that received the click
646      * @param which  The button that was clicked
647      */
callOnAlertDialogCheckboxClicked(DialogInterface dialog, int which)648     protected void callOnAlertDialogCheckboxClicked(DialogInterface dialog, int which) {
649         if (mComponentName == null) {
650             return;
651         }
652 
653         final int value = getShortcutTypeCheckBoxValue();
654 
655         saveNonEmptyUserShortcutType(value);
656         AccessibilityUtil.optInAllValuesToSettings(getPrefContext(), value, mComponentName);
657         AccessibilityUtil.optOutAllValuesFromSettings(getPrefContext(), ~value, mComponentName);
658         mShortcutPreference.setChecked(value != UserShortcutType.EMPTY);
659         mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext()));
660     }
661 
updateShortcutPreferenceData()662     protected void updateShortcutPreferenceData() {
663         if (mComponentName == null) {
664             return;
665         }
666 
667         final int shortcutTypes = AccessibilityUtil.getUserShortcutTypesFromSettings(
668                 getPrefContext(), mComponentName);
669         if (shortcutTypes != UserShortcutType.EMPTY) {
670             final PreferredShortcut shortcut = new PreferredShortcut(
671                     mComponentName.flattenToString(), shortcutTypes);
672             PreferredShortcuts.saveUserShortcutType(getPrefContext(), shortcut);
673         }
674     }
675 
updateShortcutPreference()676     protected void updateShortcutPreference() {
677         if (mComponentName == null) {
678             return;
679         }
680 
681         final int shortcutTypes = PreferredShortcuts.retrieveUserShortcutType(getPrefContext(),
682                 mComponentName.flattenToString(), UserShortcutType.SOFTWARE);
683         mShortcutPreference.setChecked(
684                 AccessibilityUtil.hasValuesInSettings(getPrefContext(), shortcutTypes,
685                         mComponentName));
686         mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext()));
687     }
688 
getShortcutPreferenceKey()689     protected String getShortcutPreferenceKey() {
690         return KEY_SHORTCUT_PREFERENCE;
691     }
692 
693     @Override
onToggleClicked(ShortcutPreference preference)694     public void onToggleClicked(ShortcutPreference preference) {
695         if (mComponentName == null) {
696             return;
697         }
698 
699         final int shortcutTypes = PreferredShortcuts.retrieveUserShortcutType(getPrefContext(),
700                 mComponentName.flattenToString(), UserShortcutType.SOFTWARE);
701         if (preference.isChecked()) {
702             AccessibilityUtil.optInAllValuesToSettings(getPrefContext(), shortcutTypes,
703                     mComponentName);
704             showDialog(DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL);
705         } else {
706             AccessibilityUtil.optOutAllValuesFromSettings(getPrefContext(), shortcutTypes,
707                     mComponentName);
708         }
709         mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext()));
710     }
711 
712     @Override
onSettingsClicked(ShortcutPreference preference)713     public void onSettingsClicked(ShortcutPreference preference) {
714         showDialog(DialogEnums.EDIT_SHORTCUT);
715     }
716 
717     /**
718      * Setups a configurable default if the setting has never been set.
719      */
setupDefaultShortcutIfNecessary(Context context)720     private static void setupDefaultShortcutIfNecessary(Context context) {
721         final String targetKey = Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE;
722         String targetString = Settings.Secure.getString(context.getContentResolver(), targetKey);
723         if (!TextUtils.isEmpty(targetString)) {
724             // The shortcut setting has been set
725             return;
726         }
727 
728         // AccessibilityManager#getAccessibilityShortcutTargets may not return correct shortcut
729         // targets during boot. Needs to read settings directly here.
730         targetString = AccessibilityUtils.getShortcutTargetServiceComponentNameString(context,
731                 UserHandle.myUserId());
732         if (TextUtils.isEmpty(targetString)) {
733             // No configurable default accessibility service
734             return;
735         }
736 
737         // Only fallback to default accessibility service when setting is never updated.
738         final ComponentName shortcutName = ComponentName.unflattenFromString(targetString);
739         if (shortcutName != null) {
740             Settings.Secure.putString(context.getContentResolver(), targetKey,
741                     shortcutName.flattenToString());
742         }
743     }
744 
updateEditShortcutDialogIfNeeded()745     private void updateEditShortcutDialogIfNeeded() {
746         if (mDialog == null || !mDialog.isShowing()) {
747             return;
748         }
749 
750         // Content in software shortcut need to be adjusted depend on the accessibility button
751         // mode status which can be changed in background.
752         final boolean valueChanged = mSavedAccessibilityFloatingMenuEnabled
753                 != AccessibilityUtil.isFloatingMenuEnabled(getContext());
754         if (valueChanged) {
755             AccessibilityDialogUtils.updateSoftwareShortcutInDialog(getContext(), mDialog);
756         }
757     }
758 
759     @VisibleForTesting
saveNonEmptyUserShortcutType(int type)760     void saveNonEmptyUserShortcutType(int type) {
761         if (type == UserShortcutType.EMPTY) {
762             return;
763         }
764 
765         final PreferredShortcut shortcut = new PreferredShortcut(
766                 mComponentName.flattenToString(), type);
767         PreferredShortcuts.saveUserShortcutType(getPrefContext(), shortcut);
768     }
769 }
770