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