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.NO_ID;
19 
20 import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL;
21 import static com.android.quickstep.interaction.GestureSandboxActivity.KEY_GESTURE_COMPLETE;
22 import static com.android.quickstep.interaction.GestureSandboxActivity.KEY_TUTORIAL_TYPE;
23 import static com.android.quickstep.interaction.GestureSandboxActivity.KEY_USE_TUTORIAL_MENU;
24 
25 import android.animation.Animator;
26 import android.animation.AnimatorListenerAdapter;
27 import android.app.Activity;
28 import android.content.Context;
29 import android.content.Intent;
30 import android.content.SharedPreferences;
31 import android.graphics.Insets;
32 import android.graphics.drawable.Animatable2;
33 import android.graphics.drawable.AnimatedVectorDrawable;
34 import android.graphics.drawable.Drawable;
35 import android.os.Bundle;
36 import android.util.ArraySet;
37 import android.util.Log;
38 import android.view.LayoutInflater;
39 import android.view.MotionEvent;
40 import android.view.View;
41 import android.view.View.OnTouchListener;
42 import android.view.ViewGroup;
43 import android.view.ViewTreeObserver;
44 import android.view.WindowInsets;
45 import android.widget.ImageView;
46 
47 import androidx.annotation.NonNull;
48 import androidx.annotation.Nullable;
49 
50 import com.android.launcher3.DeviceProfile;
51 import com.android.launcher3.InvariantDeviceProfile;
52 import com.android.launcher3.R;
53 import com.android.launcher3.logging.StatsLogManager;
54 import com.android.quickstep.interaction.TutorialController.TutorialType;
55 
56 import java.util.Set;
57 
58 /** Displays a gesture nav tutorial step. */
59 abstract class TutorialFragment extends GestureSandboxFragment implements OnTouchListener {
60 
61     private static final String LOG_TAG = "TutorialFragment";
62 
63     private static final String TUTORIAL_SKIPPED_PREFERENCE_KEY = "pref_gestureTutorialSkipped";
64     private static final String COMPLETED_TUTORIAL_STEPS_PREFERENCE_KEY =
65             "pref_completedTutorialSteps";
66 
67     private final boolean mFromTutorialMenu;
68 
69     TutorialType mTutorialType;
70     boolean mGestureComplete = false;
71     @Nullable TutorialController mTutorialController = null;
72     RootSandboxLayout mRootView;
73     View mFingerDotView;
74     View mFakePreviousTaskView;
75     EdgeBackGestureHandler mEdgeBackGestureHandler;
76     NavBarGestureHandler mNavBarGestureHandler;
77     private ImageView mEdgeGestureVideoView;
78 
79     @Nullable private Animator mGestureAnimation = null;
80     @Nullable private AnimatedVectorDrawable mEdgeAnimation = null;
81     private boolean mIntroductionShown = false;
82 
83     private boolean mFragmentStopped = false;
84 
85     private DeviceProfile mDeviceProfile;
86     private boolean mIsLargeScreen;
87     private boolean mIsFoldable;
88     private boolean mOnAttachedToWindowPendingCreate;
89 
90     @Nullable private Runnable mOnAttachedOnGlobalLayoutCallback = null;
91 
newInstance( TutorialType tutorialType, boolean gestureComplete, boolean fromTutorialMenu)92     public static TutorialFragment newInstance(
93             TutorialType tutorialType, boolean gestureComplete, boolean fromTutorialMenu) {
94         TutorialFragment fragment = getFragmentForTutorialType(tutorialType, fromTutorialMenu);
95         if (fragment == null) {
96             fragment = new BackGestureTutorialFragment(fromTutorialMenu);
97             tutorialType = TutorialType.BACK_NAVIGATION;
98         }
99 
100         Bundle args = new Bundle();
101         args.putSerializable(KEY_TUTORIAL_TYPE, tutorialType);
102         args.putBoolean(KEY_GESTURE_COMPLETE, gestureComplete);
103         fragment.setArguments(args);
104         return fragment;
105     }
106 
107     @Nullable
108     @Override
recreateFragment()109     GestureSandboxFragment recreateFragment() {
110         TutorialType tutorialType = mTutorialController == null
111                 ? (mTutorialType == null
112                         ? getDefaultTutorialType() : mTutorialType)
113                 : mTutorialController.mTutorialType;
114         return newInstance(tutorialType, isGestureComplete(), mFromTutorialMenu);
115     }
116 
117     @Override
canRecreateFragment()118     boolean canRecreateFragment() {
119         return true;
120     }
121 
122     @NonNull
getDefaultTutorialType()123     abstract TutorialType getDefaultTutorialType();
124 
TutorialFragment(boolean fromTutorialMenu)125     TutorialFragment(boolean fromTutorialMenu) {
126         mFromTutorialMenu = fromTutorialMenu;
127     }
128 
129     @Nullable
getFragmentForTutorialType( TutorialType tutorialType, boolean fromTutorialMenu)130     private static TutorialFragment getFragmentForTutorialType(
131             TutorialType tutorialType, boolean fromTutorialMenu) {
132         switch (tutorialType) {
133             case BACK_NAVIGATION:
134             case BACK_NAVIGATION_COMPLETE:
135                 return new BackGestureTutorialFragment(fromTutorialMenu);
136             case HOME_NAVIGATION:
137             case HOME_NAVIGATION_COMPLETE:
138                 return new HomeGestureTutorialFragment(fromTutorialMenu);
139             case OVERVIEW_NAVIGATION:
140             case OVERVIEW_NAVIGATION_COMPLETE:
141                 return new OverviewGestureTutorialFragment(fromTutorialMenu);
142             default:
143                 Log.e(LOG_TAG, "Failed to find an appropriate fragment for " + tutorialType.name());
144         }
145         return null;
146     }
147 
getEdgeAnimationResId()148     @Nullable Integer getEdgeAnimationResId() {
149         return null;
150     }
151 
152     @Nullable
getGestureAnimation()153     Animator getGestureAnimation() {
154         return mGestureAnimation;
155     }
156 
157     @Nullable
getEdgeAnimation()158     AnimatedVectorDrawable getEdgeAnimation() {
159         return mEdgeAnimation;
160     }
161 
162 
163     @Nullable
createGestureAnimation()164     protected Animator createGestureAnimation() {
165         return null;
166     }
167 
168     @NonNull
createController(TutorialType type)169     abstract TutorialController createController(TutorialType type);
170 
getControllerClass()171     abstract Class<? extends TutorialController> getControllerClass();
172 
173     @Override
onCreate(Bundle savedInstanceState)174     public void onCreate(Bundle savedInstanceState) {
175         super.onCreate(savedInstanceState);
176         Bundle args = savedInstanceState != null ? savedInstanceState : getArguments();
177         mTutorialType = (TutorialType) args.getSerializable(KEY_TUTORIAL_TYPE);
178         mGestureComplete = args.getBoolean(KEY_GESTURE_COMPLETE, false);
179         mEdgeBackGestureHandler = new EdgeBackGestureHandler(getContext());
180         mNavBarGestureHandler = new NavBarGestureHandler(getContext());
181 
182         mDeviceProfile = InvariantDeviceProfile.INSTANCE.get(getContext())
183                 .getDeviceProfile(getContext());
184         mIsLargeScreen = mDeviceProfile.isTablet;
185         mIsFoldable = mDeviceProfile.isTwoPanels;
186 
187         if (mOnAttachedToWindowPendingCreate) {
188             mOnAttachedToWindowPendingCreate = false;
189             onAttachedToWindow();
190         }
191     }
192 
isLargeScreen()193     public boolean isLargeScreen() {
194         return mIsLargeScreen;
195     }
196 
isFoldable()197     public boolean isFoldable() {
198         return mIsFoldable;
199     }
200 
getDeviceProfile()201     DeviceProfile getDeviceProfile() {
202         return mDeviceProfile;
203     }
204 
205     @Override
onDestroy()206     public void onDestroy() {
207         super.onDestroy();
208         mEdgeBackGestureHandler.unregisterBackGestureAttemptCallback();
209         mNavBarGestureHandler.unregisterNavBarGestureAttemptCallback();
210     }
211 
212     @Override
onCreateView( @onNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)213     public View onCreateView(
214             @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
215         super.onCreateView(inflater, container, savedInstanceState);
216 
217         mRootView = (RootSandboxLayout) inflater.inflate(
218                 ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
219                         ? R.layout.redesigned_gesture_tutorial_fragment
220                         : R.layout.gesture_tutorial_fragment,
221                 container,
222                 false);
223 
224         mRootView.setOnApplyWindowInsetsListener((view, insets) -> {
225             Insets systemInsets = insets.getInsets(WindowInsets.Type.systemBars());
226             mEdgeBackGestureHandler.setInsets(systemInsets.left, systemInsets.right);
227             return insets;
228         });
229         mRootView.setOnTouchListener(this);
230         mEdgeGestureVideoView = mRootView.findViewById(R.id.gesture_tutorial_edge_gesture_video);
231         mFingerDotView = mRootView.findViewById(R.id.gesture_tutorial_finger_dot);
232         mFakePreviousTaskView = mRootView.findViewById(
233                 R.id.gesture_tutorial_fake_previous_task_view);
234 
235         return mRootView;
236     }
237 
238     @Override
onStop()239     public void onStop() {
240         super.onStop();
241         releaseFeedbackAnimation();
242         mFragmentStopped = true;
243     }
244 
initializeFeedbackVideoView()245     void initializeFeedbackVideoView() {
246         if (!updateFeedbackAnimation() || mTutorialController == null) {
247             return;
248         }
249 
250         if (isGestureComplete()) {
251             mTutorialController.showSuccessFeedback();
252         } else if (!mIntroductionShown) {
253             int introTitleResId = mTutorialController.getIntroductionTitle();
254             int introSubtitleResId = mTutorialController.getIntroductionSubtitle();
255             if (introTitleResId == NO_ID) {
256                 // Allow crash since this should never be reached with a tutorial controller used in
257                 // production.
258                 Log.e(LOG_TAG,
259                         "Cannot show introduction feedback for tutorial step: " + mTutorialType
260                                 + ", no introduction feedback title",
261                         new IllegalStateException());
262             }
263             if (introTitleResId == NO_ID) {
264                 // Allow crash since this should never be reached with a tutorial controller used in
265                 // production.
266                 Log.e(LOG_TAG,
267                         "Cannot show introduction feedback for tutorial step: " + mTutorialType
268                                 + ", no introduction feedback subtitle",
269                         new IllegalStateException());
270             }
271             mTutorialController.showFeedback(
272                     introTitleResId,
273                     introSubtitleResId,
274                     mTutorialController.getSpokenIntroductionSubtitle(),
275                     false,
276                     true);
277             mIntroductionShown = true;
278         }
279     }
280 
updateFeedbackAnimation()281     boolean updateFeedbackAnimation() {
282         if (!updateEdgeAnimation()) {
283             return false;
284         }
285         mGestureAnimation = createGestureAnimation();
286 
287         if (mGestureAnimation != null) {
288             mGestureAnimation.addListener(new AnimatorListenerAdapter() {
289                 @Override
290                 public void onAnimationStart(Animator animation) {
291                     super.onAnimationStart(animation);
292                     mFingerDotView.setVisibility(View.VISIBLE);
293                 }
294 
295                 @Override
296                 public void onAnimationCancel(Animator animation) {
297                     super.onAnimationCancel(animation);
298                     mFingerDotView.setVisibility(View.GONE);
299                 }
300 
301                 @Override
302                 public void onAnimationEnd(Animator animation) {
303                     super.onAnimationEnd(animation);
304                     mFingerDotView.setVisibility(View.GONE);
305                 }
306             });
307         }
308 
309         return mGestureAnimation != null;
310     }
311 
updateEdgeAnimation()312     boolean updateEdgeAnimation() {
313         Integer edgeAnimationResId = getEdgeAnimationResId();
314         if (edgeAnimationResId == null || getContext() == null) {
315             return false;
316         }
317         mEdgeAnimation = (AnimatedVectorDrawable) getContext().getDrawable(edgeAnimationResId);
318 
319         if (mEdgeAnimation != null) {
320             mEdgeAnimation.registerAnimationCallback(new Animatable2.AnimationCallback() {
321 
322                 @Override
323                 public void onAnimationEnd(Drawable drawable) {
324                     super.onAnimationEnd(drawable);
325 
326                     mEdgeAnimation.start();
327                 }
328             });
329         }
330         mEdgeGestureVideoView.setImageDrawable(mEdgeAnimation);
331 
332         return mEdgeAnimation != null;
333     }
334 
releaseFeedbackAnimation()335     void releaseFeedbackAnimation() {
336         if (mTutorialController != null && !mTutorialController.isGestureCompleted()) {
337             mTutorialController.cancelQueuedGestureAnimation();
338         }
339         if (mGestureAnimation != null && mGestureAnimation.isRunning()) {
340             mGestureAnimation.cancel();
341         }
342         if (mEdgeAnimation != null && mEdgeAnimation.isRunning()) {
343             mEdgeAnimation.stop();
344         }
345         mEdgeGestureVideoView.setVisibility(View.GONE);
346     }
347 
348     @Override
onResume()349     public void onResume() {
350         super.onResume();
351         releaseFeedbackAnimation();
352         if (mFragmentStopped && mTutorialController != null) {
353             mTutorialController.showFeedback();
354             mFragmentStopped = false;
355         } else {
356             mRootView.getViewTreeObserver().addOnGlobalLayoutListener(
357                     new ViewTreeObserver.OnGlobalLayoutListener() {
358                         @Override
359                         public void onGlobalLayout() {
360                             runOnAttached(() -> changeController(mTutorialType));
361                             mRootView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
362                         }
363                     });
364         }
365     }
366 
runOnAttached(Runnable callback)367     private void runOnAttached(Runnable callback) {
368         mOnAttachedOnGlobalLayoutCallback = callback;
369         if (getContext() != null) {
370             onAttached();
371         }
372     }
373 
onAttached()374     private void onAttached() {
375         if (mOnAttachedOnGlobalLayoutCallback != null) {
376             mOnAttachedOnGlobalLayoutCallback.run();
377             mOnAttachedOnGlobalLayoutCallback = null;
378         }
379     }
380 
381     @Override
onTouch(View view, MotionEvent motionEvent)382     public boolean onTouch(View view, MotionEvent motionEvent) {
383         if (mTutorialController != null && !isGestureComplete()) {
384             mTutorialController.hideFeedback();
385         }
386 
387         if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
388             mTutorialController.pauseAndHideLottieAnimation();
389         }
390 
391         // Note: Using logical-or to ensure both functions get called.
392         return mEdgeBackGestureHandler.onTouch(view, motionEvent)
393                 | mNavBarGestureHandler.onTouch(view, motionEvent);
394     }
395 
onInterceptTouch(MotionEvent motionEvent)396     boolean onInterceptTouch(MotionEvent motionEvent) {
397         // Note: Using logical-or to ensure both functions get called.
398         return mEdgeBackGestureHandler.onInterceptTouch(motionEvent)
399                 | mNavBarGestureHandler.onInterceptTouch(motionEvent);
400     }
401 
402     @Override
onAttach(@onNull Context context)403     public void onAttach(@NonNull Context context) {
404         super.onAttach(context);
405         onAttached();
406     }
407 
408     @Override
onAttachedToWindow()409     void onAttachedToWindow() {
410         if (mEdgeBackGestureHandler == null) {
411             mOnAttachedToWindowPendingCreate = true;
412             return;
413         }
414         StatsLogManager statsLogManager = getStatsLogManager();
415         if (statsLogManager != null) {
416             logTutorialStepShown(statsLogManager);
417         }
418         mEdgeBackGestureHandler.setViewGroupParent(getRootView());
419     }
420 
421     @Override
onDetachedFromWindow()422     void onDetachedFromWindow() {
423         mOnAttachedToWindowPendingCreate = false;
424         mEdgeBackGestureHandler.setViewGroupParent(null);
425     }
426 
changeController(TutorialType tutorialType)427     void changeController(TutorialType tutorialType) {
428         if (getControllerClass().isInstance(mTutorialController)) {
429             mTutorialController.setTutorialType(tutorialType);
430             if (isGestureComplete()) {
431                 mTutorialController.setGestureCompleted();
432             }
433             mTutorialController.fadeTaskViewAndRun(mTutorialController::transitToController);
434         } else {
435             mTutorialController = createController(tutorialType);
436             if (isGestureComplete()) {
437                 mTutorialController.setGestureCompleted();
438             }
439             mTutorialController.transitToController();
440         }
441         mEdgeBackGestureHandler.registerBackGestureAttemptCallback(mTutorialController);
442         mNavBarGestureHandler.registerNavBarGestureAttemptCallback(mTutorialController);
443         mTutorialType = tutorialType;
444 
445         initializeFeedbackVideoView();
446     }
447 
448     @Override
onSaveInstanceState(Bundle savedInstanceState)449     public void onSaveInstanceState(Bundle savedInstanceState) {
450         savedInstanceState.putSerializable(KEY_TUTORIAL_TYPE, mTutorialType);
451         savedInstanceState.putBoolean(KEY_GESTURE_COMPLETE, isGestureComplete());
452         savedInstanceState.putBoolean(KEY_USE_TUTORIAL_MENU, mFromTutorialMenu);
453         super.onSaveInstanceState(savedInstanceState);
454     }
455 
getRootView()456     RootSandboxLayout getRootView() {
457         return mRootView;
458     }
459 
continueTutorial()460     void continueTutorial() {
461         SharedPreferences sharedPrefs = getSharedPreferences();
462         if (sharedPrefs != null) {
463             Set<String> updatedCompletedSteps = new ArraySet<>(sharedPrefs.getStringSet(
464                     COMPLETED_TUTORIAL_STEPS_PREFERENCE_KEY, new ArraySet<>()));
465 
466             updatedCompletedSteps.add(mTutorialType.toString());
467 
468             sharedPrefs.edit().putStringSet(
469                     COMPLETED_TUTORIAL_STEPS_PREFERENCE_KEY, updatedCompletedSteps).apply();
470         }
471         StatsLogManager statsLogManager = getStatsLogManager();
472         if (statsLogManager != null) {
473             logTutorialStepCompleted(statsLogManager);
474         }
475 
476         GestureSandboxActivity gestureSandboxActivity = getGestureSandboxActivity();
477         if (gestureSandboxActivity == null) {
478             close();
479             return;
480         }
481         gestureSandboxActivity.continueTutorial();
482     }
483 
484     @Override
close()485     void close() {
486         closeTutorialStep(false);
487     }
488 
closeTutorialStep(boolean tutorialSkipped)489     void closeTutorialStep(boolean tutorialSkipped) {
490         if (tutorialSkipped) {
491             SharedPreferences sharedPrefs = getSharedPreferences();
492             if (sharedPrefs != null) {
493                 sharedPrefs.edit().putBoolean(TUTORIAL_SKIPPED_PREFERENCE_KEY, true).apply();
494             }
495             StatsLogManager statsLogManager = getStatsLogManager();
496             if (statsLogManager != null) {
497                 statsLogManager.logger().log(
498                         StatsLogManager.LauncherEvent.LAUNCHER_GESTURE_TUTORIAL_SKIPPED);
499             }
500         }
501         GestureSandboxActivity gestureSandboxActivity = getGestureSandboxActivity();
502         if (mFromTutorialMenu && gestureSandboxActivity != null) {
503             gestureSandboxActivity.launchTutorialMenu();
504             return;
505         }
506         super.close();
507     }
508 
startSystemNavigationSetting()509     void startSystemNavigationSetting() {
510         startActivity(new Intent("com.android.settings.GESTURE_NAVIGATION_SETTINGS"));
511     }
512 
getCurrentStep()513     int getCurrentStep() {
514         GestureSandboxActivity gestureSandboxActivity = getGestureSandboxActivity();
515 
516         return gestureSandboxActivity == null ? -1 : gestureSandboxActivity.getCurrentStep();
517     }
518 
getNumSteps()519     int getNumSteps() {
520         GestureSandboxActivity gestureSandboxActivity = getGestureSandboxActivity();
521 
522         return gestureSandboxActivity == null ? -1 : gestureSandboxActivity.getNumSteps();
523     }
524 
isAtFinalStep()525     boolean isAtFinalStep() {
526         return getCurrentStep() == getNumSteps();
527     }
528 
isGestureComplete()529     boolean isGestureComplete() {
530         return mGestureComplete
531                 || (mTutorialController != null && mTutorialController.isGestureCompleted());
532     }
533 
logTutorialStepShown(@onNull StatsLogManager statsLogManager)534     abstract void logTutorialStepShown(@NonNull StatsLogManager statsLogManager);
535 
logTutorialStepCompleted(@onNull StatsLogManager statsLogManager)536     abstract void logTutorialStepCompleted(@NonNull StatsLogManager statsLogManager);
537 
538     @Nullable
getGestureSandboxActivity()539     private GestureSandboxActivity getGestureSandboxActivity() {
540         Activity activity = getActivity();
541 
542         return activity instanceof GestureSandboxActivity
543                 ? (GestureSandboxActivity) activity : null;
544     }
545 
546     @Nullable
getStatsLogManager()547     private StatsLogManager getStatsLogManager() {
548         GestureSandboxActivity activity = getGestureSandboxActivity();
549 
550         return activity != null ? activity.getStatsLogManager() : null;
551     }
552 
553     @Nullable
getSharedPreferences()554     private SharedPreferences getSharedPreferences() {
555         GestureSandboxActivity activity = getGestureSandboxActivity();
556 
557         return activity != null ? activity.getSharedPrefs() : null;
558     }
559 }
560