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

Commit 75d3ee72 authored by Treehugger Robot's avatar Treehugger Robot Committed by Android (Google) Code Review
Browse files

Merge "Add support for swiping up on the status bar in OngoingCallInteractor" into main

parents 744a3f5d 171d0880
Loading
Loading
Loading
Loading
+112 −45
Original line number Diff line number Diff line
@@ -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
@@ -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)
@@ -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)
@@ -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
@@ -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()
        }
@@ -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
@@ -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()
        }
@@ -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 {
@@ -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 {
+14 −0
Original line number Diff line number Diff line
@@ -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
@@ -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 {
+68 −6
Original line number Diff line number Diff line
@@ -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
@@ -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

@@ -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.
     */
@@ -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> {
@@ -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
@@ -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.
@@ -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"
    }
+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
+2 −0
Original line number Diff line number Diff line
@@ -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

@@ -32,6 +33,7 @@ val Kosmos.ongoingCallInteractor: OngoingCallInteractor by
          activityManagerRepository = activityManagerRepository,
          statusBarModeRepositoryStore = fakeStatusBarModeRepository,
          statusBarWindowControllerStore = fakeStatusBarWindowControllerStore,
          swipeStatusBarAwayGestureHandler = swipeStatusBarAwayGestureHandler,
          logBuffer = logcatLogBuffer("OngoingCallInteractorKosmos"),
      )
    }