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

Commit 85c90f60 authored by Grace Cheng's avatar Grace Cheng
Browse files

Add system health CUJs

Defines CUJ_SECURE_LOCK_DEVICE_BIOMETRIC_AUTH_APPEAR and
CUJ_SECURE_LOCK_DEVICE_BIOMETRIC_AUTH_DISAPPEAR UI interaction
CUJs

Flag: android.security.secure_lock_device
Bug: 401645997
Fixes: 436359935
Test: atest SecureLockDeviceBiometricAuthContentTest
Change-Id: Ibf24f2954a144c75ff466945256a9c02bc08fdfa
parent 0e506b9a
Loading
Loading
Loading
Loading
+22 −1
Original line number Diff line number Diff line
@@ -516,8 +516,21 @@ public class Cuj {
     */
    public static final int CUJ_AMBIENT_CUE_COLLAPSE = 148;

    /**
     * Tracking transition from primary auth (PIN/pattern/password bouncer) to the biometric auth
     * bouncer during secure lock device two-factor authentication.
     */
    public static final int CUJ_BOUNCER_SECURE_LOCK_DEVICE_BIOMETRIC_AUTH_APPEAR = 149;

    /**
     * Tracking bouncer dismissal following two-factor authentication completion in secure
     * lock device.
     */
    public static final int CUJ_BOUNCER_SECURE_LOCK_DEVICE_BIOMETRIC_AUTH_DISAPPEAR = 150;

    // When adding a CUJ, update this and make sure to also update CUJ_TO_STATSD_INTERACTION_TYPE.
    @VisibleForTesting static final int LAST_CUJ = CUJ_AMBIENT_CUE_COLLAPSE;
    @VisibleForTesting static final int LAST_CUJ =
            CUJ_BOUNCER_SECURE_LOCK_DEVICE_BIOMETRIC_AUTH_DISAPPEAR;

    /** @hide */
    @IntDef({
@@ -658,6 +671,8 @@ public class Cuj {
            CUJ_AMBIENT_CUE_HIDE,
            CUJ_AMBIENT_CUE_EXPAND,
            CUJ_AMBIENT_CUE_COLLAPSE,
            CUJ_BOUNCER_SECURE_LOCK_DEVICE_BIOMETRIC_AUTH_APPEAR,
            CUJ_BOUNCER_SECURE_LOCK_DEVICE_BIOMETRIC_AUTH_DISAPPEAR
    })
    @Retention(RetentionPolicy.SOURCE)
    public @interface CujType {}
@@ -808,6 +823,8 @@ public class Cuj {
        CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_AMBIENT_CUE_HIDE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__AMBIENT_CUE_HIDE;
        CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_AMBIENT_CUE_EXPAND] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__AMBIENT_CUE_EXPAND;
        CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_AMBIENT_CUE_COLLAPSE] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__AMBIENT_CUE_COLLAPSE;
        CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_BOUNCER_SECURE_LOCK_DEVICE_BIOMETRIC_AUTH_APPEAR] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__BOUNCER_SECURE_LOCK_DEVICE_BIOMETRIC_AUTH_APPEAR;
        CUJ_TO_STATSD_INTERACTION_TYPE[CUJ_BOUNCER_SECURE_LOCK_DEVICE_BIOMETRIC_AUTH_DISAPPEAR] = FrameworkStatsLog.UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__BOUNCER_SECURE_LOCK_DEVICE_BIOMETRIC_AUTH_DISAPPEAR;
    }

    private Cuj() {
@@ -1100,6 +1117,10 @@ public class Cuj {
                return "AMBIENT_CUE_EXPAND";
            case CUJ_AMBIENT_CUE_COLLAPSE:
                return "AMBIENT_CUE_COLLAPSE";
            case CUJ_BOUNCER_SECURE_LOCK_DEVICE_BIOMETRIC_AUTH_APPEAR:
                return "BOUNCER_SECURE_LOCK_DEVICE_BIOMETRIC_AUTH_APPEAR";
            case CUJ_BOUNCER_SECURE_LOCK_DEVICE_BIOMETRIC_AUTH_DISAPPEAR:
                return "BOUNCER_SECURE_LOCK_DEVICE_BIOMETRIC_AUTH_DISAPPEAR";
        }
        return "UNKNOWN";
    }
+125 −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.securelockdevice.ui.composable

import android.platform.test.annotations.EnableFlags
import android.security.Flags
import android.view.View
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.jank.Cuj
import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.SysuiTestCase
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
import org.mockito.kotlin.verify

@SmallTest
@RunWith(AndroidJUnit4::class)
@EnableFlags(Flags.FLAG_SECURE_LOCK_DEVICE)
class SecureLockDeviceBiometricAuthContentTest : SysuiTestCase() {
    private val interactionJankMonitor: InteractionJankMonitor = mock()
    private val view: View = mock()
    private val onDisappearAnimationFinished: () -> Unit = mock()

