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