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