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 android.content.Context;
19 import android.graphics.drawable.RippleDrawable;
20 import android.view.View;
21 import android.view.View.OnClickListener;
22 import android.widget.Button;
23 import android.widget.ImageButton;
24 import android.widget.ImageView;
25 import android.widget.TextView;
26 
27 import androidx.annotation.CallSuper;
28 import androidx.annotation.Nullable;
29 
30 import com.android.launcher3.R;
31 import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureAttemptCallback;
32 import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureAttemptCallback;
33 
34 abstract class TutorialController implements BackGestureAttemptCallback,
35         NavBarGestureAttemptCallback {
36 
37     private static final int FEEDBACK_VISIBLE_MS = 3000;
38     private static final int FEEDBACK_ANIMATION_MS = 500;
39     private static final int RIPPLE_VISIBLE_MS = 300;
40 
41     final TutorialFragment mTutorialFragment;
42     TutorialType mTutorialType;
43     final Context mContext;
44 
45     final ImageButton mCloseButton;
46     final TextView mTitleTextView;
47     final TextView mSubtitleTextView;
48     final TextView mFeedbackView;
49     final View mFakeTaskView;
50     final View mRippleView;
51     final RippleDrawable mRippleDrawable;
52     final TutorialHandAnimation mHandCoachingAnimation;
53     final ImageView mHandCoachingView;
54     final Button mActionTextButton;
55     final Button mActionButton;
56     private final Runnable mHideFeedbackRunnable;
57 
TutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType)58     TutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType) {
59         mTutorialFragment = tutorialFragment;
60         mTutorialType = tutorialType;
61         mContext = mTutorialFragment.getContext();
62 
63         View rootView = tutorialFragment.getRootView();
64         mCloseButton = rootView.findViewById(R.id.gesture_tutorial_fragment_close_button);
65         mCloseButton.setOnClickListener(button -> mTutorialFragment.closeTutorial());
66         mTitleTextView = rootView.findViewById(R.id.gesture_tutorial_fragment_title_view);
67         mSubtitleTextView = rootView.findViewById(R.id.gesture_tutorial_fragment_subtitle_view);
68         mFeedbackView = rootView.findViewById(R.id.gesture_tutorial_fragment_feedback_view);
69         mFakeTaskView = rootView.findViewById(R.id.gesture_tutorial_fake_task_view);
70         mRippleView = rootView.findViewById(R.id.gesture_tutorial_ripple_view);
71         mRippleDrawable = (RippleDrawable) mRippleView.getBackground();
72         mHandCoachingAnimation = tutorialFragment.getHandAnimation();
73         mHandCoachingView = rootView.findViewById(R.id.gesture_tutorial_fragment_hand_coaching);
74         mHandCoachingView.bringToFront();
75         mActionTextButton =
76                 rootView.findViewById(R.id.gesture_tutorial_fragment_action_text_button);
77         mActionButton = rootView.findViewById(R.id.gesture_tutorial_fragment_action_button);
78 
79         mHideFeedbackRunnable =
80                 () -> mFeedbackView.animate().alpha(0).setDuration(FEEDBACK_ANIMATION_MS)
81                         .withEndAction(this::showHandCoachingAnimation).start();
82     }
83 
setTutorialType(TutorialType tutorialType)84     void setTutorialType(TutorialType tutorialType) {
85         mTutorialType = tutorialType;
86     }
87 
88     @Nullable
getTitleStringId()89     Integer getTitleStringId() {
90         return null;
91     }
92 
93     @Nullable
getSubtitleStringId()94     Integer getSubtitleStringId() {
95         return null;
96     }
97 
98     @Nullable
getActionButtonStringId()99     Integer getActionButtonStringId() {
100         return null;
101     }
102 
103     @Nullable
getActionTextButtonStringId()104     Integer getActionTextButtonStringId() {
105         return null;
106     }
107 
showFeedback(int resId)108     void showFeedback(int resId) {
109         hideHandCoachingAnimation();
110         mFeedbackView.setText(resId);
111         mFeedbackView.animate().alpha(1).setDuration(FEEDBACK_ANIMATION_MS).start();
112         mFeedbackView.removeCallbacks(mHideFeedbackRunnable);
113         mFeedbackView.postDelayed(mHideFeedbackRunnable, FEEDBACK_VISIBLE_MS);
114     }
115 
hideFeedback()116     void hideFeedback() {
117         mFeedbackView.setText(null);
118         mFeedbackView.removeCallbacks(mHideFeedbackRunnable);
119         mFeedbackView.clearAnimation();
120         mFeedbackView.setAlpha(0);
121     }
122 
setRippleHotspot(float x, float y)123     void setRippleHotspot(float x, float y) {
124         mRippleDrawable.setHotspot(x, y);
125     }
126 
showRippleEffect(@ullable Runnable onCompleteRunnable)127     void showRippleEffect(@Nullable Runnable onCompleteRunnable) {
128         mRippleDrawable.setState(
129                 new int[] {android.R.attr.state_pressed, android.R.attr.state_enabled});
130         mRippleView.postDelayed(() -> {
131             mRippleDrawable.setState(new int[] {});
132             if (onCompleteRunnable != null) {
133                 onCompleteRunnable.run();
134             }
135         }, RIPPLE_VISIBLE_MS);
136     }
137 
onActionButtonClicked(View button)138     void onActionButtonClicked(View button) {}
139 
onActionTextButtonClicked(View button)140     void onActionTextButtonClicked(View button) {}
141 
showHandCoachingAnimation()142     void showHandCoachingAnimation() {
143         if (isComplete()) {
144             return;
145         }
146         mHandCoachingAnimation.startLoopedAnimation(mTutorialType);
147     }
148 
hideHandCoachingAnimation()149     void hideHandCoachingAnimation() {
150         mHandCoachingAnimation.stop();
151         mHandCoachingView.setVisibility(View.INVISIBLE);
152     }
153 
154     @CallSuper
transitToController()155     void transitToController() {
156         hideFeedback();
157         updateTitles();
158         updateActionButtons();
159 
160         if (isComplete()) {
161             hideHandCoachingAnimation();
162         } else {
163             showHandCoachingAnimation();
164         }
165     }
166 
updateTitles()167     private void updateTitles() {
168         updateTitleView(mTitleTextView, getTitleStringId(),
169                 R.style.TextAppearance_GestureTutorial_Title);
170         updateTitleView(mSubtitleTextView, getSubtitleStringId(),
171                 R.style.TextAppearance_GestureTutorial_Subtitle);
172     }
173 
updateTitleView(TextView textView, @Nullable Integer stringId, int styleId)174     private void updateTitleView(TextView textView, @Nullable Integer stringId, int styleId) {
175         if (stringId == null) {
176             textView.setVisibility(View.GONE);
177             return;
178         }
179 
180         textView.setVisibility(View.VISIBLE);
181         textView.setText(stringId);
182         textView.setTextAppearance(styleId);
183     }
184 
updateActionButtons()185     private void updateActionButtons() {
186         updateButton(mActionButton, getActionButtonStringId(), this::onActionButtonClicked);
187         updateButton(
188                 mActionTextButton, getActionTextButtonStringId(), this::onActionTextButtonClicked);
189     }
190 
updateButton(Button button, @Nullable Integer stringId, OnClickListener listener)191     private void updateButton(Button button, @Nullable Integer stringId, OnClickListener listener) {
192         if (stringId == null) {
193             button.setVisibility(View.INVISIBLE);
194             return;
195         }
196 
197         button.setVisibility(View.VISIBLE);
198         button.setText(stringId);
199         button.setOnClickListener(listener);
200     }
201 
isComplete()202     private boolean isComplete() {
203         return mTutorialType == TutorialType.BACK_NAVIGATION_COMPLETE
204                 || mTutorialType == TutorialType.HOME_NAVIGATION_COMPLETE
205                 || mTutorialType == TutorialType.OVERVIEW_NAVIGATION_COMPLETE
206                 || mTutorialType == TutorialType.ASSISTANT_COMPLETE;
207     }
208 
209     /** Denotes the type of the tutorial. */
210     enum TutorialType {
211         RIGHT_EDGE_BACK_NAVIGATION,
212         LEFT_EDGE_BACK_NAVIGATION,
213         BACK_NAVIGATION_COMPLETE,
214         HOME_NAVIGATION,
215         HOME_NAVIGATION_COMPLETE,
216         OVERVIEW_NAVIGATION,
217         OVERVIEW_NAVIGATION_COMPLETE,
218         ASSISTANT,
219         ASSISTANT_COMPLETE
220     }
221 }
222