diff options
12 files changed, 859 insertions, 870 deletions
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt index 037b1ec9247c..c988c2fb5103 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * 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. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package com.android.wm.shell.back import android.animation.Animator @@ -34,6 +35,7 @@ import android.view.RemoteAnimationTarget import android.view.SurfaceControl import android.view.animation.DecelerateInterpolator import android.view.animation.Interpolator +import android.view.animation.Transformation import android.window.BackEvent import android.window.BackMotionEvent import android.window.BackNavigationInfo @@ -46,52 +48,45 @@ import com.android.wm.shell.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.animation.Interpolators import com.android.wm.shell.protolog.ShellProtoLogGroup -import com.android.wm.shell.shared.annotations.ShellMainThread -import javax.inject.Inject import kotlin.math.abs import kotlin.math.max import kotlin.math.min -/** Class that defines cross-activity animation. */ -@ShellMainThread -class CrossActivityBackAnimation @Inject constructor( +abstract class CrossActivityBackAnimation( private val context: Context, private val background: BackAnimationBackground, - private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer + private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + protected val transaction: SurfaceControl.Transaction, + private val choreographer: Choreographer ) : ShellBackAnimation() { - private val startClosingRect = RectF() - private val targetClosingRect = RectF() - private val currentClosingRect = RectF() + protected val startClosingRect = RectF() + protected val targetClosingRect = RectF() + protected val currentClosingRect = RectF() - private val startEnteringRect = RectF() - private val targetEnteringRect = RectF() - private val currentEnteringRect = RectF() + protected val startEnteringRect = RectF() + protected val targetEnteringRect = RectF() + protected val currentEnteringRect = RectF() - private val backAnimRect = Rect() + protected val backAnimRect = Rect() private val cropRect = Rect() private var cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context) - private val backAnimationRunner = BackAnimationRunner( - Callback(), Runner(), context, Cuj.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY - ) + private val backAnimationRunner = + BackAnimationRunner(Callback(), Runner(), context, Cuj.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY) private val initialTouchPos = PointF() private val transformMatrix = Matrix() private val tmpFloat9 = FloatArray(9) - private var enteringTarget: RemoteAnimationTarget? = null - private var closingTarget: RemoteAnimationTarget? = null - private val transaction = SurfaceControl.Transaction() + protected var enteringTarget: RemoteAnimationTarget? = null + protected var closingTarget: RemoteAnimationTarget? = null private var triggerBack = false private var finishCallback: IRemoteAnimationFinishedCallback? = null private val progressAnimator = BackProgressAnimator() private val displayBoundsMargin = context.resources.getDimension(R.dimen.cross_task_back_vertical_margin) - private val enteringStartOffset = - context.resources.getDimension(R.dimen.cross_activity_back_entering_start_offset) private val gestureInterpolator = Interpolators.BACK_GESTURE - private val postCommitInterpolator = Interpolators.FAST_OUT_SLOW_IN private val verticalMoveInterpolator: Interpolator = DecelerateInterpolator() private var scrimLayer: SurfaceControl? = null @@ -103,13 +98,42 @@ class CrossActivityBackAnimation @Inject constructor( private var rightLetterboxLayer: SurfaceControl? = null private var letterboxColor: Int = 0 + /** Background color to be used during the animation, also see [getBackgroundColor] */ + protected var customizedBackgroundColor = 0 + + /** + * Whether the entering target should be shifted vertically with the user gesture in pre-commit + */ + abstract val allowEnteringYShift: Boolean + + /** + * Subclasses must set the [startEnteringRect] and [targetEnteringRect] to define the movement + * of the enteringTarget during pre-commit phase. + */ + abstract fun preparePreCommitEnteringRectMovement() + + /** + * Returns a base transformation to apply to the entering target during pre-commit. The system + * will apply the default animation on top of it. + */ + protected open fun getPreCommitEnteringBaseTransformation(progress: Float): Transformation? = + null + override fun onConfigurationChanged(newConfiguration: Configuration) { cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context) } override fun getRunner() = backAnimationRunner - private fun startBackAnimation(backMotionEvent: BackMotionEvent) { + private fun getBackgroundColor(): Int = + when { + customizedBackgroundColor != 0 -> customizedBackgroundColor + isLetterboxed -> letterboxColor + enteringTarget != null -> enteringTarget!!.taskInfo.taskDescription!!.backgroundColor + else -> 0 + } + + protected open fun startBackAnimation(backMotionEvent: BackMotionEvent) { if (enteringTarget == null || closingTarget == null) { ProtoLog.d( ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW, @@ -122,8 +146,8 @@ class CrossActivityBackAnimation @Inject constructor( transaction.setAnimationTransaction() isLetterboxed = closingTarget!!.taskInfo.appCompatTaskInfo.topActivityBoundsLetterboxed - enteringHasSameLetterbox = isLetterboxed && - closingTarget!!.localBounds.equals(enteringTarget!!.localBounds) + enteringHasSameLetterbox = + isLetterboxed && closingTarget!!.localBounds.equals(enteringTarget!!.localBounds) if (isLetterboxed && !enteringHasSameLetterbox) { // Play animation with letterboxes, if closing and entering target have mismatching @@ -143,32 +167,27 @@ class CrossActivityBackAnimation @Inject constructor( targetClosingRect.scaleCentered(MAX_SCALE) if (backMotionEvent.swipeEdge != BackEvent.EDGE_RIGHT) { targetClosingRect.offset( - startClosingRect.right - targetClosingRect.right - displayBoundsMargin, 0f + startClosingRect.right - targetClosingRect.right - displayBoundsMargin, + 0f ) } - // the entering target starts 96dp to the left of the screen edge... - startEnteringRect.set(startClosingRect) - startEnteringRect.offset(-enteringStartOffset, 0f) - - // ...and gets scaled in sync with the closing target - targetEnteringRect.set(startEnteringRect) - targetEnteringRect.scaleCentered(MAX_SCALE) + preparePreCommitEnteringRectMovement() - // Draw background with task background color (or letterbox color). - val backgroundColor = if (isLetterboxed) { - letterboxColor - } else { - enteringTarget!!.taskInfo.taskDescription!!.backgroundColor - } background.ensureBackground( - closingTarget!!.windowConfiguration.bounds, backgroundColor, transaction + closingTarget!!.windowConfiguration.bounds, + getBackgroundColor(), + transaction ) ensureScrimLayer() if (isLetterboxed && enteringHasSameLetterbox) { // crop left and right letterboxes - cropRect.set(closingTarget!!.localBounds.left, 0, closingTarget!!.localBounds.right, - closingTarget!!.windowConfiguration.bounds.height()) + cropRect.set( + closingTarget!!.localBounds.left, + 0, + closingTarget!!.localBounds.right, + closingTarget!!.windowConfiguration.bounds.height() + ) // and add fake letterbox square surfaces instead ensureLetterboxes() } else { @@ -185,8 +204,14 @@ class CrossActivityBackAnimation @Inject constructor( currentClosingRect.offset(0f, yOffset) applyTransform(closingTarget?.leash, currentClosingRect, 1f) currentEnteringRect.setInterpolatedRectF(startEnteringRect, targetEnteringRect, progress) - currentEnteringRect.offset(0f, yOffset) - applyTransform(enteringTarget?.leash, currentEnteringRect, 1f) + if (allowEnteringYShift) currentEnteringRect.offset(0f, yOffset) + val enteringTransformation = getPreCommitEnteringBaseTransformation(progress) + applyTransform( + enteringTarget?.leash, + currentEnteringRect, + enteringTransformation?.alpha ?: 1f, + enteringTransformation + ) applyTransaction() } @@ -199,30 +224,25 @@ class CrossActivityBackAnimation @Inject constructor( val deltaYRatio = min(screenHeight / 2f, abs(rawYDelta)) / (screenHeight / 2f) val interpolatedYRatio: Float = verticalMoveInterpolator.getInterpolation(deltaYRatio) // limit y-shift so surface never passes 8dp screen margin - val deltaY = yDirection * interpolatedYRatio * max( - 0f, (screenHeight - centeredRect.height()) / 2f - displayBoundsMargin - ) + val deltaY = + max(0f, (screenHeight - centeredRect.height()) / 2f - displayBoundsMargin) * + interpolatedYRatio * + yDirection return deltaY } - private fun onGestureCommitted() { - if (closingTarget?.leash == null || enteringTarget?.leash == null || - !enteringTarget!!.leash.isValid || !closingTarget!!.leash.isValid + protected open fun onGestureCommitted() { + if ( + closingTarget?.leash == null || + enteringTarget?.leash == null || + !enteringTarget!!.leash.isValid || + !closingTarget!!.leash.isValid ) { finishAnimation() return } - // We enter phase 2 of the animation, the starting coordinates for phase 2 are the current - // coordinate of the gesture driven phase. Let's update the start and target rects and kick - // off the animator - startClosingRect.set(currentClosingRect) - startEnteringRect.set(currentEnteringRect) - targetEnteringRect.set(backAnimRect) - targetClosingRect.set(backAnimRect) - targetClosingRect.offset(currentClosingRect.left + enteringStartOffset, 0f) - - val valueAnimator = ValueAnimator.ofFloat(1f, 0f).setDuration(POST_ANIMATION_DURATION) + val valueAnimator = ValueAnimator.ofFloat(1f, 0f).setDuration(POST_COMMIT_DURATION) valueAnimator.addUpdateListener { animation: ValueAnimator -> val progress = animation.animatedFraction onPostCommitProgress(progress) @@ -230,27 +250,22 @@ class CrossActivityBackAnimation @Inject constructor( background.resetStatusBarCustomization() } } - valueAnimator.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - background.resetStatusBarCustomization() - finishAnimation() + valueAnimator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + background.resetStatusBarCustomization() + finishAnimation() + } } - }) + ) valueAnimator.start() } - private fun onPostCommitProgress(linearProgress: Float) { - val closingAlpha = max(1f - linearProgress * 2, 0f) - val progress = postCommitInterpolator.getInterpolation(linearProgress) + protected open fun onPostCommitProgress(linearProgress: Float) { scrimLayer?.let { transaction.setAlpha(it, maxScrimAlpha * (1f - linearProgress)) } - currentClosingRect.setInterpolatedRectF(startClosingRect, targetClosingRect, progress) - applyTransform(closingTarget?.leash, currentClosingRect, closingAlpha) - currentEnteringRect.setInterpolatedRectF(startEnteringRect, targetEnteringRect, progress) - applyTransform(enteringTarget?.leash, currentEnteringRect, 1f) - applyTransaction() } - private fun finishAnimation() { + protected open fun finishAnimation() { enteringTarget?.let { if (it.leash != null && it.leash.isValid) { transaction.setCornerRadius(it.leash, 0f) @@ -278,47 +293,56 @@ class CrossActivityBackAnimation @Inject constructor( enteringHasSameLetterbox = false } - private fun applyTransform(leash: SurfaceControl?, rect: RectF, alpha: Float) { + protected fun applyTransform( + leash: SurfaceControl?, + rect: RectF, + alpha: Float, + baseTransformation: Transformation? = null + ) { if (leash == null || !leash.isValid) return val scale = rect.width() / backAnimRect.width() - transformMatrix.reset() - val scalePivotX = if (isLetterboxed && enteringHasSameLetterbox) { - closingTarget!!.localBounds.left.toFloat() - } else { - 0f - } - transformMatrix.setScale(scale, scale, scalePivotX, 0f) - transformMatrix.postTranslate(rect.left, rect.top) - transaction.setAlpha(leash, alpha) - .setMatrix(leash, transformMatrix, tmpFloat9) + val matrix = baseTransformation?.matrix ?: transformMatrix.apply { reset() } + val scalePivotX = + if (isLetterboxed && enteringHasSameLetterbox) { + closingTarget!!.localBounds.left.toFloat() + } else { + 0f + } + matrix.postScale(scale, scale, scalePivotX, 0f) + matrix.postTranslate(rect.left, rect.top) + transaction + .setAlpha(leash, keepMinimumAlpha(alpha)) + .setMatrix(leash, matrix, tmpFloat9) .setCrop(leash, cropRect) .setCornerRadius(leash, cornerRadius) } - private fun applyTransaction() { - transaction.setFrameTimelineVsync(Choreographer.getInstance().vsyncId) + protected fun applyTransaction() { + transaction.setFrameTimelineVsync(choreographer.vsyncId) transaction.apply() } private fun ensureScrimLayer() { if (scrimLayer != null) return val isDarkTheme: Boolean = isDarkMode(context) - val scrimBuilder = SurfaceControl.Builder() - .setName("Cross-Activity back animation scrim") - .setCallsite("CrossActivityBackAnimation") - .setColorLayer() - .setOpaque(false) - .setHidden(false) + val scrimBuilder = + SurfaceControl.Builder() + .setName("Cross-Activity back animation scrim") + .setCallsite("CrossActivityBackAnimation") + .setColorLayer() + .setOpaque(false) + .setHidden(false) rootTaskDisplayAreaOrganizer.attachToDisplayArea(Display.DEFAULT_DISPLAY, scrimBuilder) scrimLayer = scrimBuilder.build() val colorComponents = floatArrayOf(0f, 0f, 0f) maxScrimAlpha = if (isDarkTheme) MAX_SCRIM_ALPHA_DARK else MAX_SCRIM_ALPHA_LIGHT - val scrimCrop = if (isLetterboxed) { - closingTarget!!.windowConfiguration.bounds - } else { - closingTarget!!.localBounds - } + val scrimCrop = + if (isLetterboxed) { + closingTarget!!.windowConfiguration.bounds + } else { + closingTarget!!.localBounds + } transaction .setColor(scrimLayer, colorComponents) .setAlpha(scrimLayer!!, maxScrimAlpha) @@ -339,21 +363,34 @@ class CrossActivityBackAnimation @Inject constructor( private fun ensureLetterboxes() { closingTarget?.let { t -> if (t.localBounds.left != 0 && leftLetterboxLayer == null) { - val bounds = Rect(0, t.windowConfiguration.bounds.top, t.localBounds.left, - t.windowConfiguration.bounds.bottom) + val bounds = + Rect( + 0, + t.windowConfiguration.bounds.top, + t.localBounds.left, + t.windowConfiguration.bounds.bottom + ) leftLetterboxLayer = ensureLetterbox(bounds) } - if (t.localBounds.right != t.windowConfiguration.bounds.right && - rightLetterboxLayer == null) { - val bounds = Rect(t.localBounds.right, t.windowConfiguration.bounds.top, - t.windowConfiguration.bounds.right, t.windowConfiguration.bounds.bottom) + if ( + t.localBounds.right != t.windowConfiguration.bounds.right && + rightLetterboxLayer == null + ) { + val bounds = + Rect( + t.localBounds.right, + t.windowConfiguration.bounds.top, + t.windowConfiguration.bounds.right, + t.windowConfiguration.bounds.bottom + ) rightLetterboxLayer = ensureLetterbox(bounds) } } } private fun ensureLetterbox(bounds: Rect): SurfaceControl { - val letterboxBuilder = SurfaceControl.Builder() + val letterboxBuilder = + SurfaceControl.Builder() .setName("Cross-Activity back animation letterbox") .setCallsite("CrossActivityBackAnimation") .setColorLayer() @@ -362,13 +399,17 @@ class CrossActivityBackAnimation @Inject constructor( rootTaskDisplayAreaOrganizer.attachToDisplayArea(Display.DEFAULT_DISPLAY, letterboxBuilder) val layer = letterboxBuilder.build() - val colorComponents = floatArrayOf(Color.red(letterboxColor) / 255f, - Color.green(letterboxColor) / 255f, Color.blue(letterboxColor) / 255f) + val colorComponents = + floatArrayOf( + Color.red(letterboxColor) / 255f, + Color.green(letterboxColor) / 255f, + Color.blue(letterboxColor) / 255f + ) transaction - .setColor(layer, colorComponents) - .setCrop(layer, bounds) - .setRelativeLayer(layer, closingTarget!!.leash, 1) - .show(layer) + .setColor(layer, colorComponents) + .setCrop(layer, bounds) + .setRelativeLayer(layer, closingTarget!!.leash, 1) + .show(layer) return layer } @@ -389,8 +430,8 @@ class CrossActivityBackAnimation @Inject constructor( } override fun prepareNextAnimation( - animationInfo: BackNavigationInfo.CustomAnimationInfo?, - letterboxColor: Int + animationInfo: BackNavigationInfo.CustomAnimationInfo?, + letterboxColor: Int ): Boolean { this.letterboxColor = letterboxColor return false @@ -415,9 +456,7 @@ class CrossActivityBackAnimation @Inject constructor( } override fun onBackCancelled() { - progressAnimator.onBackCancelled { - finishAnimation() - } + progressAnimator.onBackCancelled { finishAnimation() } } override fun onBackInvoked() { @@ -435,7 +474,8 @@ class CrossActivityBackAnimation @Inject constructor( finishedCallback: IRemoteAnimationFinishedCallback ) { ProtoLog.d( - ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW, "Start back to activity animation." + ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW, + "Start back to activity animation." ) for (a in apps) { when (a.mode) { @@ -452,23 +492,25 @@ class CrossActivityBackAnimation @Inject constructor( } companion object { - /** Max scale of the entering/closing window.*/ - private const val MAX_SCALE = 0.9f - - /** Duration of post animation after gesture committed. */ - private const val POST_ANIMATION_DURATION = 300L - + /** Max scale of the closing window. */ + internal const val MAX_SCALE = 0.9f private const val MAX_SCRIM_ALPHA_DARK = 0.8f private const val MAX_SCRIM_ALPHA_LIGHT = 0.2f + private const val POST_COMMIT_DURATION = 300L } } +// The target will loose focus when alpha == 0, so keep a minimum value for it. +private fun keepMinimumAlpha(transAlpha: Float): Float { + return max(transAlpha.toDouble(), 0.005).toFloat() +} + private fun isDarkMode(context: Context): Boolean { return context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == - Configuration.UI_MODE_NIGHT_YES + Configuration.UI_MODE_NIGHT_YES } -private fun RectF.setInterpolatedRectF(start: RectF, target: RectF, progress: Float) { +internal fun RectF.setInterpolatedRectF(start: RectF, target: RectF, progress: Float) { require(!(progress < 0 || progress > 1)) { "Progress value must be between 0 and 1" } left = start.left + (target.left - start.left) * progress top = start.top + (target.top - start.top) * progress @@ -476,7 +518,7 @@ private fun RectF.setInterpolatedRectF(start: RectF, target: RectF, progress: Fl bottom = start.bottom + (target.bottom - start.bottom) * progress } -private fun RectF.scaleCentered( +internal fun RectF.scaleCentered( scale: Float, pivotX: Float = left + width() / 2, pivotY: Float = top + height() / 2 diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt new file mode 100644 index 000000000000..e6ec2b449616 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomCrossActivityBackAnimation.kt @@ -0,0 +1,256 @@ +/* + * 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.wm.shell.back + +import android.content.Context +import android.graphics.Rect +import android.graphics.RectF +import android.util.MathUtils +import android.view.Choreographer +import android.view.SurfaceControl +import android.view.animation.Animation +import android.view.animation.Transformation +import android.window.BackMotionEvent +import android.window.BackNavigationInfo +import com.android.internal.R +import com.android.internal.policy.TransitionAnimation +import com.android.internal.protolog.common.ProtoLog +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.protolog.ShellProtoLogGroup +import com.android.wm.shell.shared.annotations.ShellMainThread +import javax.inject.Inject + +/** Class that handles customized predictive cross activity back animations. */ +@ShellMainThread +class CustomCrossActivityBackAnimation( + context: Context, + background: BackAnimationBackground, + rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + transaction: SurfaceControl.Transaction, + choreographer: Choreographer, + private val customAnimationLoader: CustomAnimationLoader +) : + CrossActivityBackAnimation( + context, + background, + rootTaskDisplayAreaOrganizer, + transaction, + choreographer + ) { + + private var enterAnimation: Animation? = null + private var closeAnimation: Animation? = null + private val transformation = Transformation() + private var gestureProgress = 0f + + override val allowEnteringYShift = false + + @Inject + constructor( + context: Context, + background: BackAnimationBackground, + rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer + ) : this( + context, + background, + rootTaskDisplayAreaOrganizer, + SurfaceControl.Transaction(), + Choreographer.getInstance(), + CustomAnimationLoader( + TransitionAnimation(context, false /* debug */, "CustomCrossActivityBackAnimation") + ) + ) + + override fun preparePreCommitEnteringRectMovement() { + // No movement for the entering rect + startEnteringRect.set(startClosingRect) + targetEnteringRect.set(startClosingRect) + } + + override fun getPreCommitEnteringBaseTransformation(progress: Float): Transformation { + gestureProgress = progress + transformation.clear() + enterAnimation!!.getTransformationAt(progress * PRE_COMMIT_MAX_PROGRESS, transformation) + return transformation + } + + override fun startBackAnimation(backMotionEvent: BackMotionEvent) { + super.startBackAnimation(backMotionEvent) + if ( + closeAnimation == null || + enterAnimation == null || + closingTarget == null || + enteringTarget == null + ) { + ProtoLog.d( + ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW, + "Enter animation or close animation is null." + ) + return + } + initializeAnimation(closeAnimation!!, closingTarget!!.localBounds) + initializeAnimation(enterAnimation!!, enteringTarget!!.localBounds) + } + + override fun onPostCommitProgress(linearProgress: Float) { + super.onPostCommitProgress(linearProgress) + if (closingTarget == null || enteringTarget == null) return + + // TODO: Should we use the duration from the custom xml spec for the post-commit animation? + applyTransform(closingTarget!!.leash, currentClosingRect, linearProgress, closeAnimation!!) + val enteringProgress = + MathUtils.lerp(gestureProgress * PRE_COMMIT_MAX_PROGRESS, 1f, linearProgress) + applyTransform( + enteringTarget!!.leash, + currentEnteringRect, + enteringProgress, + enterAnimation!! + ) + applyTransaction() + } + + private fun applyTransform( + leash: SurfaceControl, + rect: RectF, + progress: Float, + animation: Animation + ) { + transformation.clear() + animation.getTransformationAt(progress, transformation) + applyTransform(leash, rect, transformation.alpha, transformation) + } + + override fun finishAnimation() { + closeAnimation?.reset() + closeAnimation = null + enterAnimation?.reset() + enterAnimation = null + transformation.clear() + gestureProgress = 0f + super.finishAnimation() + } + + /** Load customize animation before animation start. */ + override fun prepareNextAnimation( + animationInfo: BackNavigationInfo.CustomAnimationInfo?, + letterboxColor: Int + ): Boolean { + super.prepareNextAnimation(animationInfo, letterboxColor) + if (animationInfo == null) return false + customAnimationLoader.loadAll(animationInfo)?.let { result -> + closeAnimation = result.closeAnimation + enterAnimation = result.enterAnimation + customizedBackgroundColor = result.backgroundColor + return true + } + return false + } + + class AnimationLoadResult { + var closeAnimation: Animation? = null + var enterAnimation: Animation? = null + var backgroundColor = 0 + } + + companion object { + private const val PRE_COMMIT_MAX_PROGRESS = 0.2f + } +} + +/** Helper class to load custom animation. */ +class CustomAnimationLoader(private val transitionAnimation: TransitionAnimation) { + + /** + * Load both enter and exit animation for the close activity transition. Note that the result is + * only valid if the exit animation has set and loaded success. If the entering animation has + * not set(i.e. 0), here will load the default entering animation for it. + * + * @param animationInfo The information of customize animation, which can be set from + * [Activity.overrideActivityTransition] and/or [LayoutParams.windowAnimations] + */ + fun loadAll( + animationInfo: BackNavigationInfo.CustomAnimationInfo + ): CustomCrossActivityBackAnimation.AnimationLoadResult? { + if (animationInfo.packageName.isEmpty()) return null + val close = loadAnimation(animationInfo, false) ?: return null + val open = loadAnimation(animationInfo, true) + val result = CustomCrossActivityBackAnimation.AnimationLoadResult() + result.closeAnimation = close + result.enterAnimation = open + result.backgroundColor = animationInfo.customBackground + return result + } + + /** + * Load enter or exit animation from CustomAnimationInfo + * + * @param animationInfo The information for customize animation. + * @param enterAnimation true when load for enter animation, false for exit animation. + * @return Loaded animation. + */ + fun loadAnimation( + animationInfo: BackNavigationInfo.CustomAnimationInfo, + enterAnimation: Boolean + ): Animation? { + var a: Animation? = null + // Activity#overrideActivityTransition has higher priority than windowAnimations + // Try to get animation from Activity#overrideActivityTransition + if ( + enterAnimation && animationInfo.customEnterAnim != 0 || + !enterAnimation && animationInfo.customExitAnim != 0 + ) { + a = + transitionAnimation.loadAppTransitionAnimation( + animationInfo.packageName, + if (enterAnimation) animationInfo.customEnterAnim + else animationInfo.customExitAnim + ) + } else if (animationInfo.windowAnimations != 0) { + // try to get animation from LayoutParams#windowAnimations + a = + transitionAnimation.loadAnimationAttr( + animationInfo.packageName, + animationInfo.windowAnimations, + if (enterAnimation) R.styleable.WindowAnimation_activityCloseEnterAnimation + else R.styleable.WindowAnimation_activityCloseExitAnimation, + false /* translucent */ + ) + } + // Only allow to load default animation for opening target. + if (a == null && enterAnimation) { + a = loadDefaultOpenAnimation() + } + if (a != null) { + ProtoLog.d(ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW, "custom animation loaded %s", a) + } else { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW, "No custom animation loaded") + } + return a + } + + private fun loadDefaultOpenAnimation(): Animation? { + return transitionAnimation.loadDefaultAnimationAttr( + R.styleable.WindowAnimation_activityCloseEnterAnimation, + false /* translucent */ + ) + } +} + +private fun initializeAnimation(animation: Animation, bounds: Rect) { + val width = bounds.width() + val height = bounds.height() + animation.initialize(width, height, width, height) +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java deleted file mode 100644 index e27b40e58591..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CustomizeActivityAnimation.java +++ /dev/null @@ -1,443 +0,0 @@ -/* - * Copyright (C) 2023 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.wm.shell.back; - -import static android.view.RemoteAnimationTarget.MODE_CLOSING; -import static android.view.RemoteAnimationTarget.MODE_OPENING; - -import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY; -import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW; - -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.ValueAnimator; -import android.annotation.NonNull; -import android.annotation.Nullable; -import android.app.Activity; -import android.content.Context; -import android.content.res.Configuration; -import android.graphics.Color; -import android.graphics.Rect; -import android.os.RemoteException; -import android.util.FloatProperty; -import android.view.Choreographer; -import android.view.IRemoteAnimationFinishedCallback; -import android.view.IRemoteAnimationRunner; -import android.view.RemoteAnimationTarget; -import android.view.SurfaceControl; -import android.view.WindowManager.LayoutParams; -import android.view.animation.Animation; -import android.view.animation.DecelerateInterpolator; -import android.view.animation.Transformation; -import android.window.BackEvent; -import android.window.BackMotionEvent; -import android.window.BackNavigationInfo; -import android.window.BackProgressAnimator; -import android.window.IOnBackInvokedCallback; - -import com.android.internal.R; -import com.android.internal.dynamicanimation.animation.SpringAnimation; -import com.android.internal.dynamicanimation.animation.SpringForce; -import com.android.internal.policy.ScreenDecorationsUtils; -import com.android.internal.policy.TransitionAnimation; -import com.android.internal.protolog.common.ProtoLog; -import com.android.wm.shell.shared.annotations.ShellMainThread; - -import javax.inject.Inject; - -/** Class that handle customized close activity transition animation. */ -@ShellMainThread -public class CustomizeActivityAnimation extends ShellBackAnimation { - private final BackProgressAnimator mProgressAnimator = new BackProgressAnimator(); - private final BackAnimationRunner mBackAnimationRunner; - private float mCornerRadius; - private final SurfaceControl.Transaction mTransaction; - private final BackAnimationBackground mBackground; - private RemoteAnimationTarget mEnteringTarget; - private RemoteAnimationTarget mClosingTarget; - private IRemoteAnimationFinishedCallback mFinishCallback; - /** Duration of post animation after gesture committed. */ - private static final int POST_ANIMATION_DURATION = 250; - - private static final int SCALE_FACTOR = 1000; - private final SpringAnimation mProgressSpring; - private float mLatestProgress = 0.0f; - - private static final float TARGET_COMMIT_PROGRESS = 0.5f; - - private final float[] mTmpFloat9 = new float[9]; - private final DecelerateInterpolator mDecelerateInterpolator = new DecelerateInterpolator(); - - final CustomAnimationLoader mCustomAnimationLoader; - private Animation mEnterAnimation; - private Animation mCloseAnimation; - private int mNextBackgroundColor; - final Transformation mTransformation = new Transformation(); - - private final Choreographer mChoreographer; - private final Context mContext; - - @Inject - public CustomizeActivityAnimation(Context context, BackAnimationBackground background) { - this(context, background, new SurfaceControl.Transaction(), null); - } - - CustomizeActivityAnimation(Context context, BackAnimationBackground background, - SurfaceControl.Transaction transaction, Choreographer choreographer) { - mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context); - mBackground = background; - mBackAnimationRunner = new BackAnimationRunner( - new Callback(), new Runner(), context, CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY); - mCustomAnimationLoader = new CustomAnimationLoader(context); - - mProgressSpring = new SpringAnimation(this, ENTER_PROGRESS_PROP); - mProgressSpring.setSpring(new SpringForce() - .setStiffness(SpringForce.STIFFNESS_MEDIUM) - .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY)); - mTransaction = transaction == null ? new SurfaceControl.Transaction() : transaction; - mChoreographer = choreographer != null ? choreographer : Choreographer.getInstance(); - mContext = context; - } - - @Override - public void onConfigurationChanged(Configuration newConfig) { - mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(mContext); - } - - private float getLatestProgress() { - return mLatestProgress * SCALE_FACTOR; - } - private void setLatestProgress(float value) { - mLatestProgress = value / SCALE_FACTOR; - applyTransformTransaction(mLatestProgress); - } - - private static final FloatProperty<CustomizeActivityAnimation> ENTER_PROGRESS_PROP = - new FloatProperty<>("enter") { - @Override - public void setValue(CustomizeActivityAnimation anim, float value) { - anim.setLatestProgress(value); - } - - @Override - public Float get(CustomizeActivityAnimation object) { - return object.getLatestProgress(); - } - }; - - // The target will lose focus when alpha == 0, so keep a minimum value for it. - private static float keepMinimumAlpha(float transAlpha) { - return Math.max(transAlpha, 0.005f); - } - - private static void initializeAnimation(Animation animation, Rect bounds) { - final int width = bounds.width(); - final int height = bounds.height(); - animation.initialize(width, height, width, height); - } - - private void startBackAnimation() { - if (mEnteringTarget == null || mClosingTarget == null - || mCloseAnimation == null || mEnterAnimation == null) { - ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Entering target or closing target is null."); - return; - } - initializeAnimation(mCloseAnimation, mClosingTarget.localBounds); - initializeAnimation(mEnterAnimation, mEnteringTarget.localBounds); - - // Draw background with task background color. - if (mEnteringTarget.taskInfo != null && mEnteringTarget.taskInfo.taskDescription != null) { - mBackground.ensureBackground(mClosingTarget.windowConfiguration.getBounds(), - mNextBackgroundColor == Color.TRANSPARENT - ? mEnteringTarget.taskInfo.taskDescription.getBackgroundColor() - : mNextBackgroundColor, - mTransaction); - } - } - - private void applyTransformTransaction(float progress) { - if (mClosingTarget == null || mEnteringTarget == null) { - return; - } - applyTransform(mClosingTarget.leash, progress, mCloseAnimation); - applyTransform(mEnteringTarget.leash, progress, mEnterAnimation); - mTransaction.setFrameTimelineVsync(mChoreographer.getVsyncId()); - mTransaction.apply(); - } - - private void applyTransform(SurfaceControl leash, float progress, Animation animation) { - mTransformation.clear(); - animation.getTransformationAt(progress, mTransformation); - mTransaction.setMatrix(leash, mTransformation.getMatrix(), mTmpFloat9); - mTransaction.setAlpha(leash, keepMinimumAlpha(mTransformation.getAlpha())); - mTransaction.setCornerRadius(leash, mCornerRadius); - } - - void finishAnimation() { - if (mCloseAnimation != null) { - mCloseAnimation.reset(); - mCloseAnimation = null; - } - if (mEnterAnimation != null) { - mEnterAnimation.reset(); - mEnterAnimation = null; - } - if (mEnteringTarget != null) { - mEnteringTarget.leash.release(); - mEnteringTarget = null; - } - if (mClosingTarget != null) { - mClosingTarget.leash.release(); - mClosingTarget = null; - } - if (mBackground != null) { - mBackground.removeBackground(mTransaction); - } - mTransaction.setFrameTimelineVsync(mChoreographer.getVsyncId()); - mTransaction.apply(); - mTransformation.clear(); - mLatestProgress = 0; - mNextBackgroundColor = Color.TRANSPARENT; - if (mFinishCallback != null) { - try { - mFinishCallback.onAnimationFinished(); - } catch (RemoteException e) { - e.printStackTrace(); - } - mFinishCallback = null; - } - mProgressSpring.animateToFinalPosition(0); - mProgressSpring.skipToEnd(); - } - - void onGestureProgress(@NonNull BackEvent backEvent) { - if (mEnteringTarget == null || mClosingTarget == null - || mCloseAnimation == null || mEnterAnimation == null) { - return; - } - - final float progress = backEvent.getProgress(); - - float springProgress = (progress > 0.1f - ? mapLinear(progress, 0.1f, 1f, TARGET_COMMIT_PROGRESS, 1f) - : mapLinear(progress, 0, 1f, 0f, TARGET_COMMIT_PROGRESS)) * SCALE_FACTOR; - - mProgressSpring.animateToFinalPosition(springProgress); - } - - static float mapLinear(float x, float a1, float a2, float b1, float b2) { - return b1 + (x - a1) * (b2 - b1) / (a2 - a1); - } - - void onGestureCommitted() { - if (mEnteringTarget == null || mClosingTarget == null - || mCloseAnimation == null || mEnterAnimation == null) { - finishAnimation(); - return; - } - mProgressSpring.cancel(); - - // Enter phase 2 of the animation - final ValueAnimator valueAnimator = ValueAnimator.ofFloat(mLatestProgress, 1f) - .setDuration(POST_ANIMATION_DURATION); - valueAnimator.setInterpolator(mDecelerateInterpolator); - valueAnimator.addUpdateListener(animation -> { - float progress = (float) animation.getAnimatedValue(); - applyTransformTransaction(progress); - }); - - valueAnimator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - finishAnimation(); - } - }); - valueAnimator.start(); - } - - /** Load customize animation before animation start. */ - @Override - public boolean prepareNextAnimation(BackNavigationInfo.CustomAnimationInfo animationInfo, - int letterboxColor) { - if (animationInfo == null) { - return false; - } - final AnimationLoadResult result = mCustomAnimationLoader.loadAll(animationInfo); - if (result != null) { - mCloseAnimation = result.mCloseAnimation; - mEnterAnimation = result.mEnterAnimation; - mNextBackgroundColor = result.mBackgroundColor; - return true; - } - return false; - } - - @Override - public BackAnimationRunner getRunner() { - return mBackAnimationRunner; - } - - private final class Callback extends IOnBackInvokedCallback.Default { - @Override - public void onBackStarted(BackMotionEvent backEvent) { - // in case we're still animating an onBackCancelled event, let's remove the finish- - // callback from the progress animator to prevent calling finishAnimation() before - // restarting a new animation - mProgressAnimator.removeOnBackCancelledFinishCallback(); - - mProgressAnimator.onBackStarted(backEvent, - CustomizeActivityAnimation.this::onGestureProgress); - } - - @Override - public void onBackProgressed(@NonNull BackMotionEvent backEvent) { - mProgressAnimator.onBackProgressed(backEvent); - } - - @Override - public void onBackCancelled() { - mProgressAnimator.onBackCancelled(CustomizeActivityAnimation.this::finishAnimation); - } - - @Override - public void onBackInvoked() { - mProgressAnimator.reset(); - onGestureCommitted(); - } - } - - private final class Runner extends IRemoteAnimationRunner.Default { - @Override - public void onAnimationStart( - int transit, - RemoteAnimationTarget[] apps, - RemoteAnimationTarget[] wallpapers, - RemoteAnimationTarget[] nonApps, - IRemoteAnimationFinishedCallback finishedCallback) { - ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Start back to customize animation."); - for (RemoteAnimationTarget a : apps) { - if (a.mode == MODE_CLOSING) { - mClosingTarget = a; - } - if (a.mode == MODE_OPENING) { - mEnteringTarget = a; - } - } - if (mCloseAnimation == null || mEnterAnimation == null) { - ProtoLog.d(WM_SHELL_BACK_PREVIEW, - "No animation loaded, should choose cross-activity animation?"); - } - - startBackAnimation(); - mFinishCallback = finishedCallback; - } - - @Override - public void onAnimationCancelled() { - finishAnimation(); - } - } - - - static final class AnimationLoadResult { - Animation mCloseAnimation; - Animation mEnterAnimation; - int mBackgroundColor; - } - - /** - * Helper class to load custom animation. - */ - static class CustomAnimationLoader { - final TransitionAnimation mTransitionAnimation; - - CustomAnimationLoader(Context context) { - mTransitionAnimation = new TransitionAnimation( - context, false /* debug */, "CustomizeBackAnimation"); - } - - /** - * Load both enter and exit animation for the close activity transition. - * Note that the result is only valid if the exit animation has set and loaded success. - * If the entering animation has not set(i.e. 0), here will load the default entering - * animation for it. - * - * @param animationInfo The information of customize animation, which can be set from - * {@link Activity#overrideActivityTransition} and/or - * {@link LayoutParams#windowAnimations} - */ - AnimationLoadResult loadAll(BackNavigationInfo.CustomAnimationInfo animationInfo) { - if (animationInfo.getPackageName().isEmpty()) { - return null; - } - final Animation close = loadAnimation(animationInfo, false); - if (close == null) { - return null; - } - final Animation open = loadAnimation(animationInfo, true); - AnimationLoadResult result = new AnimationLoadResult(); - result.mCloseAnimation = close; - result.mEnterAnimation = open; - result.mBackgroundColor = animationInfo.getCustomBackground(); - return result; - } - - /** - * Load enter or exit animation from CustomAnimationInfo - * @param animationInfo The information for customize animation. - * @param enterAnimation true when load for enter animation, false for exit animation. - * @return Loaded animation. - */ - @Nullable - Animation loadAnimation(BackNavigationInfo.CustomAnimationInfo animationInfo, - boolean enterAnimation) { - Animation a = null; - // Activity#overrideActivityTransition has higher priority than windowAnimations - // Try to get animation from Activity#overrideActivityTransition - if ((enterAnimation && animationInfo.getCustomEnterAnim() != 0) - || (!enterAnimation && animationInfo.getCustomExitAnim() != 0)) { - a = mTransitionAnimation.loadAppTransitionAnimation( - animationInfo.getPackageName(), - enterAnimation ? animationInfo.getCustomEnterAnim() - : animationInfo.getCustomExitAnim()); - } else if (animationInfo.getWindowAnimations() != 0) { - // try to get animation from LayoutParams#windowAnimations - a = mTransitionAnimation.loadAnimationAttr(animationInfo.getPackageName(), - animationInfo.getWindowAnimations(), enterAnimation - ? R.styleable.WindowAnimation_activityCloseEnterAnimation - : R.styleable.WindowAnimation_activityCloseExitAnimation, - false /* translucent */); - } - // Only allow to load default animation for opening target. - if (a == null && enterAnimation) { - a = loadDefaultOpenAnimation(); - } - if (a != null) { - ProtoLog.d(WM_SHELL_BACK_PREVIEW, "custom animation loaded %s", a); - } else { - ProtoLog.e(WM_SHELL_BACK_PREVIEW, "No custom animation loaded"); - } - return a; - } - - private Animation loadDefaultOpenAnimation() { - return mTransitionAnimation.loadDefaultAnimationAttr( - R.styleable.WindowAnimation_activityCloseEnterAnimation, - false /* translucent */); - } - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt new file mode 100644 index 000000000000..f33c5b9bd183 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/DefaultCrossActivityBackAnimation.kt @@ -0,0 +1,81 @@ +/* + * 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.wm.shell.back + +import android.content.Context +import android.view.Choreographer +import android.view.SurfaceControl +import com.android.wm.shell.R +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.animation.Interpolators +import com.android.wm.shell.shared.annotations.ShellMainThread +import javax.inject.Inject +import kotlin.math.max + +/** Class that defines cross-activity animation. */ +@ShellMainThread +class DefaultCrossActivityBackAnimation +@Inject +constructor( + context: Context, + background: BackAnimationBackground, + rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer +) : + CrossActivityBackAnimation( + context, + background, + rootTaskDisplayAreaOrganizer, + SurfaceControl.Transaction(), + Choreographer.getInstance() + ) { + + private val postCommitInterpolator = Interpolators.FAST_OUT_SLOW_IN + private val enteringStartOffset = + context.resources.getDimension(R.dimen.cross_activity_back_entering_start_offset) + override val allowEnteringYShift = true + + override fun preparePreCommitEnteringRectMovement() { + // the entering target starts 96dp to the left of the screen edge... + startEnteringRect.set(startClosingRect) + startEnteringRect.offset(-enteringStartOffset, 0f) + // ...and gets scaled in sync with the closing target + targetEnteringRect.set(startEnteringRect) + targetEnteringRect.scaleCentered(MAX_SCALE) + } + + override fun onGestureCommitted() { + // We enter phase 2 of the animation, the starting coordinates for phase 2 are the current + // coordinate of the gesture driven phase. Let's update the start and target rects and kick + // off the animator in the superclass + startClosingRect.set(currentClosingRect) + startEnteringRect.set(currentEnteringRect) + targetEnteringRect.set(backAnimRect) + targetClosingRect.set(backAnimRect) + targetClosingRect.offset(currentClosingRect.left + enteringStartOffset, 0f) + super.onGestureCommitted() + } + + override fun onPostCommitProgress(linearProgress: Float) { + super.onPostCommitProgress(linearProgress) + val closingAlpha = max(1f - linearProgress * 2, 0f) + val progress = postCommitInterpolator.getInterpolation(linearProgress) + currentClosingRect.setInterpolatedRectF(startClosingRect, targetClosingRect, progress) + applyTransform(closingTarget?.leash, currentClosingRect, closingAlpha) + currentEnteringRect.setInterpolatedRectF(startEnteringRect, targetEnteringRect, progress) + applyTransform(enteringTarget?.leash, currentEnteringRect, 1f) + applyTransaction() + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/back/ShellBackAnimationModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/back/ShellBackAnimationModule.java index 795bc1a7113b..d2895b149b2c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/back/ShellBackAnimationModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/back/ShellBackAnimationModule.java @@ -16,9 +16,9 @@ package com.android.wm.shell.dagger.back; -import com.android.wm.shell.back.CrossActivityBackAnimation; import com.android.wm.shell.back.CrossTaskBackAnimation; -import com.android.wm.shell.back.CustomizeActivityAnimation; +import com.android.wm.shell.back.CustomCrossActivityBackAnimation; +import com.android.wm.shell.back.DefaultCrossActivityBackAnimation; import com.android.wm.shell.back.ShellBackAnimation; import com.android.wm.shell.back.ShellBackAnimationRegistry; @@ -47,7 +47,7 @@ public interface ShellBackAnimationModule { @Binds @ShellBackAnimation.CrossActivity ShellBackAnimation bindCrossActivityShellBackAnimation( - CrossActivityBackAnimation crossActivityBackAnimation); + DefaultCrossActivityBackAnimation defaultCrossActivityBackAnimation); /** Default cross task back animation */ @Binds @@ -59,5 +59,5 @@ public interface ShellBackAnimationModule { @Binds @ShellBackAnimation.CustomizeActivity ShellBackAnimation provideCustomizeActivityShellBackAnimation( - CustomizeActivityAnimation customizeActivityAnimation); + CustomCrossActivityBackAnimation customCrossActivityBackAnimation); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java index f99b4b2beef0..f6f3aa49bc6e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java @@ -120,7 +120,7 @@ public class BackAnimationControllerTest extends ShellTestCase { private TestableContentResolver mContentResolver; private TestableLooper mTestableLooper; - private CrossActivityBackAnimation mCrossActivityBackAnimation; + private DefaultCrossActivityBackAnimation mDefaultCrossActivityBackAnimation; private CrossTaskBackAnimation mCrossTaskBackAnimation; private ShellBackAnimationRegistry mShellBackAnimationRegistry; @@ -135,13 +135,14 @@ public class BackAnimationControllerTest extends ShellTestCase { ANIMATION_ENABLED); mTestableLooper = TestableLooper.get(this); mShellInit = spy(new ShellInit(mShellExecutor)); - mCrossActivityBackAnimation = new CrossActivityBackAnimation(mContext, mAnimationBackground, - mRootTaskDisplayAreaOrganizer); + mDefaultCrossActivityBackAnimation = new DefaultCrossActivityBackAnimation(mContext, + mAnimationBackground, mRootTaskDisplayAreaOrganizer); mCrossTaskBackAnimation = new CrossTaskBackAnimation(mContext, mAnimationBackground); mShellBackAnimationRegistry = - new ShellBackAnimationRegistry(mCrossActivityBackAnimation, mCrossTaskBackAnimation, - /* dialogCloseAnimation= */ null, - new CustomizeActivityAnimation(mContext, mAnimationBackground), + new ShellBackAnimationRegistry(mDefaultCrossActivityBackAnimation, + mCrossTaskBackAnimation, /* dialogCloseAnimation= */ null, + new CustomCrossActivityBackAnimation(mContext, mAnimationBackground, + mRootTaskDisplayAreaOrganizer), /* defaultBackToHomeAnimation= */ null); mController = new BackAnimationController( @@ -582,7 +583,7 @@ public class BackAnimationControllerTest extends ShellTestCase { @Test public void testBackToActivity() throws RemoteException { verifySystemBackBehavior(BackNavigationInfo.TYPE_CROSS_ACTIVITY, - mCrossActivityBackAnimation.getRunner()); + mDefaultCrossActivityBackAnimation.getRunner()); } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomCrossActivityBackAnimationTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomCrossActivityBackAnimationTest.kt new file mode 100644 index 000000000000..8bf011192347 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomCrossActivityBackAnimationTest.kt @@ -0,0 +1,264 @@ +/* + * Copyright (C) 2023 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.f + */ +package com.android.wm.shell.back + +import android.app.ActivityManager +import android.app.ActivityManager.RunningTaskInfo +import android.app.AppCompatTaskInfo +import android.app.WindowConfiguration +import android.graphics.Color +import android.graphics.Point +import android.graphics.Rect +import android.os.RemoteException +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.view.Choreographer +import android.view.RemoteAnimationTarget +import android.view.SurfaceControl +import android.view.SurfaceControl.Transaction +import android.view.animation.Animation +import android.window.BackEvent +import android.window.BackMotionEvent +import android.window.BackNavigationInfo +import androidx.test.filters.SmallTest +import com.android.internal.policy.TransitionAnimation +import com.android.wm.shell.RootTaskDisplayAreaOrganizer +import com.android.wm.shell.ShellTestCase +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import junit.framework.TestCase.assertEquals +import org.junit.Assert +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyBoolean +import org.mockito.ArgumentMatchers.anyFloat +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.ArgumentMatchers.eq +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.Mockito.never +import org.mockito.Mockito.times +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner::class) +class CustomCrossActivityBackAnimationTest : ShellTestCase() { + @Mock private lateinit var backAnimationBackground: BackAnimationBackground + @Mock private lateinit var mockCloseAnimation: Animation + @Mock private lateinit var mockOpenAnimation: Animation + @Mock private lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer + @Mock private lateinit var transitionAnimation: TransitionAnimation + @Mock private lateinit var appCompatTaskInfo: AppCompatTaskInfo + @Mock private lateinit var transaction: Transaction + + private lateinit var customCrossActivityBackAnimation: CustomCrossActivityBackAnimation + private lateinit var customAnimationLoader: CustomAnimationLoader + + @Before + @Throws(Exception::class) + fun setUp() { + customAnimationLoader = CustomAnimationLoader(transitionAnimation) + customCrossActivityBackAnimation = + CustomCrossActivityBackAnimation( + context, + backAnimationBackground, + rootTaskDisplayAreaOrganizer, + transaction, + mock(Choreographer::class.java), + customAnimationLoader + ) + + whenever(transitionAnimation.loadAppTransitionAnimation(eq(PACKAGE_NAME), eq(OPEN_RES_ID))) + .thenReturn(mockOpenAnimation) + whenever(transitionAnimation.loadAppTransitionAnimation(eq(PACKAGE_NAME), eq(CLOSE_RES_ID))) + .thenReturn(mockCloseAnimation) + whenever(transaction.setColor(any(), any())).thenReturn(transaction) + whenever(transaction.setAlpha(any(), anyFloat())).thenReturn(transaction) + whenever(transaction.setCrop(any(), any())).thenReturn(transaction) + whenever(transaction.setRelativeLayer(any(), any(), anyInt())).thenReturn(transaction) + spy(customCrossActivityBackAnimation) + } + + @Test + @Throws(InterruptedException::class) + fun receiveFinishAfterInvoke() { + val finishCalled = startCustomAnimation() + try { + customCrossActivityBackAnimation.getRunner().callback.onBackInvoked() + } catch (r: RemoteException) { + Assert.fail("onBackInvoked throw remote exception") + } + finishCalled.await(1, TimeUnit.SECONDS) + } + + @Test + @Throws(InterruptedException::class) + fun receiveFinishAfterCancel() { + val finishCalled = startCustomAnimation() + try { + customCrossActivityBackAnimation.getRunner().callback.onBackCancelled() + } catch (r: RemoteException) { + Assert.fail("onBackCancelled throw remote exception") + } + finishCalled.await(1, TimeUnit.SECONDS) + } + + @Test + @Throws(InterruptedException::class) + fun receiveFinishWithoutAnimationAfterInvoke() { + val finishCalled = startCustomAnimation(targets = arrayOf()) + try { + customCrossActivityBackAnimation.getRunner().callback.onBackInvoked() + } catch (r: RemoteException) { + Assert.fail("onBackInvoked throw remote exception") + } + finishCalled.await(1, TimeUnit.SECONDS) + } + + @Test + fun testLoadCustomAnimation() { + testLoadCustomAnimation(OPEN_RES_ID, CLOSE_RES_ID, 0) + } + + @Test + fun testLoadCustomAnimationNoEnter() { + testLoadCustomAnimation(0, CLOSE_RES_ID, 0) + } + + @Test + fun testLoadWindowAnimations() { + testLoadCustomAnimation(0, 0, 30) + } + + @Test + fun testCustomAnimationHigherThanWindowAnimations() { + testLoadCustomAnimation(OPEN_RES_ID, CLOSE_RES_ID, 30) + } + + private fun testLoadCustomAnimation(enterResId: Int, exitResId: Int, windowAnimations: Int) { + val builder = + BackNavigationInfo.Builder() + .setCustomAnimation(PACKAGE_NAME, enterResId, exitResId, Color.GREEN) + .setWindowAnimations(PACKAGE_NAME, windowAnimations) + val info = builder.build().customAnimationInfo!! + whenever( + transitionAnimation.loadAnimationAttr( + eq(PACKAGE_NAME), + eq(windowAnimations), + anyInt(), + anyBoolean() + ) + ) + .thenReturn(mockCloseAnimation) + whenever(transitionAnimation.loadDefaultAnimationAttr(anyInt(), anyBoolean())) + .thenReturn(mockOpenAnimation) + val result = customAnimationLoader.loadAll(info)!! + if (exitResId != 0) { + if (enterResId == 0) { + verify(transitionAnimation, never()) + .loadAppTransitionAnimation(eq(PACKAGE_NAME), eq(enterResId)) + verify(transitionAnimation).loadDefaultAnimationAttr(anyInt(), anyBoolean()) + } else { + assertEquals(result.enterAnimation, mockOpenAnimation) + } + assertEquals(result.backgroundColor.toLong(), Color.GREEN.toLong()) + assertEquals(result.closeAnimation, mockCloseAnimation) + verify(transitionAnimation, never()) + .loadAnimationAttr(eq(PACKAGE_NAME), anyInt(), anyInt(), anyBoolean()) + } else if (windowAnimations != 0) { + verify(transitionAnimation, times(2)) + .loadAnimationAttr(eq(PACKAGE_NAME), anyInt(), anyInt(), anyBoolean()) + Assert.assertEquals(result.closeAnimation, mockCloseAnimation) + } + } + + private fun startCustomAnimation( + targets: Array<RemoteAnimationTarget> = + arrayOf(createAnimationTarget(false), createAnimationTarget(true)) + ): CountDownLatch { + val backNavigationInfo = + BackNavigationInfo.Builder() + .setCustomAnimation(PACKAGE_NAME, OPEN_RES_ID, CLOSE_RES_ID, /*backgroundColor*/ 0) + .build() + customCrossActivityBackAnimation.prepareNextAnimation( + backNavigationInfo.customAnimationInfo, + 0 + ) + val finishCalled = CountDownLatch(1) + val finishCallback = Runnable { finishCalled.countDown() } + customCrossActivityBackAnimation + .getRunner() + .startAnimation(targets, null, null, finishCallback) + customCrossActivityBackAnimation.runner.callback.onBackStarted(backMotionEventFrom(0f, 0f)) + if (targets.isNotEmpty()) { + verify(mockCloseAnimation) + .initialize(eq(BOUND_SIZE), eq(BOUND_SIZE), eq(BOUND_SIZE), eq(BOUND_SIZE)) + verify(mockOpenAnimation) + .initialize(eq(BOUND_SIZE), eq(BOUND_SIZE), eq(BOUND_SIZE), eq(BOUND_SIZE)) + } + return finishCalled + } + + private fun backMotionEventFrom(touchX: Float, progress: Float) = + BackMotionEvent( + /* touchX = */ touchX, + /* touchY = */ 0f, + /* progress = */ progress, + /* velocityX = */ 0f, + /* velocityY = */ 0f, + /* triggerBack = */ false, + /* swipeEdge = */ BackEvent.EDGE_LEFT, + /* departingAnimationTarget = */ null + ) + + private fun createAnimationTarget(open: Boolean): RemoteAnimationTarget { + val topWindowLeash = SurfaceControl() + val taskInfo = RunningTaskInfo() + taskInfo.appCompatTaskInfo = appCompatTaskInfo + taskInfo.taskDescription = ActivityManager.TaskDescription() + return RemoteAnimationTarget( + 1, + if (open) RemoteAnimationTarget.MODE_OPENING else RemoteAnimationTarget.MODE_CLOSING, + topWindowLeash, + false, + Rect(), + Rect(), + -1, + Point(0, 0), + Rect(0, 0, BOUND_SIZE, BOUND_SIZE), + Rect(), + WindowConfiguration(), + true, + null, + null, + taskInfo, + false, + -1 + ) + } + + companion object { + private const val BOUND_SIZE = 100 + private const val OPEN_RES_ID = 1000 + private const val CLOSE_RES_ID = 1001 + private const val PACKAGE_NAME = "TestPackage" + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomizeActivityAnimationTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomizeActivityAnimationTest.java deleted file mode 100644 index 158d640dca30..000000000000 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/CustomizeActivityAnimationTest.java +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Copyright (C) 2023 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.wm.shell.back; - -import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn; -import static com.android.dx.mockito.inline.extended.ExtendedMockito.times; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.fail; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; - -import android.app.WindowConfiguration; -import android.graphics.Color; -import android.graphics.Point; -import android.graphics.Rect; -import android.os.RemoteException; -import android.testing.AndroidTestingRunner; -import android.testing.TestableLooper; -import android.view.Choreographer; -import android.view.RemoteAnimationTarget; -import android.view.SurfaceControl; -import android.view.animation.Animation; -import android.window.BackNavigationInfo; - -import androidx.test.filters.SmallTest; - -import com.android.wm.shell.ShellTestCase; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.mockito.Mock; - -import java.util.concurrent.CountDownLatch; -import java.util.concurrent.TimeUnit; - -@SmallTest -@TestableLooper.RunWithLooper -@RunWith(AndroidTestingRunner.class) -public class CustomizeActivityAnimationTest extends ShellTestCase { - private static final int BOUND_SIZE = 100; - @Mock - private BackAnimationBackground mBackAnimationBackground; - @Mock - private Animation mMockCloseAnimation; - @Mock - private Animation mMockOpenAnimation; - - private CustomizeActivityAnimation mCustomizeActivityAnimation; - - @Before - public void setUp() throws Exception { - mCustomizeActivityAnimation = new CustomizeActivityAnimation(mContext, - mBackAnimationBackground, mock(SurfaceControl.Transaction.class), - mock(Choreographer.class)); - spyOn(mCustomizeActivityAnimation); - spyOn(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation); - } - - RemoteAnimationTarget createAnimationTarget(boolean open) { - SurfaceControl topWindowLeash = new SurfaceControl(); - return new RemoteAnimationTarget(1, - open ? RemoteAnimationTarget.MODE_OPENING : RemoteAnimationTarget.MODE_CLOSING, - topWindowLeash, false, new Rect(), new Rect(), -1, - new Point(0, 0), new Rect(0, 0, BOUND_SIZE, BOUND_SIZE), new Rect(), - new WindowConfiguration(), true, null, null, null, false, -1); - } - - @Test - public void receiveFinishAfterInvoke() throws InterruptedException { - spyOn(mCustomizeActivityAnimation.mCustomAnimationLoader); - doReturn(mMockCloseAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader) - .loadAnimation(any(), eq(false)); - doReturn(mMockOpenAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader) - .loadAnimation(any(), eq(true)); - - mCustomizeActivityAnimation.prepareNextAnimation( - new BackNavigationInfo.CustomAnimationInfo("TestPackage"), 0); - final RemoteAnimationTarget close = createAnimationTarget(false); - final RemoteAnimationTarget open = createAnimationTarget(true); - // start animation with remote animation targets - final CountDownLatch finishCalled = new CountDownLatch(1); - final Runnable finishCallback = finishCalled::countDown; - mCustomizeActivityAnimation - .getRunner() - .startAnimation( - new RemoteAnimationTarget[] {close, open}, null, null, finishCallback); - verify(mMockCloseAnimation).initialize(eq(BOUND_SIZE), eq(BOUND_SIZE), - eq(BOUND_SIZE), eq(BOUND_SIZE)); - verify(mMockOpenAnimation).initialize(eq(BOUND_SIZE), eq(BOUND_SIZE), - eq(BOUND_SIZE), eq(BOUND_SIZE)); - - try { - mCustomizeActivityAnimation.getRunner().getCallback().onBackInvoked(); - } catch (RemoteException r) { - fail("onBackInvoked throw remote exception"); - } - verify(mCustomizeActivityAnimation).onGestureCommitted(); - finishCalled.await(1, TimeUnit.SECONDS); - } - - @Test - public void receiveFinishAfterCancel() throws InterruptedException { - spyOn(mCustomizeActivityAnimation.mCustomAnimationLoader); - doReturn(mMockCloseAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader) - .loadAnimation(any(), eq(false)); - doReturn(mMockOpenAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader) - .loadAnimation(any(), eq(true)); - - mCustomizeActivityAnimation.prepareNextAnimation( - new BackNavigationInfo.CustomAnimationInfo("TestPackage"), 0); - final RemoteAnimationTarget close = createAnimationTarget(false); - final RemoteAnimationTarget open = createAnimationTarget(true); - // start animation with remote animation targets - final CountDownLatch finishCalled = new CountDownLatch(1); - final Runnable finishCallback = finishCalled::countDown; - mCustomizeActivityAnimation - .getRunner() - .startAnimation( - new RemoteAnimationTarget[] {close, open}, null, null, finishCallback); - verify(mMockCloseAnimation).initialize(eq(BOUND_SIZE), eq(BOUND_SIZE), - eq(BOUND_SIZE), eq(BOUND_SIZE)); - verify(mMockOpenAnimation).initialize(eq(BOUND_SIZE), eq(BOUND_SIZE), - eq(BOUND_SIZE), eq(BOUND_SIZE)); - - try { - mCustomizeActivityAnimation.getRunner().getCallback().onBackCancelled(); - } catch (RemoteException r) { - fail("onBackCancelled throw remote exception"); - } - finishCalled.await(1, TimeUnit.SECONDS); - } - - @Test - public void receiveFinishWithoutAnimationAfterInvoke() throws InterruptedException { - mCustomizeActivityAnimation.prepareNextAnimation( - new BackNavigationInfo.CustomAnimationInfo("TestPackage"), 0); - // start animation without any remote animation targets - final CountDownLatch finishCalled = new CountDownLatch(1); - final Runnable finishCallback = finishCalled::countDown; - mCustomizeActivityAnimation - .getRunner() - .startAnimation(new RemoteAnimationTarget[] {}, null, null, finishCallback); - - try { - mCustomizeActivityAnimation.getRunner().getCallback().onBackInvoked(); - } catch (RemoteException r) { - fail("onBackInvoked throw remote exception"); - } - verify(mCustomizeActivityAnimation).onGestureCommitted(); - finishCalled.await(1, TimeUnit.SECONDS); - } - - @Test - public void testLoadCustomAnimation() { - testLoadCustomAnimation(10, 20, 0); - } - - @Test - public void testLoadCustomAnimationNoEnter() { - testLoadCustomAnimation(0, 10, 0); - } - - @Test - public void testLoadWindowAnimations() { - testLoadCustomAnimation(0, 0, 30); - } - - @Test - public void testCustomAnimationHigherThanWindowAnimations() { - testLoadCustomAnimation(10, 20, 30); - } - - private void testLoadCustomAnimation(int enterResId, int exitResId, int windowAnimations) { - final String testPackage = "TestPackage"; - BackNavigationInfo.Builder builder = new BackNavigationInfo.Builder() - .setCustomAnimation(testPackage, enterResId, exitResId, Color.GREEN) - .setWindowAnimations(testPackage, windowAnimations); - final BackNavigationInfo.CustomAnimationInfo info = builder.build() - .getCustomAnimationInfo(); - - doReturn(mMockOpenAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader - .mTransitionAnimation) - .loadAppTransitionAnimation(eq(testPackage), eq(enterResId)); - doReturn(mMockCloseAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader - .mTransitionAnimation) - .loadAppTransitionAnimation(eq(testPackage), eq(exitResId)); - doReturn(mMockCloseAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader - .mTransitionAnimation) - .loadAnimationAttr(eq(testPackage), eq(windowAnimations), anyInt(), anyBoolean()); - doReturn(mMockOpenAnimation).when(mCustomizeActivityAnimation.mCustomAnimationLoader - .mTransitionAnimation).loadDefaultAnimationAttr(anyInt(), anyBoolean()); - - CustomizeActivityAnimation.AnimationLoadResult result = - mCustomizeActivityAnimation.mCustomAnimationLoader.loadAll(info); - - if (exitResId != 0) { - if (enterResId == 0) { - verify(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation, - never()).loadAppTransitionAnimation(eq(testPackage), eq(enterResId)); - verify(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation) - .loadDefaultAnimationAttr(anyInt(), anyBoolean()); - } else { - assertEquals(result.mEnterAnimation, mMockOpenAnimation); - } - assertEquals(result.mBackgroundColor, Color.GREEN); - assertEquals(result.mCloseAnimation, mMockCloseAnimation); - verify(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation, never()) - .loadAnimationAttr(eq(testPackage), anyInt(), anyInt(), anyBoolean()); - } else if (windowAnimations != 0) { - verify(mCustomizeActivityAnimation.mCustomAnimationLoader.mTransitionAnimation, - times(2)).loadAnimationAttr(eq(testPackage), anyInt(), anyInt(), anyBoolean()); - assertEquals(result.mCloseAnimation, mMockCloseAnimation); - } - } -} diff --git a/packages/PrintSpooler/res/values-night/themes.xml b/packages/PrintSpooler/res/values-night/themes.xml index 3cc64a6ef266..76fa7b921e77 100644 --- a/packages/PrintSpooler/res/values-night/themes.xml +++ b/packages/PrintSpooler/res/values-night/themes.xml @@ -24,6 +24,7 @@ <style name="Theme.SelectPrinterActivity" parent="android:style/Theme.DeviceDefault"> <item name="android:textAppearanceListItemSecondary">@style/ListItemSecondary</item> + <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item> </style> <style name="Theme.PrintActivity" parent="@android:style/Theme.DeviceDefault"> diff --git a/packages/PrintSpooler/res/values/themes.xml b/packages/PrintSpooler/res/values/themes.xml index bd9602540878..22842f724036 100644 --- a/packages/PrintSpooler/res/values/themes.xml +++ b/packages/PrintSpooler/res/values/themes.xml @@ -24,6 +24,7 @@ parent="android:style/Theme.DeviceDefault.Light"> <item name="android:textAppearanceListItemSecondary">@style/ListItemSecondary</item> <item name="android:windowLightStatusBar">true</item> + <item name="android:windowOptOutEdgeToEdgeEnforcement">true</item> </style> <style name="Theme.PrintActivity" parent="@android:style/Theme.DeviceDefault.Light"> diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt index fe1ebf975303..3976c618c179 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/AncButtonComponent.kt @@ -19,10 +19,13 @@ package com.android.systemui.volume.panel.component.anc.ui.composable import android.view.Gravity import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.height -import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -32,15 +35,15 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color import androidx.compose.ui.layout.onGloballyPositioned import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.res.stringResource -import androidx.compose.ui.semantics.Role +import androidx.compose.ui.semantics.LiveRegionMode import androidx.compose.ui.semantics.clearAndSetSemantics import androidx.compose.ui.semantics.contentDescription -import androidx.compose.ui.semantics.role +import androidx.compose.ui.semantics.liveRegion import androidx.compose.ui.semantics.semantics import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -66,13 +69,6 @@ constructor( with(LocalDensity.current) { LocalConfiguration.current.screenWidthDp.dp.toPx() } var gravity by remember { mutableIntStateOf(Gravity.CENTER_HORIZONTAL) } val isClickable = viewModel.isClickable(slice) - val onClick = - if (isClickable) { - { with(ancPopup) { show(null, gravity) } } - } else { - null - } - Column( modifier = modifier.onGloballyPositioned { @@ -81,20 +77,33 @@ constructor( verticalArrangement = Arrangement.spacedBy(12.dp), horizontalAlignment = Alignment.CenterHorizontally, ) { - SliceAndroidView( - modifier = - Modifier.height(64.dp) - .fillMaxWidth() - .semantics { - role = Role.Button + Box( + modifier = Modifier.height(64.dp), + ) { + SliceAndroidView( + modifier = modifier.fillMaxSize(), + slice = slice, + onWidthChanged = viewModel::onButtonSliceWidthChanged, + enableAccessibility = false, + ) + Button( + modifier = + modifier.fillMaxSize().padding(8.dp).semantics { + liveRegion = LiveRegionMode.Polite contentDescription = label - } - .clip(RoundedCornerShape(28.dp)), - slice = slice, - isEnabled = onClick != null, - onWidthChanged = viewModel::onButtonSliceWidthChanged, - onClick = onClick, - ) + }, + enabled = isClickable, + onClick = { with(ancPopup) { show(null, gravity) } }, + colors = + ButtonColors( + contentColor = Color.Transparent, + containerColor = Color.Transparent, + disabledContentColor = Color.Transparent, + disabledContainerColor = Color.Transparent, + ) + ) {} + } + Text( modifier = Modifier.clearAndSetSemantics {}.basicMarquee(), text = label, diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/SliceAndroidView.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/SliceAndroidView.kt index fc5d212a0be7..23d50c577300 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/SliceAndroidView.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/anc/ui/composable/SliceAndroidView.kt @@ -16,11 +16,12 @@ package com.android.systemui.volume.panel.component.anc.ui.composable -import android.annotation.SuppressLint import android.content.Context +import android.os.Bundle import android.view.ContextThemeWrapper -import android.view.MotionEvent import android.view.View +import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityNodeInfo import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.viewinterop.AndroidView @@ -32,14 +33,13 @@ import com.android.systemui.res.R fun SliceAndroidView( slice: Slice?, modifier: Modifier = Modifier, - isEnabled: Boolean = true, onWidthChanged: ((Int) -> Unit)? = null, - onClick: (() -> Unit)? = null, + enableAccessibility: Boolean = true, ) { AndroidView( modifier = modifier, factory = { context: Context -> - ClickableSliceView( + ComposeSliceView( ContextThemeWrapper(context, R.style.Widget_SliceView_VolumePanel), ) .apply { @@ -47,17 +47,18 @@ fun SliceAndroidView( isScrollable = false importantForAccessibility = View.IMPORTANT_FOR_ACCESSIBILITY_NO setShowTitleItems(true) - if (onWidthChanged != null) { - addOnLayoutChangeListener(OnWidthChangedLayoutListener(onWidthChanged)) - } } }, - update = { sliceView: ClickableSliceView -> + update = { sliceView: ComposeSliceView -> sliceView.slice = slice - sliceView.onClick = onClick - sliceView.isEnabled = isEnabled - sliceView.isClickable = isEnabled - } + sliceView.layoutListener = onWidthChanged?.let(::OnWidthChangedLayoutListener) + sliceView.enableAccessibility = enableAccessibility + }, + onRelease = { sliceView: ComposeSliceView -> + sliceView.layoutListener = null + sliceView.slice = null + sliceView.enableAccessibility = true + }, ) } @@ -83,26 +84,39 @@ class OnWidthChangedLayoutListener(private val widthChanged: (Int) -> Unit) : } } -/** - * [SliceView] that prioritises [onClick] when its clicked instead of passing the event to the slice - * first. - */ -@SuppressLint("ViewConstructor") // only used in this class -private class ClickableSliceView(context: Context) : SliceView(context) { +private class ComposeSliceView(context: Context) : SliceView(context) { + + var enableAccessibility: Boolean = true + var layoutListener: OnLayoutChangeListener? = null + set(value) { + field?.let { removeOnLayoutChangeListener(it) } + field = value + field?.let { addOnLayoutChangeListener(it) } + } - var onClick: (() -> Unit)? = null + override fun onInitializeAccessibilityNodeInfo(info: AccessibilityNodeInfo?) { + if (enableAccessibility) { + super.onInitializeAccessibilityNodeInfo(info) + } + } - init { - if (onClick != null) { - setOnClickListener {} + override fun onInitializeAccessibilityEvent(event: AccessibilityEvent?) { + if (enableAccessibility) { + super.onInitializeAccessibilityEvent(event) } } - override fun onInterceptTouchEvent(ev: MotionEvent?): Boolean { - return (isSliceViewClickable && onClick != null) || super.onInterceptTouchEvent(ev) + override fun performAccessibilityAction(action: Int, arguments: Bundle?): Boolean { + return if (enableAccessibility) { + super.performAccessibilityAction(action, arguments) + } else { + false + } } - override fun onClick(v: View?) { - onClick?.takeIf { isSliceViewClickable }?.let { it() } ?: super.onClick(v) + override fun addChildrenForAccessibility(outChildren: ArrayList<View>?) { + if (enableAccessibility) { + super.addChildrenForAccessibility(outChildren) + } } } |