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 com.android.app.animation.Interpolators.ACCELERATE;
19 import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL;
20 
21 import android.animation.Animator;
22 import android.animation.AnimatorListenerAdapter;
23 import android.animation.AnimatorSet;
24 import android.graphics.PointF;
25 import android.os.Handler;
26 
27 import androidx.annotation.ColorInt;
28 import androidx.core.graphics.ColorUtils;
29 
30 import com.android.launcher3.R;
31 import com.android.launcher3.Utilities;
32 import com.android.launcher3.anim.AnimatedFloat;
33 import com.android.launcher3.anim.PendingAnimation;
34 import com.android.quickstep.SwipeUpAnimationLogic;
35 import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
36 import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult;
37 import com.android.quickstep.util.LottieAnimationColorUtils;
38 
39 import java.util.ArrayList;
40 import java.util.Map;
41 
42 /** A {@link TutorialController} for the Overview tutorial. */
43 final class OverviewGestureTutorialController extends SwipeUpGestureTutorialController {
44 
45     private static final float LAUNCHER_COLOR_BLENDING_RATIO = 0.4f;
46 
OverviewGestureTutorialController(OverviewGestureTutorialFragment fragment, TutorialType tutorialType)47     OverviewGestureTutorialController(OverviewGestureTutorialFragment fragment,
48             TutorialType tutorialType) {
49         super(fragment, tutorialType);
50 
51         // Set the Lottie animation colors specifically for the Overview gesture
52         if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
53             LottieAnimationColorUtils.updateToArgbColors(
54                     mAnimatedGestureDemonstration,
55                     Map.of(".onSurfaceOverview", fragment.mRootView.mColorOnSurfaceOverview,
56                             ".surfaceOverview", fragment.mRootView.mColorSurfaceOverview,
57                             ".secondaryOverview", fragment.mRootView.mColorSecondaryOverview));
58 
59             LottieAnimationColorUtils.updateToArgbColors(
60                     mCheckmarkAnimation,
61                     Map.of(".checkmark",
62                             Utilities.isDarkTheme(mContext)
63                                     ? fragment.mRootView.mColorOnSurfaceOverview
64                                     : fragment.mRootView.mColorSecondaryOverview,
65                             ".checkmarkBackground", fragment.mRootView.mColorSurfaceOverview));
66         }
67     }
68     @Override
getIntroductionTitle()69     public int getIntroductionTitle() {
70         return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
71                 ? R.string.overview_gesture_tutorial_title
72                 : R.string.overview_gesture_intro_title;
73     }
74 
75     @Override
getIntroductionSubtitle()76     public int getIntroductionSubtitle() {
77         return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
78                 ? R.string.overview_gesture_tutorial_subtitle
79                 : R.string.overview_gesture_intro_subtitle;
80     }
81 
82     @Override
getSpokenIntroductionSubtitle()83     public int getSpokenIntroductionSubtitle() {
84         return R.string.overview_gesture_spoken_intro_subtitle;
85     }
86 
87     @Override
getSuccessFeedbackTitle()88     public int getSuccessFeedbackTitle() {
89         return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
90                 ? R.string.overview_gesture_tutorial_success
91                 : R.string.gesture_tutorial_nice;
92     }
93 
94     @Override
getSuccessFeedbackSubtitle()95     public int getSuccessFeedbackSubtitle() {
96         return mTutorialFragment.getNumSteps() > 1 && mTutorialFragment.isAtFinalStep()
97                 ? R.string.overview_gesture_feedback_complete_with_follow_up
98                 : R.string.overview_gesture_feedback_complete_without_follow_up;
99     }
100 
101     @Override
getTitleTextAppearance()102     public int getTitleTextAppearance() {
103         return R.style.TextAppearance_GestureTutorial_MainTitle_Overview;
104     }
105 
106     @Override
getSuccessTitleTextAppearance()107     public int getSuccessTitleTextAppearance() {
108         return R.style.TextAppearance_GestureTutorial_MainTitle_Success_Overview;
109     }
110 
111     @Override
getDoneButtonTextAppearance()112     public int getDoneButtonTextAppearance() {
113         return R.style.TextAppearance_GestureTutorial_ButtonLabel_Overview;
114     }
115 
116     @Override
getDoneButtonColor()117     public int getDoneButtonColor() {
118         return Utilities.isDarkTheme(mContext)
119                 ? mTutorialFragment.mRootView.mColorOnSurfaceOverview
120                 : mTutorialFragment.mRootView.mColorSecondaryOverview;
121     }
122 
123     @Override
getMockAppTaskLayoutResId()124     protected int getMockAppTaskLayoutResId() {
125         return mTutorialFragment.isLargeScreen()
126                 ? R.layout.gesture_tutorial_tablet_mock_conversation_list
127                 : R.layout.gesture_tutorial_mock_conversation_list;
128     }
129 
130     @Override
getGestureLottieAnimationId()131     protected int getGestureLottieAnimationId() {
132         return mTutorialFragment.isLargeScreen()
133                 ? mTutorialFragment.isFoldable()
134                     ? R.raw.overview_gesture_tutorial_open_foldable_animation
135                     : R.raw.overview_gesture_tutorial_tablet_animation
136                 : R.raw.overview_gesture_tutorial_animation;
137     }
138 
139     @ColorInt
getFakeTaskViewStartColor()140     private int getFakeTaskViewStartColor() {
141         return mTutorialFragment.mRootView.mColorSurfaceOverview;
142     }
143 
144     @ColorInt
getFakeTaskViewEndColor()145     private int getFakeTaskViewEndColor() {
146         return getMockPreviousAppTaskThumbnailColor();
147     }
148 
149     @Override
getFakeTaskViewColor()150     protected int getFakeTaskViewColor() {
151         return isGestureCompleted()
152                 ? getFakeTaskViewEndColor()
153                 : getFakeTaskViewStartColor();
154     }
155 
156     @Override
getFakeLauncherColor()157     protected int getFakeLauncherColor() {
158         return ColorUtils.blendARGB(
159                 mTutorialFragment.mRootView.mColorSurfaceContainer,
160                 mTutorialFragment.mRootView.mColorOnSurfaceOverview,
161                 LAUNCHER_COLOR_BLENDING_RATIO);
162     }
163 
164     @Override
getHotseatIconColor()165     protected int getHotseatIconColor() {
166         return mTutorialFragment.mRootView.mColorOnSurfaceOverview;
167     }
168 
169     @Override
getMockPreviousAppTaskThumbnailColor()170     protected int getMockPreviousAppTaskThumbnailColor() {
171         return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
172                 ? mTutorialFragment.mRootView.mColorSurfaceContainer
173                 : mContext.getResources().getColor(
174                         R.color.gesture_tutorial_fake_previous_task_view_color);
175     }
176 
177     @Override
onBackGestureAttempted(BackGestureResult result)178     public void onBackGestureAttempted(BackGestureResult result) {
179         if (isGestureCompleted()) {
180             return;
181         }
182         switch (mTutorialType) {
183             case OVERVIEW_NAVIGATION:
184                 switch (result) {
185                     case BACK_COMPLETED_FROM_LEFT:
186                     case BACK_COMPLETED_FROM_RIGHT:
187                     case BACK_CANCELLED_FROM_LEFT:
188                     case BACK_CANCELLED_FROM_RIGHT:
189                     case BACK_NOT_STARTED_TOO_FAR_FROM_EDGE:
190                         resetTaskViews();
191                         showFeedback(R.string.overview_gesture_feedback_swipe_too_far_from_edge);
192                         break;
193                 }
194                 break;
195             case OVERVIEW_NAVIGATION_COMPLETE:
196                 if (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT
197                         || result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT) {
198                     mTutorialFragment.close();
199                 }
200                 break;
201         }
202     }
203 
204     @Override
onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity)205     public void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity) {
206         if (isGestureCompleted()) {
207             return;
208         }
209         switch (mTutorialType) {
210             case OVERVIEW_NAVIGATION:
211                 switch (result) {
212                     case HOME_GESTURE_COMPLETED: {
213                         animateFakeTaskViewHome(finalVelocity, () -> {
214                             showFeedback(R.string.overview_gesture_feedback_home_detected);
215                             resetFakeTaskView(true);
216                         });
217                         break;
218                     }
219                     case HOME_NOT_STARTED_TOO_FAR_FROM_EDGE:
220                     case OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE:
221                         resetTaskViews();
222                         showFeedback(R.string.overview_gesture_feedback_swipe_too_far_from_edge);
223                         break;
224                     case OVERVIEW_GESTURE_COMPLETED:
225                         setGestureCompleted();
226                         mTutorialFragment.releaseFeedbackAnimation();
227                         animateTaskViewToOverview(ENABLE_NEW_GESTURE_NAV_TUTORIAL.get());
228                         onMotionPaused(true /*arbitrary value*/);
229                         if (!ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
230                             showSuccessFeedback();
231                         }
232                         break;
233                     case HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION:
234                     case HOME_OR_OVERVIEW_CANCELLED:
235                         fadeOutFakeTaskView(false, null);
236                         showFeedback(R.string.overview_gesture_feedback_wrong_swipe_direction);
237                         break;
238                 }
239                 break;
240             case OVERVIEW_NAVIGATION_COMPLETE:
241                 if (result == NavBarGestureResult.HOME_GESTURE_COMPLETED) {
242                     mTutorialFragment.close();
243                 }
244                 break;
245         }
246     }
247 
248     /**
249      * runnable executed with slight delay to ease the swipe animation after landing on overview
250      */
animateTaskViewToOverview(boolean animateDelayedSuccessFeedback)251     public void animateTaskViewToOverview(boolean animateDelayedSuccessFeedback) {
252         PendingAnimation anim = new PendingAnimation(TASK_VIEW_END_ANIMATION_DURATION_MILLIS);
253         anim.setFloat(mTaskViewSwipeUpAnimation
254                 .getCurrentShift(), AnimatedFloat.VALUE, 1, ACCELERATE);
255 
256         if (animateDelayedSuccessFeedback) {
257             anim.addListener(new AnimatorListenerAdapter() {
258                 @Override
259                 public void onAnimationEnd(Animator animator) {
260                     new Handler().postDelayed(
261                             () -> fadeOutFakeTaskView(
262                                     /* toOverviewFirst= */ true,
263                                     /* animatePreviousTask= */ false,
264                                     /* resetViews= */ false,
265                                     /* updateListener= */ v -> mFakeTaskView.setBackgroundColor(
266                                             ColorUtils.blendARGB(
267                                                     getFakeTaskViewStartColor(),
268                                                     getFakeTaskViewEndColor(),
269                                                     v.getAnimatedFraction())),
270                                     /* onEndRunnable= */ () -> {
271                                         showSuccessFeedback();
272                                         resetTaskViews();
273                                     }),
274                             TASK_VIEW_FILL_SCREEN_ANIMATION_DELAY_MILLIS);
275                 }
276             });
277         }
278 
279         ArrayList<Animator> animators = new ArrayList<>();
280 
281         if (mTutorialFragment.isLargeScreen()) {
282             Animator multiRowAnimation = mFakePreviousTaskView.createAnimationToMultiRowLayout();
283 
284             if (multiRowAnimation != null) {
285                 multiRowAnimation.setDuration(TASK_VIEW_END_ANIMATION_DURATION_MILLIS);
286                 animators.add(multiRowAnimation);
287             }
288         }
289         animators.add(anim.buildAnim());
290 
291         AnimatorSet animset = new AnimatorSet();
292         animset.playTogether(animators);
293         animset.start();
294         mRunningWindowAnim = SwipeUpAnimationLogic.RunningWindowAnim.wrap(animset);
295     }
296 }
297