Loading packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt +50 −7 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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, Loading @@ -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 Loading Loading @@ -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 " + Loading @@ -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) { Loading Loading @@ -184,6 +226,7 @@ constructor( if (TransitionAnimator.returnAnimationsEnabled()) { ghostedView.removeOnAttachStateChangeListener(detachListener) } transitionToken?.let { token -> transitionRegistry?.unregister(token) } } /** Loading Loading @@ -237,7 +280,7 @@ constructor( val insets = backgroundInsets val boundCorrections: Rect = if (ghostedView is LaunchableView) { ghostedView.getPaddingForLaunchAnimation() (ghostedView as LaunchableView).getPaddingForLaunchAnimation() } else { Rect() } Loading Loading @@ -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 Loading @@ -398,7 +441,7 @@ constructor( ghostedView.invalidate() } if (isEphemeral) { if (isEphemeral || Flags.decoupleViewControllerInAnimlib()) { onDispose() } } Loading packages/SystemUI/animation/src/com/android/systemui/animation/IViewTransitionRegistry.kt 0 → 100644 +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() } packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionRegistry.kt +29 −9 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) } } } } Loading @@ -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() } /** Loading @@ -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]) Loading @@ -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() } } } packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionToken.kt +6 −4 Original line number Diff line number Diff line Loading @@ -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()}") } packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt +59 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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() { Loading Loading @@ -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. Loading @@ -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 } } } Loading
packages/SystemUI/animation/src/com/android/systemui/animation/GhostedViewTransitionAnimatorController.kt +50 −7 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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, Loading @@ -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 Loading Loading @@ -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 " + Loading @@ -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) { Loading Loading @@ -184,6 +226,7 @@ constructor( if (TransitionAnimator.returnAnimationsEnabled()) { ghostedView.removeOnAttachStateChangeListener(detachListener) } transitionToken?.let { token -> transitionRegistry?.unregister(token) } } /** Loading Loading @@ -237,7 +280,7 @@ constructor( val insets = backgroundInsets val boundCorrections: Rect = if (ghostedView is LaunchableView) { ghostedView.getPaddingForLaunchAnimation() (ghostedView as LaunchableView).getPaddingForLaunchAnimation() } else { Rect() } Loading Loading @@ -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 Loading @@ -398,7 +441,7 @@ constructor( ghostedView.invalidate() } if (isEphemeral) { if (isEphemeral || Flags.decoupleViewControllerInAnimlib()) { onDispose() } } Loading
packages/SystemUI/animation/src/com/android/systemui/animation/IViewTransitionRegistry.kt 0 → 100644 +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() }
packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionRegistry.kt +29 −9 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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) } } } } Loading @@ -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() } /** Loading @@ -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]) Loading @@ -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() } } }
packages/SystemUI/animation/src/com/android/systemui/animation/ViewTransitionToken.kt +6 −4 Original line number Diff line number Diff line Loading @@ -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()}") }
packages/SystemUI/multivalentTests/src/com/android/systemui/animation/GhostedViewTransitionAnimatorControllerTest.kt +59 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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() { Loading Loading @@ -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. Loading @@ -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 } } }