diff options
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, + ) + } |