diff options
| author | 2023-11-17 13:11:38 +0000 | |
|---|---|---|
| committer | 2023-11-17 13:11:38 +0000 | |
| commit | 2b221f88b01d04a4d314a3a79e2d81c4a9d36b1d (patch) | |
| tree | f5d50af244bc899c4c040336393bb41a275ee61d | |
| parent | fcbb655570ee96ba09f76a859566f908e8b939f6 (diff) | |
| parent | 3b0af20d0c9a98a9df40ecd89af847cf644e97aa (diff) | |
Merge changes I210d4ae0,I7076b98f into main
* changes:
Edit activity for hub mode
Introduce BaseCommunalViewModel
17 files changed, 493 insertions, 221 deletions
diff --git a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt index ddd1c67bd5fa..914e5f2c17bf 100644 --- a/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt +++ b/packages/SystemUI/compose/facade/disabled/src/com/android/systemui/compose/ComposeFacade.kt @@ -22,7 +22,7 @@ import android.view.View import android.view.WindowInsets import androidx.activity.ComponentActivity import androidx.lifecycle.LifecycleOwner -import com.android.systemui.communal.ui.viewmodel.CommunalViewModel +import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel import com.android.systemui.people.ui.viewmodel.PeopleViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.scene.shared.model.Scene @@ -47,6 +47,14 @@ object ComposeFacade : BaseComposeFacade { throwComposeUnavailableError() } + override fun setCommunalEditWidgetActivityContent( + activity: ComponentActivity, + viewModel: BaseCommunalViewModel, + onOpenWidgetPicker: () -> Unit, + ) { + throwComposeUnavailableError() + } + override fun createFooterActionsView( context: Context, viewModel: FooterActionsViewModel, @@ -67,12 +75,12 @@ object ComposeFacade : BaseComposeFacade { override fun createCommunalView( context: Context, - viewModel: CommunalViewModel, + viewModel: BaseCommunalViewModel, ): View { throwComposeUnavailableError() } - override fun createCommunalContainer(context: Context, viewModel: CommunalViewModel): View { + override fun createCommunalContainer(context: Context, viewModel: BaseCommunalViewModel): View { throwComposeUnavailableError() } diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt index eeda6c63b68f..59bd95bd9027 100644 --- a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/compose/ComposeFacade.kt @@ -32,7 +32,7 @@ import com.android.systemui.common.ui.compose.windowinsets.DisplayCutout import com.android.systemui.common.ui.compose.windowinsets.DisplayCutoutProvider import com.android.systemui.communal.ui.compose.CommunalContainer import com.android.systemui.communal.ui.compose.CommunalHub -import com.android.systemui.communal.ui.viewmodel.CommunalViewModel +import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel import com.android.systemui.people.ui.compose.PeopleScreen import com.android.systemui.people.ui.viewmodel.PeopleViewModel import com.android.systemui.qs.footer.ui.compose.FooterActions @@ -62,6 +62,21 @@ object ComposeFacade : BaseComposeFacade { activity.setContent { PlatformTheme { PeopleScreen(viewModel, onResult) } } } + override fun setCommunalEditWidgetActivityContent( + activity: ComponentActivity, + viewModel: BaseCommunalViewModel, + onOpenWidgetPicker: () -> Unit, + ) { + activity.setContent { + PlatformTheme { + CommunalHub( + viewModel = viewModel, + onOpenWidgetPicker = onOpenWidgetPicker, + ) + } + } + } + override fun createFooterActionsView( context: Context, viewModel: FooterActionsViewModel, @@ -98,14 +113,14 @@ object ComposeFacade : BaseComposeFacade { override fun createCommunalView( context: Context, - viewModel: CommunalViewModel, + viewModel: BaseCommunalViewModel, ): View { return ComposeView(context).apply { setContent { PlatformTheme { CommunalHub(viewModel = viewModel) } } } } - override fun createCommunalContainer(context: Context, viewModel: CommunalViewModel): View { + override fun createCommunalContainer(context: Context, viewModel: BaseCommunalViewModel): View { return ComposeView(context).apply { setContent { PlatformTheme { CommunalContainer(viewModel = viewModel) } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index 09706bed1921..ce84c19a53ee 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -32,7 +32,7 @@ import com.android.compose.animation.scene.Swipe import com.android.compose.animation.scene.SwipeDirection import com.android.compose.animation.scene.transitions import com.android.systemui.communal.shared.model.CommunalSceneKey -import com.android.systemui.communal.ui.viewmodel.CommunalViewModel +import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel import kotlinx.coroutines.flow.transform object Communal { @@ -59,7 +59,7 @@ val sceneTransitions = transitions { @Composable fun CommunalContainer( modifier: Modifier = Modifier, - viewModel: CommunalViewModel, + viewModel: BaseCommunalViewModel, ) { val currentScene: SceneKey by viewModel.currentScene @@ -129,7 +129,7 @@ private fun BlankScene( /** Scene containing the glanceable hub UI. */ @Composable private fun SceneScope.CommunalScene( - viewModel: CommunalViewModel, + viewModel: BaseCommunalViewModel, modifier: Modifier = Modifier, ) { Box(modifier.element(Communal.Elements.Content)) { CommunalHub(viewModel = viewModel) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index d822d1996eff..2ba1b77fb76e 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -34,6 +34,7 @@ import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Close +import androidx.compose.material.icons.filled.Edit import androidx.compose.material3.Card import androidx.compose.material3.Icon import androidx.compose.material3.IconButton @@ -51,7 +52,7 @@ import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import com.android.systemui.communal.domain.model.CommunalContentModel import com.android.systemui.communal.shared.model.CommunalContentSize -import com.android.systemui.communal.ui.viewmodel.CommunalViewModel +import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel import com.android.systemui.media.controls.ui.MediaHierarchyManager import com.android.systemui.media.controls.ui.MediaHostState import com.android.systemui.res.R @@ -59,7 +60,8 @@ import com.android.systemui.res.R @Composable fun CommunalHub( modifier: Modifier = Modifier, - viewModel: CommunalViewModel, + viewModel: BaseCommunalViewModel, + onOpenWidgetPicker: (() -> Unit)? = null, ) { val communalContent by viewModel.communalContent.collectAsState(initial = emptyList()) Box( @@ -81,7 +83,7 @@ fun CommunalHub( modifier = Modifier.fillMaxHeight().width(Dimensions.CardWidth), model = communalContent[index], viewModel = viewModel, - deleteOnClick = viewModel::onDeleteWidget, + deleteOnClick = if (viewModel.isEditMode) viewModel::onDeleteWidget else null, size = SizeF( Dimensions.CardWidth.value, @@ -90,8 +92,14 @@ fun CommunalHub( ) } } - IconButton(onClick = viewModel::onOpenWidgetEditor) { - Icon(Icons.Default.Add, stringResource(R.string.button_to_open_widget_editor)) + if (viewModel.isEditMode && onOpenWidgetPicker != null) { + IconButton(onClick = onOpenWidgetPicker) { + Icon(Icons.Default.Add, stringResource(R.string.hub_mode_add_widget_button_text)) + } + } else { + IconButton(onClick = viewModel::onOpenWidgetEditor) { + Icon(Icons.Default.Edit, stringResource(R.string.button_to_open_widget_editor)) + } } // This spacer covers the edge of the LazyHorizontalGrid and prevents it from receiving @@ -109,9 +117,9 @@ fun CommunalHub( @Composable private fun CommunalContent( model: CommunalContentModel, - viewModel: CommunalViewModel, + viewModel: BaseCommunalViewModel, size: SizeF, - deleteOnClick: (id: Int) -> Unit, + deleteOnClick: ((id: Int) -> Unit)?, modifier: Modifier = Modifier, ) { when (model) { @@ -126,19 +134,22 @@ private fun CommunalContent( private fun WidgetContent( model: CommunalContentModel.Widget, size: SizeF, - deleteOnClick: (id: Int) -> Unit, + deleteOnClick: ((id: Int) -> Unit)?, modifier: Modifier = Modifier, ) { // TODO(b/309009246): update background color Box( modifier = modifier.fillMaxSize().background(Color.White), ) { - IconButton(onClick = { deleteOnClick(model.appWidgetId) }) { - Icon( - Icons.Default.Close, - LocalContext.current.getString(R.string.button_to_remove_widget) - ) + if (deleteOnClick != null) { + IconButton(onClick = { deleteOnClick(model.appWidgetId) }) { + Icon( + Icons.Default.Close, + LocalContext.current.getString(R.string.button_to_remove_widget) + ) + } } + AndroidView( modifier = modifier, factory = { context -> @@ -171,7 +182,7 @@ private fun TutorialContent(modifier: Modifier = Modifier) { } @Composable -private fun Umo(viewModel: CommunalViewModel, modifier: Modifier = Modifier) { +private fun Umo(viewModel: BaseCommunalViewModel, modifier: Modifier = Modifier) { AndroidView( modifier = modifier, factory = { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt new file mode 100644 index 000000000000..ce7db80db7da --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt @@ -0,0 +1,126 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.view.viewmodel + +import android.app.smartspace.SmartspaceTarget +import android.provider.Settings +import android.widget.RemoteViews +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository +import com.android.systemui.communal.data.repository.FakeCommunalRepository +import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository +import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository +import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory +import com.android.systemui.communal.domain.model.CommunalContentModel +import com.android.systemui.communal.shared.model.CommunalWidgetContentModel +import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.media.controls.ui.MediaHost +import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidJUnit4::class) +class CommunalEditModeViewModelTest : SysuiTestCase() { + @Mock private lateinit var mediaHost: MediaHost + + private lateinit var testScope: TestScope + + private lateinit var keyguardRepository: FakeKeyguardRepository + private lateinit var communalRepository: FakeCommunalRepository + private lateinit var tutorialRepository: FakeCommunalTutorialRepository + private lateinit var widgetRepository: FakeCommunalWidgetRepository + private lateinit var smartspaceRepository: FakeSmartspaceRepository + private lateinit var mediaRepository: FakeCommunalMediaRepository + + private lateinit var underTest: CommunalEditModeViewModel + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + testScope = TestScope() + + val withDeps = CommunalInteractorFactory.create() + keyguardRepository = withDeps.keyguardRepository + communalRepository = withDeps.communalRepository + tutorialRepository = withDeps.tutorialRepository + widgetRepository = withDeps.widgetRepository + smartspaceRepository = withDeps.smartspaceRepository + mediaRepository = withDeps.mediaRepository + + underTest = + CommunalEditModeViewModel( + withDeps.communalInteractor, + mediaHost, + ) + } + + @Test + fun communalContent_onlyWidgetsAreShownInEditMode() = + testScope.runTest { + tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) + + // Widgets available. + val widgets = + listOf( + CommunalWidgetContentModel( + appWidgetId = 0, + priority = 30, + providerInfo = mock(), + ), + CommunalWidgetContentModel( + appWidgetId = 1, + priority = 20, + providerInfo = mock(), + ), + ) + widgetRepository.setCommunalWidgets(widgets) + + // Smartspace available. + val target = Mockito.mock(SmartspaceTarget::class.java) + whenever(target.smartspaceTargetId).thenReturn("target") + whenever(target.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER) + whenever(target.remoteViews).thenReturn(Mockito.mock(RemoteViews::class.java)) + smartspaceRepository.setLockscreenSmartspaceTargets(listOf(target)) + + // Media playing. + mediaRepository.mediaPlaying.value = true + + val communalContent by collectLastValue(underTest.communalContent) + + // Only Widgets are shown. + assertThat(communalContent?.size).isEqualTo(2) + assertThat(communalContent?.get(0)) + .isInstanceOf(CommunalContentModel.Widget::class.java) + assertThat(communalContent?.get(1)) + .isInstanceOf(CommunalContentModel.Widget::class.java) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt new file mode 100644 index 000000000000..32f4d075a873 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.view.viewmodel + +import android.app.smartspace.SmartspaceTarget +import android.provider.Settings +import android.widget.RemoteViews +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.communal.data.repository.FakeCommunalMediaRepository +import com.android.systemui.communal.data.repository.FakeCommunalRepository +import com.android.systemui.communal.data.repository.FakeCommunalTutorialRepository +import com.android.systemui.communal.data.repository.FakeCommunalWidgetRepository +import com.android.systemui.communal.domain.interactor.CommunalInteractorFactory +import com.android.systemui.communal.domain.model.CommunalContentModel +import com.android.systemui.communal.shared.model.CommunalWidgetContentModel +import com.android.systemui.communal.ui.viewmodel.CommunalViewModel +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.keyguard.data.repository.FakeKeyguardRepository +import com.android.systemui.media.controls.ui.MediaHost +import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidJUnit4::class) +class CommunalViewModelTest : SysuiTestCase() { + @Mock private lateinit var mediaHost: MediaHost + + private lateinit var testScope: TestScope + + private lateinit var keyguardRepository: FakeKeyguardRepository + private lateinit var communalRepository: FakeCommunalRepository + private lateinit var tutorialRepository: FakeCommunalTutorialRepository + private lateinit var widgetRepository: FakeCommunalWidgetRepository + private lateinit var smartspaceRepository: FakeSmartspaceRepository + private lateinit var mediaRepository: FakeCommunalMediaRepository + + private lateinit var underTest: CommunalViewModel + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + testScope = TestScope() + + val withDeps = CommunalInteractorFactory.create() + keyguardRepository = withDeps.keyguardRepository + communalRepository = withDeps.communalRepository + tutorialRepository = withDeps.tutorialRepository + widgetRepository = withDeps.widgetRepository + smartspaceRepository = withDeps.smartspaceRepository + mediaRepository = withDeps.mediaRepository + + underTest = + CommunalViewModel( + withDeps.communalInteractor, + withDeps.tutorialInteractor, + mediaHost, + ) + } + + @Test + fun tutorial_tutorialNotCompletedAndKeyguardVisible_showTutorialContent() = + testScope.runTest { + // Keyguard showing, and tutorial not started. + keyguardRepository.setKeyguardShowing(true) + keyguardRepository.setKeyguardOccluded(false) + tutorialRepository.setTutorialSettingState( + Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED + ) + + val communalContent by collectLastValue(underTest.communalContent) + + assertThat(communalContent!!).isNotEmpty() + communalContent!!.forEach { model -> + assertThat(model is CommunalContentModel.Tutorial).isTrue() + } + } + + @Test + fun ordering_smartspaceBeforeUmoBeforeWidgets() = + testScope.runTest { + tutorialRepository.setTutorialSettingState(Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED) + + // Widgets available. + val widgets = + listOf( + CommunalWidgetContentModel( + appWidgetId = 0, + priority = 30, + providerInfo = mock(), + ), + CommunalWidgetContentModel( + appWidgetId = 1, + priority = 20, + providerInfo = mock(), + ), + ) + widgetRepository.setCommunalWidgets(widgets) + + // Smartspace available. + val target = Mockito.mock(SmartspaceTarget::class.java) + whenever(target.smartspaceTargetId).thenReturn("target") + whenever(target.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER) + whenever(target.remoteViews).thenReturn(Mockito.mock(RemoteViews::class.java)) + smartspaceRepository.setLockscreenSmartspaceTargets(listOf(target)) + + // Media playing. + mediaRepository.mediaPlaying.value = true + + val communalContent by collectLastValue(underTest.communalContent) + + // Order is smart space, then UMO, then widget content. + assertThat(communalContent?.size).isEqualTo(4) + assertThat(communalContent?.get(0)) + .isInstanceOf(CommunalContentModel.Smartspace::class.java) + assertThat(communalContent?.get(1)).isInstanceOf(CommunalContentModel.Umo::class.java) + assertThat(communalContent?.get(2)) + .isInstanceOf(CommunalContentModel.Widget::class.java) + assertThat(communalContent?.get(3)) + .isInstanceOf(CommunalContentModel.Widget::class.java) + } +} diff --git a/packages/SystemUI/res/layout/edit_widgets.xml b/packages/SystemUI/res/layout/edit_widgets.xml deleted file mode 100644 index 182e651aa66d..000000000000 --- a/packages/SystemUI/res/layout/edit_widgets.xml +++ /dev/null @@ -1,32 +0,0 @@ -<!-- - ~ Copyright (C) 2023 The Android Open Source Project - ~ - ~ Licensed under the Apache License, Version 2.0 (the "License"); - ~ you may not use this file except in compliance with the License. - ~ You may obtain a copy of the License at - ~ - ~ http://www.apache.org/licenses/LICENSE-2.0 - ~ - ~ Unless required by applicable law or agreed to in writing, software - ~ distributed under the License is distributed on an "AS IS" BASIS, - ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - ~ See the License for the specific language governing permissions and - ~ limitations under the License. - --> - -<FrameLayout - xmlns:android="http://schemas.android.com/apk/res/android" - android:id="@+id/edit_widgets" - android:layout_width="match_parent" - android:layout_height="match_parent"> - - <Button - style="@android:Widget.DeviceDefault.Button.Colored" - android:id="@+id/add_widget" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_gravity="center" - android:textSize="28sp" - android:text="@string/hub_mode_add_widget_button_text"/> - -</FrameLayout> diff --git a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt index 7391a5e8e5fc..927bf024215d 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/domain/interactor/CommunalInteractor.kt @@ -32,7 +32,6 @@ import javax.inject.Inject import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.map @@ -47,7 +46,6 @@ constructor( private val widgetRepository: CommunalWidgetRepository, mediaRepository: CommunalMediaRepository, smartspaceRepository: SmartspaceRepository, - tutorialInteractor: CommunalTutorialInteractor, private val appWidgetHost: AppWidgetHost, private val editWidgetsActivityStarter: EditWidgetsActivityStarter ) { @@ -86,20 +84,8 @@ constructor( /** Delete a widget by id. */ fun deleteWidget(id: Int) = widgetRepository.deleteWidget(id) - /** A list of all the communal content to be displayed in the communal hub. */ - @OptIn(ExperimentalCoroutinesApi::class) - val communalContent: Flow<List<CommunalContentModel>> = - tutorialInteractor.isTutorialAvailable.flatMapLatest { isTutorialMode -> - if (isTutorialMode) { - return@flatMapLatest flowOf(tutorialContent) - } - combine(smartspaceContent, umoContent, widgetContent) { smartspace, umo, widgets -> - smartspace + umo + widgets - } - } - /** A list of widget content to be displayed in the communal hub. */ - private val widgetContent: Flow<List<CommunalContentModel.Widget>> = + val widgetContent: Flow<List<CommunalContentModel.Widget>> = widgetRepository.communalWidgets.map { widgets -> widgets.map Widget@{ widget -> return@Widget CommunalContentModel.Widget( @@ -111,7 +97,7 @@ constructor( } /** A flow of available smartspace content. Currently only showing timer targets. */ - private val smartspaceContent: Flow<List<CommunalContentModel.Smartspace>> = + val smartspaceContent: Flow<List<CommunalContentModel.Smartspace>> = if (!smartspaceRepository.isSmartspaceRemoteViewsEnabled) { flowOf(emptyList()) } else { @@ -133,7 +119,7 @@ constructor( } /** A list of tutorial content to be displayed in the communal hub in tutorial mode. */ - private val tutorialContent: List<CommunalContentModel.Tutorial> = + val tutorialContent: List<CommunalContentModel.Tutorial> = listOf( CommunalContentModel.Tutorial(id = 0, CommunalContentSize.FULL), CommunalContentModel.Tutorial(id = 1, CommunalContentSize.THIRD), @@ -145,7 +131,7 @@ constructor( CommunalContentModel.Tutorial(id = 7, CommunalContentSize.HALF), ) - private val umoContent: Flow<List<CommunalContentModel.Umo>> = + val umoContent: Flow<List<CommunalContentModel.Umo>> = mediaRepository.mediaPlaying.flatMapLatest { mediaPlaying -> if (mediaPlaying) { // TODO(b/310254801): support HALF and FULL layouts diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt new file mode 100644 index 000000000000..98f3594801f3 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/BaseCommunalViewModel.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.ui.viewmodel + +import com.android.systemui.communal.domain.interactor.CommunalInteractor +import com.android.systemui.communal.domain.model.CommunalContentModel +import com.android.systemui.communal.shared.model.CommunalSceneKey +import com.android.systemui.media.controls.ui.MediaHost +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.StateFlow + +/** The base view model for the communal hub. */ +abstract class BaseCommunalViewModel( + private val communalInteractor: CommunalInteractor, + val mediaHost: MediaHost, +) { + val currentScene: StateFlow<CommunalSceneKey> = communalInteractor.desiredScene + + fun onSceneChanged(scene: CommunalSceneKey) { + communalInteractor.onSceneChanged(scene) + } + + /** A list of all the communal content to be displayed in the communal hub. */ + abstract val communalContent: Flow<List<CommunalContentModel>> + + /** Whether in edit mode for the communal hub. */ + open val isEditMode = false + + /** Called as the UI requests deleting a widget. */ + open fun onDeleteWidget(id: Int) {} + + /** Called as the UI requests opening the widget editor. */ + open fun onOpenWidgetEditor() {} +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt new file mode 100644 index 000000000000..14d9b2ca80f0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2023 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.communal.ui.viewmodel + +import com.android.systemui.communal.domain.interactor.CommunalInteractor +import com.android.systemui.communal.domain.model.CommunalContentModel +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.media.controls.ui.MediaHost +import com.android.systemui.media.dagger.MediaModule +import javax.inject.Inject +import javax.inject.Named +import kotlinx.coroutines.flow.Flow + +/** The view model for communal hub in edit mode. */ +@SysUISingleton +class CommunalEditModeViewModel +@Inject +constructor( + private val communalInteractor: CommunalInteractor, + @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost, +) : BaseCommunalViewModel(communalInteractor, mediaHost) { + + override val isEditMode = true + + // Only widgets are editable. + override val communalContent: Flow<List<CommunalContentModel>> = + communalInteractor.widgetContent + + override fun onDeleteWidget(id: Int) = communalInteractor.deleteWidget(id) +} diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt index 14edc8e0a88c..eaf2d5741e23 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalViewModel.kt @@ -17,34 +17,43 @@ package com.android.systemui.communal.ui.viewmodel import com.android.systemui.communal.domain.interactor.CommunalInteractor +import com.android.systemui.communal.domain.interactor.CommunalTutorialInteractor import com.android.systemui.communal.domain.model.CommunalContentModel -import com.android.systemui.communal.shared.model.CommunalSceneKey import com.android.systemui.dagger.SysUISingleton import com.android.systemui.media.controls.ui.MediaHost import com.android.systemui.media.dagger.MediaModule import javax.inject.Inject import javax.inject.Named +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +/** The default view model used for showing the communal hub. */ @SysUISingleton class CommunalViewModel @Inject constructor( private val communalInteractor: CommunalInteractor, - @Named(MediaModule.COMMUNAL_HUB) val mediaHost: MediaHost, -) { - val currentScene: StateFlow<CommunalSceneKey> = communalInteractor.desiredScene - fun onSceneChanged(scene: CommunalSceneKey) { - communalInteractor.onSceneChanged(scene) - } + tutorialInteractor: CommunalTutorialInteractor, + @Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost, +) : BaseCommunalViewModel(communalInteractor, mediaHost) { - /** A list of all the communal content to be displayed in the communal hub. */ - val communalContent: Flow<List<CommunalContentModel>> = communalInteractor.communalContent + @OptIn(ExperimentalCoroutinesApi::class) + override val communalContent: Flow<List<CommunalContentModel>> = + tutorialInteractor.isTutorialAvailable.flatMapLatest { isTutorialMode -> + if (isTutorialMode) { + return@flatMapLatest flowOf(communalInteractor.tutorialContent) + } + combine( + communalInteractor.smartspaceContent, + communalInteractor.umoContent, + communalInteractor.widgetContent, + ) { smartspace, umo, widgets -> + smartspace + umo + widgets + } + } - /** Delete a widget by id. */ - fun onDeleteWidget(id: Int) = communalInteractor.deleteWidget(id) - - /** Open the widget editor */ - fun onOpenWidgetEditor() = communalInteractor.showWidgetEditor() + override fun onOpenWidgetEditor() = communalInteractor.showWidgetEditor() } diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt index 78e85db9ea05..7b94fc182fe2 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt @@ -20,17 +20,21 @@ import android.appwidget.AppWidgetProviderInfo import android.content.Intent import android.os.Bundle import android.util.Log -import android.view.View import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult import com.android.systemui.communal.domain.interactor.CommunalInteractor -import com.android.systemui.res.R +import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel +import com.android.systemui.compose.ComposeFacade.setCommunalEditWidgetActivityContent import javax.inject.Inject /** An Activity for editing the widgets that appear in hub mode. */ -class EditWidgetsActivity @Inject constructor(private val communalInteractor: CommunalInteractor) : - ComponentActivity() { +class EditWidgetsActivity +@Inject +constructor( + private val communalViewModel: CommunalEditModeViewModel, + private val communalInteractor: CommunalInteractor, +) : ComponentActivity() { companion object { /** * Intent extra name for the {@link AppWidgetProviderInfo} of a widget to add to hub mode. @@ -59,20 +63,19 @@ class EditWidgetsActivity @Inject constructor(private val communalInteractor: Co "Failed to receive result from widget picker, code=${result.resultCode}" ) } - this@EditWidgetsActivity.finish() } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setShowWhenLocked(true) - setContentView(R.layout.edit_widgets) - - val addWidgetsButton = findViewById<View>(R.id.add_widget) - addWidgetsButton?.setOnClickListener({ - addWidgetActivityLauncher.launch( - Intent(applicationContext, WidgetPickerActivity::class.java) - ) - }) + setCommunalEditWidgetActivityContent( + activity = this, + viewModel = communalViewModel, + onOpenWidgetPicker = { + addWidgetActivityLauncher.launch( + Intent(applicationContext, WidgetPickerActivity::class.java) + ) + }, + ) } } diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetPickerActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetPickerActivity.kt index 3e6dbd5a7115..a2765486bf2d 100644 --- a/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetPickerActivity.kt +++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/WidgetPickerActivity.kt @@ -43,7 +43,6 @@ constructor( super.onCreate(savedInstanceState) setContentView(R.layout.widget_picker) - setShowWhenLocked(true) loadWidgets() } diff --git a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt index 4bdea75d9d71..65d44957222a 100644 --- a/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt +++ b/packages/SystemUI/src/com/android/systemui/compose/BaseComposeFacade.kt @@ -22,7 +22,7 @@ import android.view.View import android.view.WindowInsets import androidx.activity.ComponentActivity import androidx.lifecycle.LifecycleOwner -import com.android.systemui.communal.ui.viewmodel.CommunalViewModel +import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel import com.android.systemui.people.ui.viewmodel.PeopleViewModel import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel import com.android.systemui.scene.shared.model.Scene @@ -58,6 +58,13 @@ interface BaseComposeFacade { onResult: (PeopleViewModel.Result) -> Unit, ) + /** Bind the content of [activity] to [viewModel]. */ + fun setCommunalEditWidgetActivityContent( + activity: ComponentActivity, + viewModel: BaseCommunalViewModel, + onOpenWidgetPicker: () -> Unit, + ) + /** Create a [View] to represent [viewModel] on screen. */ fun createFooterActionsView( context: Context, @@ -77,9 +84,9 @@ interface BaseComposeFacade { /** Create a [View] to represent [viewModel] on screen. */ fun createCommunalView( context: Context, - viewModel: CommunalViewModel, + viewModel: BaseCommunalViewModel, ): View /** Creates a container that hosts the communal UI and handles gesture transitions. */ - fun createCommunalContainer(context: Context, viewModel: CommunalViewModel): View + fun createCommunalContainer(context: Context, viewModel: BaseCommunalViewModel): View } diff --git a/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt index e0567a4c6de5..16cfa2398fd5 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/communal/domain/interactor/CommunalInteractorTest.kt @@ -18,7 +18,6 @@ package com.android.systemui.communal.domain.interactor import android.app.smartspace.SmartspaceTarget -import android.provider.Settings import android.provider.Settings.Secure.HUB_MODE_TUTORIAL_COMPLETED import android.widget.RemoteViews import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -99,24 +98,6 @@ class CommunalInteractorTest : SysuiTestCase() { } @Test - fun tutorial_tutorialNotCompletedAndKeyguardVisible_showTutorialContent() = - testScope.runTest { - // Keyguard showing, and tutorial not started. - keyguardRepository.setKeyguardShowing(true) - keyguardRepository.setKeyguardOccluded(false) - tutorialRepository.setTutorialSettingState( - Settings.Secure.HUB_MODE_TUTORIAL_NOT_STARTED - ) - - val communalContent by collectLastValue(underTest.communalContent) - - assertThat(communalContent!!).isNotEmpty() - communalContent!!.forEach { model -> - assertThat(model is CommunalContentModel.Tutorial).isTrue() - } - } - - @Test fun widget_tutorialCompletedAndWidgetsAvailable_showWidgetContent() = testScope.runTest { // Keyguard showing, and tutorial completed. @@ -145,12 +126,11 @@ class CommunalInteractorTest : SysuiTestCase() { ) widgetRepository.setCommunalWidgets(widgets) - val communalContent by collectLastValue(underTest.communalContent) + val widgetContent by collectLastValue(underTest.widgetContent) - assertThat(communalContent!!).isNotEmpty() - communalContent!!.forEachIndexed { index, model -> - assertThat((model as CommunalContentModel.Widget).appWidgetId) - .isEqualTo(widgets[index].appWidgetId) + assertThat(widgetContent!!).isNotEmpty() + widgetContent!!.forEachIndexed { index, model -> + assertThat(model.appWidgetId).isEqualTo(widgets[index].appWidgetId) } } @@ -183,48 +163,9 @@ class CommunalInteractorTest : SysuiTestCase() { val targets = listOf(target1, target2, target3) smartspaceRepository.setLockscreenSmartspaceTargets(targets) - val communalContent by collectLastValue(underTest.communalContent) - assertThat(communalContent?.size).isEqualTo(1) - assertThat(communalContent?.get(0)?.key).isEqualTo("smartspace_target3") - } - - @Test - fun smartspace_smartspaceAndWidgetsAvailable_showSmartspaceAndWidgetContent() = - testScope.runTest { - // Keyguard showing, and tutorial completed. - keyguardRepository.setKeyguardShowing(true) - keyguardRepository.setKeyguardOccluded(false) - tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) - - // Widgets available. - val widgets = - listOf( - CommunalWidgetContentModel( - appWidgetId = 0, - priority = 30, - providerInfo = mock(), - ), - CommunalWidgetContentModel( - appWidgetId = 1, - priority = 20, - providerInfo = mock(), - ), - ) - widgetRepository.setCommunalWidgets(widgets) - - // Smartspace available. - val target = mock(SmartspaceTarget::class.java) - whenever(target.smartspaceTargetId).thenReturn("target") - whenever(target.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER) - whenever(target.remoteViews).thenReturn(mock(RemoteViews::class.java)) - smartspaceRepository.setLockscreenSmartspaceTargets(listOf(target)) - - val communalContent by collectLastValue(underTest.communalContent) - - assertThat(communalContent?.size).isEqualTo(3) - assertThat(communalContent?.get(0)?.key).isEqualTo("smartspace_target") - assertThat(communalContent?.get(1)?.key).isEqualTo("widget_0") - assertThat(communalContent?.get(2)?.key).isEqualTo("widget_1") + val smartspaceContent by collectLastValue(underTest.smartspaceContent) + assertThat(smartspaceContent?.size).isEqualTo(1) + assertThat(smartspaceContent?.get(0)?.key).isEqualTo("smartspace_target3") } @Test @@ -236,55 +177,11 @@ class CommunalInteractorTest : SysuiTestCase() { // Media is playing. mediaRepository.mediaPlaying.value = true - val communalContent by collectLastValue(underTest.communalContent) - - assertThat(communalContent?.size).isEqualTo(1) - assertThat(communalContent?.get(0)).isInstanceOf(CommunalContentModel.Umo::class.java) - assertThat(communalContent?.get(0)?.key).isEqualTo(CommunalContentModel.UMO_KEY) - } - - @Test - fun ordering_smartspaceBeforeUmoBeforeWidgets() = - testScope.runTest { - tutorialRepository.setTutorialSettingState(HUB_MODE_TUTORIAL_COMPLETED) - - // Widgets available. - val widgets = - listOf( - CommunalWidgetContentModel( - appWidgetId = 0, - priority = 30, - providerInfo = mock(), - ), - CommunalWidgetContentModel( - appWidgetId = 1, - priority = 20, - providerInfo = mock(), - ), - ) - widgetRepository.setCommunalWidgets(widgets) - - // Smartspace available. - val target = mock(SmartspaceTarget::class.java) - whenever(target.smartspaceTargetId).thenReturn("target") - whenever(target.featureType).thenReturn(SmartspaceTarget.FEATURE_TIMER) - whenever(target.remoteViews).thenReturn(mock(RemoteViews::class.java)) - smartspaceRepository.setLockscreenSmartspaceTargets(listOf(target)) - - // Media playing. - mediaRepository.mediaPlaying.value = true + val umoContent by collectLastValue(underTest.umoContent) - val communalContent by collectLastValue(underTest.communalContent) - - // Order is smart space, then UMO, then widget content. - assertThat(communalContent?.size).isEqualTo(4) - assertThat(communalContent?.get(0)) - .isInstanceOf(CommunalContentModel.Smartspace::class.java) - assertThat(communalContent?.get(1)).isInstanceOf(CommunalContentModel.Umo::class.java) - assertThat(communalContent?.get(2)) - .isInstanceOf(CommunalContentModel.Widget::class.java) - assertThat(communalContent?.get(3)) - .isInstanceOf(CommunalContentModel.Widget::class.java) + assertThat(umoContent?.size).isEqualTo(1) + assertThat(umoContent?.get(0)).isInstanceOf(CommunalContentModel.Umo::class.java) + assertThat(umoContent?.get(0)?.key).isEqualTo(CommunalContentModel.UMO_KEY) } @Test diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt index 3aee889d55f4..b27926c8ba50 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/domain/interactor/CommunalInteractorFactory.kt @@ -65,7 +65,6 @@ object CommunalInteractorFactory { widgetRepository, mediaRepository, smartspaceRepository, - withDeps.communalTutorialInteractor, appWidgetHost, editWidgetsActivityStarter, ), diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt index d3744d55c55d..4068e408f0bd 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt @@ -167,6 +167,10 @@ class FakeKeyguardRepository @Inject constructor() : KeyguardRepository { _isKeyguardOccluded.value = isOccluded } + fun setKeyguardUnlocked(isUnlocked: Boolean) { + _isKeyguardUnlocked.value = isUnlocked + } + override fun setIsDozing(isDozing: Boolean) { _isDozing.value = isDozing } |