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