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