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.AccessibilityUtil.getScreenHeightPixels;
20 import static com.android.settings.accessibility.AccessibilityUtil.getScreenWidthPixels;
21 
22 import android.app.Dialog;
23 import android.app.settings.SettingsEnums;
24 import android.content.ComponentName;
25 import android.content.ContentResolver;
26 import android.content.Context;
27 import android.content.DialogInterface;
28 import android.content.Intent;
29 import android.content.pm.ResolveInfo;
30 import android.graphics.drawable.Drawable;
31 import android.icu.text.CaseMap;
32 import android.net.Uri;
33 import android.os.Bundle;
34 import android.os.Handler;
35 import android.os.UserHandle;
36 import android.provider.Settings;
37 import android.text.Html;
38 import android.text.TextUtils;
39 import android.view.LayoutInflater;
40 import android.view.View;
41 import android.view.ViewGroup;
42 import android.view.accessibility.AccessibilityManager;
43 import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener;
44 import android.widget.CheckBox;
45 import android.widget.ImageView;
46 
47 import androidx.preference.Preference;
48 import androidx.preference.PreferenceCategory;
49 import androidx.preference.PreferenceScreen;
50 import androidx.preference.SwitchPreference;
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.AccessibilityUtil.UserShortcutType;
56 import com.android.settings.widget.SwitchBar;
57 import com.android.settingslib.accessibility.AccessibilityUtils;
58 import com.android.settingslib.widget.FooterPreference;
59 
60 import java.lang.annotation.Retention;
61 import java.lang.annotation.RetentionPolicy;
62 import java.util.ArrayList;
63 import java.util.HashSet;
64 import java.util.List;
65 import java.util.Locale;
66 import java.util.Set;
67 import java.util.StringJoiner;
68 import java.util.stream.Collectors;
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 {
76 
77     protected DividerSwitchPreference mToggleServiceDividerSwitchPreference;
78     protected ShortcutPreference mShortcutPreference;
79     protected Preference mSettingsPreference;
80     protected String mPreferenceKey;
81 
82     protected CharSequence mSettingsTitle;
83     protected Intent mSettingsIntent;
84     // The mComponentName maybe null, such as Magnify
85     protected ComponentName mComponentName;
86     protected CharSequence mPackageName;
87     protected Uri mImageUri;
88     private CharSequence mDescription;
89     protected CharSequence mHtmlDescription;
90     // Used to restore the edit dialog status.
91     protected int mUserShortcutTypesCache = UserShortcutType.EMPTY;
92     private static final String DRAWABLE_FOLDER = "drawable";
93     protected static final String KEY_USE_SERVICE_PREFERENCE = "use_service";
94     protected static final String KEY_GENERAL_CATEGORY = "general_categories";
95     protected static final String KEY_INTRODUCTION_CATEGORY = "introduction_categories";
96     private static final String KEY_SHORTCUT_PREFERENCE = "shortcut_preference";
97     private static final String EXTRA_SHORTCUT_TYPE = "shortcut_type";
98     private TouchExplorationStateChangeListener mTouchExplorationStateChangeListener;
99     private int mUserShortcutTypes = UserShortcutType.EMPTY;
100     private CheckBox mSoftwareTypeCheckBox;
101     private CheckBox mHardwareTypeCheckBox;
102     private SettingsContentObserver mSettingsContentObserver;
103 
104     // For html description of accessibility service, must follow the rule, such as
105     // <img src="R.drawable.fileName"/>, a11y settings will get the resources successfully.
106     private static final String IMG_PREFIX = "R.drawable.";
107 
108     private ImageView mImageGetterCacheView;
109 
110     private final Html.ImageGetter mImageGetter = (String str) -> {
111         if (str != null && str.startsWith(IMG_PREFIX)) {
112             final String fileName = str.substring(IMG_PREFIX.length());
113             return getDrawableFromUri(Uri.parse(
114                     ContentResolver.SCHEME_ANDROID_RESOURCE + "://"
115                             + mComponentName.getPackageName() + "/" + DRAWABLE_FOLDER + "/"
116                             + fileName));
117         }
118         return null;
119     };
120 
121     @Override
onCreate(Bundle savedInstanceState)122     public void onCreate(Bundle savedInstanceState) {
123         super.onCreate(savedInstanceState);
124         setupDefaultShortcutIfNecessary(getPrefContext());
125         final int resId = getPreferenceScreenResId();
126         if (resId <= 0) {
127             PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(
128                     getPrefContext());
129             setPreferenceScreen(preferenceScreen);
130         }
131 
132         final List<String> shortcutFeatureKeys = new ArrayList<>();
133         shortcutFeatureKeys.add(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS);
134         shortcutFeatureKeys.add(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE);
135         mSettingsContentObserver = new SettingsContentObserver(new Handler(), shortcutFeatureKeys) {
136             @Override
137             public void onChange(boolean selfChange, Uri uri) {
138                 updateShortcutPreferenceData();
139                 updateShortcutPreference();
140             }
141         };
142     }
143 
144     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)145     public View onCreateView(LayoutInflater inflater, ViewGroup container,
146             Bundle savedInstanceState) {
147         mTouchExplorationStateChangeListener = isTouchExplorationEnabled -> {
148             removeDialog(DialogEnums.EDIT_SHORTCUT);
149             mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext()));
150         };
151         return super.onCreateView(inflater, container, savedInstanceState);
152     }
153 
154     @Override
onViewCreated(View view, Bundle savedInstanceState)155     public void onViewCreated(View view, Bundle savedInstanceState) {
156         super.onViewCreated(view, savedInstanceState);
157 
158         final SettingsActivity activity = (SettingsActivity) getActivity();
159         final SwitchBar switchBar = activity.getSwitchBar();
160         switchBar.hide();
161 
162         // Need to be called as early as possible. Protected variables will be assigned here.
163         onProcessArguments(getArguments());
164 
165         PreferenceScreen preferenceScreen = getPreferenceScreen();
166         if (mImageUri != null) {
167             final int screenHalfHeight = getScreenHeightPixels(getPrefContext()) / /* half */ 2;
168             final AnimatedImagePreference animatedImagePreference = new AnimatedImagePreference(
169                     getPrefContext());
170             animatedImagePreference.setImageUri(mImageUri);
171             animatedImagePreference.setSelectable(false);
172             animatedImagePreference.setMaxHeight(screenHalfHeight);
173             preferenceScreen.addPreference(animatedImagePreference);
174         }
175 
176         mToggleServiceDividerSwitchPreference = new DividerSwitchPreference(getPrefContext());
177         mToggleServiceDividerSwitchPreference.setKey(KEY_USE_SERVICE_PREFERENCE);
178         if (getArguments().containsKey(AccessibilitySettings.EXTRA_CHECKED)) {
179             final boolean enabled = getArguments().getBoolean(AccessibilitySettings.EXTRA_CHECKED);
180             mToggleServiceDividerSwitchPreference.setChecked(enabled);
181         }
182 
183         preferenceScreen.addPreference(mToggleServiceDividerSwitchPreference);
184 
185         updateToggleServiceTitle(mToggleServiceDividerSwitchPreference);
186 
187         final PreferenceCategory groupCategory = new PreferenceCategory(getPrefContext());
188         groupCategory.setKey(KEY_GENERAL_CATEGORY);
189         groupCategory.setTitle(R.string.accessibility_screen_option);
190         preferenceScreen.addPreference(groupCategory);
191 
192         initShortcutPreference(savedInstanceState);
193         groupCategory.addPreference(mShortcutPreference);
194 
195         // Show the "Settings" menu as if it were a preference screen.
196         if (mSettingsTitle != null && mSettingsIntent != null) {
197             mSettingsPreference = new Preference(getPrefContext());
198             mSettingsPreference.setTitle(mSettingsTitle);
199             mSettingsPreference.setIconSpaceReserved(true);
200             mSettingsPreference.setIntent(mSettingsIntent);
201         }
202 
203         // The downloaded app may not show Settings. The framework app has Settings.
204         if (mSettingsPreference != null) {
205             groupCategory.addPreference(mSettingsPreference);
206         }
207 
208         if (!TextUtils.isEmpty(mHtmlDescription)) {
209             final PreferenceCategory introductionCategory = new PreferenceCategory(
210                     getPrefContext());
211             final CharSequence title = getString(R.string.accessibility_introduction_title,
212                     mPackageName);
213             introductionCategory.setKey(KEY_INTRODUCTION_CATEGORY);
214             introductionCategory.setTitle(title);
215             preferenceScreen.addPreference(introductionCategory);
216 
217             final HtmlTextPreference htmlTextPreference = new HtmlTextPreference(getPrefContext());
218             htmlTextPreference.setSummary(mHtmlDescription);
219             htmlTextPreference.setImageGetter(mImageGetter);
220             htmlTextPreference.setSelectable(false);
221             introductionCategory.addPreference(htmlTextPreference);
222         }
223 
224         if (!TextUtils.isEmpty(mDescription)) {
225             createFooterPreference(mDescription);
226         }
227 
228         if (TextUtils.isEmpty(mHtmlDescription) && TextUtils.isEmpty(mDescription)) {
229             final CharSequence defaultDescription = getText(
230                     R.string.accessibility_service_default_description);
231             createFooterPreference(defaultDescription);
232         }
233     }
234 
235     @Override
onActivityCreated(Bundle savedInstanceState)236     public void onActivityCreated(Bundle savedInstanceState) {
237         super.onActivityCreated(savedInstanceState);
238         installActionBarToggleSwitch();
239     }
240 
241     @Override
onResume()242     public void onResume() {
243         super.onResume();
244         final AccessibilityManager am = getPrefContext().getSystemService(
245                 AccessibilityManager.class);
246         am.addTouchExplorationStateChangeListener(mTouchExplorationStateChangeListener);
247         mSettingsContentObserver.register(getContentResolver());
248         updateShortcutPreferenceData();
249         updateShortcutPreference();
250     }
251 
252     @Override
onPause()253     public void onPause() {
254         final AccessibilityManager am = getPrefContext().getSystemService(
255                 AccessibilityManager.class);
256         am.removeTouchExplorationStateChangeListener(mTouchExplorationStateChangeListener);
257         mSettingsContentObserver.unregister(getContentResolver());
258         super.onPause();
259     }
260 
261     @Override
onSaveInstanceState(Bundle outState)262     public void onSaveInstanceState(Bundle outState) {
263         outState.putInt(EXTRA_SHORTCUT_TYPE, mUserShortcutTypesCache);
264         super.onSaveInstanceState(outState);
265     }
266 
267     @Override
onCreateDialog(int dialogId)268     public Dialog onCreateDialog(int dialogId) {
269         Dialog dialog;
270         switch (dialogId) {
271             case DialogEnums.EDIT_SHORTCUT:
272                 final CharSequence dialogTitle = getPrefContext().getString(
273                         R.string.accessibility_shortcut_title, mPackageName);
274                 dialog = AccessibilityEditDialogUtils.showEditShortcutDialog(
275                         getPrefContext(), dialogTitle, this::callOnAlertDialogCheckboxClicked);
276                 initializeDialogCheckBox(dialog);
277                 return dialog;
278             case DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL:
279                 dialog = AccessibilityGestureNavigationTutorial
280                         .createAccessibilityTutorialDialog(getPrefContext(),
281                                 getUserShortcutTypes());
282                 dialog.setCanceledOnTouchOutside(false);
283                 return dialog;
284             default:
285                 throw new IllegalArgumentException("Unsupported dialogId " + dialogId);
286         }
287     }
288 
289     @Override
getDialogMetricsCategory(int dialogId)290     public int getDialogMetricsCategory(int dialogId) {
291         switch (dialogId) {
292             case DialogEnums.EDIT_SHORTCUT:
293                 return SettingsEnums.DIALOG_ACCESSIBILITY_SERVICE_EDIT_SHORTCUT;
294             case DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL:
295                 return SettingsEnums.DIALOG_ACCESSIBILITY_TUTORIAL;
296             default:
297                 return SettingsEnums.ACTION_UNKNOWN;
298         }
299     }
300 
301     /** Denotes the dialog emuns for show dialog */
302     @Retention(RetentionPolicy.SOURCE)
303     protected @interface DialogEnums {
304 
305         /** OPEN: Settings > Accessibility > Any toggle service > Shortcut > Settings. */
306         int EDIT_SHORTCUT = 1;
307 
308         /** OPEN: Settings > Accessibility > Magnification > Shortcut > Settings. */
309         int MAGNIFICATION_EDIT_SHORTCUT = 1001;
310 
311         /**
312          * OPEN: Settings > Accessibility > Downloaded toggle service > Toggle use service to
313          * enable service.
314          */
315         int ENABLE_WARNING_FROM_TOGGLE = 1002;
316 
317         /** OPEN: Settings > Accessibility > Downloaded toggle service > Shortcut checkbox. */
318         int ENABLE_WARNING_FROM_SHORTCUT = 1003;
319 
320         /**
321          * OPEN: Settings > Accessibility > Downloaded toggle service > Shortcut checkbox
322          * toggle.
323          */
324         int ENABLE_WARNING_FROM_SHORTCUT_TOGGLE = 1004;
325 
326         /**
327          * OPEN: Settings > Accessibility > Downloaded toggle service > Toggle use service to
328          * disable service.
329          */
330         int DISABLE_WARNING_FROM_TOGGLE = 1005;
331 
332         /**
333          * OPEN: Settings > Accessibility > Magnification > Toggle user service in button
334          * navigation.
335          */
336         int ACCESSIBILITY_BUTTON_TUTORIAL = 1006;
337 
338         /**
339          * OPEN: Settings > Accessibility > Magnification > Toggle user service in gesture
340          * navigation.
341          */
342         int GESTURE_NAVIGATION_TUTORIAL = 1007;
343 
344         /**
345          * OPEN: Settings > Accessibility > Downloaded toggle service > Toggle user service > Show
346          * launch tutorial.
347          */
348         int LAUNCH_ACCESSIBILITY_TUTORIAL = 1008;
349     }
350 
351     @Override
getMetricsCategory()352     public int getMetricsCategory() {
353         return SettingsEnums.ACCESSIBILITY_SERVICE;
354     }
355 
356     @Override
onDestroyView()357     public void onDestroyView() {
358         super.onDestroyView();
359         removeActionBarToggleSwitch();
360     }
361 
362     /**
363      * Returns the shortcut type list which has been checked by user.
364      */
getUserShortcutTypes()365     abstract int getUserShortcutTypes();
366 
updateToggleServiceTitle(SwitchPreference switchPreference)367     protected void updateToggleServiceTitle(SwitchPreference switchPreference) {
368         switchPreference.setTitle(R.string.accessibility_service_master_switch_title);
369     }
370 
onPreferenceToggled(String preferenceKey, boolean enabled)371     protected abstract void onPreferenceToggled(String preferenceKey, boolean enabled);
372 
onInstallSwitchPreferenceToggleSwitch()373     protected void onInstallSwitchPreferenceToggleSwitch() {
374         // Implement this to set a checked listener.
375     }
376 
onRemoveSwitchPreferenceToggleSwitch()377     protected void onRemoveSwitchPreferenceToggleSwitch() {
378         // Implement this to reset a checked listener.
379     }
380 
installActionBarToggleSwitch()381     private void installActionBarToggleSwitch() {
382         onInstallSwitchPreferenceToggleSwitch();
383     }
384 
removeActionBarToggleSwitch()385     private void removeActionBarToggleSwitch() {
386         mToggleServiceDividerSwitchPreference.setOnPreferenceClickListener(null);
387         onRemoveSwitchPreferenceToggleSwitch();
388     }
389 
setTitle(String title)390     public void setTitle(String title) {
391         getActivity().setTitle(title);
392     }
393 
onProcessArguments(Bundle arguments)394     protected void onProcessArguments(Bundle arguments) {
395         // Key.
396         mPreferenceKey = arguments.getString(AccessibilitySettings.EXTRA_PREFERENCE_KEY);
397 
398         // Title.
399         if (arguments.containsKey(AccessibilitySettings.EXTRA_RESOLVE_INFO)) {
400             ResolveInfo info = arguments.getParcelable(AccessibilitySettings.EXTRA_RESOLVE_INFO);
401             getActivity().setTitle(info.loadLabel(getPackageManager()).toString());
402         } else if (arguments.containsKey(AccessibilitySettings.EXTRA_TITLE)) {
403             setTitle(arguments.getString(AccessibilitySettings.EXTRA_TITLE));
404         }
405 
406         // Summary.
407         if (arguments.containsKey(AccessibilitySettings.EXTRA_SUMMARY)) {
408             mDescription = arguments.getCharSequence(AccessibilitySettings.EXTRA_SUMMARY);
409         }
410 
411         // Settings html description.
412         if (arguments.containsKey(AccessibilitySettings.EXTRA_HTML_DESCRIPTION)) {
413             mHtmlDescription = arguments.getCharSequence(
414                     AccessibilitySettings.EXTRA_HTML_DESCRIPTION);
415         }
416     }
417 
getDrawableFromUri(Uri imageUri)418     private Drawable getDrawableFromUri(Uri imageUri) {
419         if (mImageGetterCacheView == null) {
420             mImageGetterCacheView = new ImageView(getPrefContext());
421         }
422 
423         mImageGetterCacheView.setAdjustViewBounds(true);
424         mImageGetterCacheView.setImageURI(imageUri);
425 
426         if (mImageGetterCacheView.getDrawable() == null) {
427             return null;
428         }
429 
430         final Drawable drawable =
431                 mImageGetterCacheView.getDrawable().mutate().getConstantState().newDrawable();
432         mImageGetterCacheView.setImageURI(null);
433         final int imageWidth = drawable.getIntrinsicWidth();
434         final int imageHeight = drawable.getIntrinsicHeight();
435         final int screenHalfHeight = getScreenHeightPixels(getPrefContext()) / /* half */ 2;
436         if ((imageWidth > getScreenWidthPixels(getPrefContext()))
437                 || (imageHeight > screenHalfHeight)) {
438             return null;
439         }
440 
441         drawable.setBounds(/* left= */0, /* top= */0, drawable.getIntrinsicWidth(),
442                 drawable.getIntrinsicHeight());
443 
444         return drawable;
445     }
446 
447     static final class AccessibilityUserShortcutType {
448         private static final char COMPONENT_NAME_SEPARATOR = ':';
449         private static final TextUtils.SimpleStringSplitter sStringColonSplitter =
450                 new TextUtils.SimpleStringSplitter(COMPONENT_NAME_SEPARATOR);
451 
452         private String mComponentName;
453         private int mType;
454 
AccessibilityUserShortcutType(String componentName, int type)455         AccessibilityUserShortcutType(String componentName, int type) {
456             this.mComponentName = componentName;
457             this.mType = type;
458         }
459 
AccessibilityUserShortcutType(String flattenedString)460         AccessibilityUserShortcutType(String flattenedString) {
461             sStringColonSplitter.setString(flattenedString);
462             if (sStringColonSplitter.hasNext()) {
463                 this.mComponentName = sStringColonSplitter.next();
464                 this.mType = Integer.parseInt(sStringColonSplitter.next());
465             }
466         }
467 
getComponentName()468         String getComponentName() {
469             return mComponentName;
470         }
471 
setComponentName(String componentName)472         void setComponentName(String componentName) {
473             this.mComponentName = componentName;
474         }
475 
getType()476         int getType() {
477             return mType;
478         }
479 
setType(int type)480         void setType(int type) {
481             this.mType = type;
482         }
483 
flattenToString()484         String flattenToString() {
485             final StringJoiner joiner = new StringJoiner(String.valueOf(COMPONENT_NAME_SEPARATOR));
486             joiner.add(mComponentName);
487             joiner.add(String.valueOf(mType));
488             return joiner.toString();
489         }
490     }
491 
setDialogTextAreaClickListener(View dialogView, CheckBox checkBox)492     private void setDialogTextAreaClickListener(View dialogView, CheckBox checkBox) {
493         final View dialogTextArea = dialogView.findViewById(R.id.container);
494         dialogTextArea.setOnClickListener(v -> {
495             checkBox.toggle();
496             updateUserShortcutType(/* saveChanges= */ false);
497         });
498     }
499 
initializeDialogCheckBox(Dialog dialog)500     private void initializeDialogCheckBox(Dialog dialog) {
501         final View dialogSoftwareView = dialog.findViewById(R.id.software_shortcut);
502         mSoftwareTypeCheckBox = dialogSoftwareView.findViewById(R.id.checkbox);
503         setDialogTextAreaClickListener(dialogSoftwareView, mSoftwareTypeCheckBox);
504 
505         final View dialogHardwareView = dialog.findViewById(R.id.hardware_shortcut);
506         mHardwareTypeCheckBox = dialogHardwareView.findViewById(R.id.checkbox);
507         setDialogTextAreaClickListener(dialogHardwareView, mHardwareTypeCheckBox);
508 
509         updateAlertDialogCheckState();
510     }
511 
updateAlertDialogCheckState()512     private void updateAlertDialogCheckState() {
513         if (mUserShortcutTypesCache != UserShortcutType.EMPTY) {
514             updateCheckStatus(mSoftwareTypeCheckBox, UserShortcutType.SOFTWARE);
515             updateCheckStatus(mHardwareTypeCheckBox, UserShortcutType.HARDWARE);
516         }
517     }
518 
updateCheckStatus(CheckBox checkBox, @UserShortcutType int type)519     private void updateCheckStatus(CheckBox checkBox, @UserShortcutType int type) {
520         checkBox.setChecked((mUserShortcutTypesCache & type) == type);
521     }
522 
updateUserShortcutType(boolean saveChanges)523     private void updateUserShortcutType(boolean saveChanges) {
524         mUserShortcutTypesCache = UserShortcutType.EMPTY;
525         if (mSoftwareTypeCheckBox.isChecked()) {
526             mUserShortcutTypesCache |= UserShortcutType.SOFTWARE;
527         }
528         if (mHardwareTypeCheckBox.isChecked()) {
529             mUserShortcutTypesCache |= UserShortcutType.HARDWARE;
530         }
531 
532         if (saveChanges) {
533             final boolean isChanged = (mUserShortcutTypesCache != UserShortcutType.EMPTY);
534             if (isChanged) {
535                 setUserShortcutType(getPrefContext(), mUserShortcutTypesCache);
536             }
537             mUserShortcutTypes = mUserShortcutTypesCache;
538         }
539     }
540 
setUserShortcutType(Context context, int type)541     private void setUserShortcutType(Context context, int type) {
542         if (mComponentName == null) {
543             return;
544         }
545 
546         Set<String> info = SharedPreferenceUtils.getUserShortcutTypes(context);
547         final String componentName = mComponentName.flattenToString();
548         if (info.isEmpty()) {
549             info = new HashSet<>();
550         } else {
551             final Set<String> filtered = info.stream()
552                     .filter(str -> str.contains(componentName))
553                     .collect(Collectors.toSet());
554             info.removeAll(filtered);
555         }
556         final AccessibilityUserShortcutType shortcut = new AccessibilityUserShortcutType(
557                 componentName, type);
558         info.add(shortcut.flattenToString());
559         SharedPreferenceUtils.setUserShortcutType(context, info);
560     }
561 
getShortcutTypeSummary(Context context)562     protected CharSequence getShortcutTypeSummary(Context context) {
563         if (!mShortcutPreference.isSettingsEditable()) {
564             return context.getText(R.string.accessibility_shortcut_edit_dialog_title_hardware);
565         }
566 
567         if (!mShortcutPreference.isChecked()) {
568             return context.getText(R.string.switch_off_text);
569         }
570 
571         final int shortcutTypes = getUserShortcutTypes(context, UserShortcutType.SOFTWARE);
572         int resId = R.string.accessibility_shortcut_edit_summary_software;
573         if (AccessibilityUtil.isGestureNavigateEnabled(context)) {
574             resId = AccessibilityUtil.isTouchExploreEnabled(context)
575                     ? R.string.accessibility_shortcut_edit_dialog_title_software_gesture_talkback
576                     : R.string.accessibility_shortcut_edit_dialog_title_software_gesture;
577         }
578         final CharSequence softwareTitle = context.getText(resId);
579 
580         List<CharSequence> list = new ArrayList<>();
581         if ((shortcutTypes & UserShortcutType.SOFTWARE) == UserShortcutType.SOFTWARE) {
582             list.add(softwareTitle);
583         }
584         if ((shortcutTypes & UserShortcutType.HARDWARE) == UserShortcutType.HARDWARE) {
585             final CharSequence hardwareTitle = context.getText(
586                     R.string.accessibility_shortcut_hardware_keyword);
587             list.add(hardwareTitle);
588         }
589 
590         // Show software shortcut if first time to use.
591         if (list.isEmpty()) {
592             list.add(softwareTitle);
593         }
594         final String joinStrings = TextUtils.join(/* delimiter= */", ", list);
595 
596         return CaseMap.toTitle().wholeString().noLowercase().apply(Locale.getDefault(), /* iter= */
597                 null, joinStrings);
598     }
599 
getUserShortcutTypes(Context context, @UserShortcutType int defaultValue)600     protected int getUserShortcutTypes(Context context, @UserShortcutType int defaultValue) {
601         if (mComponentName == null) {
602             return defaultValue;
603         }
604 
605         final Set<String> info = SharedPreferenceUtils.getUserShortcutTypes(context);
606         final String componentName = mComponentName.flattenToString();
607         final Set<String> filtered = info.stream()
608                 .filter(str -> str.contains(componentName))
609                 .collect(Collectors.toSet());
610         if (filtered.isEmpty()) {
611             return defaultValue;
612         }
613 
614         final String str = (String) filtered.toArray()[0];
615         final AccessibilityUserShortcutType shortcut = new AccessibilityUserShortcutType(str);
616         return shortcut.getType();
617     }
618 
619     /**
620      * This method will be invoked when a button in the edit shortcut dialog is clicked.
621      *
622      * @param dialog The dialog that received the click
623      * @param which The button that was clicked
624      */
callOnAlertDialogCheckboxClicked(DialogInterface dialog, int which)625     protected void callOnAlertDialogCheckboxClicked(DialogInterface dialog, int which) {
626         if (mComponentName == null) {
627             return;
628         }
629 
630         updateUserShortcutType(/* saveChanges= */ true);
631         AccessibilityUtil.optInAllValuesToSettings(getPrefContext(), mUserShortcutTypes,
632                 mComponentName);
633         AccessibilityUtil.optOutAllValuesFromSettings(getPrefContext(), ~mUserShortcutTypes,
634                 mComponentName);
635         mShortcutPreference.setChecked(mUserShortcutTypes != UserShortcutType.EMPTY);
636         mShortcutPreference.setSummary(
637                 getShortcutTypeSummary(getPrefContext()));
638     }
639 
updateShortcutPreferenceData()640     protected void updateShortcutPreferenceData() {
641         if (mComponentName == null) {
642             return;
643         }
644 
645         // Get the user shortcut type from settings provider.
646         mUserShortcutTypes = AccessibilityUtil.getUserShortcutTypesFromSettings(getPrefContext(),
647                 mComponentName);
648         if (mUserShortcutTypes != UserShortcutType.EMPTY) {
649             setUserShortcutType(getPrefContext(), mUserShortcutTypes);
650         } else {
651             //  Get the user shortcut type from shared_prefs if cannot get from settings provider.
652             mUserShortcutTypes = getUserShortcutTypes(getPrefContext(), UserShortcutType.SOFTWARE);
653         }
654     }
655 
initShortcutPreference(Bundle savedInstanceState)656     private void initShortcutPreference(Bundle savedInstanceState) {
657         // Restore the user shortcut type.
658         if (savedInstanceState != null && savedInstanceState.containsKey(EXTRA_SHORTCUT_TYPE)) {
659             mUserShortcutTypesCache = savedInstanceState.getInt(EXTRA_SHORTCUT_TYPE,
660                     UserShortcutType.EMPTY);
661         }
662 
663         // Initial the shortcut preference.
664         mShortcutPreference = new ShortcutPreference(getPrefContext(), null);
665         mShortcutPreference.setPersistent(false);
666         mShortcutPreference.setKey(getShortcutPreferenceKey());
667         mShortcutPreference.setOnClickCallback(this);
668 
669         final CharSequence title = getString(R.string.accessibility_shortcut_title, mPackageName);
670         mShortcutPreference.setTitle(title);
671     }
672 
updateShortcutPreference()673     protected void updateShortcutPreference() {
674         if (mComponentName == null) {
675             return;
676         }
677 
678         final int shortcutTypes = getUserShortcutTypes(getPrefContext(), UserShortcutType.SOFTWARE);
679         mShortcutPreference.setChecked(
680                     AccessibilityUtil.hasValuesInSettings(getPrefContext(), shortcutTypes,
681                             mComponentName));
682         mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext()));
683     }
684 
getShortcutPreferenceKey()685     private String getShortcutPreferenceKey() {
686         return KEY_SHORTCUT_PREFERENCE;
687     }
688 
689     @Override
onToggleClicked(ShortcutPreference preference)690     public void onToggleClicked(ShortcutPreference preference) {
691         if (mComponentName == null) {
692             return;
693         }
694 
695         final int shortcutTypes = getUserShortcutTypes(getPrefContext(), UserShortcutType.SOFTWARE);
696         if (preference.isChecked()) {
697             AccessibilityUtil.optInAllValuesToSettings(getPrefContext(), shortcutTypes,
698                     mComponentName);
699             showDialog(DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL);
700         } else {
701             AccessibilityUtil.optOutAllValuesFromSettings(getPrefContext(), shortcutTypes,
702                     mComponentName);
703         }
704         mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext()));
705     }
706 
707     @Override
onSettingsClicked(ShortcutPreference preference)708     public void onSettingsClicked(ShortcutPreference preference) {
709         // Do not restore shortcut in shortcut chooser dialog when shortcutPreference is turned off.
710         mUserShortcutTypesCache = mShortcutPreference.isChecked()
711                 ? getUserShortcutTypes(getPrefContext(), UserShortcutType.SOFTWARE)
712                 : UserShortcutType.EMPTY;
713     }
714 
createFooterPreference(CharSequence title)715     private void createFooterPreference(CharSequence title) {
716         final PreferenceScreen preferenceScreen = getPreferenceScreen();
717         preferenceScreen.addPreference(new FooterPreference.Builder(getActivity()).setTitle(
718                 title).build());
719     }
720 
721     /**
722      *  Setups a configurable default if the setting has never been set.
723      */
setupDefaultShortcutIfNecessary(Context context)724     private static void setupDefaultShortcutIfNecessary(Context context) {
725         final String targetKey = Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE;
726         String targetString = Settings.Secure.getString(context.getContentResolver(), targetKey);
727         if (!TextUtils.isEmpty(targetString)) {
728             // The shortcut setting has been set
729             return;
730         }
731 
732         // AccessibilityManager#getAccessibilityShortcutTargets may not return correct shortcut
733         // targets during boot. Needs to read settings directly here.
734         targetString = AccessibilityUtils.getShortcutTargetServiceComponentNameString(context,
735                 UserHandle.myUserId());
736         if (TextUtils.isEmpty(targetString)) {
737             // No configurable default accessibility service
738             return;
739         }
740 
741         // Only fallback to default accessibility service when setting is never updated.
742         final ComponentName shortcutName = ComponentName.unflattenFromString(targetString);
743         if (shortcutName != null) {
744             Settings.Secure.putString(context.getContentResolver(), targetKey,
745                     shortcutName.flattenToString());
746         }
747     }
748 }
749