Files
Lawnchair/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
T
Schneider Victor-tulias 2851be8cba Add the gesture nav tutorial menu page
- Added the gesture tutorial menu page (launched using an intent extra)
- tutorial steps now launch the menu when complete if launched from the menu
- the new default set of tutorial steps is home -> back -> overview. this is to handle the case where an intent is launched that is meant to launch the tutorial menu, but ENABLE_NEW_GESTURE_NAV_TUTORIAL is false

Flag: ENABLE_NEW_GESTURE_NAV_TUTORIAL
Bug: 274463555
Test: Ran the tutorial and menu on a large screen, foldable and phone
Change-Id: I2eb5f658115be4d5ecb0233a8f09d22efe6ebadc
2023-03-27 14:36:01 -07:00

506 lines
18 KiB
Java

/*
* Copyright (C) 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.android.quickstep.interaction;
import static android.view.View.NO_ID;
import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL;
import static com.android.quickstep.interaction.GestureSandboxActivity.KEY_GESTURE_COMPLETE;
import static com.android.quickstep.interaction.GestureSandboxActivity.KEY_TUTORIAL_TYPE;
import static com.android.quickstep.interaction.GestureSandboxActivity.KEY_USE_TUTORIAL_MENU;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.app.Activity;
import android.content.Intent;
import android.content.SharedPreferences;
import android.graphics.Insets;
import android.graphics.drawable.Animatable2;
import android.graphics.drawable.AnimatedVectorDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.util.ArraySet;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.View.OnTouchListener;
import android.view.ViewGroup;
import android.view.ViewTreeObserver;
import android.view.WindowInsets;
import android.widget.ImageView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.launcher3.DeviceProfile;
import com.android.launcher3.InvariantDeviceProfile;
import com.android.launcher3.R;
import com.android.launcher3.logging.StatsLogManager;
import com.android.quickstep.interaction.TutorialController.TutorialType;
import java.util.Set;
/** Displays a gesture nav tutorial step. */
abstract class TutorialFragment extends GestureSandboxFragment implements OnTouchListener {
private static final String LOG_TAG = "TutorialFragment";
private static final String TUTORIAL_SKIPPED_PREFERENCE_KEY = "pref_gestureTutorialSkipped";
private static final String COMPLETED_TUTORIAL_STEPS_PREFERENCE_KEY =
"pref_completedTutorialSteps";
private final boolean mFromTutorialMenu;
TutorialType mTutorialType;
boolean mGestureComplete = false;
@Nullable TutorialController mTutorialController = null;
RootSandboxLayout mRootView;
View mFingerDotView;
View mFakePreviousTaskView;
EdgeBackGestureHandler mEdgeBackGestureHandler;
NavBarGestureHandler mNavBarGestureHandler;
private ImageView mEdgeGestureVideoView;
@Nullable private Animator mGestureAnimation = null;
@Nullable private AnimatedVectorDrawable mEdgeAnimation = null;
private boolean mIntroductionShown = false;
private boolean mFragmentStopped = false;
private DeviceProfile mDeviceProfile;
private boolean mIsLargeScreen;
private boolean mIsFoldable;
public static TutorialFragment newInstance(
TutorialType tutorialType, boolean gestureComplete, boolean fromTutorialMenu) {
TutorialFragment fragment = getFragmentForTutorialType(tutorialType, fromTutorialMenu);
if (fragment == null) {
fragment = new BackGestureTutorialFragment(fromTutorialMenu);
tutorialType = TutorialType.BACK_NAVIGATION;
}
Bundle args = new Bundle();
args.putSerializable(KEY_TUTORIAL_TYPE, tutorialType);
args.putBoolean(KEY_GESTURE_COMPLETE, gestureComplete);
fragment.setArguments(args);
return fragment;
}
TutorialFragment(boolean fromTutorialMenu) {
mFromTutorialMenu = fromTutorialMenu;
}
@Nullable
private static TutorialFragment getFragmentForTutorialType(
TutorialType tutorialType, boolean fromTutorialMenu) {
switch (tutorialType) {
case BACK_NAVIGATION:
case BACK_NAVIGATION_COMPLETE:
return new BackGestureTutorialFragment(fromTutorialMenu);
case HOME_NAVIGATION:
case HOME_NAVIGATION_COMPLETE:
return new HomeGestureTutorialFragment(fromTutorialMenu);
case OVERVIEW_NAVIGATION:
case OVERVIEW_NAVIGATION_COMPLETE:
return new OverviewGestureTutorialFragment(fromTutorialMenu);
case ASSISTANT:
case ASSISTANT_COMPLETE:
return new AssistantGestureTutorialFragment(fromTutorialMenu);
case SANDBOX_MODE:
return new SandboxModeTutorialFragment(fromTutorialMenu);
default:
Log.e(LOG_TAG, "Failed to find an appropriate fragment for " + tutorialType.name());
}
return null;
}
@Nullable Integer getEdgeAnimationResId() {
return null;
}
@Nullable
Animator getGestureAnimation() {
return mGestureAnimation;
}
@Nullable
AnimatedVectorDrawable getEdgeAnimation() {
return mEdgeAnimation;
}
@Nullable
protected Animator createGestureAnimation() {
return null;
}
abstract TutorialController createController(TutorialType type);
abstract Class<? extends TutorialController> getControllerClass();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Bundle args = savedInstanceState != null ? savedInstanceState : getArguments();
mTutorialType = (TutorialType) args.getSerializable(KEY_TUTORIAL_TYPE);
mGestureComplete = args.getBoolean(KEY_GESTURE_COMPLETE, false);
mEdgeBackGestureHandler = new EdgeBackGestureHandler(getContext());
mNavBarGestureHandler = new NavBarGestureHandler(getContext());
mDeviceProfile = InvariantDeviceProfile.INSTANCE.get(getContext())
.getDeviceProfile(getContext());
mIsLargeScreen = mDeviceProfile.isTablet;
mIsFoldable = mDeviceProfile.isTwoPanels;
}
public boolean isLargeScreen() {
return mIsLargeScreen;
}
public boolean isFoldable() {
return mIsFoldable;
}
DeviceProfile getDeviceProfile() {
return mDeviceProfile;
}
@Override
public void onDestroy() {
super.onDestroy();
mEdgeBackGestureHandler.unregisterBackGestureAttemptCallback();
mNavBarGestureHandler.unregisterNavBarGestureAttemptCallback();
}
@Override
public View onCreateView(
@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
super.onCreateView(inflater, container, savedInstanceState);
mRootView = (RootSandboxLayout) inflater.inflate(
ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()
? R.layout.redesigned_gesture_tutorial_fragment
: R.layout.gesture_tutorial_fragment,
container,
false);
mRootView.setOnApplyWindowInsetsListener((view, insets) -> {
Insets systemInsets = insets.getInsets(WindowInsets.Type.systemBars());
mEdgeBackGestureHandler.setInsets(systemInsets.left, systemInsets.right);
return insets;
});
mRootView.setOnTouchListener(this);
mEdgeGestureVideoView = mRootView.findViewById(R.id.gesture_tutorial_edge_gesture_video);
mFingerDotView = mRootView.findViewById(R.id.gesture_tutorial_finger_dot);
mFakePreviousTaskView = mRootView.findViewById(
R.id.gesture_tutorial_fake_previous_task_view);
return mRootView;
}
@Override
public void onStop() {
super.onStop();
releaseFeedbackAnimation();
mFragmentStopped = true;
}
void initializeFeedbackVideoView() {
if (!updateFeedbackAnimation() || mTutorialController == null) {
return;
}
if (isGestureComplete()) {
mTutorialController.showSuccessFeedback();
} else if (!mIntroductionShown) {
int introTitleResId = mTutorialController.getIntroductionTitle();
int introSubtitleResId = mTutorialController.getIntroductionSubtitle();
if (introTitleResId == NO_ID) {
// Allow crash since this should never be reached with a tutorial controller used in
// production.
Log.e(LOG_TAG,
"Cannot show introduction feedback for tutorial step: " + mTutorialType
+ ", no introduction feedback title",
new IllegalStateException());
}
if (introTitleResId == NO_ID) {
// Allow crash since this should never be reached with a tutorial controller used in
// production.
Log.e(LOG_TAG,
"Cannot show introduction feedback for tutorial step: " + mTutorialType
+ ", no introduction feedback subtitle",
new IllegalStateException());
}
mTutorialController.showFeedback(
introTitleResId,
introSubtitleResId,
mTutorialController.getSpokenIntroductionSubtitle(),
false,
true);
mIntroductionShown = true;
}
}
boolean updateFeedbackAnimation() {
if (!updateEdgeAnimation()) {
return false;
}
mGestureAnimation = createGestureAnimation();
if (mGestureAnimation != null) {
mGestureAnimation.addListener(new AnimatorListenerAdapter() {
@Override
public void onAnimationStart(Animator animation) {
super.onAnimationStart(animation);
mFingerDotView.setVisibility(View.VISIBLE);
}
@Override
public void onAnimationCancel(Animator animation) {
super.onAnimationCancel(animation);
mFingerDotView.setVisibility(View.GONE);
}
@Override
public void onAnimationEnd(Animator animation) {
super.onAnimationEnd(animation);
mFingerDotView.setVisibility(View.GONE);
}
});
}
return mGestureAnimation != null;
}
boolean updateEdgeAnimation() {
Integer edgeAnimationResId = getEdgeAnimationResId();
if (edgeAnimationResId == null || getContext() == null) {
return false;
}
mEdgeAnimation = (AnimatedVectorDrawable) getContext().getDrawable(edgeAnimationResId);
if (mEdgeAnimation != null) {
mEdgeAnimation.registerAnimationCallback(new Animatable2.AnimationCallback() {
@Override
public void onAnimationEnd(Drawable drawable) {
super.onAnimationEnd(drawable);
mEdgeAnimation.start();
}
});
}
mEdgeGestureVideoView.setImageDrawable(mEdgeAnimation);
return mEdgeAnimation != null;
}
void releaseFeedbackAnimation() {
if (mTutorialController != null && !mTutorialController.isGestureCompleted()) {
mTutorialController.cancelQueuedGestureAnimation();
}
if (mGestureAnimation != null && mGestureAnimation.isRunning()) {
mGestureAnimation.cancel();
}
if (mEdgeAnimation != null && mEdgeAnimation.isRunning()) {
mEdgeAnimation.stop();
}
mEdgeGestureVideoView.setVisibility(View.GONE);
}
@Override
public void onResume() {
super.onResume();
releaseFeedbackAnimation();
if (mFragmentStopped && mTutorialController != null) {
mTutorialController.showFeedback();
mFragmentStopped = false;
} else {
mRootView.getViewTreeObserver().addOnGlobalLayoutListener(
new ViewTreeObserver.OnGlobalLayoutListener() {
@Override
public void onGlobalLayout() {
changeController(mTutorialType);
mRootView.getViewTreeObserver().removeOnGlobalLayoutListener(this);
}
});
}
}
@Override
public boolean onTouch(View view, MotionEvent motionEvent) {
if (mTutorialController != null && !isGestureComplete()) {
mTutorialController.hideFeedback();
}
if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) {
mTutorialController.pauseAndHideLottieAnimation();
}
// Note: Using logical-or to ensure both functions get called.
return mEdgeBackGestureHandler.onTouch(view, motionEvent)
| mNavBarGestureHandler.onTouch(view, motionEvent);
}
boolean onInterceptTouch(MotionEvent motionEvent) {
// Note: Using logical-or to ensure both functions get called.
return mEdgeBackGestureHandler.onInterceptTouch(motionEvent)
| mNavBarGestureHandler.onInterceptTouch(motionEvent);
}
@Override
void onAttachedToWindow() {
StatsLogManager statsLogManager = getStatsLogManager();
if (statsLogManager != null) {
logTutorialStepShown(statsLogManager);
}
mEdgeBackGestureHandler.setViewGroupParent(getRootView());
}
@Override
void onDetachedFromWindow() {
mEdgeBackGestureHandler.setViewGroupParent(null);
}
void changeController(TutorialType tutorialType) {
if (getControllerClass().isInstance(mTutorialController)) {
mTutorialController.setTutorialType(tutorialType);
mTutorialController.fadeTaskViewAndRun(mTutorialController::transitToController);
} else {
mTutorialController = createController(tutorialType);
mTutorialController.transitToController();
}
mEdgeBackGestureHandler.registerBackGestureAttemptCallback(mTutorialController);
mNavBarGestureHandler.registerNavBarGestureAttemptCallback(mTutorialController);
mTutorialType = tutorialType;
initializeFeedbackVideoView();
}
@Override
public void onSaveInstanceState(Bundle savedInstanceState) {
savedInstanceState.putSerializable(KEY_TUTORIAL_TYPE, mTutorialType);
savedInstanceState.putBoolean(KEY_GESTURE_COMPLETE, isGestureComplete());
savedInstanceState.putBoolean(KEY_USE_TUTORIAL_MENU, mFromTutorialMenu);
super.onSaveInstanceState(savedInstanceState);
}
RootSandboxLayout getRootView() {
return mRootView;
}
void continueTutorial() {
SharedPreferences sharedPrefs = getSharedPreferences();
if (sharedPrefs != null) {
Set<String> updatedCompletedSteps = new ArraySet<>(sharedPrefs.getStringSet(
COMPLETED_TUTORIAL_STEPS_PREFERENCE_KEY, new ArraySet<>()));
updatedCompletedSteps.add(mTutorialType.toString());
sharedPrefs.edit().putStringSet(
COMPLETED_TUTORIAL_STEPS_PREFERENCE_KEY, updatedCompletedSteps).apply();
}
StatsLogManager statsLogManager = getStatsLogManager();
if (statsLogManager != null) {
logTutorialStepCompleted(statsLogManager);
}
GestureSandboxActivity gestureSandboxActivity = getGestureSandboxActivity();
if (gestureSandboxActivity == null) {
close();
return;
}
gestureSandboxActivity.continueTutorial();
}
@Override
void close() {
closeTutorialStep(false);
}
void closeTutorialStep(boolean tutorialSkipped) {
if (tutorialSkipped) {
SharedPreferences sharedPrefs = getSharedPreferences();
if (sharedPrefs != null) {
sharedPrefs.edit().putBoolean(TUTORIAL_SKIPPED_PREFERENCE_KEY, true).apply();
}
StatsLogManager statsLogManager = getStatsLogManager();
if (statsLogManager != null) {
statsLogManager.logger().log(
StatsLogManager.LauncherEvent.LAUNCHER_GESTURE_TUTORIAL_SKIPPED);
}
}
GestureSandboxActivity gestureSandboxActivity = getGestureSandboxActivity();
if (mFromTutorialMenu && gestureSandboxActivity != null) {
gestureSandboxActivity.launchTutorialMenu();
return;
}
super.close();
}
void startSystemNavigationSetting() {
startActivity(new Intent("com.android.settings.GESTURE_NAVIGATION_SETTINGS"));
}
int getCurrentStep() {
GestureSandboxActivity gestureSandboxActivity = getGestureSandboxActivity();
return gestureSandboxActivity == null ? -1 : gestureSandboxActivity.getCurrentStep();
}
int getNumSteps() {
GestureSandboxActivity gestureSandboxActivity = getGestureSandboxActivity();
return gestureSandboxActivity == null ? -1 : gestureSandboxActivity.getNumSteps();
}
boolean isAtFinalStep() {
return getCurrentStep() == getNumSteps();
}
boolean isGestureComplete() {
return mGestureComplete
|| (mTutorialController != null && mTutorialController.isGestureCompleted());
}
abstract void logTutorialStepShown(@NonNull StatsLogManager statsLogManager);
abstract void logTutorialStepCompleted(@NonNull StatsLogManager statsLogManager);
@Nullable
private GestureSandboxActivity getGestureSandboxActivity() {
Activity activity = getActivity();
return activity instanceof GestureSandboxActivity
? (GestureSandboxActivity) activity : null;
}
@Nullable
private StatsLogManager getStatsLogManager() {
GestureSandboxActivity activity = getGestureSandboxActivity();
return activity != null ? activity.getStatsLogManager() : null;
}
@Nullable
private SharedPreferences getSharedPreferences() {
GestureSandboxActivity activity = getGestureSandboxActivity();
return activity != null ? activity.getSharedPrefs() : null;
}
}