Loading packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt +20 −28 Original line number Diff line number Diff line Loading @@ -72,7 +72,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.LowestZIndexContentPicker Loading @@ -92,6 +91,7 @@ import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.kairos.ExperimentalKairosApi import com.android.systemui.kairos.util.nameTag import com.android.systemui.privacy.OngoingPrivacyChip import com.android.systemui.privacy.PrivacyItem import com.android.systemui.res.R import com.android.systemui.scene.shared.model.DualShadeEducationElement import com.android.systemui.scene.shared.model.Scenes Loading Loading @@ -192,8 +192,6 @@ fun ContentScope.CollapsedShadeHeader( } } val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle() // This layout assumes it is globally positioned at (0, 0) and is the same size as the screen. CutoutAwareShadeHeader( modifier = modifier, Loading @@ -213,10 +211,11 @@ fun ContentScope.CollapsedShadeHeader( } }, endContent = { if (isPrivacyChipVisible) { if (viewModel.isPrivacyChipVisible) { Box(modifier = Modifier.fillMaxSize().padding(horizontal = horizontalPadding)) { PrivacyChip( viewModel = viewModel, privacyList = viewModel.privacyItems, onClick = viewModel::onPrivacyChipClicked, modifier = Modifier.align(Alignment.CenterEnd), ) } Loading Loading @@ -267,12 +266,14 @@ fun ContentScope.ExpandedShadeHeader( derivedStateOf { shouldUseExpandedFormat(layoutState.transitionState) } } val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle() Box(modifier = modifier.sysuiResTag(ShadeHeader.TestTags.Root)) { if (isPrivacyChipVisible) { if (viewModel.isPrivacyChipVisible) { Box(modifier = Modifier.height(ShadeHeader.Dimensions.StatusBarHeight).fillMaxWidth()) { PrivacyChip(viewModel = viewModel, modifier = Modifier.align(Alignment.CenterEnd)) PrivacyChip( privacyList = viewModel.privacyItems, onClick = viewModel::onPrivacyChipClicked, modifier = Modifier.align(Alignment.CenterEnd), ) } } Column( Loading Loading @@ -345,8 +346,6 @@ fun ContentScope.OverlayShadeHeader( val horizontalPadding = max(LocalScreenCornerRadius.current / 2f, Shade.Dimensions.HorizontalPadding) val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle() // This layout assumes it is globally positioned at (0, 0) and is the same size as the screen. CutoutAwareShadeHeader( modifier = modifier, Loading Loading @@ -422,10 +421,11 @@ fun ContentScope.OverlayShadeHeader( isHighlighted = isHighlighted, ) } if (isPrivacyChipVisible) { if (viewModel.isPrivacyChipVisible) { Box(modifier = Modifier.fillMaxSize().padding(horizontal = horizontalPadding)) { PrivacyChip( viewModel = viewModel, privacyList = viewModel.privacyItems, onClick = viewModel::onPrivacyChipClicked, modifier = Modifier.align(Alignment.CenterEnd), ) } Loading Loading @@ -723,13 +723,6 @@ private fun ContentScope.StatusIcons( val micSlot = stringResource(id = com.android.internal.R.string.status_bar_microphone) val locationSlot = stringResource(id = com.android.internal.R.string.status_bar_location) val isSingleCarrier by viewModel.isSingleCarrier.collectAsStateWithLifecycle() val isPrivacyChipEnabled by viewModel.isPrivacyChipEnabled.collectAsStateWithLifecycle() val isMicCameraIndicationEnabled by viewModel.isMicCameraIndicationEnabled.collectAsStateWithLifecycle() val isLocationIndicationEnabled by viewModel.isLocationIndicationEnabled.collectAsStateWithLifecycle() val iconContainer = remember { StatusIconContainer(themedContext, null) } val iconManager = remember { viewModel.createTintedIconManager(iconContainer, StatusBarLocation.QS) Loading @@ -747,21 +740,21 @@ private fun ContentScope.StatusIcons( iconContainer.setQsExpansionTransitioning( layoutState.isTransitioningBetween(Scenes.Shade, Scenes.QuickSettings) ) if (isSingleCarrier || !useExpandedFormat) { if (viewModel.isSingleCarrier || !useExpandedFormat) { iconContainer.removeIgnoredSlots(carrierIconSlots) } else { iconContainer.addIgnoredSlots(carrierIconSlots) } if (isPrivacyChipEnabled) { if (isMicCameraIndicationEnabled) { if (viewModel.isPrivacyChipEnabled) { if (viewModel.isMicCameraIndicationEnabled) { iconContainer.addIgnoredSlot(cameraSlot) iconContainer.addIgnoredSlot(micSlot) } else { iconContainer.removeIgnoredSlot(cameraSlot) iconContainer.removeIgnoredSlot(micSlot) } if (isLocationIndicationEnabled) { if (viewModel.isLocationIndicationEnabled) { iconContainer.addIgnoredSlot(locationSlot) } else { iconContainer.removeIgnoredSlot(locationSlot) Loading Loading @@ -829,17 +822,16 @@ private fun SystemIconChip( @Composable private fun ContentScope.PrivacyChip( viewModel: ShadeHeaderViewModel, privacyList: List<PrivacyItem>, onClick: (OngoingPrivacyChip) -> Unit, modifier: Modifier = Modifier, ) { val privacyList by viewModel.privacyItems.collectAsStateWithLifecycle() AndroidView( factory = { context -> val view = OngoingPrivacyChip(context, null).also { privacyChip -> privacyChip.privacyList = privacyList privacyChip.setOnClickListener { viewModel.onPrivacyChipClicked(privacyChip) } privacyChip.setOnClickListener { onClick(privacyChip) } } view }, Loading packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/PrivacyChipInteractorTest.kt +0 −72 Original line number Diff line number Diff line Loading @@ -19,19 +19,14 @@ package com.android.systemui.shade.domain.interactor import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.privacy.OngoingPrivacyChip import com.android.systemui.privacy.PrivacyApplication import com.android.systemui.privacy.PrivacyItem import com.android.systemui.privacy.PrivacyType import com.android.systemui.privacy.privacyDialogController import com.android.systemui.privacy.privacyDialogControllerV2 import com.android.systemui.shade.data.repository.fakePrivacyChipRepository import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest Loading Loading @@ -61,73 +56,6 @@ class PrivacyChipInteractorTest : SysuiTestCase() { whenever(privacyChip.context).thenReturn(this.context) } @Test fun isChipVisible_updates() = testScope.runTest { val actual by collectLastValue(underTest.isChipVisible) privacyChipRepository.setPrivacyItems(emptyList()) runCurrent() assertThat(actual).isFalse() val privacyItems = listOf( PrivacyItem( privacyType = PrivacyType.TYPE_CAMERA, application = PrivacyApplication("", 0) ), ) privacyChipRepository.setPrivacyItems(privacyItems) runCurrent() assertThat(actual).isTrue() } @Test fun isChipEnabled_noIndicationEnabled() = testScope.runTest { val actual by collectLastValue(underTest.isChipEnabled) privacyChipRepository.setIsMicCameraIndicationEnabled(false) privacyChipRepository.setIsLocationIndicationEnabled(false) assertThat(actual).isFalse() } @Test fun isChipEnabled_micCameraIndicationEnabled() = testScope.runTest { val actual by collectLastValue(underTest.isChipEnabled) privacyChipRepository.setIsMicCameraIndicationEnabled(true) privacyChipRepository.setIsLocationIndicationEnabled(false) assertThat(actual).isTrue() } @Test fun isChipEnabled_locationIndicationEnabled() = testScope.runTest { val actual by collectLastValue(underTest.isChipEnabled) privacyChipRepository.setIsMicCameraIndicationEnabled(false) privacyChipRepository.setIsLocationIndicationEnabled(true) assertThat(actual).isTrue() } @Test fun isChipEnabled_allIndicationEnabled() = testScope.runTest { val actual by collectLastValue(underTest.isChipEnabled) privacyChipRepository.setIsMicCameraIndicationEnabled(true) privacyChipRepository.setIsLocationIndicationEnabled(true) assertThat(actual).isTrue() } @OptIn(ExperimentalCoroutinesApi::class) @Test fun onPrivacyChipClicked_safetyCenterEnabled() = Loading packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt +61 −21 Original line number Diff line number Diff line Loading @@ -15,13 +15,18 @@ import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.lifecycle.activateIn import com.android.systemui.plugins.activityStarter import com.android.systemui.privacy.PrivacyApplication import com.android.systemui.privacy.PrivacyItem import com.android.systemui.privacy.PrivacyType import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.data.repository.fakePrivacyChipRepository import com.android.systemui.shade.domain.interactor.disableDualShade import com.android.systemui.shade.domain.interactor.enableDualShade import com.android.systemui.shade.domain.interactor.shadeMode Loading @@ -31,10 +36,8 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.fakeMobi import com.android.systemui.testKosmos import com.android.systemui.util.mockito.argThat import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test Loading @@ -44,7 +47,6 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) @EnableSceneContainer Loading @@ -66,12 +68,10 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { fun mobileSubIds_update() = testScope.runTest { mobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1) runCurrent() assertThat(underTest.mobileSubIds).isEqualTo(listOf(1)) mobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) runCurrent() assertThat(underTest.mobileSubIds).isEqualTo(listOf(1, 2)) } Loading Loading @@ -110,7 +110,6 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { setScene(Scenes.Shade) underTest.onSystemIconChipClicked() runCurrent() assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Lockscreen) } Loading @@ -123,7 +122,6 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) underTest.onSystemIconChipClicked() runCurrent() assertThat(currentScene).isEqualTo(Scenes.Lockscreen) assertThat(currentOverlays).isEmpty() Loading @@ -137,7 +135,6 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) underTest.onSystemIconChipClicked() runCurrent() assertThat(currentScene).isEqualTo(Scenes.Lockscreen) assertThat(currentOverlays).contains(Overlays.QuickSettingsShade) Loading @@ -152,7 +149,6 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { setScene(Scenes.Shade) underTest.onSystemIconChipClicked() runCurrent() assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Gone) } Loading @@ -165,7 +161,6 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) underTest.onSystemIconChipClicked() runCurrent() assertThat(currentScene).isEqualTo(Scenes.Gone) assertThat(currentOverlays).isEmpty() Loading @@ -179,7 +174,6 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) underTest.onSystemIconChipClicked() runCurrent() assertThat(currentScene).isEqualTo(Scenes.Gone) assertThat(currentOverlays).contains(Overlays.QuickSettingsShade) Loading @@ -194,7 +188,6 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) underTest.onNotificationIconChipClicked() runCurrent() assertThat(currentScene).isEqualTo(Scenes.Lockscreen) assertThat(currentOverlays).isEmpty() Loading @@ -208,7 +201,6 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) underTest.onNotificationIconChipClicked() runCurrent() assertThat(currentScene).isEqualTo(Scenes.Lockscreen) assertThat(currentOverlays).contains(Overlays.NotificationsShade) Loading @@ -223,7 +215,6 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) underTest.onNotificationIconChipClicked() runCurrent() assertThat(currentScene).isEqualTo(Scenes.Gone) assertThat(currentOverlays).isEmpty() Loading @@ -237,13 +228,67 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) underTest.onNotificationIconChipClicked() runCurrent() assertThat(currentScene).isEqualTo(Scenes.Gone) assertThat(currentOverlays).contains(Overlays.NotificationsShade) assertThat(currentOverlays).doesNotContain(Overlays.QuickSettingsShade) } @Test fun isPrivacyChipVisible_updates() = kosmos.runTest { fakePrivacyChipRepository.setPrivacyItems(emptyList()) assertThat(underTest.isPrivacyChipVisible).isFalse() fakePrivacyChipRepository.setPrivacyItems( listOf( PrivacyItem( privacyType = PrivacyType.TYPE_CAMERA, application = PrivacyApplication("", 0), ) ) ) assertThat(underTest.isPrivacyChipVisible).isTrue() } @Test fun isPrivacyChipEnabled_noIndicationEnabled() = kosmos.runTest { fakePrivacyChipRepository.setIsMicCameraIndicationEnabled(false) fakePrivacyChipRepository.setIsLocationIndicationEnabled(false) assertThat(underTest.isPrivacyChipEnabled).isFalse() } @Test fun isPrivacyChipEnabled_micCameraIndicationEnabled() = kosmos.runTest { fakePrivacyChipRepository.setIsMicCameraIndicationEnabled(true) fakePrivacyChipRepository.setIsLocationIndicationEnabled(false) assertThat(underTest.isPrivacyChipEnabled).isTrue() } @Test fun isPrivacyChipEnabled_locationIndicationEnabled() = kosmos.runTest { fakePrivacyChipRepository.setIsMicCameraIndicationEnabled(false) fakePrivacyChipRepository.setIsLocationIndicationEnabled(true) assertThat(underTest.isPrivacyChipEnabled).isTrue() } @Test fun isPrivacyChipEnabled_allIndicationEnabled() = kosmos.runTest { fakePrivacyChipRepository.setIsMicCameraIndicationEnabled(true) fakePrivacyChipRepository.setIsLocationIndicationEnabled(true) assertThat(underTest.isPrivacyChipEnabled).isTrue() } companion object { private val SUB_1 = SubscriptionModel( Loading Loading @@ -272,19 +317,16 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { SuccessFingerprintAuthenticationStatus(0, true) ) } runCurrent() assertThat(shadeMode).isEqualTo(ShadeMode.Dual) sceneInteractor.changeScene(scene, "test") checkNotNull(currentOverlays).forEach { sceneInteractor.instantlyHideOverlay(it, "test") } runCurrent() overlay?.let { sceneInteractor.showOverlay(it, "test") } sceneInteractor.setTransitionState( MutableStateFlow<ObservableTransitionState>( ObservableTransitionState.Idle(scene, setOfNotNull(overlay)) ) ) runCurrent() assertThat(currentScene).isEqualTo(scene) if (overlay == null) { Loading @@ -299,16 +341,14 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { sceneInteractor.setTransitionState( MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key)) ) testScope.runCurrent() } private fun TestScope.setDeviceEntered(isEntered: Boolean) { private fun setDeviceEntered(isEntered: Boolean) { if (isEntered) { // Unlock the device marking the device has entered. kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( SuccessFingerprintAuthenticationStatus(0, true) ) runCurrent() } setScene( if (isEntered) { Loading packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PrivacyChipInteractor.kt +1 −29 Original line number Diff line number Diff line Loading @@ -26,11 +26,7 @@ import com.android.systemui.shade.data.repository.PrivacyChipRepository import com.android.systemui.statusbar.policy.DeviceProvisionedController import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @SysUISingleton Loading @@ -53,30 +49,6 @@ constructor( /** Whether or not location indicators are enabled in the device privacy config. */ val isLocationIndicationEnabled: StateFlow<Boolean> = repository.isLocationIndicationEnabled /** Whether or not the privacy chip should be visible. */ val isChipVisible: StateFlow<Boolean> = privacyItems .map { it.isNotEmpty() } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = false, ) /** Whether or not the privacy chip is enabled in the device privacy config. */ val isChipEnabled: StateFlow<Boolean> = combine( isMicCameraIndicationEnabled, isLocationIndicationEnabled, ) { micCamera, location -> micCamera || location } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = false, ) /** Notifies that the privacy chip was clicked. */ fun onPrivacyChipClicked(privacyChip: OngoingPrivacyChip) { if (!deviceProvisionedController.isDeviceProvisioned) return Loading @@ -85,7 +57,7 @@ constructor( if (repository.isSafetyCenterEnabled()) { privacyDialogControllerV2.showDialog( shadeDialogContextInteractor.context, privacyChip privacyChip, ) } else { privacyDialogController.showDialog(shadeDialogContextInteractor.context) Loading packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt +26 −9 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import android.icu.text.DateFormat import android.icu.text.DisplayContext import android.provider.Settings import android.view.ViewGroup import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.ui.unit.IntRect import com.android.app.tracing.coroutines.launchTraced as launch Loading Loading @@ -63,7 +64,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest Loading Loading @@ -109,7 +109,12 @@ constructor( batteryMeterViewControllerFactory::create /** True if there is exactly one mobile connection. */ val isSingleCarrier: StateFlow<Boolean> = mobileIconsInteractor.isSingleCarrier val isSingleCarrier: Boolean by hydrator.hydratedStateOf( traceName = "isSingleCarrier", initialValue = mobileIconsInteractor.isSingleCarrier.value, source = mobileIconsInteractor.isSingleCarrier, ) /** The list of subscription Ids for current mobile connections. */ val mobileSubIds: List<Int> by Loading @@ -123,21 +128,33 @@ constructor( ) /** The list of PrivacyItems to be displayed by the privacy chip. */ val privacyItems: StateFlow<List<PrivacyItem>> = privacyChipInteractor.privacyItems val privacyItems: List<PrivacyItem> by hydrator.hydratedStateOf( traceName = "privacyItems", source = privacyChipInteractor.privacyItems, ) /** Whether or not mic & camera indicators are enabled in the device privacy config. */ val isMicCameraIndicationEnabled: StateFlow<Boolean> = privacyChipInteractor.isMicCameraIndicationEnabled val isMicCameraIndicationEnabled: Boolean by hydrator.hydratedStateOf( traceName = "isMicCameraIndicationEnabled", source = privacyChipInteractor.isMicCameraIndicationEnabled, ) /** Whether or not location indicators are enabled in the device privacy config. */ val isLocationIndicationEnabled: StateFlow<Boolean> = privacyChipInteractor.isLocationIndicationEnabled val isLocationIndicationEnabled: Boolean by hydrator.hydratedStateOf( traceName = "isLocationIndicationEnabled", source = privacyChipInteractor.isLocationIndicationEnabled, ) /** Whether or not the privacy chip should be visible. */ val isPrivacyChipVisible: StateFlow<Boolean> = privacyChipInteractor.isChipVisible val isPrivacyChipVisible: Boolean by derivedStateOf { privacyItems.isNotEmpty() } /** Whether or not the privacy chip is enabled in the device privacy config. */ val isPrivacyChipEnabled: StateFlow<Boolean> = privacyChipInteractor.isChipEnabled val isPrivacyChipEnabled: Boolean by derivedStateOf { isMicCameraIndicationEnabled || isLocationIndicationEnabled } val animateNotificationsChipBounce: Boolean get() = Loading Loading
packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt +20 −28 Original line number Diff line number Diff line Loading @@ -72,7 +72,6 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.max import androidx.compose.ui.unit.sp import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.compose.animation.scene.ContentScope import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.LowestZIndexContentPicker Loading @@ -92,6 +91,7 @@ import com.android.systemui.compose.modifiers.sysuiResTag import com.android.systemui.kairos.ExperimentalKairosApi import com.android.systemui.kairos.util.nameTag import com.android.systemui.privacy.OngoingPrivacyChip import com.android.systemui.privacy.PrivacyItem import com.android.systemui.res.R import com.android.systemui.scene.shared.model.DualShadeEducationElement import com.android.systemui.scene.shared.model.Scenes Loading Loading @@ -192,8 +192,6 @@ fun ContentScope.CollapsedShadeHeader( } } val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle() // This layout assumes it is globally positioned at (0, 0) and is the same size as the screen. CutoutAwareShadeHeader( modifier = modifier, Loading @@ -213,10 +211,11 @@ fun ContentScope.CollapsedShadeHeader( } }, endContent = { if (isPrivacyChipVisible) { if (viewModel.isPrivacyChipVisible) { Box(modifier = Modifier.fillMaxSize().padding(horizontal = horizontalPadding)) { PrivacyChip( viewModel = viewModel, privacyList = viewModel.privacyItems, onClick = viewModel::onPrivacyChipClicked, modifier = Modifier.align(Alignment.CenterEnd), ) } Loading Loading @@ -267,12 +266,14 @@ fun ContentScope.ExpandedShadeHeader( derivedStateOf { shouldUseExpandedFormat(layoutState.transitionState) } } val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle() Box(modifier = modifier.sysuiResTag(ShadeHeader.TestTags.Root)) { if (isPrivacyChipVisible) { if (viewModel.isPrivacyChipVisible) { Box(modifier = Modifier.height(ShadeHeader.Dimensions.StatusBarHeight).fillMaxWidth()) { PrivacyChip(viewModel = viewModel, modifier = Modifier.align(Alignment.CenterEnd)) PrivacyChip( privacyList = viewModel.privacyItems, onClick = viewModel::onPrivacyChipClicked, modifier = Modifier.align(Alignment.CenterEnd), ) } } Column( Loading Loading @@ -345,8 +346,6 @@ fun ContentScope.OverlayShadeHeader( val horizontalPadding = max(LocalScreenCornerRadius.current / 2f, Shade.Dimensions.HorizontalPadding) val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle() // This layout assumes it is globally positioned at (0, 0) and is the same size as the screen. CutoutAwareShadeHeader( modifier = modifier, Loading Loading @@ -422,10 +421,11 @@ fun ContentScope.OverlayShadeHeader( isHighlighted = isHighlighted, ) } if (isPrivacyChipVisible) { if (viewModel.isPrivacyChipVisible) { Box(modifier = Modifier.fillMaxSize().padding(horizontal = horizontalPadding)) { PrivacyChip( viewModel = viewModel, privacyList = viewModel.privacyItems, onClick = viewModel::onPrivacyChipClicked, modifier = Modifier.align(Alignment.CenterEnd), ) } Loading Loading @@ -723,13 +723,6 @@ private fun ContentScope.StatusIcons( val micSlot = stringResource(id = com.android.internal.R.string.status_bar_microphone) val locationSlot = stringResource(id = com.android.internal.R.string.status_bar_location) val isSingleCarrier by viewModel.isSingleCarrier.collectAsStateWithLifecycle() val isPrivacyChipEnabled by viewModel.isPrivacyChipEnabled.collectAsStateWithLifecycle() val isMicCameraIndicationEnabled by viewModel.isMicCameraIndicationEnabled.collectAsStateWithLifecycle() val isLocationIndicationEnabled by viewModel.isLocationIndicationEnabled.collectAsStateWithLifecycle() val iconContainer = remember { StatusIconContainer(themedContext, null) } val iconManager = remember { viewModel.createTintedIconManager(iconContainer, StatusBarLocation.QS) Loading @@ -747,21 +740,21 @@ private fun ContentScope.StatusIcons( iconContainer.setQsExpansionTransitioning( layoutState.isTransitioningBetween(Scenes.Shade, Scenes.QuickSettings) ) if (isSingleCarrier || !useExpandedFormat) { if (viewModel.isSingleCarrier || !useExpandedFormat) { iconContainer.removeIgnoredSlots(carrierIconSlots) } else { iconContainer.addIgnoredSlots(carrierIconSlots) } if (isPrivacyChipEnabled) { if (isMicCameraIndicationEnabled) { if (viewModel.isPrivacyChipEnabled) { if (viewModel.isMicCameraIndicationEnabled) { iconContainer.addIgnoredSlot(cameraSlot) iconContainer.addIgnoredSlot(micSlot) } else { iconContainer.removeIgnoredSlot(cameraSlot) iconContainer.removeIgnoredSlot(micSlot) } if (isLocationIndicationEnabled) { if (viewModel.isLocationIndicationEnabled) { iconContainer.addIgnoredSlot(locationSlot) } else { iconContainer.removeIgnoredSlot(locationSlot) Loading Loading @@ -829,17 +822,16 @@ private fun SystemIconChip( @Composable private fun ContentScope.PrivacyChip( viewModel: ShadeHeaderViewModel, privacyList: List<PrivacyItem>, onClick: (OngoingPrivacyChip) -> Unit, modifier: Modifier = Modifier, ) { val privacyList by viewModel.privacyItems.collectAsStateWithLifecycle() AndroidView( factory = { context -> val view = OngoingPrivacyChip(context, null).also { privacyChip -> privacyChip.privacyList = privacyList privacyChip.setOnClickListener { viewModel.onPrivacyChipClicked(privacyChip) } privacyChip.setOnClickListener { onClick(privacyChip) } } view }, Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/PrivacyChipInteractorTest.kt +0 −72 Original line number Diff line number Diff line Loading @@ -19,19 +19,14 @@ package com.android.systemui.shade.domain.interactor import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.privacy.OngoingPrivacyChip import com.android.systemui.privacy.PrivacyApplication import com.android.systemui.privacy.PrivacyItem import com.android.systemui.privacy.PrivacyType import com.android.systemui.privacy.privacyDialogController import com.android.systemui.privacy.privacyDialogControllerV2 import com.android.systemui.shade.data.repository.fakePrivacyChipRepository import com.android.systemui.testKosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest Loading Loading @@ -61,73 +56,6 @@ class PrivacyChipInteractorTest : SysuiTestCase() { whenever(privacyChip.context).thenReturn(this.context) } @Test fun isChipVisible_updates() = testScope.runTest { val actual by collectLastValue(underTest.isChipVisible) privacyChipRepository.setPrivacyItems(emptyList()) runCurrent() assertThat(actual).isFalse() val privacyItems = listOf( PrivacyItem( privacyType = PrivacyType.TYPE_CAMERA, application = PrivacyApplication("", 0) ), ) privacyChipRepository.setPrivacyItems(privacyItems) runCurrent() assertThat(actual).isTrue() } @Test fun isChipEnabled_noIndicationEnabled() = testScope.runTest { val actual by collectLastValue(underTest.isChipEnabled) privacyChipRepository.setIsMicCameraIndicationEnabled(false) privacyChipRepository.setIsLocationIndicationEnabled(false) assertThat(actual).isFalse() } @Test fun isChipEnabled_micCameraIndicationEnabled() = testScope.runTest { val actual by collectLastValue(underTest.isChipEnabled) privacyChipRepository.setIsMicCameraIndicationEnabled(true) privacyChipRepository.setIsLocationIndicationEnabled(false) assertThat(actual).isTrue() } @Test fun isChipEnabled_locationIndicationEnabled() = testScope.runTest { val actual by collectLastValue(underTest.isChipEnabled) privacyChipRepository.setIsMicCameraIndicationEnabled(false) privacyChipRepository.setIsLocationIndicationEnabled(true) assertThat(actual).isTrue() } @Test fun isChipEnabled_allIndicationEnabled() = testScope.runTest { val actual by collectLastValue(underTest.isChipEnabled) privacyChipRepository.setIsMicCameraIndicationEnabled(true) privacyChipRepository.setIsLocationIndicationEnabled(true) assertThat(actual).isTrue() } @OptIn(ExperimentalCoroutinesApi::class) @Test fun onPrivacyChipClicked_safetyCenterEnabled() = Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt +61 −21 Original line number Diff line number Diff line Loading @@ -15,13 +15,18 @@ import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor import com.android.systemui.flags.EnableSceneContainer import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.testScope import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.lifecycle.activateIn import com.android.systemui.plugins.activityStarter import com.android.systemui.privacy.PrivacyApplication import com.android.systemui.privacy.PrivacyItem import com.android.systemui.privacy.PrivacyType import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Overlays import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.shade.data.repository.fakePrivacyChipRepository import com.android.systemui.shade.domain.interactor.disableDualShade import com.android.systemui.shade.domain.interactor.enableDualShade import com.android.systemui.shade.domain.interactor.shadeMode Loading @@ -31,10 +36,8 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.fakeMobi import com.android.systemui.testKosmos import com.android.systemui.util.mockito.argThat import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test Loading @@ -44,7 +47,6 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) @EnableSceneContainer Loading @@ -66,12 +68,10 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { fun mobileSubIds_update() = testScope.runTest { mobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1) runCurrent() assertThat(underTest.mobileSubIds).isEqualTo(listOf(1)) mobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2) runCurrent() assertThat(underTest.mobileSubIds).isEqualTo(listOf(1, 2)) } Loading Loading @@ -110,7 +110,6 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { setScene(Scenes.Shade) underTest.onSystemIconChipClicked() runCurrent() assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Lockscreen) } Loading @@ -123,7 +122,6 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) underTest.onSystemIconChipClicked() runCurrent() assertThat(currentScene).isEqualTo(Scenes.Lockscreen) assertThat(currentOverlays).isEmpty() Loading @@ -137,7 +135,6 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) underTest.onSystemIconChipClicked() runCurrent() assertThat(currentScene).isEqualTo(Scenes.Lockscreen) assertThat(currentOverlays).contains(Overlays.QuickSettingsShade) Loading @@ -152,7 +149,6 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { setScene(Scenes.Shade) underTest.onSystemIconChipClicked() runCurrent() assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Gone) } Loading @@ -165,7 +161,6 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) underTest.onSystemIconChipClicked() runCurrent() assertThat(currentScene).isEqualTo(Scenes.Gone) assertThat(currentOverlays).isEmpty() Loading @@ -179,7 +174,6 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) underTest.onSystemIconChipClicked() runCurrent() assertThat(currentScene).isEqualTo(Scenes.Gone) assertThat(currentOverlays).contains(Overlays.QuickSettingsShade) Loading @@ -194,7 +188,6 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) underTest.onNotificationIconChipClicked() runCurrent() assertThat(currentScene).isEqualTo(Scenes.Lockscreen) assertThat(currentOverlays).isEmpty() Loading @@ -208,7 +201,6 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) underTest.onNotificationIconChipClicked() runCurrent() assertThat(currentScene).isEqualTo(Scenes.Lockscreen) assertThat(currentOverlays).contains(Overlays.NotificationsShade) Loading @@ -223,7 +215,6 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) underTest.onNotificationIconChipClicked() runCurrent() assertThat(currentScene).isEqualTo(Scenes.Gone) assertThat(currentOverlays).isEmpty() Loading @@ -237,13 +228,67 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { val currentOverlays by collectLastValue(sceneInteractor.currentOverlays) underTest.onNotificationIconChipClicked() runCurrent() assertThat(currentScene).isEqualTo(Scenes.Gone) assertThat(currentOverlays).contains(Overlays.NotificationsShade) assertThat(currentOverlays).doesNotContain(Overlays.QuickSettingsShade) } @Test fun isPrivacyChipVisible_updates() = kosmos.runTest { fakePrivacyChipRepository.setPrivacyItems(emptyList()) assertThat(underTest.isPrivacyChipVisible).isFalse() fakePrivacyChipRepository.setPrivacyItems( listOf( PrivacyItem( privacyType = PrivacyType.TYPE_CAMERA, application = PrivacyApplication("", 0), ) ) ) assertThat(underTest.isPrivacyChipVisible).isTrue() } @Test fun isPrivacyChipEnabled_noIndicationEnabled() = kosmos.runTest { fakePrivacyChipRepository.setIsMicCameraIndicationEnabled(false) fakePrivacyChipRepository.setIsLocationIndicationEnabled(false) assertThat(underTest.isPrivacyChipEnabled).isFalse() } @Test fun isPrivacyChipEnabled_micCameraIndicationEnabled() = kosmos.runTest { fakePrivacyChipRepository.setIsMicCameraIndicationEnabled(true) fakePrivacyChipRepository.setIsLocationIndicationEnabled(false) assertThat(underTest.isPrivacyChipEnabled).isTrue() } @Test fun isPrivacyChipEnabled_locationIndicationEnabled() = kosmos.runTest { fakePrivacyChipRepository.setIsMicCameraIndicationEnabled(false) fakePrivacyChipRepository.setIsLocationIndicationEnabled(true) assertThat(underTest.isPrivacyChipEnabled).isTrue() } @Test fun isPrivacyChipEnabled_allIndicationEnabled() = kosmos.runTest { fakePrivacyChipRepository.setIsMicCameraIndicationEnabled(true) fakePrivacyChipRepository.setIsLocationIndicationEnabled(true) assertThat(underTest.isPrivacyChipEnabled).isTrue() } companion object { private val SUB_1 = SubscriptionModel( Loading Loading @@ -272,19 +317,16 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { SuccessFingerprintAuthenticationStatus(0, true) ) } runCurrent() assertThat(shadeMode).isEqualTo(ShadeMode.Dual) sceneInteractor.changeScene(scene, "test") checkNotNull(currentOverlays).forEach { sceneInteractor.instantlyHideOverlay(it, "test") } runCurrent() overlay?.let { sceneInteractor.showOverlay(it, "test") } sceneInteractor.setTransitionState( MutableStateFlow<ObservableTransitionState>( ObservableTransitionState.Idle(scene, setOfNotNull(overlay)) ) ) runCurrent() assertThat(currentScene).isEqualTo(scene) if (overlay == null) { Loading @@ -299,16 +341,14 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { sceneInteractor.setTransitionState( MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key)) ) testScope.runCurrent() } private fun TestScope.setDeviceEntered(isEntered: Boolean) { private fun setDeviceEntered(isEntered: Boolean) { if (isEntered) { // Unlock the device marking the device has entered. kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( SuccessFingerprintAuthenticationStatus(0, true) ) runCurrent() } setScene( if (isEntered) { Loading
packages/SystemUI/src/com/android/systemui/shade/domain/interactor/PrivacyChipInteractor.kt +1 −29 Original line number Diff line number Diff line Loading @@ -26,11 +26,7 @@ import com.android.systemui.shade.data.repository.PrivacyChipRepository import com.android.systemui.statusbar.policy.DeviceProvisionedController import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch @SysUISingleton Loading @@ -53,30 +49,6 @@ constructor( /** Whether or not location indicators are enabled in the device privacy config. */ val isLocationIndicationEnabled: StateFlow<Boolean> = repository.isLocationIndicationEnabled /** Whether or not the privacy chip should be visible. */ val isChipVisible: StateFlow<Boolean> = privacyItems .map { it.isNotEmpty() } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = false, ) /** Whether or not the privacy chip is enabled in the device privacy config. */ val isChipEnabled: StateFlow<Boolean> = combine( isMicCameraIndicationEnabled, isLocationIndicationEnabled, ) { micCamera, location -> micCamera || location } .stateIn( scope = applicationScope, started = SharingStarted.WhileSubscribed(), initialValue = false, ) /** Notifies that the privacy chip was clicked. */ fun onPrivacyChipClicked(privacyChip: OngoingPrivacyChip) { if (!deviceProvisionedController.isDeviceProvisioned) return Loading @@ -85,7 +57,7 @@ constructor( if (repository.isSafetyCenterEnabled()) { privacyDialogControllerV2.showDialog( shadeDialogContextInteractor.context, privacyChip privacyChip, ) } else { privacyDialogController.showDialog(shadeDialogContextInteractor.context) Loading
packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt +26 −9 Original line number Diff line number Diff line Loading @@ -24,6 +24,7 @@ import android.icu.text.DateFormat import android.icu.text.DisplayContext import android.provider.Settings import android.view.ViewGroup import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.ui.unit.IntRect import com.android.app.tracing.coroutines.launchTraced as launch Loading Loading @@ -63,7 +64,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.awaitCancellation import kotlinx.coroutines.coroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.mapLatest Loading Loading @@ -109,7 +109,12 @@ constructor( batteryMeterViewControllerFactory::create /** True if there is exactly one mobile connection. */ val isSingleCarrier: StateFlow<Boolean> = mobileIconsInteractor.isSingleCarrier val isSingleCarrier: Boolean by hydrator.hydratedStateOf( traceName = "isSingleCarrier", initialValue = mobileIconsInteractor.isSingleCarrier.value, source = mobileIconsInteractor.isSingleCarrier, ) /** The list of subscription Ids for current mobile connections. */ val mobileSubIds: List<Int> by Loading @@ -123,21 +128,33 @@ constructor( ) /** The list of PrivacyItems to be displayed by the privacy chip. */ val privacyItems: StateFlow<List<PrivacyItem>> = privacyChipInteractor.privacyItems val privacyItems: List<PrivacyItem> by hydrator.hydratedStateOf( traceName = "privacyItems", source = privacyChipInteractor.privacyItems, ) /** Whether or not mic & camera indicators are enabled in the device privacy config. */ val isMicCameraIndicationEnabled: StateFlow<Boolean> = privacyChipInteractor.isMicCameraIndicationEnabled val isMicCameraIndicationEnabled: Boolean by hydrator.hydratedStateOf( traceName = "isMicCameraIndicationEnabled", source = privacyChipInteractor.isMicCameraIndicationEnabled, ) /** Whether or not location indicators are enabled in the device privacy config. */ val isLocationIndicationEnabled: StateFlow<Boolean> = privacyChipInteractor.isLocationIndicationEnabled val isLocationIndicationEnabled: Boolean by hydrator.hydratedStateOf( traceName = "isLocationIndicationEnabled", source = privacyChipInteractor.isLocationIndicationEnabled, ) /** Whether or not the privacy chip should be visible. */ val isPrivacyChipVisible: StateFlow<Boolean> = privacyChipInteractor.isChipVisible val isPrivacyChipVisible: Boolean by derivedStateOf { privacyItems.isNotEmpty() } /** Whether or not the privacy chip is enabled in the device privacy config. */ val isPrivacyChipEnabled: StateFlow<Boolean> = privacyChipInteractor.isChipEnabled val isPrivacyChipEnabled: Boolean by derivedStateOf { isMicCameraIndicationEnabled || isLocationIndicationEnabled } val animateNotificationsChipBounce: Boolean get() = Loading