Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6222e00d authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Re-enable app handle animations" into main

parents 6928396a fe0f82a7
Loading
Loading
Loading
Loading
+2 −0
Original line number Diff line number Diff line
@@ -231,6 +231,8 @@ public enum DesktopExperienceFlags {
            Flags.FLAG_ENABLE_PRESENTATION_FOR_CONNECTED_DISPLAYS),
    ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE(Flags::enableProjectedDisplayDesktopMode, true,
            Flags.FLAG_ENABLE_PROJECTED_DISPLAY_DESKTOP_MODE),
    ENABLE_REENABLE_APP_HANDLE_ANIMATIONS(Flags::reenableAppHandleAnimations,
            false, Flags.FLAG_REENABLE_APP_HANDLE_ANIMATIONS),
    ENABLE_REJECT_HOME_TRANSITION(
            Flags::enableRejectHomeTransition, true,
            Flags.FLAG_ENABLE_REJECT_HOME_TRANSITION),
+10 −0
Original line number Diff line number Diff line
@@ -1717,6 +1717,16 @@ flag {
    }
}

flag {
    name: "reenable_app_handle_animations"
    namespace: "lse_desktop_experience"
    description: "Re-enables the app handle animations"
    bug: "412647178"
    metadata {
        purpose: PURPOSE_BUGFIX
    }
}

