summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt27
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractorTest.kt88
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt139
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractor.kt37
-rw-r--r--packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt106
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractorKosmos.kt4
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt2
-rw-r--r--packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt4
8 files changed, 232 insertions, 175 deletions
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 02de78bc84ce..30623acbbd31 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
@@ -149,9 +149,6 @@ fun ContentScope.CollapsedShadeHeader(
}
}
- val longerDateText by viewModel.longerDateText.collectAsStateWithLifecycle()
- val shorterDateText by viewModel.shorterDateText.collectAsStateWithLifecycle()
-
val isShadeLayoutWide = viewModel.isShadeLayoutWide
val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle()
@@ -167,8 +164,8 @@ fun ContentScope.CollapsedShadeHeader(
) {
Clock(scale = 1f, onClick = viewModel::onClockClicked)
VariableDayDate(
- longerDateText = longerDateText,
- shorterDateText = shorterDateText,
+ longerDateText = viewModel.longerDateText,
+ shorterDateText = viewModel.shorterDateText,
chipHighlight = viewModel.notificationsChipHighlight,
modifier = Modifier.element(ShadeHeader.Elements.CollapsedContentStart),
)
@@ -229,8 +226,6 @@ fun ContentScope.ExpandedShadeHeader(
derivedStateOf { shouldUseExpandedFormat(layoutState.transitionState) }
}
- val longerDateText by viewModel.longerDateText.collectAsStateWithLifecycle()
- val shorterDateText by viewModel.shorterDateText.collectAsStateWithLifecycle()
val isPrivacyChipVisible by viewModel.isPrivacyChipVisible.collectAsStateWithLifecycle()
Box(modifier = modifier.sysuiResTag(ShadeHeader.TestTags.Root)) {
@@ -269,8 +264,8 @@ fun ContentScope.ExpandedShadeHeader(
modifier = Modifier.element(ShadeHeader.Elements.ExpandedContent),
) {
VariableDayDate(
- longerDateText = longerDateText,
- shorterDateText = shorterDateText,
+ longerDateText = viewModel.longerDateText,
+ shorterDateText = viewModel.shorterDateText,
chipHighlight = viewModel.notificationsChipHighlight,
modifier = Modifier.widthIn(max = 90.dp),
)
@@ -337,12 +332,9 @@ fun ContentScope.OverlayShadeHeader(
modifier = Modifier.width(IntrinsicSize.Min).height(20.dp),
)
} else {
- val longerDateText by viewModel.longerDateText.collectAsStateWithLifecycle()
- val shorterDateText by
- viewModel.shorterDateText.collectAsStateWithLifecycle()
VariableDayDate(
- longerDateText = longerDateText,
- shorterDateText = shorterDateText,
+ longerDateText = viewModel.longerDateText,
+ shorterDateText = viewModel.shorterDateText,
chipHighlight = viewModel.notificationsChipHighlight,
)
}
@@ -546,11 +538,8 @@ private fun BatteryIcon(
@Composable
private fun ShadeCarrierGroup(viewModel: ShadeHeaderViewModel, modifier: Modifier = Modifier) {
- Row(modifier = modifier) {
- val subIds by viewModel.mobileSubIds.collectAsStateWithLifecycle()
-
- for (subId in subIds) {
- Spacer(modifier = Modifier.width(5.dp))
+ Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(5.dp)) {
+ for (subId in viewModel.mobileSubIds) {
AndroidView(
factory = { context ->
ModernShadeCarrierGroupMobileView.constructAndBind(
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractorTest.kt
index 84fc93008f49..a3dd67f85150 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractorTest.kt
@@ -22,6 +22,9 @@ import android.provider.AlarmClock
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.coroutines.collectValues
import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.activityStarter
import com.android.systemui.statusbar.policy.NextAlarmController.NextAlarmChangeCallback
@@ -31,22 +34,32 @@ import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argThat
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.withArgCaptor
+import com.google.common.truth.Truth.assertThat
+import java.util.Date
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.milliseconds
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.ArgumentMatcher
-import org.mockito.Mockito.times
import org.mockito.Mockito.verify
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class ShadeHeaderClockInteractorTest : SysuiTestCase() {
+
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
private val activityStarter = kosmos.activityStarter
private val nextAlarmController = kosmos.nextAlarmController
- val underTest = kosmos.shadeHeaderClockInteractor
+ private val underTest = kosmos.shadeHeaderClockInteractor
@Test
fun launchClockActivity_default() =
@@ -55,7 +68,7 @@ class ShadeHeaderClockInteractorTest : SysuiTestCase() {
verify(activityStarter)
.postStartActivityDismissingKeyguard(
argThat(IntentMatcherAction(AlarmClock.ACTION_SHOW_ALARMS)),
- any()
+ any(),
)
}
@@ -71,6 +84,75 @@ class ShadeHeaderClockInteractorTest : SysuiTestCase() {
underTest.launchClockActivity()
verify(activityStarter).postStartActivityDismissingKeyguard(any())
}
+
+ @Test
+ fun onTimezoneOrLocaleChanged_localeAndTimezoneChanged_emitsForEach() =
+ testScope.runTest {
+ val timeZoneOrLocaleChanges by collectValues(underTest.onTimezoneOrLocaleChanged)
+
+ sendIntentActionBroadcast(Intent.ACTION_TIMEZONE_CHANGED)
+ sendIntentActionBroadcast(Intent.ACTION_LOCALE_CHANGED)
+ sendIntentActionBroadcast(Intent.ACTION_LOCALE_CHANGED)
+ sendIntentActionBroadcast(Intent.ACTION_TIMEZONE_CHANGED)
+
+ assertThat(timeZoneOrLocaleChanges).hasSize(4)
+ }
+
+ @Test
+ fun onTimezoneOrLocaleChanged_timeChanged_doesNotEmit() =
+ testScope.runTest {
+ val timeZoneOrLocaleChanges by collectValues(underTest.onTimezoneOrLocaleChanged)
+ assertThat(timeZoneOrLocaleChanges).hasSize(1)
+
+ sendIntentActionBroadcast(Intent.ACTION_TIME_CHANGED)
+ sendIntentActionBroadcast(Intent.ACTION_TIME_TICK)
+
+ // Expect only 1 event to have been emitted onStart, but no more.
+ assertThat(timeZoneOrLocaleChanges).hasSize(1)
+ }
+
+ @Test
+ fun currentTime_timeChanged() =
+ testScope.runTest {
+ val currentTime by collectLastValue(underTest.currentTime)
+
+ sendIntentActionBroadcast(Intent.ACTION_TIME_CHANGED)
+ val earlierTime = checkNotNull(currentTime)
+
+ advanceTimeBy(3.seconds)
+ runCurrent()
+
+ sendIntentActionBroadcast(Intent.ACTION_TIME_CHANGED)
+ val laterTime = checkNotNull(currentTime)
+
+ assertThat(differenceBetween(laterTime, earlierTime)).isEqualTo(3.seconds)
+ }
+
+ @Test
+ fun currentTime_timeTicked() =
+ testScope.runTest {
+ val currentTime by collectLastValue(underTest.currentTime)
+
+ sendIntentActionBroadcast(Intent.ACTION_TIME_TICK)
+ val earlierTime = checkNotNull(currentTime)
+
+ advanceTimeBy(7.seconds)
+ runCurrent()
+
+ sendIntentActionBroadcast(Intent.ACTION_TIME_TICK)
+ val laterTime = checkNotNull(currentTime)
+
+ assertThat(differenceBetween(laterTime, earlierTime)).isEqualTo(7.seconds)
+ }
+
+ private fun differenceBetween(date1: Date, date2: Date): Duration {
+ return (date1.time - date2.time).milliseconds
+ }
+
+ private fun TestScope.sendIntentActionBroadcast(intentAction: String) {
+ kosmos.broadcastDispatcher.sendIntentToMatchingReceiversOnly(context, Intent(intentAction))
+ runCurrent()
+ }
}
private class IntentMatcherAction(private val action: String) : ArgumentMatcher<Intent> {
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 061e04ef29f7..37b4688f753d 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
@@ -25,12 +25,15 @@ import com.android.systemui.shade.domain.interactor.disableDualShade
import com.android.systemui.shade.domain.interactor.enableDualShade
import com.android.systemui.shade.domain.interactor.enableSingleShade
import com.android.systemui.shade.domain.interactor.enableSplitShade
+import com.android.systemui.shade.domain.interactor.shadeMode
+import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel.HeaderChipHighlight
import com.android.systemui.statusbar.pipeline.mobile.data.model.SubscriptionModel
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.fakeMobileIconsInteractor
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
@@ -43,6 +46,7 @@ import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
@EnableSceneContainer
@@ -64,14 +68,15 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
@Test
fun mobileSubIds_update() =
testScope.runTest {
- val mobileSubIds by collectLastValue(underTest.mobileSubIds)
mobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1)
+ runCurrent()
- assertThat(mobileSubIds).isEqualTo(listOf(1))
+ assertThat(underTest.mobileSubIds).isEqualTo(listOf(1))
mobileIconsInteractor.filteredSubscriptions.value = listOf(SUB_1, SUB_2)
+ runCurrent()
- assertThat(mobileSubIds).isEqualTo(listOf(1, 2))
+ assertThat(underTest.mobileSubIds).isEqualTo(listOf(1, 2))
}
@Test
@@ -116,13 +121,9 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
@Test
fun onSystemIconChipClicked_lockedOnQsShade_collapsesShadeToLockscreen() =
testScope.runTest {
- kosmos.enableDualShade()
+ setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.QuickSettingsShade)
val currentScene by collectLastValue(sceneInteractor.currentScene)
val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
- setDeviceEntered(false)
- setScene(Scenes.Lockscreen)
- setOverlay(Overlays.QuickSettingsShade)
- assertThat(currentOverlays).isNotEmpty()
underTest.onSystemIconChipClicked()
runCurrent()
@@ -134,13 +135,9 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
@Test
fun onSystemIconChipClicked_lockedOnNotifShade_expandsQsShade() =
testScope.runTest {
- kosmos.enableDualShade()
+ setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.NotificationsShade)
val currentScene by collectLastValue(sceneInteractor.currentScene)
val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
- setDeviceEntered(false)
- setScene(Scenes.Lockscreen)
- setOverlay(Overlays.NotificationsShade)
- assertThat(currentOverlays).isNotEmpty()
underTest.onSystemIconChipClicked()
runCurrent()
@@ -166,13 +163,9 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
@Test
fun onSystemIconChipClicked_unlockedOnQsShade_collapsesShadeToGone() =
testScope.runTest {
- kosmos.enableDualShade()
+ setupDualShadeState(scene = Scenes.Gone, overlay = Overlays.QuickSettingsShade)
val currentScene by collectLastValue(sceneInteractor.currentScene)
val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
- setDeviceEntered(true)
- setScene(Scenes.Gone)
- setOverlay(Overlays.QuickSettingsShade)
- assertThat(currentOverlays).isNotEmpty()
underTest.onSystemIconChipClicked()
runCurrent()
@@ -184,13 +177,9 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
@Test
fun onSystemIconChipClicked_unlockedOnNotifShade_expandsQsShade() =
testScope.runTest {
- kosmos.enableDualShade()
+ setupDualShadeState(scene = Scenes.Gone, overlay = Overlays.NotificationsShade)
val currentScene by collectLastValue(sceneInteractor.currentScene)
val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
- setDeviceEntered(true)
- setScene(Scenes.Gone)
- setOverlay(Overlays.NotificationsShade)
- assertThat(currentOverlays).isNotEmpty()
underTest.onSystemIconChipClicked()
runCurrent()
@@ -203,13 +192,9 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
@Test
fun onNotificationIconChipClicked_lockedOnNotifShade_collapsesShadeToLockscreen() =
testScope.runTest {
- kosmos.enableDualShade()
+ setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.NotificationsShade)
val currentScene by collectLastValue(sceneInteractor.currentScene)
val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
- setDeviceEntered(false)
- setScene(Scenes.Lockscreen)
- setOverlay(Overlays.NotificationsShade)
- assertThat(currentOverlays).isNotEmpty()
underTest.onNotificationIconChipClicked()
runCurrent()
@@ -221,13 +206,9 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
@Test
fun onNotificationIconChipClicked_lockedOnQsShade_expandsNotifShade() =
testScope.runTest {
- kosmos.enableDualShade()
+ setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.QuickSettingsShade)
val currentScene by collectLastValue(sceneInteractor.currentScene)
val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
- setDeviceEntered(false)
- setScene(Scenes.Lockscreen)
- setOverlay(Overlays.QuickSettingsShade)
- assertThat(currentOverlays).isNotEmpty()
underTest.onNotificationIconChipClicked()
runCurrent()
@@ -240,13 +221,9 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
@Test
fun onNotificationIconChipClicked_unlockedOnNotifShade_collapsesShadeToGone() =
testScope.runTest {
- kosmos.enableDualShade()
+ setupDualShadeState(scene = Scenes.Gone, overlay = Overlays.NotificationsShade)
val currentScene by collectLastValue(sceneInteractor.currentScene)
val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
- setDeviceEntered(true)
- setScene(Scenes.Gone)
- setOverlay(Overlays.NotificationsShade)
- assertThat(currentOverlays).isNotEmpty()
underTest.onNotificationIconChipClicked()
runCurrent()
@@ -258,13 +235,9 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
@Test
fun onNotificationIconChipClicked_unlockedOnQsShade_expandsNotifShade() =
testScope.runTest {
- kosmos.enableDualShade()
+ setupDualShadeState(scene = Scenes.Gone, overlay = Overlays.QuickSettingsShade)
val currentScene by collectLastValue(sceneInteractor.currentScene)
val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
- setDeviceEntered(true)
- setScene(Scenes.Gone)
- setOverlay(Overlays.QuickSettingsShade)
- assertThat(currentOverlays).isNotEmpty()
underTest.onNotificationIconChipClicked()
runCurrent()
@@ -319,22 +292,13 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
@Test
fun highlightChips_notifsOpenInDualShade_notifsStrongQuickSettingsWeak() =
testScope.runTest {
- kosmos.enableDualShade()
- val currentScene by collectLastValue(sceneInteractor.currentScene)
- val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
-
// Test the lockscreen scenario.
- setScene(Scenes.Lockscreen)
- setOverlay(Overlays.NotificationsShade)
+ setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.NotificationsShade)
assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.Strong)
assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.Weak)
// Test the unlocked scenario.
- setDeviceEntered(true)
- setScene(Scenes.Gone)
- setOverlay(Overlays.NotificationsShade)
- assertThat(currentScene).isEqualTo(Scenes.Gone)
- assertThat(currentOverlays).isNotEmpty()
+ setupDualShadeState(scene = Scenes.Gone, overlay = Overlays.NotificationsShade)
assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.Strong)
assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.Weak)
}
@@ -342,22 +306,13 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
@Test
fun highlightChips_quickSettingsOpenInDualShade_notifsWeakQuickSettingsStrong() =
testScope.runTest {
- kosmos.enableDualShade()
- val currentScene by collectLastValue(sceneInteractor.currentScene)
- val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
-
// Test the lockscreen scenario.
- setScene(Scenes.Lockscreen)
- setOverlay(Overlays.QuickSettingsShade)
+ setupDualShadeState(scene = Scenes.Lockscreen, overlay = Overlays.QuickSettingsShade)
assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.Weak)
assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.Strong)
// Test the unlocked scenario.
- setDeviceEntered(true)
- setScene(Scenes.Gone)
- setOverlay(Overlays.QuickSettingsShade)
- assertThat(currentScene).isEqualTo(Scenes.Gone)
- assertThat(currentOverlays).isNotEmpty()
+ setupDualShadeState(scene = Scenes.Gone, overlay = Overlays.QuickSettingsShade)
assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.Weak)
assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.Strong)
}
@@ -365,21 +320,13 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
@Test
fun highlightChips_noOverlaysInDualShade_bothNone() =
testScope.runTest {
- kosmos.enableDualShade()
- val currentScene by collectLastValue(sceneInteractor.currentScene)
- val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
-
// Test the lockscreen scenario.
- setScene(Scenes.Lockscreen)
- assertThat(currentOverlays).isEmpty()
+ setupDualShadeState(scene = Scenes.Lockscreen)
assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.None)
assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.None)
// Test the unlocked scenario.
- setDeviceEntered(true)
- setScene(Scenes.Gone)
- assertThat(currentScene).isEqualTo(Scenes.Gone)
- assertThat(currentOverlays).isEmpty()
+ setupDualShadeState(scene = Scenes.Gone)
assertThat(underTest.notificationsChipHighlight).isEqualTo(HeaderChipHighlight.None)
assertThat(underTest.quickSettingsChipHighlight).isEqualTo(HeaderChipHighlight.None)
}
@@ -401,21 +348,43 @@ class ShadeHeaderViewModelTest : SysuiTestCase() {
)
}
- private fun setScene(key: SceneKey) {
- sceneInteractor.changeScene(key, "test")
+ private fun TestScope.setupDualShadeState(scene: SceneKey, overlay: OverlayKey? = null) {
+ kosmos.enableDualShade()
+ val shadeMode by collectLastValue(kosmos.shadeMode)
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ val currentOverlays by collectLastValue(sceneInteractor.currentOverlays)
+ if (scene == Scenes.Gone) {
+ // Unlock the device, marking the device has been entered.
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+ 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(key))
+ MutableStateFlow<ObservableTransitionState>(
+ ObservableTransitionState.Idle(scene, setOfNotNull(overlay))
+ )
)
- testScope.runCurrent()
+ runCurrent()
+
+ assertThat(currentScene).isEqualTo(scene)
+ if (overlay == null) {
+ assertThat(currentOverlays).isEmpty()
+ } else {
+ assertThat(currentOverlays).containsExactly(overlay)
+ }
}
- private fun setOverlay(key: OverlayKey) {
- val currentOverlays = sceneInteractor.currentOverlays.value + key
- sceneInteractor.showOverlay(key, "test")
+ private fun setScene(key: SceneKey) {
+ sceneInteractor.changeScene(key, "test")
sceneInteractor.setTransitionState(
- MutableStateFlow<ObservableTransitionState>(
- ObservableTransitionState.Idle(sceneInteractor.currentScene.value, currentOverlays)
- )
+ MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key))
)
testScope.runCurrent()
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractor.kt
index 186bfcbbc8e2..a4de1d675a15 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractor.kt
@@ -17,11 +17,19 @@
package com.android.systemui.shade.domain.interactor
import android.content.Intent
+import android.content.IntentFilter
+import android.os.UserHandle
import android.provider.AlarmClock
+import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.shade.data.repository.ShadeHeaderClockRepository
+import com.android.systemui.util.kotlin.emitOnStart
+import com.android.systemui.util.time.SystemClock
+import java.util.Date
import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
@SysUISingleton
class ShadeHeaderClockInteractor
@@ -29,7 +37,20 @@ class ShadeHeaderClockInteractor
constructor(
private val repository: ShadeHeaderClockRepository,
private val activityStarter: ActivityStarter,
+ private val broadcastDispatcher: BroadcastDispatcher,
+ private val systemClock: SystemClock,
) {
+ /** [Flow] that emits `Unit` whenever the timezone or locale has changed. */
+ val onTimezoneOrLocaleChanged: Flow<Unit> =
+ broadcastFlowForActions(Intent.ACTION_TIMEZONE_CHANGED, Intent.ACTION_LOCALE_CHANGED)
+ .emitOnStart()
+
+ /** [Flow] that emits the current `Date` every minute, or when the system time has changed. */
+ val currentTime: Flow<Date> =
+ broadcastFlowForActions(Intent.ACTION_TIME_TICK, Intent.ACTION_TIME_CHANGED)
+ .emitOnStart()
+ .map { Date(systemClock.currentTimeMillis()) }
+
/** Launch the clock activity. */
fun launchClockActivity() {
val nextAlarmIntent = repository.nextAlarmIntent
@@ -38,8 +59,22 @@ constructor(
} else {
activityStarter.postStartActivityDismissingKeyguard(
Intent(AlarmClock.ACTION_SHOW_ALARMS),
- 0
+ 0,
)
}
}
+
+ /**
+ * Returns a `Flow` that, when collected, emits `Unit` whenever a broadcast matching one of the
+ * given [actionsToFilter] is received.
+ */
+ private fun broadcastFlowForActions(
+ vararg actionsToFilter: String,
+ user: UserHandle = UserHandle.SYSTEM,
+ ): Flow<Unit> {
+ return broadcastDispatcher.broadcastFlow(
+ filter = IntentFilter().apply { actionsToFilter.forEach(::addAction) },
+ user = user,
+ )
+ }
}
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 8c38d2e7550c..63161db7902b 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
@@ -18,16 +18,13 @@ package com.android.systemui.shade.ui.viewmodel
import android.content.Context
import android.content.Intent
-import android.content.IntentFilter
import android.icu.text.DateFormat
import android.icu.text.DisplayContext
-import android.os.UserHandle
import android.provider.Settings
import android.view.ViewGroup
import androidx.compose.runtime.getValue
import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.battery.BatteryMeterViewController
-import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.lifecycle.ExclusiveActivatable
import com.android.systemui.lifecycle.Hydrator
import com.android.systemui.plugins.ActivityStarter
@@ -50,18 +47,18 @@ import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIc
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
-import java.util.Date
import java.util.Locale
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.launchIn
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.mapLatest
/** Models UI state for the shade header. */
+@OptIn(ExperimentalCoroutinesApi::class)
class ShadeHeaderViewModel
@AssistedInject
constructor(
@@ -70,7 +67,7 @@ constructor(
private val sceneInteractor: SceneInteractor,
private val shadeInteractor: ShadeInteractor,
private val shadeModeInteractor: ShadeModeInteractor,
- private val mobileIconsInteractor: MobileIconsInteractor,
+ mobileIconsInteractor: MobileIconsInteractor,
val mobileIconsViewModel: MobileIconsViewModel,
private val privacyChipInteractor: PrivacyChipInteractor,
private val clockInteractor: ShadeHeaderClockInteractor,
@@ -78,8 +75,8 @@ constructor(
private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory,
val statusBarIconController: StatusBarIconController,
val notificationIconContainerStatusBarViewBinder: NotificationIconContainerStatusBarViewBinder,
- private val broadcastDispatcher: BroadcastDispatcher,
) : ExclusiveActivatable() {
+
private val hydrator = Hydrator("ShadeHeaderViewModel.hydrator")
val createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager =
@@ -127,9 +124,16 @@ constructor(
/** True if there is exactly one mobile connection. */
val isSingleCarrier: StateFlow<Boolean> = mobileIconsInteractor.isSingleCarrier
- private val _mobileSubIds = MutableStateFlow(emptyList<Int>())
/** The list of subscription Ids for current mobile connections. */
- val mobileSubIds: StateFlow<List<Int>> = _mobileSubIds.asStateFlow()
+ val mobileSubIds: List<Int> by
+ hydrator.hydratedStateOf(
+ traceName = "mobileSubIds",
+ initialValue = emptyList(),
+ source =
+ mobileIconsInteractor.filteredSubscriptions.map { list ->
+ list.map { it.subscriptionId }
+ },
+ )
/** The list of PrivacyItems to be displayed by the privacy chip. */
val privacyItems: StateFlow<List<PrivacyItem>> = privacyChipInteractor.privacyItems
@@ -150,45 +154,34 @@ constructor(
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))
- private val shorterDateFormat = MutableStateFlow(getFormatFromPattern(shorterPattern))
- private val _shorterDateText: MutableStateFlow<String> = MutableStateFlow("")
- val shorterDateText: StateFlow<String> = _shorterDateText.asStateFlow()
+ private val longerDateFormat: Flow<DateFormat> =
+ clockInteractor.onTimezoneOrLocaleChanged.mapLatest { getFormatFromPattern(longerPattern) }
+ private val shorterDateFormat: Flow<DateFormat> =
+ clockInteractor.onTimezoneOrLocaleChanged.mapLatest { getFormatFromPattern(shorterPattern) }
+
+ val longerDateText: String by
+ hydrator.hydratedStateOf(
+ traceName = "longerDateText",
+ initialValue = "",
+ source =
+ combine(longerDateFormat, clockInteractor.currentTime) { format, time ->
+ format.format(time)
+ },
+ )
- private val _longerDateText: MutableStateFlow<String> = MutableStateFlow("")
- val longerDateText: StateFlow<String> = _longerDateText.asStateFlow()
+ val shorterDateText: String by
+ hydrator.hydratedStateOf(
+ traceName = "shorterDateText",
+ initialValue = "",
+ source =
+ combine(shorterDateFormat, clockInteractor.currentTime) { format, time ->
+ format.format(time)
+ },
+ )
override suspend fun onActivated(): Nothing {
coroutineScope {
- launch {
- broadcastDispatcher
- .broadcastFlow(
- filter =
- IntentFilter().apply {
- addAction(Intent.ACTION_TIME_TICK)
- addAction(Intent.ACTION_TIME_CHANGED)
- addAction(Intent.ACTION_TIMEZONE_CHANGED)
- addAction(Intent.ACTION_LOCALE_CHANGED)
- },
- user = UserHandle.SYSTEM,
- map = { intent, _ ->
- intent.action == Intent.ACTION_TIMEZONE_CHANGED ||
- intent.action == Intent.ACTION_LOCALE_CHANGED
- },
- )
- .onEach { invalidateFormats -> updateDateTexts(invalidateFormats) }
- .launchIn(this)
- }
-
- launch { updateDateTexts(false) }
-
- launch {
- mobileIconsInteractor.filteredSubscriptions
- .map { list -> list.map { it.subscriptionId } }
- .collect { _mobileSubIds.value = it }
- }
-
launch { hydrator.activate() }
awaitCancellation()
@@ -260,26 +253,9 @@ constructor(
data object Strong : HeaderChipHighlight
}
- private fun updateDateTexts(invalidateFormats: Boolean) {
- if (invalidateFormats) {
- longerDateFormat.value = getFormatFromPattern(longerPattern)
- shorterDateFormat.value = getFormatFromPattern(shorterPattern)
- }
-
- val currentTime = Date()
-
- _longerDateText.value = longerDateFormat.value.format(currentTime)
- _shorterDateText.value = shorterDateFormat.value.format(currentTime)
- }
-
private fun getFormatFromPattern(pattern: String?): DateFormat {
- val l = Locale.getDefault()
- val format = DateFormat.getInstanceForSkeleton(pattern, l)
- // The use of CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE instead of
- // CAPITALIZATION_FOR_STANDALONE is to address
- // https://unicode-org.atlassian.net/browse/ICU-21631
- // TODO(b/229287642): Switch back to CAPITALIZATION_FOR_STANDALONE
- format.setContext(DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE)
+ val format = DateFormat.getInstanceForSkeleton(pattern, Locale.getDefault())
+ format.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE)
return format
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractorKosmos.kt
index 6fd7cf6edbe4..2ea2119970ad 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractorKosmos.kt
@@ -16,14 +16,18 @@
package com.android.systemui.shade.domain.interactor
+import com.android.systemui.broadcast.broadcastDispatcher
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.plugins.activityStarter
import com.android.systemui.shade.data.repository.shadeHeaderClockRepository
+import com.android.systemui.util.time.systemClock
var Kosmos.shadeHeaderClockInteractor: ShadeHeaderClockInteractor by
Kosmos.Fixture {
ShadeHeaderClockInteractor(
repository = shadeHeaderClockRepository,
activityStarter = activityStarter,
+ broadcastDispatcher = broadcastDispatcher,
+ systemClock = systemClock,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt
index 08de73be1128..2f8387b2d0f9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt
@@ -18,7 +18,6 @@ package com.android.systemui.shade.ui.viewmodel
import android.content.applicationContext
import com.android.systemui.battery.batteryMeterViewControllerFactory
-import com.android.systemui.broadcast.broadcastDispatcher
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.plugins.activityStarter
import com.android.systemui.scene.domain.interactor.sceneInteractor
@@ -50,7 +49,6 @@ val Kosmos.shadeHeaderViewModel: ShadeHeaderViewModel by
statusBarIconController = mock<StatusBarIconController>(),
notificationIconContainerStatusBarViewBinder =
mock<NotificationIconContainerStatusBarViewBinder>(),
- broadcastDispatcher = broadcastDispatcher,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt
index 06af32e69b75..b23ccbfbd93f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/time/FakeSystemClockKosmos.kt
@@ -14,12 +14,15 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
package com.android.systemui.util.time
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.currentTime
var Kosmos.systemClock by
@@ -27,6 +30,7 @@ var Kosmos.systemClock by
mock {
whenever(elapsedRealtime()).thenAnswer { testScope.currentTime }
whenever(uptimeMillis()).thenAnswer { testScope.currentTime }
+ whenever(currentTimeMillis()).thenAnswer { testScope.currentTime }
}
}