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

Commit 2350d917 authored by burakov's avatar burakov
Browse files

[Dual Shade] Refactor ShadeHeaderViewModel, removing all StateFlows.

Fix: 417215516
Test: Added unit tests.
Flag: com.android.systemui.scene_container
Change-Id: I8e3363408db476c279c2a71ca4555ab9b89d13b2
parent 64fab229
Loading
Loading
Loading
Loading
+20 −28
Original line number Diff line number Diff line
@@ -72,7 +72,6 @@ import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.max
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.animation.scene.ContentScope
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.LowestZIndexContentPicker
@@ -92,6 +91,7 @@ import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.kairos.ExperimentalKairosApi
import com.android.systemui.kairos.util.nameTag
import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.privacy.PrivacyItem
import com.android.systemui.res.R
import com.android.systemui.scene.shared.model.DualShadeEducationElement
import com.android.systemui.scene.shared.model.Scenes
@@ -192,8 +192,6 @@ fun ContentScope.CollapsedShadeHeader(
            }
        }

    val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle()

    // This layout assumes it is globally positioned at (0, 0) and is the same size as the screen.
    CutoutAwareShadeHeader(
        modifier = modifier,
@@ -213,10 +211,11 @@ fun ContentScope.CollapsedShadeHeader(
            }
        },
        endContent = {
            if (isPrivacyChipVisible) {
            if (viewModel.isPrivacyChipVisible) {
                Box(modifier = Modifier.fillMaxSize().padding(horizontal = horizontalPadding)) {
                    PrivacyChip(
                        viewModel = viewModel,
                        privacyList = viewModel.privacyItems,
                        onClick = viewModel::onPrivacyChipClicked,
                        modifier = Modifier.align(Alignment.CenterEnd),
                    )
                }
@@ -267,12 +266,14 @@ fun ContentScope.ExpandedShadeHeader(
        derivedStateOf { shouldUseExpandedFormat(layoutState.transitionState) }
    }

    val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle()

    Box(modifier = modifier.sysuiResTag(ShadeHeader.TestTags.Root)) {
        if (isPrivacyChipVisible) {
        if (viewModel.isPrivacyChipVisible) {
            Box(modifier = Modifier.height(ShadeHeader.Dimensions.StatusBarHeight).fillMaxWidth()) {
                PrivacyChip(viewModel = viewModel, modifier = Modifier.align(Alignment.CenterEnd))
                PrivacyChip(
                    privacyList = viewModel.privacyItems,
                    onClick = viewModel::onPrivacyChipClicked,
                    modifier = Modifier.align(Alignment.CenterEnd),
                )
            }
        }
        Column(
@@ -345,8 +346,6 @@ fun ContentScope.OverlayShadeHeader(
    val horizontalPadding =
        max(LocalScreenCornerRadius.current / 2f, Shade.Dimensions.HorizontalPadding)

    val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle()

    // This layout assumes it is globally positioned at (0, 0) and is the same size as the screen.
    CutoutAwareShadeHeader(
        modifier = modifier,
@@ -422,10 +421,11 @@ fun ContentScope.OverlayShadeHeader(
                        isHighlighted = isHighlighted,
                    )
                }
                if (isPrivacyChipVisible) {
                if (viewModel.isPrivacyChipVisible) {
                    Box(modifier = Modifier.fillMaxSize().padding(horizontal = horizontalPadding)) {
                        PrivacyChip(
                            viewModel = viewModel,
                            privacyList = viewModel.privacyItems,
                            onClick = viewModel::onPrivacyChipClicked,
                            modifier = Modifier.align(Alignment.CenterEnd),
                        )
                    }
@@ -723,13 +723,6 @@ private fun ContentScope.StatusIcons(
    val micSlot = stringResource(id = com.android.internal.R.string.status_bar_microphone)
    val locationSlot = stringResource(id = com.android.internal.R.string.status_bar_location)

    val isSingleCarrier by viewModel.isSingleCarrier.collectAsStateWithLifecycle()
    val isPrivacyChipEnabled by viewModel.isPrivacyChipEnabled.collectAsStateWithLifecycle()
    val isMicCameraIndicationEnabled by
        viewModel.isMicCameraIndicationEnabled.collectAsStateWithLifecycle()
    val isLocationIndicationEnabled by
        viewModel.isLocationIndicationEnabled.collectAsStateWithLifecycle()

    val iconContainer = remember { StatusIconContainer(themedContext, null) }
    val iconManager = remember {
        viewModel.createTintedIconManager(iconContainer, StatusBarLocation.QS)
@@ -747,21 +740,21 @@ private fun ContentScope.StatusIcons(
            iconContainer.setQsExpansionTransitioning(
                layoutState.isTransitioningBetween(Scenes.Shade, Scenes.QuickSettings)
            )
            if (isSingleCarrier || !useExpandedFormat) {
            if (viewModel.isSingleCarrier || !useExpandedFormat) {
                iconContainer.removeIgnoredSlots(carrierIconSlots)
            } else {
                iconContainer.addIgnoredSlots(carrierIconSlots)
            }

            if (isPrivacyChipEnabled) {
                if (isMicCameraIndicationEnabled) {
            if (viewModel.isPrivacyChipEnabled) {
                if (viewModel.isMicCameraIndicationEnabled) {
                    iconContainer.addIgnoredSlot(cameraSlot)
                    iconContainer.addIgnoredSlot(micSlot)
                } else {
                    iconContainer.removeIgnoredSlot(cameraSlot)
                    iconContainer.removeIgnoredSlot(micSlot)
                }
                if (isLocationIndicationEnabled) {
                if (viewModel.isLocationIndicationEnabled) {
                    iconContainer.addIgnoredSlot(locationSlot)
                } else {
                    iconContainer.removeIgnoredSlot(locationSlot)
@@ -829,17 +822,16 @@ private fun SystemIconChip(

@Composable
private fun ContentScope.PrivacyChip(
    viewModel: ShadeHeaderViewModel,
    privacyList: List<PrivacyItem>,
    onClick: (OngoingPrivacyChip) -> Unit,
    modifier: Modifier = Modifier,
) {
    val privacyList by viewModel.privacyItems.collectAsStateWithLifecycle()

    AndroidView(
        factory = { context ->
            val view =
                OngoingPrivacyChip(context, null).also { privacyChip ->
                    privacyChip.privacyList = privacyList
                    privacyChip.setOnClickListener { viewModel.onPrivacyChipClicked(privacyChip) }
                    privacyChip.setOnClickListener { onClick(privacyChip) }
                }
            view
        },
+0 −72
Original line number Diff line number Diff line
@@ -19,19 +19,14 @@ package com.android.systemui.shade.domain.interactor
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.privacy.PrivacyApplication
import com.android.systemui.privacy.PrivacyItem
import com.android.systemui.privacy.PrivacyType
import com.android.systemui.privacy.privacyDialogController
import com.android.systemui.privacy.privacyDialogControllerV2
import com.android.systemui.shade.data.repository.fakePrivacyChipRepository
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
@@ -61,73 +56,6 @@ class PrivacyChipInteractorTest : SysuiTestCase() {
        whenever(privacyChip.context).thenReturn(this.context)
    }

    @Test
    fun isChipVisible_updates() =
        testScope.runTest {
            val actual by collectLastValue(underTest.isChipVisible)

            privacyChipRepository.setPrivacyItems(emptyList())
            runCurrent()

            assertThat(actual).isFalse()

            val privacyItems =
                listOf(
                    PrivacyItem(
                        privacyType = PrivacyType.TYPE_CAMERA,
                        application = PrivacyApplication("", 0)
                    ),
                )
            privacyChipRepository.setPrivacyItems(privacyItems)
            runCurrent()

            assertThat(actual).isTrue()
        }

    @Test
    fun isChipEnabled_noIndicationEnabled() =
        testScope.runTest {
            val actual by collectLastValue(underTest.isChipEnabled)

            privacyChipRepository.setIsMicCameraIndicationEnabled(false)
            privacyChipRepository.setIsLocationIndicationEnabled(false)

            assertThat(actual).isFalse()
        }

    @Test
    fun isChipEnabled_micCameraIndicationEnabled() =
        testScope.runTest {
            val actual by collectLastValue(underTest.isChipEnabled)

            privacyChipRepository.setIsMicCameraIndicationEnabled(true)
            privacyChipRepository.setIsLocationIndicationEnabled(false)

            assertThat(actual).isTrue()
        }

    @Test
    fun isChipEnabled_locationIndicationEnabled() =
        testScope.runTest {
            val actual by collectLastValue(underTest.isChipEnabled)

            privacyChipRepository.setIsMicCameraIndicationEnabled(false)
            privacyChipRepository.setIsLocationIndicationEnabled(true)

            assertThat(actual).isTrue()
        }

    @Test
    fun isChipEnabled_allIndicationEnabled() =
        testScope.runTest {
            val actual by collectLastValue(underTest.isChipEnabled)

            privacyChipRepository.setIsMicCameraIndicationEnabled(true)
            privacyChipRepository.setIsLocationIndicationEnabled(true)

            assertThat(actual).isTrue()
        }

    @OptIn(ExperimentalCoroutinesApi::class)
    @Test
    fun onPrivacyChipClicked_safetyCenterEnabled() =
+61 −21
Original line number Diff line number Diff line
@@ -15,13 +15,18 @@ import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.runTest
import com.android.systemui.kosmos.testScope
import com.android.systemui.kosmos.useUnconfinedTestDispatcher
import com.android.systemui.lifecycle.activateIn
import com.android.systemui.plugins.activityStarter
import com.android.systemui.privacy.PrivacyApplication
import com.android.systemui.privacy.PrivacyItem
import com.android.systemui.privacy.PrivacyType
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Overlays
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.data.repository.fakePrivacyChipRepository
import com.android.systemui.shade.domain.interactor.disableDualShade
import com.android.systemui.shade.domain.interactor.enableDualShade
import com.android.systemui.shade.domain.interactor.shadeMode
@@ -31,10 +36,8 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.fakeMobi
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.argThat
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Test
@@ -44,7 +47,6 @@ import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations

@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
@EnableSceneContainer
@@ -66,12 +68,10 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
    fun mobileSubIds_update() =
        testScope.runTest {
            mobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1)
            runCurrent()

            assertThat(underTest.mobileSubIds).isEqualTo(listOf(1))

            mobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
            runCurrent()

            assertThat(underTest.mobileSubIds).isEqualTo(listOf(1, 2))
        }
@@ -110,7 +110,6 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
            setScene(Scenes.Shade)

            underTest.onSystemIconChipClicked()
            runCurrent()

            assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Lockscreen)
        }
@@ -123,7 +122,6 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)

            underTest.onSystemIconChipClicked()
            runCurrent()

            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
            assertThat(currentOverlays).isEmpty()
@@ -137,7 +135,6 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)

            underTest.onSystemIconChipClicked()
            runCurrent()

            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
            assertThat(currentOverlays).contains(Overlays.QuickSettingsShade)
@@ -152,7 +149,6 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
            setScene(Scenes.Shade)

            underTest.onSystemIconChipClicked()
            runCurrent()

            assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Gone)
        }
@@ -165,7 +161,6 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)

            underTest.onSystemIconChipClicked()
            runCurrent()

            assertThat(currentScene).isEqualTo(Scenes.Gone)
            assertThat(currentOverlays).isEmpty()
@@ -179,7 +174,6 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)

            underTest.onSystemIconChipClicked()
            runCurrent()

            assertThat(currentScene).isEqualTo(Scenes.Gone)
            assertThat(currentOverlays).contains(Overlays.QuickSettingsShade)
@@ -194,7 +188,6 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)

            underTest.onNotificationIconChipClicked()
            runCurrent()

            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
            assertThat(currentOverlays).isEmpty()
@@ -208,7 +201,6 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)

            underTest.onNotificationIconChipClicked()
            runCurrent()

            assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
            assertThat(currentOverlays).contains(Overlays.NotificationsShade)
@@ -223,7 +215,6 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)

            underTest.onNotificationIconChipClicked()
            runCurrent()

            assertThat(currentScene).isEqualTo(Scenes.Gone)
            assertThat(currentOverlays).isEmpty()
@@ -237,13 +228,67 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
            val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)

            underTest.onNotificationIconChipClicked()
            runCurrent()

            assertThat(currentScene).isEqualTo(Scenes.Gone)
            assertThat(currentOverlays).contains(Overlays.NotificationsShade)
            assertThat(currentOverlays).doesNotContain(Overlays.QuickSettingsShade)
        }

    @Test
    fun isPrivacyChipVisible_updates() =
        kosmos.runTest {
            fakePrivacyChipRepository.setPrivacyItems(emptyList())

            assertThat(underTest.isPrivacyChipVisible).isFalse()

            fakePrivacyChipRepository.setPrivacyItems(
                listOf(
                    PrivacyItem(
                        privacyType = PrivacyType.TYPE_CAMERA,
                        application = PrivacyApplication("", 0),
                    )
                )
            )

            assertThat(underTest.isPrivacyChipVisible).isTrue()
        }

    @Test
    fun isPrivacyChipEnabled_noIndicationEnabled() =
        kosmos.runTest {
            fakePrivacyChipRepository.setIsMicCameraIndicationEnabled(false)
            fakePrivacyChipRepository.setIsLocationIndicationEnabled(false)

            assertThat(underTest.isPrivacyChipEnabled).isFalse()
        }

    @Test
    fun isPrivacyChipEnabled_micCameraIndicationEnabled() =
        kosmos.runTest {
            fakePrivacyChipRepository.setIsMicCameraIndicationEnabled(true)
            fakePrivacyChipRepository.setIsLocationIndicationEnabled(false)

            assertThat(underTest.isPrivacyChipEnabled).isTrue()
        }

    @Test
    fun isPrivacyChipEnabled_locationIndicationEnabled() =
        kosmos.runTest {
            fakePrivacyChipRepository.setIsMicCameraIndicationEnabled(false)
            fakePrivacyChipRepository.setIsLocationIndicationEnabled(true)

            assertThat(underTest.isPrivacyChipEnabled).isTrue()
        }

    @Test
    fun isPrivacyChipEnabled_allIndicationEnabled() =
        kosmos.runTest {
            fakePrivacyChipRepository.setIsMicCameraIndicationEnabled(true)
            fakePrivacyChipRepository.setIsLocationIndicationEnabled(true)

            assertThat(underTest.isPrivacyChipEnabled).isTrue()
        }

    companion object {
        private val SUB_1 =
            SubscriptionModel(
@@ -272,19 +317,16 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
                SuccessFingerprintAuthenticationStatus(0, true)
            )
        }
        runCurrent()
        assertThat(shadeMode).isEqualTo(ShadeMode.Dual)

        sceneInteractor.changeScene(scene, "test")
        checkNotNull(currentOverlays).forEach { sceneInteractor.instantlyHideOverlay(it, "test") }
        runCurrent()
        overlay?.let { sceneInteractor.showOverlay(it, "test") }
        sceneInteractor.setTransitionState(
            MutableStateFlow<ObservableTransitionState>(
                ObservableTransitionState.Idle(scene, setOfNotNull(overlay))
            )
        )
        runCurrent()

        assertThat(currentScene).isEqualTo(scene)
        if (overlay == null) {
@@ -299,16 +341,14 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
        sceneInteractor.setTransitionState(
            MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key))
        )
        testScope.runCurrent()
    }

    private fun TestScope.setDeviceEntered(isEntered: Boolean) {
    private fun setDeviceEntered(isEntered: Boolean) {
        if (isEntered) {
            // Unlock the device marking the device has entered.
            kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
                SuccessFingerprintAuthenticationStatus(0, true)
            )
            runCurrent()
        }
        setScene(
            if (isEntered) {
+1 −29
Original line number Diff line number Diff line
@@ -26,11 +26,7 @@ import com.android.systemui.shade.data.repository.PrivacyChipRepository
import com.android.systemui.statusbar.policy.DeviceProvisionedController
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch

@SysUISingleton
@@ -53,30 +49,6 @@ constructor(
    /** Whether or not location indicators are enabled in the device privacy config. */
    val isLocationIndicationEnabled: StateFlow<Boolean> = repository.isLocationIndicationEnabled

    /** Whether or not the privacy chip should be visible. */
    val isChipVisible: StateFlow<Boolean> =
        privacyItems
            .map { it.isNotEmpty() }
            .stateIn(
                scope = applicationScope,
                started = SharingStarted.WhileSubscribed(),
                initialValue = false,
            )

    /** Whether or not the privacy chip is enabled in the device privacy config. */
    val isChipEnabled: StateFlow<Boolean> =
        combine(
                isMicCameraIndicationEnabled,
                isLocationIndicationEnabled,
            ) { micCamera, location ->
                micCamera || location
            }
            .stateIn(
                scope = applicationScope,
                started = SharingStarted.WhileSubscribed(),
                initialValue = false,
            )

    /** Notifies that the privacy chip was clicked. */
    fun onPrivacyChipClicked(privacyChip: OngoingPrivacyChip) {
        if (!deviceProvisionedController.isDeviceProvisioned) return
@@ -85,7 +57,7 @@ constructor(
            if (repository.isSafetyCenterEnabled()) {
                privacyDialogControllerV2.showDialog(
                    shadeDialogContextInteractor.context,
                    privacyChip
                    privacyChip,
                )
            } else {
                privacyDialogController.showDialog(shadeDialogContextInteractor.context)
+26 −9
Original line number Diff line number Diff line
@@ -24,6 +24,7 @@ import android.icu.text.DateFormat
import android.icu.text.DisplayContext
import android.provider.Settings
import android.view.ViewGroup
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.ui.unit.IntRect
import com.android.app.tracing.coroutines.launchTraced as launch
@@ -63,7 +64,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.mapLatest
@@ -109,7 +109,12 @@ constructor(
        batteryMeterViewControllerFactory::create

    /** True if there is exactly one mobile connection. */
    val isSingleCarrier: StateFlow<Boolean> = mobileIconsInteractor.isSingleCarrier
    val isSingleCarrier: Boolean by
        hydrator.hydratedStateOf(
            traceName = "isSingleCarrier",
            initialValue = mobileIconsInteractor.isSingleCarrier.value,
            source = mobileIconsInteractor.isSingleCarrier,
        )

    /** The list of subscription Ids for current mobile connections. */
    val mobileSubIds: List<Int> by
@@ -123,21 +128,33 @@ constructor(
        )

    /** The list of PrivacyItems to be displayed by the privacy chip. */
    val privacyItems: StateFlow<List<PrivacyItem>> = privacyChipInteractor.privacyItems
    val privacyItems: List<PrivacyItem> by
        hydrator.hydratedStateOf(
            traceName = "privacyItems",
            source = privacyChipInteractor.privacyItems,
        )

    /** Whether or not mic & camera indicators are enabled in the device privacy config. */
    val isMicCameraIndicationEnabled: StateFlow<Boolean> =
        privacyChipInteractor.isMicCameraIndicationEnabled
    val isMicCameraIndicationEnabled: Boolean by
        hydrator.hydratedStateOf(
            traceName = "isMicCameraIndicationEnabled",
            source = privacyChipInteractor.isMicCameraIndicationEnabled,
        )

    /** Whether or not location indicators are enabled in the device privacy config. */
    val isLocationIndicationEnabled: StateFlow<Boolean> =
        privacyChipInteractor.isLocationIndicationEnabled
    val isLocationIndicationEnabled: Boolean by
        hydrator.hydratedStateOf(
            traceName = "isLocationIndicationEnabled",
            source = privacyChipInteractor.isLocationIndicationEnabled,
        )

    /** Whether or not the privacy chip should be visible. */
    val isPrivacyChipVisible: StateFlow<Boolean> = privacyChipInteractor.isChipVisible
    val isPrivacyChipVisible: Boolean by derivedStateOf { privacyItems.isNotEmpty() }

    /** Whether or not the privacy chip is enabled in the device privacy config. */
    val isPrivacyChipEnabled: StateFlow<Boolean> = privacyChipInteractor.isChipEnabled
    val isPrivacyChipEnabled: Boolean by derivedStateOf {
        isMicCameraIndicationEnabled || isLocationIndicationEnabled
    }

    val animateNotificationsChipBounce: Boolean
        get() =