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