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 static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL;
23 
24 import android.animation.Animator;
25 import android.animation.AnimatorListenerAdapter;
26 import android.animation.AnimatorSet;
27 import android.animation.ObjectAnimator;
28 import android.animation.ValueAnimator;
29 import android.annotation.RawRes;
30 import android.content.Context;
31 import android.content.pm.PackageManager;
32 import android.graphics.Color;
33 import android.graphics.Matrix;
34 import android.graphics.Outline;
35 import android.graphics.Rect;
36 import android.graphics.drawable.AnimatedVectorDrawable;
37 import android.graphics.drawable.ColorDrawable;
38 import android.graphics.drawable.RippleDrawable;
39 import android.util.Log;
40 import android.view.View;
41 import android.view.ViewGroup;
42 import android.view.ViewOutlineProvider;
43 import android.view.accessibility.AccessibilityEvent;
44 import android.view.accessibility.AccessibilityManager;
45 import android.widget.Button;
46 import android.widget.FrameLayout;
47 import android.widget.ImageView;
48 import android.widget.RelativeLayout;
49 import android.widget.TextView;
50 
51 import androidx.annotation.CallSuper;
52 import androidx.annotation.ColorInt;
53 import androidx.annotation.DrawableRes;
54 import androidx.annotation.LayoutRes;
55 import androidx.annotation.NonNull;
56 import androidx.annotation.Nullable;
57 import androidx.annotation.StringRes;
58 import androidx.annotation.StyleRes;
59 import androidx.appcompat.app.AlertDialog;
60 import androidx.appcompat.content.res.AppCompatResources;
61 
62 import com.android.launcher3.DeviceProfile;
63 import com.android.launcher3.R;
64 import com.android.launcher3.Utilities;
65 import com.android.launcher3.anim.AnimatorListeners;
66 import com.android.launcher3.views.ClipIconView;
67 import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureAttemptCallback;
68 import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureAttemptCallback;
69 import com.android.systemui.shared.system.QuickStepContract;
70 
71 import com.airbnb.lottie.LottieAnimationView;
72 import com.airbnb.lottie.LottieComposition;
73 
74 import java.util.ArrayList;
75 
76 abstract class TutorialController implements BackGestureAttemptCallback,
77         NavBarGestureAttemptCallback {
78 
79     private static final String LOG_TAG = "TutorialController";
80 
81     private static final float FINGER_DOT_VISIBLE_ALPHA = 0.7f;
82     private static final float FINGER_DOT_SMALL_SCALE = 0.7f;
83     private static final int FINGER_DOT_ANIMATION_DURATION_MILLIS = 500;
84 
85     private static final String PIXEL_TIPS_APP_PACKAGE_NAME = "com.google.android.apps.tips";
86     private static final CharSequence DEFAULT_PIXEL_TIPS_APP_NAME = "Pixel Tips";
87 
88     private static final int FEEDBACK_ANIMATION_MS = 133;
89     private static final int RIPPLE_VISIBLE_MS = 300;
90     private static final int GESTURE_ANIMATION_DELAY_MS = 1500;
91     private static final int ADVANCE_TUTORIAL_TIMEOUT_MS = 3000;
92     private static final long GESTURE_ANIMATION_PAUSE_DURATION_MILLIS = 1000;
93     protected float mExitingAppEndingCornerRadius;
94     protected float mExitingAppStartingCornerRadius;
95     protected int mScreenHeight;
96     protected float mScreenWidth;
97     protected float mExitingAppMargin;
98 
99     final TutorialFragment mTutorialFragment;
100     TutorialType mTutorialType;
101     final Context mContext;
102 
103     final TextView mSkipButton;
104     final Button mDoneButton;
105     final ViewGroup mFeedbackView;
106     final TextView mFeedbackTitleView;
107     final TextView mFeedbackSubtitleView;
108     final ImageView mEdgeGestureVideoView;
109     final RelativeLayout mFakeLauncherView;
110     final FrameLayout mFakeHotseatView;
111     @Nullable View mHotseatIconView;
112     final ClipIconView mFakeIconView;
113     final FrameLayout mFakeTaskView;
114     @Nullable final AnimatedTaskbarView mFakeTaskbarView;
115     final AnimatedTaskView mFakePreviousTaskView;
116     final View mRippleView;
117     final RippleDrawable mRippleDrawable;
118     final TutorialStepIndicator mTutorialStepView;
119     final ImageView mFingerDotView;
120     private final Rect mExitingAppRect = new Rect();
121     protected View mExitingAppView;
122     protected int mExitingAppRadius;
123     private final AlertDialog mSkipTutorialDialog;
124 
125     private boolean mGestureCompleted = false;
126     protected LottieAnimationView mAnimatedGestureDemonstration;
127     protected LottieAnimationView mCheckmarkAnimation;
128     private RelativeLayout mFullGestureDemonstration;
129 
130     // These runnables  should be used when posting callbacks to their views and cleared from their
131     // views before posting new callbacks.
132     private final Runnable mTitleViewCallback;
133     @Nullable private Runnable mFeedbackViewCallback;
134     @Nullable private Runnable mFakeTaskViewCallback;
135     @Nullable private Runnable mFakeTaskbarViewCallback;
136     private final Runnable mShowFeedbackRunnable;
137 
TutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType)138     TutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType) {
139         mTutorialFragment = tutorialFragment;
140         mTutorialType = tutorialType;
141         mContext = mTutorialFragment.getContext();
142 
143         RootSandboxLayout rootView = tutorialFragment.getRootView();
144         mSkipButton = rootView.findViewById(R.id.gesture_tutorial_fragment_close_button);
145         mSkipButton.setOnClickListener(button -> showSkipTutorialDialog());
146         mFeedbackView = rootView.findViewById(R.id.gesture_tutorial_fragment_feedback_view);
147         mFeedbackTitleView = mFeedbackView.findViewById(
148                 R.id.gesture_tutorial_fragment_feedback_title);
149         mFeedbackSubtitleView = mFeedbackView.findViewById(
150                 R.id.gesture_tutorial_fragment_feedback_subtitle);
151         mEdgeGestureVideoView = rootView.findViewById(R.id.gesture_tutorial_edge_gesture_video);
152         mFakeLauncherView = rootView.findViewById(R.id.gesture_tutorial_fake_launcher_view);
153         mFakeHotseatView = rootView.findViewById(R.id.gesture_tutorial_fake_hotseat_view);
154         mFakeIconView = rootView.findViewById(R.id.gesture_tutorial_fake_icon_view);
155         mFakeTaskView = rootView.findViewById(R.id.gesture_tutorial_fake_task_view);
156         mFakeTaskbarView = ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
157                 ? null : rootView.findViewById(R.id.gesture_tutorial_fake_taskbar_view);
158         mFakePreviousTaskView =
159                 rootView.findViewById(R.id.gesture_tutorial_fake_previous_task_view);
160         mRippleView = rootView.findViewById(R.id.gesture_tutorial_ripple_view);
161         mRippleDrawable = (RippleDrawable) mRippleView.getBackground();
162         mDoneButton = rootView.findViewById(R.id.gesture_tutorial_fragment_action_button);
163         mTutorialStepView =
164                 rootView.findViewById(R.id.gesture_tutorial_fragment_feedback_tutorial_step);
165         mFingerDotView = rootView.findViewById(R.id.gesture_tutorial_finger_dot);
166         mSkipTutorialDialog = createSkipTutorialDialog();
167 
168         if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
169             mFullGestureDemonstration = rootView.findViewById(R.id.full_gesture_demonstration);
170             mCheckmarkAnimation = rootView.findViewById(R.id.checkmark_animation);
171             mAnimatedGestureDemonstration = rootView.findViewById(
172                     R.id.gesture_demonstration_animations);
173             mExitingAppView = rootView.findViewById(R.id.exiting_app_back);
174             mScreenWidth = mTutorialFragment.getDeviceProfile().widthPx;
175             mScreenHeight = mTutorialFragment.getDeviceProfile().heightPx;
176             mExitingAppMargin = mContext.getResources().getDimensionPixelSize(
177                     R.dimen.gesture_tutorial_back_gesture_exiting_app_margin);
178             mExitingAppStartingCornerRadius = QuickStepContract.getWindowCornerRadius(mContext);
179             mExitingAppEndingCornerRadius = mContext.getResources().getDimensionPixelSize(
180                     R.dimen.gesture_tutorial_back_gesture_end_corner_radius);
181             mAnimatedGestureDemonstration.addLottieOnCompositionLoadedListener(
182                     this::createScalingMatrix);
183 
184             mFeedbackTitleView.setText(getIntroductionTitle());
185             mFeedbackSubtitleView.setText(getIntroductionSubtitle());
186             mExitingAppView.setClipToOutline(true);
187             mExitingAppView.setOutlineProvider(new ViewOutlineProvider() {
188                 @Override
189                 public void getOutline(View view, Outline outline) {
190                     outline.setRoundRect(mExitingAppRect, mExitingAppRadius);
191                 }
192             });
193         }
194 
195         mTitleViewCallback = () -> mFeedbackTitleView.sendAccessibilityEvent(
196                 AccessibilityEvent.TYPE_VIEW_FOCUSED);
197         mShowFeedbackRunnable = () -> {
198             mFeedbackView.setAlpha(0f);
199             mFeedbackView.setScaleX(0.95f);
200             mFeedbackView.setScaleY(0.95f);
201             mFeedbackView.setVisibility(View.VISIBLE);
202             mFeedbackView.animate()
203                     .setDuration(FEEDBACK_ANIMATION_MS)
204                     .alpha(1f)
205                     .scaleX(1f)
206                     .scaleY(1f)
207                     .withEndAction(() -> {
208                         if (mGestureCompleted && !mTutorialFragment.isAtFinalStep()) {
209                             if (mFeedbackViewCallback != null) {
210                                 mFeedbackView.removeCallbacks(mFeedbackViewCallback);
211                             }
212                             mFeedbackViewCallback = mTutorialFragment::continueTutorial;
213                             mFeedbackView.postDelayed(
214                                     mFeedbackViewCallback,
215                                     AccessibilityManager.getInstance(mContext)
216                                             .getRecommendedTimeoutMillis(
217                                                     ADVANCE_TUTORIAL_TIMEOUT_MS,
218                                                     AccessibilityManager.FLAG_CONTENT_TEXT));
219                         }
220                     })
221                     .start();
222             mFeedbackTitleView.postDelayed(mTitleViewCallback, FEEDBACK_ANIMATION_MS);
223         };
224     }
225 
226     /** Scale the Lottie gesture animation to fit the device based on device dimensions */
createScalingMatrix(LottieComposition composition)227     private void createScalingMatrix(LottieComposition composition) {
228         Rect animationBoundsRect = composition.getBounds();
229         if (animationBoundsRect == null) {
230             mAnimatedGestureDemonstration.setScaleType(ImageView.ScaleType.CENTER_CROP);
231             return;
232         }
233         Matrix scaleMatrix = new Matrix();
234         float scaleFactor = mScreenWidth / animationBoundsRect.width();
235         float heightTranslate = (mScreenHeight - (scaleFactor * animationBoundsRect.height()));
236 
237         scaleMatrix.postScale(scaleFactor, scaleFactor);
238         scaleMatrix.postTranslate(0, heightTranslate);
239         mAnimatedGestureDemonstration.setImageMatrix(scaleMatrix);
240     }
241 
showSkipTutorialDialog()242     private void showSkipTutorialDialog() {
243         if (mSkipTutorialDialog != null) {
244             mSkipTutorialDialog.show();
245         }
246     }
247 
getHotseatIconTop()248     public int getHotseatIconTop() {
249         return mHotseatIconView == null
250                 ? 0 : mFakeHotseatView.getTop() + mHotseatIconView.getTop();
251     }
252 
getHotseatIconLeft()253     public int getHotseatIconLeft() {
254         return mHotseatIconView == null
255                 ? 0 : mFakeHotseatView.getLeft() + mHotseatIconView.getLeft();
256     }
257 
setTutorialType(TutorialType tutorialType)258     void setTutorialType(TutorialType tutorialType) {
259         mTutorialType = tutorialType;
260     }
261 
262     @LayoutRes
getMockHotseatResId()263     protected int getMockHotseatResId() {
264         if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
265             return mTutorialFragment.isLargeScreen()
266                     ? mTutorialFragment.isFoldable()
267                         ? R.layout.redesigned_gesture_tutorial_foldable_mock_hotseat
268                         : R.layout.redesigned_gesture_tutorial_tablet_mock_hotseat
269                     : R.layout.redesigned_gesture_tutorial_mock_hotseat;
270         } else {
271             return mTutorialFragment.isLargeScreen()
272                     ? mTutorialFragment.isFoldable()
273                         ? R.layout.gesture_tutorial_foldable_mock_hotseat
274                         : R.layout.gesture_tutorial_tablet_mock_hotseat
275                     : R.layout.gesture_tutorial_mock_hotseat;
276         }
277     }
278 
279     @LayoutRes
getMockAppTaskLayoutResId()280     protected int getMockAppTaskLayoutResId() {
281         return NO_ID;
282     }
283 
284     @RawRes
getGestureLottieAnimationId()285     protected int getGestureLottieAnimationId() {
286         return NO_ID;
287     }
288 
289     @ColorInt
getMockPreviousAppTaskThumbnailColor()290     protected int getMockPreviousAppTaskThumbnailColor() {
291         return mContext.getResources().getColor(
292                 R.color.gesture_tutorial_fake_previous_task_view_color);
293     }
294 
295     @ColorInt
getFakeTaskViewColor()296     protected int getFakeTaskViewColor() {
297         return Color.TRANSPARENT;
298     }
299 
300     @ColorInt
getFakeLauncherColor()301     protected abstract int getFakeLauncherColor();
302 
303     @ColorInt
getExitingAppColor()304     protected int getExitingAppColor() {
305         return Color.TRANSPARENT;
306     }
307 
308     @ColorInt
getHotseatIconColor()309     protected int getHotseatIconColor() {
310         return Color.TRANSPARENT;
311     }
312 
313     @DrawableRes
getMockAppIconResId()314     public int getMockAppIconResId() {
315         return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
316                 ? R.drawable.redesigned_hotseat_icon
317                 : R.drawable.default_sandbox_app_icon;
318     }
319 
320     @DrawableRes
getMockWallpaperResId()321     public int getMockWallpaperResId() {
322         return R.drawable.default_sandbox_wallpaper;
323     }
324 
fadeTaskViewAndRun(Runnable r)325     void fadeTaskViewAndRun(Runnable r) {
326         mFakeTaskView.animate().alpha(0).setListener(AnimatorListeners.forSuccessCallback(r));
327     }
328 
329     @StringRes
getIntroductionTitle()330     public int getIntroductionTitle() {
331         return NO_ID;
332     }
333 
334     @StringRes
getIntroductionSubtitle()335     public int getIntroductionSubtitle() {
336         return NO_ID;
337     }
338 
339     @StringRes
getSpokenIntroductionSubtitle()340     public int getSpokenIntroductionSubtitle() {
341         return NO_ID;
342     }
343 
344     @StringRes
getSuccessFeedbackSubtitle()345     public int getSuccessFeedbackSubtitle() {
346         return NO_ID;
347     }
348 
349     @StringRes
getSuccessFeedbackTitle()350     public int getSuccessFeedbackTitle() {
351         return NO_ID;
352     }
353 
354     @StyleRes
getTitleTextAppearance()355     public int getTitleTextAppearance() {
356         return NO_ID;
357     }
358 
359     @StyleRes
getSuccessTitleTextAppearance()360     public int getSuccessTitleTextAppearance() {
361         return NO_ID;
362     }
363 
364     @StyleRes
getDoneButtonTextAppearance()365     public int getDoneButtonTextAppearance() {
366         return NO_ID;
367     }
368 
369     @ColorInt
getDoneButtonColor()370     public abstract int getDoneButtonColor();
371 
showFeedback()372     void showFeedback() {
373         if (mGestureCompleted) {
374             mFeedbackView.setTranslationY(0);
375             return;
376         }
377         Animator gestureAnimation = mTutorialFragment.getGestureAnimation();
378         AnimatedVectorDrawable edgeAnimation = mTutorialFragment.getEdgeAnimation();
379         if (gestureAnimation != null && edgeAnimation != null) {
380             playFeedbackAnimation(gestureAnimation, edgeAnimation, mShowFeedbackRunnable, true);
381         }
382     }
383 
384     /**
385      * Only use this when a gesture is completed, but the feedback shouldn't be shown immediately.
386      * In that case, call this method immediately instead.
387      */
setGestureCompleted()388     public void setGestureCompleted() {
389         mGestureCompleted = true;
390     }
391 
392     /**
393      * Show feedback reflecting a successful gesture attempt.
394      **/
showSuccessFeedback()395     void showSuccessFeedback() {
396         int successSubtitleResId = getSuccessFeedbackSubtitle();
397         if (successSubtitleResId == NO_ID) {
398             // Allow crash since this should never be reached with a tutorial controller used in
399             // production.
400             Log.e(LOG_TAG,
401                     "Cannot show success feedback for tutorial step: " + mTutorialType
402                             + ", no success feedback subtitle",
403                     new IllegalStateException());
404         }
405         showFeedback(successSubtitleResId, true);
406     }
407 
408     /**
409      * Show feedback reflecting a failed gesture attempt.
410      *
411      * @param subtitleResId Resource of the text to display.
412      **/
showFeedback(int subtitleResId)413     void showFeedback(int subtitleResId) {
414         showFeedback(subtitleResId, false);
415     }
416 
417     /**
418      * Show feedback reflecting the result of a gesture attempt.
419      *
420      * @param isGestureSuccessful Whether the tutorial feedback's action button should be shown.
421      **/
showFeedback(int subtitleResId, boolean isGestureSuccessful)422     void showFeedback(int subtitleResId, boolean isGestureSuccessful) {
423         showFeedback(
424                 isGestureSuccessful
425                         ? getSuccessFeedbackTitle() : R.string.gesture_tutorial_try_again,
426                 subtitleResId,
427                 NO_ID,
428                 isGestureSuccessful,
429                 false);
430     }
431 
showFeedback( int titleResId, int subtitleResId, int spokenSubtitleResId, boolean isGestureSuccessful, boolean useGestureAnimationDelay)432     void showFeedback(
433             int titleResId,
434             int subtitleResId,
435             int spokenSubtitleResId,
436             boolean isGestureSuccessful,
437             boolean useGestureAnimationDelay) {
438         mFeedbackTitleView.removeCallbacks(mTitleViewCallback);
439         if (mFeedbackViewCallback != null) {
440             mFeedbackView.removeCallbacks(mFeedbackViewCallback);
441             mFeedbackViewCallback = null;
442         }
443 
444         mFeedbackTitleView.setText(titleResId);
445         mFeedbackSubtitleView.setText(
446                 ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() || spokenSubtitleResId == NO_ID
447                         ? mContext.getText(subtitleResId)
448                         : Utilities.wrapForTts(
449                                 mContext.getText(subtitleResId),
450                                 mContext.getString(spokenSubtitleResId)));
451         if (isGestureSuccessful) {
452             if (mTutorialFragment.isAtFinalStep()) {
453                 showActionButton();
454             }
455 
456             if (mFakeTaskViewCallback != null) {
457                 mFakeTaskView.removeCallbacks(mFakeTaskViewCallback);
458                 mFakeTaskViewCallback = null;
459             }
460 
461             if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
462                 showSuccessPage();
463             }
464         }
465         mGestureCompleted = isGestureSuccessful;
466 
467         Animator gestureAnimation = mTutorialFragment.getGestureAnimation();
468         AnimatedVectorDrawable edgeAnimation = mTutorialFragment.getEdgeAnimation();
469         if (!isGestureSuccessful && gestureAnimation != null && edgeAnimation != null) {
470             playFeedbackAnimation(
471                     gestureAnimation,
472                     edgeAnimation,
473                     mShowFeedbackRunnable,
474                     useGestureAnimationDelay);
475             return;
476         } else {
477             mTutorialFragment.releaseFeedbackAnimation();
478         }
479         mFeedbackViewCallback = mShowFeedbackRunnable;
480 
481         mFeedbackView.post(mFeedbackViewCallback);
482     }
483 
showSuccessPage()484     private void showSuccessPage() {
485         pauseAndHideLottieAnimation();
486         mCheckmarkAnimation.setVisibility(View.VISIBLE);
487         mCheckmarkAnimation.playAnimation();
488         mFeedbackTitleView.setTextAppearance(mContext, getSuccessTitleTextAppearance());
489     }
490 
isGestureCompleted()491     public boolean isGestureCompleted() {
492         return mGestureCompleted;
493     }
494 
hideFeedback()495     void hideFeedback() {
496         if (mFeedbackView.getVisibility() != View.VISIBLE) {
497             return;
498         }
499         cancelQueuedGestureAnimation();
500         mFeedbackView.clearAnimation();
501         mFeedbackView.setVisibility(View.INVISIBLE);
502     }
503 
cancelQueuedGestureAnimation()504     void cancelQueuedGestureAnimation() {
505         if (mFeedbackViewCallback != null) {
506             mFeedbackView.removeCallbacks(mFeedbackViewCallback);
507             mFeedbackViewCallback = null;
508         }
509         if (mFakeTaskViewCallback != null) {
510             mFakeTaskView.removeCallbacks(mFakeTaskViewCallback);
511             mFakeTaskViewCallback = null;
512         }
513         if (mFakeTaskbarViewCallback != null && mFakeTaskbarView != null) {
514             mFakeTaskbarView.removeCallbacks(mFakeTaskbarViewCallback);
515             mFakeTaskbarViewCallback = null;
516         }
517         mFeedbackTitleView.removeCallbacks(mTitleViewCallback);
518     }
519 
playFeedbackAnimation( @onNull Animator gestureAnimation, @NonNull AnimatedVectorDrawable edgeAnimation, @NonNull Runnable onStartRunnable, boolean useGestureAnimationDelay)520     private void playFeedbackAnimation(
521             @NonNull Animator gestureAnimation,
522             @NonNull AnimatedVectorDrawable edgeAnimation,
523             @NonNull Runnable onStartRunnable,
524             boolean useGestureAnimationDelay) {
525 
526         if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
527             mFeedbackView.setVisibility(View.VISIBLE);
528             mAnimatedGestureDemonstration.setVisibility(View.VISIBLE);
529             mFullGestureDemonstration.setVisibility(View.VISIBLE);
530             mAnimatedGestureDemonstration.playAnimation();
531             return;
532         }
533 
534         if (gestureAnimation.isRunning()) {
535             gestureAnimation.cancel();
536         }
537         if (edgeAnimation.isRunning()) {
538             edgeAnimation.reset();
539         }
540         gestureAnimation.addListener(new AnimatorListenerAdapter() {
541             @Override
542             public void onAnimationStart(Animator animation) {
543                 super.onAnimationStart(animation);
544 
545                 mEdgeGestureVideoView.setVisibility(GONE);
546                 if (edgeAnimation.isRunning()) {
547                     edgeAnimation.stop();
548                 }
549 
550                 if (!useGestureAnimationDelay) {
551                     onStartRunnable.run();
552                 }
553             }
554 
555             @Override
556             public void onAnimationEnd(Animator animation) {
557                 super.onAnimationEnd(animation);
558 
559                 mEdgeGestureVideoView.setVisibility(View.VISIBLE);
560                 edgeAnimation.start();
561 
562                 gestureAnimation.removeListener(this);
563             }
564         });
565 
566         cancelQueuedGestureAnimation();
567         if (useGestureAnimationDelay) {
568             mFeedbackViewCallback = onStartRunnable;
569             mFakeTaskViewCallback = gestureAnimation::start;
570 
571             mFeedbackView.post(mFeedbackViewCallback);
572             mFakeTaskView.postDelayed(mFakeTaskViewCallback, GESTURE_ANIMATION_DELAY_MS);
573         } else {
574             gestureAnimation.start();
575         }
576     }
577 
setRippleHotspot(float x, float y)578     void setRippleHotspot(float x, float y) {
579         mRippleDrawable.setHotspot(x, y);
580     }
581 
showRippleEffect(@ullable Runnable onCompleteRunnable)582     void showRippleEffect(@Nullable Runnable onCompleteRunnable) {
583         mRippleDrawable.setState(
584                 new int[] {android.R.attr.state_pressed, android.R.attr.state_enabled});
585         mRippleView.postDelayed(() -> {
586             mRippleDrawable.setState(new int[] {});
587             if (onCompleteRunnable != null) {
588                 onCompleteRunnable.run();
589             }
590         }, RIPPLE_VISIBLE_MS);
591     }
592 
onActionButtonClicked(View button)593     void onActionButtonClicked(View button) {
594         mTutorialFragment.continueTutorial();
595     }
596 
597     @CallSuper
transitToController()598     void transitToController() {
599         updateCloseButton();
600         updateSubtext();
601         updateDrawables();
602         updateLayout();
603 
604         if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
605             mFeedbackTitleView.setTextAppearance(mContext, getTitleTextAppearance());
606             mDoneButton.setTextAppearance(mContext, getDoneButtonTextAppearance());
607             mDoneButton.getBackground().setTint(getDoneButtonColor());
608             mCheckmarkAnimation.setAnimation(mTutorialFragment.isAtFinalStep()
609                     ? R.raw.checkmark_animation_end
610                     : R.raw.checkmark_animation_in_progress);
611             if (!isGestureCompleted()) {
612                 mCheckmarkAnimation.setVisibility(GONE);
613                 startGestureAnimation();
614                 if (mTutorialType == TutorialType.BACK_NAVIGATION) {
615                     resetViewsForBackGesture();
616                 }
617 
618             }
619         } else {
620             hideFeedback();
621             hideActionButton();
622         }
623 
624         mGestureCompleted = false;
625         if (mFakeHotseatView != null) {
626             mFakeHotseatView.setVisibility(View.INVISIBLE);
627         }
628     }
629 
resetViewsForBackGesture()630     protected void resetViewsForBackGesture() {
631         mFakeTaskView.setVisibility(View.VISIBLE);
632         mFakeTaskView.setBackgroundColor(getFakeTaskViewColor());
633         mExitingAppView.setVisibility(View.VISIBLE);
634 
635         // reset the exiting app's dimensions
636         mExitingAppRect.set(0, 0, (int) mScreenWidth, (int) mScreenHeight);
637         mExitingAppRadius = 0;
638         mExitingAppView.resetPivot();
639         mExitingAppView.setScaleX(1f);
640         mExitingAppView.setScaleY(1f);
641         mExitingAppView.setTranslationX(0);
642         mExitingAppView.setTranslationY(0);
643         mExitingAppView.invalidateOutline();
644     }
645 
startGestureAnimation()646     private void startGestureAnimation() {
647         mAnimatedGestureDemonstration.setAnimation(getGestureLottieAnimationId());
648         mAnimatedGestureDemonstration.playAnimation();
649     }
650 
updateCloseButton()651     void updateCloseButton() {
652         mSkipButton.setTextAppearance(Utilities.isDarkTheme(mContext)
653                 ? R.style.TextAppearance_GestureTutorial_Feedback_Subtext
654                 : R.style.TextAppearance_GestureTutorial_Feedback_Subtext_Dark);
655     }
656 
hideActionButton()657     void hideActionButton() {
658         mSkipButton.setVisibility(View.VISIBLE);
659         // Invisible to maintain the layout.
660         mDoneButton.setVisibility(View.INVISIBLE);
661         mDoneButton.setOnClickListener(null);
662     }
663 
showActionButton()664     void showActionButton() {
665         mSkipButton.setVisibility(GONE);
666         mDoneButton.setVisibility(View.VISIBLE);
667         mDoneButton.setOnClickListener(this::onActionButtonClicked);
668     }
669 
hideFakeTaskbar(boolean animateToHotseat)670     void hideFakeTaskbar(boolean animateToHotseat) {
671         if (!mTutorialFragment.isLargeScreen() || mFakeTaskbarView == null) {
672             return;
673         }
674         if (mFakeTaskbarViewCallback != null) {
675             mFakeTaskbarView.removeCallbacks(mFakeTaskbarViewCallback);
676         }
677         if (animateToHotseat) {
678             mFakeTaskbarViewCallback = () ->
679                     mFakeTaskbarView.animateDisappearanceToHotseat(mFakeHotseatView);
680         }
681         mFakeTaskbarView.post(mFakeTaskbarViewCallback);
682     }
683 
showFakeTaskbar(boolean animateFromHotseat)684     void showFakeTaskbar(boolean animateFromHotseat) {
685         if (!mTutorialFragment.isLargeScreen() || mFakeTaskbarView == null) {
686             return;
687         }
688         if (mFakeTaskbarViewCallback != null) {
689             mFakeTaskbarView.removeCallbacks(mFakeTaskbarViewCallback);
690         }
691         if (animateFromHotseat) {
692             mFakeTaskbarViewCallback = () ->
693                     mFakeTaskbarView.animateAppearanceFromHotseat(mFakeHotseatView);
694         }
695         mFakeTaskbarView.post(mFakeTaskbarViewCallback);
696     }
697 
updateFakeAppTaskViewLayout(@ayoutRes int mockAppTaskLayoutResId)698     void updateFakeAppTaskViewLayout(@LayoutRes int mockAppTaskLayoutResId) {
699         updateFakeViewLayout(mFakeTaskView, mockAppTaskLayoutResId);
700     }
701 
updateFakeViewLayout(ViewGroup view, @LayoutRes int mockLayoutResId)702     void updateFakeViewLayout(ViewGroup view, @LayoutRes int mockLayoutResId) {
703         view.removeAllViews();
704         if (mockLayoutResId != NO_ID) {
705             view.addView(
706                     inflate(mContext, mockLayoutResId, null),
707                     new FrameLayout.LayoutParams(
708                             ViewGroup.LayoutParams.MATCH_PARENT,
709                             ViewGroup.LayoutParams.MATCH_PARENT));
710         }
711     }
712 
updateSubtext()713     private void updateSubtext() {
714         if (!ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
715             mTutorialStepView.setTutorialProgress(
716                     mTutorialFragment.getCurrentStep(), mTutorialFragment.getNumSteps());
717         }
718     }
719 
updateHotseatChildViewColor(@ullable View child)720     private void updateHotseatChildViewColor(@Nullable View child) {
721         if (child == null) return;
722         child.getBackground().setTint(getHotseatIconColor());
723     }
724 
updateDrawables()725     private void updateDrawables() {
726         if (mContext != null) {
727             mTutorialFragment.getRootView().setBackground(AppCompatResources.getDrawable(
728                     mContext, getMockWallpaperResId()));
729             mTutorialFragment.updateFeedbackAnimation();
730             mFakeLauncherView.setBackgroundColor(ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
731                     ? getFakeLauncherColor()
732                     : mContext.getColor(R.color.gesture_tutorial_fake_wallpaper_color));
733             updateFakeViewLayout(mFakeHotseatView, getMockHotseatResId());
734             mHotseatIconView = mFakeHotseatView.findViewById(R.id.hotseat_icon_1);
735             mFakeTaskView.animate().alpha(1).setListener(
736                     AnimatorListeners.forSuccessCallback(() -> mFakeTaskView.animate().cancel()));
737             mFakePreviousTaskView.setFakeTaskViewFillColor(getMockPreviousAppTaskThumbnailColor());
738             mFakeIconView.setBackground(AppCompatResources.getDrawable(
739                     mContext, getMockAppIconResId()));
740 
741             if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
742                 mExitingAppView.setBackgroundColor(getExitingAppColor());
743                 mFakeTaskView.setBackgroundColor(getFakeTaskViewColor());
744                 updateHotseatChildViewColor(mHotseatIconView);
745                 updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_2));
746                 updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_3));
747                 updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_4));
748                 updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_5));
749                 updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_6));
750                 updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_search_bar));
751             } else {
752                 updateFakeViewLayout(mFakeTaskView, getMockAppTaskLayoutResId());
753             }
754         }
755     }
756 
updateLayout()757     private void updateLayout() {
758         if (mContext == null) {
759             return;
760         }
761         RelativeLayout.LayoutParams feedbackLayoutParams =
762                 (RelativeLayout.LayoutParams) mFeedbackView.getLayoutParams();
763         feedbackLayoutParams.setMarginStart(mContext.getResources().getDimensionPixelSize(
764                 mTutorialFragment.isLargeScreen()
765                         ? R.dimen.gesture_tutorial_tablet_feedback_margin_start_end
766                         : R.dimen.gesture_tutorial_feedback_margin_start_end));
767         feedbackLayoutParams.setMarginEnd(mContext.getResources().getDimensionPixelSize(
768                 mTutorialFragment.isLargeScreen()
769                         ? R.dimen.gesture_tutorial_tablet_feedback_margin_start_end
770                         : R.dimen.gesture_tutorial_feedback_margin_start_end));
771         feedbackLayoutParams.topMargin = mContext.getResources().getDimensionPixelSize(
772                 mTutorialFragment.isLargeScreen()
773                         ? R.dimen.gesture_tutorial_tablet_feedback_margin_top
774                         : R.dimen.gesture_tutorial_feedback_margin_top);
775 
776         if (mFakeTaskbarView != null) {
777             mFakeTaskbarView.setVisibility(
778                     mTutorialFragment.isLargeScreen() ? View.VISIBLE : GONE);
779         }
780 
781         RelativeLayout.LayoutParams hotseatLayoutParams =
782                 (RelativeLayout.LayoutParams) mFakeHotseatView.getLayoutParams();
783         if (!mTutorialFragment.isLargeScreen()) {
784             DeviceProfile dp = mTutorialFragment.getDeviceProfile();
785             dp.updateIsSeascape(mContext);
786 
787             hotseatLayoutParams.addRule(dp.isLandscape
788                     ? (dp.isSeascape()
789                             ? RelativeLayout.ALIGN_PARENT_START
790                             : RelativeLayout.ALIGN_PARENT_END)
791                     : RelativeLayout.ALIGN_PARENT_BOTTOM);
792         } else {
793             hotseatLayoutParams.width = RelativeLayout.LayoutParams.MATCH_PARENT;
794             hotseatLayoutParams.height = RelativeLayout.LayoutParams.WRAP_CONTENT;
795             hotseatLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM);
796             hotseatLayoutParams.removeRule(RelativeLayout.ALIGN_PARENT_START);
797             hotseatLayoutParams.removeRule(RelativeLayout.ALIGN_PARENT_END);
798         }
799         mFakeHotseatView.setLayoutParams(hotseatLayoutParams);
800     }
801 
createSkipTutorialDialog()802     private AlertDialog createSkipTutorialDialog() {
803         if (!(mContext instanceof GestureSandboxActivity)) {
804             return null;
805         }
806         GestureSandboxActivity sandboxActivity = (GestureSandboxActivity) mContext;
807         View contentView = View.inflate(
808                 sandboxActivity, R.layout.gesture_tutorial_dialog, null);
809         AlertDialog tutorialDialog = new AlertDialog
810                 .Builder(sandboxActivity, R.style.Theme_AppCompat_Dialog_Alert)
811                 .setView(contentView)
812                 .create();
813 
814         PackageManager packageManager = mContext.getPackageManager();
815         CharSequence tipsAppName = DEFAULT_PIXEL_TIPS_APP_NAME;
816 
817         try {
818             tipsAppName = packageManager.getApplicationLabel(
819                     packageManager.getApplicationInfo(
820                             PIXEL_TIPS_APP_PACKAGE_NAME, PackageManager.GET_META_DATA));
821         } catch (PackageManager.NameNotFoundException e) {
822             Log.e(LOG_TAG,
823                     "Could not find app label for package name: "
824                             + PIXEL_TIPS_APP_PACKAGE_NAME
825                             + ". Defaulting to 'Pixel Tips.'",
826                     e);
827         }
828 
829         TextView subtitleTextView = (TextView) contentView.findViewById(
830                 R.id.gesture_tutorial_dialog_subtitle);
831         if (subtitleTextView != null) {
832             subtitleTextView.setText(
833                     mContext.getString(R.string.skip_tutorial_dialog_subtitle, tipsAppName));
834         } else {
835             Log.w(LOG_TAG, "No subtitle view in the skip tutorial dialog to update.");
836         }
837 
838         Button cancelButton = (Button) contentView.findViewById(
839                 R.id.gesture_tutorial_dialog_cancel_button);
840         if (cancelButton != null) {
841             cancelButton.setOnClickListener(
842                     v -> tutorialDialog.dismiss());
843         } else {
844             Log.w(LOG_TAG, "No cancel button in the skip tutorial dialog to update.");
845         }
846 
847         Button confirmButton = contentView.findViewById(
848                 R.id.gesture_tutorial_dialog_confirm_button);
849         if (confirmButton != null) {
850             confirmButton.setOnClickListener(v -> {
851                 mTutorialFragment.closeTutorialStep(true);
852                 tutorialDialog.dismiss();
853             });
854         } else {
855             Log.w(LOG_TAG, "No confirm button in the skip tutorial dialog to update.");
856         }
857 
858         tutorialDialog.getWindow().setBackgroundDrawable(
859                 new ColorDrawable(sandboxActivity.getColor(android.R.color.transparent)));
860 
861         return tutorialDialog;
862     }
863 
createFingerDotAppearanceAnimatorSet()864     protected AnimatorSet createFingerDotAppearanceAnimatorSet() {
865         ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(
866                 mFingerDotView, View.ALPHA, 0f, FINGER_DOT_VISIBLE_ALPHA);
867         ObjectAnimator yScaleAnimator = ObjectAnimator.ofFloat(
868                 mFingerDotView, View.SCALE_Y, FINGER_DOT_SMALL_SCALE, 1f);
869         ObjectAnimator xScaleAnimator = ObjectAnimator.ofFloat(
870                 mFingerDotView, View.SCALE_X, FINGER_DOT_SMALL_SCALE, 1f);
871         ArrayList<Animator> animators = new ArrayList<>();
872 
873         animators.add(alphaAnimator);
874         animators.add(xScaleAnimator);
875         animators.add(yScaleAnimator);
876 
877         AnimatorSet appearanceAnimatorSet = new AnimatorSet();
878 
879         appearanceAnimatorSet.playTogether(animators);
880         appearanceAnimatorSet.setDuration(FINGER_DOT_ANIMATION_DURATION_MILLIS);
881 
882         return appearanceAnimatorSet;
883     }
884 
createFingerDotDisappearanceAnimatorSet()885     protected AnimatorSet createFingerDotDisappearanceAnimatorSet() {
886         ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat(
887                 mFingerDotView, View.ALPHA, FINGER_DOT_VISIBLE_ALPHA, 0f);
888         ObjectAnimator yScaleAnimator = ObjectAnimator.ofFloat(
889                 mFingerDotView, View.SCALE_Y, 1f, FINGER_DOT_SMALL_SCALE);
890         ObjectAnimator xScaleAnimator = ObjectAnimator.ofFloat(
891                 mFingerDotView, View.SCALE_X, 1f, FINGER_DOT_SMALL_SCALE);
892         ArrayList<Animator> animators = new ArrayList<>();
893 
894         animators.add(alphaAnimator);
895         animators.add(xScaleAnimator);
896         animators.add(yScaleAnimator);
897 
898         AnimatorSet appearanceAnimatorSet = new AnimatorSet();
899 
900         appearanceAnimatorSet.playTogether(animators);
901         appearanceAnimatorSet.setDuration(FINGER_DOT_ANIMATION_DURATION_MILLIS);
902 
903         return appearanceAnimatorSet;
904     }
905 
createAnimationPause()906     protected Animator createAnimationPause() {
907         return ValueAnimator.ofFloat(0f, 1f).setDuration(GESTURE_ANIMATION_PAUSE_DURATION_MILLIS);
908     }
909 
pauseAndHideLottieAnimation()910     void pauseAndHideLottieAnimation() {
911         mAnimatedGestureDemonstration.pauseAnimation();
912         mAnimatedGestureDemonstration.setVisibility(View.INVISIBLE);
913         mFullGestureDemonstration.setVisibility(View.INVISIBLE);
914     }
915 
916     /** Denotes the type of the tutorial. */
917     enum TutorialType {
918         BACK_NAVIGATION,
919         BACK_NAVIGATION_COMPLETE,
920         HOME_NAVIGATION,
921         HOME_NAVIGATION_COMPLETE,
922         OVERVIEW_NAVIGATION,
923         OVERVIEW_NAVIGATION_COMPLETE
924     }
925 }
926