summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author Shawn Lee <syeonlee@google.com> 2024-02-21 17:02:55 +0000
committer Android (Google) Code Review <android-gerrit@google.com> 2024-02-21 17:02:55 +0000
commit47f8752acebd71ef0c42a785943119b9a7cb41b6 (patch)
tree4a58e688dadb0295dc81ad3eb5abac1d92437651
parentda215de7d004ebe8462dfd4c95a1454916b47783 (diff)
parent9e031094988bcb56b04bb6993f627e54cfc74b0e (diff)
Merge "[flexiglass] Integrate Privacy Chip into Flexiglass Shade Header" into main
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt4
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt9
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt237
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt5
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt3
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/PrivacyChipRepositoryTest.kt183
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/PrivacyChipInteractorTest.kt154
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt5
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt3
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt6
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/data/repository/PrivacyChipRepository.kt133
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PrivacyChipInteractor.kt88
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt28
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/privacy/PrivacyDialogControllerKosmos.kt23
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/privacy/PrivacyDialogControllerV2Kosmos.kt23
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakePrivacyChipRepository.kt55
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/PrivacyChipRepositoryKosmos.kt24
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/PrivacyChipInteractorKosmos.kt35
19 files changed, 931 insertions, 93 deletions
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt
index 6f115d88dbe2..30c82f46f01d 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt
@@ -13,7 +13,9 @@ fun TransitionBuilder.goneToShadeTransition(
) {
spec = tween(durationMillis = DefaultDuration.times(durationScale).inWholeMilliseconds.toInt())
- fractionRange(start = .58f) { fade(ShadeHeader.Elements.CollapsedContent) }
+ fractionRange(start = .58f) { fade(ShadeHeader.Elements.CollapsedContentStart) }
+ fractionRange(start = .58f) { fade(ShadeHeader.Elements.CollapsedContentEnd) }
+ fractionRange(start = .58f) { fade(ShadeHeader.Elements.PrivacyChip) }
translate(QuickSettings.Elements.Content, y = -ShadeHeader.Dimensions.CollapsedHeight * .66f)
translate(Notifications.Elements.NotificationScrim, Edge.Top, false)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt
index d5c2a03b3f9f..3c15da12b647 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromShadeToQuickSettingsTransition.kt
@@ -13,10 +13,15 @@ fun TransitionBuilder.shadeToQuickSettingsTransition() {
translate(Notifications.Elements.NotificationScrim, Edge.Bottom)
timestampRange(endMillis = 83) { fade(QuickSettings.Elements.FooterActions) }
- translate(ShadeHeader.Elements.CollapsedContent, y = ShadeHeader.Dimensions.CollapsedHeight)
+ translate(
+ ShadeHeader.Elements.CollapsedContentStart,
+ y = ShadeHeader.Dimensions.CollapsedHeight
+ )
+ translate(ShadeHeader.Elements.CollapsedContentEnd, y = ShadeHeader.Dimensions.CollapsedHeight)
translate(ShadeHeader.Elements.ExpandedContent, y = (-ShadeHeader.Dimensions.ExpandedHeight))
- fractionRange(end = .14f) { fade(ShadeHeader.Elements.CollapsedContent) }
+ fractionRange(end = .14f) { fade(ShadeHeader.Elements.CollapsedContentStart) }
+ fractionRange(end = .14f) { fade(ShadeHeader.Elements.CollapsedContentEnd) }
fractionRange(start = .58f) { fade(ShadeHeader.Elements.ExpandedContent) }
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
index b11edf7b47b7..00b494b7f994 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
@@ -20,6 +20,7 @@ package com.android.systemui.shade.ui.composable
import android.view.ContextThemeWrapper
import android.view.ViewGroup
import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
@@ -48,6 +49,7 @@ import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.LowestZIndexScenePicker
import com.android.compose.animation.scene.SceneScope
import com.android.compose.animation.scene.ValueKey
import com.android.compose.animation.scene.animateSceneFloatAsState
@@ -57,9 +59,11 @@ import com.android.systemui.battery.BatteryMeterView
import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout
+import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.res.R
import com.android.systemui.scene.ui.composable.QuickSettings
import com.android.systemui.scene.ui.composable.Shade as ShadeKey
+import com.android.systemui.shade.ui.composable.ShadeHeader.Dimensions.CollapsedHeight
import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
import com.android.systemui.statusbar.phone.StatusBarIconController
import com.android.systemui.statusbar.phone.StatusBarIconController.TintedIconManager
@@ -72,7 +76,9 @@ import com.android.systemui.statusbar.policy.Clock
object ShadeHeader {
object Elements {
val ExpandedContent = ElementKey("ShadeHeaderExpandedContent")
- val CollapsedContent = ElementKey("ShadeHeaderCollapsedContent")
+ val CollapsedContentStart = ElementKey("ShadeHeaderCollapsedContentStart")
+ val CollapsedContentEnd = ElementKey("ShadeHeaderCollapsedContentEnd")
+ val PrivacyChip = ElementKey("PrivacyChip", scenePicker = LowestZIndexScenePicker)
}
object Keys {
@@ -106,15 +112,16 @@ fun SceneScope.CollapsedShadeHeader(
cutoutLocation != CutoutLocation.CENTER || formatProgress.value > 0.5f
}
}
+ val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsState()
// This layout assumes it is globally positioned at (0, 0) and is the
// same size as the screen.
Layout(
- modifier = modifier.element(ShadeHeader.Elements.CollapsedContent),
+ modifier = modifier,
contents =
listOf(
{
- Row {
+ Row(modifier = Modifier.element(ShadeHeader.Elements.CollapsedContentStart)) {
AndroidView(
factory = { context ->
Clock(
@@ -132,32 +139,45 @@ fun SceneScope.CollapsedShadeHeader(
}
},
{
- Row(horizontalArrangement = Arrangement.End) {
- SystemIconContainer {
- when (LocalWindowSizeClass.current.widthSizeClass) {
- WindowWidthSizeClass.Medium,
- WindowWidthSizeClass.Expanded ->
- ShadeCarrierGroup(
- viewModel = viewModel,
- modifier = Modifier.align(Alignment.CenterVertically),
- )
- }
- StatusIcons(
+ if (isPrivacyChipVisible) {
+ Box(modifier = Modifier.height(CollapsedHeight).fillMaxWidth()) {
+ PrivacyChip(
viewModel = viewModel,
- createTintedIconManager = createTintedIconManager,
- statusBarIconController = statusBarIconController,
- useExpandedFormat = useExpandedFormat,
- modifier =
- Modifier.align(Alignment.CenterVertically)
- .padding(end = 6.dp)
- .weight(1f, fill = false)
- )
- BatteryIcon(
- createBatteryMeterViewController = createBatteryMeterViewController,
- useExpandedFormat = useExpandedFormat,
- modifier = Modifier.align(Alignment.CenterVertically),
+ modifier = Modifier.align(Alignment.CenterEnd),
)
}
+ } else {
+ Row(
+ horizontalArrangement = Arrangement.End,
+ modifier = Modifier.element(ShadeHeader.Elements.CollapsedContentEnd)
+ ) {
+ SystemIconContainer {
+ when (LocalWindowSizeClass.current.widthSizeClass) {
+ WindowWidthSizeClass.Medium,
+ WindowWidthSizeClass.Expanded ->
+ ShadeCarrierGroup(
+ viewModel = viewModel,
+ modifier = Modifier.align(Alignment.CenterVertically),
+ )
+ }
+ StatusIcons(
+ viewModel = viewModel,
+ createTintedIconManager = createTintedIconManager,
+ statusBarIconController = statusBarIconController,
+ useExpandedFormat = useExpandedFormat,
+ modifier =
+ Modifier.align(Alignment.CenterVertically)
+ .padding(end = 6.dp)
+ .weight(1f, fill = false)
+ )
+ BatteryIcon(
+ createBatteryMeterViewController =
+ createBatteryMeterViewController,
+ useExpandedFormat = useExpandedFormat,
+ modifier = Modifier.align(Alignment.CenterVertically),
+ )
+ }
+ }
}
},
),
@@ -223,67 +243,77 @@ fun SceneScope.ExpandedShadeHeader(
.unsafeCompositionState(initialValue = 1f)
val useExpandedFormat by
remember(formatProgress) { derivedStateOf { formatProgress.value > 0.5f } }
+ val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsState()
- Column(
- verticalArrangement = Arrangement.Bottom,
- modifier =
- modifier
- .element(ShadeHeader.Elements.ExpandedContent)
- .fillMaxWidth()
- .defaultMinSize(minHeight = ShadeHeader.Dimensions.ExpandedHeight)
- ) {
- Row {
- AndroidView(
- factory = { context ->
- Clock(ContextThemeWrapper(context, R.style.TextAppearance_QS_Status), null)
- },
- modifier =
- Modifier.align(Alignment.CenterVertically)
- // use graphicsLayer instead of Modifier.scale to anchor transform to
- // the (start, top) corner
- .graphicsLayer(
- scaleX = 2.57f,
- scaleY = 2.57f,
- transformOrigin =
- TransformOrigin(
- when (LocalLayoutDirection.current) {
- LayoutDirection.Ltr -> 0f
- LayoutDirection.Rtl -> 1f
- },
- 0.5f
- )
- ),
- )
- Spacer(modifier = Modifier.weight(1f))
- ShadeCarrierGroup(
- viewModel = viewModel,
- modifier = Modifier.align(Alignment.CenterVertically),
- )
- }
- Spacer(modifier = Modifier.width(5.dp))
- Row {
- VariableDayDate(
- viewModel = viewModel,
- modifier = Modifier.widthIn(max = 90.dp).align(Alignment.CenterVertically),
- )
- Spacer(modifier = Modifier.weight(1f))
- SystemIconContainer {
- StatusIcons(
+ Box(modifier = modifier) {
+ if (isPrivacyChipVisible) {
+ Box(modifier = Modifier.height(CollapsedHeight).fillMaxWidth()) {
+ PrivacyChip(
viewModel = viewModel,
- createTintedIconManager = createTintedIconManager,
- statusBarIconController = statusBarIconController,
- useExpandedFormat = useExpandedFormat,
+ modifier = Modifier.align(Alignment.CenterEnd),
+ )
+ }
+ }
+ Column(
+ verticalArrangement = Arrangement.Bottom,
+ modifier =
+ Modifier.element(ShadeHeader.Elements.ExpandedContent)
+ .fillMaxWidth()
+ .defaultMinSize(minHeight = ShadeHeader.Dimensions.ExpandedHeight)
+ ) {
+ Row {
+ AndroidView(
+ factory = { context ->
+ Clock(ContextThemeWrapper(context, R.style.TextAppearance_QS_Status), null)
+ },
modifier =
Modifier.align(Alignment.CenterVertically)
- .padding(end = 6.dp)
- .weight(1f, fill = false),
+ // use graphicsLayer instead of Modifier.scale to anchor transform to
+ // the (start, top) corner
+ .graphicsLayer(
+ scaleX = 2.57f,
+ scaleY = 2.57f,
+ transformOrigin =
+ TransformOrigin(
+ when (LocalLayoutDirection.current) {
+ LayoutDirection.Ltr -> 0f
+ LayoutDirection.Rtl -> 1f
+ },
+ 0.5f
+ )
+ ),
)
- BatteryIcon(
- useExpandedFormat = useExpandedFormat,
- createBatteryMeterViewController = createBatteryMeterViewController,
+ Spacer(modifier = Modifier.weight(1f))
+ ShadeCarrierGroup(
+ viewModel = viewModel,
modifier = Modifier.align(Alignment.CenterVertically),
)
}
+ Spacer(modifier = Modifier.width(5.dp))
+ Row {
+ VariableDayDate(
+ viewModel = viewModel,
+ modifier = Modifier.widthIn(max = 90.dp).align(Alignment.CenterVertically),
+ )
+ Spacer(modifier = Modifier.weight(1f))
+ SystemIconContainer {
+ StatusIcons(
+ viewModel = viewModel,
+ createTintedIconManager = createTintedIconManager,
+ statusBarIconController = statusBarIconController,
+ useExpandedFormat = useExpandedFormat,
+ modifier =
+ Modifier.align(Alignment.CenterVertically)
+ .padding(end = 6.dp)
+ .weight(1f, fill = false),
+ )
+ BatteryIcon(
+ useExpandedFormat = useExpandedFormat,
+ createBatteryMeterViewController = createBatteryMeterViewController,
+ modifier = Modifier.align(Alignment.CenterVertically),
+ )
+ }
+ }
}
}
}
@@ -359,7 +389,14 @@ private fun SceneScope.StatusIcons(
) {
val carrierIconSlots =
listOf(stringResource(id = com.android.internal.R.string.status_bar_mobile))
+ val cameraSlot = stringResource(id = com.android.internal.R.string.status_bar_camera)
+ 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.collectAsState()
+ val isPrivacyChipEnabled by viewModel.isPrivacyChipEnabled.collectAsState()
+ val isMicCameraIndicationEnabled by viewModel.isMicCameraIndicationEnabled.collectAsState()
+ val isLocationIndicationEnabled by viewModel.isLocationIndicationEnabled.collectAsState()
AndroidView(
factory = { context ->
@@ -382,6 +419,25 @@ private fun SceneScope.StatusIcons(
} else {
iconContainer.addIgnoredSlots(carrierIconSlots)
}
+
+ if (isPrivacyChipEnabled) {
+ if (isMicCameraIndicationEnabled) {
+ iconContainer.addIgnoredSlot(cameraSlot)
+ iconContainer.addIgnoredSlot(micSlot)
+ } else {
+ iconContainer.removeIgnoredSlot(cameraSlot)
+ iconContainer.removeIgnoredSlot(micSlot)
+ }
+ if (isLocationIndicationEnabled) {
+ iconContainer.addIgnoredSlot(locationSlot)
+ } else {
+ iconContainer.removeIgnoredSlot(locationSlot)
+ }
+ } else {
+ iconContainer.removeIgnoredSlot(cameraSlot)
+ iconContainer.removeIgnoredSlot(micSlot)
+ iconContainer.removeIgnoredSlot(locationSlot)
+ }
},
modifier = modifier,
)
@@ -394,7 +450,28 @@ private fun SystemIconContainer(
) {
// TODO(b/298524053): add hover state for this container
Row(
- modifier = modifier.height(ShadeHeader.Dimensions.CollapsedHeight),
+ modifier = modifier.height(CollapsedHeight),
content = content,
)
}
+
+@Composable
+private fun SceneScope.PrivacyChip(
+ viewModel: ShadeHeaderViewModel,
+ modifier: Modifier = Modifier,
+) {
+ val privacyList by viewModel.privacyItems.collectAsState()
+
+ AndroidView(
+ factory = { context ->
+ val view =
+ OngoingPrivacyChip(context, null).also { privacyChip ->
+ privacyChip.privacyList = privacyList
+ privacyChip.setOnClickListener { viewModel.onPrivacyChipClicked(privacyChip) }
+ }
+ view
+ },
+ update = { it.privacyList = privacyList },
+ modifier = modifier.element(ShadeHeader.Elements.PrivacyChip),
+ )
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
index d47da3e47d2f..82862e021b4c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
@@ -26,11 +26,11 @@ import com.android.systemui.kosmos.testScope
import com.android.systemui.qs.FooterActionsController
import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter
-import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Direction
import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.UserAction
import com.android.systemui.scene.shared.model.UserActionResult
+import com.android.systemui.shade.domain.interactor.privacyChipInteractor
import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel
import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
@@ -58,7 +58,6 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
- private val sceneInteractor by lazy { kosmos.sceneInteractor }
private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
private val flags = FakeFeatureFlagsClassic().also { it.set(Flags.NEW_NETWORK_SLICE_UI, false) }
private val qsFlexiglassAdapter = FakeQSSceneAdapter({ mock() })
@@ -95,9 +94,9 @@ class QuickSettingsSceneViewModelTest : SysuiTestCase() {
ShadeHeaderViewModel(
applicationScope = testScope.backgroundScope,
context = context,
- sceneInteractor = sceneInteractor,
mobileIconsInteractor = mobileIconsInteractor,
mobileIconsViewModel = mobileIconsViewModel,
+ privacyChipInteractor = kosmos.privacyChipInteractor,
broadcastDispatcher = fakeBroadcastDispatcher,
)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index 9f89d346fd51..d74585da0cf0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -65,6 +65,7 @@ import com.android.systemui.scene.shared.model.SceneKey
import com.android.systemui.scene.shared.model.fakeSceneDataSource
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
import com.android.systemui.settings.FakeDisplayTracker
+import com.android.systemui.shade.domain.interactor.privacyChipInteractor
import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel
@@ -229,9 +230,9 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() {
ShadeHeaderViewModel(
applicationScope = testScope.backgroundScope,
context = context,
- sceneInteractor = sceneInteractor,
mobileIconsInteractor = mobileIconsInteractor,
mobileIconsViewModel = mobileIconsViewModel,
+ privacyChipInteractor = kosmos.privacyChipInteractor,
broadcastDispatcher = fakeBroadcastDispatcher,
)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/PrivacyChipRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/PrivacyChipRepositoryTest.kt
new file mode 100644
index 000000000000..613f256113f3
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/PrivacyChipRepositoryTest.kt
@@ -0,0 +1,183 @@
+/*
+ * Copyright (C) 2023 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.shade.data.repository
+
+import android.content.Intent
+import android.safetycenter.SafetyCenterManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.broadcast.broadcastDispatcher
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.privacy.PrivacyApplication
+import com.android.systemui.privacy.PrivacyConfig
+import com.android.systemui.privacy.PrivacyItem
+import com.android.systemui.privacy.PrivacyItemController
+import com.android.systemui.privacy.PrivacyType
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.kotlinArgumentCaptor
+import com.android.systemui.util.mockito.whenever
+import com.android.systemui.util.mockito.withArgCaptor
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.times
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations.initMocks
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PrivacyChipRepositoryTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val broadcastDispatcher = kosmos.broadcastDispatcher
+
+ @Mock private lateinit var privacyConfig: PrivacyConfig
+ @Mock private lateinit var privacyItemController: PrivacyItemController
+ @Mock private lateinit var safetyCenterManager: SafetyCenterManager
+
+ lateinit var underTest: PrivacyChipRepositoryImpl
+
+ @Before
+ fun setUp() {
+ initMocks(this)
+ setUpUnderTest()
+ }
+
+ @Test
+ fun isSafetyCenterEnabled_startEnabled() =
+ testScope.runTest {
+ setUpUnderTest(true)
+
+ val actual by collectLastValue(underTest.isSafetyCenterEnabled)
+ runCurrent()
+
+ assertThat(actual).isTrue()
+ }
+
+ @Test
+ fun isSafetyCenterEnabled_startDisabled() =
+ testScope.runTest {
+ setUpUnderTest(false)
+
+ val actual by collectLastValue(underTest.isSafetyCenterEnabled)
+
+ assertThat(actual).isFalse()
+ }
+
+ @Test
+ fun isSafetyCenterEnabled_updates() =
+ testScope.runTest {
+ val actual by collectLastValue(underTest.isSafetyCenterEnabled)
+ runCurrent()
+
+ assertThat(actual).isFalse()
+
+ whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(true)
+
+ broadcastDispatcher.sendIntentToMatchingReceiversOnly(
+ context,
+ Intent(SafetyCenterManager.ACTION_SAFETY_CENTER_ENABLED_CHANGED),
+ )
+
+ runCurrent()
+
+ assertThat(actual).isTrue()
+ }
+
+ @Test
+ fun privacyItems_updates() =
+ testScope.runTest {
+ val actual by collectLastValue(underTest.privacyItems)
+ runCurrent()
+
+ val callback =
+ withArgCaptor<PrivacyItemController.Callback> {
+ verify(privacyItemController).addCallback(capture())
+ }
+
+ callback.onPrivacyItemsChanged(emptyList())
+ assertThat(actual).isEmpty()
+
+ val privacyItems =
+ listOf(
+ PrivacyItem(
+ privacyType = PrivacyType.TYPE_CAMERA,
+ application = PrivacyApplication("", 0)
+ ),
+ )
+ callback.onPrivacyItemsChanged(privacyItems)
+ assertThat(actual).isEqualTo(privacyItems)
+ }
+
+ @Test
+ fun isMicCameraIndicationEnabled_updates() =
+ testScope.runTest {
+ val actual by collectLastValue(underTest.isMicCameraIndicationEnabled)
+ runCurrent()
+
+ val captor = kotlinArgumentCaptor<PrivacyConfig.Callback>()
+ verify(privacyConfig, times(2)).addCallback(captor.capture())
+ val callback = captor.allValues[0]
+
+ callback.onFlagMicCameraChanged(false)
+ assertThat(actual).isFalse()
+
+ callback.onFlagMicCameraChanged(true)
+ assertThat(actual).isTrue()
+ }
+
+ @Test
+ fun isLocationIndicationEnabled_updates() =
+ testScope.runTest {
+ val actual by collectLastValue(underTest.isLocationIndicationEnabled)
+ runCurrent()
+
+ val captor = kotlinArgumentCaptor<PrivacyConfig.Callback>()
+ verify(privacyConfig, times(2)).addCallback(captor.capture())
+ val callback = captor.allValues[1]
+
+ callback.onFlagLocationChanged(false)
+ assertThat(actual).isFalse()
+
+ callback.onFlagLocationChanged(true)
+ assertThat(actual).isTrue()
+ }
+
+ private fun setUpUnderTest(isSafetyCenterEnabled: Boolean = false) {
+ whenever(safetyCenterManager.isSafetyCenterEnabled).thenReturn(isSafetyCenterEnabled)
+
+ underTest =
+ PrivacyChipRepositoryImpl(
+ applicationScope = kosmos.applicationCoroutineScope,
+ privacyConfig = privacyConfig,
+ privacyItemController = privacyItemController,
+ backgroundDispatcher = kosmos.testDispatcher,
+ broadcastDispatcher = broadcastDispatcher,
+ safetyCenterManager = safetyCenterManager,
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/PrivacyChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/PrivacyChipInteractorTest.kt
new file mode 100644
index 000000000000..f0293a8efc8a
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/PrivacyChipInteractorTest.kt
@@ -0,0 +1,154 @@
+/*
+ * Copyright (C) 2023 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.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.shade.data.repository.privacyChipRepository
+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
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.Mockito.never
+import org.mockito.Mockito.verify
+import org.mockito.MockitoAnnotations.initMocks
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class PrivacyChipInteractorTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val privacyChipRepository = kosmos.fakePrivacyChipRepository
+ private val privacyDialogController = kosmos.privacyDialogController
+ private val privacyDialogControllerV2 = kosmos.privacyDialogControllerV2
+ @Mock private lateinit var privacyChip: OngoingPrivacyChip
+
+ val underTest = kosmos.privacyChipInteractor
+
+ @Before
+ fun setUp() {
+ initMocks(this)
+ 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()
+ }
+
+ @Test
+ fun onPrivacyChipClicked_safetyCenterEnabled() =
+ testScope.runTest {
+ privacyChipRepository.setIsSafetyCenterEnabled(true)
+
+ underTest.onPrivacyChipClicked(privacyChip)
+
+ verify(privacyDialogControllerV2).showDialog(any(), any())
+ verify(privacyDialogController, never()).showDialog(any())
+ }
+
+ @Test
+ fun onPrivacyChipClicked_safetyCenterDisabled() =
+ testScope.runTest {
+ privacyChipRepository.setIsSafetyCenterEnabled(false)
+
+ underTest.onPrivacyChipClicked(privacyChip)
+
+ verify(privacyDialogController).showDialog(any())
+ verify(privacyDialogControllerV2, never()).showDialog(any(), any())
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
index c0aaab3ad6e1..1ef307617c4e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
@@ -8,7 +8,7 @@ import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.FakeFeatureFlagsClassic
import com.android.systemui.flags.Flags
import com.android.systemui.kosmos.testScope
-import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.shade.domain.interactor.privacyChipInteractor
import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
@@ -31,7 +31,6 @@ import org.mockito.MockitoAnnotations
class ShadeHeaderViewModelTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
- private val sceneInteractor by lazy { kosmos.sceneInteractor }
private val mobileIconsInteractor = FakeMobileIconsInteractor(FakeMobileMappingsProxy(), mock())
private val flags = FakeFeatureFlagsClassic().also { it.set(Flags.NEW_NETWORK_SLICE_UI, false) }
@@ -62,9 +61,9 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
ShadeHeaderViewModel(
applicationScope = testScope.backgroundScope,
context = context,
- sceneInteractor = sceneInteractor,
mobileIconsInteractor = mobileIconsInteractor,
mobileIconsViewModel = mobileIconsViewModel,
+ privacyChipInteractor = kosmos.privacyChipInteractor,
broadcastDispatcher = fakeBroadcastDispatcher,
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
index 799e8f054d51..cc47218d57e0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -31,6 +31,7 @@ import com.android.systemui.media.controls.pipeline.MediaDataManager
import com.android.systemui.qs.ui.adapter.FakeQSSceneAdapter
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.SceneKey
+import com.android.systemui.shade.domain.interactor.privacyChipInteractor
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel
import com.android.systemui.statusbar.pipeline.airplane.data.repository.FakeAirplaneModeRepository
import com.android.systemui.statusbar.pipeline.airplane.domain.interactor.AirplaneModeInteractor
@@ -96,9 +97,9 @@ class ShadeSceneViewModelTest : SysuiTestCase() {
ShadeHeaderViewModel(
applicationScope = testScope.backgroundScope,
context = context,
- sceneInteractor = sceneInteractor,
mobileIconsInteractor = mobileIconsInteractor,
mobileIconsViewModel = mobileIconsViewModel,
+ privacyChipInteractor = kosmos.privacyChipInteractor,
broadcastDispatcher = fakeBroadcastDispatcher,
)
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt
index d393f0d0b72b..4054a86960d3 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeEmptyImplModule.kt
@@ -17,6 +17,8 @@
package com.android.systemui.shade
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.shade.data.repository.PrivacyChipRepository
+import com.android.systemui.shade.data.repository.PrivacyChipRepositoryImpl
import com.android.systemui.shade.data.repository.ShadeRepository
import com.android.systemui.shade.data.repository.ShadeRepositoryImpl
import com.android.systemui.shade.domain.interactor.ShadeAnimationInteractor
@@ -61,4 +63,8 @@ abstract class ShadeEmptyImplModule {
abstract fun bindsShadeAnimationInteractor(
sai: ShadeAnimationInteractorEmptyImpl
): ShadeAnimationInteractor
+
+ @Binds
+ @SysUISingleton
+ abstract fun bindsPrivacyChipRepository(impl: PrivacyChipRepositoryImpl): PrivacyChipRepository
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
index 86fdceea57ef..5632766f2633 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeModule.kt
@@ -18,6 +18,8 @@ package com.android.systemui.shade
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.scene.shared.flag.SceneContainerFlags
+import com.android.systemui.shade.data.repository.PrivacyChipRepository
+import com.android.systemui.shade.data.repository.PrivacyChipRepositoryImpl
import com.android.systemui.shade.data.repository.ShadeRepository
import com.android.systemui.shade.data.repository.ShadeRepositoryImpl
import com.android.systemui.shade.domain.interactor.BaseShadeInteractor
@@ -124,4 +126,8 @@ abstract class ShadeModule {
abstract fun bindsShadeViewController(
notificationPanelViewController: NotificationPanelViewController
): ShadeViewController
+
+ @Binds
+ @SysUISingleton
+ abstract fun bindsPrivacyChipRepository(impl: PrivacyChipRepositoryImpl): PrivacyChipRepository
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/PrivacyChipRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/PrivacyChipRepository.kt
new file mode 100644
index 000000000000..91c92cc8cd2f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/PrivacyChipRepository.kt
@@ -0,0 +1,133 @@
+/*
+ * 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.shade.data.repository
+
+import android.content.IntentFilter
+import android.os.UserHandle
+import android.safetycenter.SafetyCenterManager
+import com.android.systemui.broadcast.BroadcastDispatcher
+import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.privacy.PrivacyConfig
+import com.android.systemui.privacy.PrivacyItem
+import com.android.systemui.privacy.PrivacyItemController
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.flow.stateIn
+
+interface PrivacyChipRepository {
+ /** Whether or not the Safety Center is enabled. */
+ val isSafetyCenterEnabled: StateFlow<Boolean>
+
+ /** The list of PrivacyItems to be displayed by the privacy chip. */
+ val privacyItems: StateFlow<List<PrivacyItem>>
+
+ /** Whether or not mic & camera indicators are enabled in the device privacy config. */
+ val isMicCameraIndicationEnabled: StateFlow<Boolean>
+
+ /** Whether or not location indicators are enabled in the device privacy config. */
+ val isLocationIndicationEnabled: StateFlow<Boolean>
+}
+
+@SysUISingleton
+class PrivacyChipRepositoryImpl
+@Inject
+constructor(
+ @Application applicationScope: CoroutineScope,
+ private val privacyConfig: PrivacyConfig,
+ private val privacyItemController: PrivacyItemController,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+ broadcastDispatcher: BroadcastDispatcher,
+ private val safetyCenterManager: SafetyCenterManager,
+) : PrivacyChipRepository {
+ override val isSafetyCenterEnabled: StateFlow<Boolean> =
+ broadcastDispatcher
+ .broadcastFlow(
+ filter =
+ IntentFilter().apply {
+ addAction(SafetyCenterManager.ACTION_SAFETY_CENTER_ENABLED_CHANGED)
+ },
+ user = UserHandle.SYSTEM,
+ map = { _, _ -> safetyCenterManager.isSafetyCenterEnabled }
+ )
+ .onStart { emit(safetyCenterManager.isSafetyCenterEnabled) }
+ .flowOn(backgroundDispatcher)
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = false,
+ )
+
+ override val privacyItems: StateFlow<List<PrivacyItem>> =
+ conflatedCallbackFlow {
+ val callback =
+ object : PrivacyItemController.Callback {
+ override fun onPrivacyItemsChanged(privacyItems: List<PrivacyItem>) {
+ trySend(privacyItems)
+ }
+ }
+ privacyItemController.addCallback(callback)
+ awaitClose { privacyItemController.removeCallback(callback) }
+ }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.Eagerly,
+ initialValue = emptyList(),
+ )
+
+ override val isMicCameraIndicationEnabled: StateFlow<Boolean> =
+ conflatedCallbackFlow {
+ val callback =
+ object : PrivacyConfig.Callback {
+ override fun onFlagMicCameraChanged(flag: Boolean) {
+ trySend(flag)
+ }
+ }
+ privacyConfig.addCallback(callback)
+ awaitClose { privacyConfig.removeCallback(callback) }
+ }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.Eagerly,
+ initialValue = privacyItemController.micCameraAvailable,
+ )
+
+ override val isLocationIndicationEnabled: StateFlow<Boolean> =
+ conflatedCallbackFlow {
+ val callback =
+ object : PrivacyConfig.Callback {
+ override fun onFlagLocationChanged(flag: Boolean) {
+ trySend(flag)
+ }
+ }
+ privacyConfig.addCallback(callback)
+ awaitClose { privacyConfig.removeCallback(callback) }
+ }
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.Eagerly,
+ initialValue = privacyItemController.locationAvailable,
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PrivacyChipInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PrivacyChipInteractor.kt
new file mode 100644
index 000000000000..4c6c31809275
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PrivacyChipInteractor.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.shade.domain.interactor
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.privacy.OngoingPrivacyChip
+import com.android.systemui.privacy.PrivacyDialogController
+import com.android.systemui.privacy.PrivacyDialogControllerV2
+import com.android.systemui.privacy.PrivacyItem
+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
+
+@SysUISingleton
+class PrivacyChipInteractor
+@Inject
+constructor(
+ @Application applicationScope: CoroutineScope,
+ private val repository: PrivacyChipRepository,
+ private val privacyDialogController: PrivacyDialogController,
+ private val privacyDialogControllerV2: PrivacyDialogControllerV2,
+ private val deviceProvisionedController: DeviceProvisionedController,
+) {
+ /** The list of PrivacyItems to be displayed by the privacy chip. */
+ val privacyItems: StateFlow<List<PrivacyItem>> = repository.privacyItems
+
+ /** Whether or not mic & camera indicators are enabled in the device privacy config. */
+ val isMicCameraIndicationEnabled: StateFlow<Boolean> = repository.isMicCameraIndicationEnabled
+
+ /** 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
+
+ if (repository.isSafetyCenterEnabled.value) {
+ privacyDialogControllerV2.showDialog(privacyChip.context, privacyChip)
+ } else {
+ privacyDialogController.showDialog(privacyChip.context)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
index 314637e4b27e..700825dd639c 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
@@ -25,8 +25,10 @@ import android.os.UserHandle
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.privacy.OngoingPrivacyChip
+import com.android.systemui.privacy.PrivacyItem
import com.android.systemui.res.R
-import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.shade.domain.interactor.PrivacyChipInteractor
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
import java.util.Date
@@ -50,9 +52,9 @@ class ShadeHeaderViewModel
constructor(
@Application private val applicationScope: CoroutineScope,
context: Context,
- sceneInteractor: SceneInteractor,
mobileIconsInteractor: MobileIconsInteractor,
val mobileIconsViewModel: MobileIconsViewModel,
+ private val privacyChipInteractor: PrivacyChipInteractor,
broadcastDispatcher: BroadcastDispatcher,
) {
/** True if there is exactly one mobile connection. */
@@ -64,6 +66,23 @@ constructor(
.map { list -> list.map { it.subscriptionId } }
.stateIn(applicationScope, SharingStarted.WhileSubscribed(), emptyList())
+ /** The list of PrivacyItems to be displayed by the privacy chip. */
+ val privacyItems: StateFlow<List<PrivacyItem>> = privacyChipInteractor.privacyItems
+
+ /** Whether or not mic & camera indicators are enabled in the device privacy config. */
+ val isMicCameraIndicationEnabled: StateFlow<Boolean> =
+ privacyChipInteractor.isMicCameraIndicationEnabled
+
+ /** Whether or not location indicators are enabled in the device privacy config. */
+ val isLocationIndicationEnabled: StateFlow<Boolean> =
+ privacyChipInteractor.isLocationIndicationEnabled
+
+ /** Whether or not the privacy chip should be visible. */
+ val isPrivacyChipVisible: StateFlow<Boolean> = privacyChipInteractor.isChipVisible
+
+ /** Whether or not the privacy chip is enabled in the device privacy config. */
+ val isPrivacyChipEnabled: StateFlow<Boolean> = privacyChipInteractor.isChipEnabled
+
private val longerPattern = context.getString(R.string.abbrev_wday_month_day_no_year_alarm)
private val shorterPattern = context.getString(R.string.abbrev_month_day_no_year)
private val longerDateFormat = MutableStateFlow(getFormatFromPattern(longerPattern))
@@ -97,6 +116,11 @@ constructor(
applicationScope.launch { updateDateTexts(false) }
}
+ /** Notifies that the privacy chip was clicked. */
+ fun onPrivacyChipClicked(privacyChip: OngoingPrivacyChip) {
+ privacyChipInteractor.onPrivacyChipClicked(privacyChip)
+ }
+
private fun updateDateTexts(invalidateFormats: Boolean) {
if (invalidateFormats) {
longerDateFormat.value = getFormatFromPattern(longerPattern)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/privacy/PrivacyDialogControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/privacy/PrivacyDialogControllerKosmos.kt
new file mode 100644
index 000000000000..960a06940a94
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/privacy/PrivacyDialogControllerKosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.privacy
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.privacyDialogController: PrivacyDialogController by
+ Kosmos.Fixture { mock<PrivacyDialogController>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/privacy/PrivacyDialogControllerV2Kosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/privacy/PrivacyDialogControllerV2Kosmos.kt
new file mode 100644
index 000000000000..7628c0e64e6a
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/privacy/PrivacyDialogControllerV2Kosmos.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.privacy
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.mock
+
+var Kosmos.privacyDialogControllerV2: PrivacyDialogControllerV2 by
+ Kosmos.Fixture { mock<PrivacyDialogControllerV2>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakePrivacyChipRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakePrivacyChipRepository.kt
new file mode 100644
index 000000000000..5bc61e23cd6e
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/FakePrivacyChipRepository.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (C) 2022 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.shade.data.repository
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.privacy.PrivacyItem
+import javax.inject.Inject
+import kotlinx.coroutines.flow.MutableStateFlow
+
+/** Fake implementation of [PrivacyChipRepository] */
+@SysUISingleton
+class FakePrivacyChipRepository @Inject constructor() : PrivacyChipRepository {
+ private val _isSafetyCenterEnabled = MutableStateFlow(false)
+ override val isSafetyCenterEnabled = _isSafetyCenterEnabled
+
+ private val _privacyItems: MutableStateFlow<List<PrivacyItem>> = MutableStateFlow(emptyList())
+ override val privacyItems = _privacyItems
+
+ private val _isMicCameraIndicationEnabled = MutableStateFlow(false)
+ override val isMicCameraIndicationEnabled = _isMicCameraIndicationEnabled
+
+ private val _isLocationIndicationEnabled = MutableStateFlow(false)
+ override val isLocationIndicationEnabled = _isLocationIndicationEnabled
+
+ fun setIsSafetyCenterEnabled(value: Boolean) {
+ _isSafetyCenterEnabled.value = value
+ }
+
+ fun setPrivacyItems(value: List<PrivacyItem>) {
+ _privacyItems.value = value
+ }
+
+ fun setIsMicCameraIndicationEnabled(value: Boolean) {
+ _isMicCameraIndicationEnabled.value = value
+ }
+
+ fun setIsLocationIndicationEnabled(value: Boolean) {
+ _isLocationIndicationEnabled.value = value
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/PrivacyChipRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/PrivacyChipRepositoryKosmos.kt
new file mode 100644
index 000000000000..2428c6156720
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/PrivacyChipRepositoryKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * Copyright (C) 2023 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.shade.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+var Kosmos.privacyChipRepository: PrivacyChipRepository by
+ Kosmos.Fixture { fakePrivacyChipRepository }
+val Kosmos.fakePrivacyChipRepository: FakePrivacyChipRepository by
+ Kosmos.Fixture { FakePrivacyChipRepository() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/PrivacyChipInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/PrivacyChipInteractorKosmos.kt
new file mode 100644
index 000000000000..7334286f00c4
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/PrivacyChipInteractorKosmos.kt
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2023 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.shade.domain.interactor
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.privacy.privacyDialogController
+import com.android.systemui.privacy.privacyDialogControllerV2
+import com.android.systemui.shade.data.repository.fakePrivacyChipRepository
+import com.android.systemui.statusbar.policy.deviceProvisionedController
+
+var Kosmos.privacyChipInteractor: PrivacyChipInteractor by
+ Kosmos.Fixture {
+ PrivacyChipInteractor(
+ applicationScope = applicationCoroutineScope,
+ repository = fakePrivacyChipRepository,
+ privacyDialogController = privacyDialogController,
+ privacyDialogControllerV2 = privacyDialogControllerV2,
+ deviceProvisionedController = deviceProvisionedController,
+ )
+ }