mirror of
https://github.com/LawnchairLauncher/lawnchair.git
synced 2026-02-19 18:58:19 +00:00
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
481 lines
17 KiB
Java
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;
|
|
}
|
|
}
|