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