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 static android.view.View.NO_ID; 19 20 import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL; 21 import static com.android.quickstep.interaction.GestureSandboxActivity.KEY_GESTURE_COMPLETE; 22 import static com.android.quickstep.interaction.GestureSandboxActivity.KEY_TUTORIAL_TYPE; 23 import static com.android.quickstep.interaction.GestureSandboxActivity.KEY_USE_TUTORIAL_MENU; 24 25 import android.animation.Animator; 26 import android.animation.AnimatorListenerAdapter; 27 import android.app.Activity; 28 import android.content.Context; 29 import android.content.Intent; 30 import android.content.SharedPreferences; 31 import android.graphics.Insets; 32 import android.graphics.drawable.Animatable2; 33 import android.graphics.drawable.AnimatedVectorDrawable; 34 import android.graphics.drawable.Drawable; 35 import android.os.Bundle; 36 import android.util.ArraySet; 37 import android.util.Log; 38 import android.view.LayoutInflater; 39 import android.view.MotionEvent; 40 import android.view.View; 41 import android.view.View.OnTouchListener; 42 import android.view.ViewGroup; 43 import android.view.ViewTreeObserver; 44 import android.view.WindowInsets; 45 import android.widget.ImageView; 46 47 import androidx.annotation.NonNull; 48 import androidx.annotation.Nullable; 49 50 import com.android.launcher3.DeviceProfile; 51 import com.android.launcher3.InvariantDeviceProfile; 52 import com.android.launcher3.R; 53 import com.android.launcher3.logging.StatsLogManager; 54 import com.android.quickstep.interaction.TutorialController.TutorialType; 55 56 import java.util.Set; 57 58 /** Displays a gesture nav tutorial step. */ 59 abstract class TutorialFragment extends GestureSandboxFragment implements OnTouchListener { 60 61 private static final String LOG_TAG = "TutorialFragment"; 62 63 private static final String TUTORIAL_SKIPPED_PREFERENCE_KEY = "pref_gestureTutorialSkipped"; 64 private static final String COMPLETED_TUTORIAL_STEPS_PREFERENCE_KEY = 65 "pref_completedTutorialSteps"; 66 67 private final boolean mFromTutorialMenu; 68 69 TutorialType mTutorialType; 70 boolean mGestureComplete = false; 71 @Nullable TutorialController mTutorialController = null; 72 RootSandboxLayout mRootView; 73 View mFingerDotView; 74 View mFakePreviousTaskView; 75 EdgeBackGestureHandler mEdgeBackGestureHandler; 76 NavBarGestureHandler mNavBarGestureHandler; 77 private ImageView mEdgeGestureVideoView; 78 79 @Nullable private Animator mGestureAnimation = null; 80 @Nullable private AnimatedVectorDrawable mEdgeAnimation = null; 81 private boolean mIntroductionShown = false; 82 83 private boolean mFragmentStopped = false; 84 85 private DeviceProfile mDeviceProfile; 86 private boolean mIsLargeScreen; 87 private boolean mIsFoldable; 88 private boolean mOnAttachedToWindowPendingCreate; 89 90 @Nullable private Runnable mOnAttachedOnGlobalLayoutCallback = null; 91 newInstance( TutorialType tutorialType, boolean gestureComplete, boolean fromTutorialMenu)92 public static TutorialFragment newInstance( 93 TutorialType tutorialType, boolean gestureComplete, boolean fromTutorialMenu) { 94 TutorialFragment fragment = getFragmentForTutorialType(tutorialType, fromTutorialMenu); 95 if (fragment == null) { 96 fragment = new BackGestureTutorialFragment(fromTutorialMenu); 97 tutorialType = TutorialType.BACK_NAVIGATION; 98 } 99 100 Bundle args = new Bundle(); 101 args.putSerializable(KEY_TUTORIAL_TYPE, tutorialType); 102 args.putBoolean(KEY_GESTURE_COMPLETE, gestureComplete); 103 fragment.setArguments(args); 104 return fragment; 105 } 106 107 @Nullable 108 @Override recreateFragment()109 GestureSandboxFragment recreateFragment() { 110 TutorialType tutorialType = mTutorialController == null 111 ? (mTutorialType == null 112 ? getDefaultTutorialType() : mTutorialType) 113 : mTutorialController.mTutorialType; 114 return newInstance(tutorialType, isGestureComplete(), mFromTutorialMenu); 115 } 116 117 @Override canRecreateFragment()118 boolean canRecreateFragment() { 119 return true; 120 } 121 122 @NonNull getDefaultTutorialType()123 abstract TutorialType getDefaultTutorialType(); 124 TutorialFragment(boolean fromTutorialMenu)125 TutorialFragment(boolean fromTutorialMenu) { 126 mFromTutorialMenu = fromTutorialMenu; 127 } 128 129 @Nullable getFragmentForTutorialType( TutorialType tutorialType, boolean fromTutorialMenu)130 private static TutorialFragment getFragmentForTutorialType( 131 TutorialType tutorialType, boolean fromTutorialMenu) { 132 switch (tutorialType) { 133 case BACK_NAVIGATION: 134 case BACK_NAVIGATION_COMPLETE: 135 return new BackGestureTutorialFragment(fromTutorialMenu); 136 case HOME_NAVIGATION: 137 case HOME_NAVIGATION_COMPLETE: 138 return new HomeGestureTutorialFragment(fromTutorialMenu); 139 case OVERVIEW_NAVIGATION: 140 case OVERVIEW_NAVIGATION_COMPLETE: 141 return new OverviewGestureTutorialFragment(fromTutorialMenu); 142 default: 143 Log.e(LOG_TAG, "Failed to find an appropriate fragment for " + tutorialType.name()); 144 } 145 return null; 146 } 147 getEdgeAnimationResId()148 @Nullable Integer getEdgeAnimationResId() { 149 return null; 150 } 151 152 @Nullable getGestureAnimation()153 Animator getGestureAnimation() { 154 return mGestureAnimation; 155 } 156 157 @Nullable getEdgeAnimation()158 AnimatedVectorDrawable getEdgeAnimation() { 159 return mEdgeAnimation; 160 } 161 162 163 @Nullable createGestureAnimation()164 protected Animator createGestureAnimation() { 165 return null; 166 } 167 168 @NonNull createController(TutorialType type)169 abstract TutorialController createController(TutorialType type); 170 getControllerClass()171 abstract Class<? extends TutorialController> getControllerClass(); 172 173 @Override onCreate(Bundle savedInstanceState)174 public void onCreate(Bundle savedInstanceState) { 175 super.onCreate(savedInstanceState); 176 Bundle args = savedInstanceState != null ? savedInstanceState : getArguments(); 177 mTutorialType = (TutorialType) args.getSerializable(KEY_TUTORIAL_TYPE); 178 mGestureComplete = args.getBoolean(KEY_GESTURE_COMPLETE, false); 179 mEdgeBackGestureHandler = new EdgeBackGestureHandler(getContext()); 180 mNavBarGestureHandler = new NavBarGestureHandler(getContext()); 181 182 mDeviceProfile = InvariantDeviceProfile.INSTANCE.get(getContext()) 183 .getDeviceProfile(getContext()); 184 mIsLargeScreen = mDeviceProfile.isTablet; 185 mIsFoldable = mDeviceProfile.isTwoPanels; 186 187 if (mOnAttachedToWindowPendingCreate) { 188 mOnAttachedToWindowPendingCreate = false; 189 onAttachedToWindow(); 190 } 191 } 192 isLargeScreen()193 public boolean isLargeScreen() { 194 return mIsLargeScreen; 195 } 196 isFoldable()197 public boolean isFoldable() { 198 return mIsFoldable; 199 } 200 getDeviceProfile()201 DeviceProfile getDeviceProfile() { 202 return mDeviceProfile; 203 } 204 205 @Override onDestroy()206 public void onDestroy() { 207 super.onDestroy(); 208 mEdgeBackGestureHandler.unregisterBackGestureAttemptCallback(); 209 mNavBarGestureHandler.unregisterNavBarGestureAttemptCallback(); 210 } 211 212 @Override onCreateView( @onNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)213 public View onCreateView( 214 @NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { 215 super.onCreateView(inflater, container, savedInstanceState); 216 217 mRootView = (RootSandboxLayout) inflater.inflate( 218 ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() 219 ? R.layout.redesigned_gesture_tutorial_fragment 220 : R.layout.gesture_tutorial_fragment, 221 container, 222 false); 223 224 mRootView.setOnApplyWindowInsetsListener((view, insets) -> { 225 Insets systemInsets = insets.getInsets(WindowInsets.Type.systemBars()); 226 mEdgeBackGestureHandler.setInsets(systemInsets.left, systemInsets.right); 227 return insets; 228 }); 229 mRootView.setOnTouchListener(this); 230 mEdgeGestureVideoView = mRootView.findViewById(R.id.gesture_tutorial_edge_gesture_video); 231 mFingerDotView = mRootView.findViewById(R.id.gesture_tutorial_finger_dot); 232 mFakePreviousTaskView = mRootView.findViewById( 233 R.id.gesture_tutorial_fake_previous_task_view); 234 235 return mRootView; 236 } 237 238 @Override onStop()239 public void onStop() { 240 super.onStop(); 241 releaseFeedbackAnimation(); 242 mFragmentStopped = true; 243 } 244 initializeFeedbackVideoView()245 void initializeFeedbackVideoView() { 246 if (!updateFeedbackAnimation() || mTutorialController == null) { 247 return; 248 } 249 250 if (isGestureComplete()) { 251 mTutorialController.showSuccessFeedback(); 252 } else if (!mIntroductionShown) { 253 int introTitleResId = mTutorialController.getIntroductionTitle(); 254 int introSubtitleResId = mTutorialController.getIntroductionSubtitle(); 255 if (introTitleResId == NO_ID) { 256 // Allow crash since this should never be reached with a tutorial controller used in 257 // production. 258 Log.e(LOG_TAG, 259 "Cannot show introduction feedback for tutorial step: " + mTutorialType 260 + ", no introduction feedback title", 261 new IllegalStateException()); 262 } 263 if (introTitleResId == NO_ID) { 264 // Allow crash since this should never be reached with a tutorial controller used in 265 // production. 266 Log.e(LOG_TAG, 267 "Cannot show introduction feedback for tutorial step: " + mTutorialType 268 + ", no introduction feedback subtitle", 269 new IllegalStateException()); 270 } 271 mTutorialController.showFeedback( 272 introTitleResId, 273 introSubtitleResId, 274 mTutorialController.getSpokenIntroductionSubtitle(), 275 false, 276 true); 277 mIntroductionShown = true; 278 } 279 } 280 updateFeedbackAnimation()281 boolean updateFeedbackAnimation() { 282 if (!updateEdgeAnimation()) { 283 return false; 284 } 285 mGestureAnimation = createGestureAnimation(); 286 287 if (mGestureAnimation != null) { 288 mGestureAnimation.addListener(new AnimatorListenerAdapter() { 289 @Override 290 public void onAnimationStart(Animator animation) { 291 super.onAnimationStart(animation); 292 mFingerDotView.setVisibility(View.VISIBLE); 293 } 294 295 @Override 296 public void onAnimationCancel(Animator animation) { 297 super.onAnimationCancel(animation); 298 mFingerDotView.setVisibility(View.GONE); 299 } 300 301 @Override 302 public void onAnimationEnd(Animator animation) { 303 super.onAnimationEnd(animation); 304 mFingerDotView.setVisibility(View.GONE); 305 } 306 }); 307 } 308 309 return mGestureAnimation != null; 310 } 311 updateEdgeAnimation()312 boolean updateEdgeAnimation() { 313 Integer edgeAnimationResId = getEdgeAnimationResId(); 314 if (edgeAnimationResId == null || getContext() == null) { 315 return false; 316 } 317 mEdgeAnimation = (AnimatedVectorDrawable) getContext().getDrawable(edgeAnimationResId); 318 319 if (mEdgeAnimation != null) { 320 mEdgeAnimation.registerAnimationCallback(new Animatable2.AnimationCallback() { 321 322 @Override 323 public void onAnimationEnd(Drawable drawable) { 324 super.onAnimationEnd(drawable); 325 326 mEdgeAnimation.start(); 327 } 328 }); 329 } 330 mEdgeGestureVideoView.setImageDrawable(mEdgeAnimation); 331 332 return mEdgeAnimation != null; 333 } 334 releaseFeedbackAnimation()335 void releaseFeedbackAnimation() { 336 if (mTutorialController != null && !mTutorialController.isGestureCompleted()) { 337 mTutorialController.cancelQueuedGestureAnimation(); 338 } 339 if (mGestureAnimation != null && mGestureAnimation.isRunning()) { 340 mGestureAnimation.cancel(); 341 } 342 if (mEdgeAnimation != null && mEdgeAnimation.isRunning()) { 343 mEdgeAnimation.stop(); 344 } 345 mEdgeGestureVideoView.setVisibility(View.GONE); 346 } 347 348 @Override onResume()349 public void onResume() { 350 super.onResume(); 351 releaseFeedbackAnimation(); 352 if (mFragmentStopped && mTutorialController != null) { 353 mTutorialController.showFeedback(); 354 mFragmentStopped = false; 355 } else { 356 mRootView.getViewTreeObserver().addOnGlobalLayoutListener( 357 new ViewTreeObserver.OnGlobalLayoutListener() { 358 @Override 359 public void onGlobalLayout() { 360 runOnAttached(() -> changeController(mTutorialType)); 361 mRootView.getViewTreeObserver().removeOnGlobalLayoutListener(this); 362 } 363 }); 364 } 365 } 366 runOnAttached(Runnable callback)367 private void runOnAttached(Runnable callback) { 368 mOnAttachedOnGlobalLayoutCallback = callback; 369 if (getContext() != null) { 370 onAttached(); 371 } 372 } 373 onAttached()374 private void onAttached() { 375 if (mOnAttachedOnGlobalLayoutCallback != null) { 376 mOnAttachedOnGlobalLayoutCallback.run(); 377 mOnAttachedOnGlobalLayoutCallback = null; 378 } 379 } 380 381 @Override onTouch(View view, MotionEvent motionEvent)382 public boolean onTouch(View view, MotionEvent motionEvent) { 383 if (mTutorialController != null && !isGestureComplete()) { 384 mTutorialController.hideFeedback(); 385 } 386 387 if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { 388 mTutorialController.pauseAndHideLottieAnimation(); 389 } 390 391 // Note: Using logical-or to ensure both functions get called. 392 return mEdgeBackGestureHandler.onTouch(view, motionEvent) 393 | mNavBarGestureHandler.onTouch(view, motionEvent); 394 } 395 onInterceptTouch(MotionEvent motionEvent)396 boolean onInterceptTouch(MotionEvent motionEvent) { 397 // Note: Using logical-or to ensure both functions get called. 398 return mEdgeBackGestureHandler.onInterceptTouch(motionEvent) 399 | mNavBarGestureHandler.onInterceptTouch(motionEvent); 400 } 401 402 @Override onAttach(@onNull Context context)403 public void onAttach(@NonNull Context context) { 404 super.onAttach(context); 405 onAttached(); 406 } 407 408 @Override onAttachedToWindow()409 void onAttachedToWindow() { 410 if (mEdgeBackGestureHandler == null) { 411 mOnAttachedToWindowPendingCreate = true; 412 return; 413 } 414 StatsLogManager statsLogManager = getStatsLogManager(); 415 if (statsLogManager != null) { 416 logTutorialStepShown(statsLogManager); 417 } 418 mEdgeBackGestureHandler.setViewGroupParent(getRootView()); 419 } 420 421 @Override onDetachedFromWindow()422 void onDetachedFromWindow() { 423 mOnAttachedToWindowPendingCreate = false; 424 mEdgeBackGestureHandler.setViewGroupParent(null); 425 } 426 changeController(TutorialType tutorialType)427 void changeController(TutorialType tutorialType) { 428 if (getControllerClass().isInstance(mTutorialController)) { 429 mTutorialController.setTutorialType(tutorialType); 430 if (isGestureComplete()) { 431 mTutorialController.setGestureCompleted(); 432 } 433 mTutorialController.fadeTaskViewAndRun(mTutorialController::transitToController); 434 } else { 435 mTutorialController = createController(tutorialType); 436 if (isGestureComplete()) { 437 mTutorialController.setGestureCompleted(); 438 } 439 mTutorialController.transitToController(); 440 } 441 mEdgeBackGestureHandler.registerBackGestureAttemptCallback(mTutorialController); 442 mNavBarGestureHandler.registerNavBarGestureAttemptCallback(mTutorialController); 443 mTutorialType = tutorialType; 444 445 initializeFeedbackVideoView(); 446 } 447 448 @Override onSaveInstanceState(Bundle savedInstanceState)449 public void onSaveInstanceState(Bundle savedInstanceState) { 450 savedInstanceState.putSerializable(KEY_TUTORIAL_TYPE, mTutorialType); 451 savedInstanceState.putBoolean(KEY_GESTURE_COMPLETE, isGestureComplete()); 452 savedInstanceState.putBoolean(KEY_USE_TUTORIAL_MENU, mFromTutorialMenu); 453 super.onSaveInstanceState(savedInstanceState); 454 } 455 getRootView()456 RootSandboxLayout getRootView() { 457 return mRootView; 458 } 459 continueTutorial()460 void continueTutorial() { 461 SharedPreferences sharedPrefs = getSharedPreferences(); 462 if (sharedPrefs != null) { 463 Set<String> updatedCompletedSteps = new ArraySet<>(sharedPrefs.getStringSet( 464 COMPLETED_TUTORIAL_STEPS_PREFERENCE_KEY, new ArraySet<>())); 465 466 updatedCompletedSteps.add(mTutorialType.toString()); 467 468 sharedPrefs.edit().putStringSet( 469 COMPLETED_TUTORIAL_STEPS_PREFERENCE_KEY, updatedCompletedSteps).apply(); 470 } 471 StatsLogManager statsLogManager = getStatsLogManager(); 472 if (statsLogManager != null) { 473 logTutorialStepCompleted(statsLogManager); 474 } 475 476 GestureSandboxActivity gestureSandboxActivity = getGestureSandboxActivity(); 477 if (gestureSandboxActivity == null) { 478 close(); 479 return; 480 } 481 gestureSandboxActivity.continueTutorial(); 482 } 483 484 @Override close()485 void close() { 486 closeTutorialStep(false); 487 } 488 closeTutorialStep(boolean tutorialSkipped)489 void closeTutorialStep(boolean tutorialSkipped) { 490 if (tutorialSkipped) { 491 SharedPreferences sharedPrefs = getSharedPreferences(); 492 if (sharedPrefs != null) { 493 sharedPrefs.edit().putBoolean(TUTORIAL_SKIPPED_PREFERENCE_KEY, true).apply(); 494 } 495 StatsLogManager statsLogManager = getStatsLogManager(); 496 if (statsLogManager != null) { 497 statsLogManager.logger().log( 498 StatsLogManager.LauncherEvent.LAUNCHER_GESTURE_TUTORIAL_SKIPPED); 499 } 500 } 501 GestureSandboxActivity gestureSandboxActivity = getGestureSandboxActivity(); 502 if (mFromTutorialMenu && gestureSandboxActivity != null) { 503 gestureSandboxActivity.launchTutorialMenu(); 504 return; 505 } 506 super.close(); 507 } 508 startSystemNavigationSetting()509 void startSystemNavigationSetting() { 510 startActivity(new Intent("com.android.settings.GESTURE_NAVIGATION_SETTINGS")); 511 } 512 getCurrentStep()513 int getCurrentStep() { 514 GestureSandboxActivity gestureSandboxActivity = getGestureSandboxActivity(); 515 516 return gestureSandboxActivity == null ? -1 : gestureSandboxActivity.getCurrentStep(); 517 } 518 getNumSteps()519 int getNumSteps() { 520 GestureSandboxActivity gestureSandboxActivity = getGestureSandboxActivity(); 521 522 return gestureSandboxActivity == null ? -1 : gestureSandboxActivity.getNumSteps(); 523 } 524 isAtFinalStep()525 boolean isAtFinalStep() { 526 return getCurrentStep() == getNumSteps(); 527 } 528 isGestureComplete()529 boolean isGestureComplete() { 530 return mGestureComplete 531 || (mTutorialController != null && mTutorialController.isGestureCompleted()); 532 } 533 logTutorialStepShown(@onNull StatsLogManager statsLogManager)534 abstract void logTutorialStepShown(@NonNull StatsLogManager statsLogManager); 535 logTutorialStepCompleted(@onNull StatsLogManager statsLogManager)536 abstract void logTutorialStepCompleted(@NonNull StatsLogManager statsLogManager); 537 538 @Nullable getGestureSandboxActivity()539 private GestureSandboxActivity getGestureSandboxActivity() { 540 Activity activity = getActivity(); 541 542 return activity instanceof GestureSandboxActivity 543 ? (GestureSandboxActivity) activity : null; 544 } 545 546 @Nullable getStatsLogManager()547 private StatsLogManager getStatsLogManager() { 548 GestureSandboxActivity activity = getGestureSandboxActivity(); 549 550 return activity != null ? activity.getStatsLogManager() : null; 551 } 552 553 @Nullable getSharedPreferences()554 private SharedPreferences getSharedPreferences() { 555 GestureSandboxActivity activity = getGestureSandboxActivity(); 556 557 return activity != null ? activity.getSharedPrefs() : null; 558 } 559 } 560