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