• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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.Activity;
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.PackageManager;
30 import android.content.pm.ResolveInfo;
31 import android.content.pm.ServiceInfo;
32 import android.graphics.drawable.Drawable;
33 import android.icu.text.CaseMap;
34 import android.net.Uri;
35 import android.os.Bundle;
36 import android.os.Handler;
37 import android.provider.Settings;
38 import android.service.quicksettings.TileService;
39 import android.text.Html;
40 import android.text.TextUtils;
41 import android.view.LayoutInflater;
42 import android.view.View;
43 import android.view.ViewGroup;
44 import android.view.accessibility.AccessibilityManager;
45 import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener;
46 import android.widget.CheckBox;
47 import android.widget.ImageView;
48 import android.widget.Switch;
49 
50 import androidx.annotation.VisibleForTesting;
51 import androidx.preference.Preference;
52 import androidx.preference.PreferenceCategory;
53 import androidx.preference.PreferenceScreen;
54 
55 import com.android.settings.R;
56 import com.android.settings.SettingsActivity;
57 import com.android.settings.accessibility.AccessibilityDialogUtils.DialogType;
58 import com.android.settings.accessibility.AccessibilityUtil.QuickSettingsTooltipType;
59 import com.android.settings.accessibility.AccessibilityUtil.UserShortcutType;
60 import com.android.settings.dashboard.DashboardFragment;
61 import com.android.settings.utils.LocaleUtils;
62 import com.android.settings.widget.SettingsMainSwitchBar;
63 import com.android.settings.widget.SettingsMainSwitchPreference;
64 import com.android.settingslib.widget.IllustrationPreference;
65 import com.android.settingslib.widget.OnMainSwitchChangeListener;
66 import com.android.settingslib.widget.TopIntroPreference;
67 
68 import com.google.android.setupcompat.util.WizardManagerHelper;
69 
70 import java.util.ArrayList;
71 import java.util.List;
72 import java.util.Locale;
73 
74 /**
75  * Base class for accessibility fragments with toggle, shortcut, some helper functions
76  * and dialog management.
77  */
78 public abstract class ToggleFeaturePreferenceFragment extends DashboardFragment
79         implements ShortcutPreference.OnClickCallback, OnMainSwitchChangeListener {
80 
81     public static final String KEY_GENERAL_CATEGORY = "general_categories";
82     public static final String KEY_SHORTCUT_PREFERENCE = "shortcut_preference";
83     public static final int NOT_SET = -1;
84     protected static final String KEY_TOP_INTRO_PREFERENCE = "top_intro";
85     protected static final String KEY_USE_SERVICE_PREFERENCE = "use_service";
86     protected static final String KEY_HTML_DESCRIPTION_PREFERENCE = "html_description";
87     protected static final String KEY_SAVED_USER_SHORTCUT_TYPE = "shortcut_type";
88     protected static final String KEY_SAVED_QS_TOOLTIP_RESHOW = "qs_tooltip_reshow";
89     protected static final String KEY_SAVED_QS_TOOLTIP_TYPE = "qs_tooltip_type";
90     protected static final String KEY_ANIMATED_IMAGE = "animated_image";
91     // For html description of accessibility service, must follow the rule, such as
92     // <img src="R.drawable.fileName"/>, a11y settings will get the resources successfully.
93     private static final String IMG_PREFIX = "R.drawable.";
94     private static final String DRAWABLE_FOLDER = "drawable";
95 
96     protected TopIntroPreference mTopIntroPreference;
97     protected SettingsMainSwitchPreference mToggleServiceSwitchPreference;
98     protected ShortcutPreference mShortcutPreference;
99     protected Preference mSettingsPreference;
100     protected AccessibilityFooterPreferenceController mFooterPreferenceController;
101     protected String mPreferenceKey;
102     protected Dialog mDialog;
103     protected CharSequence mSettingsTitle;
104     protected Intent mSettingsIntent;
105     // The mComponentName maybe null, such as Magnify
106     protected ComponentName mComponentName;
107     protected CharSequence mPackageName;
108     protected Uri mImageUri;
109     protected CharSequence mHtmlDescription;
110     protected CharSequence mTopIntroTitle;
111     // Save user's shortcutType value when savedInstance has value (e.g. device rotated).
112     protected int mSavedCheckBoxValue = NOT_SET;
113     private CharSequence mDescription;
114     private TouchExplorationStateChangeListener mTouchExplorationStateChangeListener;
115     private AccessibilitySettingsContentObserver mSettingsContentObserver;
116 
117     private CheckBox mSoftwareTypeCheckBox;
118     private CheckBox mHardwareTypeCheckBox;
119 
120     private AccessibilityQuickSettingsTooltipWindow mTooltipWindow;
121     private boolean mNeedsQSTooltipReshow = false;
122     private int mNeedsQSTooltipType = QuickSettingsTooltipType.GUIDE_TO_EDIT;
123     private ImageView mImageGetterCacheView;
124     protected final Html.ImageGetter mImageGetter = (String str) -> {
125         if (str != null && str.startsWith(IMG_PREFIX)) {
126             final String fileName = str.substring(IMG_PREFIX.length());
127             return getDrawableFromUri(Uri.parse(
128                     ContentResolver.SCHEME_ANDROID_RESOURCE + "://"
129                             + mComponentName.getPackageName() + "/" + DRAWABLE_FOLDER + "/"
130                             + fileName));
131         }
132         return null;
133     };
134 
135     @Override
onCreate(Bundle savedInstanceState)136     public void onCreate(Bundle savedInstanceState) {
137         super.onCreate(savedInstanceState);
138 
139         onProcessArguments(getArguments());
140         // Restore the user shortcut type and tooltip.
141         if (savedInstanceState != null) {
142             if (savedInstanceState.containsKey(KEY_SAVED_USER_SHORTCUT_TYPE)) {
143                 mSavedCheckBoxValue = savedInstanceState.getInt(KEY_SAVED_USER_SHORTCUT_TYPE,
144                         NOT_SET);
145             }
146             if (savedInstanceState.containsKey(KEY_SAVED_QS_TOOLTIP_RESHOW)) {
147                 mNeedsQSTooltipReshow = savedInstanceState.getBoolean(KEY_SAVED_QS_TOOLTIP_RESHOW);
148             }
149             if (savedInstanceState.containsKey(KEY_SAVED_QS_TOOLTIP_TYPE)) {
150                 mNeedsQSTooltipType = savedInstanceState.getInt(KEY_SAVED_QS_TOOLTIP_TYPE);
151             }
152         }
153 
154         final int resId = getPreferenceScreenResId();
155         if (resId <= 0) {
156             final PreferenceScreen preferenceScreen = getPreferenceManager().createPreferenceScreen(
157                     getPrefContext());
158             setPreferenceScreen(preferenceScreen);
159         }
160 
161         mSettingsContentObserver = new AccessibilitySettingsContentObserver(new Handler());
162         registerKeysToObserverCallback(mSettingsContentObserver);
163     }
164 
registerKeysToObserverCallback( AccessibilitySettingsContentObserver contentObserver)165     protected void registerKeysToObserverCallback(
166             AccessibilitySettingsContentObserver contentObserver) {
167         final List<String> shortcutFeatureKeys = getShortcutFeatureSettingsKeys();
168 
169         contentObserver.registerKeysToObserverCallback(shortcutFeatureKeys, key -> {
170             updateShortcutPreferenceData();
171             updateShortcutPreference();
172         });
173     }
174 
getShortcutFeatureSettingsKeys()175     protected List<String> getShortcutFeatureSettingsKeys() {
176         final List<String> shortcutFeatureKeys = new ArrayList<>();
177         shortcutFeatureKeys.add(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS);
178         shortcutFeatureKeys.add(Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE);
179         return shortcutFeatureKeys;
180     }
181 
182     @Override
onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)183     public View onCreateView(LayoutInflater inflater, ViewGroup container,
184             Bundle savedInstanceState) {
185         initTopIntroPreference();
186         initAnimatedImagePreference();
187         initToggleServiceSwitchPreference();
188         initGeneralCategory();
189         initShortcutPreference();
190         initSettingsPreference();
191         initHtmlTextPreference();
192         initFooterPreference();
193 
194         installActionBarToggleSwitch();
195 
196         updateToggleServiceTitle(mToggleServiceSwitchPreference);
197 
198         mTouchExplorationStateChangeListener = isTouchExplorationEnabled -> {
199             removeDialog(DialogEnums.EDIT_SHORTCUT);
200             mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext()));
201         };
202 
203         updatePreferenceOrder();
204         return super.onCreateView(inflater, container, savedInstanceState);
205     }
206 
207     @Override
onCreateDialog(int dialogId)208     public Dialog onCreateDialog(int dialogId) {
209         switch (dialogId) {
210             case DialogEnums.EDIT_SHORTCUT:
211                 final int dialogType = WizardManagerHelper.isAnySetupWizard(getIntent())
212                         ? DialogType.EDIT_SHORTCUT_GENERIC_SUW : DialogType.EDIT_SHORTCUT_GENERIC;
213                 mDialog = AccessibilityDialogUtils.showEditShortcutDialog(
214                         getPrefContext(), dialogType, getShortcutTitle(),
215                         this::callOnAlertDialogCheckboxClicked);
216                 setupEditShortcutDialog(mDialog);
217                 return mDialog;
218             case DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL:
219                 if (WizardManagerHelper.isAnySetupWizard(getIntent())) {
220                     mDialog = AccessibilityGestureNavigationTutorial
221                             .createAccessibilityTutorialDialogForSetupWizard(
222                                     getPrefContext(), getUserShortcutTypes(),
223                                     this::callOnTutorialDialogButtonClicked);
224                 } else {
225                     mDialog = AccessibilityGestureNavigationTutorial
226                             .createAccessibilityTutorialDialog(
227                                     getPrefContext(), getUserShortcutTypes(),
228                                     this::callOnTutorialDialogButtonClicked);
229                 }
230                 mDialog.setCanceledOnTouchOutside(false);
231                 return mDialog;
232             default:
233                 throw new IllegalArgumentException("Unsupported dialogId " + dialogId);
234         }
235     }
236 
237     @Override
onViewCreated(View view, Bundle savedInstanceState)238     public void onViewCreated(View view, Bundle savedInstanceState) {
239         super.onViewCreated(view, savedInstanceState);
240 
241         final SettingsActivity settingsActivity = (SettingsActivity) getActivity();
242         final SettingsMainSwitchBar switchBar = settingsActivity.getSwitchBar();
243         switchBar.hide();
244 
245         // Reshow tooltip when activity recreate, such as rotate device.
246         if (mNeedsQSTooltipReshow) {
247             view.post(() -> {
248                 final Activity activity = getActivity();
249                 if (activity != null && !activity.isFinishing()) {
250                     showQuickSettingsTooltipIfNeeded();
251                 }
252             });
253         }
254 
255         writeConfigDefaultAccessibilityServiceIntoShortcutTargetServiceIfNeeded(getContext());
256     }
257 
258     @Override
onResume()259     public void onResume() {
260         super.onResume();
261 
262         final AccessibilityManager am = getPrefContext().getSystemService(
263                 AccessibilityManager.class);
264         am.addTouchExplorationStateChangeListener(mTouchExplorationStateChangeListener);
265         mSettingsContentObserver.register(getContentResolver());
266         updateShortcutPreferenceData();
267         updateShortcutPreference();
268 
269         updateEditShortcutDialogIfNeeded();
270     }
271 
272     @Override
onPause()273     public void onPause() {
274         final AccessibilityManager am = getPrefContext().getSystemService(
275                 AccessibilityManager.class);
276         am.removeTouchExplorationStateChangeListener(mTouchExplorationStateChangeListener);
277         mSettingsContentObserver.unregister(getContentResolver());
278         super.onPause();
279     }
280 
281     @Override
onSaveInstanceState(Bundle outState)282     public void onSaveInstanceState(Bundle outState) {
283         final int value = getShortcutTypeCheckBoxValue();
284         if (value != NOT_SET) {
285             outState.putInt(KEY_SAVED_USER_SHORTCUT_TYPE, value);
286         }
287         final boolean isTooltipWindowShowing = mTooltipWindow != null && mTooltipWindow.isShowing();
288         if (mNeedsQSTooltipReshow || isTooltipWindowShowing) {
289             outState.putBoolean(KEY_SAVED_QS_TOOLTIP_RESHOW, /* value= */ true);
290             outState.putInt(KEY_SAVED_QS_TOOLTIP_TYPE, mNeedsQSTooltipType);
291         }
292         super.onSaveInstanceState(outState);
293     }
294 
295     @Override
onDestroyView()296     public void onDestroyView() {
297         super.onDestroyView();
298         removeActionBarToggleSwitch();
299         final boolean isTooltipWindowShowing = mTooltipWindow != null && mTooltipWindow.isShowing();
300         if (isTooltipWindowShowing) {
301             mTooltipWindow.dismiss();
302         }
303     }
304 
305     @Override
getDialogMetricsCategory(int dialogId)306     public int getDialogMetricsCategory(int dialogId) {
307         switch (dialogId) {
308             case DialogEnums.EDIT_SHORTCUT:
309                 return SettingsEnums.DIALOG_ACCESSIBILITY_SERVICE_EDIT_SHORTCUT;
310             case DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL:
311                 return SettingsEnums.DIALOG_ACCESSIBILITY_TUTORIAL;
312             default:
313                 return SettingsEnums.ACTION_UNKNOWN;
314         }
315     }
316 
317     @Override
getMetricsCategory()318     public int getMetricsCategory() {
319         return SettingsEnums.ACCESSIBILITY_SERVICE;
320     }
321 
322     @Override
getHelpResource()323     public int getHelpResource() {
324         return 0;
325     }
326 
327     @Override
onSwitchChanged(Switch switchView, boolean isChecked)328     public void onSwitchChanged(Switch switchView, boolean isChecked) {
329         onPreferenceToggled(mPreferenceKey, isChecked);
330     }
331 
332     /**
333      * Returns the shortcut type list which has been checked by user.
334      */
getUserShortcutTypes()335     abstract int getUserShortcutTypes();
336 
337     /** Returns the accessibility tile component name. */
getTileComponentName()338     abstract ComponentName getTileComponentName();
339 
340     /** Returns the accessibility tile tooltip content. */
getTileTooltipContent(@uickSettingsTooltipType int type)341     abstract CharSequence getTileTooltipContent(@QuickSettingsTooltipType int type);
342 
updateToggleServiceTitle(SettingsMainSwitchPreference switchPreference)343     protected void updateToggleServiceTitle(SettingsMainSwitchPreference switchPreference) {
344         final CharSequence title =
345                 getString(R.string.accessibility_service_primary_switch_title, mPackageName);
346         switchPreference.setTitle(title);
347     }
348 
getShortcutTitle()349     protected CharSequence getShortcutTitle() {
350         return getString(R.string.accessibility_shortcut_title, mPackageName);
351     }
352 
onPreferenceToggled(String preferenceKey, boolean enabled)353     protected void onPreferenceToggled(String preferenceKey, boolean enabled) {
354         if (enabled) {
355             showQuickSettingsTooltipIfNeeded();
356         }
357     }
358 
onInstallSwitchPreferenceToggleSwitch()359     protected void onInstallSwitchPreferenceToggleSwitch() {
360         // Implement this to set a checked listener.
361         updateSwitchBarToggleSwitch();
362         mToggleServiceSwitchPreference.addOnSwitchChangeListener(this);
363     }
364 
onRemoveSwitchPreferenceToggleSwitch()365     protected void onRemoveSwitchPreferenceToggleSwitch() {
366         // Implement this to reset a checked listener.
367     }
368 
updateSwitchBarToggleSwitch()369     protected void updateSwitchBarToggleSwitch() {
370         // Implement this to update the state of switch.
371     }
372 
setTitle(String title)373     public void setTitle(String title) {
374         getActivity().setTitle(title);
375     }
376 
onProcessArguments(Bundle arguments)377     protected void onProcessArguments(Bundle arguments) {
378         // Key.
379         mPreferenceKey = arguments.getString(AccessibilitySettings.EXTRA_PREFERENCE_KEY);
380 
381         // Title.
382         if (arguments.containsKey(AccessibilitySettings.EXTRA_RESOLVE_INFO)) {
383             ResolveInfo info = arguments.getParcelable(AccessibilitySettings.EXTRA_RESOLVE_INFO);
384             getActivity().setTitle(info.loadLabel(getPackageManager()).toString());
385         } else if (arguments.containsKey(AccessibilitySettings.EXTRA_TITLE)) {
386             setTitle(arguments.getString(AccessibilitySettings.EXTRA_TITLE));
387         }
388 
389         // Summary.
390         if (arguments.containsKey(AccessibilitySettings.EXTRA_SUMMARY)) {
391             mDescription = arguments.getCharSequence(AccessibilitySettings.EXTRA_SUMMARY);
392         }
393 
394         // Settings html description.
395         if (arguments.containsKey(AccessibilitySettings.EXTRA_HTML_DESCRIPTION)) {
396             mHtmlDescription = arguments.getCharSequence(
397                     AccessibilitySettings.EXTRA_HTML_DESCRIPTION);
398         }
399 
400         // Intro.
401         if (arguments.containsKey(AccessibilitySettings.EXTRA_INTRO)) {
402             mTopIntroTitle = arguments.getCharSequence(AccessibilitySettings.EXTRA_INTRO);
403         }
404     }
405 
installActionBarToggleSwitch()406     private void installActionBarToggleSwitch() {
407         onInstallSwitchPreferenceToggleSwitch();
408     }
409 
removeActionBarToggleSwitch()410     private void removeActionBarToggleSwitch() {
411         mToggleServiceSwitchPreference.setOnPreferenceClickListener(null);
412         onRemoveSwitchPreferenceToggleSwitch();
413     }
414 
updatePreferenceOrder()415     private void updatePreferenceOrder() {
416         final List<String> lists = getPreferenceOrderList();
417 
418         final PreferenceScreen preferenceScreen = getPreferenceScreen();
419         preferenceScreen.setOrderingAsAdded(false);
420 
421         final int size = lists.size();
422         for (int i = 0; i < size; i++) {
423             final Preference preference = preferenceScreen.findPreference(lists.get(i));
424             if (preference != null) {
425                 preference.setOrder(i);
426             }
427         }
428     }
429 
430     /** Customizes the order by preference key. */
getPreferenceOrderList()431     protected List<String> getPreferenceOrderList() {
432         final List<String> lists = new ArrayList<>();
433         lists.add(KEY_TOP_INTRO_PREFERENCE);
434         lists.add(KEY_ANIMATED_IMAGE);
435         lists.add(KEY_USE_SERVICE_PREFERENCE);
436         lists.add(KEY_GENERAL_CATEGORY);
437         lists.add(KEY_HTML_DESCRIPTION_PREFERENCE);
438         return lists;
439     }
440 
getDrawableFromUri(Uri imageUri)441     private Drawable getDrawableFromUri(Uri imageUri) {
442         if (mImageGetterCacheView == null) {
443             mImageGetterCacheView = new ImageView(getPrefContext());
444         }
445 
446         mImageGetterCacheView.setAdjustViewBounds(true);
447         mImageGetterCacheView.setImageURI(imageUri);
448 
449         if (mImageGetterCacheView.getDrawable() == null) {
450             return null;
451         }
452 
453         final Drawable drawable =
454                 mImageGetterCacheView.getDrawable().mutate().getConstantState().newDrawable();
455         mImageGetterCacheView.setImageURI(null);
456         final int imageWidth = drawable.getIntrinsicWidth();
457         final int imageHeight = drawable.getIntrinsicHeight();
458         final int screenHalfHeight = AccessibilityUtil.getScreenHeightPixels(getPrefContext()) / 2;
459         if ((imageWidth > AccessibilityUtil.getScreenWidthPixels(getPrefContext()))
460                 || (imageHeight > screenHalfHeight)) {
461             return null;
462         }
463 
464         drawable.setBounds(/* left= */0, /* top= */0, drawable.getIntrinsicWidth(),
465                 drawable.getIntrinsicHeight());
466 
467         return drawable;
468     }
469 
initAnimatedImagePreference()470     private void initAnimatedImagePreference() {
471         if (mImageUri == null) {
472             return;
473         }
474 
475         final int displayHalfHeight =
476                 AccessibilityUtil.getDisplayBounds(getPrefContext()).height() / 2;
477         final IllustrationPreference illustrationPreference =
478                 new IllustrationPreference(getPrefContext());
479         illustrationPreference.setImageUri(mImageUri);
480         illustrationPreference.setSelectable(false);
481         illustrationPreference.setMaxHeight(displayHalfHeight);
482         illustrationPreference.setKey(KEY_ANIMATED_IMAGE);
483 
484         getPreferenceScreen().addPreference(illustrationPreference);
485     }
486 
487     @VisibleForTesting
initTopIntroPreference()488     void initTopIntroPreference() {
489         if (TextUtils.isEmpty(mTopIntroTitle)) {
490             return;
491         }
492         mTopIntroPreference = new TopIntroPreference(getPrefContext());
493         mTopIntroPreference.setKey(KEY_TOP_INTRO_PREFERENCE);
494         mTopIntroPreference.setTitle(mTopIntroTitle);
495         getPreferenceScreen().addPreference(mTopIntroPreference);
496     }
497 
initToggleServiceSwitchPreference()498     private void initToggleServiceSwitchPreference() {
499         mToggleServiceSwitchPreference = new SettingsMainSwitchPreference(getPrefContext());
500         mToggleServiceSwitchPreference.setKey(KEY_USE_SERVICE_PREFERENCE);
501         if (getArguments().containsKey(AccessibilitySettings.EXTRA_CHECKED)) {
502             final boolean enabled = getArguments().getBoolean(AccessibilitySettings.EXTRA_CHECKED);
503             mToggleServiceSwitchPreference.setChecked(enabled);
504         }
505 
506         getPreferenceScreen().addPreference(mToggleServiceSwitchPreference);
507     }
508 
initGeneralCategory()509     private void initGeneralCategory() {
510         final PreferenceCategory generalCategory = new PreferenceCategory(getPrefContext());
511         generalCategory.setKey(KEY_GENERAL_CATEGORY);
512         generalCategory.setTitle(R.string.accessibility_screen_option);
513 
514         getPreferenceScreen().addPreference(generalCategory);
515     }
516 
initShortcutPreference()517     protected void initShortcutPreference() {
518         // Initial the shortcut preference.
519         mShortcutPreference = new ShortcutPreference(getPrefContext(), /* attrs= */ null);
520         mShortcutPreference.setPersistent(false);
521         mShortcutPreference.setKey(getShortcutPreferenceKey());
522         mShortcutPreference.setOnClickCallback(this);
523         mShortcutPreference.setTitle(getShortcutTitle());
524 
525         final PreferenceCategory generalCategory = findPreference(KEY_GENERAL_CATEGORY);
526         generalCategory.addPreference(mShortcutPreference);
527     }
528 
initSettingsPreference()529     protected void initSettingsPreference() {
530         if (mSettingsTitle == null || mSettingsIntent == null) {
531             return;
532         }
533 
534         // Show the "Settings" menu as if it were a preference screen.
535         mSettingsPreference = new Preference(getPrefContext());
536         mSettingsPreference.setTitle(mSettingsTitle);
537         mSettingsPreference.setIconSpaceReserved(false);
538         mSettingsPreference.setIntent(mSettingsIntent);
539 
540         final PreferenceCategory generalCategory = findPreference(KEY_GENERAL_CATEGORY);
541         generalCategory.addPreference(mSettingsPreference);
542     }
543 
initHtmlTextPreference()544     private void initHtmlTextPreference() {
545         if (TextUtils.isEmpty(mHtmlDescription)) {
546             return;
547         }
548         final PreferenceScreen screen = getPreferenceScreen();
549         final CharSequence htmlDescription = Html.fromHtml(mHtmlDescription.toString(),
550                 Html.FROM_HTML_MODE_COMPACT, mImageGetter, /* tagHandler= */ null);
551 
552         final AccessibilityFooterPreference htmlFooterPreference =
553                 new AccessibilityFooterPreference(screen.getContext());
554         htmlFooterPreference.setKey(KEY_HTML_DESCRIPTION_PREFERENCE);
555         htmlFooterPreference.setSummary(htmlDescription);
556         screen.addPreference(htmlFooterPreference);
557 
558         // TODO(b/171272809): Migrate to DashboardFragment.
559         final String title = getString(R.string.accessibility_introduction_title, mPackageName);
560         mFooterPreferenceController = new AccessibilityFooterPreferenceController(
561                 screen.getContext(), htmlFooterPreference.getKey());
562         mFooterPreferenceController.setIntroductionTitle(title);
563         mFooterPreferenceController.displayPreference(screen);
564     }
565 
initFooterPreference()566     private void initFooterPreference() {
567         if (!TextUtils.isEmpty(mDescription)) {
568             createFooterPreference(getPreferenceScreen(), mDescription,
569                     getString(R.string.accessibility_introduction_title, mPackageName));
570         }
571     }
572 
573 
574     /**
575      * Creates {@link AccessibilityFooterPreference} and append into {@link PreferenceScreen}
576      *
577      * @param screen The preference screen to add the footer preference
578      * @param summary The summary of the preference summary
579      * @param introductionTitle The title of introduction in the footer
580      */
581     @VisibleForTesting
createFooterPreference(PreferenceScreen screen, CharSequence summary, String introductionTitle)582     void createFooterPreference(PreferenceScreen screen, CharSequence summary,
583             String introductionTitle) {
584         final AccessibilityFooterPreference footerPreference =
585                 new AccessibilityFooterPreference(screen.getContext());
586         footerPreference.setSummary(summary);
587         screen.addPreference(footerPreference);
588 
589         mFooterPreferenceController = new AccessibilityFooterPreferenceController(
590                 screen.getContext(), footerPreference.getKey());
591         mFooterPreferenceController.setIntroductionTitle(introductionTitle);
592         mFooterPreferenceController.displayPreference(screen);
593     }
594 
595     @VisibleForTesting
setupEditShortcutDialog(Dialog dialog)596     void setupEditShortcutDialog(Dialog dialog) {
597         final View dialogSoftwareView = dialog.findViewById(R.id.software_shortcut);
598         mSoftwareTypeCheckBox = dialogSoftwareView.findViewById(R.id.checkbox);
599         setDialogTextAreaClickListener(dialogSoftwareView, mSoftwareTypeCheckBox);
600 
601         final View dialogHardwareView = dialog.findViewById(R.id.hardware_shortcut);
602         mHardwareTypeCheckBox = dialogHardwareView.findViewById(R.id.checkbox);
603         setDialogTextAreaClickListener(dialogHardwareView, mHardwareTypeCheckBox);
604 
605         updateEditShortcutDialogCheckBox();
606     }
607 
setDialogTextAreaClickListener(View dialogView, CheckBox checkBox)608     private void setDialogTextAreaClickListener(View dialogView, CheckBox checkBox) {
609         final View dialogTextArea = dialogView.findViewById(R.id.container);
610         dialogTextArea.setOnClickListener(v -> checkBox.toggle());
611     }
612 
updateEditShortcutDialogCheckBox()613     private void updateEditShortcutDialogCheckBox() {
614         // If it is during onConfigChanged process then restore the value, or get the saved value
615         // when shortcutPreference is checked.
616         int value = restoreOnConfigChangedValue();
617         if (value == NOT_SET) {
618             final int lastNonEmptyUserShortcutType = PreferredShortcuts.retrieveUserShortcutType(
619                     getPrefContext(), mComponentName.flattenToString(), UserShortcutType.SOFTWARE);
620             value = mShortcutPreference.isChecked() ? lastNonEmptyUserShortcutType
621                     : UserShortcutType.EMPTY;
622         }
623 
624         mSoftwareTypeCheckBox.setChecked(
625                 hasShortcutType(value, UserShortcutType.SOFTWARE));
626         mHardwareTypeCheckBox.setChecked(
627                 hasShortcutType(value, UserShortcutType.HARDWARE));
628     }
629 
restoreOnConfigChangedValue()630     private int restoreOnConfigChangedValue() {
631         final int savedValue = mSavedCheckBoxValue;
632         mSavedCheckBoxValue = NOT_SET;
633         return savedValue;
634     }
635 
hasShortcutType(int value, @UserShortcutType int type)636     private boolean hasShortcutType(int value, @UserShortcutType int type) {
637         return (value & type) == type;
638     }
639 
640     /**
641      * Returns accumulated {@link UserShortcutType} checkbox value or {@code NOT_SET} if checkboxes
642      * did not exist.
643      */
getShortcutTypeCheckBoxValue()644     protected int getShortcutTypeCheckBoxValue() {
645         if (mSoftwareTypeCheckBox == null || mHardwareTypeCheckBox == null) {
646             return NOT_SET;
647         }
648 
649         int value = UserShortcutType.EMPTY;
650         if (mSoftwareTypeCheckBox.isChecked()) {
651             value |= UserShortcutType.SOFTWARE;
652         }
653         if (mHardwareTypeCheckBox.isChecked()) {
654             value |= UserShortcutType.HARDWARE;
655         }
656         return value;
657     }
658 
getShortcutTypeSummary(Context context)659     protected CharSequence getShortcutTypeSummary(Context context) {
660         if (!mShortcutPreference.isSettingsEditable()) {
661             return context.getText(R.string.accessibility_shortcut_edit_dialog_title_hardware);
662         }
663 
664         if (!mShortcutPreference.isChecked()) {
665             return context.getText(R.string.switch_off_text);
666         }
667 
668         final int shortcutTypes = PreferredShortcuts.retrieveUserShortcutType(context,
669                 mComponentName.flattenToString(), UserShortcutType.SOFTWARE);
670 
671         final List<CharSequence> list = new ArrayList<>();
672         if (hasShortcutType(shortcutTypes, UserShortcutType.SOFTWARE)) {
673             list.add(getSoftwareShortcutTypeSummary(context));
674         }
675         if (hasShortcutType(shortcutTypes, UserShortcutType.HARDWARE)) {
676             final CharSequence hardwareTitle = context.getText(
677                     R.string.accessibility_shortcut_hardware_keyword);
678             list.add(hardwareTitle);
679         }
680 
681         // Show software shortcut if first time to use.
682         if (list.isEmpty()) {
683             list.add(getSoftwareShortcutTypeSummary(context));
684         }
685 
686         return CaseMap.toTitle().wholeString().noLowercase().apply(Locale.getDefault(), /* iter= */
687                 null, LocaleUtils.getConcatenatedString(list));
688     }
689 
getSoftwareShortcutTypeSummary(Context context)690     private static CharSequence getSoftwareShortcutTypeSummary(Context context) {
691         int resId;
692         if (AccessibilityUtil.isFloatingMenuEnabled(context)) {
693             resId = R.string.accessibility_shortcut_edit_summary_software;
694         } else if (AccessibilityUtil.isGestureNavigateEnabled(context)) {
695             resId = R.string.accessibility_shortcut_edit_summary_software_gesture;
696         } else {
697             resId = R.string.accessibility_shortcut_edit_summary_software;
698         }
699         return context.getText(resId);
700     }
701 
702     /**
703      * This method will be invoked when a button in the tutorial dialog is clicked.
704      *
705      * @param dialog The dialog that received the click
706      * @param which  The button that was clicked
707      */
callOnTutorialDialogButtonClicked(DialogInterface dialog, int which)708     private void callOnTutorialDialogButtonClicked(DialogInterface dialog, int which) {
709         dialog.dismiss();
710         showQuickSettingsTooltipIfNeeded();
711     }
712 
713     /**
714      * This method will be invoked when a button in the edit shortcut dialog is clicked.
715      *
716      * @param dialog The dialog that received the click
717      * @param which  The button that was clicked
718      */
callOnAlertDialogCheckboxClicked(DialogInterface dialog, int which)719     protected void callOnAlertDialogCheckboxClicked(DialogInterface dialog, int which) {
720         if (mComponentName == null) {
721             return;
722         }
723 
724         final int value = getShortcutTypeCheckBoxValue();
725         saveNonEmptyUserShortcutType(value);
726         AccessibilityUtil.optInAllValuesToSettings(getPrefContext(), value, mComponentName);
727         AccessibilityUtil.optOutAllValuesFromSettings(getPrefContext(), ~value, mComponentName);
728         final boolean shortcutAssigned = value != UserShortcutType.EMPTY;
729         mShortcutPreference.setChecked(shortcutAssigned);
730         mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext()));
731 
732         if (mHardwareTypeCheckBox.isChecked()) {
733             AccessibilityUtil.skipVolumeShortcutDialogTimeoutRestriction(getPrefContext());
734         }
735 
736         // Show the quick setting tooltip if the shortcut assigned in the first time
737         if (shortcutAssigned) {
738             showQuickSettingsTooltipIfNeeded();
739         }
740     }
741 
updateShortcutPreferenceData()742     protected void updateShortcutPreferenceData() {
743         if (mComponentName == null) {
744             return;
745         }
746 
747         final int shortcutTypes = AccessibilityUtil.getUserShortcutTypesFromSettings(
748                 getPrefContext(), mComponentName);
749         if (shortcutTypes != UserShortcutType.EMPTY) {
750             final PreferredShortcut shortcut = new PreferredShortcut(
751                     mComponentName.flattenToString(), shortcutTypes);
752             PreferredShortcuts.saveUserShortcutType(getPrefContext(), shortcut);
753         }
754     }
755 
updateShortcutPreference()756     protected void updateShortcutPreference() {
757         if (mComponentName == null) {
758             return;
759         }
760 
761         final int shortcutTypes = PreferredShortcuts.retrieveUserShortcutType(getPrefContext(),
762                 mComponentName.flattenToString(), UserShortcutType.SOFTWARE);
763         mShortcutPreference.setChecked(
764                 AccessibilityUtil.hasValuesInSettings(getPrefContext(), shortcutTypes,
765                         mComponentName));
766         mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext()));
767     }
768 
getShortcutPreferenceKey()769     protected String getShortcutPreferenceKey() {
770         return KEY_SHORTCUT_PREFERENCE;
771     }
772 
773     @Override
onToggleClicked(ShortcutPreference preference)774     public void onToggleClicked(ShortcutPreference preference) {
775         if (mComponentName == null) {
776             return;
777         }
778 
779         final int shortcutTypes = PreferredShortcuts.retrieveUserShortcutType(getPrefContext(),
780                 mComponentName.flattenToString(), UserShortcutType.SOFTWARE);
781         if (preference.isChecked()) {
782             AccessibilityUtil.optInAllValuesToSettings(getPrefContext(), shortcutTypes,
783                     mComponentName);
784             showDialog(DialogEnums.LAUNCH_ACCESSIBILITY_TUTORIAL);
785         } else {
786             AccessibilityUtil.optOutAllValuesFromSettings(getPrefContext(), shortcutTypes,
787                     mComponentName);
788         }
789         mShortcutPreference.setSummary(getShortcutTypeSummary(getPrefContext()));
790     }
791 
792     @Override
onSettingsClicked(ShortcutPreference preference)793     public void onSettingsClicked(ShortcutPreference preference) {
794         showDialog(DialogEnums.EDIT_SHORTCUT);
795     }
796 
797     /**
798      * Setups {@link com.android.internal.R.string#config_defaultAccessibilityService} into
799      * {@link Settings.Secure#ACCESSIBILITY_SHORTCUT_TARGET_SERVICE} if that settings key has never
800      * been set and only write the key when user enter into corresponding page.
801      */
802     @VisibleForTesting
writeConfigDefaultAccessibilityServiceIntoShortcutTargetServiceIfNeeded(Context context)803     void writeConfigDefaultAccessibilityServiceIntoShortcutTargetServiceIfNeeded(Context context) {
804         if (mComponentName == null) {
805             return;
806         }
807 
808         // It might be shortened form (with a leading '.'). Need to unflatten back to ComponentName
809         // first, or it will encounter errors when getting service from
810         // `ACCESSIBILITY_SHORTCUT_TARGET_SERVICE`.
811         final ComponentName configDefaultService = ComponentName.unflattenFromString(
812                 getString(com.android.internal.R.string.config_defaultAccessibilityService));
813 
814         if (!mComponentName.equals(configDefaultService)) {
815             return;
816         }
817 
818         final String targetKey = Settings.Secure.ACCESSIBILITY_SHORTCUT_TARGET_SERVICE;
819         final String targetString = Settings.Secure.getString(context.getContentResolver(),
820                 targetKey);
821 
822         // By intentional, we only need to write the config string when the Settings key has never
823         // been set (== null). Empty string also means someone already wrote it before, so we need
824         // to respect the value.
825         if (targetString == null) {
826             Settings.Secure.putString(context.getContentResolver(), targetKey,
827                     configDefaultService.flattenToString());
828         }
829     }
830 
updateEditShortcutDialogIfNeeded()831     private void updateEditShortcutDialogIfNeeded() {
832         if (mDialog == null || !mDialog.isShowing()) {
833             return;
834         }
835         AccessibilityDialogUtils.updateShortcutInDialog(getContext(), mDialog);
836     }
837 
838     @VisibleForTesting
saveNonEmptyUserShortcutType(int type)839     void saveNonEmptyUserShortcutType(int type) {
840         if (type == UserShortcutType.EMPTY) {
841             return;
842         }
843 
844         final PreferredShortcut shortcut = new PreferredShortcut(
845                 mComponentName.flattenToString(), type);
846         PreferredShortcuts.saveUserShortcutType(getPrefContext(), shortcut);
847     }
848 
849     /**
850      * Shows the quick settings tooltip if the quick settings feature is assigned. The tooltip only
851      * shows once.
852      *
853      * @param type The quick settings tooltip type
854      */
showQuickSettingsTooltipIfNeeded(@uickSettingsTooltipType int type)855     protected void showQuickSettingsTooltipIfNeeded(@QuickSettingsTooltipType int type) {
856         mNeedsQSTooltipType = type;
857         showQuickSettingsTooltipIfNeeded();
858     }
859 
showQuickSettingsTooltipIfNeeded()860     private void showQuickSettingsTooltipIfNeeded() {
861         final ComponentName tileComponentName = getTileComponentName();
862         if (tileComponentName == null) {
863             // Returns if no tile service assigned.
864             return;
865         }
866 
867         if (!mNeedsQSTooltipReshow && AccessibilityQuickSettingUtils.hasValueInSharedPreferences(
868                 getContext(), tileComponentName)) {
869             // Returns if quick settings tooltip only show once.
870             return;
871         }
872 
873         final CharSequence content = getTileTooltipContent(mNeedsQSTooltipType);
874         if (TextUtils.isEmpty(content)) {
875             // Returns if no content of tile tooltip assigned.
876             return;
877         }
878 
879         final int imageResId = mNeedsQSTooltipType == QuickSettingsTooltipType.GUIDE_TO_EDIT
880                 ? R.drawable.accessibility_qs_tooltip_illustration
881                 : R.drawable.accessibility_auto_added_qs_tooltip_illustration;
882         mTooltipWindow = new AccessibilityQuickSettingsTooltipWindow(getContext());
883         mTooltipWindow.setup(content, imageResId);
884         mTooltipWindow.showAtTopCenter(getView());
885         AccessibilityQuickSettingUtils.optInValueToSharedPreferences(getContext(),
886                 tileComponentName);
887         mNeedsQSTooltipReshow = false;
888     }
889 
890     /** Returns user visible name of the tile by given {@link ComponentName}. */
loadTileLabel(Context context, ComponentName componentName)891     protected CharSequence loadTileLabel(Context context, ComponentName componentName) {
892         final PackageManager packageManager = context.getPackageManager();
893         final Intent queryIntent = new Intent(TileService.ACTION_QS_TILE);
894         final List<ResolveInfo> resolveInfos =
895                 packageManager.queryIntentServices(queryIntent, PackageManager.GET_META_DATA);
896         for (ResolveInfo info : resolveInfos) {
897             final ServiceInfo serviceInfo = info.serviceInfo;
898             if (TextUtils.equals(componentName.getPackageName(), serviceInfo.packageName)
899                     && TextUtils.equals(componentName.getClassName(), serviceInfo.name)) {
900                 return serviceInfo.loadLabel(packageManager);
901             }
902         }
903         return null;
904     }
905 }
906