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

Commit f273f064 authored by Justin Weir's avatar Justin Weir Committed by Android (Google) Code Review
Browse files

Merge "Migrate AuthInteractionDetector to ShadeInteractor" into main

parents ea6b298c f80d4868
Loading
Loading
Loading
Loading
+21 −56
Original line number Original line Diff line number Diff line
package com.android.systemui.biometrics
package com.android.systemui.biometrics


import android.annotation.AnyThread
import android.annotation.MainThread
import android.annotation.MainThread
import android.util.Log
import android.util.Log
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.shade.ShadeExpansionChangeEvent
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.ShadeExpansionStateManager
import dagger.Lazy
import java.util.concurrent.Executor
import javax.inject.Inject
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch


class AuthDialogPanelInteractionDetector
class AuthDialogPanelInteractionDetector
@Inject
@Inject
constructor(
constructor(
    private val shadeExpansionStateManager: ShadeExpansionStateManager,
    @Application private val scope: CoroutineScope,
    @Main private val mainExecutor: Executor,
    private val shadeInteractorLazy: Lazy<ShadeInteractor>,
) {
) {
    private var action: Action? = null
    private var shadeExpansionCollectorJob: Job? = null
    private var panelState: Int = -1


    @MainThread
    @MainThread
    fun enable(onPanelInteraction: Runnable) {
    fun enable(onShadeInteraction: Runnable) {
        if (action == null) {
        if (shadeExpansionCollectorJob == null) {
            action = Action(onPanelInteraction)
            shadeExpansionCollectorJob =
            shadeExpansionStateManager.addStateListener(this::onPanelStateChanged)
                scope.launch {
            shadeExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged)
                    // wait for it to emit true once
                    shadeInteractorLazy.get().anyExpanding.first { it }
                    onShadeInteraction.run()
                }
            shadeExpansionCollectorJob?.invokeOnCompletion { shadeExpansionCollectorJob = null }
        } else {
        } else {
            Log.e(TAG, "Already enabled")
            Log.e(TAG, "Already enabled")
        }
        }
@@ -31,49 +36,9 @@ constructor(


    @MainThread
    @MainThread
    fun disable() {
    fun disable() {
        if (action != null) {
        Log.i(TAG, "Disable detector")
            Log.i(TAG, "Disable dectector")
        shadeExpansionCollectorJob?.cancel()
            action = null
            panelState = -1
            shadeExpansionStateManager.removeStateListener(this::onPanelStateChanged)
            shadeExpansionStateManager.removeExpansionListener(this::onPanelExpansionChanged)
    }
    }
}
}


    @AnyThread
    private fun onPanelExpansionChanged(event: ShadeExpansionChangeEvent) =
        mainExecutor.execute {
            action?.let {
                if (event.tracking || (event.expanded && event.fraction > 0 && panelState == 1)) {
                    Log.i(TAG, "onPanelExpansionChanged, event: $event")
                    it.onPanelInteraction.run()
                    disable()
                }
            }
        }

    @AnyThread
    private fun onPanelStateChanged(state: Int) =
        mainExecutor.execute {
            // When device owner set screen lock type as Swipe, and install work profile with
            // pin/pattern/password & fingerprint or face, if work profile allow user to verify
            // by BP, it is possible that BP will be displayed when keyguard is closing, in this
            // case event.expanded = true and event.fraction > 0, so BP will be closed, adding
            // panel state into consideration is workaround^2, this workaround works because
            // onPanelStateChanged is earlier than onPanelExpansionChanged

            // we don't want to close BP in below case
            //
            // |      Action       |  tracking  |  expanded  |  fraction  |  panelState  |
            // |      HeadsUp      |    NA      |     NA     |     NA     |      1       |
            // |   b/285111529     |   false    |    true    |    > 0     |      2       |

            // Note: HeadsUp behavior was changed, so we can't got onPanelExpansionChanged now
            panelState = state
            Log.i(TAG, "onPanelStateChanged, state: $state")
        }
}

private data class Action(val onPanelInteraction: Runnable)

private const val TAG = "AuthDialogPanelInteractionDetector"
private const val TAG = "AuthDialogPanelInteractionDetector"
+13 −0
Original line number Original line Diff line number Diff line
@@ -26,6 +26,7 @@ import com.android.systemui.statusbar.notification.stack.domain.interactor.Share
import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.UserSetupRepository
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.user.domain.interactor.UserInteractor
import com.android.systemui.user.domain.interactor.UserInteractor
import com.android.systemui.util.kotlin.pairwise
import javax.inject.Inject
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.Flow
@@ -90,6 +91,18 @@ constructor(
     */
     */
    val qsExpansion: StateFlow<Float> = repository.qsExpansion
    val qsExpansion: StateFlow<Float> = repository.qsExpansion


    /** The amount [0-1] either QS or the shade has been opened */
    val anyExpansion: StateFlow<Float> =
        combine(shadeExpansion, qsExpansion) { shadeExp, qsExp -> maxOf(shadeExp, qsExp) }
            .stateIn(scope, SharingStarted.Eagerly, 0f)

    /** Whether either the shade or QS is expanding from a fully collapsed state. */
    val anyExpanding =
        anyExpansion
            .pairwise(1f)
            .map { (prev, curr) -> curr > 0f && curr < 1f && prev < 1f }
            .distinctUntilChanged()

    /** Emits true if the shade can be expanded from QQS to QS and false otherwise. */
    /** Emits true if the shade can be expanded from QQS to QS and false otherwise. */
    val isExpandToQsEnabled: Flow<Boolean> =
    val isExpandToQsEnabled: Flow<Boolean> =
        combine(
        combine(
+162 −52
Original line number Original line Diff line number Diff line
@@ -16,84 +16,194 @@


package com.android.systemui.biometrics
package com.android.systemui.biometrics


import android.testing.AndroidTestingRunner
import android.app.ActivityManager
import android.os.UserManager
import androidx.test.filters.SmallTest
import androidx.test.filters.SmallTest
import androidx.test.filters.RequiresDevice
import com.android.internal.logging.UiEventLogger
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.SysuiTestCase
import com.android.systemui.SysuiTestCase
import com.android.systemui.shade.ShadeExpansionStateManager
import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
import org.junit.Assert
import com.android.systemui.flags.FakeFeatureFlags
import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractorFactory
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.shade.data.repository.FakeShadeRepository
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.disableflags.data.repository.FakeDisableFlagsRepository
import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeUserSetupRepository
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import com.android.systemui.telephony.data.repository.FakeTelephonyRepository
import com.android.systemui.telephony.domain.interactor.TelephonyInteractor
import com.android.systemui.user.data.repository.FakeUserRepository
import com.android.systemui.user.domain.interactor.GuestUserInteractor
import com.android.systemui.user.domain.interactor.HeadlessSystemUserMode
import com.android.systemui.user.domain.interactor.RefreshUsersScheduler
import com.android.systemui.user.domain.interactor.UserInteractor
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mock
import org.mockito.Mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.verify
import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.Mockito.verifyZeroInteractions
import org.mockito.junit.MockitoJUnit
import org.mockito.MockitoAnnotations


@RequiresDevice
@SmallTest
@SmallTest
@RunWith(AndroidTestingRunner::class)
@OptIn(ExperimentalCoroutinesApi::class)
class AuthDialogPanelInteractionDetectorTest : SysuiTestCase() {
class AuthDialogPanelInteractionDetectorTest : SysuiTestCase() {
    private val disableFlagsRepository = FakeDisableFlagsRepository()
    private val featureFlags = FakeFeatureFlags()
    private val keyguardRepository = FakeKeyguardRepository()
    private val shadeRepository = FakeShadeRepository()
    private val testDispatcher = StandardTestDispatcher()
    private val testScope = TestScope(testDispatcher)
    private val userSetupRepository = FakeUserSetupRepository()
    private val userRepository = FakeUserRepository()
    private val configurationRepository = FakeConfigurationRepository()
    private val sharedNotificationContainerInteractor =
        SharedNotificationContainerInteractor(
            configurationRepository,
            mContext,
        )


    private lateinit var shadeExpansionStateManager: ShadeExpansionStateManager
    private lateinit var detector: AuthDialogPanelInteractionDetector
    private lateinit var detector: AuthDialogPanelInteractionDetector
    private lateinit var shadeInteractor: ShadeInteractor
    private lateinit var userInteractor: UserInteractor


    @Mock private lateinit var action: Runnable
    @Mock private lateinit var action: Runnable

    @Mock private lateinit var activityManager: ActivityManager
    @JvmField @Rule var mockitoRule = MockitoJUnit.rule()
    @Mock private lateinit var activityStarter: ActivityStarter
    @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController
    @Mock private lateinit var guestInteractor: GuestUserInteractor
    @Mock private lateinit var headlessSystemUserMode: HeadlessSystemUserMode
    @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
    @Mock private lateinit var manager: UserManager
    @Mock private lateinit var uiEventLogger: UiEventLogger


    @Before
    @Before
    fun setUp() {
    fun setUp() {
        shadeExpansionStateManager = ShadeExpansionStateManager()
        MockitoAnnotations.initMocks(this)
        detector =
            AuthDialogPanelInteractionDetector(shadeExpansionStateManager, mContext.mainExecutor)
    }


    @Test
        featureFlags.set(Flags.FACE_AUTH_REFACTOR, false)
    fun testEnableDetector_expandWithTrack_shouldPostRunnable() {
        featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true)
        detector.enable(action)

        shadeExpansionStateManager.onPanelExpansionChanged(1.0f, true, true, 0f)
        val refreshUsersScheduler =
        verify(action).run()
            RefreshUsersScheduler(
                applicationScope = testScope.backgroundScope,
                mainDispatcher = testDispatcher,
                repository = userRepository,
            )
        userInteractor =
            UserInteractor(
                applicationContext = context,
                repository = userRepository,
                activityStarter = activityStarter,
                keyguardInteractor =
                    KeyguardInteractorFactory.create(featureFlags = featureFlags)
                        .keyguardInteractor,
                featureFlags = featureFlags,
                manager = manager,
                headlessSystemUserMode = headlessSystemUserMode,
                applicationScope = testScope.backgroundScope,
                telephonyInteractor =
                    TelephonyInteractor(
                        repository = FakeTelephonyRepository(),
                    ),
                broadcastDispatcher = fakeBroadcastDispatcher,
                keyguardUpdateMonitor = keyguardUpdateMonitor,
                backgroundDispatcher = testDispatcher,
                activityManager = activityManager,
                refreshUsersScheduler = refreshUsersScheduler,
                guestUserInteractor = guestInteractor,
                uiEventLogger = uiEventLogger,
            )
        shadeInteractor =
            ShadeInteractor(
                testScope.backgroundScope,
                disableFlagsRepository,
                keyguardRepository,
                userSetupRepository,
                deviceProvisionedController,
                userInteractor,
                sharedNotificationContainerInteractor,
                shadeRepository,
            )
        detector = AuthDialogPanelInteractionDetector(testScope, { shadeInteractor })
    }
    }


    @Test
    @Test
    fun testEnableDetector_trackOnly_shouldPostRunnable() {
    fun enableDetector_expand_shouldRunAction() =
        testScope.runTest {
            // GIVEN shade is closed and detector is enabled
            shadeRepository.setLegacyShadeExpansion(0f)
            detector.enable(action)
            detector.enable(action)
        shadeExpansionStateManager.onPanelExpansionChanged(1.0f, false, true, 0f)
            runCurrent()

            // WHEN shade expands
            shadeRepository.setLegacyShadeExpansion(.5f)
            runCurrent()

            // THEN action was run
            verify(action).run()
            verify(action).run()
        }
        }


    @Test
    @Test
    fun testEnableDetector_expandOnly_shouldNotPostRunnable() {
    fun enableDetector_shadeExpandImmediate_shouldNotPostRunnable() =
        testScope.runTest {
            // GIVEN shade is closed and detector is enabled
            shadeRepository.setLegacyShadeExpansion(0f)
            detector.enable(action)
            detector.enable(action)
        shadeExpansionStateManager.onPanelExpansionChanged(1.0f, true, false, 0f)
            runCurrent()
        verifyZeroInteractions(action)
    }


    @Test
            // WHEN shade expands fully instantly
    fun testEnableDetector_expandWithoutFraction_shouldPostRunnable() {
            shadeRepository.setLegacyShadeExpansion(1f)
        detector.enable(action)
            runCurrent()
        // simulate headsup notification

        shadeExpansionStateManager.onPanelExpansionChanged(0.0f, true, false, 0f)
            // THEN action not run
            verifyZeroInteractions(action)
            verifyZeroInteractions(action)

            // Clean up job
            detector.disable()
        }
        }


    @Test
    @Test
    fun testEnableDetector_shouldNotPostRunnable() {
    fun disableDetector_shouldNotPostRunnable() =
        testScope.runTest {
            // GIVEN shade is closed and detector is enabled
            shadeRepository.setLegacyShadeExpansion(0f)
            detector.enable(action)
            detector.enable(action)
            runCurrent()

            // WHEN detector is disabled and shade opens
            detector.disable()
            detector.disable()
        shadeExpansionStateManager.onPanelExpansionChanged(1.0f, true, true, 0f)
            shadeRepository.setLegacyShadeExpansion(.5f)
            runCurrent()

            // THEN action not run
            verifyZeroInteractions(action)
            verifyZeroInteractions(action)
        }
        }


    @Test
    @Test
    fun testFromOpenState_becomeStateClose_enableDetector_shouldNotPostRunnable() {
    fun enableDetector_beginCollapse_shouldNotPostRunnable() =
        // STATE_OPEN is 2
        testScope.runTest {
        shadeExpansionStateManager.updateState(2)
            // GIVEN shade is open and detector is enabled
            shadeRepository.setLegacyShadeExpansion(1f)
            detector.enable(action)
            detector.enable(action)
        shadeExpansionStateManager.onPanelExpansionChanged(0.5f, false, false, 0f)
            runCurrent()

            // WHEN shade begins to collapse
            shadeRepository.setLegacyShadeExpansion(.5f)
            runCurrent()

            // THEN action not run
            verifyZeroInteractions(action)
            verifyZeroInteractions(action)
        Assert.assertEquals(true, shadeExpansionStateManager.isClosed())

            // Clean up job
            detector.disable()
        }
        }
}
}
+123 −0
Original line number Original line Diff line number Diff line
@@ -434,4 +434,127 @@ class ShadeInteractorTest : SysuiTestCase() {
            // THEN shade expansion is zero
            // THEN shade expansion is zero
            assertThat(actual).isEqualTo(.6f)
            assertThat(actual).isEqualTo(.6f)
        }
        }

    @Test
    fun anyExpansion_shadeGreater() =
        testScope.runTest() {
            // WHEN shade is more expanded than QS
            shadeRepository.setLegacyShadeExpansion(.5f)
            shadeRepository.setQsExpansion(0f)
            runCurrent()

            // THEN anyExpansion is .5f
            assertThat(underTest.anyExpansion.value).isEqualTo(.5f)
        }

    @Test
    fun anyExpansion_qsGreater() =
        testScope.runTest() {
            // WHEN qs is more expanded than shade
            shadeRepository.setLegacyShadeExpansion(0f)
            shadeRepository.setQsExpansion(.5f)
            runCurrent()

            // THEN anyExpansion is .5f
            assertThat(underTest.anyExpansion.value).isEqualTo(.5f)
        }

    @Test
    fun expanding_shadeDraggedDown_expandingTrue() =
        testScope.runTest() {
            val actual by collectLastValue(underTest.anyExpanding)

            // GIVEN shade and QS collapsed
            shadeRepository.setLegacyShadeExpansion(0f)
            shadeRepository.setQsExpansion(0f)
            runCurrent()

            // WHEN shade partially expanded
            shadeRepository.setLegacyShadeExpansion(.5f)
            runCurrent()

            // THEN anyExpanding is true
            assertThat(actual).isTrue()
        }

    @Test
    fun expanding_qsDraggedDown_expandingTrue() =
        testScope.runTest() {
            val actual by collectLastValue(underTest.anyExpanding)

            // GIVEN shade and QS collapsed
            shadeRepository.setLegacyShadeExpansion(0f)
            shadeRepository.setQsExpansion(0f)
            runCurrent()

            // WHEN shade partially expanded
            shadeRepository.setQsExpansion(.5f)
            runCurrent()

            // THEN anyExpanding is true
            assertThat(actual).isTrue()
        }

    @Test
    fun expanding_shadeDraggedUpAndDown() =
        testScope.runTest() {
            val actual by collectLastValue(underTest.anyExpanding)

            // WHEN shade starts collapsed then partially expanded
            shadeRepository.setLegacyShadeExpansion(0f)
            shadeRepository.setLegacyShadeExpansion(.5f)
            shadeRepository.setQsExpansion(0f)
            runCurrent()

            // THEN anyExpanding is true
            assertThat(actual).isTrue()

            // WHEN shade dragged up a bit
            shadeRepository.setLegacyShadeExpansion(.2f)
            runCurrent()

            // THEN anyExpanding is still true
            assertThat(actual).isTrue()

            // WHEN shade dragged down a bit
            shadeRepository.setLegacyShadeExpansion(.7f)
            runCurrent()

            // THEN anyExpanding is still true
            assertThat(actual).isTrue()

            // WHEN shade fully shadeExpanded
            shadeRepository.setLegacyShadeExpansion(1f)
            runCurrent()

            // THEN anyExpanding is now false
            assertThat(actual).isFalse()

            // WHEN shade dragged up a bit
            shadeRepository.setLegacyShadeExpansion(.7f)
            runCurrent()

            // THEN anyExpanding is still false
            assertThat(actual).isFalse()
        }

    @Test
    fun expanding_shadeDraggedDownThenUp_expandingFalse() =
        testScope.runTest() {
            val actual by collectLastValue(underTest.anyExpanding)

            // GIVEN shade starts collapsed
            shadeRepository.setLegacyShadeExpansion(0f)
            shadeRepository.setQsExpansion(0f)
            runCurrent()

            // WHEN shade expands but doesn't complete
            shadeRepository.setLegacyShadeExpansion(.5f)
            runCurrent()
            shadeRepository.setLegacyShadeExpansion(0f)
            runCurrent()

            // THEN anyExpanding is false
            assertThat(actual).isFalse()
        }
}
}