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