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