• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 /*
2  * Copyright (C) 2019 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 android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU;
20 import static android.provider.Settings.Secure.ACCESSIBILITY_BUTTON_MODE_GESTURE;
21 import static android.view.View.GONE;
22 import static android.view.View.VISIBLE;
23 
24 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.GESTURE;
25 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.HARDWARE;
26 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.QUICK_SETTINGS;
27 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.SOFTWARE;
28 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TRIPLETAP;
29 import static com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType.TWOFINGER_DOUBLETAP;
30 
31 import android.annotation.SuppressLint;
32 import android.app.settings.SettingsEnums;
33 import android.content.Context;
34 import android.content.DialogInterface;
35 import android.graphics.drawable.Drawable;
36 import android.text.Spannable;
37 import android.text.SpannableString;
38 import android.text.SpannableStringBuilder;
39 import android.text.style.ImageSpan;
40 import android.util.ArrayMap;
41 import android.util.Log;
42 import android.view.Gravity;
43 import android.view.LayoutInflater;
44 import android.view.View;
45 import android.view.ViewGroup;
46 import android.view.Window;
47 import android.widget.Button;
48 import android.widget.FrameLayout;
49 import android.widget.ImageView;
50 import android.widget.LinearLayout;
51 import android.widget.TextSwitcher;
52 import android.widget.TextView;
53 
54 import androidx.annotation.AnimRes;
55 import androidx.annotation.DrawableRes;
56 import androidx.annotation.IntDef;
57 import androidx.annotation.NonNull;
58 import androidx.annotation.Nullable;
59 import androidx.annotation.RawRes;
60 import androidx.annotation.VisibleForTesting;
61 import androidx.appcompat.app.AlertDialog;
62 import androidx.core.util.Preconditions;
63 import androidx.core.widget.TextViewCompat;
64 import androidx.viewpager.widget.PagerAdapter;
65 import androidx.viewpager.widget.ViewPager;
66 
67 import com.android.internal.accessibility.common.ShortcutConstants.UserShortcutType;
68 import com.android.internal.accessibility.util.ShortcutUtils;
69 import com.android.settings.R;
70 import com.android.settings.core.SubSettingLauncher;
71 import com.android.settingslib.utils.StringUtil;
72 import com.android.settingslib.widget.LottieColorUtils;
73 
74 import com.airbnb.lottie.LottieAnimationView;
75 
76 import java.lang.annotation.Retention;
77 import java.lang.annotation.RetentionPolicy;
78 import java.util.ArrayList;
79 import java.util.List;
80 import java.util.Map;
81 
82 /**
83  * Utility class for creating the dialog that shows tutorials on how to use the selected
84  * accessibility shortcut types
85  */
86 public final class AccessibilityShortcutsTutorial {
87     private static final String TAG = "AccessibilityGestureNavigationTutorial";
88 
89     /** IntDef enum for dialog type. */
90     @Retention(RetentionPolicy.SOURCE)
91     @IntDef({
92             DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_BUTTON,
93             DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_GESTURE,
94             DialogType.GESTURE_NAVIGATION_SETTINGS,
95     })
96 
97     private @interface DialogType {
98         int LAUNCH_SERVICE_BY_ACCESSIBILITY_BUTTON = 0;
99         int LAUNCH_SERVICE_BY_ACCESSIBILITY_GESTURE = 1;
100         int GESTURE_NAVIGATION_SETTINGS = 2;
101     }
102 
AccessibilityShortcutsTutorial()103     private AccessibilityShortcutsTutorial() {}
104 
105     private static final DialogInterface.OnClickListener ON_CLICK_LISTENER =
106             (DialogInterface dialog, int which) -> dialog.dismiss();
107 
108     /**
109      * Displays a dialog that guides users to use accessibility features with accessibility
110      * gestures under system gesture navigation mode.
111      */
showGestureNavigationTutorialDialog(Context context, DialogInterface.OnDismissListener onDismissListener)112     public static AlertDialog showGestureNavigationTutorialDialog(Context context,
113             DialogInterface.OnDismissListener onDismissListener) {
114         final AlertDialog alertDialog = new AlertDialog.Builder(context)
115                 .setView(createTutorialDialogContentView(context,
116                         DialogType.GESTURE_NAVIGATION_SETTINGS))
117                 .setPositiveButton(R.string.accessibility_tutorial_dialog_button, ON_CLICK_LISTENER)
118                 .setOnDismissListener(onDismissListener)
119                 .create();
120 
121         alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
122         alertDialog.setCanceledOnTouchOutside(false);
123         alertDialog.show();
124 
125         return alertDialog;
126     }
127 
showAccessibilityGestureTutorialDialog(Context context)128     static AlertDialog showAccessibilityGestureTutorialDialog(Context context) {
129         return createDialog(context, DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_GESTURE);
130     }
131 
createAccessibilityTutorialDialog( @onNull Context context, int shortcutTypes, @NonNull CharSequence featureName)132     static AlertDialog createAccessibilityTutorialDialog(
133             @NonNull Context context, int shortcutTypes, @NonNull CharSequence featureName) {
134         return createAccessibilityTutorialDialog(
135                 context, shortcutTypes, ON_CLICK_LISTENER, featureName);
136     }
137 
createAccessibilityTutorialDialog( @onNull Context context, int shortcutTypes, @Nullable DialogInterface.OnClickListener actionButtonListener, @NonNull CharSequence featureName)138     static AlertDialog createAccessibilityTutorialDialog(
139             @NonNull Context context,
140             int shortcutTypes,
141             @Nullable DialogInterface.OnClickListener actionButtonListener,
142             @NonNull CharSequence featureName) {
143 
144         final int category = SettingsEnums.SWITCH_SHORTCUT_DIALOG_ACCESSIBILITY_BUTTON_SETTINGS;
145         final DialogInterface.OnClickListener linkButtonListener =
146                 (dialog, which) -> new SubSettingLauncher(context)
147                         .setDestination(AccessibilityButtonFragment.class.getName())
148                         .setSourceMetricsCategory(category)
149                         .launch();
150 
151         final AlertDialog alertDialog = new AlertDialog.Builder(context)
152                 .setPositiveButton(R.string.accessibility_tutorial_dialog_button,
153                         actionButtonListener)
154                 .setNegativeButton(R.string.accessibility_tutorial_dialog_link_button,
155                         linkButtonListener)
156                 .create();
157 
158         final List<TutorialPage> tutorialPages = createShortcutTutorialPages(
159                 context, shortcutTypes, featureName, /* isInSetupWizard= */ false);
160         Preconditions.checkArgument(!tutorialPages.isEmpty(),
161                 /* errorMessage= */ "Unexpected tutorial pages size");
162 
163         final TutorialPageChangeListener.OnPageSelectedCallback callback =
164                 index -> updateTutorialNegativeButtonTextAndVisibility(
165                         alertDialog, tutorialPages, index);
166 
167         alertDialog.setView(createShortcutNavigationContentView(context, tutorialPages, callback));
168 
169         // Showing first page won't invoke onPageSelectedCallback. Need to check the first tutorial
170         // page type manually to set correct visibility of the link button.
171         alertDialog.setOnShowListener(
172                 dialog -> updateTutorialNegativeButtonTextAndVisibility(
173                         alertDialog, tutorialPages, /* selectedPageIndex= */ 0));
174 
175         return alertDialog;
176     }
177 
updateTutorialNegativeButtonTextAndVisibility( AlertDialog dialog, List<TutorialPage> pages, int selectedPageIndex)178     private static void updateTutorialNegativeButtonTextAndVisibility(
179             AlertDialog dialog, List<TutorialPage> pages, int selectedPageIndex) {
180         final Button button = dialog.getButton(DialogInterface.BUTTON_NEGATIVE);
181         final int pageType = pages.get(selectedPageIndex).getType();
182         final int buttonVisibility = (pageType == SOFTWARE) ? VISIBLE : GONE;
183         button.setVisibility(buttonVisibility);
184         if (buttonVisibility == VISIBLE) {
185             final int textResId = AccessibilityUtil.isFloatingMenuEnabled(dialog.getContext())
186                     ? R.string.accessibility_tutorial_dialog_link_button
187                     : R.string.accessibility_tutorial_dialog_configure_software_shortcut_type;
188             button.setText(textResId);
189         }
190     }
191 
createAccessibilityTutorialDialogForSetupWizard(Context context, int shortcutTypes, CharSequence featureName)192     static AlertDialog createAccessibilityTutorialDialogForSetupWizard(Context context,
193             int shortcutTypes, CharSequence featureName) {
194         return createAccessibilityTutorialDialogForSetupWizard(context, shortcutTypes,
195                 ON_CLICK_LISTENER, featureName);
196     }
197 
createAccessibilityTutorialDialogForSetupWizard( @onNull Context context, int shortcutTypes, @Nullable DialogInterface.OnClickListener actionButtonListener, @NonNull CharSequence featureName)198     static AlertDialog createAccessibilityTutorialDialogForSetupWizard(
199             @NonNull Context context,
200             int shortcutTypes,
201             @Nullable DialogInterface.OnClickListener actionButtonListener,
202             @NonNull CharSequence featureName) {
203 
204         final AlertDialog alertDialog = new AlertDialog.Builder(context)
205                 .setPositiveButton(R.string.accessibility_tutorial_dialog_button,
206                         actionButtonListener)
207                 .create();
208 
209         final List<TutorialPage> tutorialPages = createShortcutTutorialPages(
210                 context, shortcutTypes, featureName, /* inSetupWizard= */ true);
211         Preconditions.checkArgument(!tutorialPages.isEmpty(),
212                 /* errorMessage= */ "Unexpected tutorial pages size");
213 
214         alertDialog.setView(createShortcutNavigationContentView(context, tutorialPages, null));
215 
216         return alertDialog;
217     }
218 
219     /**
220      * Gets a content View for a dialog to confirm that they want to enable a service.
221      *
222      * @param context    A valid context
223      * @param dialogType The type of tutorial dialog
224      * @return A content view suitable for viewing
225      */
createTutorialDialogContentView(Context context, int dialogType)226     private static View createTutorialDialogContentView(Context context, int dialogType) {
227         final LayoutInflater inflater = (LayoutInflater) context.getSystemService(
228                 Context.LAYOUT_INFLATER_SERVICE);
229 
230         View content = null;
231 
232         switch (dialogType) {
233             case DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_BUTTON:
234                 content = inflater.inflate(
235                         R.layout.tutorial_dialog_launch_service_by_accessibility_button, null);
236                 break;
237             case DialogType.LAUNCH_SERVICE_BY_ACCESSIBILITY_GESTURE:
238                 content = inflater.inflate(
239                         R.layout.tutorial_dialog_launch_service_by_gesture_navigation, null);
240                 setupGestureNavigationTextWithImage(context, content);
241                 break;
242             case DialogType.GESTURE_NAVIGATION_SETTINGS:
243                 content = inflater.inflate(
244                         R.layout.tutorial_dialog_launch_by_gesture_navigation_settings, null);
245                 setupGestureNavigationTextWithImage(context, content);
246                 break;
247         }
248 
249         return content;
250     }
251 
setupGestureNavigationTextWithImage(Context context, View view)252     private static void setupGestureNavigationTextWithImage(Context context, View view) {
253         final boolean isTouchExploreEnabled = AccessibilityUtil.isTouchExploreEnabled(context);
254 
255         final ImageView imageView = view.findViewById(R.id.image);
256         final int gestureSettingsImageResId =
257                 isTouchExploreEnabled
258                         ? R.drawable.accessibility_shortcut_type_gesture_preview_touch_explore_on
259                         : R.drawable.accessibility_shortcut_type_gesture_preview;
260         imageView.setImageResource(gestureSettingsImageResId);
261 
262         final TextView textView = view.findViewById(R.id.gesture_tutorial_message);
263         textView.setText(isTouchExploreEnabled
264                 ? R.string.accessibility_tutorial_dialog_message_gesture_settings_talkback
265                 : R.string.accessibility_tutorial_dialog_message_gesture_settings);
266     }
267 
createDialog(Context context, int dialogType)268     private static AlertDialog createDialog(Context context, int dialogType) {
269         final AlertDialog alertDialog = new AlertDialog.Builder(context)
270                 .setView(createTutorialDialogContentView(context, dialogType))
271                 .setPositiveButton(R.string.accessibility_tutorial_dialog_button, ON_CLICK_LISTENER)
272                 .create();
273 
274         alertDialog.requestWindowFeature(Window.FEATURE_NO_TITLE);
275         alertDialog.setCanceledOnTouchOutside(false);
276         alertDialog.show();
277 
278         return alertDialog;
279     }
280 
281     private static class TutorialPagerAdapter extends PagerAdapter {
282         private final List<TutorialPage> mTutorialPages;
TutorialPagerAdapter(List<TutorialPage> tutorialPages)283         private TutorialPagerAdapter(List<TutorialPage> tutorialPages) {
284             this.mTutorialPages = tutorialPages;
285         }
286 
287         @NonNull
288         @Override
instantiateItem(@onNull ViewGroup container, int position)289         public Object instantiateItem(@NonNull ViewGroup container, int position) {
290             final View itemView = mTutorialPages.get(position).getIllustrationView();
291             container.addView(itemView);
292             return itemView;
293         }
294 
295         @Override
getCount()296         public int getCount() {
297             return mTutorialPages.size();
298         }
299 
300         @Override
isViewFromObject(@onNull View view, @NonNull Object o)301         public boolean isViewFromObject(@NonNull View view, @NonNull Object o) {
302             return view == o;
303         }
304 
305         @Override
destroyItem(@onNull ViewGroup container, int position, @NonNull Object object)306         public void destroyItem(@NonNull ViewGroup container, int position,
307                 @NonNull Object object) {
308             final View itemView = mTutorialPages.get(position).getIllustrationView();
309             container.removeView(itemView);
310         }
311     }
312 
createImageView(Context context, int imageRes)313     private static ImageView createImageView(Context context, int imageRes) {
314         final ImageView imageView = new ImageView(context);
315         imageView.setImageResource(imageRes);
316         imageView.setAdjustViewBounds(true);
317 
318         return imageView;
319     }
320 
createIllustrationView(Context context, @DrawableRes int imageRes)321     private static View createIllustrationView(Context context, @DrawableRes int imageRes) {
322         final View illustrationFrame = inflateAndInitIllustrationFrame(context);
323         final LottieAnimationView lottieView = illustrationFrame.findViewById(R.id.image);
324         lottieView.setImageResource(imageRes);
325 
326         return illustrationFrame;
327     }
328 
createIllustrationViewWithImageRawResource(Context context, @RawRes int imageRawRes)329     private static View createIllustrationViewWithImageRawResource(Context context,
330             @RawRes int imageRawRes) {
331         final View illustrationFrame = inflateAndInitIllustrationFrame(context);
332         final LottieAnimationView lottieView = illustrationFrame.findViewById(R.id.image);
333         lottieView.setFailureListener(
334                 result -> Log.w(TAG, "Invalid image raw resource id: " + imageRawRes,
335                         result));
336         lottieView.setAnimation(imageRawRes);
337         // Follow the Motion Stoppable requirement by using a finite animation.
338         lottieView.setRepeatCount(0);
339         LottieColorUtils.applyDynamicColors(context, lottieView);
340         lottieView.playAnimation();
341 
342         return illustrationFrame;
343     }
344 
inflateAndInitIllustrationFrame(Context context)345     private static View inflateAndInitIllustrationFrame(Context context) {
346         final LayoutInflater inflater = context.getSystemService(LayoutInflater.class);
347 
348         return inflater.inflate(R.layout.accessibility_lottie_animation_view, /* root= */ null);
349     }
350 
createShortcutNavigationContentView(Context context, List<TutorialPage> tutorialPages, TutorialPageChangeListener.OnPageSelectedCallback onPageSelectedCallback)351     private static View createShortcutNavigationContentView(Context context,
352             List<TutorialPage> tutorialPages,
353             TutorialPageChangeListener.OnPageSelectedCallback onPageSelectedCallback) {
354 
355         final LayoutInflater inflater = context.getSystemService(LayoutInflater.class);
356         final View contentView = inflater.inflate(
357                 R.layout.accessibility_shortcut_tutorial_dialog, /* root= */ null);
358 
359         final LinearLayout indicatorContainer = contentView.findViewById(R.id.indicator_container);
360         indicatorContainer.setVisibility(tutorialPages.size() > 1 ? VISIBLE : GONE);
361         for (TutorialPage page : tutorialPages) {
362             indicatorContainer.addView(page.getIndicatorIcon());
363         }
364         tutorialPages.get(/* firstIndex */ 0).getIndicatorIcon().setEnabled(true);
365 
366         final TextSwitcher title = contentView.findViewById(R.id.title);
367         title.setFactory(() -> makeTitleView(context));
368         title.setText(tutorialPages.get(/* firstIndex */ 0).getTitle());
369 
370         final TextSwitcher instruction = contentView.findViewById(R.id.instruction);
371         instruction.setFactory(() -> makeInstructionView(context));
372         instruction.setText(tutorialPages.get(/* firstIndex */ 0).getInstruction());
373 
374         final ViewPager viewPager = contentView.findViewById(R.id.view_pager);
375         viewPager.setAdapter(new TutorialPagerAdapter(tutorialPages));
376         viewPager.setContentDescription(context.getString(R.string.accessibility_tutorial_pager,
377                 /* firstPage */ 1, tutorialPages.size()));
378         viewPager.setImportantForAccessibility(tutorialPages.size() > 1
379                 ? View.IMPORTANT_FOR_ACCESSIBILITY_YES
380                 : View.IMPORTANT_FOR_ACCESSIBILITY_NO_HIDE_DESCENDANTS);
381 
382         TutorialPageChangeListener listener = new TutorialPageChangeListener(context, viewPager,
383                 title, instruction, tutorialPages);
384         listener.setOnPageSelectedCallback(onPageSelectedCallback);
385 
386         return contentView;
387     }
388 
makeTitleView(Context context)389     private static View makeTitleView(Context context) {
390         final TextView textView = new TextView(context);
391         // Sets the text color, size, style, hint color, and highlight color from the specified
392         // TextAppearance resource.
393         TextViewCompat.setTextAppearance(textView, R.style.AccessibilityDialogTitle);
394         textView.setGravity(Gravity.CENTER);
395         return textView;
396     }
397 
makeInstructionView(Context context)398     private static View makeInstructionView(Context context) {
399         final TextView textView = new TextView(context);
400         TextViewCompat.setTextAppearance(textView, R.style.AccessibilityDialogDescription);
401         return textView;
402     }
403 
404     @SuppressLint("SwitchIntDef")
getShortcutTitle( @onNull Context context, @UserShortcutType int shortcutType, int buttonMode)405     private static CharSequence getShortcutTitle(
406             @NonNull Context context, @UserShortcutType int shortcutType, int buttonMode) {
407         return switch (shortcutType) {
408             case HARDWARE -> context.getText(R.string.accessibility_tutorial_dialog_title_volume);
409             case SOFTWARE -> getSoftwareTitle(context, buttonMode);
410             case GESTURE -> context.getText(R.string.accessibility_tutorial_dialog_title_gesture);
411             case TRIPLETAP -> context.getText(R.string.accessibility_tutorial_dialog_title_triple);
412             case TWOFINGER_DOUBLETAP -> context.getString(
413                     R.string.accessibility_tutorial_dialog_title_two_finger_double, 2);
414             case QUICK_SETTINGS -> context.getText(
415                     R.string.accessibility_tutorial_dialog_title_quick_setting);
416             default -> "";
417         };
418     }
419 
420     @SuppressLint("SwitchIntDef")
getShortcutImage( @onNull Context context, @UserShortcutType int shortcutType, int buttonMode)421     private static View getShortcutImage(
422             @NonNull Context context, @UserShortcutType int shortcutType, int buttonMode) {
423         return switch (shortcutType) {
424             case HARDWARE -> createIllustrationView(
425                     context, R.drawable.accessibility_shortcut_type_volume_keys);
426             case SOFTWARE -> createSoftwareImage(context, buttonMode);
427             case GESTURE -> createIllustrationView(context,
428                     AccessibilityUtil.isTouchExploreEnabled(context)
429                             ? R.drawable.accessibility_shortcut_type_gesture_touch_explore_on
430                             : R.drawable.accessibility_shortcut_type_gesture);
431             case TRIPLETAP -> createIllustrationViewWithImageRawResource(context,
432                     R.raw.accessibility_shortcut_type_tripletap);
433             case TWOFINGER_DOUBLETAP -> createIllustrationViewWithImageRawResource(context,
434                     R.raw.accessibility_shortcut_type_2finger_doubletap);
435             case QUICK_SETTINGS -> {
436                 View v = createIllustrationView(context,
437                         R.drawable.accessibility_shortcut_type_quick_settings);
438                 View bg = v.findViewById(R.id.image_background);
439                 if (bg != null) {
440                     bg.setVisibility(GONE);
441                 }
442                 yield v;
443             }
444             default -> new View(context);
445         };
446     }
447 
448     private static CharSequence getShortcutInstruction(
449             @NonNull Context context, @UserShortcutType int shortcutType, int buttonMode,
450             @NonNull CharSequence featureName, boolean inSetupWizard) {
451         return switch (shortcutType) {
452             case HARDWARE -> context.getText(R.string.accessibility_tutorial_dialog_message_volume);
453             case SOFTWARE -> getSoftwareInstruction(context, buttonMode);
454             case GESTURE -> StringUtil.getIcuPluralsString(
455                     context,
456                     AccessibilityUtil.isTouchExploreEnabled(context) ? 3 : 2,
457                     R.string.accessibility_tutorial_dialog_gesture_shortcut_instruction);
458             case TRIPLETAP -> context.getString(
459                     R.string.accessibility_tutorial_dialog_tripletap_instruction, 3);
460             case TWOFINGER_DOUBLETAP -> context.getString(
461                     R.string.accessibility_tutorial_dialog_twofinger_doubletap_instruction, 2);
462             case QUICK_SETTINGS -> getQuickSettingsInstruction(context, featureName, inSetupWizard);
463             default -> "";
464         };
465     }
466 
467     @SuppressLint("SwitchIntDef")
468     private static TutorialPage createShortcutTutorialPage(
469             @NonNull Context context, @UserShortcutType int shortcutType, int buttonMode,
470             @NonNull CharSequence featureName, boolean inSetupWizard) {
471 
472         final ImageView indicatorIcon =
473                 createImageView(context, R.drawable.ic_accessibility_page_indicator);
474         indicatorIcon.setEnabled(false);
475 
476         return new TutorialPage(shortcutType,
477                 getShortcutTitle(context, shortcutType, buttonMode),
478                 getShortcutImage(context, shortcutType, buttonMode),
479                 createImageView(context, R.drawable.ic_accessibility_page_indicator),
480                 getShortcutInstruction(
481                         context, shortcutType, buttonMode, featureName, inSetupWizard));
482     }
483 
484     /**
485      * Create the tutorial pages for selected shortcut types in the same order as shown in the
486      * edit shortcut screen.
487      */
488     @VisibleForTesting
489     static List<TutorialPage> createShortcutTutorialPages(
490             @NonNull Context context, int shortcutTypes, @NonNull CharSequence featureName,
491             boolean inSetupWizard) {
492         final List<TutorialPage> tutorialPages = new ArrayList<>();
493         int buttonMode = ShortcutUtils.getButtonMode(context, context.getUserId());
494 
495         for (int shortcutType: AccessibilityUtil.SHORTCUTS_ORDER_IN_UI) {
496             if ((shortcutTypes & shortcutType) == 0) {
497                 continue;
498             }
499             tutorialPages.add(
500                     createShortcutTutorialPage(
501                             context, shortcutType, buttonMode, featureName, inSetupWizard));
502         }
503 
504         return tutorialPages;
505     }
506 
507     private static View createSoftwareImage(Context context, int buttonMode) {
508         return switch(buttonMode) {
509             case ACCESSIBILITY_BUTTON_MODE_GESTURE ->
510                     createIllustrationView(context,
511                             AccessibilityUtil.isTouchExploreEnabled(context)
512                                     ? R.drawable
513                                     .accessibility_shortcut_type_gesture_touch_explore_on
514                                     : R.drawable.accessibility_shortcut_type_gesture);
515             case ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU ->
516                 createIllustrationViewWithImageRawResource(
517                         context, R.raw.accessibility_shortcut_type_fab);
518             default -> createIllustrationView(
519                     context, R.drawable.accessibility_shortcut_type_navbar);
520         };
521     }
522 
523     private static CharSequence getSoftwareTitle(Context context, int buttonMode) {
524         return context.getText(buttonMode == ACCESSIBILITY_BUTTON_MODE_GESTURE
525                 ? R.string.accessibility_tutorial_dialog_title_gesture
526                 : R.string.accessibility_tutorial_dialog_title_button);
527     }
528 
529     private static CharSequence getSoftwareInstruction(Context context, int buttonMode) {
530         return switch(buttonMode) {
531             case ACCESSIBILITY_BUTTON_MODE_FLOATING_MENU -> context.getText(
532                     R.string.accessibility_tutorial_dialog_message_floating_button);
533             case ACCESSIBILITY_BUTTON_MODE_GESTURE -> StringUtil.getIcuPluralsString(
534                     context,
535                     AccessibilityUtil.isTouchExploreEnabled(context) ? 3 : 2,
536                     R.string.accessibility_tutorial_dialog_gesture_shortcut_instruction);
537             default -> getSoftwareInstructionWithIcon(context,
538                     context.getText(R.string.accessibility_tutorial_dialog_message_button));
539         };
540     }
541 
542     private static CharSequence getSoftwareInstructionWithIcon(Context context, CharSequence text) {
543         final String message = text.toString();
544         final SpannableString spannableInstruction = SpannableString.valueOf(message);
545         final int indexIconStart = message.indexOf("%s");
546         final int indexIconEnd = indexIconStart + 2;
547         final ImageView iconView = new ImageView(context);
548         iconView.setImageDrawable(context.getDrawable(R.drawable.ic_accessibility_new));
549         final Drawable icon = iconView.getDrawable().mutate();
550         final ImageSpan imageSpan = new ImageSpan(icon);
551         imageSpan.setContentDescription("");
552         icon.setBounds(/* left= */ 0, /* top= */ 0,
553                 icon.getIntrinsicWidth(), icon.getIntrinsicHeight());
554         spannableInstruction.setSpan(imageSpan, indexIconStart, indexIconEnd,
555                 Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
556 
557         return spannableInstruction;
558     }
559 
560     private static CharSequence getQuickSettingsInstruction(
561             Context context, CharSequence featureName, boolean inSetupWizard) {
562         Map<String, Object> arguments = new ArrayMap<>();
563         arguments.put("count",
564                 AccessibilityUtil.isTouchExploreEnabled(context) ? 2 : 1);
565         arguments.put("featureName", featureName);
566         final CharSequence pluralsString = StringUtil.getIcuPluralsString(
567                 context, arguments,
568                 R.string.accessibility_tutorial_dialog_message_quick_setting);
569         final SpannableStringBuilder tutorialText = new SpannableStringBuilder();
570         if (inSetupWizard) {
571             tutorialText.append(context.getText(R.string
572                             .accessibility_tutorial_dialog_shortcut_unavailable_in_suw))
573                     .append("\n\n");
574         }
575         return tutorialText.append(pluralsString);
576     }
577 
578     private static class TutorialPage {
579         private final int mType;
580         private final CharSequence mTitle;
581         private final View mIllustrationView;
582         private final ImageView mIndicatorIcon;
583         private final CharSequence mInstruction;
584 
585         TutorialPage(int type, CharSequence title, View illustrationView, ImageView indicatorIcon,
586                 CharSequence instruction) {
587             this.mType = type;
588             this.mTitle = title;
589             this.mIllustrationView = illustrationView;
590             this.mIndicatorIcon = indicatorIcon;
591             this.mInstruction = instruction;
592 
593             setupIllustrationChildViewsGravity();
594         }
595 
596         public int getType() {
597             return mType;
598         }
599 
600         public CharSequence getTitle() {
601             return mTitle;
602         }
603 
604         public View getIllustrationView() {
605             return mIllustrationView;
606         }
607 
608         public ImageView getIndicatorIcon() {
609             return mIndicatorIcon;
610         }
611 
612         public CharSequence getInstruction() {
613             return mInstruction;
614         }
615 
616         private void setupIllustrationChildViewsGravity() {
617             final View backgroundView = mIllustrationView.findViewById(R.id.image_background);
618             initViewGravity(backgroundView);
619 
620             final View lottieView = mIllustrationView.findViewById(R.id.image);
621             initViewGravity(lottieView);
622         }
623 
624         private void initViewGravity(@NonNull View view) {
625             final FrameLayout.LayoutParams layoutParams =
626                     new FrameLayout.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT,
627                             FrameLayout.LayoutParams.WRAP_CONTENT);
628             layoutParams.gravity = Gravity.CENTER;
629 
630             view.setLayoutParams(layoutParams);
631         }
632     }
633 
634     private static class TutorialPageChangeListener implements ViewPager.OnPageChangeListener {
635         private int mLastTutorialPagePosition = 0;
636         private final Context mContext;
637         private final TextSwitcher mTitle;
638         private final TextSwitcher mInstruction;
639         private final List<TutorialPage> mTutorialPages;
640         private final ViewPager mViewPager;
641         private OnPageSelectedCallback mOnPageSelectedCallback;
642 
643         TutorialPageChangeListener(Context context, ViewPager viewPager, ViewGroup title,
644                 ViewGroup instruction, List<TutorialPage> tutorialPages) {
645             this.mContext = context;
646             this.mViewPager = viewPager;
647             this.mTitle = (TextSwitcher) title;
648             this.mInstruction = (TextSwitcher) instruction;
649             this.mTutorialPages = tutorialPages;
650             this.mOnPageSelectedCallback = null;
651 
652             this.mViewPager.addOnPageChangeListener(this);
653         }
654 
655         public void setOnPageSelectedCallback(
656                 OnPageSelectedCallback callback) {
657             this.mOnPageSelectedCallback = callback;
658         }
659 
660         @Override
661         public void onPageScrolled(int position, float positionOffset,
662                 int positionOffsetPixels) {
663             // Do nothing.
664         }
665 
666         @Override
667         public void onPageSelected(int position) {
668             final boolean isPreviousPosition =
669                     mLastTutorialPagePosition > position;
670             @AnimRes
671             final int inAnimationResId = isPreviousPosition
672                     ? android.R.anim.slide_in_left
673                     : com.android.internal.R.anim.slide_in_right;
674 
675             @AnimRes
676             final int outAnimationResId = isPreviousPosition
677                     ? android.R.anim.slide_out_right
678                     : com.android.internal.R.anim.slide_out_left;
679 
680             mTitle.setInAnimation(mContext, inAnimationResId);
681             mTitle.setOutAnimation(mContext, outAnimationResId);
682             mTitle.setText(mTutorialPages.get(position).getTitle());
683 
684             mInstruction.setInAnimation(mContext, inAnimationResId);
685             mInstruction.setOutAnimation(mContext, outAnimationResId);
686             mInstruction.setText(mTutorialPages.get(position).getInstruction());
687 
688             for (TutorialPage page : mTutorialPages) {
689                 page.getIndicatorIcon().setEnabled(false);
690             }
691             mTutorialPages.get(position).getIndicatorIcon().setEnabled(true);
692             mLastTutorialPagePosition = position;
693 
694             final int currentPageNumber = position + 1;
695             mViewPager.setContentDescription(
696                     mContext.getString(R.string.accessibility_tutorial_pager,
697                             currentPageNumber, mTutorialPages.size()));
698 
699             if (mOnPageSelectedCallback != null) {
700                 mOnPageSelectedCallback.onPageSelected(position);
701             }
702         }
703 
704         @Override
705         public void onPageScrollStateChanged(int state) {
706             // Do nothing.
707         }
708 
709         /** The interface that provides a callback method after tutorial page is selected. */
710         private interface OnPageSelectedCallback {
711 
712             /** The callback method after tutorial page is selected. */
713             void onPageSelected(int index);
714         }
715     }
716 }
717