1 /*
2  * Copyright (C) 2020 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 package com.android.quickstep.interaction;
17 
18 import static android.view.View.GONE;
19 import static android.view.View.NO_ID;
20 import static android.view.View.inflate;
21 
22 import android.animation.Animator;
23 import android.animation.AnimatorListenerAdapter;
24 import android.animation.AnimatorSet;
25 import android.animation.ObjectAnimator;
26 import android.animation.ValueAnimator;
27 import android.annotation.ColorRes;
28 import android.content.Context;
29 import android.content.pm.PackageManager;
30 import android.graphics.drawable.AnimatedVectorDrawable;
31 import android.graphics.drawable.ColorDrawable;
32 import android.graphics.drawable.RippleDrawable;
33 import android.util.Log;
34 import android.view.View;
35 import android.view.ViewGroup;
36 import android.view.accessibility.AccessibilityEvent;
37 import android.widget.Button;
38 import android.widget.FrameLayout;
39 import android.widget.ImageView;
40 import android.widget.RelativeLayout;
41 import android.widget.TextView;
42 
43 import androidx.annotation.CallSuper;
44 import androidx.annotation.DrawableRes;
45 import androidx.annotation.LayoutRes;
46 import androidx.annotation.NonNull;
47 import androidx.annotation.Nullable;
48 import androidx.annotation.StringRes;
49 import androidx.appcompat.app.AlertDialog;
50 import androidx.appcompat.content.res.AppCompatResources;
51 
52 import com.android.launcher3.R;
53 import com.android.launcher3.Utilities;
54 import com.android.launcher3.anim.AnimatorListeners;
55 import com.android.launcher3.views.ClipIconView;
56 import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureAttemptCallback;
57 import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureAttemptCallback;
58 
59 import java.util.ArrayList;
60 
61 abstract class TutorialController implements BackGestureAttemptCallback,
62         NavBarGestureAttemptCallback {
63 
64     private static final String TAG = "TutorialController";
65 
66     private static final float FINGER_DOT_VISIBLE_ALPHA = 0.6f;
67     private static final float FINGER_DOT_SMALL_SCALE = 0.7f;
68     private static final int FINGER_DOT_ANIMATION_DURATION_MILLIS = 500;
69 
70     private static final String PIXEL_TIPS_APP_PACKAGE_NAME = "com.google.android.apps.tips";
71     private static final CharSequence DEFAULT_PIXEL_TIPS_APP_NAME = "Pixel Tips";
72 
73     private static final int FEEDBACK_ANIMATION_MS = 133;
74     private static final int RIPPLE_VISIBLE_MS = 300;
75     private static final int GESTURE_ANIMATION_DELAY_MS = 1500;
76     private static final int ADVANCE_TUTORIAL_TIMEOUT_MS = 4000;
77     private static final long GESTURE_ANIMATION_PAUSE_DURATION_MILLIS = 1000;
78 
79     final TutorialFragment mTutorialFragment;
80     TutorialType mTutorialType;
81     final Context mContext;
82 
83     final TextView mCloseButton;
84     final ViewGroup mFeedbackView;
85     final TextView mFeedbackTitleView;
86     final ImageView mEdgeGestureVideoView;
87     final RelativeLayout mFakeLauncherView;
88     final FrameLayout mFakeHotseatView;
89     @Nullable View mHotseatIconView;
90     final ClipIconView mFakeIconView;
91     final FrameLayout mFakeTaskView;
92     final AnimatedTaskbarView mFakeTaskbarView;
93     final AnimatedTaskView mFakePreviousTaskView;
94     final View mRippleView;
95     final RippleDrawable mRippleDrawable;
96     final Button mActionButton;
97     final TutorialStepIndicator mTutorialStepView;
98     final ImageView mFingerDotView;
99     private final AlertDialog mSkipTutorialDialog;
100 
101     private boolean mGestureCompleted = false;
102 
103     // These runnables  should be used when posting callbacks to their views and cleared from their
104     // views before posting new callbacks.
105     private final Runnable mTitleViewCallback;
106     @Nullable private Runnable mFeedbackViewCallback;
107     @Nullable private Runnable mFakeTaskViewCallback;
108     @Nullable private Runnable mFakeTaskbarViewCallback;
109     private final Runnable mShowFeedbackRunnable;
110 
TutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType)111     TutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType) {
112         mTutorialFragment = tutorialFragment;
113         mTutorialType = tutorialType;
114         mContext = mTutorialFragment.getContext();
115 
116         RootSandboxLayout rootView = tutorialFragment.getRootView();
117         mCloseButton = rootView.findViewById(R.id.gesture_tutorial_fragment_close_button);
118         mCloseButton.setOnClickListener(button -> showSkipTutorialDialog());
119         mFeedbackView = rootView.findViewById(R.id.gesture_tutorial_fragment_feedback_view);
120         mFeedbackTitleView = mFeedbackView.findViewById(
121                 R.id.gesture_tutorial_fragment_feedback_title);
122         mEdgeGestureVideoView = rootView.findViewById(R.id.gesture_tutorial_edge_gesture_video);
123         mFakeLauncherView = rootView.findViewById(R.id.gesture_tutorial_fake_launcher_view);
124         mFakeHotseatView = rootView.findViewById(R.id.gesture_tutorial_fake_hotseat_view);
125         mFakeIconView = rootView.findViewById(R.id.gesture_tutorial_fake_icon_view);
126         mFakeTaskView = rootView.findViewById(R.id.gesture_tutorial_fake_task_view);
127         mFakeTaskbarView = rootView.findViewById(R.id.gesture_tutorial_fake_taskbar_view);
128         mFakePreviousTaskView =
129                 rootView.findViewById(R.id.gesture_tutorial_fake_previous_task_view);
130         mRippleView = rootView.findViewById(R.id.gesture_tutorial_ripple_view);
131         mRippleDrawable = (RippleDrawable) mRippleView.getBackground();
132         mActionButton = rootView.findViewById(R.id.gesture_tutorial_fragment_action_button);
133         mTutorialStepView =
134                 rootView.findViewById(R.id.gesture_tutorial_fragment_feedback_tutorial_step);
135         mFingerDotView = rootView.findViewById(R.id.gesture_tutorial_finger_dot);
136         mSkipTutorialDialog = createSkipTutorialDialog();
137 
138         mTitleViewCallback = () -> mFeedbackTitleView.sendAccessibilityEvent(
139                 AccessibilityEvent.TYPE_VIEW_FOCUSED);
140         mShowFeedbackRunnable = () -> {
141             mFeedbackView.setAlpha(0f);
142             mFeedbackView.setScaleX(0.95f);
143             mFeedbackView.setScaleY(0.95f);
144             mFeedbackView.setVisibility(View.VISIBLE);
145             mFeedbackView.animate()
146                     .setDuration(FEEDBACK_ANIMATION_MS)
147                     .alpha(1f)
148                     .scaleX(1f)
149                     .scaleY(1f)
150                     .withEndAction(() -> {
151                         if (mGestureCompleted && !mTutorialFragment.isAtFinalStep()) {
152                             if (mFeedbackViewCallback != null) {
153                                 mFeedbackView.removeCallbacks(mFeedbackViewCallback);
154                             }
155                             mFeedbackViewCallback = mTutorialFragment::continueTutorial;
156                             mFeedbackView.postDelayed(mFeedbackViewCallback,
157                                     ADVANCE_TUTORIAL_TIMEOUT_MS);
158                         }
159                     })
160                     .start();
161             mFeedbackTitleView.postDelayed(mTitleViewCallback, FEEDBACK_ANIMATION_MS);
162         };
163     }
164 
showSkipTutorialDialog()165     private void showSkipTutorialDialog() {
166         if (mSkipTutorialDialog != null) {
167             mSkipTutorialDialog.show();
168         }
169     }
170 
getHotseatIconTop()171     public int getHotseatIconTop() {
172         return mHotseatIconView == null
173                 ? 0 : mFakeHotseatView.getTop() + mHotseatIconView.getTop();
174     }
175 
getHotseatIconLeft()176     public int getHotseatIconLeft() {
177         return mHotseatIconView == null
178                 ? 0 : mFakeHotseatView.getLeft() + mHotseatIconView.getLeft();
179     }
180 
setTutorialType(TutorialType tutorialType)181     void setTutorialType(TutorialType tutorialType) {
182         mTutorialType = tutorialType;
183     }
184 
185     @LayoutRes
getMockHotseatResId()186     protected int getMockHotseatResId() {
187         return mTutorialFragment.isLargeScreen()
188                 ? R.layout.gesture_tutorial_foldable_mock_hotseat
189                 : R.layout.gesture_tutorial_mock_hotseat;
190     }
191 
192     @LayoutRes
getMockAppTaskLayoutResId()193     protected int getMockAppTaskLayoutResId() {
194         return View.NO_ID;
195     }
196 
197     @ColorRes
getMockPreviousAppTaskThumbnailColorResId()198     protected int getMockPreviousAppTaskThumbnailColorResId() {
199         return R.color.gesture_tutorial_fake_previous_task_view_color;
200     }
201 
202     @DrawableRes
getMockAppIconResId()203     public int getMockAppIconResId() {
204         return R.drawable.default_sandbox_app_icon;
205     }
206 
207     @DrawableRes
getMockWallpaperResId()208     public int getMockWallpaperResId() {
209         return R.drawable.default_sandbox_wallpaper;
210     }
211 
fadeTaskViewAndRun(Runnable r)212     void fadeTaskViewAndRun(Runnable r) {
213         mFakeTaskView.animate().alpha(0).setListener(AnimatorListeners.forSuccessCallback(r));
214     }
215 
216     @StringRes
getIntroductionTitle()217     public Integer getIntroductionTitle() {
218         return null;
219     }
220 
221     @StringRes
getIntroductionSubtitle()222     public Integer getIntroductionSubtitle() {
223         return null;
224     }
225 
226     @StringRes
getSuccessFeedbackSubtitle()227     public Integer getSuccessFeedbackSubtitle() {
228         return null;
229     }
230 
showFeedback()231     void showFeedback() {
232         if (mGestureCompleted) {
233             mFeedbackView.setTranslationY(0);
234             return;
235         }
236         Animator gestureAnimation = mTutorialFragment.getGestureAnimation();
237         AnimatedVectorDrawable edgeAnimation = mTutorialFragment.getEdgeAnimation();
238         if (gestureAnimation != null && edgeAnimation != null) {
239             playFeedbackAnimation(gestureAnimation, edgeAnimation, mShowFeedbackRunnable, true);
240         }
241     }
242 
243     /**
244      * Show feedback reflecting a successful gesture attempt.
245      **/
showSuccessFeedback()246     void showSuccessFeedback() {
247         showFeedback(getSuccessFeedbackSubtitle(), true);
248     }
249 
250     /**
251      * Show feedback reflecting a failed gesture attempt.
252      *
253      * @param subtitleResId Resource of the text to display.
254      **/
showFeedback(int subtitleResId)255     void showFeedback(int subtitleResId) {
256         showFeedback(subtitleResId, false);
257     }
258 
259     /**
260      * Show feedback reflecting the result of a gesture attempt.
261      *
262      * @param isGestureSuccessful Whether the tutorial feedback's action button should be shown.
263      **/
showFeedback(int subtitleResId, boolean isGestureSuccessful)264     void showFeedback(int subtitleResId, boolean isGestureSuccessful) {
265         showFeedback(
266                 isGestureSuccessful
267                         ? R.string.gesture_tutorial_nice : R.string.gesture_tutorial_try_again,
268                 subtitleResId,
269                 isGestureSuccessful,
270                 false);
271     }
272 
showFeedback( int titleResId, int subtitleResId, boolean isGestureSuccessful, boolean useGestureAnimationDelay)273     void showFeedback(
274             int titleResId,
275             int subtitleResId,
276             boolean isGestureSuccessful,
277             boolean useGestureAnimationDelay) {
278         mFeedbackTitleView.removeCallbacks(mTitleViewCallback);
279         if (mFeedbackViewCallback != null) {
280             mFeedbackView.removeCallbacks(mFeedbackViewCallback);
281             mFeedbackViewCallback = null;
282         }
283 
284         mFeedbackTitleView.setText(titleResId);
285         TextView subtitle =
286                 mFeedbackView.findViewById(R.id.gesture_tutorial_fragment_feedback_subtitle);
287         subtitle.setText(subtitleResId);
288         if (isGestureSuccessful) {
289             if (mTutorialFragment.isAtFinalStep()) {
290                 showActionButton();
291             }
292 
293             if (mFakeTaskViewCallback != null) {
294                 mFakeTaskView.removeCallbacks(mFakeTaskViewCallback);
295                 mFakeTaskViewCallback = null;
296             }
297         }
298         mGestureCompleted = isGestureSuccessful;
299 
300         Animator gestureAnimation = mTutorialFragment.getGestureAnimation();
301         AnimatedVectorDrawable edgeAnimation = mTutorialFragment.getEdgeAnimation();
302         if (!isGestureSuccessful && gestureAnimation != null && edgeAnimation != null) {
303             playFeedbackAnimation(
304                     gestureAnimation,
305                     edgeAnimation,
306                     mShowFeedbackRunnable,
307                     useGestureAnimationDelay);
308             return;
309         } else {
310             mTutorialFragment.releaseFeedbackAnimation();
311         }
312         mFeedbackViewCallback = mShowFeedbackRunnable;
313 
314         mFeedbackView.post(mFeedbackViewCallback);
315     }
316 
isGestureCompleted()317     public boolean isGestureCompleted() {
318         return mGestureCompleted;
319     }
320 
hideFeedback()321     void hideFeedback() {
322         cancelQueuedGestureAnimation();
323         mFeedbackView.clearAnimation();
324         mFeedbackView.setVisibility(View.INVISIBLE);
325     }
326 
cancelQueuedGestureAnimation()327     void cancelQueuedGestureAnimation() {
328         if (mFeedbackViewCallback != null) {
329             mFeedbackView.removeCallbacks(mFeedbackViewCallback);
330             mFeedbackViewCallback = null;
331         }
332         if (mFakeTaskViewCallback != null) {
333             mFakeTaskView.removeCallbacks(mFakeTaskViewCallback);
334             mFakeTaskViewCallback = null;
335         }
336         if (mFakeTaskbarViewCallback != null) {
337             mFakeTaskbarView.removeCallbacks(mFakeTaskbarViewCallback);
338             mFakeTaskbarViewCallback = null;
339         }
340         mFeedbackTitleView.removeCallbacks(mTitleViewCallback);
341     }
342 
playFeedbackAnimation( @onNull Animator gestureAnimation, @NonNull AnimatedVectorDrawable edgeAnimation, @NonNull Runnable onStartRunnable, boolean useGestureAnimationDelay)343     private void playFeedbackAnimation(
344             @NonNull Animator gestureAnimation,
345             @NonNull AnimatedVectorDrawable edgeAnimation,
346             @NonNull Runnable onStartRunnable,
347             boolean useGestureAnimationDelay) {
348 
349         if (gestureAnimation.isRunning()) {
350             gestureAnimation.cancel();
351         }
352         if (edgeAnimation.isRunning()) {
353             edgeAnimation.reset();
354         }
355         gestureAnimation.addListener(new AnimatorListenerAdapter() {
356             @Override
357             public void onAnimationStart(Animator animation) {
358                 super.onAnimationStart(animation);
359 
360                 mEdgeGestureVideoView.setVisibility(GONE);
361                 if (edgeAnimation.isRunning()) {
362                     edgeAnimation.stop();
363                 }
364 
365                 if (!useGestureAnimationDelay) {
366                     onStartRunnable.run();
367                 }
368             }
369 
370             @Override
371             public void onAnimationEnd(Animator animation) {
372                 super.onAnimationEnd(animation);
373 
374                 mEdgeGestureVideoView.setVisibility(View.VISIBLE);
375                 edgeAnimation.start();
376 
377                 gestureAnimation.removeListener(this);
378             }
379         });
380 
381         cancelQueuedGestureAnimation();
382         if (useGestureAnimationDelay) {
383             mFeedbackViewCallback = onStartRunnable;
384             mFakeTaskViewCallback = gestureAnimation::start;
385 
386             mFeedbackView.post(mFeedbackViewCallback);
387             mFakeTaskView.postDelayed(mFakeTaskViewCallback, GESTURE_ANIMATION_DELAY_MS);
388         } else {
389             gestureAnimation.start();
390         }
391     }
392 
setRippleHotspot(float x, float y)393     void setRippleHotspot(float x, float y) {
394         mRippleDrawable.setHotspot(x, y);
395     }
396 
showRippleEffect(@ullable Runnable onCompleteRunnable)397     void showRippleEffect(@Nullable Runnable onCompleteRunnable) {
398         mRippleDrawable.setState(
399                 new int[] {android.R.attr.state_pressed, android.R.attr.state_enabled});
400         mRippleView.postDelayed(() -> {
401             mRippleDrawable.setState(new int[] {});
402             if (onCompleteRunnable != null) {
403                 onCompleteRunnable.run();
404             }
405         }, RIPPLE_VISIBLE_MS);
406     }
407 
onActionButtonClicked(View button)408     void onActionButtonClicked(View button) {
409         mTutorialFragment.continueTutorial();
410     }
411 
412     @CallSuper
transitToController()413     void transitToController() {
414         hideFeedback();
415         hideActionButton();
416         updateCloseButton();
417         updateSubtext();
418         updateDrawables();
419         updateLayout();
420 
421         mGestureCompleted = false;
422         if (mFakeHotseatView != null) {
423             mFakeHotseatView.setVisibility(View.INVISIBLE);
424         }
425     }
426 
updateCloseButton()427     void updateCloseButton() {
428         mCloseButton.setTextAppearance(Utilities.isDarkTheme(mContext)
429                 ? R.style.TextAppearance_GestureTutorial_Feedback_Subtext
430                 : R.style.TextAppearance_GestureTutorial_Feedback_Subtext_Dark);
431     }
432 
hideActionButton()433     void hideActionButton() {
434         mCloseButton.setVisibility(View.VISIBLE);
435         // Invisible to maintain the layout.
436         mActionButton.setVisibility(View.INVISIBLE);
437         mActionButton.setOnClickListener(null);
438     }
439 
showActionButton()440     void showActionButton() {
441         mCloseButton.setVisibility(GONE);
442         mActionButton.setVisibility(View.VISIBLE);
443         mActionButton.setOnClickListener(this::onActionButtonClicked);
444     }
445 
hideFakeTaskbar(boolean animateToHotseat)446     void hideFakeTaskbar(boolean animateToHotseat) {
447         if (!mTutorialFragment.isLargeScreen()) {
448             return;
449         }
450         if (mFakeTaskbarViewCallback != null) {
451             mFakeTaskbarView.removeCallbacks(mFakeTaskbarViewCallback);
452         }
453         if (animateToHotseat) {
454             mFakeTaskbarViewCallback = () ->
455                     mFakeTaskbarView.animateDisappearanceToHotseat(mFakeHotseatView);
456         } else {
457             mFakeTaskbarViewCallback = mFakeTaskbarView::animateDisappearanceToBottom;
458         }
459         mFakeTaskbarView.post(mFakeTaskbarViewCallback);
460     }
461 
showFakeTaskbar(boolean animateFromHotseat)462     void showFakeTaskbar(boolean animateFromHotseat) {
463         if (!mTutorialFragment.isLargeScreen()) {
464             return;
465         }
466         if (mFakeTaskbarViewCallback != null) {
467             mFakeTaskbarView.removeCallbacks(mFakeTaskbarViewCallback);
468         }
469         if (animateFromHotseat) {
470             mFakeTaskbarViewCallback = () ->
471                     mFakeTaskbarView.animateAppearanceFromHotseat(mFakeHotseatView);
472         } else {
473             mFakeTaskbarViewCallback = mFakeTaskbarView::animateAppearanceFromBottom;
474         }
475         mFakeTaskbarView.post(mFakeTaskbarViewCallback);
476     }
477 
updateFakeAppTaskViewLayout(@ayoutRes int mockAppTaskLayoutResId)478     void updateFakeAppTaskViewLayout(@LayoutRes int mockAppTaskLayoutResId) {
479         updateFakeViewLayout(mFakeTaskView, mockAppTaskLayoutResId);
480     }
481 
updateFakeViewLayout(ViewGroup view, @LayoutRes int mockLayoutResId)482     void updateFakeViewLayout(ViewGroup view, @LayoutRes int mockLayoutResId) {
483         view.removeAllViews();
484         if (mockLayoutResId != NO_ID) {
485             view.addView(
486                     inflate(mContext, mockLayoutResId, null),
487                     new FrameLayout.LayoutParams(
488                             ViewGroup.LayoutParams.MATCH_PARENT,
489                             ViewGroup.LayoutParams.MATCH_PARENT));
490         }
491     }
492 
updateSubtext()493     private void updateSubtext() {
494         mTutorialStepView.setTutorialProgress(
495                 mTutorialFragment.getCurrentStep(), mTutorialFragment.getNumSteps());
496     }
497 
updateDrawables()498     private void updateDrawables() {
499         if (mContext != null) {
500             mTutorialFragment.getRootView().setBackground(AppCompatResources.getDrawable(
501                     mContext, getMockWallpaperResId()));
502             mTutorialFragment.updateFeedbackAnimation();
503             mFakeLauncherView.setBackgroundColor(
504                     mContext.getColor(R.color.gesture_tutorial_fake_wallpaper_color));
505             updateFakeViewLayout(mFakeHotseatView, getMockHotseatResId());
506             mHotseatIconView = mFakeHotseatView.findViewById(R.id.hotseat_icon_1);
507             updateFakeViewLayout(mFakeTaskView, getMockAppTaskLayoutResId());
508             mFakeTaskView.animate().alpha(1).setListener(
509                     AnimatorListeners.forSuccessCallback(() -> mFakeTaskView.animate().cancel()));
510             mFakePreviousTaskView.setFakeTaskViewFillColor(mContext.getResources().getColor(
511                     getMockPreviousAppTaskThumbnailColorResId()));
512             mFakeIconView.setBackground(AppCompatResources.getDrawable(
513                     mContext, getMockAppIconResId()));
514         }
515     }
516 
updateLayout()517     private void updateLayout() {
518         if (mContext != null) {
519             RelativeLayout.LayoutParams feedbackLayoutParams =
520                     (RelativeLayout.LayoutParams) mFeedbackView.getLayoutParams();
521             feedbackLayoutParams.setMarginStart(mContext.getResources().getDimensionPixelSize(
522                     mTutorialFragment.isLargeScreen()
523                             ? R.dimen.gesture_tutorial_foldable_feedback_margin_start_end
524                             : R.dimen.gesture_tutorial_feedback_margin_start_end));
525             feedbackLayoutParams.setMarginEnd(mContext.getResources().getDimensionPixelSize(
526                     mTutorialFragment.isLargeScreen()
527                             ? R.dimen.gesture_tutorial_foldable_feedback_margin_start_end
528                             : R.dimen.gesture_tutorial_feedback_margin_start_end));
529 
530             mFakeTaskbarView.setVisibility(mTutorialFragment.isLargeScreen() ? View.VISIBLE : GONE);
531         }
532     }
533 
createSkipTutorialDialog()534     private AlertDialog createSkipTutorialDialog() {
535         if (mContext instanceof GestureSandboxActivity) {
536             GestureSandboxActivity sandboxActivity = (GestureSandboxActivity) mContext;
537             View contentView = View.inflate(
538                     sandboxActivity, R.layout.gesture_tutorial_dialog, null);
539             AlertDialog tutorialDialog = new AlertDialog
540                     .Builder(sandboxActivity, R.style.Theme_AppCompat_Dialog_Alert)
541                     .setView(contentView)
542                     .create();
543 
544             PackageManager packageManager = mContext.getPackageManager();
545             CharSequence tipsAppName = DEFAULT_PIXEL_TIPS_APP_NAME;
546 
547             try {
548                 tipsAppName = packageManager.getApplicationLabel(
549                         packageManager.getApplicationInfo(
550                                 PIXEL_TIPS_APP_PACKAGE_NAME, PackageManager.GET_META_DATA));
551             } catch (PackageManager.NameNotFoundException e) {
552                 Log.e(TAG,
553                         "Could not find app label for package name: "
554                                 + PIXEL_TIPS_APP_PACKAGE_NAME
555                                 + ". Defaulting to 'Pixel Tips.'",
556                         e);
557             }
558 
559             TextView subtitleTextView = (TextView) contentView.findViewById(
560                     R.id.gesture_tutorial_dialog_subtitle);
561             if (subtitleTextView != null) {
562                 subtitleTextView.setText(
563                         mContext.getString(R.string.skip_tutorial_dialog_subtitle, tipsAppName));
564             } else {
565                 Log.w(TAG, "No subtitle view in the skip tutorial dialog to update.");
566             }
567 
568             Button cancelButton = (Button) contentView.findViewById(
569                     R.id.gesture_tutorial_dialog_cancel_button);
570             if (cancelButton != null) {
571                 cancelButton.setOnClickListener(
572                         v -> tutorialDialog.dismiss());
573             } else {
574                 Log.w(TAG, "No cancel button in the skip tutorial dialog to update.");
575             }
576 
577             Button confirmButton = contentView.findViewById(
578                     R.id.gesture_tutorial_dialog_confirm_button);
579             if (confirmButton != null) {
580                 confirmButton.setOnClickListener(v -> {
581                     sandboxActivity.closeTutorial();
582                     tutorialDialog.dismiss();
583                 });
584             } else {
585                 Log.w(TAG, "No confirm button in the skip tutorial dialog to update.");
586             }
587 
588             tutorialDialog.getWindow().setBackgroundDrawable(
589                     new ColorDrawable(sandboxActivity.getColor(android.R.color.transparent)));
590 
591             return tutorialDialog;
592         }
593 
594         return null;
595     }
596 
createFingerDotAppearanceAnimatorSet()597     protected AnimatorSet createFingerDotAppearanceAnimatorSet() {
598         ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(
599                 mFingerDotView, View.ALPHA, 0f, FINGER_DOT_VISIBLE_ALPHA);
600         ObjectAnimator yScaleAnimator = ObjectAnimator.ofFloat(
601                 mFingerDotView, View.SCALE_Y, FINGER_DOT_SMALL_SCALE, 1f);
602         ObjectAnimator xScaleAnimator = ObjectAnimator.ofFloat(
603                 mFingerDotView, View.SCALE_X, FINGER_DOT_SMALL_SCALE, 1f);
604         ArrayList<Animator> animators = new ArrayList<>();
605 
606         animators.add(alphaAnimator);
607         animators.add(xScaleAnimator);
608         animators.add(yScaleAnimator);
609 
610         AnimatorSet appearanceAnimatorSet = new AnimatorSet();
611 
612         appearanceAnimatorSet.playTogether(animators);
613         appearanceAnimatorSet.setDuration(FINGER_DOT_ANIMATION_DURATION_MILLIS);
614 
615         return appearanceAnimatorSet;
616     }
617 
createFingerDotDisappearanceAnimatorSet()618     protected AnimatorSet createFingerDotDisappearanceAnimatorSet() {
619         ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(
620                 mFingerDotView, View.ALPHA, FINGER_DOT_VISIBLE_ALPHA, 0f);
621         ObjectAnimator yScaleAnimator = ObjectAnimator.ofFloat(
622                 mFingerDotView, View.SCALE_Y, 1f, FINGER_DOT_SMALL_SCALE);
623         ObjectAnimator xScaleAnimator = ObjectAnimator.ofFloat(
624                 mFingerDotView, View.SCALE_X, 1f, FINGER_DOT_SMALL_SCALE);
625         ArrayList<Animator> animators = new ArrayList<>();
626 
627         animators.add(alphaAnimator);
628         animators.add(xScaleAnimator);
629         animators.add(yScaleAnimator);
630 
631         AnimatorSet appearanceAnimatorSet = new AnimatorSet();
632 
633         appearanceAnimatorSet.playTogether(animators);
634         appearanceAnimatorSet.setDuration(FINGER_DOT_ANIMATION_DURATION_MILLIS);
635 
636         return appearanceAnimatorSet;
637     }
638 
createAnimationPause()639     protected Animator createAnimationPause() {
640         return ValueAnimator.ofFloat(0f, 1f).setDuration(GESTURE_ANIMATION_PAUSE_DURATION_MILLIS);
641     }
642 
643     /** Denotes the type of the tutorial. */
644     enum TutorialType {
645         BACK_NAVIGATION,
646         BACK_NAVIGATION_COMPLETE,
647         HOME_NAVIGATION,
648         HOME_NAVIGATION_COMPLETE,
649         OVERVIEW_NAVIGATION,
650         OVERVIEW_NAVIGATION_COMPLETE,
651         ASSISTANT,
652         ASSISTANT_COMPLETE,
653         SANDBOX_MODE
654     }
655 }
656