flag {
    name: "remove_desk_on_last_task_removal"
    namespace: "lse_desktop_experience"
+10 −1
Original line number Diff line number Diff line
@@ -18,6 +18,7 @@ package com.android.wm.shell.windowdecor.common
import android.annotation.ColorInt
import android.annotation.IntRange
import android.app.ActivityManager.RunningTaskInfo
import android.app.ActivityManager.TaskDescription
import android.content.Context
import android.content.res.Configuration
import android.content.res.Configuration.UI_MODE_NIGHT_MASK
@@ -71,9 +72,17 @@ internal class DecorThemeUtil(private val context: Context) {

    /** Returns the [Theme] used by the app with the given [RunningTaskInfo]. */
    fun getAppTheme(task: RunningTaskInfo): Theme {
        return task.taskDescription?.let { getAppTheme(it) } ?: systemTheme
    }

    /** Returns the [Theme] used by the app with the given [TaskDescription]. */
    fun getAppTheme(description: TaskDescription): Theme {
        // TODO: use app's uiMode to find its actual light/dark value. It needs to be added to the
        //   TaskInfo/TaskDescription.
        val backgroundColor = task.taskDescription?.backgroundColor ?: return systemTheme
        val backgroundColor = description.backgroundColor
        if (backgroundColor == Color.TRANSPARENT) {
            return systemTheme
        }
        return if (Color.valueOf(backgroundColor).luminance() < 0.5) {
            Theme.DARK
        } else {
+266 −35
Original line number Diff line number Diff line
@@ -14,42 +14,80 @@
 * limitations under the License.
 */

package com.android.wm.shell.windowdecor
package com.android.wm.shell.windowdecor.viewholder

import android.animation.Animator
import android.animation.ObjectAnimator
import android.animation.ValueAnimator
import android.annotation.ColorInt
import android.content.res.ColorStateList
import android.graphics.Color
import android.os.SystemProperties
import android.view.View
import android.view.View.Visibility
import android.view.animation.PathInterpolator
import android.widget.ImageButton
import android.window.DesktopExperienceFlags
import androidx.core.animation.doOnEnd
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.protolog.ProtoLog
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_WINDOW_DECORATION
import com.android.wm.shell.shared.animation.Interpolators

/** Animates the Desktop View's app handle. */
class AppHandleAnimator(private val appHandleView: View, private val captionHandle: ImageButton) {
class AppHandleAnimator(appHandleView: View, private val captionHandle: ImageButton) {
    companion object {
        private val DEBUG_ANIMATOR_STEPS =
            SystemProperties.getBoolean(
                "persist.wm.debug.window_decoration_app_handle_visibility_anim_debug_steps",
                false,
            )

        //  Constants for animating the whole caption
        private const val APP_HANDLE_ALPHA_FADE_IN_ANIMATION_DURATION_MS: Long = 275L
        private const val APP_HANDLE_ALPHA_FADE_OUT_ANIMATION_DURATION_MS: Long = 340
        private val APP_HANDLE_ANIMATION_INTERPOLATOR = PathInterpolator(0.4f, 0f, 0.2f, 1f)
        @VisibleForTesting
        val APP_HANDLE_ALPHA_FADE_IN_ANIMATION_DURATION_MS =
            SystemProperties.getLong(
                "persist.wm.debug.window_decoration_app_handle_fade_in_duration_ms",
                275L,
            )
        @VisibleForTesting
        val APP_HANDLE_ALPHA_FADE_OUT_ANIMATION_DURATION_MS =
            SystemProperties.getLong(
                "persist.wm.debug.window_decoration_app_handle_fade_out_duration_ms",
                340L,
            )
        private val APP_HANDLE_COLOR_ANIMATION_DURATION_MS =
            SystemProperties.getLong(
                "persist.wm.debug.window_decoration_app_handle_color_duration_ms",
                275L,
            )
        @VisibleForTesting
        val APP_HANDLE_FADE_ANIMATION_INTERPOLATOR = PathInterpolator(0.4f, 0f, 0.2f, 1f)

        // Constants for animating the caption's handle
        private const val HANDLE_ANIMATION_DURATION: Long = 100
        private val HANDLE_ANIMATION_INTERPOLATOR = Interpolators.FAST_OUT_SLOW_IN
    }

    private val visibilityAnimator = VisibilityAnimator(appHandleView)
    private val colorAnimator = ColorAnimator(captionHandle)
    private var animator: ObjectAnimator? = null

    /** Animates the given caption view to the given visibility after a visibility change. */
    fun animateVisibilityChange(@Visibility visible: Int) {
        when (visible) {
            View.VISIBLE -> animateShowAppHandle()
            else -> animateHideAppHandle()
    /** Animates the app handle to the given visibility after a visibility change. */
    fun animateVisibilityChange(visible: Boolean) {
        cancelCaptionHandleAlphaAnimation()
        visibilityAnimator.animate(visible)
    }

    /** Animates the app handle to the given color. */
    fun animateColorChange(@ColorInt color: Int) {
        colorAnimator.animate(color)
    }

    /** Animate appearance/disappearance of caption's handle. */
    fun animateCaptionHandleAlpha(startValue: Float, endValue: Float) {
        cancel()
        cancelCaptionHandleAlphaAnimation()
        visibilityAnimator.cancel()
        animator =
            ObjectAnimator.ofFloat(captionHandle, View.ALPHA, startValue, endValue).apply {
                duration = HANDLE_ANIMATION_DURATION
@@ -58,33 +96,226 @@ class AppHandleAnimator(private val appHandleView: View, private val captionHand
            }
    }

    private fun animateShowAppHandle() {
        cancel()
        appHandleView.alpha = 0f
        appHandleView.visibility = View.VISIBLE
        animator =
            ObjectAnimator.ofFloat(appHandleView, View.ALPHA, 1f).apply {
                duration = APP_HANDLE_ALPHA_FADE_IN_ANIMATION_DURATION_MS
                interpolator = APP_HANDLE_ANIMATION_INTERPOLATOR
                start()
    private fun cancelCaptionHandleAlphaAnimation() {
        animator?.removeAllListeners()
        animator?.cancel()
        animator = null
    }

    /** Cancels any active animations. */
    fun cancel() {
        if (DesktopExperienceFlags.ENABLE_REENABLE_APP_HANDLE_ANIMATIONS.isTrue) {
            visibilityAnimator.cancel()
            colorAnimator.cancel()
        }
        cancelCaptionHandleAlphaAnimation()
    }

    /** Returns the current visibility animator. */
    @VisibleForTesting fun getAnimator(): ValueAnimator? = visibilityAnimator.animator

    private class VisibilityAnimator(private val targetView: View) {
        private enum class Target(
            val start: Float,
            val end: Float,
            @Visibility val viewVisibility: Int,
            val duration: Long,
        ) {
            VISIBLE(0f, 1f, View.VISIBLE, APP_HANDLE_ALPHA_FADE_IN_ANIMATION_DURATION_MS),
            INVISIBLE(1f, 0f, View.GONE, APP_HANDLE_ALPHA_FADE_OUT_ANIMATION_DURATION_MS),
        }

        private var currentAnimator: ObjectAnimator? = null
        private var currentTarget: Target? = null
        private val doOnEnd = DoOnEnd(targetView)

        /** The current animator. */
        @VisibleForTesting
        val animator: ValueAnimator?
            get() = currentAnimator

        /** Animates the target view. */
        fun animate(visible: Boolean) {
            animate(if (visible) Target.VISIBLE else Target.INVISIBLE)
        }

        /** Cancels the ongoing animation. */
        fun cancel() {
            currentAnimator?.removeAllListeners()
            currentAnimator?.cancel()
            reset()
        }

        private fun animate(target: Target) {
            val inProgress = currentTarget
            logD("animate from=%s to=%s", inProgress?.name, target.name)
            when {
                inProgress == null -> {
                    // Not animating, animate from start if needed.
                    if (targetView.visibility == target.viewVisibility) {
                        logD("skipping animation, already at target")
                        return
                    } else {
                        logD("animating from start")
                        targetView.visibility = View.VISIBLE
                        targetView.alpha = target.start
                        doOnEnd.target = target
                        currentAnimator = newAnimator(target).apply { start() }
                    }
                }
                target == inProgress -> {
                    logD("skipping animation, already animating to target")
                    return
                }
                else -> {
                    logD("was animating to opposite target, reversing")
                    doOnEnd.target = target
                    currentAnimator?.reverse()
                }
            }
            currentTarget = target
        }

        @OptIn(ExperimentalStdlibApi::class)
        private fun newAnimator(target: Target): ObjectAnimator =
            ObjectAnimator.ofFloat(targetView, View.ALPHA, target.end).apply {
                duration = target.duration
                interpolator = APP_HANDLE_FADE_ANIMATION_INTERPOLATOR
                if (DEBUG_ANIMATOR_STEPS) {
                    addUpdateListener { animator ->
                        logD(
                            "update: animator=ObjectAnimator@%s f=%f alpha=%f",
                            animator.hashCode().toHexString(),
                            animator.animatedFraction,
                            animator.animatedValue,
                        )
                    }
                }
                doOnEnd(doOnEnd)
            }

        private fun reset() {
            currentAnimator = null
            currentTarget = null
        }

        private inner class DoOnEnd(private val targetView: View) : (Animator) -> Unit {
            var target: Target? = null

            override fun invoke(animator: Animator) {
                if (DEBUG_ANIMATOR_STEPS) {
                    logD("end: target=%s", target)
                }
                target?.let { targetView.visibility = it.viewVisibility }
                target = null
                reset()
            }
        }

        private fun logD(msg: String, vararg arguments: Any?) {
            ProtoLog.d(WM_SHELL_WINDOW_DECORATION, "%s: $msg", TAG, *arguments)
        }

        companion object {
            private const val TAG = "AppHandleVisibilityAnimator"
        }
    }

    private class ColorAnimator(private val targetView: ImageButton) {
        private var currentAnimator: ValueAnimator? = null
        @ColorInt private var currentTarget: Int? = null

    private fun animateHideAppHandle() {
        /** Animates the target view. */
        fun animate(@ColorInt color: Int) {
            val inProgress = currentTarget
            when {
                color == inProgress -> {
                    logD("skipping animation, already animating to target")
                    return
                }
                else -> {
                    if (inProgress != null) {
                        logD("was animating to other target, cancelling first")
                        currentAnimator?.apply {
                            removeAllListeners()
                            cancel()
        animator =
            ObjectAnimator.ofFloat(appHandleView, View.ALPHA, 0f).apply {
                duration = APP_HANDLE_ALPHA_FADE_OUT_ANIMATION_DURATION_MS
                interpolator = APP_HANDLE_ANIMATION_INTERPOLATOR
                doOnEnd { appHandleView.visibility = View.GONE }
                start()
                        }
                        currentAnimator = null
                    }
                    val fromColor = getCurrentColor()
                    if (fromColor == color) {
                        logD("skipping animation, already at target")
                        return
                    }
                    if (fromColor == null) {
                        logD("skipping animation, no current color to animate from")
                        targetView.imageTintList = ColorStateList.valueOf(color)
                        return
                    }
                    logD("animate from=%s to=%s", fromColor.toArgbString(), color.toArgbString())
                    currentAnimator = newAnimator(fromColor, color).apply { start() }
                }
            }
            currentTarget = color
        }

    /** Cancels any active animations. */
        /** Cancels the ongoing animation. */
        fun cancel() {
        animator?.removeAllListeners()
        animator?.cancel()
        animator = null
            currentAnimator?.removeAllListeners()
            currentAnimator?.cancel()
            reset()
        }

        private fun getCurrentColor() = targetView.imageTintList?.defaultColor

        private fun reset() {
            currentAnimator = null
            currentTarget = null
        }

        @OptIn(ExperimentalStdlibApi::class)
        private fun newAnimator(@ColorInt from: Int, @ColorInt to: Int): ValueAnimator =
            ValueAnimator.ofArgb(from, to)
                .setDuration(APP_HANDLE_COLOR_ANIMATION_DURATION_MS)
                .apply {
                    addUpdateListener { animator ->
                        targetView.imageTintList =
                            ColorStateList.valueOf(animator.animatedValue as Int)
                        if (DEBUG_ANIMATOR_STEPS) {
                            logD(
                                "update: animator=ValueAnimator@%s f=%f color=%f",
                                animator.hashCode().toHexString(),
                                animator.animatedFraction,
                                (animator.animatedValue as Int).toArgbString(),
                            )
                        }
                    }
                    doOnEnd { animator ->
                        if (DEBUG_ANIMATOR_STEPS) {
                            logD(
                                "end: animator=ValueAnimator@%s",
                                animator.hashCode().toHexString(),
                            )
                        }
                        reset()
                    }
                }

        private fun logD(msg: String, vararg arguments: Any?) {
            ProtoLog.d(WM_SHELL_WINDOW_DECORATION, "%s: $msg", TAG, *arguments)
        }

        private fun @receiver:ColorInt Int.toArgbString() =
            String.format(
                "#%02X%02X%02X%02X",
                Color.alpha(this),
                Color.red(this),
                Color.green(this),
                Color.blue(this),
            )

        companion object {
            private const val TAG = "AppHandleColorAnimator"
        }
    }
}
+65 −15
Original line number Diff line number Diff line
@@ -15,20 +15,22 @@
 */
package com.android.wm.shell.windowdecor.viewholder

import android.animation.ValueAnimator
import android.annotation.ColorInt
import android.app.ActivityManager.RunningTaskInfo
import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM
import android.content.Context
import android.content.res.ColorStateList
import android.graphics.Color
import android.graphics.Point
import android.hardware.input.InputManager
import android.os.Bundle
import android.os.Handler
import android.view.InsetsFlags
import android.view.LayoutInflater
import android.view.MotionEvent.ACTION_DOWN
import android.view.SurfaceControl
import android.view.View
import android.view.View.OnClickListener
import android.view.ViewDebug
import android.view.WindowInsetsController.APPEARANCE_LIGHT_STATUS_BARS
import android.view.WindowManager
import android.view.accessibility.AccessibilityEvent
@@ -39,16 +41,20 @@ import android.window.DesktopExperienceFlags
import android.window.DesktopModeFlags
import androidx.core.view.ViewCompat
import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat
import com.android.internal.annotations.VisibleForTesting
import com.android.internal.policy.SystemBarUtils
import com.android.internal.protolog.ProtoLog
import com.android.window.flags.Flags
import com.android.wm.shell.R
import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger
import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum.A11Y_APP_HANDLE_MENU_OPENED
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_WINDOW_DECORATION
import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper
import com.android.wm.shell.windowdecor.AppHandleAnimator
import com.android.wm.shell.windowdecor.WindowDecorLinearLayout
import com.android.wm.shell.windowdecor.WindowManagerWrapper
import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer
import com.android.wm.shell.windowdecor.common.DecorThemeUtil
import com.android.wm.shell.windowdecor.common.Theme
import com.android.wm.shell.windowdecor.extension.identityHashCode

/**
@@ -86,6 +92,7 @@ class AppHandleViewHolder(
    private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption)
    private val captionHandle: ImageButton = rootView.requireViewById(R.id.caption_handle)
    private val inputManager = context.getSystemService(InputManager::class.java)
    private val decorThemeUtil = DecorThemeUtil(context)
    private val animator: AppHandleAnimator = AppHandleAnimator(rootView, captionHandle)
    private var statusBarInputLayerExists = false

@@ -144,7 +151,11 @@ class AppHandleViewHolder(
        isCaptionVisible: Boolean,
    ) {
        setVisibility(isCaptionVisible)
        if (DesktopExperienceFlags.ENABLE_REENABLE_APP_HANDLE_ANIMATIONS.isTrue) {
            animator.animateColorChange(getCaptionHandleBarColor(taskInfo))
        } else {
            captionHandle.imageTintList = ColorStateList.valueOf(getCaptionHandleBarColor(taskInfo))
        }
        this.taskInfo = taskInfo
        if (
            DesktopExperienceFlags.ENABLE_REMOVE_STATUS_BAR_INPUT_LAYER.isTrue &&
@@ -300,6 +311,10 @@ class AppHandleViewHolder(
    }

    private fun setVisibility(visible: Boolean) {
        if (DesktopExperienceFlags.ENABLE_REENABLE_APP_HANDLE_ANIMATIONS.isTrue) {
            animator.animateVisibilityChange(visible)
            return
        }
        val v = if (visible) View.VISIBLE else View.GONE
        if (
            captionView.visibility == v ||
@@ -307,11 +322,10 @@ class AppHandleViewHolder(
        ) {
            return
        }
        // TODO(b/405251465): animate app handle visibility change after creation and animation are
        //  moved to a background thread.
        captionView.visibility = v
    }

    @ColorInt
    private fun getCaptionHandleBarColor(taskInfo: RunningTaskInfo): Int {
        return if (shouldUseLightCaptionColors(taskInfo)) {
            context.getColor(R.color.desktop_mode_caption_handle_bar_light)
@@ -325,16 +339,42 @@ class AppHandleViewHolder(
     * with the caption background color.
     */
    private fun shouldUseLightCaptionColors(taskInfo: RunningTaskInfo): Boolean {
        return taskInfo.taskDescription?.let { taskDescription ->
            if (
                Color.alpha(taskDescription.statusBarColor) != 0 &&
                    taskInfo.windowingMode == WINDOWING_MODE_FREEFORM
            ) {
                Color.valueOf(taskDescription.statusBarColor).luminance() < 0.5
            } else {
                taskDescription.systemBarsAppearance and APPEARANCE_LIGHT_STATUS_BARS == 0
        val description = taskInfo.taskDescription
        if (description == null) {
            logD("color calculation: using light color, reason: null description")
            return false
        }
        if (description.systemBarsAppearance == 0) {
            val bgColor = description.backgroundColor
            when (decorThemeUtil.getAppTheme(description)) {
                Theme.LIGHT -> {
                    logD(
                        "color calculation: using light color, reason: light app theme (bgColor=%s)",
                        bgColor,
                    )
                    return false
                }
                Theme.DARK -> {
                    logD(
                        "color calculation: using dark color, reason: dark app theme (bgColor=%s)",
                        bgColor,
                    )
                    return true
                }
            }
        } ?: false
        }
        val hasDarkSystemBarsAppearance =
            description.systemBarsAppearance and APPEARANCE_LIGHT_STATUS_BARS == 0
        logD(
            "color calculation: using %s color, reason: systemBarsAppearance=%s",
            if (hasDarkSystemBarsAppearance) "light" else "dark",
            ViewDebug.flagsToString(
                InsetsFlags::class.java,
                "appearance",
                description.systemBarsAppearance,
            ),
        )
        return hasDarkSystemBarsAppearance
    }

    /** Sets whether the caption's handle is currently being hovered over. */
@@ -355,6 +395,12 @@ class AppHandleViewHolder(
        animator.cancel()
    }

    private fun logD(msg: String, vararg arguments: Any?) {
        ProtoLog.d(WM_SHELL_WINDOW_DECORATION, "%s: $msg", TAG, *arguments)
    }

    @VisibleForTesting fun getAnimator(): ValueAnimator? = animator.getAnimator()

    @OptIn(ExperimentalStdlibApi::class)
    override fun toString(): String {
        return "AppHandleViewHolder(" + "rootView=${rootView.identityHashCode.toHexString()}" + ")"
@@ -385,4 +431,8 @@ class AppHandleViewHolder(
                desktopModeUiEventLogger,
            )
    }

    companion object {
        private const val TAG = "AppHandleViewHolder"
    }
}
Loading