/*
* Copyright (C) 2025 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.launcher3.desktop
import android.animation.Animator
import android.animation.AnimatorSet
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Rect
import android.util.Log
import android.view.Choreographer
import android.view.SurfaceControl.Transaction
import android.view.WindowManager.TRANSIT_CLOSE
import android.view.WindowManager.TRANSIT_OPEN
import android.view.WindowManager.TRANSIT_TO_BACK
import android.window.DesktopModeFlags
import android.window.TransitionInfo
import android.window.TransitionInfo.Change
import androidx.core.animation.addListener
import androidx.core.util.Supplier
import com.android.app.animation.Interpolators
import com.android.internal.jank.Cuj
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.policy.ScreenDecorationsUtils
import com.android.launcher3.desktop.DesktopAppLaunchTransition.AppLaunchType
import com.android.launcher3.desktop.DesktopAppLaunchTransition.Companion.LAUNCH_CHANGE_MODES
import com.android.wm.shell.shared.animation.MinimizeAnimator
import com.android.wm.shell.shared.animation.WindowAnimator
/**
* Helper class responsible for creating and managing animators for desktop app launch and related
* transitions.
*
*
This class handles the complex logic of creating various animators, including launch,
* minimize, and trampoline close animations, based on the provided transition information and
* launch type. It also utilizes {@link InteractionJankMonitor} to monitor animation jank.
*
* @param context The application context.
* @param launchType The type of app launch, containing animation parameters.
* @param cujType The CUJ (Critical User Journey) type for jank monitoring.
*/
class DesktopAppLaunchAnimatorHelper(
private val context: Context,
private val launchType: AppLaunchType,
@Cuj.CujType private val cujType: Int,
private val transactionSupplier: Supplier,
) {
private val interactionJankMonitor = InteractionJankMonitor.getInstance()
fun createAnimators(info: TransitionInfo, finishCallback: (Animator) -> Unit): List {
val launchChange = getLaunchChange(info)
if (launchChange == null) {
val tasksInfo =
info.changes.joinToString(", ") { change ->
"${change.taskInfo?.taskId}:${change.taskInfo?.isFreeform}"
}
Log.e(TAG, "No launch change found: Transition info=$info, tasks state=$tasksInfo")
return emptyList()
}
val transaction = transactionSupplier.get()
val minimizeChange = getMinimizeChange(info)
val trampolineCloseChange = getTrampolineCloseChange(info)
val launchAnimator =
createLaunchAnimator(
launchChange,
transaction,
finishCallback,
isTrampoline = trampolineCloseChange != null,
)
val animatorsList = mutableListOf(launchAnimator)
if (minimizeChange != null) {
val minimizeAnimator =
createMinimizeAnimator(minimizeChange, transaction, finishCallback)
animatorsList.add(minimizeAnimator)
}
if (trampolineCloseChange != null) {
val trampolineCloseAnimator =
createTrampolineCloseAnimator(trampolineCloseChange, transaction, finishCallback)
animatorsList.add(trampolineCloseAnimator)
}
return animatorsList
}
private fun getLaunchChange(info: TransitionInfo): Change? =
info.changes.firstOrNull { change ->
change.mode in LAUNCH_CHANGE_MODES && change.taskInfo?.isFreeform == true
}
private fun getMinimizeChange(info: TransitionInfo): Change? =
info.changes.firstOrNull { change ->
change.mode == TRANSIT_TO_BACK && change.taskInfo?.isFreeform == true
}
private fun getTrampolineCloseChange(info: TransitionInfo): Change? {
if (
info.changes.size < 2 ||
!DesktopModeFlags.ENABLE_DESKTOP_TRAMPOLINE_CLOSE_ANIMATION_BUGFIX.isTrue
) {
return null
}
val openChange =
info.changes.firstOrNull { change ->
change.mode == TRANSIT_OPEN && change.taskInfo?.isFreeform == true
}
val closeChange =
info.changes.firstOrNull { change ->
change.mode == TRANSIT_CLOSE && change.taskInfo?.isFreeform == true
}
val openPackage = openChange?.taskInfo?.baseIntent?.component?.packageName
val closePackage = closeChange?.taskInfo?.baseIntent?.component?.packageName
return if (openPackage != null && closePackage != null && openPackage == closePackage) {
closeChange
} else {
null
}
}
private fun createLaunchAnimator(
change: Change,
transaction: Transaction,
onAnimFinish: (Animator) -> Unit,
isTrampoline: Boolean,
): Animator {
val boundsAnimator =
WindowAnimator.createBoundsAnimator(
context.resources.displayMetrics,
launchType.boundsAnimationParams,
change,
transaction,
)
val alphaAnimator =
ValueAnimator.ofFloat(0f, 1f).apply {
duration = launchType.alphaDurationMs
interpolator = Interpolators.LINEAR
addUpdateListener { animation ->
transaction
.setAlpha(change.leash, animation.animatedValue as Float)
.setFrameTimeline(Choreographer.getInstance().vsyncId)
.apply()
}
}
val clipRect = Rect(change.endAbsBounds).apply { offsetTo(0, 0) }
transaction.setCrop(change.leash, clipRect)
transaction.setCornerRadius(
change.leash,
ScreenDecorationsUtils.getWindowCornerRadius(context),
)
return AnimatorSet().apply {
interactionJankMonitor.begin(change.leash, context, context.mainThreadHandler, cujType)
if (isTrampoline) {
play(alphaAnimator)
} else {
playTogether(boundsAnimator, alphaAnimator)
}
addListener(
onEnd = { animation ->
onAnimFinish(animation)
interactionJankMonitor.end(cujType)
}
)
}
}
private fun createMinimizeAnimator(
change: Change,
transaction: Transaction,
onAnimFinish: (Animator) -> Unit,
): Animator {
return MinimizeAnimator.create(
context,
change,
transaction,
onAnimFinish,
interactionJankMonitor,
context.mainThreadHandler,
)
}
private fun createTrampolineCloseAnimator(
change: Change,
transaction: Transaction,
onAnimFinish: (Animator) -> Unit,
): Animator {
return ValueAnimator.ofFloat(1f, 0f).apply {
duration = 100L
interpolator = Interpolators.LINEAR
addUpdateListener { animation ->
transaction.setAlpha(change.leash, animation.animatedValue as Float).apply()
}
addListener(
onEnd = { animation ->
onAnimFinish(animation)
}
)
}
}
private companion object {
const val TAG = "DesktopAppLaunchAnimatorHelper"
}
}