diff options
8 files changed, 408 insertions, 7 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt index c3dc84d0a12c..a6a63624cf7c 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/SceneContainer.kt @@ -33,6 +33,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalView import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ContentKey import com.android.compose.animation.scene.MutableSceneTransitionLayoutState @@ -43,11 +44,13 @@ import com.android.compose.animation.scene.SceneTransitions import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.compose.animation.scene.observableTransitionState +import com.android.systemui.lifecycle.rememberActivated import com.android.systemui.qs.ui.adapter.QSSceneAdapter import com.android.systemui.qs.ui.composable.QuickSettingsTheme import com.android.systemui.ribbon.ui.composable.BottomRightCornerRibbon import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.scene.ui.view.SceneJankMonitor import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel import javax.inject.Provider @@ -82,16 +85,38 @@ fun SceneContainer( sceneTransitions: SceneTransitions, dataSourceDelegator: SceneDataSourceDelegator, qsSceneAdapter: Provider<QSSceneAdapter>, + sceneJankMonitorFactory: SceneJankMonitor.Factory, modifier: Modifier = Modifier, ) { val coroutineScope = rememberCoroutineScope() - val state: MutableSceneTransitionLayoutState = remember { - MutableSceneTransitionLayoutState( - initialScene = initialSceneKey, - canChangeScene = { toScene -> viewModel.canChangeScene(toScene) }, - transitions = sceneTransitions, - ) - } + + val view = LocalView.current + val sceneJankMonitor = + rememberActivated(traceName = "sceneJankMonitor") { sceneJankMonitorFactory.create() } + + val state: MutableSceneTransitionLayoutState = + remember(view, sceneJankMonitor) { + MutableSceneTransitionLayoutState( + initialScene = initialSceneKey, + canChangeScene = { toScene -> viewModel.canChangeScene(toScene) }, + transitions = sceneTransitions, + onTransitionStart = { transition -> + sceneJankMonitor.onTransitionStart( + view = view, + from = transition.fromContent, + to = transition.toContent, + cuj = transition.cuj, + ) + }, + onTransitionEnd = { transition -> + sceneJankMonitor.onTransitionEnd( + from = transition.fromContent, + to = transition.toContent, + cuj = transition.cuj, + ) + }, + ) + } DisposableEffect(state) { val dataSource = SceneTransitionLayoutDataSource(state, coroutineScope) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt index b33a83cf202a..a65415509d38 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/composable/BouncerPredictiveBackTest.kt @@ -69,6 +69,7 @@ import com.android.systemui.scene.shared.model.sceneDataSourceDelegator import com.android.systemui.scene.ui.composable.Scene import com.android.systemui.scene.ui.composable.SceneContainer import com.android.systemui.scene.ui.composable.SceneContainerTransitions +import com.android.systemui.scene.ui.view.sceneJankMonitorFactory import com.android.systemui.testKosmos import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.awaitCancellation @@ -193,6 +194,7 @@ class BouncerPredictiveBackTest : SysuiTestCase() { overlayByKey = emptyMap(), dataSourceDelegator = kosmos.sceneDataSourceDelegator, qsSceneAdapter = { kosmos.fakeQsSceneAdapter }, + sceneJankMonitorFactory = kosmos.sceneJankMonitorFactory, ) } }, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/view/SceneJankMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/view/SceneJankMonitorTest.kt new file mode 100644 index 000000000000..19369e7b343d --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/ui/view/SceneJankMonitorTest.kt @@ -0,0 +1,206 @@ +/* + * 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.scene.ui.view + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.internal.jank.Cuj +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor +import com.android.systemui.jank.interactionJankMonitor +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.runCurrent +import com.android.systemui.kosmos.runTestWithSnapshots +import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.kotlin.any +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.times +import org.mockito.kotlin.verify + +@SmallTest +@RunWith(AndroidJUnit4::class) +class SceneJankMonitorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val underTest: SceneJankMonitor = kosmos.sceneJankMonitorFactory.create() + + @Before + fun setUp() { + underTest.activateIn(kosmos.testScope) + } + + @Test + fun onTransitionStart_withProvidedCuj_beginsThatCuj() = + kosmos.runTestWithSnapshots { + val cuj = 1337 + underTest.onTransitionStart( + view = mock(), + from = Scenes.Communal, + to = Scenes.Dream, + cuj = cuj, + ) + verify(interactionJankMonitor).begin(any(), eq(cuj)) + verify(interactionJankMonitor, never()).end(anyInt()) + } + + @Test + fun onTransitionEnd_withProvidedCuj_endsThatCuj() = + kosmos.runTestWithSnapshots { + val cuj = 1337 + underTest.onTransitionEnd(from = Scenes.Communal, to = Scenes.Dream, cuj = cuj) + verify(interactionJankMonitor, never()).begin(any(), anyInt()) + verify(interactionJankMonitor).end(cuj) + } + + @Test + fun bouncer_authMethodPin() = + kosmos.runTestWithSnapshots { + bouncer( + authenticationMethod = AuthenticationMethodModel.Pin, + appearCuj = Cuj.CUJ_LOCKSCREEN_PIN_APPEAR, + disappearCuj = Cuj.CUJ_LOCKSCREEN_PIN_DISAPPEAR, + ) + } + + @Test + fun bouncer_authMethodSim() = + kosmos.runTestWithSnapshots { + bouncer( + authenticationMethod = AuthenticationMethodModel.Sim, + appearCuj = Cuj.CUJ_LOCKSCREEN_PIN_APPEAR, + disappearCuj = Cuj.CUJ_LOCKSCREEN_PIN_DISAPPEAR, + // When the auth method is SIM, unlocking doesn't work like normal. Instead of + // leaving the bouncer, the bouncer is switched over to the real authentication + // method when the SIM is unlocked. + // + // Therefore, there's no point in testing this code path and it will, in fact, fail + // to unlock. + testUnlockedDisappearance = false, + ) + } + + @Test + fun bouncer_authMethodPattern() = + kosmos.runTestWithSnapshots { + bouncer( + authenticationMethod = AuthenticationMethodModel.Pattern, + appearCuj = Cuj.CUJ_LOCKSCREEN_PATTERN_APPEAR, + disappearCuj = Cuj.CUJ_LOCKSCREEN_PATTERN_DISAPPEAR, + ) + } + + @Test + fun bouncer_authMethodPassword() = + kosmos.runTestWithSnapshots { + bouncer( + authenticationMethod = AuthenticationMethodModel.Password, + appearCuj = Cuj.CUJ_LOCKSCREEN_PASSWORD_APPEAR, + disappearCuj = Cuj.CUJ_LOCKSCREEN_PASSWORD_DISAPPEAR, + ) + } + + private fun Kosmos.bouncer( + authenticationMethod: AuthenticationMethodModel, + appearCuj: Int, + disappearCuj: Int, + testUnlockedDisappearance: Boolean = true, + ) { + // Set up state: + fakeAuthenticationRepository.setAuthenticationMethod(authenticationMethod) + runCurrent() + + fun verifyCujCounts( + beginAppearCount: Int = 0, + beginDisappearCount: Int = 0, + endAppearCount: Int = 0, + endDisappearCount: Int = 0, + ) { + verify(interactionJankMonitor, times(beginAppearCount)).begin(any(), eq(appearCuj)) + verify(interactionJankMonitor, times(beginDisappearCount)) + .begin(any(), eq(disappearCuj)) + verify(interactionJankMonitor, times(endAppearCount)).end(appearCuj) + verify(interactionJankMonitor, times(endDisappearCount)).end(disappearCuj) + } + + // Precondition checks: + assertThat(deviceUnlockedInteractor.deviceUnlockStatus.value.isUnlocked).isFalse() + verifyCujCounts() + + // Bouncer appears CUJ: + underTest.onTransitionStart( + view = mock(), + from = Scenes.Lockscreen, + to = Scenes.Bouncer, + cuj = null, + ) + verifyCujCounts(beginAppearCount = 1) + underTest.onTransitionEnd(from = Scenes.Lockscreen, to = Scenes.Bouncer, cuj = null) + verifyCujCounts(beginAppearCount = 1, endAppearCount = 1) + + // Bouncer disappear CUJ but it doesn't log because the device isn't unlocked. + underTest.onTransitionStart( + view = mock(), + from = Scenes.Bouncer, + to = Scenes.Lockscreen, + cuj = null, + ) + verifyCujCounts(beginAppearCount = 1, endAppearCount = 1) + underTest.onTransitionEnd(from = Scenes.Bouncer, to = Scenes.Lockscreen, cuj = null) + verifyCujCounts(beginAppearCount = 1, endAppearCount = 1) + + if (!testUnlockedDisappearance) { + return + } + + // Unlock the device and transition away from the bouncer. + fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + runCurrent() + assertThat(deviceUnlockedInteractor.deviceUnlockStatus.value.isUnlocked).isTrue() + + // Bouncer disappear CUJ and it doeslog because the device is unlocked. + underTest.onTransitionStart( + view = mock(), + from = Scenes.Bouncer, + to = Scenes.Gone, + cuj = null, + ) + verifyCujCounts(beginAppearCount = 1, endAppearCount = 1, beginDisappearCount = 1) + underTest.onTransitionEnd(from = Scenes.Bouncer, to = Scenes.Gone, cuj = null) + verifyCujCounts( + beginAppearCount = 1, + endAppearCount = 1, + beginDisappearCount = 1, + endDisappearCount = 1, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneJankMonitor.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneJankMonitor.kt new file mode 100644 index 000000000000..48a49c60d8a2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneJankMonitor.kt @@ -0,0 +1,124 @@ +/* + * 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.scene.ui.view + +import android.view.View +import androidx.compose.runtime.getValue +import com.android.compose.animation.scene.ContentKey +import com.android.internal.jank.Cuj +import com.android.internal.jank.Cuj.CujType +import com.android.internal.jank.InteractionJankMonitor +import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor +import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.lifecycle.Hydrator +import com.android.systemui.scene.shared.model.Scenes +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject + +/** + * Monitors scene transitions and reports the beginning and ending of each scene-related CUJ. + * + * This general-purpose monitor can be expanded to include other rules that respond to the beginning + * and/or ending of transitions and reports jank CUI markers to the [InteractionJankMonitor]. + */ +class SceneJankMonitor +@AssistedInject +constructor( + authenticationInteractor: AuthenticationInteractor, + private val deviceUnlockedInteractor: DeviceUnlockedInteractor, + private val interactionJankMonitor: InteractionJankMonitor, +) : ExclusiveActivatable() { + + private val hydrator = Hydrator("SceneJankMonitor.hydrator") + private val authMethod: AuthenticationMethodModel? by + hydrator.hydratedStateOf( + traceName = "authMethod", + initialValue = null, + source = authenticationInteractor.authenticationMethod, + ) + + override suspend fun onActivated(): Nothing { + hydrator.activate() + } + + /** + * Notifies that a transition is at its start. + * + * Should be called exactly once each time a new transition starts. + */ + fun onTransitionStart(view: View, from: ContentKey, to: ContentKey, @CujType cuj: Int?) { + cuj.orCalculated(from, to) { nonNullCuj -> interactionJankMonitor.begin(view, nonNullCuj) } + } + + /** + * Notifies that the previous transition is at its end. + * + * Should be called exactly once each time a transition ends. + */ + fun onTransitionEnd(from: ContentKey, to: ContentKey, @CujType cuj: Int?) { + cuj.orCalculated(from, to) { nonNullCuj -> interactionJankMonitor.end(nonNullCuj) } + } + + /** + * Returns this CUI marker (CUJ identifier), one that's calculated based on other state, or + * `null`, if no appropriate CUJ could be calculated. + */ + private fun Int?.orCalculated( + from: ContentKey, + to: ContentKey, + ifNotNull: (nonNullCuj: Int) -> Unit, + ) { + val thisOrCalculatedCuj = this ?: calculatedCuj(from = from, to = to) + + if (thisOrCalculatedCuj != null) { + ifNotNull(thisOrCalculatedCuj) + } + } + + @CujType + private fun calculatedCuj(from: ContentKey, to: ContentKey): Int? { + val isDeviceUnlocked = deviceUnlockedInteractor.deviceUnlockStatus.value.isUnlocked + return when { + to == Scenes.Bouncer -> + when (authMethod) { + is AuthenticationMethodModel.Pin, + is AuthenticationMethodModel.Sim -> Cuj.CUJ_LOCKSCREEN_PIN_APPEAR + is AuthenticationMethodModel.Pattern -> Cuj.CUJ_LOCKSCREEN_PATTERN_APPEAR + is AuthenticationMethodModel.Password -> Cuj.CUJ_LOCKSCREEN_PASSWORD_APPEAR + is AuthenticationMethodModel.None -> null + null -> null + } + from == Scenes.Bouncer && isDeviceUnlocked -> + when (authMethod) { + is AuthenticationMethodModel.Pin, + is AuthenticationMethodModel.Sim -> Cuj.CUJ_LOCKSCREEN_PIN_DISAPPEAR + is AuthenticationMethodModel.Pattern -> Cuj.CUJ_LOCKSCREEN_PATTERN_DISAPPEAR + is AuthenticationMethodModel.Password -> Cuj.CUJ_LOCKSCREEN_PASSWORD_DISAPPEAR + is AuthenticationMethodModel.None -> null + null -> null + } + else -> null + } + } + + @AssistedFactory + interface Factory { + fun create(): SceneJankMonitor + } +} diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt index c45906840385..b8da2274eec1 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt @@ -34,6 +34,7 @@ class SceneWindowRootView(context: Context, attrs: AttributeSet?) : WindowRootVi layoutInsetController: LayoutInsetsController, sceneDataSourceDelegator: SceneDataSourceDelegator, qsSceneAdapter: Provider<QSSceneAdapter>, + sceneJankMonitorFactory: SceneJankMonitor.Factory, ) { setLayoutInsetsController(layoutInsetController) SceneWindowRootViewBinder.bind( @@ -52,6 +53,7 @@ class SceneWindowRootView(context: Context, attrs: AttributeSet?) : WindowRootVi }, dataSourceDelegator = sceneDataSourceDelegator, qsSceneAdapter = qsSceneAdapter, + sceneJankMonitorFactory = sceneJankMonitorFactory, ) } diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt index f7061d9af961..7da007c2fe53 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt @@ -74,6 +74,7 @@ object SceneWindowRootViewBinder { onVisibilityChangedInternal: (isVisible: Boolean) -> Unit, dataSourceDelegator: SceneDataSourceDelegator, qsSceneAdapter: Provider<QSSceneAdapter>, + sceneJankMonitorFactory: SceneJankMonitor.Factory, ) { val unsortedSceneByKey: Map<SceneKey, Scene> = scenes.associateBy { scene -> scene.key } val sortedSceneByKey: Map<SceneKey, Scene> = @@ -133,6 +134,7 @@ object SceneWindowRootViewBinder { dataSourceDelegator = dataSourceDelegator, qsSceneAdapter = qsSceneAdapter, containerConfig = containerConfig, + sceneJankMonitorFactory = sceneJankMonitorFactory, ) .also { it.id = R.id.scene_container_root_composable } ) @@ -169,6 +171,7 @@ object SceneWindowRootViewBinder { dataSourceDelegator: SceneDataSourceDelegator, qsSceneAdapter: Provider<QSSceneAdapter>, containerConfig: SceneContainerConfig, + sceneJankMonitorFactory: SceneJankMonitor.Factory, ): View { return ComposeView(context).apply { setContent { @@ -185,6 +188,7 @@ object SceneWindowRootViewBinder { sceneTransitions = containerConfig.transitions, dataSourceDelegator = dataSourceDelegator, qsSceneAdapter = qsSceneAdapter, + sceneJankMonitorFactory = sceneJankMonitorFactory, ) } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt index e19112047d2a..3449e81a4630 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt @@ -41,6 +41,7 @@ import com.android.systemui.scene.shared.model.SceneContainerConfig import com.android.systemui.scene.shared.model.SceneDataSourceDelegator import com.android.systemui.scene.ui.composable.Overlay import com.android.systemui.scene.ui.composable.Scene +import com.android.systemui.scene.ui.view.SceneJankMonitor import com.android.systemui.scene.ui.view.SceneWindowRootView import com.android.systemui.scene.ui.view.WindowRootView import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel @@ -89,6 +90,7 @@ abstract class ShadeViewProviderModule { layoutInsetController: NotificationInsetsController, sceneDataSourceDelegator: Provider<SceneDataSourceDelegator>, qsSceneAdapter: Provider<QSSceneAdapter>, + sceneJankMonitorFactory: SceneJankMonitor.Factory, ): WindowRootView { return if (SceneContainerFlag.isEnabled) { checkNoSceneDuplicates(scenesProvider.get()) @@ -104,6 +106,7 @@ abstract class ShadeViewProviderModule { layoutInsetController = layoutInsetController, sceneDataSourceDelegator = sceneDataSourceDelegator.get(), qsSceneAdapter = qsSceneAdapter, + sceneJankMonitorFactory = sceneJankMonitorFactory, ) sceneWindowRootView } else { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/view/SceneJankMonitorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/view/SceneJankMonitorKosmos.kt new file mode 100644 index 000000000000..bcba5ee50b8c --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/ui/view/SceneJankMonitorKosmos.kt @@ -0,0 +1,35 @@ +/* + * 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.scene.ui.view + +import com.android.systemui.authentication.domain.interactor.authenticationInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor +import com.android.systemui.jank.interactionJankMonitor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture + +val Kosmos.sceneJankMonitorFactory: SceneJankMonitor.Factory by Fixture { + object : SceneJankMonitor.Factory { + override fun create(): SceneJankMonitor { + return SceneJankMonitor( + authenticationInteractor = authenticationInteractor, + deviceUnlockedInteractor = deviceUnlockedInteractor, + interactionJankMonitor = interactionJankMonitor, + ) + } + } +} |