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