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