Loading packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt +3 −0 Original line number Diff line number Diff line Loading @@ -37,6 +37,7 @@ import com.android.systemui.communal.smartspace.SmartspaceInteractionHandler import com.android.systemui.communal.ui.compose.section.AmbientStatusBarSection import com.android.systemui.communal.ui.compose.section.CommunalPopupSection import com.android.systemui.communal.ui.compose.section.CommunalToDreamButtonSection import com.android.systemui.communal.ui.compose.section.HubOnboardingSection import com.android.systemui.communal.ui.view.layout.sections.CommunalAppWidgetSection import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.keyguard.ui.composable.blueprint.BlueprintAlignmentLines Loading @@ -62,6 +63,7 @@ constructor( private val communalPopupSection: CommunalPopupSection, private val widgetSection: CommunalAppWidgetSection, private val communalToDreamButtonSection: CommunalToDreamButtonSection, private val hubOnboardingSection: HubOnboardingSection, ) { @Composable Loading @@ -83,6 +85,7 @@ constructor( modifier = Modifier.element(Communal.Elements.Grid), contentScope = this@Content, ) with(hubOnboardingSection) { BottomSheet() } } if (communalSettingsInteractor.isV2FlagEnabled()) { Icon( Loading packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/HubOnboardingSection.kt 0 → 100644 +164 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.communal.ui.compose.section import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ChargingStation import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.systemui.communal.ui.viewmodel.HubOnboardingViewModel import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.res.R import com.android.systemui.statusbar.phone.ComponentSystemUIDialog import com.android.systemui.statusbar.phone.SystemUIDialogFactory import com.android.systemui.statusbar.phone.createBottomSheet import javax.inject.Inject class HubOnboardingSection @Inject constructor( private val viewModelFactory: HubOnboardingViewModel.Factory, private val dialogFactory: SystemUIDialogFactory, ) { @Composable fun BottomSheet() { val viewModel = rememberViewModel("HubOnboardingSection") { viewModelFactory.create() } val shouldShowHubOnboarding by viewModel.shouldShowHubOnboarding.collectAsStateWithLifecycle(false) if (!shouldShowHubOnboarding) { return } HubOnboardingBottomSheet(shouldShowBottomSheet = true, dialogFactory = dialogFactory) { viewModel.onDismissed() } } } @Composable private fun HubOnboardingBottomSheet( shouldShowBottomSheet: Boolean, dialogFactory: SystemUIDialogFactory, onDismiss: () -> Unit, ) { var dialog: ComponentSystemUIDialog? by remember { mutableStateOf(null) } var dismissingDueToCancel by remember { mutableStateOf(false) } DisposableEffect(shouldShowBottomSheet) { if (shouldShowBottomSheet) { dialog = dialogFactory .createBottomSheet( content = { HubOnboardingBottomSheetContent { dialog?.dismiss() } }, isDraggable = true, maxWidth = 627.dp, ) .apply { setOnDismissListener { // Don't set the onboarding dismissed flag if the dismiss was due to a // cancel. Note that a "dismiss" is something initiated by the user // (e.g. swipe down or tapping outside), while a "cancel" is a dismiss // not initiated by the user (e.g. timing out to dream). We only want // to mark the bottom sheet as dismissed if the user explicitly // dismissed it. if (!dismissingDueToCancel) { onDismiss() } } setOnCancelListener { dismissingDueToCancel = true } show() } } onDispose { dialog?.cancel() dialog = null } } } @Composable private fun HubOnboardingBottomSheetContent(onButtonClicked: () -> Unit) { val colors = MaterialTheme.colorScheme Column( modifier = Modifier.fillMaxWidth().padding(48.dp), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { Icon( imageVector = Icons.Outlined.ChargingStation, contentDescription = null, modifier = Modifier.size(32.dp), ) Spacer(modifier = Modifier.height(16.dp)) Text( text = stringResource(R.string.hub_onboarding_bottom_sheet_title), style = MaterialTheme.typography.headlineMedium, ) Spacer(modifier = Modifier.height(32.dp)) // TODO(b/388283881): Replace with correct animations and possibly add a content description // if necessary. Image(painter = painterResource(R.drawable.hub_onboarding_bg), contentDescription = null) Spacer(modifier = Modifier.height(32.dp)) Text( modifier = Modifier.width(300.dp), text = stringResource(R.string.hub_onboarding_bottom_sheet_text), textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.height(32.dp)) Button( modifier = Modifier.align(Alignment.End), colors = ButtonDefaults.buttonColors( containerColor = colors.primary, contentColor = colors.onPrimary, ), onClick = onButtonClicked, ) { Text( stringResource(R.string.hub_onboarding_bottom_sheet_action_button), style = MaterialTheme.typography.labelLarge, ) } } } packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt +28 −0 Original line number Diff line number Diff line Loading @@ -87,6 +87,34 @@ class CommunalPrefsRepositoryImplTest : SysuiTestCase() { assertThat(isCtaDismissed).isFalse() } @Test fun isHubOnboardingDismissedValue_byDefault_isFalse() = testScope.runTest { val isHubOnboardingDismissed by collectLastValue(underTest.isHubOnboardingDismissed(MAIN_USER)) assertThat(isHubOnboardingDismissed).isFalse() } @Test fun isHubOnboardingDismissedValue_onSet_isTrue() = testScope.runTest { val isHubOnboardingDismissed by collectLastValue(underTest.isHubOnboardingDismissed(MAIN_USER)) underTest.setHubOnboardingDismissed(MAIN_USER) assertThat(isHubOnboardingDismissed).isTrue() } @Test fun isHubOnboardingDismissedValue_onSetForDifferentUser_isStillFalse() = testScope.runTest { val isHubOnboardingDismissed by collectLastValue(underTest.isHubOnboardingDismissed(MAIN_USER)) underTest.setHubOnboardingDismissed(SECONDARY_USER) assertThat(isHubOnboardingDismissed).isFalse() } @Test fun getSharedPreferences_whenFileRestored() = testScope.runTest { Loading packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractorTest.kt +34 −0 Original line number Diff line number Diff line Loading @@ -74,6 +74,40 @@ class CommunalPrefsInteractorTest : SysuiTestCase() { assertThat(isCtaDismissed).isFalse() } @Test fun setHubOnboardingDismissed_currentUser() = testScope.runTest { setSelectedUser(MAIN_USER) val isHubOnboardingDismissed by collectLastValue(underTest.isHubOnboardingDismissed) assertThat(isHubOnboardingDismissed).isFalse() underTest.setHubOnboardingDismissed(MAIN_USER) assertThat(isHubOnboardingDismissed).isTrue() } @Test fun setHubOnboardingDismissed_anotherUser() = testScope.runTest { setSelectedUser(MAIN_USER) val isHubOnboardingDismissed by collectLastValue(underTest.isHubOnboardingDismissed) assertThat(isHubOnboardingDismissed).isFalse() underTest.setHubOnboardingDismissed(SECONDARY_USER) assertThat(isHubOnboardingDismissed).isFalse() } @Test fun isHubOnboardingDismissed_userSwitch() = testScope.runTest { setSelectedUser(MAIN_USER) underTest.setHubOnboardingDismissed(MAIN_USER) val isHubOnboardingDismissed by collectLastValue(underTest.isHubOnboardingDismissed) assertThat(isHubOnboardingDismissed).isTrue() setSelectedUser(SECONDARY_USER) assertThat(isHubOnboardingDismissed).isFalse() } private suspend fun setSelectedUser(user: UserInfo) { with(kosmos.fakeUserRepository) { setUserInfos(listOf(user)) Loading packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/HubOnboardingInteractorTest.kt 0 → 100644 +130 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.communal.domain.interactor import android.content.pm.UserInfo import android.content.pm.UserInfo.FLAG_MAIN import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.fakeCommunalPrefsRepository import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.settings.fakeUserTracker import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.fakeUserRepository import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class HubOnboardingInteractorTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val sceneInteractor = kosmos.sceneInteractor private val underTest: HubOnboardingInteractor by lazy { kosmos.hubOnboardingInteractor } @Test @EnableFlags(FLAG_GLANCEABLE_HUB_V2) fun setHubOnboardingDismissed() = kosmos.runTest { setSelectedUser(MAIN_USER) val isHubOnboardingDismissed by collectLastValue(fakeCommunalPrefsRepository.isHubOnboardingDismissed(MAIN_USER)) underTest.setHubOnboardingDismissed() assertThat(isHubOnboardingDismissed).isTrue() } @Test @EnableFlags(FLAG_GLANCEABLE_HUB_V2) fun shouldShowHubOnboarding_falseWhenDismissed() = kosmos.runTest { setSelectedUser(MAIN_USER) val shouldShowHubOnboarding by collectLastValue(underTest.shouldShowHubOnboarding) fakeCommunalPrefsRepository.setHubOnboardingDismissed(MAIN_USER) assertThat(shouldShowHubOnboarding).isFalse() } @Test @EnableFlags(FLAG_GLANCEABLE_HUB_V2) fun shouldShowHubOnboarding_falseWhenNotIdleOnCommunal() = kosmos.runTest { setSelectedUser(MAIN_USER) val shouldShowHubOnboarding by collectLastValue(underTest.shouldShowHubOnboarding) assertThat(shouldShowHubOnboarding).isFalse() } @Test @EnableFlags(FLAG_GLANCEABLE_HUB_V2) fun shouldShowHubOnboarding_trueWhenIdleOnCommunal() = kosmos.runTest { setSelectedUser(MAIN_USER) val shouldShowHubOnboarding by collectLastValue(underTest.shouldShowHubOnboarding) // Change to Communal scene. setIdleScene(Scenes.Communal) assertThat(shouldShowHubOnboarding).isFalse() } @Test @DisableFlags(FLAG_GLANCEABLE_HUB_V2) fun shouldShowHubOnboarding_falseWhenFlagDisabled() = kosmos.runTest { setSelectedUser(MAIN_USER) val shouldShowHubOnboarding by collectLastValue(underTest.shouldShowHubOnboarding) // Change to Communal scene. setIdleScene(Scenes.Communal) assertThat(shouldShowHubOnboarding).isFalse() } private fun setIdleScene(scene: SceneKey) { sceneInteractor.changeScene(scene, "test") val transitionState = MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(scene)) sceneInteractor.setTransitionState(transitionState) } private suspend fun setSelectedUser(user: UserInfo) { with(kosmos.fakeUserRepository) { setUserInfos(listOf(user)) setSelectedUserInfo(user) } kosmos.fakeUserTracker.set(userInfos = listOf(user), selectedUserIndex = 0) } companion object { val MAIN_USER = UserInfo(0, "main", FLAG_MAIN) } } Loading
packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt +3 −0 Original line number Diff line number Diff line Loading @@ -37,6 +37,7 @@ import com.android.systemui.communal.smartspace.SmartspaceInteractionHandler import com.android.systemui.communal.ui.compose.section.AmbientStatusBarSection import com.android.systemui.communal.ui.compose.section.CommunalPopupSection import com.android.systemui.communal.ui.compose.section.CommunalToDreamButtonSection import com.android.systemui.communal.ui.compose.section.HubOnboardingSection import com.android.systemui.communal.ui.view.layout.sections.CommunalAppWidgetSection import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.keyguard.ui.composable.blueprint.BlueprintAlignmentLines Loading @@ -62,6 +63,7 @@ constructor( private val communalPopupSection: CommunalPopupSection, private val widgetSection: CommunalAppWidgetSection, private val communalToDreamButtonSection: CommunalToDreamButtonSection, private val hubOnboardingSection: HubOnboardingSection, ) { @Composable Loading @@ -83,6 +85,7 @@ constructor( modifier = Modifier.element(Communal.Elements.Grid), contentScope = this@Content, ) with(hubOnboardingSection) { BottomSheet() } } if (communalSettingsInteractor.isV2FlagEnabled()) { Icon( Loading
packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/HubOnboardingSection.kt 0 → 100644 +164 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.communal.ui.compose.section import androidx.compose.foundation.Image import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.material.icons.Icons import androidx.compose.material.icons.outlined.ChargingStation import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.systemui.communal.ui.viewmodel.HubOnboardingViewModel import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.res.R import com.android.systemui.statusbar.phone.ComponentSystemUIDialog import com.android.systemui.statusbar.phone.SystemUIDialogFactory import com.android.systemui.statusbar.phone.createBottomSheet import javax.inject.Inject class HubOnboardingSection @Inject constructor( private val viewModelFactory: HubOnboardingViewModel.Factory, private val dialogFactory: SystemUIDialogFactory, ) { @Composable fun BottomSheet() { val viewModel = rememberViewModel("HubOnboardingSection") { viewModelFactory.create() } val shouldShowHubOnboarding by viewModel.shouldShowHubOnboarding.collectAsStateWithLifecycle(false) if (!shouldShowHubOnboarding) { return } HubOnboardingBottomSheet(shouldShowBottomSheet = true, dialogFactory = dialogFactory) { viewModel.onDismissed() } } } @Composable private fun HubOnboardingBottomSheet( shouldShowBottomSheet: Boolean, dialogFactory: SystemUIDialogFactory, onDismiss: () -> Unit, ) { var dialog: ComponentSystemUIDialog? by remember { mutableStateOf(null) } var dismissingDueToCancel by remember { mutableStateOf(false) } DisposableEffect(shouldShowBottomSheet) { if (shouldShowBottomSheet) { dialog = dialogFactory .createBottomSheet( content = { HubOnboardingBottomSheetContent { dialog?.dismiss() } }, isDraggable = true, maxWidth = 627.dp, ) .apply { setOnDismissListener { // Don't set the onboarding dismissed flag if the dismiss was due to a // cancel. Note that a "dismiss" is something initiated by the user // (e.g. swipe down or tapping outside), while a "cancel" is a dismiss // not initiated by the user (e.g. timing out to dream). We only want // to mark the bottom sheet as dismissed if the user explicitly // dismissed it. if (!dismissingDueToCancel) { onDismiss() } } setOnCancelListener { dismissingDueToCancel = true } show() } } onDispose { dialog?.cancel() dialog = null } } } @Composable private fun HubOnboardingBottomSheetContent(onButtonClicked: () -> Unit) { val colors = MaterialTheme.colorScheme Column( modifier = Modifier.fillMaxWidth().padding(48.dp), verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.CenterHorizontally, ) { Icon( imageVector = Icons.Outlined.ChargingStation, contentDescription = null, modifier = Modifier.size(32.dp), ) Spacer(modifier = Modifier.height(16.dp)) Text( text = stringResource(R.string.hub_onboarding_bottom_sheet_title), style = MaterialTheme.typography.headlineMedium, ) Spacer(modifier = Modifier.height(32.dp)) // TODO(b/388283881): Replace with correct animations and possibly add a content description // if necessary. Image(painter = painterResource(R.drawable.hub_onboarding_bg), contentDescription = null) Spacer(modifier = Modifier.height(32.dp)) Text( modifier = Modifier.width(300.dp), text = stringResource(R.string.hub_onboarding_bottom_sheet_text), textAlign = TextAlign.Center, ) Spacer(modifier = Modifier.height(32.dp)) Button( modifier = Modifier.align(Alignment.End), colors = ButtonDefaults.buttonColors( containerColor = colors.primary, contentColor = colors.onPrimary, ), onClick = onButtonClicked, ) { Text( stringResource(R.string.hub_onboarding_bottom_sheet_action_button), style = MaterialTheme.typography.labelLarge, ) } } }
packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/repository/CommunalPrefsRepositoryImplTest.kt +28 −0 Original line number Diff line number Diff line Loading @@ -87,6 +87,34 @@ class CommunalPrefsRepositoryImplTest : SysuiTestCase() { assertThat(isCtaDismissed).isFalse() } @Test fun isHubOnboardingDismissedValue_byDefault_isFalse() = testScope.runTest { val isHubOnboardingDismissed by collectLastValue(underTest.isHubOnboardingDismissed(MAIN_USER)) assertThat(isHubOnboardingDismissed).isFalse() } @Test fun isHubOnboardingDismissedValue_onSet_isTrue() = testScope.runTest { val isHubOnboardingDismissed by collectLastValue(underTest.isHubOnboardingDismissed(MAIN_USER)) underTest.setHubOnboardingDismissed(MAIN_USER) assertThat(isHubOnboardingDismissed).isTrue() } @Test fun isHubOnboardingDismissedValue_onSetForDifferentUser_isStillFalse() = testScope.runTest { val isHubOnboardingDismissed by collectLastValue(underTest.isHubOnboardingDismissed(MAIN_USER)) underTest.setHubOnboardingDismissed(SECONDARY_USER) assertThat(isHubOnboardingDismissed).isFalse() } @Test fun getSharedPreferences_whenFileRestored() = testScope.runTest { Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalPrefsInteractorTest.kt +34 −0 Original line number Diff line number Diff line Loading @@ -74,6 +74,40 @@ class CommunalPrefsInteractorTest : SysuiTestCase() { assertThat(isCtaDismissed).isFalse() } @Test fun setHubOnboardingDismissed_currentUser() = testScope.runTest { setSelectedUser(MAIN_USER) val isHubOnboardingDismissed by collectLastValue(underTest.isHubOnboardingDismissed) assertThat(isHubOnboardingDismissed).isFalse() underTest.setHubOnboardingDismissed(MAIN_USER) assertThat(isHubOnboardingDismissed).isTrue() } @Test fun setHubOnboardingDismissed_anotherUser() = testScope.runTest { setSelectedUser(MAIN_USER) val isHubOnboardingDismissed by collectLastValue(underTest.isHubOnboardingDismissed) assertThat(isHubOnboardingDismissed).isFalse() underTest.setHubOnboardingDismissed(SECONDARY_USER) assertThat(isHubOnboardingDismissed).isFalse() } @Test fun isHubOnboardingDismissed_userSwitch() = testScope.runTest { setSelectedUser(MAIN_USER) underTest.setHubOnboardingDismissed(MAIN_USER) val isHubOnboardingDismissed by collectLastValue(underTest.isHubOnboardingDismissed) assertThat(isHubOnboardingDismissed).isTrue() setSelectedUser(SECONDARY_USER) assertThat(isHubOnboardingDismissed).isFalse() } private suspend fun setSelectedUser(user: UserInfo) { with(kosmos.fakeUserRepository) { setUserInfos(listOf(user)) Loading
packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/HubOnboardingInteractorTest.kt 0 → 100644 +130 −0 Original line number Diff line number Diff line /* * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.android.systemui.communal.domain.interactor import android.content.pm.UserInfo import android.content.pm.UserInfo.FLAG_MAIN import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.compose.animation.scene.ObservableTransitionState import com.android.compose.animation.scene.SceneKey import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 import com.android.systemui.SysuiTestCase import com.android.systemui.communal.data.repository.fakeCommunalPrefsRepository import com.android.systemui.kosmos.collectLastValue import com.android.systemui.kosmos.runTest import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.settings.fakeUserTracker import com.android.systemui.testKosmos import com.android.systemui.user.data.repository.fakeUserRepository import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.flow.MutableStateFlow import org.junit.Test import org.junit.runner.RunWith @SmallTest @RunWith(AndroidJUnit4::class) class HubOnboardingInteractorTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val sceneInteractor = kosmos.sceneInteractor private val underTest: HubOnboardingInteractor by lazy { kosmos.hubOnboardingInteractor } @Test @EnableFlags(FLAG_GLANCEABLE_HUB_V2) fun setHubOnboardingDismissed() = kosmos.runTest { setSelectedUser(MAIN_USER) val isHubOnboardingDismissed by collectLastValue(fakeCommunalPrefsRepository.isHubOnboardingDismissed(MAIN_USER)) underTest.setHubOnboardingDismissed() assertThat(isHubOnboardingDismissed).isTrue() } @Test @EnableFlags(FLAG_GLANCEABLE_HUB_V2) fun shouldShowHubOnboarding_falseWhenDismissed() = kosmos.runTest { setSelectedUser(MAIN_USER) val shouldShowHubOnboarding by collectLastValue(underTest.shouldShowHubOnboarding) fakeCommunalPrefsRepository.setHubOnboardingDismissed(MAIN_USER) assertThat(shouldShowHubOnboarding).isFalse() } @Test @EnableFlags(FLAG_GLANCEABLE_HUB_V2) fun shouldShowHubOnboarding_falseWhenNotIdleOnCommunal() = kosmos.runTest { setSelectedUser(MAIN_USER) val shouldShowHubOnboarding by collectLastValue(underTest.shouldShowHubOnboarding) assertThat(shouldShowHubOnboarding).isFalse() } @Test @EnableFlags(FLAG_GLANCEABLE_HUB_V2) fun shouldShowHubOnboarding_trueWhenIdleOnCommunal() = kosmos.runTest { setSelectedUser(MAIN_USER) val shouldShowHubOnboarding by collectLastValue(underTest.shouldShowHubOnboarding) // Change to Communal scene. setIdleScene(Scenes.Communal) assertThat(shouldShowHubOnboarding).isFalse() } @Test @DisableFlags(FLAG_GLANCEABLE_HUB_V2) fun shouldShowHubOnboarding_falseWhenFlagDisabled() = kosmos.runTest { setSelectedUser(MAIN_USER) val shouldShowHubOnboarding by collectLastValue(underTest.shouldShowHubOnboarding) // Change to Communal scene. setIdleScene(Scenes.Communal) assertThat(shouldShowHubOnboarding).isFalse() } private fun setIdleScene(scene: SceneKey) { sceneInteractor.changeScene(scene, "test") val transitionState = MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(scene)) sceneInteractor.setTransitionState(transitionState) } private suspend fun setSelectedUser(user: UserInfo) { with(kosmos.fakeUserRepository) { setUserInfos(listOf(user)) setSelectedUserInfo(user) } kosmos.fakeUserTracker.set(userInfos = listOf(user), selectedUserIndex = 0) } companion object { val MAIN_USER = UserInfo(0, "main", FLAG_MAIN) } }