Loading packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt +8 −19 Original line number Diff line number Diff line Loading @@ -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() Loading @@ -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), ) Loading Loading @@ -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)) { Loading Loading @@ -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), ) Loading Loading @@ -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, ) } Loading Loading @@ -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( Loading packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractorTest.kt +85 −3 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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() = Loading @@ -55,7 +68,7 @@ class ShadeHeaderClockInteractorTest : SysuiTestCase() { verify(activityStarter) .postStartActivityDismissingKeyguard( argThat(IntentMatcherAction(AlarmClock.ACTION_SHOW_ALARMS)), any() any(), ) } Loading @@ -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> { Loading packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt +54 −85 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading Loading @@ -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() Loading @@ -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() Loading @@ -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() Loading @@ -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() Loading @@ -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() Loading @@ -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() Loading @@ -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() Loading @@ -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() Loading Loading @@ -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) } Loading @@ -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) } Loading @@ -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) } Loading @@ -401,21 +348,43 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { ) } private fun setScene(key: SceneKey) { sceneInteractor.changeScene(key, "test") sceneInteractor.setTransitionState( MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key)) 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) ) testScope.runCurrent() } runCurrent() assertThat(shadeMode).isEqualTo(ShadeMode.Dual) private fun setOverlay(key: OverlayKey) { val currentOverlays = sceneInteractor.currentOverlays.value + key sceneInteractor.showOverlay(key, "test") sceneInteractor.changeScene(scene, "test") checkNotNull(currentOverlays).forEach { sceneInteractor.instantlyHideOverlay(it, "test") } runCurrent() overlay?.let { sceneInteractor.showOverlay(it, "test") } sceneInteractor.setTransitionState( MutableStateFlow<ObservableTransitionState>( ObservableTransitionState.Idle(sceneInteractor.currentScene.value, currentOverlays) ObservableTransitionState.Idle(scene, setOfNotNull(overlay)) ) ) runCurrent() assertThat(currentScene).isEqualTo(scene) if (overlay == null) { assertThat(currentOverlays).isEmpty() } else { assertThat(currentOverlays).containsExactly(overlay) } } private fun setScene(key: SceneKey) { sceneInteractor.changeScene(key, "test") sceneInteractor.setTransitionState( MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key)) ) testScope.runCurrent() } Loading packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractor.kt +36 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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, ) } } packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt +41 −65 File changed.Preview size limit exceeded, changes collapsed. Show changes Loading
packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt +8 −19 Original line number Diff line number Diff line Loading @@ -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() Loading @@ -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), ) Loading Loading @@ -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)) { Loading Loading @@ -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), ) Loading Loading @@ -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, ) } Loading Loading @@ -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( Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractorTest.kt +85 −3 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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() = Loading @@ -55,7 +68,7 @@ class ShadeHeaderClockInteractorTest : SysuiTestCase() { verify(activityStarter) .postStartActivityDismissingKeyguard( argThat(IntentMatcherAction(AlarmClock.ACTION_SHOW_ALARMS)), any() any(), ) } Loading @@ -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> { Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt +54 −85 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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 Loading Loading @@ -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() Loading @@ -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() Loading @@ -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() Loading @@ -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() Loading @@ -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() Loading @@ -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() Loading @@ -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() Loading @@ -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() Loading Loading @@ -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) } Loading @@ -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) } Loading @@ -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) } Loading @@ -401,21 +348,43 @@ class ShadeHeaderViewModelTest : SysuiTestCase() { ) } private fun setScene(key: SceneKey) { sceneInteractor.changeScene(key, "test") sceneInteractor.setTransitionState( MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key)) 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) ) testScope.runCurrent() } runCurrent() assertThat(shadeMode).isEqualTo(ShadeMode.Dual) private fun setOverlay(key: OverlayKey) { val currentOverlays = sceneInteractor.currentOverlays.value + key sceneInteractor.showOverlay(key, "test") sceneInteractor.changeScene(scene, "test") checkNotNull(currentOverlays).forEach { sceneInteractor.instantlyHideOverlay(it, "test") } runCurrent() overlay?.let { sceneInteractor.showOverlay(it, "test") } sceneInteractor.setTransitionState( MutableStateFlow<ObservableTransitionState>( ObservableTransitionState.Idle(sceneInteractor.currentScene.value, currentOverlays) ObservableTransitionState.Idle(scene, setOfNotNull(overlay)) ) ) runCurrent() assertThat(currentScene).isEqualTo(scene) if (overlay == null) { assertThat(currentOverlays).isEmpty() } else { assertThat(currentOverlays).containsExactly(overlay) } } private fun setScene(key: SceneKey) { sceneInteractor.changeScene(key, "test") sceneInteractor.setTransitionState( MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key)) ) testScope.runCurrent() } Loading
packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeHeaderClockInteractor.kt +36 −1 Original line number Diff line number Diff line Loading @@ -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 Loading @@ -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 Loading @@ -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, ) } }
packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt +41 −65 File changed.Preview size limit exceeded, changes collapsed. Show changes