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

Commit 04481f29 authored by Caitlin Shkuratov's avatar Caitlin Shkuratov
Browse files

[SB][Screen Chips] Move call chip to new chips architecture.

This CL updates the new chips architecture to correctly show the call
chip when it's available and no other screen sharing events are
happening.

Bug:  332662551
Flag: com.android.systemui.status_bar_screen_sharing_chips
Test: start phone call -> see call chip
Test: then, start a screen recording -> see screen recording chip
instead, with correct time
Test: then, end screen recording -> see call chip again, with correct
time
Test: atest CallChipInteractorTest

Change-Id: I2b74c1e90081b5d41e57882d85105de43ccc1afe
parent c4c7c0f6
Loading
Loading
Loading
Loading
+63 −4
Original line number Original line Diff line number Diff line
@@ -16,17 +16,76 @@


package com.android.systemui.statusbar.chips.call.domain.interactor
package com.android.systemui.statusbar.chips.call.domain.interactor


import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.res.R
import com.android.systemui.statusbar.chips.domain.interactor.OngoingActivityChipInteractor
import com.android.systemui.statusbar.chips.domain.interactor.OngoingActivityChipInteractor
import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
import com.android.systemui.statusbar.phone.ongoingcall.data.model.OngoingCallModel
import com.android.systemui.statusbar.phone.ongoingcall.data.repository.OngoingCallRepository
import com.android.systemui.util.time.SystemClock
import javax.inject.Inject
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn


/** Interactor for the ongoing phone call chip shown in the status bar. */
/** Interactor for the ongoing phone call chip shown in the status bar. */
@SysUISingleton
@SysUISingleton
open class CallChipInteractor @Inject constructor() : OngoingActivityChipInteractor {
open class CallChipInteractor
    // TODO(b/332662551): Implement this flow.
@Inject
constructor(
    @Application private val scope: CoroutineScope,
    repository: OngoingCallRepository,
    systemClock: SystemClock,
    private val activityStarter: ActivityStarter,
) : OngoingActivityChipInteractor {
    override val chip: StateFlow<OngoingActivityChipModel> =
    override val chip: StateFlow<OngoingActivityChipModel> =
        MutableStateFlow(OngoingActivityChipModel.Hidden)
        repository.ongoingCallState
            .map { state ->
                when (state) {
                    is OngoingCallModel.NoCall -> OngoingActivityChipModel.Hidden
                    is OngoingCallModel.InCall -> {
                        // This mimics OngoingCallController#updateChip.
                        // TODO(b/332662551): Handle `state.startTimeMs = 0` correctly (see
                        // b/192379214 and
                        // OngoingCallController.CallNotificationInfo.hasValidStartTime).
                        val startTimeInElapsedRealtime =
                            state.startTimeMs - systemClock.currentTimeMillis() +
                                systemClock.elapsedRealtime()
                        OngoingActivityChipModel.Shown(
                            icon =
                                Icon.Resource(
                                    com.android.internal.R.drawable.ic_phone,
                                    contentDescription = null,
                                ),
                            startTimeMs = startTimeInElapsedRealtime,
                        ) {
                            if (state.intent != null) {
                                val backgroundView =
                                    it.requireViewById<ChipBackgroundContainer>(
                                        R.id.ongoing_activity_chip_background
                                    )
                                // TODO(b/332662551): Log the click event.
                                // This mimics OngoingCallController#updateChipClickListener.
                                activityStarter.postStartActivityDismissingKeyguard(
                                    state.intent,
                                    ActivityTransitionAnimator.Controller.fromView(
                                        backgroundView,
                                        InteractionJankMonitor
                                            .CUJ_STATUS_BAR_APP_LAUNCH_FROM_CALL_CHIP,
                                    )
                                )
                            }
                        }
                    }
                }
            }
            .stateIn(scope, SharingStarted.WhileSubscribed(), OngoingActivityChipModel.Hidden)
}
}
+33 −12
Original line number Original line Diff line number Diff line
@@ -29,6 +29,7 @@ import androidx.annotation.VisibleForTesting
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.jank.InteractionJankMonitor
import com.android.systemui.CoreStartable
import com.android.systemui.CoreStartable
import com.android.systemui.Dumpable
import com.android.systemui.Dumpable
import com.android.systemui.Flags
import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Application
@@ -188,7 +189,10 @@ constructor(
                callNotificationInfo
                callNotificationInfo
                    // This shouldn't happen, but protect against it in case
                    // This shouldn't happen, but protect against it in case
                    ?: return OngoingCallModel.NoCall
                    ?: return OngoingCallModel.NoCall
            return OngoingCallModel.InCall(currentInfo.callStartTime)
            return OngoingCallModel.InCall(
                startTimeMs = currentInfo.callStartTime,
                intent = currentInfo.intent,
            )
        } else {
        } else {
            return OngoingCallModel.NoCall
            return OngoingCallModel.NoCall
        }
        }
