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 android.app.Activity; 19 import android.content.Context; 20 import android.content.Intent; 21 import android.graphics.Insets; 22 import android.graphics.drawable.Animatable2; 23 import android.graphics.drawable.AnimatedVectorDrawable; 24 import android.graphics.drawable.Drawable; 25 import android.os.Bundle; 26 import android.util.Log; 27 import android.view.LayoutInflater; 28 import android.view.MotionEvent; 29 import android.view.View; 30 import android.view.View.OnTouchListener; 31 import android.view.ViewGroup; 32 import android.view.WindowInsets; 33 import android.widget.ImageView; 34 35 import androidx.annotation.NonNull; 36 import androidx.annotation.Nullable; 37 import androidx.fragment.app.Fragment; 38 import androidx.fragment.app.FragmentActivity; 39 40 import com.android.launcher3.R; 41 import com.android.launcher3.Utilities; 42 import com.android.quickstep.interaction.TutorialController.TutorialType; 43 44 abstract class TutorialFragment extends Fragment implements OnTouchListener { 45 46 private static final String LOG_TAG = "TutorialFragment"; 47 static final String KEY_TUTORIAL_TYPE = "tutorial_type"; 48 49 TutorialType mTutorialType; 50 @Nullable TutorialController mTutorialController = null; 51 RootSandboxLayout mRootView; 52 EdgeBackGestureHandler mEdgeBackGestureHandler; 53 NavBarGestureHandler mNavBarGestureHandler; 54 private ImageView mFeedbackVideoView; 55 private ImageView mGestureVideoView; 56 57 @Nullable private AnimatedVectorDrawable mTutorialAnimation = null; 58 @Nullable private AnimatedVectorDrawable mGestureAnimation = null; 59 private boolean mIntroductionShown = false; 60 61 private boolean mFragmentStopped = false; 62 newInstance(TutorialType tutorialType)63 public static TutorialFragment newInstance(TutorialType tutorialType) { 64 TutorialFragment fragment = getFragmentForTutorialType(tutorialType); 65 if (fragment == null) { 66 fragment = new BackGestureTutorialFragment(); 67 tutorialType = TutorialType.BACK_NAVIGATION; 68 } 69 70 Bundle args = new Bundle(); 71 args.putSerializable(KEY_TUTORIAL_TYPE, tutorialType); 72 fragment.setArguments(args); 73 return fragment; 74 } 75 76 @Nullable getFragmentForTutorialType(TutorialType tutorialType)77 private static TutorialFragment getFragmentForTutorialType(TutorialType tutorialType) { 78 switch (tutorialType) { 79 case BACK_NAVIGATION: 80 case BACK_NAVIGATION_COMPLETE: 81 return new BackGestureTutorialFragment(); 82 case HOME_NAVIGATION: 83 case HOME_NAVIGATION_COMPLETE: 84 return new HomeGestureTutorialFragment(); 85 case OVERVIEW_NAVIGATION: 86 case OVERVIEW_NAVIGATION_COMPLETE: 87 return new OverviewGestureTutorialFragment(); 88 case ASSISTANT: 89 case ASSISTANT_COMPLETE: 90 return new AssistantGestureTutorialFragment(); 91 case SANDBOX_MODE: 92 return new SandboxModeTutorialFragment(); 93 default: 94 Log.e(LOG_TAG, "Failed to find an appropriate fragment for " + tutorialType.name()); 95 } 96 return null; 97 } 98 getFeedbackVideoResId(boolean forDarkMode)99 @Nullable Integer getFeedbackVideoResId(boolean forDarkMode) { 100 return null; 101 } 102 getGestureVideoResId()103 @Nullable Integer getGestureVideoResId() { 104 return null; 105 } 106 107 @Nullable getTutorialAnimation()108 AnimatedVectorDrawable getTutorialAnimation() { 109 return mTutorialAnimation; 110 } 111 112 @Nullable getGestureAnimation()113 AnimatedVectorDrawable getGestureAnimation() { 114 return mGestureAnimation; 115 } 116 createController(TutorialType type)117 abstract TutorialController createController(TutorialType type); 118 getControllerClass()119 abstract Class<? extends TutorialController> getControllerClass(); 120 121 @Override onCreate(Bundle savedInstanceState)122 public void onCreate(Bundle savedInstanceState) { 123 super.onCreate(savedInstanceState); 124 Bundle args = savedInstanceState != null ? savedInstanceState : getArguments(); 125 mTutorialType = (TutorialType) args.getSerializable(KEY_TUTORIAL_TYPE); 126 mEdgeBackGestureHandler = new EdgeBackGestureHandler(getContext()); 127 mNavBarGestureHandler = new NavBarGestureHandler(getContext()); 128 } 129 130 @Override onDestroy()131 public void onDestroy() { 132 super.onDestroy(); 133 mEdgeBackGestureHandler.unregisterBackGestureAttemptCallback(); 134 mNavBarGestureHandler.unregisterNavBarGestureAttemptCallback(); 135 } 136 137 @Override onCreateView( @onNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)138 public View onCreateView( 139 @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 140 super.onCreateView(inflater, container, savedInstanceState); 141 142 mRootView = (RootSandboxLayout) inflater.inflate( 143 R.layout.gesture_tutorial_fragment, container, false); 144 mRootView.setOnApplyWindowInsetsListener((view, insets) -> { 145 Insets systemInsets = insets.getInsets(WindowInsets.Type.systemBars()); 146 mEdgeBackGestureHandler.setInsets(systemInsets.left, systemInsets.right); 147 return insets; 148 }); 149 mRootView.setOnTouchListener(this); 150 mFeedbackVideoView = mRootView.findViewById(R.id.gesture_tutorial_feedback_video); 151 mGestureVideoView = mRootView.findViewById(R.id.gesture_tutorial_gesture_video); 152 return mRootView; 153 } 154 155 @Override onStop()156 public void onStop() { 157 super.onStop(); 158 releaseFeedbackVideoView(); 159 releaseGestureVideoView(); 160 mFragmentStopped = true; 161 } 162 initializeFeedbackVideoView()163 void initializeFeedbackVideoView() { 164 if (!updateFeedbackVideo()) { 165 return; 166 } 167 168 if (!mIntroductionShown && mTutorialController != null) { 169 Integer introTileStringResId = mTutorialController.getIntroductionTitle(); 170 Integer introSubtitleResId = mTutorialController.getIntroductionSubtitle(); 171 if (introTileStringResId != null && introSubtitleResId != null) { 172 mTutorialController.showFeedback( 173 introTileStringResId, introSubtitleResId, false, true); 174 mIntroductionShown = true; 175 } 176 } 177 } 178 updateFeedbackVideo()179 boolean updateFeedbackVideo() { 180 if (getContext() == null) { 181 return false; 182 } 183 Integer feedbackVideoResId = getFeedbackVideoResId(Utilities.isDarkTheme(getContext())); 184 185 if (feedbackVideoResId == null || !updateGestureVideo()) { 186 return false; 187 } 188 mTutorialAnimation = (AnimatedVectorDrawable) getContext().getDrawable(feedbackVideoResId); 189 190 if (mTutorialAnimation != null) { 191 mTutorialAnimation.registerAnimationCallback(new Animatable2.AnimationCallback() { 192 193 @Override 194 public void onAnimationStart(Drawable drawable) { 195 super.onAnimationStart(drawable); 196 197 mFeedbackVideoView.setVisibility(View.VISIBLE); 198 } 199 200 @Override 201 public void onAnimationEnd(Drawable drawable) { 202 super.onAnimationEnd(drawable); 203 204 releaseFeedbackVideoView(); 205 } 206 }); 207 } 208 mFeedbackVideoView.setImageDrawable(mTutorialAnimation); 209 210 return true; 211 } 212 updateGestureVideo()213 boolean updateGestureVideo() { 214 Integer gestureVideoResId = getGestureVideoResId(); 215 if (gestureVideoResId == null || getContext() == null) { 216 return false; 217 } 218 mGestureAnimation = (AnimatedVectorDrawable) getContext().getDrawable(gestureVideoResId); 219 220 if (mGestureAnimation != null) { 221 mGestureAnimation.registerAnimationCallback(new Animatable2.AnimationCallback() { 222 223 @Override 224 public void onAnimationEnd(Drawable drawable) { 225 super.onAnimationEnd(drawable); 226 227 mGestureAnimation.start(); 228 } 229 }); 230 } 231 mGestureVideoView.setImageDrawable(mGestureAnimation); 232 233 return true; 234 } 235 releaseFeedbackVideoView()236 void releaseFeedbackVideoView() { 237 if (mTutorialAnimation != null && mTutorialAnimation.isRunning()) { 238 mTutorialAnimation.stop(); 239 } 240 241 mFeedbackVideoView.setVisibility(View.GONE); 242 } 243 releaseGestureVideoView()244 void releaseGestureVideoView() { 245 if (mGestureAnimation != null && mGestureAnimation.isRunning()) { 246 mGestureAnimation.stop(); 247 } 248 249 mGestureVideoView.setVisibility(View.GONE); 250 } 251 252 @Override onResume()253 public void onResume() { 254 super.onResume(); 255 if (mFragmentStopped && mTutorialController != null) { 256 mTutorialController.showFeedback(); 257 mFragmentStopped = false; 258 } else { 259 changeController(mTutorialType); 260 } 261 } 262 263 @Override onTouch(View view, MotionEvent motionEvent)264 public boolean onTouch(View view, MotionEvent motionEvent) { 265 // Note: Using logical-or to ensure both functions get called. 266 return mEdgeBackGestureHandler.onTouch(view, motionEvent) 267 | mNavBarGestureHandler.onTouch(view, motionEvent); 268 } 269 onInterceptTouch(MotionEvent motionEvent)270 boolean onInterceptTouch(MotionEvent motionEvent) { 271 // Note: Using logical-or to ensure both functions get called. 272 return mEdgeBackGestureHandler.onInterceptTouch(motionEvent) 273 | mNavBarGestureHandler.onInterceptTouch(motionEvent); 274 } 275 onAttachedToWindow()276 void onAttachedToWindow() { 277 mEdgeBackGestureHandler.setViewGroupParent(getRootView()); 278 } 279 onDetachedFromWindow()280 void onDetachedFromWindow() { 281 mEdgeBackGestureHandler.setViewGroupParent(null); 282 } 283 changeController(TutorialType tutorialType)284 void changeController(TutorialType tutorialType) { 285 if (getControllerClass().isInstance(mTutorialController)) { 286 mTutorialController.setTutorialType(tutorialType); 287 mTutorialController.fadeTaskViewAndRun(mTutorialController::transitToController); 288 } else { 289 mTutorialController = createController(tutorialType); 290 mTutorialController.transitToController(); 291 } 292 mEdgeBackGestureHandler.registerBackGestureAttemptCallback(mTutorialController); 293 mNavBarGestureHandler.registerNavBarGestureAttemptCallback(mTutorialController); 294 mTutorialType = tutorialType; 295 initializeFeedbackVideoView(); 296 } 297 298 @Override onSaveInstanceState(Bundle savedInstanceState)299 public void onSaveInstanceState(Bundle savedInstanceState) { 300 savedInstanceState.putSerializable(KEY_TUTORIAL_TYPE, mTutorialType); 301 super.onSaveInstanceState(savedInstanceState); 302 } 303 getRootView()304 RootSandboxLayout getRootView() { 305 return mRootView; 306 } 307 continueTutorial()308 void continueTutorial() { 309 GestureSandboxActivity gestureSandboxActivity = getGestureSandboxActivity(); 310 311 if (gestureSandboxActivity == null) { 312 closeTutorial(); 313 return; 314 } 315 gestureSandboxActivity.continueTutorial(); 316 } 317 closeTutorial()318 void closeTutorial() { 319 FragmentActivity activity = getActivity(); 320 if (activity != null) { 321 activity.setResult(Activity.RESULT_OK); 322 activity.finish(); 323 } 324 } 325 startSystemNavigationSetting()326 void startSystemNavigationSetting() { 327 startActivity(new Intent("com.android.settings.GESTURE_NAVIGATION_SETTINGS")); 328 } 329 getCurrentStep()330 int getCurrentStep() { 331 GestureSandboxActivity gestureSandboxActivity = getGestureSandboxActivity(); 332 333 return gestureSandboxActivity == null ? -1 : gestureSandboxActivity.getCurrentStep(); 334 } 335 getNumSteps()336 int getNumSteps() { 337 GestureSandboxActivity gestureSandboxActivity = getGestureSandboxActivity(); 338 339 return gestureSandboxActivity == null ? -1 : gestureSandboxActivity.getNumSteps(); 340 } 341 isAtFinalStep()342 boolean isAtFinalStep() { 343 return getCurrentStep() == getNumSteps(); 344 } 345 346 @Nullable getGestureSandboxActivity()347 private GestureSandboxActivity getGestureSandboxActivity() { 348 Context context = getContext(); 349 350 return context instanceof GestureSandboxActivity ? (GestureSandboxActivity) context : null; 351 } 352 } 353