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

Commit 333c1672 authored by Bharat Singh's avatar Bharat Singh
Browse files

[AnimLib] Integrate registry into controller

This change integrates ViewTransitionRegistry with GhostedViewTransitionAnimatorController. This change effectively decouples strong reference of View from Controller as transitioning view will now be accessed from registry.

Bug: 393241010
Flag: com.android.systemui.decouple_view_controller_in_animlib
Test: GhostedViewTransitionAnimatorControllerTest
Change-Id: I14aa8617295325e3a112421dab6c430f440df781
parent 8c6b48a9
Loading
Loading
Loading
Loading
+50 −7
Original line number Diff line number Diff line
@@ -36,6 +36,7 @@ import android.view.ViewGroupOverlay
import android.widget.FrameLayout
import com.android.internal.jank.Cuj.CujType
import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.Flags
import java.util.LinkedList
import kotlin.math.min
import kotlin.math.roundToInt
@@ -58,7 +59,7 @@ open class GhostedViewTransitionAnimatorController
@JvmOverloads
constructor(
    /** The view that will be ghosted and from which the background will be extracted. */
    private val ghostedView: View,
    transitioningView: View,

    /** The [CujType] associated to this launch animation. */
    private val launchCujType: Int? = null,
@@ -75,11 +76,24 @@ constructor(
    private val isEphemeral: Boolean = false,
    private var interactionJankMonitor: InteractionJankMonitor =
        InteractionJankMonitor.getInstance(),

    /** [ViewTransitionRegistry] to store the mapping of transitioning view and its token */
    private val transitionRegistry: IViewTransitionRegistry? =
        if (Flags.decoupleViewControllerInAnimlib()) {
            ViewTransitionRegistry.instance
        } else {
            null
        }
) : ActivityTransitionAnimator.Controller {
    override val isLaunching: Boolean = true

    /** The container to which we will add the ghost view and expanding background. */
    override var transitionContainer = ghostedView.rootView as ViewGroup
    override var transitionContainer: ViewGroup
        get() = ghostedView.rootView as ViewGroup
        set(_) {
            // empty, should never be set to avoid memory leak
        }

    private val transitionContainerOverlay: ViewGroupOverlay
        get() = transitionContainer.overlay

@@ -138,9 +152,33 @@ constructor(
            }
        }

    /** [ViewTransitionToken] to be used for storing transitioning view in [transitionRegistry] */
    private val transitionToken =
        if (Flags.decoupleViewControllerInAnimlib()) {
            ViewTransitionToken(transitioningView::class.java)
        } else {
            null
        }

    /** The view that will be ghosted and from which the background will be extracted */
    private val ghostedView: View
        get() =
            if (Flags.decoupleViewControllerInAnimlib()) {
                transitionRegistry?.getView(transitionToken!!)
            } else {
                _ghostedView
            }!!

    private val _ghostedView =
        if (Flags.decoupleViewControllerInAnimlib()) {
            null
        } else {
            transitioningView
        }

    init {
        // Make sure the View we launch from implements LaunchableView to avoid visibility issues.
        if (ghostedView !is LaunchableView) {
        if (transitioningView !is LaunchableView) {
            throw IllegalArgumentException(
                "A GhostedViewLaunchAnimatorController was created from a View that does not " +
                    "implement LaunchableView. This can lead to subtle bugs where the visibility " +
@@ -148,6 +186,10 @@ constructor(
            )
        }

        if (Flags.decoupleViewControllerInAnimlib()) {
            transitionRegistry?.register(transitionToken!!, transitioningView)
        }

        /** Find the first view with a background in [view] and its children. */
        fun findBackground(view: View): Drawable? {
            if (view.background != null) {
@@ -184,6 +226,7 @@ constructor(
        if (TransitionAnimator.returnAnimationsEnabled()) {
            ghostedView.removeOnAttachStateChangeListener(detachListener)
        }
        transitionToken?.let { token -> transitionRegistry?.unregister(token) }
    }

    /**
@@ -237,7 +280,7 @@ constructor(
        val insets = backgroundInsets
        val boundCorrections: Rect =
            if (ghostedView is LaunchableView) {
                ghostedView.getPaddingForLaunchAnimation()
                (ghostedView as LaunchableView).getPaddingForLaunchAnimation()
            } else {
                Rect()
            }
@@ -387,8 +430,8 @@ constructor(

        if (ghostedView is LaunchableView) {
            // Restore the ghosted view visibility.
            ghostedView.setShouldBlockVisibilityChanges(false)
            ghostedView.onActivityLaunchAnimationEnd()
            (ghostedView as LaunchableView).setShouldBlockVisibilityChanges(false)
            (ghostedView as LaunchableView).onActivityLaunchAnimationEnd()
        } else {
            // Make the ghosted view visible. We ensure that the view is considered VISIBLE by
            // accessibility by first making it INVISIBLE then VISIBLE (see b/204944038#comment17
@@ -398,7 +441,7 @@ constructor(
            ghostedView.invalidate()
        }

        if (isEphemeral) {
        if (isEphemeral || Flags.decoupleViewControllerInAnimlib()) {
            onDispose()
        }
    }
+56 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2025 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.systemui.animation

import android.view.View

/** Represents a Registry for holding a transitioning view mapped to a token */
interface IViewTransitionRegistry {

    /**
     * Registers the transitioning [view] mapped to a [token]
     *
     * @param token The token corresponding to the transitioning view
     * @param view The view undergoing transition
     */
    fun register(token: ViewTransitionToken, view: View)

    /**
     * Unregisters the transitioned view from its corresponding [token]
     *
     * @param token The token corresponding to the transitioning view
     */
    fun unregister(token: ViewTransitionToken)

    /**
     * Extracts a transitioning view from registry using its corresponding [token]
     *
     * @param token The token corresponding to the transitioning view
     */
    fun getView(token: ViewTransitionToken): View?

    /**
     * Return token mapped to the [view], if it is present in the registry
     *
     * @param view the transitioning view whose token we are requesting
     * @return token associated with the [view] if present, else null
     */
    fun getViewToken(view: View): ViewTransitionToken?

    /** Event call to run on registry update (on both [register] and [unregister]) */
    fun onRegistryUpdate()
}
+29 −9
Original line number Diff line number Diff line
@@ -24,7 +24,7 @@ import java.lang.ref.WeakReference
 * A registry to temporarily store the view being transitioned into a Dialog (using
 * [DialogTransitionAnimator]) or an Activity (using [ActivityTransitionAnimator])
 */
class ViewTransitionRegistry {
class ViewTransitionRegistry : IViewTransitionRegistry {

    /**
     * A map of a unique token to a WeakReference of the View being transitioned. WeakReference
@@ -45,8 +45,7 @@ class ViewTransitionRegistry {
            }

            override fun onViewDetachedFromWindow(view: View) {
                (view.getTag(R.id.tag_view_transition_token)
                        as? ViewTransitionToken)?.let { token -> unregister(token) }
                getViewToken(view)?.let { token -> unregister(token) }
            }
        }
    }
@@ -57,12 +56,12 @@ class ViewTransitionRegistry {
     * @param token unique token associated with the transitioning view
     * @param view view undergoing transitions
     */
    fun register(token: ViewTransitionToken, view: View) {
    override fun register(token: ViewTransitionToken, view: View) {
        // token embedded as a view tag enables to use a single listener for all views
        view.setTag(R.id.tag_view_transition_token, token)
        view.addOnAttachStateChangeListener(listener)
        registry[token] = WeakReference(view)
        emitCountForTrace()
        onRegistryUpdate()
    }

    /**
@@ -70,25 +69,42 @@ class ViewTransitionRegistry {
     *
     * @param token unique token associated with the transitioning view
     */
    fun unregister(token: ViewTransitionToken) {
    override fun unregister(token: ViewTransitionToken) {
        registry.remove(token)?.let {
            it.get()?.let { view ->
                view.removeOnAttachStateChangeListener(listener)
                view.setTag(R.id.tag_view_transition_token, null)
            }
            it.clear()
            onRegistryUpdate()
        }
        emitCountForTrace()
    }

    /**
     * Access a view from registry using unique "token" associated with it
     * WARNING - this returns a StrongReference to the View stored in the registry
     */
    fun getView(token: ViewTransitionToken): View? {
    override fun getView(token: ViewTransitionToken): View? {
        return registry[token]?.get()
    }

    /**
     * Return token mapped to the [view], if it is present in the registry
     *
     * @param view the transitioning view whose token we are requesting
     * @return token associated with the [view] if present, else null
     */
    override fun getViewToken(view: View): ViewTransitionToken? {
        return (view.getTag(R.id.tag_view_transition_token) as? ViewTransitionToken)?.let { token ->
            getView(token)?.let { token }
        }
    }

    /** Event call to run on registry update (on both [register] and [unregister]) */
    override fun onRegistryUpdate() {
        emitCountForTrace()
    }

    /**
     * Utility function to emit number of non-null views in the registry whenever the registry is
     * updated (via [register] or [unregister])
@@ -96,4 +112,8 @@ class ViewTransitionRegistry {
    private fun emitCountForTrace() {
        Trace.setCounter("transition_registry_view_count", registry.count().toLong())
    }

    companion object {
        val instance by lazy(LazyThreadSafetyMode.SYNCHRONIZED) { ViewTransitionRegistry() }
    }
}
+6 −4
Original line number Diff line number Diff line
@@ -16,17 +16,19 @@

package com.android.systemui.animation

import java.util.UUID

/**
 * A token uniquely mapped to a View in [ViewTransitionRegistry]. This token is guaranteed to be
 * unique as timestamp is appended to the token string
 *
 * @constructor creates an instance of [ViewTransitionToken] with token as "timestamp" or
 * "ClassName_timestamp"
 * @constructor creates an instance of [ViewTransitionToken] with token as "UUID" or
 * "ClassName_UUID"
 *
 * @property token String value of a unique token
 */
@JvmInline
value class ViewTransitionToken private constructor(val token: String) {
    constructor() : this(token = System.currentTimeMillis().toString())
    constructor(clazz: Class<*>) : this(token = clazz.simpleName + "_${System.currentTimeMillis()}")
    constructor() : this(token = UUID.randomUUID().toString())
    constructor(clazz: Class<*>) : this(token = clazz.simpleName + "_${UUID.randomUUID()}")
}
+59 −1
Original line number Diff line number Diff line
@@ -17,16 +17,20 @@
package com.android.systemui.animation

import android.os.HandlerThread
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.testing.TestableLooper
import android.view.View
import android.widget.FrameLayout
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.view.LaunchableFrameLayout
import com.google.common.truth.Truth.assertThat
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith

@@ -40,6 +44,14 @@ class GhostedViewTransitionAnimatorControllerTest : SysuiTestCase() {
    }

    private val interactionJankMonitor = FakeInteractionJankMonitor()
    private lateinit var transitionRegistry: FakeViewTransitionRegistry
    private lateinit var transitioningView: View

    @Before
    fun setup() {
        transitioningView = LaunchableFrameLayout(mContext)
        transitionRegistry = FakeViewTransitionRegistry()
    }

    @Test
    fun animatingOrphanViewDoesNotCrash() {
@@ -96,6 +108,26 @@ class GhostedViewTransitionAnimatorControllerTest : SysuiTestCase() {
        assertThat(interactionJankMonitor.finished).containsExactly(LAUNCH_CUJ, RETURN_CUJ)
    }

    @EnableFlags(Flags.FLAG_DECOUPLE_VIEW_CONTROLLER_IN_ANIMLIB)
    @Test
    fun testViewsAreRegisteredInTransitionRegistry() {
        GhostedViewTransitionAnimatorController(
            transitioningView = transitioningView,
            transitionRegistry = transitionRegistry
        )
        assertThat(transitionRegistry.registry).isNotEmpty()
    }

    @DisableFlags(Flags.FLAG_DECOUPLE_VIEW_CONTROLLER_IN_ANIMLIB)
    @Test
    fun testNotUseRegistryIfDecouplingFlagDisabled() {
        GhostedViewTransitionAnimatorController(
            transitioningView = transitioningView,
            transitionRegistry = transitionRegistry
        )
        assertThat(transitionRegistry.registry).isEmpty()
    }

    /**
     * A fake implementation of [InteractionJankMonitor] which stores ongoing and finished CUJs and
     * allows inspection.
@@ -117,4 +149,30 @@ class GhostedViewTransitionAnimatorControllerTest : SysuiTestCase() {
            return true
        }
    }

    private class FakeViewTransitionRegistry : IViewTransitionRegistry {

        val registry = mutableMapOf<ViewTransitionToken, View>()

        override fun register(token: ViewTransitionToken, view: View) {
            registry[token] = view
            view.setTag(R.id.tag_view_transition_token, token)
        }

        override fun unregister(token: ViewTransitionToken) {
            registry.remove(token)?.setTag(R.id.tag_view_transition_token, null)
        }

        override fun getView(token: ViewTransitionToken): View? {
            return registry[token]
        }

        override fun getViewToken(view: View): ViewTransitionToken? {
            return view.getTag(R.id.tag_view_transition_token) as? ViewTransitionToken
        }

        override fun onRegistryUpdate() {
            //empty
        }
    }
}