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.animation.Animator;
19 import android.animation.AnimatorListenerAdapter;
20 import android.app.Activity;
21 import android.content.Context;
22 import android.content.Intent;
23 import android.content.pm.ActivityInfo;
24 import android.graphics.Insets;
25 import android.graphics.drawable.Animatable2;
26 import android.graphics.drawable.AnimatedVectorDrawable;
27 import android.graphics.drawable.Drawable;
28 import android.os.Bundle;
29 import android.util.Log;
30 import android.view.LayoutInflater;
31 import android.view.MotionEvent;
32 import android.view.View;
33 import android.view.View.OnTouchListener;
34 import android.view.ViewGroup;
35 import android.view.ViewTreeObserver;
36 import android.view.WindowInsets;
37 import android.widget.ImageView;
38 
39 import androidx.annotation.NonNull;
40 import androidx.annotation.Nullable;
41 import androidx.fragment.app.Fragment;
42 import androidx.fragment.app.FragmentActivity;
43 
44 import com.android.launcher3.InvariantDeviceProfile;
45 import com.android.launcher3.R;
46 import com.android.quickstep.interaction.TutorialController.TutorialType;
47 
48 abstract class TutorialFragment extends Fragment implements OnTouchListener {
49 
50     private static final String LOG_TAG = "TutorialFragment";
51     static final String KEY_TUTORIAL_TYPE = "tutorial_type";
52     static final String KEY_GESTURE_COMPLETE = "gesture_complete";
53 
54     TutorialType mTutorialType;
55     boolean mGestureComplete = false;
56     @Nullable TutorialController mTutorialController = null;
57     RootSandboxLayout mRootView;
58     View mFingerDotView;
59     View mFakePreviousTaskView;
60     EdgeBackGestureHandler mEdgeBackGestureHandler;
61     NavBarGestureHandler mNavBarGestureHandler;
62     private ImageView mEdgeGestureVideoView;
63 
64     @Nullable private Animator mGestureAnimation = null;
65     @Nullable private AnimatedVectorDrawable mEdgeAnimation = null;
66     private boolean mIntroductionShown = false;
67 
68     private boolean mFragmentStopped = false;
69 
70     private boolean mIsLargeScreen;
71 
newInstance(TutorialType tutorialType, boolean gestureComplete)72     public static TutorialFragment newInstance(TutorialType tutorialType, boolean gestureComplete) {
73         TutorialFragment fragment = getFragmentForTutorialType(tutorialType);
74         if (fragment == null) {
75             fragment = new BackGestureTutorialFragment();
76             tutorialType = TutorialType.BACK_NAVIGATION;
77         }
78 
79         Bundle args = new Bundle();
80         args.putSerializable(KEY_TUTORIAL_TYPE, tutorialType);
81         args.putBoolean(KEY_GESTURE_COMPLETE, gestureComplete);
82         fragment.setArguments(args);
83         return fragment;
84     }
85 
86     @Nullable
getFragmentForTutorialType(TutorialType tutorialType)87     private static TutorialFragment getFragmentForTutorialType(TutorialType tutorialType) {
88         switch (tutorialType) {
89             case BACK_NAVIGATION:
90             case BACK_NAVIGATION_COMPLETE:
91                 return new BackGestureTutorialFragment();
92             case HOME_NAVIGATION:
93             case HOME_NAVIGATION_COMPLETE:
94                 return new HomeGestureTutorialFragment();
95             case OVERVIEW_NAVIGATION:
96             case OVERVIEW_NAVIGATION_COMPLETE:
97                 return new OverviewGestureTutorialFragment();
98             case ASSISTANT:
99             case ASSISTANT_COMPLETE:
100                 return new AssistantGestureTutorialFragment();
101             case SANDBOX_MODE:
102                 return new SandboxModeTutorialFragment();
103             default:
104                 Log.e(LOG_TAG, "Failed to find an appropriate fragment for " + tutorialType.name());
105         }
106         return null;
107     }
108 
getEdgeAnimationResId()109     @Nullable Integer getEdgeAnimationResId() {
110         return null;
111     }
112 
113     @Nullable
getGestureAnimation()114     Animator getGestureAnimation() {
115         return mGestureAnimation;
116     }
117 
118     @Nullable
getEdgeAnimation()119     AnimatedVectorDrawable getEdgeAnimation() {
120         return mEdgeAnimation;
121     }
122 
123 
124     @Nullable
createGestureAnimation()125     protected Animator createGestureAnimation() {
126         return null;
127     }
128 
createController(TutorialType type)129     abstract TutorialController createController(TutorialType type);
130 
getControllerClass()131     abstract Class<? extends TutorialController> getControllerClass();
132 
133     @Override
onCreate(Bundle savedInstanceState)134     public void onCreate(Bundle savedInstanceState) {
135         super.onCreate(savedInstanceState);
136         Bundle args = savedInstanceState != null ? savedInstanceState : getArguments();
137         mTutorialType = (TutorialType) args.getSerializable(KEY_TUTORIAL_TYPE);
138         mGestureComplete = args.getBoolean(KEY_GESTURE_COMPLETE, false);
139         mEdgeBackGestureHandler = new EdgeBackGestureHandler(getContext());
140         mNavBarGestureHandler = new NavBarGestureHandler(getContext());
141 
142         mIsLargeScreen = InvariantDeviceProfile.INSTANCE.get(getContext())
143                 .getDeviceProfile(getContext()).isTablet;
144 
145         if (mIsLargeScreen) {
146             ((Activity) getContext()).setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_USER);
147         } else {
148             // Temporary until UI mocks for landscape mode for phones are created.
149             ((Activity) getContext()).setRequestedOrientation(
150                     ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
151         }
152     }
153 
isLargeScreen()154     public boolean isLargeScreen() {
155         return mIsLargeScreen;
156     }
157 
158     @Override
onDestroy()159     public void onDestroy() {
160         super.onDestroy();
161         mEdgeBackGestureHandler.unregisterBackGestureAttemptCallback();
162         mNavBarGestureHandler.unregisterNavBarGestureAttemptCallback();
163     }
164 
165     @Override
onCreateView( @onNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)166     public View onCreateView(
167             @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
168         super.onCreateView(inflater, container, savedInstanceState);
169 
170         mRootView = (RootSandboxLayout) inflater.inflate(
171                 R.layout.gesture_tutorial_fragment, container, false);
172         mRootView.setOnApplyWindowInsetsListener((view, insets) -> {
173             Insets systemInsets = insets.getInsets(WindowInsets.Type.systemBars());
174             mEdgeBackGestureHandler.setInsets(systemInsets.left, systemInsets.right);
175             return insets;
176         });
177         mRootView.setOnTouchListener(this);
178         mEdgeGestureVideoView = mRootView.findViewById(R.id.gesture_tutorial_edge_gesture_video);
179         mFingerDotView = mRootView.findViewById(R.id.gesture_tutorial_finger_dot);
180         mFakePreviousTaskView = mRootView.findViewById(
181                 R.id.gesture_tutorial_fake_previous_task_view);
182         return mRootView;
183     }
184 
185     @Override
onStop()186     public void onStop() {
187         super.onStop();
188         releaseFeedbackAnimation();
189         mFragmentStopped = true;
190     }
191 
initializeFeedbackVideoView()192     void initializeFeedbackVideoView() {
193         if (!updateFeedbackAnimation() || mTutorialController == null) {
194             return;
195         }
196 
197         if (isGestureComplete()) {
198             mTutorialController.showSuccessFeedback();
199         } else if (!mIntroductionShown) {
200             Integer introTileStringResId = mTutorialController.getIntroductionTitle();
201             Integer introSubtitleResId = mTutorialController.getIntroductionSubtitle();
202             if (introTileStringResId != null && introSubtitleResId != null) {
203                 mTutorialController.showFeedback(
204                         introTileStringResId, introSubtitleResId, false, true);
205                 mIntroductionShown = true;
206             }
207         }
208     }
209 
updateFeedbackAnimation()210     boolean updateFeedbackAnimation() {
211         if (!updateEdgeAnimation()) {
212             return false;
213         }
214         mGestureAnimation = createGestureAnimation();
215 
216         if (mGestureAnimation != null) {
217             mGestureAnimation.addListener(new AnimatorListenerAdapter() {
218                 @Override
219                 public void onAnimationStart(Animator animation) {
220                     super.onAnimationStart(animation);
221                     mFingerDotView.setVisibility(View.VISIBLE);
222                 }
223 
224                 @Override
225                 public void onAnimationCancel(Animator animation) {
226                     super.onAnimationCancel(animation);
227                     mFingerDotView.setVisibility(View.GONE);
228                 }
229 
230                 @Override
231                 public void onAnimationEnd(Animator animation) {
232                     super.onAnimationEnd(animation);
233                     mFingerDotView.setVisibility(View.GONE);
234                 }
235             });
236         }
237 
238         return mGestureAnimation != null;
239     }
240 
updateEdgeAnimation()241     boolean updateEdgeAnimation() {
242         Integer edgeAnimationResId = getEdgeAnimationResId();
243         if (edgeAnimationResId == null || getContext() == null) {
244             return false;
245         }
246         mEdgeAnimation = (AnimatedVectorDrawable) getContext().getDrawable(edgeAnimationResId);
247 
248         if (mEdgeAnimation != null) {
249             mEdgeAnimation.registerAnimationCallback(new Animatable2.AnimationCallback() {
250 
251                 @Override
252                 public void onAnimationEnd(Drawable drawable) {
253                     super.onAnimationEnd(drawable);
254 
255                     mEdgeAnimation.start();
256                 }
257             });
258         }
259         mEdgeGestureVideoView.setImageDrawable(mEdgeAnimation);
260 
261         return mEdgeAnimation != null;
262     }
263 
releaseFeedbackAnimation()264     void releaseFeedbackAnimation() {
265         if (mTutorialController != null && !mTutorialController.isGestureCompleted()) {
266             mTutorialController.cancelQueuedGestureAnimation();
267         }
268         if (mGestureAnimation != null && mGestureAnimation.isRunning()) {
269             mGestureAnimation.cancel();
270         }
271         if (mEdgeAnimation != null && mEdgeAnimation.isRunning()) {
272             mEdgeAnimation.stop();
273         }
274 
275         mEdgeGestureVideoView.setVisibility(View.GONE);
276     }
277 
278     @Override
onResume()279     public void onResume() {
280         super.onResume();
281         releaseFeedbackAnimation();
282         if (mFragmentStopped && mTutorialController != null) {
283             mTutorialController.showFeedback();
284             mFragmentStopped = false;
285         } else {
286             mRootView.getViewTreeObserver().addOnGlobalLayoutListener(
287                     new ViewTreeObserver.OnGlobalLayoutListener() {
288                         @Override
289                         public void onGlobalLayout() {
290                             changeController(mTutorialType);
291                             mRootView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
292                         }
293                     });
294         }
295     }
296 
297     @Override
onTouch(View view, MotionEvent motionEvent)298     public boolean onTouch(View view, MotionEvent motionEvent) {
299         // Note: Using logical-or to ensure both functions get called.
300         return mEdgeBackGestureHandler.onTouch(view, motionEvent)
301                 | mNavBarGestureHandler.onTouch(view, motionEvent);
302     }
303 
onInterceptTouch(MotionEvent motionEvent)304     boolean onInterceptTouch(MotionEvent motionEvent) {
305         // Note: Using logical-or to ensure both functions get called.
306         return mEdgeBackGestureHandler.onInterceptTouch(motionEvent)
307                 | mNavBarGestureHandler.onInterceptTouch(motionEvent);
308     }
309 
onAttachedToWindow()310     void onAttachedToWindow() {
311         mEdgeBackGestureHandler.setViewGroupParent(getRootView());
312     }
313 
onDetachedFromWindow()314     void onDetachedFromWindow() {
315         mEdgeBackGestureHandler.setViewGroupParent(null);
316     }
317 
changeController(TutorialType tutorialType)318     void changeController(TutorialType tutorialType) {
319         if (getControllerClass().isInstance(mTutorialController)) {
320             mTutorialController.setTutorialType(tutorialType);
321             mTutorialController.fadeTaskViewAndRun(mTutorialController::transitToController);
322         } else {
323             mTutorialController = createController(tutorialType);
324             mTutorialController.transitToController();
325         }
326         mEdgeBackGestureHandler.registerBackGestureAttemptCallback(mTutorialController);
327         mNavBarGestureHandler.registerNavBarGestureAttemptCallback(mTutorialController);
328         mTutorialType = tutorialType;
329 
330         initializeFeedbackVideoView();
331     }
332 
333     @Override
onSaveInstanceState(Bundle savedInstanceState)334     public void onSaveInstanceState(Bundle savedInstanceState) {
335         savedInstanceState.putSerializable(KEY_TUTORIAL_TYPE, mTutorialType);
336         super.onSaveInstanceState(savedInstanceState);
337     }
338 
getRootView()339     RootSandboxLayout getRootView() {
340         return mRootView;
341     }
342 
continueTutorial()343     void continueTutorial() {
344         GestureSandboxActivity gestureSandboxActivity = getGestureSandboxActivity();
345 
346         if (gestureSandboxActivity == null) {
347             closeTutorial();
348             return;
349         }
350         gestureSandboxActivity.continueTutorial();
351     }
352 
closeTutorial()353     void closeTutorial() {
354         FragmentActivity activity = getActivity();
355         if (activity != null) {
356             activity.setResult(Activity.RESULT_OK);
357             activity.finish();
358         }
359     }
360 
startSystemNavigationSetting()361     void startSystemNavigationSetting() {
362         startActivity(new Intent("com.android.settings.GESTURE_NAVIGATION_SETTINGS"));
363     }
364 
getCurrentStep()365     int getCurrentStep() {
366         GestureSandboxActivity gestureSandboxActivity = getGestureSandboxActivity();
367 
368         return gestureSandboxActivity == null ? -1 : gestureSandboxActivity.getCurrentStep();
369     }
370 
getNumSteps()371     int getNumSteps() {
372         GestureSandboxActivity gestureSandboxActivity = getGestureSandboxActivity();
373 
374         return gestureSandboxActivity == null ? -1 : gestureSandboxActivity.getNumSteps();
375     }
376 
isAtFinalStep()377     boolean isAtFinalStep() {
378         return getCurrentStep() == getNumSteps();
379     }
380 
isGestureComplete()381     boolean isGestureComplete() {
382         return mGestureComplete
383                 || (mTutorialController != null && mTutorialController.isGestureCompleted());
384     }
385 
386     @Nullable
getGestureSandboxActivity()387     private GestureSandboxActivity getGestureSandboxActivity() {
388         Context context = getContext();
389 
390         return context instanceof GestureSandboxActivity ? (GestureSandboxActivity) context : null;
391     }
392 }
393