Loading packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt +112 −45 Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository import com.android.systemui.statusbar.gesture.swipeStatusBarAwayGestureHandler import com.android.systemui.statusbar.notification.data.model.activeNotificationModel import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository Loading @@ -40,9 +41,14 @@ import com.android.systemui.statusbar.window.fakeStatusBarWindowControllerStore import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.clearInvocations import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify @SmallTest @RunWith(AndroidJUnit4::class) Loading @@ -51,6 +57,11 @@ class OngoingCallInteractorTest : SysuiTestCase() { private val repository = kosmos.activeNotificationListRepository private val underTest = kosmos.ongoingCallInteractor @Before fun setUp() { underTest.start() } @Test fun noNotification_emitsNoCall() = runTest { val state by collectLastValue(underTest.ongoingCallState) Loading Loading @@ -210,8 +221,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { @Test fun ongoingCallNotification_setsRequiresStatusBarVisibleTrue() = kosmos.runTest { val ongoingCallState by collectLastValue(underTest.ongoingCallState) val isStatusBarRequired by collectLastValue(underTest.isStatusBarRequiredForOngoingCall) val requiresStatusBarVisibleInRepository by collectLastValue( kosmos.fakeStatusBarModeRepository.defaultDisplay Loading @@ -222,21 +232,9 @@ class OngoingCallInteractorTest : SysuiTestCase() { kosmos.fakeStatusBarWindowControllerStore.defaultDisplay .ongoingProcessRequiresStatusBarVisible ) repository.activeNotifications.value = ActiveNotificationsStore.Builder() .apply { addIndividualNotif( activeNotificationModel( key = "notif1", whenTime = 1000L, callType = CallType.Ongoing, uid = UID, ) ) } .build() postOngoingCallNotification() assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java) assertThat(isStatusBarRequired).isTrue() assertThat(requiresStatusBarVisibleInRepository).isTrue() assertThat(requiresStatusBarVisibleInWindowController).isTrue() } Loading @@ -244,8 +242,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { @Test fun notificationRemoved_setsRequiresStatusBarVisibleFalse() = kosmos.runTest { val ongoingCallState by collectLastValue(underTest.ongoingCallState) val isStatusBarRequired by collectLastValue(underTest.isStatusBarRequiredForOngoingCall) val requiresStatusBarVisibleInRepository by collectLastValue( kosmos.fakeStatusBarModeRepository.defaultDisplay Loading @@ -257,23 +254,11 @@ class OngoingCallInteractorTest : SysuiTestCase() { .ongoingProcessRequiresStatusBarVisible ) repository.activeNotifications.value = ActiveNotificationsStore.Builder() .apply { addIndividualNotif( activeNotificationModel( key = "notif1", whenTime = 1000L, callType = CallType.Ongoing, uid = UID, ) ) } .build() postOngoingCallNotification() repository.activeNotifications.value = ActiveNotificationsStore() assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.NoCall::class.java) assertThat(isStatusBarRequired).isFalse() assertThat(requiresStatusBarVisibleInRepository).isFalse() assertThat(requiresStatusBarVisibleInWindowController).isFalse() } Loading @@ -295,6 +280,99 @@ class OngoingCallInteractorTest : SysuiTestCase() { ) kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false postOngoingCallNotification() assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java) assertThat(requiresStatusBarVisibleInRepository).isTrue() assertThat(requiresStatusBarVisibleInWindowController).isTrue() kosmos.activityManagerRepository.fake.setIsAppVisible(UID, true) assertThat(ongoingCallState) .isInstanceOf(OngoingCallModel.InCallWithVisibleApp::class.java) assertThat(requiresStatusBarVisibleInRepository).isFalse() assertThat(requiresStatusBarVisibleInWindowController).isFalse() } @Test fun gestureHandler_inCall_notFullscreen_doesNotListen() = kosmos.runTest { val ongoingCallState by collectLastValue(underTest.ongoingCallState) clearInvocations(kosmos.swipeStatusBarAwayGestureHandler) // Set up notification but not in fullscreen kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false postOngoingCallNotification() assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java) verify(kosmos.swipeStatusBarAwayGestureHandler, never()) .addOnGestureDetectedCallback(any(), any()) } @Test fun gestureHandler_inCall_fullscreen_addsListener() = kosmos.runTest { val isGestureListeningEnabled by collectLastValue(underTest.isGestureListeningEnabled) // Set up notification and fullscreen mode kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true postOngoingCallNotification() assertThat(isGestureListeningEnabled).isTrue() verify(kosmos.swipeStatusBarAwayGestureHandler) .addOnGestureDetectedCallback(any(), any()) } @Test fun gestureHandler_inCall_fullscreen_chipSwiped_removesListener() = kosmos.runTest { val swipeAwayState by collectLastValue(underTest.isChipSwipedAway) // Set up notification and fullscreen mode kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true postOngoingCallNotification() clearInvocations(kosmos.swipeStatusBarAwayGestureHandler) underTest.onStatusBarSwiped() assertThat(swipeAwayState).isTrue() verify(kosmos.swipeStatusBarAwayGestureHandler).removeOnGestureDetectedCallback(any()) } @Test fun chipSwipedAway_setsRequiresStatusBarVisibleFalse() = kosmos.runTest { val isStatusBarRequiredForOngoingCall by collectLastValue(underTest.isStatusBarRequiredForOngoingCall) val requiresStatusBarVisibleInRepository by collectLastValue( kosmos.fakeStatusBarModeRepository.defaultDisplay .ongoingProcessRequiresStatusBarVisible ) val requiresStatusBarVisibleInWindowController by collectLastValue( kosmos.fakeStatusBarWindowControllerStore.defaultDisplay .ongoingProcessRequiresStatusBarVisible ) // Start with an ongoing call (which should set status bar required) postOngoingCallNotification() assertThat(isStatusBarRequiredForOngoingCall).isTrue() assertThat(requiresStatusBarVisibleInRepository).isTrue() assertThat(requiresStatusBarVisibleInWindowController).isTrue() // Swipe away the chip underTest.onStatusBarSwiped() // Verify status bar is no longer required assertThat(requiresStatusBarVisibleInRepository).isFalse() assertThat(requiresStatusBarVisibleInWindowController).isFalse() } private fun postOngoingCallNotification() { repository.activeNotifications.value = ActiveNotificationsStore.Builder() .apply { Loading @@ -308,17 +386,6 @@ class OngoingCallInteractorTest : SysuiTestCase() { ) } .build() assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java) assertThat(requiresStatusBarVisibleInRepository).isTrue() assertThat(requiresStatusBarVisibleInWindowController).isTrue() kosmos.activityManagerRepository.fake.setIsAppVisible(UID, true) assertThat(ongoingCallState) .isInstanceOf(OngoingCallModel.InCallWithVisibleApp::class.java) assertThat(requiresStatusBarVisibleInRepository).isFalse() assertThat(requiresStatusBarVisibleInWindowController).isFalse() } companion object { Loading packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt +14 −0 Original line number Diff line number Diff line Loading @@ -37,6 +37,7 @@ import com.android.systemui.statusbar.phone.StatusBarSignalPolicy import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLog import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization import com.android.systemui.statusbar.phone.ongoingcall.domain.interactor.OngoingCallInteractor import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.ui.SystemBarUtilsProxyImpl import com.android.systemui.statusbar.window.MultiDisplayStatusBarWindowControllerStore Loading Loading @@ -99,6 +100,19 @@ interface StatusBarModule { controller } @Provides @SysUISingleton @IntoMap @ClassKey(OngoingCallInteractor::class) fun ongoingCallInteractor( interactor: OngoingCallInteractor ): CoreStartable = if (StatusBarChipsModernization.isEnabled) { interactor } else { CoreStartable.NOP } @Provides @SysUISingleton fun lightBarController(store: LightBarControllerStore): LightBarController { Loading packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt +68 −6 Original line number Diff line number Diff line Loading @@ -16,12 +16,15 @@ package com.android.systemui.statusbar.phone.ongoingcall.domain.interactor import androidx.annotation.VisibleForTesting import com.android.systemui.CoreStartable import com.android.systemui.activity.data.repository.ActivityManagerRepository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLog Loading @@ -31,11 +34,15 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn Loading @@ -55,11 +62,18 @@ class OngoingCallInteractor @Inject constructor( private val activityManagerRepository: ActivityManagerRepository, private val statusBarModeRepositoryStore: StatusBarModeRepositoryStore, private val statusBarWindowControllerStore: StatusBarWindowControllerStore, private val swipeStatusBarAwayGestureHandler: SwipeStatusBarAwayGestureHandler, activeNotificationsInteractor: ActiveNotificationsInteractor, @OngoingCallLog private val logBuffer: LogBuffer, ) { ) : CoreStartable { private val logger = Logger(logBuffer, TAG) /** * Tracks whether the call chip has been swiped away. */ private val _isChipSwipedAway = MutableStateFlow(false) val isChipSwipedAway: StateFlow<Boolean> = _isChipSwipedAway.asStateFlow() /** * The current state of ongoing calls. */ Loading @@ -70,15 +84,29 @@ class OngoingCallInteractor @Inject constructor( notification = notification ) } .onEach { state -> setStatusBarRequiredForOngoingCall(state) } .stateIn( scope = scope, started = SharingStarted.WhileSubscribed(), initialValue = OngoingCallModel.NoCall ) @VisibleForTesting val isStatusBarRequiredForOngoingCall = combine( ongoingCallState, isChipSwipedAway ) { callState, chipSwipedAway -> callState is OngoingCallModel.InCall && !chipSwipedAway } @VisibleForTesting val isGestureListeningEnabled = combine( ongoingCallState, statusBarModeRepositoryStore.defaultDisplay.isInFullscreenMode, isChipSwipedAway ) { callState, isFullscreen, chipSwipedAway -> callState is OngoingCallModel.InCall && !chipSwipedAway && isFullscreen } private fun createOngoingCallStateFlow( notification: ActiveNotificationModel? ): Flow<OngoingCallModel> { Loading @@ -99,6 +127,31 @@ class OngoingCallInteractor @Inject constructor( } } override fun start() { ongoingCallState .filterIsInstance<OngoingCallModel.NoCall>() .onEach { _isChipSwipedAway.value = false }.launchIn(scope) isStatusBarRequiredForOngoingCall.onEach { statusBarRequired -> setStatusBarRequiredForOngoingCall(statusBarRequired) }.launchIn(scope) isGestureListeningEnabled.onEach { isEnabled -> updateGestureListening(isEnabled) }.launchIn(scope) } /** * Callback that must run when the status bar is swiped while gesture listening is active. */ @VisibleForTesting fun onStatusBarSwiped() { logger.d("Status bar chip swiped away") _isChipSwipedAway.value = true } private fun deriveOngoingCallState( model: ActiveNotificationModel, isVisible: Boolean Loading Loading @@ -126,8 +179,7 @@ class OngoingCallInteractor @Inject constructor( } } private fun setStatusBarRequiredForOngoingCall(state: OngoingCallModel) { val statusBarRequired = state is OngoingCallModel.InCall private fun setStatusBarRequiredForOngoingCall(statusBarRequired: Boolean) { // TODO(b/382808183): Create a single repository that can be utilized in // `statusBarModeRepositoryStore` and `statusBarWindowControllerStore` so we do not need // two separate calls to force the status bar to stay visible. Loading @@ -138,6 +190,16 @@ class OngoingCallInteractor @Inject constructor( .setOngoingProcessRequiresStatusBarVisible(statusBarRequired) } private fun updateGestureListening(isEnabled: Boolean) { if (isEnabled) { swipeStatusBarAwayGestureHandler.addOnGestureDetectedCallback(TAG) { _ -> onStatusBarSwiped() } } else { swipeStatusBarAwayGestureHandler.removeOnGestureDetectedCallback(TAG) } } companion object { private val TAG = "OngoingCall" } Loading packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureHandlerKosmos.kt 0 → 100644 +25 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.statusbar.gesture import com.android.systemui.kosmos.Kosmos import org.mockito.kotlin.mock val Kosmos.swipeStatusBarAwayGestureHandler: SwipeStatusBarAwayGestureHandler by Kosmos.Fixture { mock<SwipeStatusBarAwayGestureHandler>() } No newline at end of file packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorKosmos.kt +2 −0 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository import com.android.systemui.statusbar.gesture.swipeStatusBarAwayGestureHandler import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.statusbar.window.fakeStatusBarWindowControllerStore Loading @@ -32,6 +33,7 @@ val Kosmos.ongoingCallInteractor: OngoingCallInteractor by activityManagerRepository = activityManagerRepository, statusBarModeRepositoryStore = fakeStatusBarModeRepository, statusBarWindowControllerStore = fakeStatusBarWindowControllerStore, swipeStatusBarAwayGestureHandler = swipeStatusBarAwayGestureHandler, logBuffer = logcatLogBuffer("OngoingCallInteractorKosmos"), ) } Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorTest.kt +112 −45 Original line number Diff line number Diff line Loading @@ -31,6 +31,7 @@ import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.statusbar.StatusBarIconView import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository import com.android.systemui.statusbar.gesture.swipeStatusBarAwayGestureHandler import com.android.systemui.statusbar.notification.data.model.activeNotificationModel import com.android.systemui.statusbar.notification.data.repository.ActiveNotificationsStore import com.android.systemui.statusbar.notification.data.repository.activeNotificationListRepository Loading @@ -40,9 +41,14 @@ import com.android.systemui.statusbar.window.fakeStatusBarWindowControllerStore import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.clearInvocations import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.verify @SmallTest @RunWith(AndroidJUnit4::class) Loading @@ -51,6 +57,11 @@ class OngoingCallInteractorTest : SysuiTestCase() { private val repository = kosmos.activeNotificationListRepository private val underTest = kosmos.ongoingCallInteractor @Before fun setUp() { underTest.start() } @Test fun noNotification_emitsNoCall() = runTest { val state by collectLastValue(underTest.ongoingCallState) Loading Loading @@ -210,8 +221,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { @Test fun ongoingCallNotification_setsRequiresStatusBarVisibleTrue() = kosmos.runTest { val ongoingCallState by collectLastValue(underTest.ongoingCallState) val isStatusBarRequired by collectLastValue(underTest.isStatusBarRequiredForOngoingCall) val requiresStatusBarVisibleInRepository by collectLastValue( kosmos.fakeStatusBarModeRepository.defaultDisplay Loading @@ -222,21 +232,9 @@ class OngoingCallInteractorTest : SysuiTestCase() { kosmos.fakeStatusBarWindowControllerStore.defaultDisplay .ongoingProcessRequiresStatusBarVisible ) repository.activeNotifications.value = ActiveNotificationsStore.Builder() .apply { addIndividualNotif( activeNotificationModel( key = "notif1", whenTime = 1000L, callType = CallType.Ongoing, uid = UID, ) ) } .build() postOngoingCallNotification() assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java) assertThat(isStatusBarRequired).isTrue() assertThat(requiresStatusBarVisibleInRepository).isTrue() assertThat(requiresStatusBarVisibleInWindowController).isTrue() } Loading @@ -244,8 +242,7 @@ class OngoingCallInteractorTest : SysuiTestCase() { @Test fun notificationRemoved_setsRequiresStatusBarVisibleFalse() = kosmos.runTest { val ongoingCallState by collectLastValue(underTest.ongoingCallState) val isStatusBarRequired by collectLastValue(underTest.isStatusBarRequiredForOngoingCall) val requiresStatusBarVisibleInRepository by collectLastValue( kosmos.fakeStatusBarModeRepository.defaultDisplay Loading @@ -257,23 +254,11 @@ class OngoingCallInteractorTest : SysuiTestCase() { .ongoingProcessRequiresStatusBarVisible ) repository.activeNotifications.value = ActiveNotificationsStore.Builder() .apply { addIndividualNotif( activeNotificationModel( key = "notif1", whenTime = 1000L, callType = CallType.Ongoing, uid = UID, ) ) } .build() postOngoingCallNotification() repository.activeNotifications.value = ActiveNotificationsStore() assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.NoCall::class.java) assertThat(isStatusBarRequired).isFalse() assertThat(requiresStatusBarVisibleInRepository).isFalse() assertThat(requiresStatusBarVisibleInWindowController).isFalse() } Loading @@ -295,6 +280,99 @@ class OngoingCallInteractorTest : SysuiTestCase() { ) kosmos.activityManagerRepository.fake.startingIsAppVisibleValue = false postOngoingCallNotification() assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java) assertThat(requiresStatusBarVisibleInRepository).isTrue() assertThat(requiresStatusBarVisibleInWindowController).isTrue() kosmos.activityManagerRepository.fake.setIsAppVisible(UID, true) assertThat(ongoingCallState) .isInstanceOf(OngoingCallModel.InCallWithVisibleApp::class.java) assertThat(requiresStatusBarVisibleInRepository).isFalse() assertThat(requiresStatusBarVisibleInWindowController).isFalse() } @Test fun gestureHandler_inCall_notFullscreen_doesNotListen() = kosmos.runTest { val ongoingCallState by collectLastValue(underTest.ongoingCallState) clearInvocations(kosmos.swipeStatusBarAwayGestureHandler) // Set up notification but not in fullscreen kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = false postOngoingCallNotification() assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java) verify(kosmos.swipeStatusBarAwayGestureHandler, never()) .addOnGestureDetectedCallback(any(), any()) } @Test fun gestureHandler_inCall_fullscreen_addsListener() = kosmos.runTest { val isGestureListeningEnabled by collectLastValue(underTest.isGestureListeningEnabled) // Set up notification and fullscreen mode kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true postOngoingCallNotification() assertThat(isGestureListeningEnabled).isTrue() verify(kosmos.swipeStatusBarAwayGestureHandler) .addOnGestureDetectedCallback(any(), any()) } @Test fun gestureHandler_inCall_fullscreen_chipSwiped_removesListener() = kosmos.runTest { val swipeAwayState by collectLastValue(underTest.isChipSwipedAway) // Set up notification and fullscreen mode kosmos.fakeStatusBarModeRepository.defaultDisplay.isInFullscreenMode.value = true postOngoingCallNotification() clearInvocations(kosmos.swipeStatusBarAwayGestureHandler) underTest.onStatusBarSwiped() assertThat(swipeAwayState).isTrue() verify(kosmos.swipeStatusBarAwayGestureHandler).removeOnGestureDetectedCallback(any()) } @Test fun chipSwipedAway_setsRequiresStatusBarVisibleFalse() = kosmos.runTest { val isStatusBarRequiredForOngoingCall by collectLastValue(underTest.isStatusBarRequiredForOngoingCall) val requiresStatusBarVisibleInRepository by collectLastValue( kosmos.fakeStatusBarModeRepository.defaultDisplay .ongoingProcessRequiresStatusBarVisible ) val requiresStatusBarVisibleInWindowController by collectLastValue( kosmos.fakeStatusBarWindowControllerStore.defaultDisplay .ongoingProcessRequiresStatusBarVisible ) // Start with an ongoing call (which should set status bar required) postOngoingCallNotification() assertThat(isStatusBarRequiredForOngoingCall).isTrue() assertThat(requiresStatusBarVisibleInRepository).isTrue() assertThat(requiresStatusBarVisibleInWindowController).isTrue() // Swipe away the chip underTest.onStatusBarSwiped() // Verify status bar is no longer required assertThat(requiresStatusBarVisibleInRepository).isFalse() assertThat(requiresStatusBarVisibleInWindowController).isFalse() } private fun postOngoingCallNotification() { repository.activeNotifications.value = ActiveNotificationsStore.Builder() .apply { Loading @@ -308,17 +386,6 @@ class OngoingCallInteractorTest : SysuiTestCase() { ) } .build() assertThat(ongoingCallState).isInstanceOf(OngoingCallModel.InCall::class.java) assertThat(requiresStatusBarVisibleInRepository).isTrue() assertThat(requiresStatusBarVisibleInWindowController).isTrue() kosmos.activityManagerRepository.fake.setIsAppVisible(UID, true) assertThat(ongoingCallState) .isInstanceOf(OngoingCallModel.InCallWithVisibleApp::class.java) assertThat(requiresStatusBarVisibleInRepository).isFalse() assertThat(requiresStatusBarVisibleInWindowController).isFalse() } companion object { Loading
packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt +14 −0 Original line number Diff line number Diff line Loading @@ -37,6 +37,7 @@ import com.android.systemui.statusbar.phone.StatusBarSignalPolicy import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLog import com.android.systemui.statusbar.phone.ongoingcall.StatusBarChipsModernization import com.android.systemui.statusbar.phone.ongoingcall.domain.interactor.OngoingCallInteractor import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.ui.SystemBarUtilsProxyImpl import com.android.systemui.statusbar.window.MultiDisplayStatusBarWindowControllerStore Loading Loading @@ -99,6 +100,19 @@ interface StatusBarModule { controller } @Provides @SysUISingleton @IntoMap @ClassKey(OngoingCallInteractor::class) fun ongoingCallInteractor( interactor: OngoingCallInteractor ): CoreStartable = if (StatusBarChipsModernization.isEnabled) { interactor } else { CoreStartable.NOP } @Provides @SysUISingleton fun lightBarController(store: LightBarControllerStore): LightBarController { Loading
packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractor.kt +68 −6 Original line number Diff line number Diff line Loading @@ -16,12 +16,15 @@ package com.android.systemui.statusbar.phone.ongoingcall.domain.interactor import androidx.annotation.VisibleForTesting import com.android.systemui.CoreStartable import com.android.systemui.activity.data.repository.ActivityManagerRepository import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.log.LogBuffer import com.android.systemui.log.core.Logger import com.android.systemui.statusbar.data.repository.StatusBarModeRepositoryStore import com.android.systemui.statusbar.gesture.SwipeStatusBarAwayGestureHandler import com.android.systemui.statusbar.notification.domain.interactor.ActiveNotificationsInteractor import com.android.systemui.statusbar.notification.shared.ActiveNotificationModel import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLog Loading @@ -31,11 +34,15 @@ import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.filterIsInstance import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.stateIn Loading @@ -55,11 +62,18 @@ class OngoingCallInteractor @Inject constructor( private val activityManagerRepository: ActivityManagerRepository, private val statusBarModeRepositoryStore: StatusBarModeRepositoryStore, private val statusBarWindowControllerStore: StatusBarWindowControllerStore, private val swipeStatusBarAwayGestureHandler: SwipeStatusBarAwayGestureHandler, activeNotificationsInteractor: ActiveNotificationsInteractor, @OngoingCallLog private val logBuffer: LogBuffer, ) { ) : CoreStartable { private val logger = Logger(logBuffer, TAG) /** * Tracks whether the call chip has been swiped away. */ private val _isChipSwipedAway = MutableStateFlow(false) val isChipSwipedAway: StateFlow<Boolean> = _isChipSwipedAway.asStateFlow() /** * The current state of ongoing calls. */ Loading @@ -70,15 +84,29 @@ class OngoingCallInteractor @Inject constructor( notification = notification ) } .onEach { state -> setStatusBarRequiredForOngoingCall(state) } .stateIn( scope = scope, started = SharingStarted.WhileSubscribed(), initialValue = OngoingCallModel.NoCall ) @VisibleForTesting val isStatusBarRequiredForOngoingCall = combine( ongoingCallState, isChipSwipedAway ) { callState, chipSwipedAway -> callState is OngoingCallModel.InCall && !chipSwipedAway } @VisibleForTesting val isGestureListeningEnabled = combine( ongoingCallState, statusBarModeRepositoryStore.defaultDisplay.isInFullscreenMode, isChipSwipedAway ) { callState, isFullscreen, chipSwipedAway -> callState is OngoingCallModel.InCall && !chipSwipedAway && isFullscreen } private fun createOngoingCallStateFlow( notification: ActiveNotificationModel? ): Flow<OngoingCallModel> { Loading @@ -99,6 +127,31 @@ class OngoingCallInteractor @Inject constructor( } } override fun start() { ongoingCallState .filterIsInstance<OngoingCallModel.NoCall>() .onEach { _isChipSwipedAway.value = false }.launchIn(scope) isStatusBarRequiredForOngoingCall.onEach { statusBarRequired -> setStatusBarRequiredForOngoingCall(statusBarRequired) }.launchIn(scope) isGestureListeningEnabled.onEach { isEnabled -> updateGestureListening(isEnabled) }.launchIn(scope) } /** * Callback that must run when the status bar is swiped while gesture listening is active. */ @VisibleForTesting fun onStatusBarSwiped() { logger.d("Status bar chip swiped away") _isChipSwipedAway.value = true } private fun deriveOngoingCallState( model: ActiveNotificationModel, isVisible: Boolean Loading Loading @@ -126,8 +179,7 @@ class OngoingCallInteractor @Inject constructor( } } private fun setStatusBarRequiredForOngoingCall(state: OngoingCallModel) { val statusBarRequired = state is OngoingCallModel.InCall private fun setStatusBarRequiredForOngoingCall(statusBarRequired: Boolean) { // TODO(b/382808183): Create a single repository that can be utilized in // `statusBarModeRepositoryStore` and `statusBarWindowControllerStore` so we do not need // two separate calls to force the status bar to stay visible. Loading @@ -138,6 +190,16 @@ class OngoingCallInteractor @Inject constructor( .setOngoingProcessRequiresStatusBarVisible(statusBarRequired) } private fun updateGestureListening(isEnabled: Boolean) { if (isEnabled) { swipeStatusBarAwayGestureHandler.addOnGestureDetectedCallback(TAG) { _ -> onStatusBarSwiped() } } else { swipeStatusBarAwayGestureHandler.removeOnGestureDetectedCallback(TAG) } } companion object { private val TAG = "OngoingCall" } Loading
packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/gesture/SwipeStatusBarAwayGestureHandlerKosmos.kt 0 → 100644 +25 −0 Original line number Diff line number Diff line /* * Copyright (C) 2024 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.statusbar.gesture import com.android.systemui.kosmos.Kosmos import org.mockito.kotlin.mock val Kosmos.swipeStatusBarAwayGestureHandler: SwipeStatusBarAwayGestureHandler by Kosmos.Fixture { mock<SwipeStatusBarAwayGestureHandler>() } No newline at end of file
packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/phone/ongoingcall/domain/interactor/OngoingCallInteractorKosmos.kt +2 −0 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.log.logcatLogBuffer import com.android.systemui.statusbar.data.repository.fakeStatusBarModeRepository import com.android.systemui.statusbar.gesture.swipeStatusBarAwayGestureHandler import com.android.systemui.statusbar.notification.domain.interactor.activeNotificationsInteractor import com.android.systemui.statusbar.window.fakeStatusBarWindowControllerStore Loading @@ -32,6 +33,7 @@ val Kosmos.ongoingCallInteractor: OngoingCallInteractor by activityManagerRepository = activityManagerRepository, statusBarModeRepositoryStore = fakeStatusBarModeRepository, statusBarWindowControllerStore = fakeStatusBarWindowControllerStore, swipeStatusBarAwayGestureHandler = swipeStatusBarAwayGestureHandler, logBuffer = logcatLogBuffer("OngoingCallInteractorKosmos"), ) }