@@ -213,18 +217,29 @@ constructor(
        val timeView = currentChipView?.getTimeView()
        val timeView = currentChipView?.getTimeView()


        if (currentChipView != null && timeView != null) {
        if (currentChipView != null && timeView != null) {
            if (!Flags.statusBarScreenSharingChips()) {
                // If the new status bar screen sharing chips are enabled, then the display logic
                // for *all* status bar chips (both the call chip and the screen sharing chips) are
                // handled by CollapsedStatusBarViewBinder, *not* this class. We need to disable
                // this class from making any display changes because the new chips use the same
                // view as the old call chip.
                // TODO(b/332662551): We should move this whole controller class to recommended
                // architecture so that we don't need to awkwardly disable only some parts of this
                // class.
                if (currentCallNotificationInfo.hasValidStartTime()) {
                if (currentCallNotificationInfo.hasValidStartTime()) {
                    timeView.setShouldHideText(false)
                    timeView.setShouldHideText(false)
                    timeView.base =
                    timeView.base =
                    currentCallNotificationInfo.callStartTime - systemClock.currentTimeMillis() +
                        currentCallNotificationInfo.callStartTime -
                        systemClock.elapsedRealtime()
                            systemClock.currentTimeMillis() + systemClock.elapsedRealtime()
                    timeView.start()
                    timeView.start()
                } else {
                } else {
                    timeView.setShouldHideText(true)
                    timeView.setShouldHideText(true)
                    timeView.stop()
                    timeView.stop()
                }
                }
                updateChipClickListener()
                updateChipClickListener()
            }


            // But, this class still needs to do the non-display logic regardless of the flag.
            uidObserver.registerWithUid(currentCallNotificationInfo.uid)
            uidObserver.registerWithUid(currentCallNotificationInfo.uid)
            if (!currentCallNotificationInfo.statusBarSwipedAway) {
            if (!currentCallNotificationInfo.statusBarSwipedAway) {
                statusBarWindowController.setOngoingProcessRequiresStatusBarVisible(true)
                statusBarWindowController.setOngoingProcessRequiresStatusBarVisible(true)
@@ -247,6 +262,10 @@ constructor(
    }
    }


    private fun updateChipClickListener() {
    private fun updateChipClickListener() {
        if (Flags.statusBarScreenSharingChips()) {
            return
        }

        if (callNotificationInfo == null) {
        if (callNotificationInfo == null) {
            return
            return
        }
        }
@@ -289,7 +308,9 @@ constructor(


    private fun removeChip() {
    private fun removeChip() {
        callNotificationInfo = null
        callNotificationInfo = null
        if (!Flags.statusBarScreenSharingChips()) {
            tearDownChipView()
            tearDownChipView()
        }
        statusBarWindowController.setOngoingProcessRequiresStatusBarVisible(false)
        statusBarWindowController.setOngoingProcessRequiresStatusBarVisible(false)
        swipeStatusBarAwayGestureHandler.removeOnGestureDetectedCallback(TAG)
        swipeStatusBarAwayGestureHandler.removeOnGestureDetectedCallback(TAG)
        sendStateChangeEvent()
        sendStateChangeEvent()
+4 −1
Original line number Original line Diff line number Diff line
@@ -16,6 +16,8 @@


package com.android.systemui.statusbar.phone.ongoingcall.data.model
package com.android.systemui.statusbar.phone.ongoingcall.data.model


import android.app.PendingIntent

/** Represents the state of any ongoing calls. */
/** Represents the state of any ongoing calls. */
sealed interface OngoingCallModel {
sealed interface OngoingCallModel {
    /** There is no ongoing call. */
    /** There is no ongoing call. */
@@ -28,6 +30,7 @@ sealed interface OngoingCallModel {
     *   `when` field. Importantly, this time is relative to
     *   `when` field. Importantly, this time is relative to
     *   [com.android.systemui.util.time.SystemClock.currentTimeMillis], **not**
     *   [com.android.systemui.util.time.SystemClock.currentTimeMillis], **not**
     *   [com.android.systemui.util.time.SystemClock.elapsedRealtime].
     *   [com.android.systemui.util.time.SystemClock.elapsedRealtime].
     * @property intent the intent associated with the call notification.
     */
     */
    data class InCall(val startTimeMs: Long) : OngoingCallModel
    data class InCall(val startTimeMs: Long, val intent: PendingIntent?) : OngoingCallModel
}
}
+160 −0
Original line number Original line 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.chips.call.domain.interactor

import android.app.PendingIntent
import android.view.View
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.activityStarter
import com.android.systemui.res.R
import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
import com.android.systemui.statusbar.phone.ongoingcall.data.model.OngoingCallModel
import com.android.systemui.statusbar.phone.ongoingcall.data.repository.ongoingCallRepository
import com.android.systemui.util.time.fakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlin.test.Test
import kotlinx.coroutines.test.runTest
import org.mockito.kotlin.mock
import org.mockito.kotlin.verify
import org.mockito.kotlin.whenever

@SmallTest
class CallChipInteractorTest : SysuiTestCase() {
    private val kosmos = Kosmos()
    private val testScope = kosmos.testScope
    private val repo = kosmos.ongoingCallRepository

    private val chipBackgroundView = mock<ChipBackgroundContainer>()
    private val chipView =
        mock<View>().apply {
            whenever(
                    this.requireViewById<ChipBackgroundContainer>(
                        R.id.ongoing_activity_chip_background
                    )
                )
                .thenReturn(chipBackgroundView)
        }

    private val underTest = kosmos.callChipInteractor

    @Test
    fun chip_noCall_isHidden() =
        testScope.runTest {
            val latest by collectLastValue(underTest.chip)

            repo.setOngoingCallState(OngoingCallModel.NoCall)

            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)
        }

    @Test
    fun chip_inCall_isShown() =
        testScope.runTest {
            val latest by collectLastValue(underTest.chip)

            repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 345, intent = null))

            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
        }

    @Test
    fun chip_inCall_startTimeConvertedToElapsedRealtime() =
        testScope.runTest {
            val latest by collectLastValue(underTest.chip)

            kosmos.fakeSystemClock.setCurrentTimeMillis(3000)
            kosmos.fakeSystemClock.setElapsedRealtime(400_000)

            repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 1000, intent = null))

            // The OngoingCallModel start time is relative to currentTimeMillis, so this call
            // started 2000ms ago (1000 - 3000). The OngoingActivityChipModel start time needs to be
            // relative to elapsedRealtime, so it should be 2000ms before the elapsed realtime set
            // on the clock.
            assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(398_000)
        }

    @Test
    fun chip_inCall_iconIsPhone() =
        testScope.runTest {
            val latest by collectLastValue(underTest.chip)

            repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 1000, intent = null))

            assertThat(((latest as OngoingActivityChipModel.Shown).icon as Icon.Resource).res)
                .isEqualTo(com.android.internal.R.drawable.ic_phone)
        }

    @Test
    fun chip_resetsCorrectly() =
        testScope.runTest {
            val latest by collectLastValue(underTest.chip)
            kosmos.fakeSystemClock.setCurrentTimeMillis(3000)
            kosmos.fakeSystemClock.setElapsedRealtime(400_000)

            // Start a call
            repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 1000, intent = null))
            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
            assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(398_000)

            // End the call
            repo.setOngoingCallState(OngoingCallModel.NoCall)
            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Hidden::class.java)

            // Let 100_000ms elapse
            kosmos.fakeSystemClock.setCurrentTimeMillis(103_000)
            kosmos.fakeSystemClock.setElapsedRealtime(500_000)

            // Start a new call, which started 1000ms ago
            repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 102_000, intent = null))
            assertThat(latest).isInstanceOf(OngoingActivityChipModel.Shown::class.java)
            assertThat((latest as OngoingActivityChipModel.Shown).startTimeMs).isEqualTo(499_000)
        }

    @Test
    fun chip_inCall_nullIntent_clickListenerDoesNothing() =
        testScope.runTest {
            val latest by collectLastValue(underTest.chip)

            repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 1000, intent = null))

            val clickListener = (latest as OngoingActivityChipModel.Shown).onClickListener

            clickListener.onClick(chipView)
            // Just verify nothing crashes
        }

    @Test
    fun chip_inCall_validIntent_clickListenerLaunchesIntent() =
        testScope.runTest {
            val latest by collectLastValue(underTest.chip)

            val intent = mock<PendingIntent>()
            repo.setOngoingCallState(OngoingCallModel.InCall(startTimeMs = 1000, intent = intent))
            val clickListener = (latest as OngoingActivityChipModel.Shown).onClickListener

            clickListener.onClick(chipView)

            verify(kosmos.activityStarter).postStartActivityDismissingKeyguard(intent, null)
        }
}
+0 −1
Original line number Original line Diff line number Diff line
@@ -29,7 +29,6 @@ import com.android.systemui.screenrecord.data.model.ScreenRecordModel
import com.android.systemui.screenrecord.data.repository.screenRecordRepository
import com.android.systemui.screenrecord.data.repository.screenRecordRepository
import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
import com.android.systemui.statusbar.chips.domain.model.OngoingActivityChipModel
import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
import com.android.systemui.statusbar.chips.ui.view.ChipBackgroundContainer
import com.android.systemui.statusbar.chips.ui.viewmodel.screenRecordChipInteractor
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
import com.android.systemui.statusbar.phone.mockSystemUIDialogFactory
import com.android.systemui.util.time.fakeSystemClock
import com.android.systemui.util.time.fakeSystemClock
Loading