From fc92ee109b2c25796fe618c7674bfe3ed313c442 Mon Sep 17 00:00:00 2001 From: Will Leshner Date: Thu, 21 Nov 2024 13:02:45 -0800 Subject: Add a button to glanceable hub to swap to dream. Bug: 378174118 Test: atest CommunalViewModelTest Test: Manually by tapping button and verifying the swap to dream. Flag: com.android.systemui.glanceable_hub_v2 Change-Id: I63776346607d2b7f1666440aaaa8d7c9a4b043c4 --- .../communal/ui/compose/CommunalContent.kt | 33 ++++++- .../section/CommunalToDreamButtonSection.kt | 65 +++++++++++++ .../CommunalToDreamButtonViewModelTest.kt | 104 +++++++++++++++++++++ .../SystemUI/res/drawable/ic_screensaver_auto.xml | 26 ++++++ packages/SystemUI/res/values/strings.xml | 2 + .../ui/viewmodel/CommunalToDreamButtonViewModel.kt | 78 ++++++++++++++++ .../CommunalToDreamButtonViewModelKosmos.kt | 31 ++++++ 7 files changed, 336 insertions(+), 3 deletions(-) create mode 100644 packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalToDreamButtonSection.kt create mode 100644 packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelTest.kt create mode 100644 packages/SystemUI/res/drawable/ic_screensaver_auto.xml create mode 100644 packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModel.kt create mode 100644 packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelKosmos.kt diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt index 3926b326dacd..a17a1d46554f 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContent.kt @@ -23,12 +23,17 @@ import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.layout.Layout +import androidx.compose.ui.layout.Measurable +import androidx.compose.ui.unit.Constraints +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntRect +import androidx.compose.ui.unit.dp import androidx.compose.ui.zIndex import com.android.compose.animation.scene.SceneScope 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.view.layout.sections.CommunalAppWidgetSection import com.android.systemui.communal.ui.viewmodel.CommunalViewModel import com.android.systemui.keyguard.ui.composable.blueprint.BlueprintAlignmentLines @@ -49,6 +54,7 @@ constructor( private val ambientStatusBarSection: AmbientStatusBarSection, private val communalPopupSection: CommunalPopupSection, private val widgetSection: CommunalAppWidgetSection, + private val communalToDreamButtonSection: CommunalToDreamButtonSection, ) { @Composable @@ -82,11 +88,13 @@ constructor( Modifier.element(Communal.Elements.IndicationArea).fillMaxWidth() ) } + with(communalToDreamButtonSection) { Button() } }, ) { measurables, constraints -> val communalGridMeasurable = measurables[0] val lockIconMeasurable = measurables[1] val bottomAreaMeasurable = measurables[2] + val screensaverButtonMeasurable: Measurable? = measurables.getOrNull(3) val noMinConstraints = constraints.copy(minWidth = 0, minHeight = 0) @@ -101,6 +109,15 @@ constructor( val bottomAreaPlaceable = bottomAreaMeasurable.measure(noMinConstraints) + val screensaverButtonSizeInt = screensaverButtonSize.roundToPx() + val screensaverButtonPlaceable = + screensaverButtonMeasurable?.measure( + Constraints.fixed( + width = screensaverButtonSizeInt, + height = screensaverButtonSizeInt, + ) + ) + val communalGridPlaceable = communalGridMeasurable.measure( noMinConstraints.copy(maxHeight = lockIconBounds.top) @@ -109,12 +126,22 @@ constructor( layout(constraints.maxWidth, constraints.maxHeight) { communalGridPlaceable.place(x = 0, y = 0) lockIconPlaceable.place(x = lockIconBounds.left, y = lockIconBounds.top) - bottomAreaPlaceable.place( - x = 0, - y = constraints.maxHeight - bottomAreaPlaceable.height, + + val bottomAreaTop = constraints.maxHeight - bottomAreaPlaceable.height + bottomAreaPlaceable.place(x = 0, y = bottomAreaTop) + screensaverButtonPlaceable?.place( + x = + constraints.maxWidth - + screensaverButtonSizeInt - + Dimensions.ItemSpacing.roundToPx(), + y = lockIconBounds.top, ) } } } } + + companion object { + val screensaverButtonSize: Dp = 64.dp + } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalToDreamButtonSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalToDreamButtonSection.kt new file mode 100644 index 000000000000..9421596f7116 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/section/CommunalToDreamButtonSection.kt @@ -0,0 +1,65 @@ +/* + * Copyright (C) 2024 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.material3.IconButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.ui.res.stringResource +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.android.compose.PlatformIconButton +import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor +import com.android.systemui.communal.ui.viewmodel.CommunalToDreamButtonViewModel +import com.android.systemui.lifecycle.rememberViewModel +import com.android.systemui.res.R +import javax.inject.Inject + +class CommunalToDreamButtonSection +@Inject +constructor( + private val communalSettingsInteractor: CommunalSettingsInteractor, + private val viewModelFactory: CommunalToDreamButtonViewModel.Factory, +) { + @Composable + fun Button() { + if (!communalSettingsInteractor.isV2FlagEnabled()) { + return + } + + val viewModel = + rememberViewModel("CommunalToDreamButtonSection") { viewModelFactory.create() } + val shouldShowDreamButtonOnHub by + viewModel.shouldShowDreamButtonOnHub.collectAsStateWithLifecycle(false) + + if (!shouldShowDreamButtonOnHub) { + return + } + + PlatformIconButton( + onClick = { viewModel.onShowDreamButtonTap() }, + iconResource = R.drawable.ic_screensaver_auto, + contentDescription = + stringResource(R.string.accessibility_glanceable_hub_to_dream_button), + colors = + IconButtonDefaults.filledIconButtonColors( + contentColor = MaterialTheme.colorScheme.onPrimaryContainer, + containerColor = MaterialTheme.colorScheme.primaryContainer, + ), + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelTest.kt new file mode 100644 index 000000000000..88206850eb60 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelTest.kt @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2024 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 android.platform.test.annotations.EnableFlags +import android.service.dream.dreamManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_GLANCEABLE_HUB_V2 +import com.android.systemui.SysuiTestCase +import com.android.systemui.flags.Flags.COMMUNAL_SERVICE_ENABLED +import com.android.systemui.flags.fakeFeatureFlagsClassic +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runCurrent +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.testScope +import com.android.systemui.lifecycle.activateIn +import com.android.systemui.statusbar.policy.batteryController +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.verify +import org.mockito.kotlin.any +import org.mockito.kotlin.whenever + +@SmallTest +@EnableFlags(FLAG_GLANCEABLE_HUB_V2) +@RunWith(AndroidJUnit4::class) +class CommunalToDreamButtonViewModelTest : SysuiTestCase() { + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val underTest: CommunalToDreamButtonViewModel by lazy { + kosmos.communalToDreamButtonViewModel + } + + @Before + fun setUp() { + kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true) + underTest.activateIn(testScope) + } + + @Test + fun shouldShowDreamButtonOnHub_trueWhenCanDream() = + with(kosmos) { + runTest { + whenever(dreamManager.canStartDreaming(any())).thenReturn(true) + whenever(batteryController.isPluggedIn()).thenReturn(true) + + val shouldShowButton by collectLastValue(underTest.shouldShowDreamButtonOnHub) + assertThat(shouldShowButton).isTrue() + } + } + + @Test + fun shouldShowDreamButtonOnHub_falseWhenCannotDream() = + with(kosmos) { + runTest { + whenever(dreamManager.canStartDreaming(any())).thenReturn(false) + whenever(batteryController.isPluggedIn()).thenReturn(true) + + val shouldShowButton by collectLastValue(underTest.shouldShowDreamButtonOnHub) + assertThat(shouldShowButton).isFalse() + } + } + + @Test + fun shouldShowDreamButtonOnHub_falseWhenNotPluggedIn() = + with(kosmos) { + runTest { + whenever(dreamManager.canStartDreaming(any())).thenReturn(true) + whenever(batteryController.isPluggedIn()).thenReturn(false) + + val shouldShowButton by collectLastValue(underTest.shouldShowDreamButtonOnHub) + assertThat(shouldShowButton).isFalse() + } + } + + @Test + fun onShowDreamButtonTap_startsDream() = + with(kosmos) { + runTest { + underTest.onShowDreamButtonTap() + runCurrent() + + verify(dreamManager).startDream() + } + } +} diff --git a/packages/SystemUI/res/drawable/ic_screensaver_auto.xml b/packages/SystemUI/res/drawable/ic_screensaver_auto.xml new file mode 100644 index 000000000000..7cccff624c6e --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_screensaver_auto.xml @@ -0,0 +1,26 @@ + + + + + diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 658f2c27b4cb..49ac145d33cc 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -1342,6 +1342,8 @@ To add the \"Widgets\" shortcut, make sure \"Show widgets on lock screen\" is enabled in settings. Settings + + Show screensaver button diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModel.kt new file mode 100644 index 000000000000..7d5b196dfaa8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModel.kt @@ -0,0 +1,78 @@ +/* + * Copyright (C) 2024 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 android.annotation.SuppressLint +import android.app.DreamManager +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.lifecycle.ExclusiveActivatable +import com.android.systemui.statusbar.policy.BatteryController +import com.android.systemui.util.kotlin.isDevicePluggedIn +import dagger.assisted.AssistedFactory +import dagger.assisted.AssistedInject +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.awaitCancellation +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.coroutineScope +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.receiveAsFlow +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +class CommunalToDreamButtonViewModel +@AssistedInject +constructor( + @Background private val backgroundContext: CoroutineContext, + batteryController: BatteryController, + private val dreamManager: DreamManager, +) : ExclusiveActivatable() { + + private val _requests = Channel(Channel.BUFFERED) + + /** Whether we should show a button on hub to switch to dream. */ + @SuppressLint("MissingPermission") + val shouldShowDreamButtonOnHub = + batteryController + .isDevicePluggedIn() + .distinctUntilChanged() + .map { isPluggedIn -> isPluggedIn && dreamManager.canStartDreaming(true) } + .flowOn(backgroundContext) + + /** Handle a tap on the "show dream" button. */ + fun onShowDreamButtonTap() { + _requests.trySend(Unit) + } + + @SuppressLint("MissingPermission") + override suspend fun onActivated(): Nothing = coroutineScope { + launch { + _requests.receiveAsFlow().collectLatest { + withContext(backgroundContext) { dreamManager.startDream() } + } + } + + awaitCancellation() + } + + @AssistedFactory + interface Factory { + fun create(): CommunalToDreamButtonViewModel + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelKosmos.kt new file mode 100644 index 000000000000..b407b1ba227a --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/communal/ui/viewmodel/CommunalToDreamButtonViewModelKosmos.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2024 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 android.service.dream.dreamManager +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.statusbar.policy.batteryController + +val Kosmos.communalToDreamButtonViewModel by + Kosmos.Fixture { + CommunalToDreamButtonViewModel( + backgroundContext = testDispatcher, + batteryController = batteryController, + dreamManager = dreamManager, + ) + } -- cgit v1.2.3-59-g8ed1b