    @Test
    fun handleJankMonitoring_tracksAppear() {
        // Simulate start of transition when target state becomes visible
        handleJankMonitoring(
            currentState = false,
            isCurrentStateIdle = false,
            targetState = true,
            isReadyToDismissBiometricAuth = false,
            interactionJankMonitor = interactionJankMonitor,
            view = view,
            onDisappearAnimationFinished = onDisappearAnimationFinished,
        )

        // Verify jank monitoring begins for appear
        verify(interactionJankMonitor)
            .begin(view, Cuj.CUJ_BOUNCER_SECURE_LOCK_DEVICE_BIOMETRIC_AUTH_APPEAR)

        // Simulate end of transition when state becomes idle at visible
        handleJankMonitoring(
            currentState = true,
            isCurrentStateIdle = true,
            targetState = true,
            isReadyToDismissBiometricAuth = false,
            interactionJankMonitor = interactionJankMonitor,
            view = view,
            onDisappearAnimationFinished = onDisappearAnimationFinished,
        )

        // THEN jank monitoring ends for appear
        verify(interactionJankMonitor).end(Cuj.CUJ_BOUNCER_SECURE_LOCK_DEVICE_BIOMETRIC_AUTH_APPEAR)
    }

    @Test
    fun handleJankMonitoring_tracksDisappear() {
        // Simulate start of transition when target state becomes not visible
        handleJankMonitoring(
            currentState = true,
            isCurrentStateIdle = false,
            targetState = false,
            isReadyToDismissBiometricAuth = true,
            interactionJankMonitor = interactionJankMonitor,
            view = view,
            onDisappearAnimationFinished = onDisappearAnimationFinished,
        )

        // Verify jank monitoring begins for disappear
        verify(interactionJankMonitor)
            .begin(view, Cuj.CUJ_BOUNCER_SECURE_LOCK_DEVICE_BIOMETRIC_AUTH_DISAPPEAR)

        // Simulate end of transition when not visible state becomes idle
        handleJankMonitoring(
            currentState = false,
            isCurrentStateIdle = true,
            targetState = false,
            isReadyToDismissBiometricAuth = true,
            interactionJankMonitor = interactionJankMonitor,
            view = view,
            onDisappearAnimationFinished = onDisappearAnimationFinished,
        )

        // Verify jank monitoring ends for disappear and the callback is invoked
        verify(interactionJankMonitor)
            .end(Cuj.CUJ_BOUNCER_SECURE_LOCK_DEVICE_BIOMETRIC_AUTH_DISAPPEAR)
        verify(onDisappearAnimationFinished).invoke()
    }

    @Test
    fun handleJankMonitoring_doesNotTrackDisappear_whenNotReadyToDismiss() {
        // Simulate start of transition when target state becomes not visible, but not ready to
        // dismiss (animations haven't finished playing)
        handleJankMonitoring(
            currentState = true,
            isCurrentStateIdle = false,
            targetState = false,
            isReadyToDismissBiometricAuth = false,
            interactionJankMonitor = interactionJankMonitor,
            view = view,
            onDisappearAnimationFinished = onDisappearAnimationFinished,
        )

        // Verify jank monitoring does NOT begin
        verify(interactionJankMonitor, never())
            .begin(view, Cuj.CUJ_BOUNCER_SECURE_LOCK_DEVICE_BIOMETRIC_AUTH_DISAPPEAR)
    }
}
+60 −1
Original line number Diff line number Diff line
@@ -15,6 +15,8 @@
 */
package com.android.systemui.securelockdevice.ui.composable

import android.view.View
import androidx.annotation.VisibleForTesting
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.MutableTransitionState
@@ -43,6 +45,7 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.res.stringResource
@@ -61,6 +64,8 @@ import com.airbnb.lottie.compose.rememberLottieComposition
import com.android.compose.animation.Easings
import com.android.compose.modifiers.height
import com.android.compose.modifiers.width
import com.android.internal.jank.Cuj
import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.Flags.bpColors
import com.android.systemui.biometrics.BiometricAuthIconAssets
import com.android.systemui.bouncer.shared.model.SecureLockDeviceBouncerActionButtonModel
@@ -87,7 +92,13 @@ fun SecureLockDeviceContent(
            secureLockDeviceViewModelFactory.create()
        }

    val view = LocalView.current

    val interactionJankMonitor: InteractionJankMonitor =
        secureLockDeviceViewModel.interactionJankMonitor

    val isVisible = secureLockDeviceViewModel.isVisible
    val isReadyToDismissBiometricAuth = secureLockDeviceViewModel.isReadyToDismissBiometricAuth
    val visibleState = remember { MutableTransitionState(isVisible) }

    /** This effect is run when the composable enters the composition */
