diff --git a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt index 57b0a09a90..1d7c11bf37 100644 --- a/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt +++ b/quickstep/src/com/android/quickstep/util/SplitAnimationController.kt @@ -23,6 +23,7 @@ import android.animation.AnimatorSet import android.animation.ObjectAnimator import android.animation.ValueAnimator import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW import android.content.Context import android.graphics.Bitmap @@ -51,6 +52,7 @@ import com.android.launcher3.anim.PendingAnimation import com.android.launcher3.apppairs.AppPairIcon import com.android.launcher3.config.FeatureFlags import com.android.launcher3.logging.StatsLogManager.EventEnum +import com.android.launcher3.model.data.WorkspaceItemInfo import com.android.launcher3.statehandlers.DepthController import com.android.launcher3.statemanager.StateManager import com.android.launcher3.statemanager.StatefulActivity @@ -69,6 +71,7 @@ import com.android.quickstep.views.TaskThumbnailView import com.android.quickstep.views.TaskView import com.android.quickstep.views.TaskView.TaskIdAttributeContainer import com.android.quickstep.views.TaskViewIcon +import com.android.wm.shell.shared.TransitionUtil import java.util.Optional import java.util.function.Supplier @@ -532,8 +535,14 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC check(info != null && t != null) { "trying to launch an app pair icon, but encountered an unexpected null" } - - composeIconSplitLaunchAnimator(launchingIconView, info, t, finishCallback) + val appPairLaunchingAppIndex = hasChangesForBothAppPairs(launchingIconView, info) + if (appPairLaunchingAppIndex == -1) { + // Launch split app pair animation + composeIconSplitLaunchAnimator(launchingIconView, info, t, finishCallback) + } else { + composeFullscreenIconSplitLaunchAnimator(launchingIconView, info, t, + finishCallback, appPairLaunchingAppIndex) + } } else { // Fallback case: simple fade-in animation check(info != null && t != null) { @@ -597,6 +606,39 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC ) } + /** + * @return -1 if [transitionInfo] contains both apps of the app pair to be animated, otherwise + * the integer index corresponding to [launchingIconView]'s contents for the single app + * to be animated + */ + fun hasChangesForBothAppPairs(launchingIconView: AppPairIcon, + transitionInfo: TransitionInfo) : Int { + val intent1 = launchingIconView.info.getFirstApp().intent.component?.packageName + val intent2 = launchingIconView.info.getSecondApp().intent.component?.packageName + var launchFullscreenAppIndex = -1 + for (change in transitionInfo.changes) { + val taskInfo: RunningTaskInfo = change.taskInfo ?: continue + if (TransitionUtil.isOpeningType(change.mode) && + taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN) { + val baseIntent = taskInfo.baseIntent.component?.packageName + if (baseIntent == intent1) { + if (launchFullscreenAppIndex > -1) { + launchFullscreenAppIndex = -1 + break + } + launchFullscreenAppIndex = 0 + } else if (baseIntent == intent2) { + if (launchFullscreenAppIndex > -1) { + launchFullscreenAppIndex = -1 + break + } + launchFullscreenAppIndex = 1 + } + } + } + return launchFullscreenAppIndex + } + /** * When the user taps an app pair icon to launch split, this will play the tasks' launch * animation from the position of the icon. @@ -632,7 +674,8 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC // If launching an app pair from Taskbar inside of an app context (no access to Launcher), // use the scale-up animation if (launchingIconView.context is TaskbarActivityContext) { - composeScaleUpLaunchAnimation(transitionInfo, t, finishCallback) + composeScaleUpLaunchAnimation(transitionInfo, t, finishCallback, + WINDOWING_MODE_MULTI_WINDOW) return } @@ -642,11 +685,6 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC // Create an AnimatorSet that will run both shell and launcher transitions together val launchAnimation = AnimatorSet() - val progressUpdater = ValueAnimator.ofFloat(0f, 1f) - val timings = AnimUtils.getDeviceAppPairLaunchTimings(dp.isTablet) - progressUpdater.setDuration(timings.getDuration().toLong()) - progressUpdater.interpolator = Interpolators.LINEAR - var rootCandidate: Change? = null for (change in transitionInfo.changes) { @@ -690,27 +728,13 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC // Make sure nothing weird happened, like getChange() returning null. check(rootCandidate != null) { "Failed to find a root leash" } - // Shell animation: the apps are revealed toward end of the launch animation - progressUpdater.addUpdateListener { valueAnimator: ValueAnimator -> - val progress = - Interpolators.clampToProgress( - Interpolators.LINEAR, - valueAnimator.animatedFraction, - timings.appRevealStartOffset, - timings.appRevealEndOffset - ) - - // Set the alpha of the shell layer (2 apps + divider) - t.setAlpha(rootCandidate.leash, progress) - t.apply() - } - // Create a new floating view in Launcher, positioned above the launching icon val drawableArea = launchingIconView.iconDrawableArea val appIcon1 = launchingIconView.info.getFirstApp().newIcon(launchingIconView.context) val appIcon2 = launchingIconView.info.getSecondApp().newIcon(launchingIconView.context) appIcon1.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx) appIcon2.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx) + val floatingView = FloatingAppPairView.getFloatingAppPairView( launcher, @@ -721,84 +745,189 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC ) floatingView.bringToFront() - // Launcher animation: animate the floating view, expanding to fill the display surface - progressUpdater.addUpdateListener( - object : MultiValueUpdateListener() { - var mDx = - FloatProp( - floatingView.startingPosition.left, - dp.widthPx / 2f - floatingView.startingPosition.width() / 2f, - Interpolators.clampToProgress( - timings.getStagedRectXInterpolator(), - timings.stagedRectSlideStartOffset, - timings.stagedRectSlideEndOffset - ) - ) - var mDy = - FloatProp( - floatingView.startingPosition.top, - dp.heightPx / 2f - floatingView.startingPosition.height() / 2f, - Interpolators.clampToProgress( - Interpolators.EMPHASIZED, - timings.stagedRectSlideStartOffset, - timings.stagedRectSlideEndOffset - ) - ) - var mScaleX = - FloatProp( - 1f /* start */, - dp.widthPx / floatingView.startingPosition.width(), - Interpolators.clampToProgress( - Interpolators.EMPHASIZED, - timings.stagedRectSlideStartOffset, - timings.stagedRectSlideEndOffset - ) - ) - var mScaleY = - FloatProp( - 1f /* start */, - dp.heightPx / floatingView.startingPosition.height(), - Interpolators.clampToProgress( - Interpolators.EMPHASIZED, - timings.stagedRectSlideStartOffset, - timings.stagedRectSlideEndOffset - ) - ) - - override fun onUpdate(percent: Float, initOnly: Boolean) { - floatingView.progress = percent - floatingView.x = mDx.value - floatingView.y = mDy.value - floatingView.scaleX = mScaleX.value - floatingView.scaleY = mScaleY.value - floatingView.invalidate() - } - } - ) - - // When animation ends, remove the floating view and run finishCallback - progressUpdater.addListener( - object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - safeRemoveViewFromDragLayer(launcher, floatingView) - finishCallback.run() - } - } - ) - - launchAnimation.play(progressUpdater) + launchAnimation.play( + getIconLaunchValueAnimator(t, dp, finishCallback, launcher, floatingView, + rootCandidate)) launchAnimation.start() } + /** + * Similar to [composeIconSplitLaunchAnimator], but instructs [FloatingAppPairView] to animate + * a single fullscreen icon + background instead of for a pair + */ + @VisibleForTesting + fun composeFullscreenIconSplitLaunchAnimator( + launchingIconView: AppPairIcon, + transitionInfo: TransitionInfo, + t: Transaction, + finishCallback: Runnable, + launchFullscreenIndex: Int + ) { + // If launching an app pair from Taskbar inside of an app context (no access to Launcher), + // use the scale-up animation + if (launchingIconView.context is TaskbarActivityContext) { + composeScaleUpLaunchAnimation(transitionInfo, t, finishCallback, + WINDOWING_MODE_FULLSCREEN) + return + } + + // Else we are in Launcher and can launch with the full icon stretch-and-split animation. + val launcher = Launcher.getLauncher(launchingIconView.context) + val dp = launcher.deviceProfile + + // Create an AnimatorSet that will run both shell and launcher transitions together + val launchAnimation = AnimatorSet() + + val appInfo = launchingIconView.info + .getContents()[launchFullscreenIndex] as WorkspaceItemInfo + val intentToLaunch = appInfo.intent.component?.packageName + var rootCandidate: Change? = null + for (change in transitionInfo.changes) { + val taskInfo: RunningTaskInfo = change.taskInfo ?: continue + val baseIntent = taskInfo.baseIntent.component?.packageName + if (TransitionUtil.isOpeningType(change.mode) && + taskInfo.windowingMode == WINDOWING_MODE_FULLSCREEN && + baseIntent == intentToLaunch) { + rootCandidate = change + } + } + + // If we could not find a proper root candidate, something went wrong. + check(rootCandidate != null) { "Could not find a split root candidate" } + + // Recurse up the tree until parent is null, then we've found our root. + var parentToken: WindowContainerToken? = rootCandidate.parent + while (parentToken != null) { + rootCandidate = transitionInfo.getChange(parentToken) ?: break + parentToken = rootCandidate.parent + } + + // Make sure nothing weird happened, like getChange() returning null. + check(rootCandidate != null) { "Failed to find a root leash" } + + // Create a new floating view in Launcher, positioned above the launching icon + val drawableArea = launchingIconView.iconDrawableArea + val appIcon = appInfo.newIcon(launchingIconView.context) + appIcon.setBounds(0, 0, dp.iconSizePx, dp.iconSizePx) + + val floatingView = + FloatingAppPairView.getFloatingAppPairView( + launcher, + drawableArea, + appIcon, + null /*appIcon2*/, + 0 /*dividerPos*/ + ) + floatingView.bringToFront() + launchAnimation.play( + getIconLaunchValueAnimator(t, dp, finishCallback, launcher, floatingView, + rootCandidate)) + launchAnimation.start() + } + + private fun getIconLaunchValueAnimator(t: Transaction, + dp: com.android.launcher3.DeviceProfile, + finishCallback: Runnable, + launcher: Launcher, + floatingView: FloatingAppPairView, + rootCandidate: Change) : ValueAnimator { + val progressUpdater = ValueAnimator.ofFloat(0f, 1f) + val timings = AnimUtils.getDeviceAppPairLaunchTimings(dp.isTablet) + progressUpdater.setDuration(timings.getDuration().toLong()) + progressUpdater.interpolator = Interpolators.LINEAR + + // Shell animation: the apps are revealed toward end of the launch animation + progressUpdater.addUpdateListener { valueAnimator: ValueAnimator -> + val progress = + Interpolators.clampToProgress( + Interpolators.LINEAR, + valueAnimator.animatedFraction, + timings.appRevealStartOffset, + timings.appRevealEndOffset + ) + + // Set the alpha of the shell layer (2 apps + divider) + t.setAlpha(rootCandidate.leash, progress) + t.apply() + } + + progressUpdater.addUpdateListener( + object : MultiValueUpdateListener() { + var mDx = + FloatProp( + floatingView.startingPosition.left, + dp.widthPx / 2f - floatingView.startingPosition.width() / 2f, + Interpolators.clampToProgress( + timings.getStagedRectXInterpolator(), + timings.stagedRectSlideStartOffset, + timings.stagedRectSlideEndOffset + ) + ) + var mDy = + FloatProp( + floatingView.startingPosition.top, + dp.heightPx / 2f - floatingView.startingPosition.height() / 2f, + Interpolators.clampToProgress( + Interpolators.EMPHASIZED, + timings.stagedRectSlideStartOffset, + timings.stagedRectSlideEndOffset + ) + ) + var mScaleX = + FloatProp( + 1f /* start */, + dp.widthPx / floatingView.startingPosition.width(), + Interpolators.clampToProgress( + Interpolators.EMPHASIZED, + timings.stagedRectSlideStartOffset, + timings.stagedRectSlideEndOffset + ) + ) + var mScaleY = + FloatProp( + 1f /* start */, + dp.heightPx / floatingView.startingPosition.height(), + Interpolators.clampToProgress( + Interpolators.EMPHASIZED, + timings.stagedRectSlideStartOffset, + timings.stagedRectSlideEndOffset + ) + ) + + override fun onUpdate(percent: Float, initOnly: Boolean) { + floatingView.progress = percent + floatingView.x = mDx.value + floatingView.y = mDy.value + floatingView.scaleX = mScaleX.value + floatingView.scaleY = mScaleY.value + floatingView.invalidate() + } + } + ) + progressUpdater.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + safeRemoveViewFromDragLayer(launcher, floatingView) + finishCallback.run() + } + } + ) + + return progressUpdater + } + /** * This is a scale-up-and-fade-in animation (34% to 100%) for launching an app in Overview when * there is no visible associated tile to expand from. + * [windowingMode] helps determine whether we are looking for a split or a single fullscreen + * [Change] */ @VisibleForTesting fun composeScaleUpLaunchAnimation( transitionInfo: TransitionInfo, t: Transaction, - finishCallback: Runnable + finishCallback: Runnable, + windowingMode: Int ) { val launchAnimation = AnimatorSet() val progressUpdater = ValueAnimator.ofFloat(0f, 1f) @@ -812,9 +941,8 @@ class SplitAnimationController(val splitSelectStateController: SplitSelectStateC // TODO (b/316490565): Replace this logic when SplitBounds is available to // startAnimation() and we can know the precise taskIds of launching tasks. - // Find a change that has WINDOWING_MODE_MULTI_WINDOW. if ( - taskInfo.windowingMode == WINDOWING_MODE_MULTI_WINDOW && + taskInfo.windowingMode == windowingMode && (change.mode == TRANSIT_OPEN || change.mode == TRANSIT_TO_FRONT) ) { // Found one! diff --git a/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt index 1c1e1674e5..40937b3ed9 100644 --- a/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt +++ b/quickstep/src/com/android/quickstep/views/FloatingAppPairBackground.kt @@ -37,17 +37,18 @@ import com.android.systemui.shared.system.QuickStepContract * animation. Consists of a rectangular background that splits into two, and two app icons that * increase in size during the animation. */ -class FloatingAppPairBackground( - context: Context, - private val floatingView: FloatingAppPairView, // the view that we will draw this background on - private val appIcon1: Drawable, - private val appIcon2: Drawable, - dividerPos: Int +open class FloatingAppPairBackground( + context: Context, + // the view that we will draw this background on + protected val floatingView: FloatingAppPairView, + private val appIcon1: Drawable, + private val appIcon2: Drawable?, + dividerPos: Int ) : Drawable() { companion object { // Design specs -- app icons start small and expand during the animation - private val STARTING_ICON_SIZE_PX = Utilities.dpToPx(22f) - private val ENDING_ICON_SIZE_PX = Utilities.dpToPx(66f) + internal val STARTING_ICON_SIZE_PX = Utilities.dpToPx(22f) + internal val ENDING_ICON_SIZE_PX = Utilities.dpToPx(66f) // Null values to use with drawDoubleRoundRect(), since there doesn't seem to be any other // API for drawing rectangles with 4 different corner radii. @@ -59,13 +60,13 @@ class FloatingAppPairBackground( private val backgroundPaint = Paint(Paint.ANTI_ALIAS_FLAG) // Animation interpolators - private val expandXInterpolator: Interpolator - private val expandYInterpolator: Interpolator + protected val expandXInterpolator: Interpolator + protected val expandYInterpolator: Interpolator private val cellSplitInterpolator: Interpolator - private val iconFadeInterpolator: Interpolator + protected val iconFadeInterpolator: Interpolator // Device-specific measurements - private val deviceCornerRadius: Float + protected val deviceCornerRadius: Float private val deviceHalfDividerSize: Float private val desiredSplitRatio: Float @@ -217,7 +218,7 @@ class FloatingAppPairBackground( canvas.save() canvas.translate(changingIcon2Left, changingIconTop) canvas.scale(changingIconScaleX, changingIconScaleY) - appIcon2.alpha = changingIconAlpha + appIcon2!!.alpha = changingIconAlpha appIcon2.draw(canvas) canvas.restore() } @@ -317,7 +318,7 @@ class FloatingAppPairBackground( canvas.save() canvas.translate(changingIconLeft, changingIcon2Top) canvas.scale(changingIconScaleX, changingIconScaleY) - appIcon2.alpha = changingIconAlpha + appIcon2!!.alpha = changingIconAlpha appIcon2.draw(canvas) canvas.restore() } @@ -330,7 +331,7 @@ class FloatingAppPairBackground( * @param radii An array of 8 radii for the corners: top left x, top left y, top right x, top * right y, bottom right x, and so on. */ - private fun drawCustomRoundedRect(c: Canvas, rect: RectF, radii: FloatArray) { + protected fun drawCustomRoundedRect(c: Canvas, rect: RectF, radii: FloatArray) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { // Canvas.drawDoubleRoundRect is supported from Q onward c.drawDoubleRoundRect(rect, radii, EMPTY_RECT, ARRAY_OF_ZEROES, backgroundPaint) diff --git a/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt b/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt index e90aa13d97..e8d1cc1e36 100644 --- a/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt +++ b/quickstep/src/com/android/quickstep/views/FloatingAppPairView.kt @@ -40,8 +40,8 @@ class FloatingAppPairView @JvmOverloads constructor(context: Context, attrs: Att fun getFloatingAppPairView( launcher: StatefulActivity<*>, originalView: View, - appIcon1: Drawable, - appIcon2: Drawable, + appIcon1: Drawable?, + appIcon2: Drawable?, dividerPos: Int ): FloatingAppPairView { val dragLayer: ViewGroup = launcher.getDragLayer() @@ -64,8 +64,8 @@ class FloatingAppPairView @JvmOverloads constructor(context: Context, attrs: Att fun init( launcher: StatefulActivity<*>, originalView: View, - appIcon1: Drawable, - appIcon2: Drawable, + appIcon1: Drawable?, + appIcon2: Drawable?, dividerPos: Int ) { val viewBounds = Rect(0, 0, originalView.width, originalView.height) @@ -92,7 +92,14 @@ class FloatingAppPairView @JvmOverloads constructor(context: Context, attrs: Att layoutParams = lp // Prepare to draw app pair icon background - background = FloatingAppPairBackground(context, this, appIcon1, appIcon2, dividerPos) + background = if (appIcon1 == null || appIcon2 == null) { + val iconToAnimate = appIcon1 ?: appIcon2 + checkNotNull(iconToAnimate) + FloatingFullscreenAppPairBackground(context, this, iconToAnimate, + dividerPos) + } else { + FloatingAppPairBackground(context, this, appIcon1, appIcon2, dividerPos) + } background.setBounds(0, 0, lp.width, lp.height) } diff --git a/quickstep/src/com/android/quickstep/views/FloatingFullscreenAppPairBackground.kt b/quickstep/src/com/android/quickstep/views/FloatingFullscreenAppPairBackground.kt new file mode 100644 index 0000000000..8cd997fcaa --- /dev/null +++ b/quickstep/src/com/android/quickstep/views/FloatingFullscreenAppPairBackground.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.quickstep.views + +import android.content.Context +import android.graphics.Canvas +import android.graphics.RectF +import android.graphics.drawable.Drawable + +class FloatingFullscreenAppPairBackground( + context: Context, + floatingView: FloatingAppPairView, + private val iconToLaunch: Drawable, + dividerPos: Int) : + FloatingAppPairBackground( + context, + floatingView, + iconToLaunch, + null /*appIcon2*/, + dividerPos +) { + + /** Animates the background as if launching a fullscreen task. */ + override fun draw(canvas: Canvas) { + val progress = floatingView.progress + + // Since the entire floating app pair surface is scaling up during this animation, we + // scale down most of these drawn elements so that they appear the proper size on-screen. + val scaleFactorX = floatingView.scaleX + val scaleFactorY = floatingView.scaleY + + // Get the bounds where we will draw the background image + val width = bounds.width().toFloat() + val height = bounds.height().toFloat() + + // Get device-specific measurements + val cornerRadiusX = deviceCornerRadius / scaleFactorX + val cornerRadiusY = deviceCornerRadius / scaleFactorY + + // Draw background + drawCustomRoundedRect( + canvas, + RectF(0f, 0f, width, height), + floatArrayOf( + cornerRadiusX, + cornerRadiusY, + cornerRadiusX, + cornerRadiusY, + cornerRadiusX, + cornerRadiusY, + cornerRadiusX, + cornerRadiusY, + ) + ) + + // Calculate changing measurements for icon. + val changingIconSizeX = + (STARTING_ICON_SIZE_PX + + ((ENDING_ICON_SIZE_PX - STARTING_ICON_SIZE_PX) * + expandXInterpolator.getInterpolation(progress))) / scaleFactorX + val changingIconSizeY = + (STARTING_ICON_SIZE_PX + + ((ENDING_ICON_SIZE_PX - STARTING_ICON_SIZE_PX) * + expandYInterpolator.getInterpolation(progress))) / scaleFactorY + + val changingIcon1Left = (width / 2f) - (changingIconSizeX / 2f) + val changingIconTop = (height / 2f) - (changingIconSizeY / 2f) + val changingIconScaleX = changingIconSizeX / iconToLaunch.bounds.width() + val changingIconScaleY = changingIconSizeY / iconToLaunch.bounds.height() + val changingIconAlpha = + (255 - (255 * iconFadeInterpolator.getInterpolation(progress))).toInt() + + // Draw icon + canvas.save() + canvas.translate(changingIcon1Left, changingIconTop) + canvas.scale(changingIconScaleX, changingIconScaleY) + iconToLaunch.alpha = changingIconAlpha + iconToLaunch.draw(canvas) + canvas.restore() + } +} \ No newline at end of file diff --git a/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt b/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt index 68c9bf9799..4bed7a0bf9 100644 --- a/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt +++ b/quickstep/tests/src/com/android/quickstep/util/SplitAnimationControllerTest.kt @@ -17,6 +17,8 @@ package com.android.quickstep.util +import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN +import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.view.ContextThemeWrapper @@ -39,8 +41,10 @@ import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.eq import org.mockito.kotlin.any import org.mockito.kotlin.doNothing +import org.mockito.kotlin.doReturn import org.mockito.kotlin.mock import org.mockito.kotlin.spy import org.mockito.kotlin.verify @@ -255,6 +259,9 @@ class SplitAnimationControllerTest { doNothing() .whenever(spySplitAnimationController) .composeIconSplitLaunchAnimator(any(), any(), any(), any()) + doReturn(-1) + .whenever(spySplitAnimationController) + .hasChangesForBothAppPairs(any(), any()) spySplitAnimationController.playSplitLaunchAnimation( null /* launchingTaskView */, @@ -276,13 +283,45 @@ class SplitAnimationControllerTest { } @Test - fun playsAppropriateSplitLaunchAnimation_playsIconLaunchFromTaskbarContextCorrectly() { + fun playsAppropriateSplitLaunchAnimation_playsIconFullscreenLaunchCorrectly() { + val spySplitAnimationController = spy(splitAnimationController) + whenever(mockAppPairIcon.context).thenReturn(mockContextThemeWrapper) + doNothing() + .whenever(spySplitAnimationController) + .composeFullscreenIconSplitLaunchAnimator(any(), any(), any(), any(), any()) + doReturn(0) + .whenever(spySplitAnimationController) + .hasChangesForBothAppPairs(any(), any()) + + spySplitAnimationController.playSplitLaunchAnimation( + null /* launchingTaskView */, + mockAppPairIcon, + taskId, + taskId2, + null /* apps */, + null /* wallpapers */, + null /* nonApps */, + stateManager, + depthController, + transitionInfo, + transaction, + {} /* finishCallback */ + ) + + verify(spySplitAnimationController) + .composeFullscreenIconSplitLaunchAnimator(any(), any(), any(), any(), eq(0)) + } + + @Test + fun playsAppropriateSplitLaunchAnimation_playsIconLaunchFromTaskbarCMultiWindow() { val spySplitAnimationController = spy(splitAnimationController) whenever(mockAppPairIcon.context).thenReturn(mockTaskbarActivityContext) doNothing() .whenever(spySplitAnimationController) - .composeScaleUpLaunchAnimation(any(), any(), any()) - + .composeScaleUpLaunchAnimation(any(), any(), any(), any()) + doReturn(-1) + .whenever(spySplitAnimationController) + .hasChangesForBothAppPairs(any(), any()) spySplitAnimationController.playSplitLaunchAnimation( null /* launchingTaskView */, mockAppPairIcon, @@ -298,7 +337,37 @@ class SplitAnimationControllerTest { {} /* finishCallback */ ) - verify(spySplitAnimationController).composeScaleUpLaunchAnimation(any(), any(), any()) + verify(spySplitAnimationController).composeScaleUpLaunchAnimation(any(), any(), any(), + eq(WINDOWING_MODE_MULTI_WINDOW)) + } + + @Test + fun playsAppropriateSplitLaunchAnimation_playsIconLaunchFromTaskbarFullscreen() { + val spySplitAnimationController = spy(splitAnimationController) + whenever(mockAppPairIcon.context).thenReturn(mockTaskbarActivityContext) + doNothing() + .whenever(spySplitAnimationController) + .composeScaleUpLaunchAnimation(any(), any(), any(), any()) + doReturn(0) + .whenever(spySplitAnimationController) + .hasChangesForBothAppPairs(any(), any()) + spySplitAnimationController.playSplitLaunchAnimation( + null /* launchingTaskView */, + mockAppPairIcon, + taskId, + taskId2, + null /* apps */, + null /* wallpapers */, + null /* nonApps */, + stateManager, + depthController, + transitionInfo, + transaction, + {} /* finishCallback */ + ) + + verify(spySplitAnimationController).composeScaleUpLaunchAnimation(any(), any(), any(), + eq(WINDOWING_MODE_FULLSCREEN)) } @Test