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.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL;
19 import static com.android.quickstep.interaction.TutorialController.TutorialType.BACK_NAVIGATION;
20 import static com.android.quickstep.interaction.TutorialController.TutorialType.BACK_NAVIGATION_COMPLETE;
21 
22 import android.annotation.LayoutRes;
23 import android.graphics.PointF;
24 import android.view.View;
25 
26 import com.android.app.animation.Interpolators;
27 import com.android.launcher3.R;
28 import com.android.launcher3.Utilities;
29 import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureResult;
30 import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureResult;
31 import com.android.quickstep.util.LottieAnimationColorUtils;
32 
33 import java.util.Map;
34 
35 /** A {@link TutorialController} for the Back tutorial. */
36 final class BackGestureTutorialController extends TutorialController {
37     private static final float Y_TRANSLATION_SMOOTHENING_FACTOR = .2f;
38     private static final float EXITING_APP_MIN_SIZE_PERCENTAGE = .8f;
39 
BackGestureTutorialController(BackGestureTutorialFragment fragment, TutorialType tutorialType)40     BackGestureTutorialController(BackGestureTutorialFragment fragment, TutorialType tutorialType) {
41         super(fragment, tutorialType);
42         // Set the Lottie animation colors specifically for the Back gesture
43         if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
44             LottieAnimationColorUtils.updateToArgbColors(
45                     mAnimatedGestureDemonstration,
46                     Map.of(".onSurfaceBack", fragment.mRootView.mColorOnSurfaceBack,
47                             ".surfaceBack", fragment.mRootView.mColorSurfaceBack,
48                             ".secondaryBack", fragment.mRootView.mColorSecondaryBack));
49 
50             LottieAnimationColorUtils.updateToArgbColors(
51                     mCheckmarkAnimation,
52                     Map.of(".checkmark",
53                             Utilities.isDarkTheme(mContext)
54                                     ? fragment.mRootView.mColorOnSurfaceBack
55                                     : fragment.mRootView.mColorSecondaryBack,
56                             ".checkmarkBackground", fragment.mRootView.mColorSurfaceBack));
57         }
58     }
59 
60     @Override
getIntroductionTitle()61     public int getIntroductionTitle() {
62         return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
63                 ? R.string.back_gesture_tutorial_title
64                 : R.string.back_gesture_intro_title;
65     }
66 
67     @Override
getIntroductionSubtitle()68     public int getIntroductionSubtitle() {
69         return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
70                 ? R.string.back_gesture_tutorial_subtitle
71                 : R.string.back_gesture_intro_subtitle;
72     }
73 
74     @Override
getSpokenIntroductionSubtitle()75     public int getSpokenIntroductionSubtitle() {
76         return R.string.back_gesture_spoken_intro_subtitle;
77     }
78 
79     @Override
getSuccessFeedbackTitle()80     public int getSuccessFeedbackTitle() {
81         return R.string.gesture_tutorial_nice;
82     }
83 
84     @Override
getSuccessFeedbackSubtitle()85     public int getSuccessFeedbackSubtitle() {
86         return mTutorialFragment.isAtFinalStep()
87                 ? R.string.back_gesture_feedback_complete_without_follow_up
88                 : ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
89                         ? R.string.back_gesture_feedback_complete_with_follow_up
90                         : R.string.back_gesture_feedback_complete_with_overview_follow_up;
91     }
92 
93     @Override
getTitleTextAppearance()94     public int getTitleTextAppearance() {
95         return R.style.TextAppearance_GestureTutorial_MainTitle_Back;
96     }
97 
98     @Override
getSuccessTitleTextAppearance()99     public int getSuccessTitleTextAppearance() {
100         return R.style.TextAppearance_GestureTutorial_MainTitle_Success_Back;
101     }
102 
103     @Override
getDoneButtonTextAppearance()104     public int getDoneButtonTextAppearance() {
105         return R.style.TextAppearance_GestureTutorial_ButtonLabel_Back;
106     }
107 
108     @Override
getDoneButtonColor()109     public int getDoneButtonColor() {
110         return Utilities.isDarkTheme(mContext)
111                 ? mTutorialFragment.mRootView.mColorOnSurfaceBack
112                 : mTutorialFragment.mRootView.mColorSecondaryBack;
113     }
114 
115     @Override
getMockAppTaskLayoutResId()116     protected int getMockAppTaskLayoutResId() {
117         return getMockAppTaskCurrentPageLayoutResId();
118     }
119 
120     @Override
getGestureLottieAnimationId()121     protected int getGestureLottieAnimationId() {
122         return mTutorialFragment.isLargeScreen()
123                 ? mTutorialFragment.isFoldable()
124                     ? R.raw.back_gesture_tutorial_open_foldable_animation
125                     : R.raw.back_gesture_tutorial_tablet_animation
126                 : R.raw.back_gesture_tutorial_animation;
127     }
128 
129     @LayoutRes
getMockAppTaskCurrentPageLayoutResId()130     int getMockAppTaskCurrentPageLayoutResId() {
131         return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
132                 ? R.layout.back_gesture_tutorial_background
133                 : mTutorialFragment.isLargeScreen()
134                         ? R.layout.gesture_tutorial_tablet_mock_conversation
135                         : R.layout.gesture_tutorial_mock_conversation;
136     }
137 
138     @LayoutRes
getMockAppTaskPreviousPageLayoutResId()139     int getMockAppTaskPreviousPageLayoutResId() {
140         return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
141                 ? R.layout.back_gesture_tutorial_background
142                 : mTutorialFragment.isLargeScreen()
143                         ? R.layout.gesture_tutorial_tablet_mock_conversation_list
144                         : R.layout.gesture_tutorial_mock_conversation_list;
145     }
146 
147     @Override
getFakeLauncherColor()148     protected int getFakeLauncherColor() {
149         return mTutorialFragment.mRootView.mColorSurfaceContainer;
150     }
151 
152     @Override
getExitingAppColor()153     protected int getExitingAppColor() {
154         return mTutorialFragment.mRootView.mColorSurfaceBack;
155     }
156 
157     @Override
onBackGestureAttempted(BackGestureResult result)158     public void onBackGestureAttempted(BackGestureResult result) {
159         if (isGestureCompleted()) {
160             return;
161         }
162         switch (mTutorialType) {
163             case BACK_NAVIGATION:
164                 handleBackAttempt(result);
165                 break;
166             case BACK_NAVIGATION_COMPLETE:
167                 if (result == BackGestureResult.BACK_COMPLETED_FROM_LEFT
168                         || result == BackGestureResult.BACK_COMPLETED_FROM_RIGHT) {
169                     mTutorialFragment.close();
170                 }
171                 break;
172         }
173     }
174 
175     @Override
onBackGestureProgress(float diffx, float diffy, boolean isLeftGesture)176     public void onBackGestureProgress(float diffx, float diffy, boolean isLeftGesture) {
177         if (isGestureCompleted()) {
178             return;
179         }
180 
181         float normalizedSwipeProgress = Math.abs(diffx / mScreenWidth);
182         float smoothedExitingAppScale = Utilities.mapBoundToRange(
183                 normalizedSwipeProgress,
184                 /* lowerBound = */ 0f,
185                 /* upperBound = */ 1f,
186                 /* toMin = */ 1f,
187                 /* toMax = */ EXITING_APP_MIN_SIZE_PERCENTAGE,
188                 Interpolators.DECELERATE);
189 
190         // shrink the exiting app as we progress through the back gesture
191         mExitingAppView.setPivotX(isLeftGesture ? mScreenWidth : 0);
192         mExitingAppView.setPivotY(mScreenHeight / 2f);
193         mExitingAppView.setScaleX(smoothedExitingAppScale);
194         mExitingAppView.setScaleY(smoothedExitingAppScale);
195         mExitingAppView.setTranslationY(diffy * Y_TRANSLATION_SMOOTHENING_FACTOR);
196         mExitingAppView.setTranslationX(Utilities.mapBoundToRange(
197                 normalizedSwipeProgress,
198                 /* lowerBound = */ 0f,
199                 /* upperBound = */ 1f,
200                 /* toMin = */ 0,
201                 /* toMax = */ mExitingAppMargin,
202                 Interpolators.DECELERATE)
203                 * (isLeftGesture ? -1 : 1));
204 
205         // round the corners of the exiting app as we progress through the back gesture
206         mExitingAppRadius = (int) Utilities.mapBoundToRange(
207                 normalizedSwipeProgress,
208                 /* lowerBound = */ 0f,
209                 /* upperBound = */ 1f,
210                 /* toMin = */ mExitingAppStartingCornerRadius,
211                 /* toMax = */ mExitingAppEndingCornerRadius,
212                 Interpolators.EMPHASIZED_DECELERATE);
213         mExitingAppView.invalidateOutline();
214     }
215 
handleBackAttempt(BackGestureResult result)216     private void handleBackAttempt(BackGestureResult result) {
217         if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
218             resetViewsForBackGesture();
219         }
220 
221         switch (result) {
222             case BACK_COMPLETED_FROM_LEFT:
223             case BACK_COMPLETED_FROM_RIGHT:
224                 mTutorialFragment.releaseFeedbackAnimation();
225                 if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
226                     mExitingAppView.setVisibility(View.GONE);
227                 }
228                 updateFakeAppTaskViewLayout(getMockAppTaskPreviousPageLayoutResId());
229                 showSuccessFeedback();
230                 break;
231             case BACK_CANCELLED_FROM_LEFT:
232             case BACK_CANCELLED_FROM_RIGHT:
233                 showFeedback(R.string.back_gesture_feedback_cancelled);
234                 break;
235             case BACK_NOT_STARTED_TOO_FAR_FROM_EDGE:
236                 showFeedback(R.string.back_gesture_feedback_swipe_too_far_from_edge);
237                 break;
238             case BACK_NOT_STARTED_IN_NAV_BAR_REGION:
239                 showFeedback(R.string.back_gesture_feedback_swipe_in_nav_bar);
240                 break;
241         }
242     }
243 
244     @Override
onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity)245     public void onNavBarGestureAttempted(NavBarGestureResult result, PointF finalVelocity) {
246         if (isGestureCompleted()) {
247             return;
248         }
249         if (mTutorialType == BACK_NAVIGATION_COMPLETE) {
250             if (result == NavBarGestureResult.HOME_GESTURE_COMPLETED) {
251                 mTutorialFragment.close();
252             }
253         } else if (mTutorialType == BACK_NAVIGATION) {
254             switch (result) {
255                 case HOME_NOT_STARTED_TOO_FAR_FROM_EDGE:
256                 case OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE:
257                 case HOME_OR_OVERVIEW_CANCELLED:
258                     showFeedback(R.string.back_gesture_feedback_swipe_too_far_from_edge);
259                     break;
260                 case HOME_GESTURE_COMPLETED:
261                 case OVERVIEW_GESTURE_COMPLETED:
262                 case HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION:
263                 default:
264                     showFeedback(R.string.back_gesture_feedback_swipe_in_nav_bar);
265 
266             }
267         }
268     }
269 }
270