Files
lawnchair/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java
Schneider Victor-tulias 8e0e88bb2f Update home, back and overview gesture tutorial introductions to be clearer for blind users
Added A11Y strings for the home, back and overview gesture tutorial step introductions. These strings give clearer instructions that work with talkback.

Fixes: 202492454
Test: manual
Change-Id: I1eefd6ecf3e3c576a7c1cf6dbf7b824e5be13860
2022-04-29 16:09:11 -07:00

481 lines
17 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 android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.app.Activity;
import android.content.Context;
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 androidx.fragment.app.Fragment;
import androidx.fragment.app.FragmentActivity;
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;
abstract class TutorialFragment extends Fragment implements OnTouchListener {
private static final String LOG_TAG = "TutorialFragment";
static final String KEY_TUTORIAL_TYPE = "tutorial_type";
static final String KEY_GESTURE_COMPLETE = "gesture_complete";
private static final String TUTORIAL_SKIPPED_PREFERENCE_KEY = "pref_gestureTutorialSkipped";
private static final String COMPLETED_TUTORIAL_STEPS_PREFERENCE_KEY =
"pref_completedTutorialSteps";
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) {
TutorialFragment fragment = getFragmentForTutorialType(tutorialType);
if (fragment == null) {
fragment = new BackGestureTutorialFragment();
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;
}
@Nullable
private static TutorialFragment getFragmentForTutorialType(TutorialType tutorialType) {
switch (tutorialType) {
case BACK_NAVIGATION:
case BACK_NAVIGATION_COMPLETE:
return new BackGestureTutorialFragment();
case HOME_NAVIGATION:
case HOME_NAVIGATION_COMPLETE:
return new HomeGestureTutorialFragment();
case OVERVIEW_NAVIGATION:
case OVERVIEW_NAVIGATION_COMPLETE:
return new OverviewGestureTutorialFragment();
case ASSISTANT:
case ASSISTANT_COMPLETE:
return new AssistantGestureTutorialFragment();
case SANDBOX_MODE:
return new SandboxModeTutorialFragment();
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(
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();
}
// 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);
}
void onAttachedToWindow() {
StatsLogManager statsLogManager = getStatsLogManager();
if (statsLogManager != null) {
logTutorialStepShown(statsLogManager);
}
mEdgeBackGestureHandler.setViewGroupParent(getRootView());
}
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);
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) {
closeTutorial();
return;
}
gestureSandboxActivity.continueTutorial();
}
void closeTutorial() {
closeTutorial(false);
}
void closeTutorial(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);
}
}
FragmentActivity activity = getActivity();
if (activity != null) {
activity.setResult(Activity.RESULT_OK);
activity.finish();
}
}
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() {
Context context = getContext();
return context instanceof GestureSandboxActivity ? (GestureSandboxActivity) context : 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;
}
}