@@ -110,7 +121,17 @@ fun SecureLockDeviceContent(
     * the UI have finished playing on the UI.
     */
    LaunchedEffect(visibleState.currentState, visibleState.targetState, visibleState.isIdle) {
        // TODO(b/436359935) report interaction jank metrics
        handleJankMonitoring(
            currentState = visibleState.currentState,
            isCurrentStateIdle = visibleState.isIdle,
            targetState = visibleState.targetState,
            isReadyToDismissBiometricAuth = isReadyToDismissBiometricAuth,
            interactionJankMonitor = interactionJankMonitor,
            view = view,
            onDisappearAnimationFinished = {
                secureLockDeviceViewModel.onDisappearAnimationFinished()
            },
        )
    }

    /** Animates the biometric auth content in and out of view. */
@@ -154,6 +175,44 @@ fun SecureLockDeviceContent(
    }
}

/** Handles InteractionJankMonitor tracking for the appear and disappear animations. */
@VisibleForTesting
fun handleJankMonitoring(
    currentState: Boolean,
    isCurrentStateIdle: Boolean,
    targetState: Boolean,
    isReadyToDismissBiometricAuth: Boolean,
    interactionJankMonitor: InteractionJankMonitor,
    view: View,
    onDisappearAnimationFinished: () -> Unit,
) {
    if (!currentState && targetState) { // Start appear animation
        // Start appear animation
        interactionJankMonitor.begin(
            /* v = */ view,
            /* cujType = */ Cuj.CUJ_BOUNCER_SECURE_LOCK_DEVICE_BIOMETRIC_AUTH_APPEAR,
        )
    } else if (currentState && isCurrentStateIdle) { // Appear animation complete
        interactionJankMonitor.end(
            /* cujType = */ Cuj.CUJ_BOUNCER_SECURE_LOCK_DEVICE_BIOMETRIC_AUTH_APPEAR
        )
    } else if (currentState && !targetState) { // Disappear animation started
        if (isReadyToDismissBiometricAuth) {
            interactionJankMonitor.begin(
                /* v = */ view,
                /* cujType = */ Cuj.CUJ_BOUNCER_SECURE_LOCK_DEVICE_BIOMETRIC_AUTH_DISAPPEAR,
            )
        }
    } else if (!currentState && isCurrentStateIdle) { // Disappear animation complete
        if (isReadyToDismissBiometricAuth) {
            interactionJankMonitor.end(
                /* cujType = */ Cuj.CUJ_BOUNCER_SECURE_LOCK_DEVICE_BIOMETRIC_AUTH_DISAPPEAR
            )
            onDisappearAnimationFinished()
        }
    }
}

@Composable
private fun BiometricIconLottie(
    viewModel: SecureLockDeviceBiometricAuthContentViewModel,
+2 −0
Original line number Diff line number Diff line
@@ -22,6 +22,7 @@ import android.util.Log
import android.view.accessibility.AccessibilityManager
import androidx.annotation.VisibleForTesting
import androidx.compose.runtime.getValue
import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.biometrics.shared.model.BiometricModality
import com.android.systemui.biometrics.ui.viewmodel.BiometricAuthIconViewModel
import com.android.systemui.biometrics.ui.viewmodel.PromptAuthState
@@ -74,6 +75,7 @@ constructor(
    deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor,
    private val secureLockDeviceInteractor: SecureLockDeviceInteractor,
    val udfpsAccessibilityOverlayViewModel: AlternateBouncerUdfpsAccessibilityOverlayViewModel,
    val interactionJankMonitor: InteractionJankMonitor,
) : HydratedActivatable() {
    /** @see SecureLockDeviceInteractor.isSecureLockDeviceEnabled */
    val isSecureLockDeviceEnabled = secureLockDeviceInteractor.isSecureLockDeviceEnabled
+2 −0
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import com.android.systemui.deviceentry.domain.interactor.biometricMessageIntera
import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceEntryFingerprintAuthInteractor
import com.android.systemui.haptics.msdl.bouncerHapticPlayer
import com.android.systemui.jank.interactionJankMonitor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.kosmos.applicationCoroutineScope
@@ -41,6 +42,7 @@ var Kosmos.secureLockDeviceBiometricAuthContentViewModel by Fixture {
        deviceEntryFingerprintAuthInteractor = deviceEntryFingerprintAuthInteractor,
        secureLockDeviceInteractor = secureLockDeviceInteractor,
        udfpsAccessibilityOverlayViewModel = alternateBouncerUdfpsAccessibilityOverlayViewModel,
        interactionJankMonitor = interactionJankMonitor,
    )
}