diff options
4 files changed, 319 insertions, 108 deletions
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt index 5218537394de..97b06170e770 100644 --- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt +++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt @@ -1,29 +1,34 @@ package com.android.systemui.biometrics -import android.annotation.AnyThread import android.annotation.MainThread import android.util.Log -import com.android.systemui.dagger.qualifiers.Main -import com.android.systemui.shade.ShadeExpansionChangeEvent -import com.android.systemui.shade.ShadeExpansionStateManager -import java.util.concurrent.Executor +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.shade.domain.interactor.ShadeInteractor +import dagger.Lazy import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.launch class AuthDialogPanelInteractionDetector @Inject constructor( - private val shadeExpansionStateManager: ShadeExpansionStateManager, - @Main private val mainExecutor: Executor, + @Application private val scope: CoroutineScope, + private val shadeInteractorLazy: Lazy<ShadeInteractor>, ) { - private var action: Action? = null - private var panelState: Int = -1 + private var shadeExpansionCollectorJob: Job? = null @MainThread - fun enable(onPanelInteraction: Runnable) { - if (action == null) { - action = Action(onPanelInteraction) - shadeExpansionStateManager.addStateListener(this::onPanelStateChanged) - shadeExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged) + fun enable(onShadeInteraction: Runnable) { + if (shadeExpansionCollectorJob == null) { + shadeExpansionCollectorJob = + scope.launch { + // wait for it to emit true once + shadeInteractorLazy.get().anyExpanding.first { it } + onShadeInteraction.run() + } + shadeExpansionCollectorJob?.invokeOnCompletion { shadeExpansionCollectorJob = null } } else { Log.e(TAG, "Already enabled") } @@ -31,49 +36,9 @@ constructor( @MainThread fun disable() { - if (action != null) { - Log.i(TAG, "Disable dectector") - action = null - panelState = -1 - shadeExpansionStateManager.removeStateListener(this::onPanelStateChanged) - shadeExpansionStateManager.removeExpansionListener(this::onPanelExpansionChanged) - } + Log.i(TAG, "Disable detector") + shadeExpansionCollectorJob?.cancel() } - - @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" diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt index fd63b89d1199..3b194111bcbc 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractor.kt @@ -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.policy.DeviceProvisionedController import com.android.systemui.user.domain.interactor.UserInteractor +import com.android.systemui.util.kotlin.pairwise import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow @@ -90,6 +91,18 @@ constructor( */ 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. */ val isExpandToQsEnabled: Flow<Boolean> = combine( diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt index 5766f1be8894..9751fad55986 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt @@ -16,84 +16,194 @@ 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.RequiresDevice +import com.android.internal.logging.UiEventLogger +import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.SysuiTestCase -import com.android.systemui.shade.ShadeExpansionStateManager -import org.junit.Assert +import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository +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.Rule import org.junit.Test -import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.verify import org.mockito.Mockito.verifyZeroInteractions -import org.mockito.junit.MockitoJUnit +import org.mockito.MockitoAnnotations -@RequiresDevice @SmallTest -@RunWith(AndroidTestingRunner::class) +@OptIn(ExperimentalCoroutinesApi::class) 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 shadeInteractor: ShadeInteractor + private lateinit var userInteractor: UserInteractor @Mock private lateinit var action: Runnable - - @JvmField @Rule var mockitoRule = MockitoJUnit.rule() + @Mock private lateinit var activityManager: ActivityManager + @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 fun setUp() { - shadeExpansionStateManager = ShadeExpansionStateManager() - detector = - AuthDialogPanelInteractionDetector(shadeExpansionStateManager, mContext.mainExecutor) - } + MockitoAnnotations.initMocks(this) - @Test - fun testEnableDetector_expandWithTrack_shouldPostRunnable() { - detector.enable(action) - shadeExpansionStateManager.onPanelExpansionChanged(1.0f, true, true, 0f) - verify(action).run() - } + featureFlags.set(Flags.FACE_AUTH_REFACTOR, false) + featureFlags.set(Flags.FULL_SCREEN_USER_SWITCHER, true) - @Test - fun testEnableDetector_trackOnly_shouldPostRunnable() { - detector.enable(action) - shadeExpansionStateManager.onPanelExpansionChanged(1.0f, false, true, 0f) - verify(action).run() + val refreshUsersScheduler = + 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 - fun testEnableDetector_expandOnly_shouldNotPostRunnable() { - detector.enable(action) - shadeExpansionStateManager.onPanelExpansionChanged(1.0f, true, false, 0f) - verifyZeroInteractions(action) - } + fun enableDetector_expand_shouldRunAction() = + testScope.runTest { + // GIVEN shade is closed and detector is enabled + shadeRepository.setLegacyShadeExpansion(0f) + detector.enable(action) + runCurrent() + + // WHEN shade expands + shadeRepository.setLegacyShadeExpansion(.5f) + runCurrent() + + // THEN action was run + verify(action).run() + } @Test - fun testEnableDetector_expandWithoutFraction_shouldPostRunnable() { - detector.enable(action) - // simulate headsup notification - shadeExpansionStateManager.onPanelExpansionChanged(0.0f, true, false, 0f) - verifyZeroInteractions(action) - } + fun enableDetector_shadeExpandImmediate_shouldNotPostRunnable() = + testScope.runTest { + // GIVEN shade is closed and detector is enabled + shadeRepository.setLegacyShadeExpansion(0f) + detector.enable(action) + runCurrent() + + // WHEN shade expands fully instantly + shadeRepository.setLegacyShadeExpansion(1f) + runCurrent() + + // THEN action not run + verifyZeroInteractions(action) + + // Clean up job + detector.disable() + } @Test - fun testEnableDetector_shouldNotPostRunnable() { - detector.enable(action) - detector.disable() - shadeExpansionStateManager.onPanelExpansionChanged(1.0f, true, true, 0f) - verifyZeroInteractions(action) - } + fun disableDetector_shouldNotPostRunnable() = + testScope.runTest { + // GIVEN shade is closed and detector is enabled + shadeRepository.setLegacyShadeExpansion(0f) + detector.enable(action) + runCurrent() + + // WHEN detector is disabled and shade opens + detector.disable() + shadeRepository.setLegacyShadeExpansion(.5f) + runCurrent() + + // THEN action not run + verifyZeroInteractions(action) + } @Test - fun testFromOpenState_becomeStateClose_enableDetector_shouldNotPostRunnable() { - // STATE_OPEN is 2 - shadeExpansionStateManager.updateState(2) - detector.enable(action) - shadeExpansionStateManager.onPanelExpansionChanged(0.5f, false, false, 0f) - verifyZeroInteractions(action) - Assert.assertEquals(true, shadeExpansionStateManager.isClosed()) - } + fun enableDetector_beginCollapse_shouldNotPostRunnable() = + testScope.runTest { + // GIVEN shade is open and detector is enabled + shadeRepository.setLegacyShadeExpansion(1f) + detector.enable(action) + runCurrent() + + // WHEN shade begins to collapse + shadeRepository.setLegacyShadeExpansion(.5f) + runCurrent() + + // THEN action not run + verifyZeroInteractions(action) + + // Clean up job + detector.disable() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt index 41ea5b747e06..ba5ecce24f60 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/data/repository/ShadeInteractorTest.kt @@ -434,4 +434,127 @@ class ShadeInteractorTest : SysuiTestCase() { // THEN shade expansion is zero 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() + } } |