diff --git a/quickstep/res/layout-sw600dp-land/gesture_tutorial_step_menu.xml b/quickstep/res/layout-sw600dp-land/gesture_tutorial_step_menu.xml index b13e8dc0ba..225b4cdc39 100644 --- a/quickstep/res/layout-sw600dp-land/gesture_tutorial_step_menu.xml +++ b/quickstep/res/layout-sw600dp-land/gesture_tutorial_step_menu.xml @@ -157,6 +157,7 @@ android:layout_marginVertical="16dp" android:text="@string/gesture_tutorial_action_button_label" android:background="@drawable/gesture_tutorial_action_button_background" + android:backgroundTint="?androidprv:attr/materialColorPrimary" android:stateListAnimator="@null" app:layout_constraintTop_toBottomOf="@id/guideline" diff --git a/quickstep/res/layout/gesture_tutorial_fragment.xml b/quickstep/res/layout/gesture_tutorial_fragment.xml index 3bd0df0349..64ad1f7f4e 100644 --- a/quickstep/res/layout/gesture_tutorial_fragment.xml +++ b/quickstep/res/layout/gesture_tutorial_fragment.xml @@ -175,6 +175,7 @@ android:paddingEnd="26dp" android:text="@string/gesture_tutorial_action_button_label" android:background="@drawable/gesture_tutorial_action_button_background" + android:backgroundTint="?android:attr/colorAccent" android:stateListAnimator="@null" android:visibility="invisible" diff --git a/quickstep/res/layout/gesture_tutorial_mock_task_view.xml b/quickstep/res/layout/gesture_tutorial_mock_task_view.xml deleted file mode 100644 index 047fdb1071..0000000000 --- a/quickstep/res/layout/gesture_tutorial_mock_task_view.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - \ No newline at end of file diff --git a/quickstep/res/layout/gesture_tutorial_step_menu.xml b/quickstep/res/layout/gesture_tutorial_step_menu.xml index 7a302f437b..cf78b1b016 100644 --- a/quickstep/res/layout/gesture_tutorial_step_menu.xml +++ b/quickstep/res/layout/gesture_tutorial_step_menu.xml @@ -156,6 +156,7 @@ android:layout_marginVertical="16dp" android:text="@string/gesture_tutorial_action_button_label" android:background="@drawable/gesture_tutorial_action_button_background" + android:backgroundTint="?androidprv:attr/materialColorPrimary" android:stateListAnimator="@null" app:layout_constraintTop_toBottomOf="@id/guideline" diff --git a/quickstep/res/layout/swipe_up_gesture_tutorial_shape.xml b/quickstep/res/layout/swipe_up_gesture_tutorial_shape.xml deleted file mode 100644 index d60e84f043..0000000000 --- a/quickstep/res/layout/swipe_up_gesture_tutorial_shape.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - diff --git a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java index 7bd8898dfc..3230c661d5 100644 --- a/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java +++ b/quickstep/src/com/android/launcher3/taskbar/KeyboardQuickSwitchViewController.java @@ -185,8 +185,8 @@ public class KeyboardQuickSwitchViewController { return false; } boolean traverseBackwards = (keyCode == KeyEvent.KEYCODE_TAB && event.isShiftPressed()) - || (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && !isRTL) - || (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && isRTL); + || (keyCode == KeyEvent.KEYCODE_DPAD_RIGHT && isRTL) + || (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && !isRTL); int taskCount = mControllerCallbacks.getTaskCount(); int toIndex = mCurrentFocusIndex == -1 // Focus the second-most recent app if possible diff --git a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java index 928910d3ab..f5202b7ebe 100644 --- a/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java +++ b/quickstep/src/com/android/quickstep/AbsSwipeUpHandler.java @@ -2260,6 +2260,7 @@ public abstract class AbsSwipeUpHandler, targetCompat.taskId == mGestureState.getLastStartedTaskId()) .findFirst(); if (!taskTargetOptional.isPresent()) { + ActiveGestureLog.INSTANCE.addLog("No appeared task matching started task id"); finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */); return; } @@ -2267,6 +2268,7 @@ public abstract class AbsSwipeUpHandler, TaskView taskView = mRecentsView == null ? null : mRecentsView.getTaskViewByTaskId(taskTarget.taskId); if (taskView == null || !taskView.getThumbnail().shouldShowSplashView()) { + ActiveGestureLog.INSTANCE.addLog("Invalid task view splash state"); finishRecentsAnimationOnTasksAppeared(null /* onFinishComplete */); return; } diff --git a/quickstep/src/com/android/quickstep/TaskAnimationManager.java b/quickstep/src/com/android/quickstep/TaskAnimationManager.java index 4c4b9b4095..410ba21866 100644 --- a/quickstep/src/com/android/quickstep/TaskAnimationManager.java +++ b/quickstep/src/com/android/quickstep/TaskAnimationManager.java @@ -19,6 +19,7 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static com.android.launcher3.util.Executors.MAIN_EXECUTOR; import static com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR; +import static com.android.quickstep.GestureState.GestureEndTarget.RECENTS; import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_INITIALIZED; import static com.android.quickstep.GestureState.STATE_RECENTS_ANIMATION_STARTED; import static com.android.quickstep.util.ActiveGestureErrorDetector.GestureEvent.START_RECENTS_ANIMATION; @@ -174,16 +175,21 @@ public class TaskAnimationManager implements RecentsAnimationCallbacks.RecentsAn if (nonAppTargets == null) { nonAppTargets = new RemoteAnimationTarget[0]; } - if (activityInterface.isInLiveTileMode() + if ((activityInterface.isInLiveTileMode() + || mLastGestureState.getEndTarget() == RECENTS) && activityInterface.getCreatedActivity() != null) { RecentsView recentsView = activityInterface.getCreatedActivity().getOverviewPanel(); if (recentsView != null) { + ActiveGestureLog.INSTANCE.addLog("Launching side task id=" + + appearedTaskTarget.taskId); recentsView.launchSideTaskInLiveTileMode(appearedTaskTarget.taskId, appearedTaskTargets, new RemoteAnimationTarget[0] /* wallpaper */, nonAppTargets /* nonApps */); return; + } else { + ActiveGestureLog.INSTANCE.addLog("Unable to launch side task (no recents)"); } } else if (nonAppTargets.length > 0) { TaskViewUtils.createSplitAuxiliarySurfacesAnimator(nonAppTargets /* nonApps */, diff --git a/quickstep/src/com/android/quickstep/interaction/AnimatedTaskView.java b/quickstep/src/com/android/quickstep/interaction/AnimatedTaskView.java index 44b3d625e6..742b0fc7e8 100644 --- a/quickstep/src/com/android/quickstep/interaction/AnimatedTaskView.java +++ b/quickstep/src/com/android/quickstep/interaction/AnimatedTaskView.java @@ -15,10 +15,6 @@ */ package com.android.quickstep.interaction; -import static com.android.launcher3.QuickstepTransitionManager.ANIMATION_NAV_FADE_OUT_DURATION; -import static com.android.launcher3.QuickstepTransitionManager.NAV_FADE_OUT_INTERPOLATOR; -import static com.android.launcher3.anim.Interpolators.FAST_OUT_SLOW_IN; -import static com.android.launcher3.anim.Interpolators.LINEAR; import static com.android.launcher3.config.FeatureFlags.ENABLE_NEW_GESTURE_NAV_TUTORIAL; import android.animation.Animator; @@ -28,30 +24,19 @@ import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.ColorInt; import android.content.Context; -import android.graphics.Matrix; import android.graphics.Outline; import android.graphics.Rect; import android.util.AttributeSet; -import android.util.Log; -import android.view.Display; -import android.view.RoundedCorner; import android.view.View; import android.view.ViewOutlineProvider; -import android.view.animation.ScaleAnimation; -import android.view.animation.TranslateAnimation; -import android.widget.ViewAnimator; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.constraintlayout.widget.ConstraintLayout; import com.android.launcher3.R; -import com.android.launcher3.Utilities; -import com.android.launcher3.anim.Interpolators; -import com.android.quickstep.util.MultiValueUpdateListener; import java.util.ArrayList; -import java.util.Arrays; /** * Helper View for the gesture tutorial mock previous app task view. @@ -61,8 +46,6 @@ import java.util.Arrays; */ public class AnimatedTaskView extends ConstraintLayout { - private static final long ANIMATE_TO_FULL_SCREEN_DURATION = 300; - private View mFullTaskView; private View mTopTaskView; private View mBottomTaskView; @@ -72,17 +55,16 @@ public class AnimatedTaskView extends ConstraintLayout { private float mTaskViewAnimatedRadius; public AnimatedTaskView(@NonNull Context context) { - super(context); + this(context, null); } - public AnimatedTaskView(@NonNull Context context, - @Nullable AttributeSet attrs) { - super(context, attrs); + public AnimatedTaskView(@NonNull Context context, @Nullable AttributeSet attrs) { + this(context, attrs, 0); } public AnimatedTaskView( @NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) { - super(context, attrs, defStyleAttr); + this(context, attrs, defStyleAttr, 0); } public AnimatedTaskView( @@ -104,97 +86,6 @@ public class AnimatedTaskView extends ConstraintLayout { setToSingleRowLayout(false); } - void animateToFillScreen(@Nullable Runnable onAnimationEndCallback) { - if (mTaskViewOutlineProvider == null) { - // This is an illegal state. - return; - } - // calculate start and end corner radius - Outline startOutline = new Outline(); - mTaskViewOutlineProvider.getOutline(this, startOutline); - Rect outlineStartRect = new Rect(); - startOutline.getRect(outlineStartRect); - float outlineStartRadius = startOutline.getRadius(); - - final Display display = mContext.getDisplay();; - RoundedCorner corner = display.getRoundedCorner(RoundedCorner.POSITION_TOP_LEFT); - float outlineEndRadius = corner.getRadius(); - - // create animation - AnimatorSet set = new AnimatorSet(); - ArrayList animations = new ArrayList<>(); - - // center view - animations.add(ObjectAnimator.ofFloat(this, TRANSLATION_X, 0)); - - // retrieve start animation matrix to scale off of - Matrix matrix = getAnimationMatrix(); - if (matrix == null) { - // This is an illegal state. - return; - } - - float[] matrixValues = new float[9]; - matrix.getValues(matrixValues); - float[] newValues = matrixValues.clone(); - - ValueAnimator transformAnimation = ValueAnimator.ofFloat(0, 1); - - MultiValueUpdateListener listener = new MultiValueUpdateListener() { - Matrix currentMatrix = new Matrix(); - - FloatProp mOutlineRadius = new FloatProp(outlineStartRadius, outlineEndRadius, 0, - ANIMATE_TO_FULL_SCREEN_DURATION, LINEAR); - FloatProp mTransX = new FloatProp(matrixValues[Matrix.MTRANS_X], 0f, 0, - ANIMATE_TO_FULL_SCREEN_DURATION, LINEAR); - FloatProp mTransY = new FloatProp(matrixValues[Matrix.MTRANS_Y], 0f, 0, - ANIMATE_TO_FULL_SCREEN_DURATION, LINEAR); - FloatProp mScaleX = new FloatProp(matrixValues[Matrix.MSCALE_X], 1f, 0, - ANIMATE_TO_FULL_SCREEN_DURATION, LINEAR); - FloatProp mScaleY = new FloatProp(matrixValues[Matrix.MSCALE_Y], 1f, 0, - ANIMATE_TO_FULL_SCREEN_DURATION, LINEAR); - - @Override - public void onUpdate(float percent, boolean initOnly) { - // scale corner radius to match display radius - mTaskViewAnimatedRadius = mOutlineRadius.value; - mFullTaskView.invalidateOutline(); - - // translate to center, ends at translation x:0, y:0 - newValues[Matrix.MTRANS_X] = mTransX.value; - newValues[Matrix.MTRANS_Y] = mTransY.value; - - // scale to full size, ends at scale 1 - newValues[Matrix.MSCALE_X] = mScaleX.value; - newValues[Matrix.MSCALE_Y] = mScaleY.value; - - // create and set new animation matrix - currentMatrix.setValues(newValues); - setAnimationMatrix(currentMatrix); - } - }; - - transformAnimation.addUpdateListener(listener); - animations.add(transformAnimation); - set.playSequentially(animations); - set.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationStart(Animator animation) { - super.onAnimationStart(animation); - addAnimatedOutlineProvider(mFullTaskView, outlineStartRect, outlineStartRadius); - } - - @Override - public void onAnimationEnd(Animator animation) { - super.onAnimationEnd(animation); - if (onAnimationEndCallback != null) { - onAnimationEndCallback.run(); - } - } - }); - set.start(); - } - AnimatorSet createAnimationToMultiRowLayout() { if (mTaskViewOutlineProvider == null) { // This is an illegal state. diff --git a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java index 17e57bf84b..54c441b59e 100644 --- a/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/BackGestureTutorialController.java @@ -136,7 +136,7 @@ final class BackGestureTutorialController extends TutorialController { } @Override - protected int getSwipeActionColor() { + protected int getFakeLauncherColor() { return mTutorialFragment.mRootView.mColorSurfaceContainer; } diff --git a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java index 0012d47057..891f20e560 100644 --- a/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/HomeGestureTutorialController.java @@ -104,11 +104,9 @@ final class HomeGestureTutorialController extends SwipeUpGestureTutorialControll @Override protected int getMockAppTaskLayoutResId() { - return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.layout.swipe_up_gesture_tutorial_shape - : mTutorialFragment.isLargeScreen() - ? R.layout.gesture_tutorial_tablet_mock_webpage - : R.layout.gesture_tutorial_mock_webpage; + return mTutorialFragment.isLargeScreen() + ? R.layout.gesture_tutorial_tablet_mock_webpage + : R.layout.gesture_tutorial_mock_webpage; } @Override @@ -121,7 +119,12 @@ final class HomeGestureTutorialController extends SwipeUpGestureTutorialControll } @Override - protected int getSwipeActionColor() { + protected int getFakeTaskViewColor() { + return isGestureCompleted() ? getFakeLauncherColor() : getExitingAppColor(); + } + + @Override + protected int getFakeLauncherColor() { return mTutorialFragment.mRootView.mColorSurfaceContainer; } @@ -148,7 +151,7 @@ final class HomeGestureTutorialController extends SwipeUpGestureTutorialControll case BACK_CANCELLED_FROM_LEFT: case BACK_CANCELLED_FROM_RIGHT: case BACK_NOT_STARTED_TOO_FAR_FROM_EDGE: - resetTaskView(); + resetTaskViews(); showFeedback(R.string.home_gesture_feedback_swipe_too_far_from_edge); break; } @@ -178,18 +181,18 @@ final class HomeGestureTutorialController extends SwipeUpGestureTutorialControll } case HOME_NOT_STARTED_TOO_FAR_FROM_EDGE: case OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE: - resetTaskView(); + resetTaskViews(); showFeedback(R.string.home_gesture_feedback_swipe_too_far_from_edge); break; case OVERVIEW_GESTURE_COMPLETED: - fadeOutFakeTaskView(true, true, () -> { + fadeOutFakeTaskView(true, () -> { showFeedback(R.string.home_gesture_feedback_overview_detected); showFakeTaskbar(/* animateFromHotseat= */ false); }); break; case HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION: case HOME_OR_OVERVIEW_CANCELLED: - fadeOutFakeTaskView(false, true, null); + fadeOutFakeTaskView(false, null); showFeedback(R.string.home_gesture_feedback_wrong_swipe_direction); break; } diff --git a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java index 593e6d9a9f..667fe4dfad 100644 --- a/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/OverviewGestureTutorialController.java @@ -25,7 +25,9 @@ import android.annotation.TargetApi; import android.graphics.PointF; import android.os.Build; import android.os.Handler; -import android.view.View; + +import androidx.annotation.ColorInt; +import androidx.core.graphics.ColorUtils; import com.android.launcher3.R; import com.android.launcher3.Utilities; @@ -43,6 +45,8 @@ import java.util.Map; @TargetApi(Build.VERSION_CODES.R) final class OverviewGestureTutorialController extends SwipeUpGestureTutorialController { + private static final float LAUNCHER_COLOR_BLENDING_RATIO = 0.4f; + OverviewGestureTutorialController(OverviewGestureTutorialFragment fragment, TutorialType tutorialType) { super(fragment, tutorialType); @@ -112,11 +116,9 @@ final class OverviewGestureTutorialController extends SwipeUpGestureTutorialCont @Override protected int getMockAppTaskLayoutResId() { - return ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? R.layout.gesture_tutorial_mock_task_view - : mTutorialFragment.isLargeScreen() - ? R.layout.gesture_tutorial_tablet_mock_conversation_list - : R.layout.gesture_tutorial_mock_conversation_list; + return mTutorialFragment.isLargeScreen() + ? R.layout.gesture_tutorial_tablet_mock_conversation_list + : R.layout.gesture_tutorial_mock_conversation_list; } @Override @@ -128,11 +130,31 @@ final class OverviewGestureTutorialController extends SwipeUpGestureTutorialCont : R.raw.overview_gesture_tutorial_animation; } - @Override - protected int getSwipeActionColor() { + @ColorInt + private int getFakeTaskViewStartColor() { return mTutorialFragment.mRootView.mColorSurfaceOverview; } + @ColorInt + private int getFakeTaskViewEndColor() { + return getMockPreviousAppTaskThumbnailColor(); + } + + @Override + protected int getFakeTaskViewColor() { + return isGestureCompleted() + ? getFakeTaskViewEndColor() + : getFakeTaskViewStartColor(); + } + + @Override + protected int getFakeLauncherColor() { + return ColorUtils.blendARGB( + mTutorialFragment.mRootView.mColorSurfaceContainer, + mTutorialFragment.mRootView.mColorOnSurfaceOverview, + LAUNCHER_COLOR_BLENDING_RATIO); + } + @Override protected int getHotseatIconColor() { return mTutorialFragment.mRootView.mColorOnSurfaceOverview; @@ -159,7 +181,7 @@ final class OverviewGestureTutorialController extends SwipeUpGestureTutorialCont case BACK_CANCELLED_FROM_LEFT: case BACK_CANCELLED_FROM_RIGHT: case BACK_NOT_STARTED_TOO_FAR_FROM_EDGE: - resetTaskView(); + resetTaskViews(); showFeedback(R.string.overview_gesture_feedback_swipe_too_far_from_edge); break; } @@ -190,7 +212,7 @@ final class OverviewGestureTutorialController extends SwipeUpGestureTutorialCont } case HOME_NOT_STARTED_TOO_FAR_FROM_EDGE: case OVERVIEW_NOT_STARTED_TOO_FAR_FROM_EDGE: - resetTaskView(); + resetTaskViews(); showFeedback(R.string.overview_gesture_feedback_swipe_too_far_from_edge); break; case OVERVIEW_GESTURE_COMPLETED: @@ -204,7 +226,7 @@ final class OverviewGestureTutorialController extends SwipeUpGestureTutorialCont break; case HOME_OR_OVERVIEW_NOT_STARTED_WRONG_SWIPE_DIRECTION: case HOME_OR_OVERVIEW_CANCELLED: - fadeOutFakeTaskView(false, true, null); + fadeOutFakeTaskView(false, null); showFeedback(R.string.overview_gesture_feedback_wrong_swipe_direction); break; } @@ -229,15 +251,21 @@ final class OverviewGestureTutorialController extends SwipeUpGestureTutorialCont anim.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animator) { - new Handler().postDelayed(() -> { - mFakeTaskView.setVisibility(View.INVISIBLE); - if (!mTutorialFragment.isLargeScreen()) { - mFakePreviousTaskView.animateToFillScreen( - () -> onSuccessAnimationComplete()); - } else { - onSuccessAnimationComplete(); - } - }, TASK_VIEW_FILL_SCREEN_ANIMATION_DELAY_MILLIS); + new Handler().postDelayed( + () -> fadeOutFakeTaskView( + /* toOverviewFirst= */ true, + /* animatePreviousTask= */ false, + /* resetViews= */ false, + /* updateListener= */ v -> mFakeTaskView.setBackgroundColor( + ColorUtils.blendARGB( + getFakeTaskViewStartColor(), + getFakeTaskViewEndColor(), + v.getAnimatedFraction())), + /* onEndRunnable= */ () -> { + showSuccessFeedback(); + resetTaskViews(); + }), + TASK_VIEW_FILL_SCREEN_ANIMATION_DELAY_MILLIS); } }); } @@ -259,9 +287,4 @@ final class OverviewGestureTutorialController extends SwipeUpGestureTutorialCont animset.start(); mRunningWindowAnim = SwipeUpAnimationLogic.RunningWindowAnim.wrap(animset); } - - private void onSuccessAnimationComplete() { - mFakeLauncherView.setBackgroundColor(getMockPreviousAppTaskThumbnailColor()); - showSuccessFeedback(); - } } diff --git a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java index 66c659aa2f..0bbf373023 100644 --- a/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/SwipeUpGestureTutorialController.java @@ -78,7 +78,7 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { private final AnimatorListenerAdapter mResetTaskView = new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - resetTaskView(); + resetTaskViews(); } }; @@ -122,7 +122,7 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { mRunningWindowAnim = null; } - void resetTaskView() { + void resetTaskViews() { mFakeHotseatView.setVisibility(View.INVISIBLE); mFakeIconView.setVisibility(View.INVISIBLE); if (mTutorialFragment.getActivity() != null) { @@ -141,10 +141,21 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { mShowPreviousTasks = false; mRunningWindowAnim = null; } + void fadeOutFakeTaskView(boolean toOverviewFirst, @Nullable Runnable onEndRunnable) { + fadeOutFakeTaskView( + toOverviewFirst, + /* animatePreviousTask= */ true, + /* resetViews= */ true, + /* updateListener= */ null, + onEndRunnable); + } /** Fades the task view, optionally after animating to a fake Overview. */ - void fadeOutFakeTaskView(boolean toOverviewFirst, boolean reset, - @Nullable Runnable onEndRunnable) { + void fadeOutFakeTaskView(boolean toOverviewFirst, + boolean animatePreviousTask, + boolean resetViews, + @Nullable ValueAnimator.AnimatorUpdateListener updateListener, + @Nullable Runnable onEndRunnable) { cancelRunningAnimation(); PendingAnimation anim = new PendingAnimation(300); if (toOverviewFirst) { @@ -155,20 +166,20 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { public void onAnimationEnd(Animator animation, boolean isReverse) { PendingAnimation fadeAnim = new PendingAnimation(TASK_VIEW_END_ANIMATION_DURATION_MILLIS); - if (reset) { - fadeAnim.setFloat(mTaskViewSwipeUpAnimation - .getCurrentShift(), AnimatedFloat.VALUE, 0, ACCEL); + fadeAnim.setFloat(mTaskViewSwipeUpAnimation + .getCurrentShift(), AnimatedFloat.VALUE, 0, ACCEL); + if (resetViews) { fadeAnim.addListener(mResetTaskView); - } else { - fadeAnim.setViewAlpha(mFakeTaskView, 0, ACCEL); - fadeAnim.setViewAlpha(mFakePreviousTaskView, 0, ACCEL); } if (onEndRunnable != null) { fadeAnim.addListener(AnimatorListeners.forSuccessCallback(onEndRunnable)); } + if (updateListener != null) { + fadeAnim.addOnFrameListener(updateListener); + } AnimatorSet animset = fadeAnim.buildAnim(); - if (reset && mTutorialFragment.isLargeScreen()) { + if (animatePreviousTask && mTutorialFragment.isLargeScreen()) { animset.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationStart(Animator animation) { @@ -190,13 +201,10 @@ abstract class SwipeUpGestureTutorialController extends TutorialController { } }); } else { - if (reset) { - anim.setFloat(mTaskViewSwipeUpAnimation - .getCurrentShift(), AnimatedFloat.VALUE, 0, ACCEL); + anim.setFloat(mTaskViewSwipeUpAnimation + .getCurrentShift(), AnimatedFloat.VALUE, 0, ACCEL); + if (resetViews) { anim.addListener(mResetTaskView); - } else { - anim.setViewAlpha(mFakeTaskView, 0, ACCEL); - anim.setViewAlpha(mFakePreviousTaskView, 0, ACCEL); } if (onEndRunnable != null) { anim.addListener(AnimatorListeners.forSuccessCallback(onEndRunnable)); diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialController.java b/quickstep/src/com/android/quickstep/interaction/TutorialController.java index 36655d2b01..084f8c1298 100644 --- a/quickstep/src/com/android/quickstep/interaction/TutorialController.java +++ b/quickstep/src/com/android/quickstep/interaction/TutorialController.java @@ -268,7 +268,12 @@ abstract class TutorialController implements BackGestureAttemptCallback, } @ColorInt - protected abstract int getSwipeActionColor(); + protected int getFakeTaskViewColor() { + return Color.TRANSPARENT; + } + + @ColorInt + protected abstract int getFakeLauncherColor(); @ColorInt protected int getExitingAppColor() { @@ -445,6 +450,7 @@ abstract class TutorialController implements BackGestureAttemptCallback, } private void showSuccessPage() { + pauseAndHideLottieAnimation(); mCheckmarkAnimation.setVisibility(View.VISIBLE); mCheckmarkAnimation.playAnimation(); mFeedbackTitleView.setTextAppearance(mContext, getSuccessTitleTextAppearance()); @@ -591,7 +597,7 @@ abstract class TutorialController implements BackGestureAttemptCallback, protected void resetViewsForBackGesture() { mFakeTaskView.setVisibility(View.VISIBLE); - mFakeTaskView.setBackgroundColor(getSwipeActionColor()); + mFakeTaskView.setBackgroundColor(getFakeTaskViewColor()); mExitingAppView.setVisibility(View.VISIBLE); // reset the exiting app's dimensions @@ -690,11 +696,10 @@ abstract class TutorialController implements BackGestureAttemptCallback, mContext, getMockWallpaperResId())); mTutorialFragment.updateFeedbackAnimation(); mFakeLauncherView.setBackgroundColor(ENABLE_NEW_GESTURE_NAV_TUTORIAL.get() - ? getSwipeActionColor() + ? getFakeLauncherColor() : mContext.getColor(R.color.gesture_tutorial_fake_wallpaper_color)); updateFakeViewLayout(mFakeHotseatView, getMockHotseatResId()); mHotseatIconView = mFakeHotseatView.findViewById(R.id.hotseat_icon_1); - updateFakeViewLayout(mFakeTaskView, getMockAppTaskLayoutResId()); mFakeTaskView.animate().alpha(1).setListener( AnimatorListeners.forSuccessCallback(() -> mFakeTaskView.animate().cancel())); mFakePreviousTaskView.setFakeTaskViewFillColor(getMockPreviousAppTaskThumbnailColor()); @@ -703,12 +708,15 @@ abstract class TutorialController implements BackGestureAttemptCallback, if (ENABLE_NEW_GESTURE_NAV_TUTORIAL.get()) { mExitingAppView.setBackgroundColor(getExitingAppColor()); + mFakeTaskView.setBackgroundColor(getFakeTaskViewColor()); updateHotseatChildViewColor(mFakeIconView); updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_2)); updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_3)); updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_4)); updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_icon_5)); updateHotseatChildViewColor(mFakeHotseatView.findViewById(R.id.hotseat_search_bar)); + } else { + updateFakeViewLayout(mFakeTaskView, getMockAppTaskLayoutResId()); } } } diff --git a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java index 9f15e19ce4..bfad64308c 100644 --- a/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java +++ b/quickstep/src/com/android/quickstep/interaction/TutorialFragment.java @@ -143,6 +143,7 @@ abstract class TutorialFragment extends GestureSandboxFragment implements OnTouc return null; } + @NonNull abstract TutorialController createController(TutorialType type); abstract Class getControllerClass(); @@ -374,9 +375,15 @@ abstract class TutorialFragment extends GestureSandboxFragment implements OnTouc void changeController(TutorialType tutorialType) { if (getControllerClass().isInstance(mTutorialController)) { mTutorialController.setTutorialType(tutorialType); + if (isGestureComplete()) { + mTutorialController.setGestureCompleted(); + } mTutorialController.fadeTaskViewAndRun(mTutorialController::transitToController); } else { mTutorialController = createController(tutorialType); + if (isGestureComplete()) { + mTutorialController.setGestureCompleted(); + } mTutorialController.transitToController(); } mEdgeBackGestureHandler.registerBackGestureAttemptCallback(mTutorialController); diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt b/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt index 614dfe82d6..8e6415b303 100644 --- a/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt +++ b/quickstep/src/com/android/quickstep/util/SplitSelectDataHolder.kt @@ -61,7 +61,8 @@ class SplitSelectDataHolder( */ companion object { @IntDef(SPLIT_TASK_TASK, SPLIT_TASK_PENDINGINTENT, SPLIT_TASK_SHORTCUT, - SPLIT_PENDINGINTENT_TASK, SPLIT_PENDINGINTENT_PENDINGINTENT, SPLIT_SHORTCUT_TASK) + SPLIT_PENDINGINTENT_TASK, SPLIT_PENDINGINTENT_PENDINGINTENT, SPLIT_SHORTCUT_TASK, + SPLIT_SINGLE_TASK_FULLSCREEN, SPLIT_SINGLE_INTENT_FULLSCREEN) @Retention(AnnotationRetention.SOURCE) annotation class SplitLaunchType @@ -71,6 +72,10 @@ class SplitSelectDataHolder( const val SPLIT_PENDINGINTENT_TASK = 3 const val SPLIT_SHORTCUT_TASK = 4 const val SPLIT_PENDINGINTENT_PENDINGINTENT = 5 + + // Non-split edge case of launching the initial selected task as a fullscreen task + const val SPLIT_SINGLE_TASK_FULLSCREEN = 6 + const val SPLIT_SINGLE_INTENT_FULLSCREEN = 7 } @@ -190,7 +195,7 @@ class SplitSelectDataHolder( /** * @return [SplitLaunchData] with the necessary fields populated as determined by - * [SplitLaunchData.splitLaunchType] + * [SplitLaunchData.splitLaunchType]. This is to be used for launching splitscreen */ fun getSplitLaunchData() : SplitLaunchData { // Convert all intents to shortcut infos to see if determine if we launch shortcut or intent @@ -201,6 +206,24 @@ class SplitSelectDataHolder( initialStagePosition = getOppositeStagePosition(initialStagePosition) } + return generateSplitLaunchData(splitLaunchType) + } + + /** + * @return [SplitLaunchData] with the necessary fields populated as determined by + * [SplitLaunchData.splitLaunchType]. This is to be used for launching an initially selected + * split task in fullscreen + */ + fun getFullscreenLaunchData() : SplitLaunchData { + // Convert all intents to shortcut infos to see if determine if we launch shortcut or intent + convertIntentsToFinalTypes() + val splitLaunchType = if (initialTaskId != INVALID_TASK_ID) SPLIT_SINGLE_TASK_FULLSCREEN + else SPLIT_SINGLE_INTENT_FULLSCREEN + + return generateSplitLaunchData(splitLaunchType) + } + + private fun generateSplitLaunchData(@SplitLaunchType splitLaunchType: Int) : SplitLaunchData { return SplitLaunchData( splitLaunchType, initialTaskId, diff --git a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java index ee51af7a5d..ec8be89ecb 100644 --- a/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java +++ b/quickstep/src/com/android/quickstep/util/SplitSelectStateController.java @@ -27,6 +27,8 @@ import static com.android.launcher3.util.SplitConfigurationOptions.getOppositeSt import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_PENDINGINTENT_PENDINGINTENT; import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_PENDINGINTENT_TASK; import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_SHORTCUT_TASK; +import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_SINGLE_INTENT_FULLSCREEN; +import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_SINGLE_TASK_FULLSCREEN; import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_PENDINGINTENT; import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_SHORTCUT; import static com.android.quickstep.util.SplitSelectDataHolder.SPLIT_TASK_TASK; @@ -324,11 +326,8 @@ public class SplitSelectStateController { } boolean hasSecondaryPendingIntent = mSecondPendingIntent != null; if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) { - final RemoteSplitLaunchTransitionRunner animationRunner = - new RemoteSplitLaunchTransitionRunner(taskId1, taskId2, callback); - final RemoteTransition remoteTransition = new RemoteTransition(animationRunner, - ActivityThread.currentActivityThread().getApplicationThread(), - "LaunchSplitPair"); + final RemoteTransition remoteTransition = getShellRemoteTransition(taskId1, taskId2, + callback); if (intent1 == null && (intent2 == null && !hasSecondaryPendingIntent)) { mSystemUiProxy.startTasks(taskId1, options1.toBundle(), taskId2, null /* options2 */, stagePosition, splitRatio, remoteTransition, @@ -351,11 +350,8 @@ public class SplitSelectStateController { shellInstanceId); } } else { - final RemoteSplitLaunchAnimationRunner animationRunner = - new RemoteSplitLaunchAnimationRunner(taskId1, taskId2, callback); - final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter( - animationRunner, 300, 150, - ActivityThread.currentActivityThread().getApplicationThread()); + final RemoteAnimationAdapter adapter = getLegacyRemoteAdapter(taskId1, taskId2, + callback); if (intent1 == null && (intent2 == null && !hasSecondaryPendingIntent)) { mSystemUiProxy.startTasksWithLegacyTransition(taskId1, options1.toBundle(), @@ -402,11 +398,8 @@ public class SplitSelectStateController { Bundle optionsBundle = options1.toBundle(); if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) { - final RemoteSplitLaunchTransitionRunner animationRunner = - new RemoteSplitLaunchTransitionRunner(firstTaskId, secondTaskId, callback); - final RemoteTransition remoteTransition = new RemoteTransition(animationRunner, - ActivityThread.currentActivityThread().getApplicationThread(), - "LaunchSplitPair"); + final RemoteTransition remoteTransition = getShellRemoteTransition(firstTaskId, + secondTaskId, callback); switch (launchData.getSplitLaunchType()) { case SPLIT_TASK_TASK -> mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId, @@ -440,11 +433,8 @@ public class SplitSelectStateController { remoteTransition, shellInstanceId); } } else { - final RemoteSplitLaunchAnimationRunner animationRunner = - new RemoteSplitLaunchAnimationRunner(firstTaskId, secondTaskId, callback); - final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter( - animationRunner, 300, 150, - ActivityThread.currentActivityThread().getApplicationThread()); + final RemoteAnimationAdapter adapter = getLegacyRemoteAdapter(firstTaskId, secondTaskId, + callback); switch (launchData.getSplitLaunchType()) { case SPLIT_TASK_TASK -> mSystemUiProxy.startTasksWithLegacyTransition(firstTaskId, optionsBundle, @@ -501,26 +491,90 @@ public class SplitSelectStateController { Bundle optionsBundle = options1.toBundle(); if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) { - final RemoteSplitLaunchTransitionRunner animationRunner = - new RemoteSplitLaunchTransitionRunner(firstTaskId, secondTaskId, callback); - final RemoteTransition remoteTransition = new RemoteTransition(animationRunner, - ActivityThread.currentActivityThread().getApplicationThread(), - "LaunchSplitPair"); + final RemoteTransition remoteTransition = getShellRemoteTransition(firstTaskId, + secondTaskId, callback); mSystemUiProxy.startTasks(firstTaskId, optionsBundle, secondTaskId, null /* options2 */, stagePosition, splitRatio, remoteTransition, null /*shellInstanceId*/); } else { - final RemoteSplitLaunchAnimationRunner animationRunner = - new RemoteSplitLaunchAnimationRunner(firstTaskId, secondTaskId, callback); - final RemoteAnimationAdapter adapter = new RemoteAnimationAdapter( - animationRunner, 300, 150, - ActivityThread.currentActivityThread().getApplicationThread()); + final RemoteAnimationAdapter adapter = getLegacyRemoteAdapter(firstTaskId, + secondTaskId, callback); mSystemUiProxy.startTasksWithLegacyTransition(firstTaskId, optionsBundle, secondTaskId, null /* options2 */, stagePosition, splitRatio, adapter, null /*shellInstanceId*/); } } + /** + * Launches the initially selected task/intent in fullscreen (note the same SystemUi APIs are + * used as {@link #launchSplitTasks(Consumer)} because they are overloaded to launch both + * split and fullscreen tasks) + */ + public void launchInitialAppFullscreen(Consumer callback) { + if (!FeatureFlags.ENABLE_SPLIT_LAUNCH_DATA_REFACTOR.get()) { + launchSplitTasks(callback); + return; + } + + final ActivityOptions options1 = ActivityOptions.makeBasic(); + SplitSelectDataHolder.SplitLaunchData launchData = + mSplitSelectDataHolder.getFullscreenLaunchData(); + int firstTaskId = launchData.getInitialTaskId(); + int secondTaskId = launchData.getSecondTaskId(); + PendingIntent firstPI = launchData.getInitialPendingIntent(); + int firstUserId = launchData.getInitialUserId(); + int initialStagePosition = launchData.getInitialStagePosition(); + Bundle optionsBundle = options1.toBundle(); + + final RemoteSplitLaunchTransitionRunner animationRunner = + new RemoteSplitLaunchTransitionRunner(firstTaskId, secondTaskId, callback); + final RemoteTransition remoteTransition = new RemoteTransition(animationRunner, + ActivityThread.currentActivityThread().getApplicationThread(), + "LaunchSplitPair"); + Pair instanceIds = + LogUtils.getShellShareableInstanceId(); + if (TaskAnimationManager.ENABLE_SHELL_TRANSITIONS) { + switch (launchData.getSplitLaunchType()) { + case SPLIT_SINGLE_TASK_FULLSCREEN -> mSystemUiProxy.startTasks(firstTaskId, + optionsBundle, secondTaskId, null /* options2 */, initialStagePosition, + DEFAULT_SPLIT_RATIO, remoteTransition, instanceIds.first); + case SPLIT_SINGLE_INTENT_FULLSCREEN -> mSystemUiProxy.startIntentAndTask(firstPI, + firstUserId, optionsBundle, secondTaskId, null /*options2*/, + initialStagePosition, DEFAULT_SPLIT_RATIO, remoteTransition, + instanceIds.first); + } + } else { + final RemoteAnimationAdapter adapter = getLegacyRemoteAdapter(firstTaskId, + secondTaskId, callback); + switch (launchData.getSplitLaunchType()) { + case SPLIT_SINGLE_TASK_FULLSCREEN -> mSystemUiProxy.startTasksWithLegacyTransition( + firstTaskId, optionsBundle, secondTaskId, null /* options2 */, + initialStagePosition, DEFAULT_SPLIT_RATIO, adapter, instanceIds.first); + case SPLIT_SINGLE_INTENT_FULLSCREEN -> + mSystemUiProxy.startIntentAndTaskWithLegacyTransition(firstPI, firstUserId, + optionsBundle, secondTaskId, null /*options2*/, + initialStagePosition, DEFAULT_SPLIT_RATIO, adapter, + instanceIds.first); + } + } + } + + private RemoteTransition getShellRemoteTransition(int firstTaskId, int secondTaskId, + Consumer callback) { + final RemoteSplitLaunchTransitionRunner animationRunner = + new RemoteSplitLaunchTransitionRunner(firstTaskId, secondTaskId, callback); + return new RemoteTransition(animationRunner, + ActivityThread.currentActivityThread().getApplicationThread(), "LaunchSplitPair"); + } + + private RemoteAnimationAdapter getLegacyRemoteAdapter(int firstTaskId, int secondTaskId, + Consumer callback) { + final RemoteSplitLaunchAnimationRunner animationRunner = + new RemoteSplitLaunchAnimationRunner(firstTaskId, secondTaskId, callback); + return new RemoteAnimationAdapter(animationRunner, 300, 150, + ActivityThread.currentActivityThread().getApplicationThread()); + } + private void launchIntentOrShortcut(Intent intent, UserHandle user, ActivityOptions options1, int taskId, @StagePosition int stagePosition, float splitRatio, RemoteTransition remoteTransition, @Nullable InstanceId shellInstanceId) { diff --git a/quickstep/src/com/android/quickstep/views/RecentsView.java b/quickstep/src/com/android/quickstep/views/RecentsView.java index d8fe32db85..f740d9c082 100644 --- a/quickstep/src/com/android/quickstep/views/RecentsView.java +++ b/quickstep/src/com/android/quickstep/views/RecentsView.java @@ -3260,7 +3260,7 @@ public abstract class RecentsView - mSplitSelectStateController.launchSplitTasks(launchSuccess -> + mSplitSelectStateController.launchInitialAppFullscreen(launchSuccess -> resetFromSplitSelectionState())); pendingAnimation.buildAnim().start(); diff --git a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java index 97e34c5f10..dbe4402812 100644 --- a/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java +++ b/quickstep/tests/src/com/android/quickstep/FallbackRecentsTest.java @@ -116,12 +116,11 @@ public class FallbackRecentsTest { Utilities.enableRunningInTestHarnessForTests(); } - final ViewCaptureRule viewCaptureRule = new ViewCaptureRule(); mOrderSensitiveRules = RuleChain .outerRule(new SamplerRule()) .around(new NavigationModeSwitchRule(mLauncher)) - .around(viewCaptureRule) - .around(new FailureWatcher(mDevice, mLauncher, viewCaptureRule.getViewCapture())); + .around(new ViewCaptureRule()) + .around(new FailureWatcher(mDevice, mLauncher)); mOtherLauncherActivity = context.getPackageManager().queryIntentActivities( getHomeIntentInPackage(context), diff --git a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java index d7c4ae3857..5bd28d85a3 100644 --- a/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java +++ b/tests/src/com/android/launcher3/ui/AbstractLauncherUiTest.java @@ -216,11 +216,10 @@ public abstract class AbstractLauncherUiTest { } protected TestRule getRulesInsideActivityMonitor() { - final ViewCaptureRule viewCaptureRule = new ViewCaptureRule(); final RuleChain inner = RuleChain .outerRule(new PortraitLandscapeRunner(this)) - .around(viewCaptureRule) - .around(new FailureWatcher(mDevice, mLauncher, viewCaptureRule.getViewCapture())); + .around(new ViewCaptureRule()) + .around(new FailureWatcher(mDevice, mLauncher)); return TestHelpers.isInLauncherProcess() ? RuleChain.outerRule(ShellCommandRule.setDefaultLauncher()).around(inner) diff --git a/tests/src/com/android/launcher3/util/rule/FailureWatcher.java b/tests/src/com/android/launcher3/util/rule/FailureWatcher.java index 7ca6a06ed2..6b11fd6af4 100644 --- a/tests/src/com/android/launcher3/util/rule/FailureWatcher.java +++ b/tests/src/com/android/launcher3/util/rule/FailureWatcher.java @@ -6,12 +6,8 @@ import android.os.FileUtils; import android.os.ParcelFileDescriptor.AutoCloseInputStream; import android.util.Log; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.test.core.app.ApplicationProvider; import androidx.test.uiautomator.UiDevice; -import com.android.app.viewcapture.ViewCapture; import com.android.launcher3.tapl.LauncherInstrumentation; import com.android.launcher3.ui.AbstractLauncherUiTest; @@ -32,14 +28,10 @@ public class FailureWatcher extends TestWatcher { private static boolean sSavedBugreport = false; final private UiDevice mDevice; private final LauncherInstrumentation mLauncher; - @NonNull - private final ViewCapture mViewCapture; - public FailureWatcher(UiDevice device, LauncherInstrumentation launcher, - @NonNull ViewCapture viewCapture) { + public FailureWatcher(UiDevice device, LauncherInstrumentation launcher) { mDevice = device; mLauncher = launcher; - mViewCapture = viewCapture; } @Override @@ -71,7 +63,7 @@ public class FailureWatcher extends TestWatcher { @Override protected void failed(Throwable e, Description description) { - onError(mLauncher, description, e, mViewCapture); + onError(mLauncher, description, e); } static File diagFile(Description description, String prefix, String ext) { @@ -82,12 +74,6 @@ public class FailureWatcher extends TestWatcher { public static void onError(LauncherInstrumentation launcher, Description description, Throwable e) { - onError(launcher, description, e, null); - } - - private static void onError(LauncherInstrumentation launcher, Description description, - Throwable e, @Nullable ViewCapture viewCapture) { - final File sceenshot = diagFile(description, "TestScreenshot", "png"); final File hierarchy = diagFile(description, "Hierarchy", "zip"); @@ -102,12 +88,6 @@ public class FailureWatcher extends TestWatcher { out.putNextEntry(new ZipEntry("visible_windows.zip")); dumpCommand("cmd window dump-visible-window-views", out); out.closeEntry(); - - if (viewCapture != null) { - out.putNextEntry(new ZipEntry("FS/data/misc/wmtrace/failed_test.vc")); - viewCapture.dumpTo(out, ApplicationProvider.getApplicationContext()); - out.closeEntry(); - } } catch (Exception ignored) { } diff --git a/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt b/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt index 0c6553998d..f3fff35c90 100644 --- a/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt +++ b/tests/src/com/android/launcher3/util/rule/ViewCaptureRule.kt @@ -19,62 +19,101 @@ import android.app.Activity import android.app.Application import android.media.permission.SafeCloseable import android.os.Bundle +import android.util.Log +import androidx.annotation.AnyThread import androidx.test.core.app.ApplicationProvider import com.android.app.viewcapture.SimpleViewCapture import com.android.app.viewcapture.ViewCapture.MAIN_EXECUTOR import com.android.launcher3.util.ActivityLifecycleCallbacksAdapter -import org.junit.rules.TestRule +import java.io.File +import java.io.FileOutputStream +import java.util.zip.ZipEntry +import java.util.zip.ZipOutputStream +import org.junit.rules.TestWatcher import org.junit.runner.Description import org.junit.runners.model.Statement +private const val TAG = "ViewCaptureRule" + /** * This JUnit TestRule registers a listener for activity lifecycle events to attach a ViewCapture * instance that other test rules use to dump the timelapse hierarchy upon an error during a test. * * This rule will not work in OOP tests that don't have access to the activity under test. */ -class ViewCaptureRule : TestRule { - val viewCapture = SimpleViewCapture("test-view-capture") +class ViewCaptureRule : TestWatcher() { + private val viewCapture = SimpleViewCapture("test-view-capture") + private val windowListenerCloseables = mutableListOf() override fun apply(base: Statement, description: Description): Statement { + val testWatcherStatement = super.apply(base, description) + return object : Statement() { override fun evaluate() { - val windowListenerCloseables = mutableListOf() - - val lifecycleCallbacks = - object : ActivityLifecycleCallbacksAdapter { - override fun onActivityCreated(activity: Activity, bundle: Bundle?) { - super.onActivityCreated(activity, bundle) - windowListenerCloseables.add( - viewCapture.startCapture( - activity.window.decorView, - "${description.testClass?.simpleName}.${description.methodName}" - ) - ) - } - - override fun onActivityDestroyed(activity: Activity) { - super.onActivityDestroyed(activity) - viewCapture.stopCapture(activity.window.decorView) - } + val lifecycleCallbacks = createLifecycleCallbacks(description) + with(ApplicationProvider.getApplicationContext()) { + registerActivityLifecycleCallbacks(lifecycleCallbacks) + try { + testWatcherStatement.evaluate() + } finally { + unregisterActivityLifecycleCallbacks(lifecycleCallbacks) } - - val application = ApplicationProvider.getApplicationContext() - application.registerActivityLifecycleCallbacks(lifecycleCallbacks) - - try { - base.evaluate() - } finally { - application.unregisterActivityLifecycleCallbacks(lifecycleCallbacks) - - // Clean up ViewCapture references here rather than in onActivityDestroyed so - // test code can access view hierarchy capture. onActivityDestroyed would delete - // view capture data before FailureWatcher could output it as a test artifact. - // This is on the main thread to avoid a race condition where the onDrawListener - // is removed while onDraw is running, resulting in an IllegalStateException. - MAIN_EXECUTOR.execute { windowListenerCloseables.onEach(SafeCloseable::close) } } } } } + + private fun createLifecycleCallbacks(description: Description) = + object : ActivityLifecycleCallbacksAdapter { + override fun onActivityCreated(activity: Activity, bundle: Bundle?) { + super.onActivityCreated(activity, bundle) + windowListenerCloseables.add( + viewCapture.startCapture( + activity.window.decorView, + "${description.testClass?.simpleName}.${description.methodName}" + ) + ) + } + + override fun onActivityDestroyed(activity: Activity) { + super.onActivityDestroyed(activity) + viewCapture.stopCapture(activity.window.decorView) + } + } + + override fun succeeded(description: Description) = cleanup() + + /** If the test fails, this function will output the ViewCapture information. */ + override fun failed(e: Throwable, description: Description) { + super.failed(e, description) + + val testName = "${description.testClass.simpleName}.${description.methodName}" + val application: Application = ApplicationProvider.getApplicationContext() + val zip = File(application.filesDir, "ViewCapture-$testName.zip") + + ZipOutputStream(FileOutputStream(zip)).use { + it.putNextEntry(ZipEntry("FS/data/misc/wmtrace/failed_test.vc")) + viewCapture.dumpTo(it, ApplicationProvider.getApplicationContext()) + it.closeEntry() + } + cleanup() + + Log.d( + TAG, + "Failed $testName due to ${e::class.java.simpleName}.\n" + + "\tUse go/web-hv to open dump file: \n\t\t${zip.absolutePath}" + ) + } + + /** + * Clean up ViewCapture references can't happen in onActivityDestroyed otherwise view + * hierarchies would be erased before they could be outputted. + * + * This is on the main thread to avoid a race condition where the onDrawListener is removed + * while onDraw is running, resulting in an IllegalStateException. + */ + @AnyThread + private fun cleanup() { + MAIN_EXECUTOR.execute { windowListenerCloseables.onEach(SafeCloseable::close) } + } }