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.GONE; 19 import static android.view.View.NO_ID; 20 import static android.view.View.inflate; 21 22 import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL; 23 24 import android.animation.Animator; 25 import android.animation.AnimatorListenerAdapter; 26 import android.animation.AnimatorSet; 27 import android.animation.ObjectAnimator; 28 import android.animation.ValueAnimator; 29 import android.annotation.RawRes; 30 import android.content.Context; 31 import android.content.pm.PackageManager; 32 import android.graphics.Color; 33 import android.graphics.Matrix; 34 import android.graphics.Outline; 35 import android.graphics.Rect; 36 import android.graphics.drawable.AnimatedVectorDrawable; 37 import android.graphics.drawable.ColorDrawable; 38 import android.graphics.drawable.RippleDrawable; 39 import android.util.Log; 40 import android.view.View; 41 import android.view.ViewGroup; 42 import android.view.ViewOutlineProvider; 43 import android.view.accessibility.AccessibilityEvent; 44 import android.widget.Button; 45 import android.widget.FrameLayout; 46 import android.widget.ImageView; 47 import android.widget.RelativeLayout; 48 import android.widget.TextView; 49 50 import androidx.annotation.CallSuper; 51 import androidx.annotation.ColorInt; 52 import androidx.annotation.DrawableRes; 53 import androidx.annotation.LayoutRes; 54 import androidx.annotation.NonNull; 55 import androidx.annotation.Nullable; 56 import androidx.annotation.StringRes; 57 import androidx.annotation.StyleRes; 58 import androidx.appcompat.app.AlertDialog; 59 import androidx.appcompat.content.res.AppCompatResources; 60 61 import com.android.launcher3.DeviceProfile; 62 import com.android.launcher3.R; 63 import com.android.launcher3.Utilities; 64 import com.android.launcher3.anim.AnimatorListeners; 65 import com.android.launcher3.views.ClipIconView; 66 import com.android.quickstep.interaction.EdgeBackGestureHandler.BackGestureAttemptCallback; 67 import com.android.quickstep.interaction.NavBarGestureHandler.NavBarGestureAttemptCallback; 68 import com.android.systemui.shared.system.QuickStepContract; 69 70 import com.airbnb.lottie.LottieAnimationView; 71 import com.airbnb.lottie.LottieComposition; 72 73 import java.util.ArrayList; 74 75 abstract class TutorialController implements BackGestureAttemptCallback, 76 NavBarGestureAttemptCallback { 77 78 private static final String LOG_TAG = "TutorialController"; 79 80 private static final float FINGER_DOT_VISIBLE_ALPHA = 0.7f; 81 private static final float FINGER_DOT_SMALL_SCALE = 0.7f; 82 private static final int FINGER_DOT_ANIMATION_DURATION_MILLIS = 500; 83 84 private static final String PIXEL_TIPS_APP_PACKAGE_NAME = "com.google.android.apps.tips"; 85 private static final CharSequence DEFAULT_PIXEL_TIPS_APP_NAME = "Pixel Tips"; 86 87 private static final int FEEDBACK_ANIMATION_MS = 133; 88 private static final int RIPPLE_VISIBLE_MS = 300; 89 private static final int GESTURE_ANIMATION_DELAY_MS = 1500; 90 private static final int ADVANCE_TUTORIAL_TIMEOUT_MS = 2000; 91 private static final long GESTURE_ANIMATION_PAUSE_DURATION_MILLIS = 1000; 92 protected float mExitingAppEndingCornerRadius; 93 protected float mExitingAppStartingCornerRadius; 94 protected int mScreenHeight; 95 protected float mScreenWidth; 96 protected float mExitingAppMargin; 97 98 final TutorialFragment mTutorialFragment; 99 TutorialType mTutorialType; 100 final Context mContext; 101 102 final TextView mSkipButton; 103 final Button mDoneButton; 104 final ViewGroup mFeedbackView; 105 final TextView mFeedbackTitleView; 106 final TextView mFeedbackSubtitleView; 107 final ImageView mEdgeGestureVideoView; 108 final RelativeLayout mFakeLauncherView; 109 final FrameLayout mFakeHotseatView; 110 @Nullable View mHotseatIconView; 111 final ClipIconView mFakeIconView; 112 final FrameLayout mFakeTaskView; 113 @Nullable final AnimatedTaskbarView mFakeTaskbarView; 114 final AnimatedTaskView mFakePreviousTaskView; 115 final View mRippleView; 116 final RippleDrawable mRippleDrawable; 117 final TutorialStepIndicator mTutorialStepView; 118 final ImageView mFingerDotView; 119 private final Rect mExitingAppRect = new Rect(); 120 protected View mExitingAppView; 121 protected int mExitingAppRadius; 122 private final AlertDialog mSkipTutorialDialog; 123 124 private boolean mGestureCompleted = false; 125 protected LottieAnimationView mAnimatedGestureDemonstration; 126 protected LottieAnimationView mCheckmarkAnimation; 127 private RelativeLayout mFullGestureDemonstration; 128 129 // These runnables should be used when posting callbacks to their views and cleared from their 130 // views before posting new callbacks. 131 private final Runnable mTitleViewCallback; 132 @Nullable private Runnable mFeedbackViewCallback; 133 @Nullable private Runnable mFakeTaskViewCallback; 134 @Nullable private Runnable mFakeTaskbarViewCallback; 135 private final Runnable mShowFeedbackRunnable; 136 TutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType)137 TutorialController(TutorialFragment tutorialFragment, TutorialType tutorialType) { 138 mTutorialFragment = tutorialFragment; 139 mTutorialType = tutorialType; 140 mContext = mTutorialFragment.getContext(); 141 142 RootSandboxLayout rootView = tutorialFragment.getRootView(); 143 mSkipButton = rootView.findViewById(R.id.gesture_tutorial_fragment_close_button); 144 mSkipButton.setOnClickListener(button -> showSkipTutorialDialog()); 145 mFeedbackView = rootView.findViewById(R.id.gesture_tutorial_fragment_feedback_view); 146 mFeedbackTitleView = mFeedbackView.findViewById( 147 R.id.gesture_tutorial_fragment_feedback_title); 148 mFeedbackSubtitleView = mFeedbackView.findViewById( 149 R.id.gesture_tutorial_fragment_feedback_subtitle); 150 mEdgeGestureVideoView = rootView.findViewById(R.id.gesture_tutorial_edge_gesture_video); 151 mFakeLauncherView = rootView.findViewById(R.id.gesture_tutorial_fake_launcher_view); 152 mFakeHotseatView = rootView.findViewById(R.id.gesture_tutorial_fake_hotseat_view); 153 mFakeIconView = rootView.findViewById(R.id.gesture_tutorial_fake_icon_view); 154 mFakeTaskView = rootView.findViewById(R.id.gesture_tutorial_fake_task_view); 155 mFakeTaskbarView = ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() 156 ? null : rootView.findViewById(R.id.gesture_tutorial_fake_taskbar_view); 157 mFakePreviousTaskView = 158 rootView.findViewById(R.id.gesture_tutorial_fake_previous_task_view); 159 mRippleView = rootView.findViewById(R.id.gesture_tutorial_ripple_view); 160 mRippleDrawable = (RippleDrawable) mRippleView.getBackground(); 161 mDoneButton = rootView.findViewById(R.id.gesture_tutorial_fragment_action_button); 162 mTutorialStepView = 163 rootView.findViewById(R.id.gesture_tutorial_fragment_feedback_tutorial_step); 164 mFingerDotView = rootView.findViewById(R.id.gesture_tutorial_finger_dot); 165 mSkipTutorialDialog = createSkipTutorialDialog(); 166 167 if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { 168 mFullGestureDemonstration = rootView.findViewById(R.id.full_gesture_demonstration); 169 mCheckmarkAnimation = rootView.findViewById(R.id.checkmark_animation); 170 mAnimatedGestureDemonstration = rootView.findViewById( 171 R.id.gesture_demonstration_animations); 172 mExitingAppView = rootView.findViewById(R.id.exiting_app_back); 173 mScreenWidth = mTutorialFragment.getDeviceProfile().widthPx; 174 mScreenHeight = mTutorialFragment.getDeviceProfile().heightPx; 175 mExitingAppMargin = mContext.getResources().getDimensionPixelSize( 176 R.dimen.gesture_tutorial_back_gesture_exiting_app_margin); 177 mExitingAppStartingCornerRadius = QuickStepContract.getWindowCornerRadius(mContext); 178 mExitingAppEndingCornerRadius = mContext.getResources().getDimensionPixelSize( 179 R.dimen.gesture_tutorial_back_gesture_end_corner_radius); 180 mAnimatedGestureDemonstration.addLottieOnCompositionLoadedListener( 181 this::createScalingMatrix); 182 183 mFeedbackTitleView.setText(getIntroductionTitle()); 184 mFeedbackSubtitleView.setText(getIntroductionSubtitle()); 185 mExitingAppView.setClipToOutline(true); 186 mExitingAppView.setOutlineProvider(new ViewOutlineProvider() { 187 @Override 188 public void getOutline(View view, Outline outline) { 189 outline.setRoundRect(mExitingAppRect, mExitingAppRadius); 190 } 191 }); 192 } 193 194 mTitleViewCallback = () -> mFeedbackTitleView.sendAccessibilityEvent( 195 AccessibilityEvent.TYPE_VIEW_FOCUSED); 196 mShowFeedbackRunnable = () -> { 197 mFeedbackView.setAlpha(0f); 198 mFeedbackView.setScaleX(0.95f); 199 mFeedbackView.setScaleY(0.95f); 200 mFeedbackView.setVisibility(View.VISIBLE); 201 mFeedbackView.animate() 202 .setDuration(FEEDBACK_ANIMATION_MS) 203 .alpha(1f) 204 .scaleX(1f) 205 .scaleY(1f) 206 .withEndAction(() -> { 207 if (mGestureCompleted && !mTutorialFragment.isAtFinalStep()) { 208 if (mFeedbackViewCallback != null) { 209 mFeedbackView.removeCallbacks(mFeedbackViewCallback); 210 } 211 mFeedbackViewCallback = mTutorialFragment::continueTutorial; 212 mFeedbackView.postDelayed(mFeedbackViewCallback, 213 ADVANCE_TUTORIAL_TIMEOUT_MS); 214 } 215 }) 216 .start(); 217 mFeedbackTitleView.postDelayed(mTitleViewCallback, FEEDBACK_ANIMATION_MS); 218 }; 219 } 220 221 /** Scale the Lottie gesture animation to fit the device based on device dimensions */ createScalingMatrix(LottieComposition composition)222 private void createScalingMatrix(LottieComposition composition) { 223 Rect animationBoundsRect = composition.getBounds(); 224 if (animationBoundsRect == null) { 225 mAnimatedGestureDemonstration.setScaleType(ImageView.ScaleType.CENTER_CROP); 226 return; 227 } 228 Matrix scaleMatrix = new Matrix(); 229 float scaleFactor = mScreenWidth / animationBoundsRect.width(); 230 float heightTranslate = (mScreenHeight - (scaleFactor * animationBoundsRect.height())); 231 232 scaleMatrix.postScale(scaleFactor, scaleFactor); 233 scaleMatrix.postTranslate(0, heightTranslate); 234 mAnimatedGestureDemonstration.setImageMatrix(scaleMatrix); 235 } 236 showSkipTutorialDialog()237 private void showSkipTutorialDialog() { 238 if (mSkipTutorialDialog != null) { 239 mSkipTutorialDialog.show(); 240 } 241 } 242 getHotseatIconTop()243 public int getHotseatIconTop() { 244 return mHotseatIconView == null 245 ? 0 : mFakeHotseatView.getTop() + mHotseatIconView.getTop(); 246 } 247 getHotseatIconLeft()248 public int getHotseatIconLeft() { 249 return mHotseatIconView == null 250 ? 0 : mFakeHotseatView.getLeft() + mHotseatIconView.getLeft(); 251 } 252 setTutorialType(TutorialType tutorialType)253 void setTutorialType(TutorialType tutorialType) { 254 mTutorialType = tutorialType; 255 } 256 257 @LayoutRes getMockHotseatResId()258 protected int getMockHotseatResId() { 259 if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { 260 return mTutorialFragment.isLargeScreen() 261 ? mTutorialFragment.isFoldable() 262 ? R.layout.redesigned_gesture_tutorial_foldable_mock_hotseat 263 : R.layout.redesigned_gesture_tutorial_tablet_mock_hotseat 264 : R.layout.redesigned_gesture_tutorial_mock_hotseat; 265 } else { 266 return mTutorialFragment.isLargeScreen() 267 ? mTutorialFragment.isFoldable() 268 ? R.layout.gesture_tutorial_foldable_mock_hotseat 269 : R.layout.gesture_tutorial_tablet_mock_hotseat 270 : R.layout.gesture_tutorial_mock_hotseat; 271 } 272 } 273 274 @LayoutRes getMockAppTaskLayoutResId()275 protected int getMockAppTaskLayoutResId() { 276 return NO_ID; 277 } 278 279 @RawRes getGestureLottieAnimationId()280 protected int getGestureLottieAnimationId() { 281 return NO_ID; 282 } 283 284 @ColorInt getMockPreviousAppTaskThumbnailColor()285 protected int getMockPreviousAppTaskThumbnailColor() { 286 return mContext.getResources().getColor( 287 R.color.gesture_tutorial_fake_previous_task_view_color); 288 } 289 290 @ColorInt getFakeTaskViewColor()291 protected int getFakeTaskViewColor() { 292 return Color.TRANSPARENT; 293 } 294 295 @ColorInt getFakeLauncherColor()296 protected abstract int getFakeLauncherColor(); 297 298 @ColorInt getExitingAppColor()299 protected int getExitingAppColor() { 300 return Color.TRANSPARENT; 301 } 302 303 @ColorInt getHotseatIconColor()304 protected int getHotseatIconColor() { 305 return Color.TRANSPARENT; 306 } 307 308 @DrawableRes getMockAppIconResId()309 public int getMockAppIconResId() { 310 return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() 311 ? R.drawable.redesigned_hotseat_icon 312 : R.drawable.default_sandbox_app_icon; 313 } 314 315 @DrawableRes getMockWallpaperResId()316 public int getMockWallpaperResId() { 317 return R.drawable.default_sandbox_wallpaper; 318 } 319 fadeTaskViewAndRun(Runnable r)320 void fadeTaskViewAndRun(Runnable r) { 321 mFakeTaskView.animate().alpha(0).setListener(AnimatorListeners.forSuccessCallback(r)); 322 } 323 324 @StringRes getIntroductionTitle()325 public int getIntroductionTitle() { 326 return NO_ID; 327 } 328 329 @StringRes getIntroductionSubtitle()330 public int getIntroductionSubtitle() { 331 return NO_ID; 332 } 333 334 @StringRes getSpokenIntroductionSubtitle()335 public int getSpokenIntroductionSubtitle() { 336 return NO_ID; 337 } 338 339 @StringRes getSuccessFeedbackSubtitle()340 public int getSuccessFeedbackSubtitle() { 341 return NO_ID; 342 } 343 344 @StringRes getSuccessFeedbackTitle()345 public int getSuccessFeedbackTitle() { 346 return NO_ID; 347 } 348 349 @StyleRes getTitleTextAppearance()350 public int getTitleTextAppearance() { 351 return NO_ID; 352 } 353 354 @StyleRes getSuccessTitleTextAppearance()355 public int getSuccessTitleTextAppearance() { 356 return NO_ID; 357 } 358 359 @StyleRes getDoneButtonTextAppearance()360 public int getDoneButtonTextAppearance() { 361 return NO_ID; 362 } 363 364 @ColorInt getDoneButtonColor()365 public abstract int getDoneButtonColor(); 366 showFeedback()367 void showFeedback() { 368 if (mGestureCompleted) { 369 mFeedbackView.setTranslationY(0); 370 return; 371 } 372 Animator gestureAnimation = mTutorialFragment.getGestureAnimation(); 373 AnimatedVectorDrawable edgeAnimation = mTutorialFragment.getEdgeAnimation(); 374 if (gestureAnimation != null && edgeAnimation != null) { 375 playFeedbackAnimation(gestureAnimation, edgeAnimation, mShowFeedbackRunnable, true); 376 } 377 } 378 379 /** 380 * Only use this when a gesture is completed, but the feedback shouldn't be shown immediately. 381 * In that case, call this method immediately instead. 382 */ setGestureCompleted()383 public void setGestureCompleted() { 384 mGestureCompleted = true; 385 } 386 387 /** 388 * Show feedback reflecting a successful gesture attempt. 389 **/ showSuccessFeedback()390 void showSuccessFeedback() { 391 int successSubtitleResId = getSuccessFeedbackSubtitle(); 392 if (successSubtitleResId == NO_ID) { 393 // Allow crash since this should never be reached with a tutorial controller used in 394 // production. 395 Log.e(LOG_TAG, 396 "Cannot show success feedback for tutorial step: " + mTutorialType 397 + ", no success feedback subtitle", 398 new IllegalStateException()); 399 } 400 showFeedback(successSubtitleResId, true); 401 } 402 403 /** 404 * Show feedback reflecting a failed gesture attempt. 405 * 406 * @param subtitleResId Resource of the text to display. 407 **/ showFeedback(int subtitleResId)408 void showFeedback(int subtitleResId) { 409 showFeedback(subtitleResId, false); 410 } 411 412 /** 413 * Show feedback reflecting the result of a gesture attempt. 414 * 415 * @param isGestureSuccessful Whether the tutorial feedback's action button should be shown. 416 **/ showFeedback(int subtitleResId, boolean isGestureSuccessful)417 void showFeedback(int subtitleResId, boolean isGestureSuccessful) { 418 showFeedback( 419 isGestureSuccessful 420 ? getSuccessFeedbackTitle() : R.string.gesture_tutorial_try_again, 421 subtitleResId, 422 NO_ID, 423 isGestureSuccessful, 424 false); 425 } 426 showFeedback( int titleResId, int subtitleResId, int spokenSubtitleResId, boolean isGestureSuccessful, boolean useGestureAnimationDelay)427 void showFeedback( 428 int titleResId, 429 int subtitleResId, 430 int spokenSubtitleResId, 431 boolean isGestureSuccessful, 432 boolean useGestureAnimationDelay) { 433 mFeedbackTitleView.removeCallbacks(mTitleViewCallback); 434 if (mFeedbackViewCallback != null) { 435 mFeedbackView.removeCallbacks(mFeedbackViewCallback); 436 mFeedbackViewCallback = null; 437 } 438 439 mFeedbackTitleView.setText(titleResId); 440 mFeedbackSubtitleView.setText( 441 ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() || spokenSubtitleResId == NO_ID 442 ? mContext.getText(subtitleResId) 443 : Utilities.wrapForTts( 444 mContext.getText(subtitleResId), 445 mContext.getString(spokenSubtitleResId))); 446 if (isGestureSuccessful) { 447 if (mTutorialFragment.isAtFinalStep()) { 448 showActionButton(); 449 } 450 451 if (mFakeTaskViewCallback != null) { 452 mFakeTaskView.removeCallbacks(mFakeTaskViewCallback); 453 mFakeTaskViewCallback = null; 454 } 455 456 if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { 457 showSuccessPage(); 458 } 459 } 460 mGestureCompleted = isGestureSuccessful; 461 462 Animator gestureAnimation = mTutorialFragment.getGestureAnimation(); 463 AnimatedVectorDrawable edgeAnimation = mTutorialFragment.getEdgeAnimation(); 464 if (!isGestureSuccessful && gestureAnimation != null && edgeAnimation != null) { 465 playFeedbackAnimation( 466 gestureAnimation, 467 edgeAnimation, 468 mShowFeedbackRunnable, 469 useGestureAnimationDelay); 470 return; 471 } else { 472 mTutorialFragment.releaseFeedbackAnimation(); 473 } 474 mFeedbackViewCallback = mShowFeedbackRunnable; 475 476 mFeedbackView.post(mFeedbackViewCallback); 477 } 478 showSuccessPage()479 private void showSuccessPage() { 480 pauseAndHideLottieAnimation(); 481 mCheckmarkAnimation.setVisibility(View.VISIBLE); 482 mCheckmarkAnimation.playAnimation(); 483 mFeedbackTitleView.setTextAppearance(mContext, getSuccessTitleTextAppearance()); 484 } 485 isGestureCompleted()486 public boolean isGestureCompleted() { 487 return mGestureCompleted; 488 } 489 hideFeedback()490 void hideFeedback() { 491 if (mFeedbackView.getVisibility() != View.VISIBLE) { 492 return; 493 } 494 cancelQueuedGestureAnimation(); 495 mFeedbackView.clearAnimation(); 496 mFeedbackView.setVisibility(View.INVISIBLE); 497 } 498 cancelQueuedGestureAnimation()499 void cancelQueuedGestureAnimation() { 500 if (mFeedbackViewCallback != null) { 501 mFeedbackView.removeCallbacks(mFeedbackViewCallback); 502 mFeedbackViewCallback = null; 503 } 504 if (mFakeTaskViewCallback != null) { 505 mFakeTaskView.removeCallbacks(mFakeTaskViewCallback); 506 mFakeTaskViewCallback = null; 507 } 508 if (mFakeTaskbarViewCallback != null && mFakeTaskbarView != null) { 509 mFakeTaskbarView.removeCallbacks(mFakeTaskbarViewCallback); 510 mFakeTaskbarViewCallback = null; 511 } 512 mFeedbackTitleView.removeCallbacks(mTitleViewCallback); 513 } 514 playFeedbackAnimation( @onNull Animator gestureAnimation, @NonNull AnimatedVectorDrawable edgeAnimation, @NonNull Runnable onStartRunnable, boolean useGestureAnimationDelay)515 private void playFeedbackAnimation( 516 @NonNull Animator gestureAnimation, 517 @NonNull AnimatedVectorDrawable edgeAnimation, 518 @NonNull Runnable onStartRunnable, 519 boolean useGestureAnimationDelay) { 520 521 if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { 522 mFeedbackView.setVisibility(View.VISIBLE); 523 mAnimatedGestureDemonstration.setVisibility(View.VISIBLE); 524 mFullGestureDemonstration.setVisibility(View.VISIBLE); 525 mAnimatedGestureDemonstration.playAnimation(); 526 return; 527 } 528 529 if (gestureAnimation.isRunning()) { 530 gestureAnimation.cancel(); 531 } 532 if (edgeAnimation.isRunning()) { 533 edgeAnimation.reset(); 534 } 535 gestureAnimation.addListener(new AnimatorListenerAdapter() { 536 @Override 537 public void onAnimationStart(Animator animation) { 538 super.onAnimationStart(animation); 539 540 mEdgeGestureVideoView.setVisibility(GONE); 541 if (edgeAnimation.isRunning()) { 542 edgeAnimation.stop(); 543 } 544 545 if (!useGestureAnimationDelay) { 546 onStartRunnable.run(); 547 } 548 } 549 550 @Override 551 public void onAnimationEnd(Animator animation) { 552 super.onAnimationEnd(animation); 553 554 mEdgeGestureVideoView.setVisibility(View.VISIBLE); 555 edgeAnimation.start(); 556 557 gestureAnimation.removeListener(this); 558 } 559 }); 560 561 cancelQueuedGestureAnimation(); 562 if (useGestureAnimationDelay) { 563 mFeedbackViewCallback = onStartRunnable; 564 mFakeTaskViewCallback = gestureAnimation::start; 565 566 mFeedbackView.post(mFeedbackViewCallback); 567 mFakeTaskView.postDelayed(mFakeTaskViewCallback, GESTURE_ANIMATION_DELAY_MS); 568 } else { 569 gestureAnimation.start(); 570 } 571 } 572 setRippleHotspot(float x, float y)573 void setRippleHotspot(float x, float y) { 574 mRippleDrawable.setHotspot(x, y); 575 } 576 showRippleEffect(@ullable Runnable onCompleteRunnable)577 void showRippleEffect(@Nullable Runnable onCompleteRunnable) { 578 mRippleDrawable.setState( 579 new int[] {android.R.attr.state_pressed, android.R.attr.state_enabled}); 580 mRippleView.postDelayed(() -> { 581 mRippleDrawable.setState(new int[] {}); 582 if (onCompleteRunnable != null) { 583 onCompleteRunnable.run(); 584 } 585 }, RIPPLE_VISIBLE_MS); 586 } 587 onActionButtonClicked(View button)588 void onActionButtonClicked(View button) { 589 mTutorialFragment.continueTutorial(); 590 } 591 592 @CallSuper transitToController()593 void transitToController() { 594 updateCloseButton(); 595 updateSubtext(); 596 updateDrawables(); 597 updateLayout(); 598 599 if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { 600 mFeedbackTitleView.setTextAppearance(mContext, getTitleTextAppearance()); 601 mDoneButton.setTextAppearance(mContext, getDoneButtonTextAppearance()); 602 mDoneButton.getBackground().setTint(getDoneButtonColor()); 603 mCheckmarkAnimation.setAnimation(mTutorialFragment.isAtFinalStep() 604 ? R.raw.checkmark_animation_end 605 : R.raw.checkmark_animation_in_progress); 606 if (!isGestureCompleted()) { 607 mCheckmarkAnimation.setVisibility(GONE); 608 startGestureAnimation(); 609 if (mTutorialType == TutorialType.BACK_NAVIGATION) { 610 resetViewsForBackGesture(); 611 } 612 613 } 614 } else { 615 hideFeedback(); 616 hideActionButton(); 617 } 618 619 mGestureCompleted = false; 620 if (mFakeHotseatView != null) { 621 mFakeHotseatView.setVisibility(View.INVISIBLE); 622 } 623 } 624 resetViewsForBackGesture()625 protected void resetViewsForBackGesture() { 626 mFakeTaskView.setVisibility(View.VISIBLE); 627 mFakeTaskView.setBackgroundColor(getFakeTaskViewColor()); 628 mExitingAppView.setVisibility(View.VISIBLE); 629 630 // reset the exiting app's dimensions 631 mExitingAppRect.set(0, 0, (int) mScreenWidth, (int) mScreenHeight); 632 mExitingAppRadius = 0; 633 mExitingAppView.resetPivot(); 634 mExitingAppView.setScaleX(1f); 635 mExitingAppView.setScaleY(1f); 636 mExitingAppView.setTranslationX(0); 637 mExitingAppView.setTranslationY(0); 638 mExitingAppView.invalidateOutline(); 639 } 640 startGestureAnimation()641 private void startGestureAnimation() { 642 mAnimatedGestureDemonstration.setAnimation(getGestureLottieAnimationId()); 643 mAnimatedGestureDemonstration.playAnimation(); 644 } 645 updateCloseButton()646 void updateCloseButton() { 647 mSkipButton.setTextAppearance(Utilities.isDarkTheme(mContext) 648 ? R.style.TextAppearance_GestureTutorial_Feedback_Subtext 649 : R.style.TextAppearance_GestureTutorial_Feedback_Subtext_Dark); 650 } 651 hideActionButton()652 void hideActionButton() { 653 mSkipButton.setVisibility(View.VISIBLE); 654 // Invisible to maintain the layout. 655 mDoneButton.setVisibility(View.INVISIBLE); 656 mDoneButton.setOnClickListener(null); 657 } 658 showActionButton()659 void showActionButton() { 660 mSkipButton.setVisibility(GONE); 661 mDoneButton.setVisibility(View.VISIBLE); 662 mDoneButton.setOnClickListener(this::onActionButtonClicked); 663 } 664 hideFakeTaskbar(boolean animateToHotseat)665 void hideFakeTaskbar(boolean animateToHotseat) { 666 if (!mTutorialFragment.isLargeScreen() || mFakeTaskbarView == null) { 667 return; 668 } 669 if (mFakeTaskbarViewCallback != null) { 670 mFakeTaskbarView.removeCallbacks(mFakeTaskbarViewCallback); 671 } 672 if (animateToHotseat) { 673 mFakeTaskbarViewCallback = () -> 674 mFakeTaskbarView.animateDisappearanceToHotseat(mFakeHotseatView); 675 } 676 mFakeTaskbarView.post(mFakeTaskbarViewCallback); 677 } 678 showFakeTaskbar(boolean animateFromHotseat)679 void showFakeTaskbar(boolean animateFromHotseat) { 680 if (!mTutorialFragment.isLargeScreen() || mFakeTaskbarView == null) { 681 return; 682 } 683 if (mFakeTaskbarViewCallback != null) { 684 mFakeTaskbarView.removeCallbacks(mFakeTaskbarViewCallback); 685 } 686 if (animateFromHotseat) { 687 mFakeTaskbarViewCallback = () -> 688 mFakeTaskbarView.animateAppearanceFromHotseat(mFakeHotseatView); 689 } 690 mFakeTaskbarView.post(mFakeTaskbarViewCallback); 691 } 692 updateFakeAppTaskViewLayout(@ayoutRes int mockAppTaskLayoutResId)693 void updateFakeAppTaskViewLayout(@LayoutRes int mockAppTaskLayoutResId) { 694 updateFakeViewLayout(mFakeTaskView, mockAppTaskLayoutResId); 695 } 696 updateFakeViewLayout(ViewGroup view, @LayoutRes int mockLayoutResId)697 void updateFakeViewLayout(ViewGroup view, @LayoutRes int mockLayoutResId) { 698 view.removeAllViews(); 699 if (mockLayoutResId != NO_ID) { 700 view.addView( 701 inflate(mContext, mockLayoutResId, null), 702 new FrameLayout.LayoutParams( 703 ViewGroup.LayoutParams.MATCH_PARENT, 704 ViewGroup.LayoutParams.MATCH_PARENT)); 705 } 706 } 707 updateSubtext()708 private void updateSubtext() { 709 if (!ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { 710 mTutorialStepView.setTutorialProgress( 711 mTutorialFragment.getCurrentStep(), mTutorialFragment.getNumSteps()); 712 } 713 } 714 updateHotseatChildViewColor(@ullable View child)715 private void updateHotseatChildViewColor(@Nullable View child) { 716 if (child == null) return; 717 child.getBackground().setTint(getHotseatIconColor()); 718 } 719 updateDrawables()720 private void updateDrawables() { 721 if (mContext != null) { 722 mTutorialFragment.getRootView().setBackground(AppCompatResources.getDrawable( 723 mContext, getMockWallpaperResId())); 724 mTutorialFragment.updateFeedbackAnimation(); 725 mFakeLauncherView.setBackgroundColor(ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() 726 ? getFakeLauncherColor() 727 : mContext.getColor(R.color.gesture_tutorial_fake_wallpaper_color)); 728 updateFakeViewLayout(mFakeHotseatView, getMockHotseatResId()); 729 mHotseatIconView = mFakeHotseatView.findViewById(R.id.hotseat_icon_1); 730 mFakeTaskView.animate().alpha(1).setListener( 731 AnimatorListeners.forSuccessCallback(() -> mFakeTaskView.animate().cancel())); 732 mFakePreviousTaskView.setFakeTaskViewFillColor(getMockPreviousAppTaskThumbnailColor()); 733 mFakeIconView.setBackground(AppCompatResources.getDrawable( 734 mContext, getMockAppIconResId())); 735 736 if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { 737 mExitingAppView.setBackgroundColor(getExitingAppColor()); 738 mFakeTaskView.setBackgroundColor(getFakeTaskViewColor()); 739 updateHotseatChildViewColor(mHotseatIconView); 740 updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_2)); 741 updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_3)); 742 updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_4)); 743 updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_5)); 744 updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_6)); 745 updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_search_bar)); 746 } else { 747 updateFakeViewLayout(mFakeTaskView, getMockAppTaskLayoutResId()); 748 } 749 } 750 } 751 updateLayout()752 private void updateLayout() { 753 if (mContext == null) { 754 return; 755 } 756 RelativeLayout.LayoutParams feedbackLayoutParams = 757 (RelativeLayout.LayoutParams) mFeedbackView.getLayoutParams(); 758 feedbackLayoutParams.setMarginStart(mContext.getResources().getDimensionPixelSize( 759 mTutorialFragment.isLargeScreen() 760 ? R.dimen.gesture_tutorial_tablet_feedback_margin_start_end 761 : R.dimen.gesture_tutorial_feedback_margin_start_end)); 762 feedbackLayoutParams.setMarginEnd(mContext.getResources().getDimensionPixelSize( 763 mTutorialFragment.isLargeScreen() 764 ? R.dimen.gesture_tutorial_tablet_feedback_margin_start_end 765 : R.dimen.gesture_tutorial_feedback_margin_start_end)); 766 feedbackLayoutParams.topMargin = mContext.getResources().getDimensionPixelSize( 767 mTutorialFragment.isLargeScreen() 768 ? R.dimen.gesture_tutorial_tablet_feedback_margin_top 769 : R.dimen.gesture_tutorial_feedback_margin_top); 770 771 if (mFakeTaskbarView != null) { 772 mFakeTaskbarView.setVisibility( 773 mTutorialFragment.isLargeScreen() ? View.VISIBLE : GONE); 774 } 775 776 RelativeLayout.LayoutParams hotseatLayoutParams = 777 (RelativeLayout.LayoutParams) mFakeHotseatView.getLayoutParams(); 778 if (!mTutorialFragment.isLargeScreen()) { 779 DeviceProfile dp = mTutorialFragment.getDeviceProfile(); 780 dp.updateIsSeascape(mContext); 781 782 hotseatLayoutParams.addRule(dp.isLandscape 783 ? (dp.isSeascape() 784 ? RelativeLayout.ALIGN_PARENT_START 785 : RelativeLayout.ALIGN_PARENT_END) 786 : RelativeLayout.ALIGN_PARENT_BOTTOM); 787 } else { 788 hotseatLayoutParams.width = RelativeLayout.LayoutParams.MATCH_PARENT; 789 hotseatLayoutParams.height = RelativeLayout.LayoutParams.WRAP_CONTENT; 790 hotseatLayoutParams.addRule(RelativeLayout.ALIGN_PARENT_BOTTOM); 791 hotseatLayoutParams.removeRule(RelativeLayout.ALIGN_PARENT_START); 792 hotseatLayoutParams.removeRule(RelativeLayout.ALIGN_PARENT_END); 793 } 794 mFakeHotseatView.setLayoutParams(hotseatLayoutParams); 795 } 796 createSkipTutorialDialog()797 private AlertDialog createSkipTutorialDialog() { 798 if (!(mContext instanceof GestureSandboxActivity)) { 799 return null; 800 } 801 GestureSandboxActivity sandboxActivity = (GestureSandboxActivity) mContext; 802 View contentView = View.inflate( 803 sandboxActivity, R.layout.gesture_tutorial_dialog, null); 804 AlertDialog tutorialDialog = new AlertDialog 805 .Builder(sandboxActivity, R.style.Theme_AppCompat_Dialog_Alert) 806 .setView(contentView) 807 .create(); 808 809 PackageManager packageManager = mContext.getPackageManager(); 810 CharSequence tipsAppName = DEFAULT_PIXEL_TIPS_APP_NAME; 811 812 try { 813 tipsAppName = packageManager.getApplicationLabel( 814 packageManager.getApplicationInfo( 815 PIXEL_TIPS_APP_PACKAGE_NAME, PackageManager.GET_META_DATA)); 816 } catch (PackageManager.NameNotFoundException e) { 817 Log.e(LOG_TAG, 818 "Could not find app label for package name: " 819 + PIXEL_TIPS_APP_PACKAGE_NAME 820 + ". Defaulting to 'Pixel Tips.'", 821 e); 822 } 823 824 TextView subtitleTextView = (TextView) contentView.findViewById( 825 R.id.gesture_tutorial_dialog_subtitle); 826 if (subtitleTextView != null) { 827 subtitleTextView.setText( 828 mContext.getString(R.string.skip_tutorial_dialog_subtitle, tipsAppName)); 829 } else { 830 Log.w(LOG_TAG, "No subtitle view in the skip tutorial dialog to update."); 831 } 832 833 Button cancelButton = (Button) contentView.findViewById( 834 R.id.gesture_tutorial_dialog_cancel_button); 835 if (cancelButton != null) { 836 cancelButton.setOnClickListener( 837 v -> tutorialDialog.dismiss()); 838 } else { 839 Log.w(LOG_TAG, "No cancel button in the skip tutorial dialog to update."); 840 } 841 842 Button confirmButton = contentView.findViewById( 843 R.id.gesture_tutorial_dialog_confirm_button); 844 if (confirmButton != null) { 845 confirmButton.setOnClickListener(v -> { 846 mTutorialFragment.closeTutorialStep(true); 847 tutorialDialog.dismiss(); 848 }); 849 } else { 850 Log.w(LOG_TAG, "No confirm button in the skip tutorial dialog to update."); 851 } 852 853 tutorialDialog.getWindow().setBackgroundDrawable( 854 new ColorDrawable(sandboxActivity.getColor(android.R.color.transparent))); 855 856 return tutorialDialog; 857 } 858 createFingerDotAppearanceAnimatorSet()859 protected AnimatorSet createFingerDotAppearanceAnimatorSet() { 860 ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat( 861 mFingerDotView, View.ALPHA, 0f, FINGER_DOT_VISIBLE_ALPHA); 862 ObjectAnimator yScaleAnimator = ObjectAnimator.ofFloat( 863 mFingerDotView, View.SCALE_Y, FINGER_DOT_SMALL_SCALE, 1f); 864 ObjectAnimator xScaleAnimator = ObjectAnimator.ofFloat( 865 mFingerDotView, View.SCALE_X, FINGER_DOT_SMALL_SCALE, 1f); 866 ArrayList<Animator> animators = new ArrayList<>(); 867 868 animators.add(alphaAnimator); 869 animators.add(xScaleAnimator); 870 animators.add(yScaleAnimator); 871 872 AnimatorSet appearanceAnimatorSet = new AnimatorSet(); 873 874 appearanceAnimatorSet.playTogether(animators); 875 appearanceAnimatorSet.setDuration(FINGER_DOT_ANIMATION_DURATION_MILLIS); 876 877 return appearanceAnimatorSet; 878 } 879 createFingerDotDisappearanceAnimatorSet()880 protected AnimatorSet createFingerDotDisappearanceAnimatorSet() { 881 ObjectAnimator alphaAnimator = ObjectAnimator.ofFloat( 882 mFingerDotView, View.ALPHA, FINGER_DOT_VISIBLE_ALPHA, 0f); 883 ObjectAnimator yScaleAnimator = ObjectAnimator.ofFloat( 884 mFingerDotView, View.SCALE_Y, 1f, FINGER_DOT_SMALL_SCALE); 885 ObjectAnimator xScaleAnimator = ObjectAnimator.ofFloat( 886 mFingerDotView, View.SCALE_X, 1f, FINGER_DOT_SMALL_SCALE); 887 ArrayList<Animator> animators = new ArrayList<>(); 888 889 animators.add(alphaAnimator); 890 animators.add(xScaleAnimator); 891 animators.add(yScaleAnimator); 892 893 AnimatorSet appearanceAnimatorSet = new AnimatorSet(); 894 895 appearanceAnimatorSet.playTogether(animators); 896 appearanceAnimatorSet.setDuration(FINGER_DOT_ANIMATION_DURATION_MILLIS); 897 898 return appearanceAnimatorSet; 899 } 900 createAnimationPause()901 protected Animator createAnimationPause() { 902 return ValueAnimator.ofFloat(0f, 1f).setDuration(GESTURE_ANIMATION_PAUSE_DURATION_MILLIS); 903 } 904 pauseAndHideLottieAnimation()905 void pauseAndHideLottieAnimation() { 906 mAnimatedGestureDemonstration.pauseAnimation(); 907 mAnimatedGestureDemonstration.setVisibility(View.INVISIBLE); 908 mFullGestureDemonstration.setVisibility(View.INVISIBLE); 909 } 910 911 /** Denotes the type of the tutorial. */ 912 enum TutorialType { 913 BACK_NAVIGATION, 914 BACK_NAVIGATION_COMPLETE, 915 HOME_NAVIGATION, 916 HOME_NAVIGATION_COMPLETE, 917 OVERVIEW_NAVIGATION, 918 OVERVIEW_NAVIGATION_COMPLETE 919 } 920 } 921