Loading packages/SystemUI/compose/facade/disabled/src/com/android/systemui/volume/panel/component/mediaoutput/MediaOutputModule.kt 0 → 100644 +21 −0 Original line number Diff line number Diff line /* * 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.volume.panel.component.mediaoutput import dagger.Module @Module interface MediaOutputModule packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/MediaOutputModule.kt 0 → 100644 +44 −0 Original line number Diff line number Diff line /* * 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.volume.panel.component.mediaoutput import com.android.systemui.volume.panel.component.mediaoutput.domain.MediaOutputAvailabilityCriteria import com.android.systemui.volume.panel.component.mediaoutput.ui.composable.MediaOutputComponent import com.android.systemui.volume.panel.component.shared.model.VolumePanelComponents import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria import com.android.systemui.volume.panel.shared.model.VolumePanelUiComponent import dagger.Binds import dagger.Module import dagger.multibindings.IntoMap import dagger.multibindings.StringKey /** Dagger module, that provides media output Volume Panel UI functionality. */ @Module interface MediaOutputModule { @Binds @IntoMap @StringKey(VolumePanelComponents.MEDIA_OUTPUT) fun bindVolumePanelUiComponent(component: MediaOutputComponent): VolumePanelUiComponent @Binds @IntoMap @StringKey(VolumePanelComponents.MEDIA_OUTPUT) fun bindComponentAvailabilityCriteria( criteria: MediaOutputAvailabilityCriteria ): ComponentAvailabilityCriteria } packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt 0 → 100644 +185 −0 Original line number Diff line number Diff line /* * 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.volume.panel.component.mediaoutput.ui.composable import androidx.compose.animation.AnimatedContent import androidx.compose.animation.core.snap import androidx.compose.animation.core.tween import androidx.compose.animation.core.updateTransition import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.android.compose.animation.Expandable import com.android.systemui.common.ui.compose.Icon import com.android.systemui.common.ui.compose.toColor import com.android.systemui.volume.panel.component.mediaoutput.ui.viewmodel.ConnectedDeviceViewModel import com.android.systemui.volume.panel.component.mediaoutput.ui.viewmodel.DeviceIconViewModel import com.android.systemui.volume.panel.component.mediaoutput.ui.viewmodel.MediaOutputViewModel import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope import com.android.systemui.volume.panel.ui.composable.ComposeVolumePanelUiComponent import com.android.systemui.volume.panel.ui.composable.VolumePanelComposeScope import javax.inject.Inject @VolumePanelScope class MediaOutputComponent @Inject constructor( private val viewModel: MediaOutputViewModel, ) : ComposeVolumePanelUiComponent { @Composable override fun VolumePanelComposeScope.Content(modifier: Modifier) { val connectedDeviceViewModel: ConnectedDeviceViewModel? by viewModel.connectedDeviceViewModel.collectAsState() val deviceIconViewModel: DeviceIconViewModel? by viewModel.deviceIconViewModel.collectAsState() Expandable( modifier = Modifier.fillMaxWidth().height(80.dp), color = MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(28.dp), onClick = { viewModel.onBarClick(it) }, ) { Row { connectedDeviceViewModel?.let { ConnectedDeviceText(it) } deviceIconViewModel?.let { ConnectedDeviceIcon(it) } } } } @Composable private fun RowScope.ConnectedDeviceText(connectedDeviceViewModel: ConnectedDeviceViewModel) { Column( modifier = Modifier.weight(1f) .padding(start = 24.dp, top = 20.dp, bottom = 20.dp) .fillMaxHeight(), verticalArrangement = Arrangement.spacedBy(4.dp), ) { Text( connectedDeviceViewModel.label.toString(), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis, ) connectedDeviceViewModel.deviceName?.let { Text( it.toString(), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface, maxLines = 1, overflow = TextOverflow.Ellipsis, ) } } } @Composable private fun ConnectedDeviceIcon(deviceIconViewModel: DeviceIconViewModel) { val transition = updateTransition(deviceIconViewModel, label = "MediaOutputIconTransition") Box( modifier = Modifier.padding(16.dp).fillMaxHeight().aspectRatio(1f), contentAlignment = Alignment.Center ) { transition.AnimatedContent( contentKey = { it.backgroundColor }, transitionSpec = { if (targetState is DeviceIconViewModel.IsPlaying) { scaleIn( initialScale = 0.9f, animationSpec = isPlayingInIconBackgroundSpec(), ) + fadeIn(animationSpec = isPlayingInIconBackgroundSpec()) togetherWith fadeOut(animationSpec = snap()) } else { fadeIn(animationSpec = snap(delayMillis = 900)) togetherWith scaleOut( targetScale = 0.9f, animationSpec = isPlayingOutSpec(), ) + fadeOut(animationSpec = isPlayingOutSpec()) } } ) { targetViewModel -> Expandable( modifier = Modifier.fillMaxSize(), color = targetViewModel.backgroundColor.toColor(), shape = RoundedCornerShape(12.dp), onClick = { viewModel.onDeviceClick(it) }, ) {} } transition.AnimatedContent( contentKey = { it.icon }, transitionSpec = { if (targetState is DeviceIconViewModel.IsPlaying) { fadeIn(animationSpec = snap(delayMillis = 700)) togetherWith slideOutVertically( targetOffsetY = { it }, animationSpec = isPlayingInIconSpec(), ) + fadeOut(animationSpec = isNotPlayingOutIconSpec()) } else { slideInVertically( initialOffsetY = { it }, animationSpec = isNotPlayingInIconSpec(), ) + fadeIn(animationSpec = isNotPlayingInIconSpec()) togetherWith fadeOut(animationSpec = isPlayingOutSpec()) } } ) { Icon( icon = it.icon, modifier = Modifier.padding(12.dp).fillMaxSize(), ) } } } } private fun <T> isPlayingOutSpec() = tween<T>(durationMillis = 400, delayMillis = 500) private fun <T> isPlayingInIconSpec() = tween<T>(durationMillis = 400, delayMillis = 300) private fun <T> isPlayingInIconBackgroundSpec() = tween<T>(durationMillis = 400, delayMillis = 700) private fun <T> isNotPlayingOutIconSpec() = tween<T>(durationMillis = 400, delayMillis = 300) private fun <T> isNotPlayingInIconSpec() = tween<T>(durationMillis = 400, delayMillis = 900) packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt +14 −4 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package com.android.systemui.volume.panel.ui.composable import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row Loading @@ -33,9 +34,16 @@ fun VolumePanelComposeScope.VerticalVolumePanelContent( modifier: Modifier = Modifier, ) { Column( modifier = modifier, modifier = modifier.animateContentSize(), verticalArrangement = Arrangement.spacedBy(20.dp), ) { for (component in layout.headerComponents) { AnimatedVisibility(component.isVisible) { with(component.component as ComposeVolumePanelUiComponent) { Content(Modifier.weight(1f)) } } } for (component in layout.contentComponents) { AnimatedVisibility(component.isVisible) { with(component.component as ComposeVolumePanelUiComponent) { Content(Modifier) } Loading @@ -44,9 +52,10 @@ fun VolumePanelComposeScope.VerticalVolumePanelContent( if (layout.footerComponents.isNotEmpty()) { Row( modifier = Modifier.fillMaxWidth().wrapContentHeight(), horizontalArrangement = Arrangement.spacedBy(20.dp) horizontalArrangement = Arrangement.spacedBy(20.dp), ) { for (component in layout.footerComponents) { AnimatedVisibility(component.isVisible) { with(component.component as ComposeVolumePanelUiComponent) { Content(Modifier.weight(1f)) } Loading @@ -55,3 +64,4 @@ fun VolumePanelComposeScope.VerticalVolumePanelContent( } } } } packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt 0 → 100644 +121 −0 Original line number Diff line number Diff line /* * 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.volume.panel.component.mediaoutput.ui.viewmodel import android.content.applicationContext import android.media.session.MediaSession import android.media.session.PlaybackState import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.res.R import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.volume.localMediaRepository import com.android.systemui.volume.mediaController import com.android.systemui.volume.mediaControllerRepository import com.android.systemui.volume.mediaOutputInteractor import com.android.systemui.volume.panel.mediaOutputActionsInteractor import com.android.systemui.volume.panel.volumePanelViewModel import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper(setAsMainLooper = true) class MediaOutputViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val playbackStateBuilder = PlaybackState.Builder() private lateinit var underTest: MediaOutputViewModel @Before fun setup() { with(kosmos) { underTest = MediaOutputViewModel( applicationContext, testScope.backgroundScope, volumePanelViewModel, mediaOutputActionsInteractor, mediaOutputInteractor, ) with(context.orCreateTestableResources) { addOverride(R.string.media_output_label_title, "media_output_label_title") addOverride( R.string.media_output_title_without_playing, "media_output_title_without_playing" ) } whenever(mediaController.packageName).thenReturn("test.pkg") whenever(mediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {})) whenever(mediaController.playbackState).then { playbackStateBuilder.build() } mediaControllerRepository.setActiveLocalMediaController(mediaController) } } @Test fun playingSession_connectedDeviceViewMode_hasTheDevice() { with(kosmos) { testScope.runTest { playbackStateBuilder.setState(PlaybackState.STATE_PLAYING, 0, 0f) localMediaRepository.updateCurrentConnectedDevice( mock { whenever(name).thenReturn("test_device") } ) val connectedDeviceViewModel by collectLastValue(underTest.connectedDeviceViewModel) runCurrent() assertThat(connectedDeviceViewModel!!.label).isEqualTo("media_output_label_title") assertThat(connectedDeviceViewModel!!.deviceName).isEqualTo("test_device") } } } @Test fun notPlaying_connectedDeviceViewMode_hasTheDevice() { with(kosmos) { testScope.runTest { playbackStateBuilder.setState(PlaybackState.STATE_STOPPED, 0, 0f) localMediaRepository.updateCurrentConnectedDevice( mock { whenever(name).thenReturn("test_device") } ) val connectedDeviceViewModel by collectLastValue(underTest.connectedDeviceViewModel) runCurrent() assertThat(connectedDeviceViewModel!!.label) .isEqualTo("media_output_title_without_playing") assertThat(connectedDeviceViewModel!!.deviceName).isEqualTo("test_device") } } } } Loading
packages/SystemUI/compose/facade/disabled/src/com/android/systemui/volume/panel/component/mediaoutput/MediaOutputModule.kt 0 → 100644 +21 −0 Original line number Diff line number Diff line /* * 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.volume.panel.component.mediaoutput import dagger.Module @Module interface MediaOutputModule
packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/MediaOutputModule.kt 0 → 100644 +44 −0 Original line number Diff line number Diff line /* * 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.volume.panel.component.mediaoutput import com.android.systemui.volume.panel.component.mediaoutput.domain.MediaOutputAvailabilityCriteria import com.android.systemui.volume.panel.component.mediaoutput.ui.composable.MediaOutputComponent import com.android.systemui.volume.panel.component.shared.model.VolumePanelComponents import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria import com.android.systemui.volume.panel.shared.model.VolumePanelUiComponent import dagger.Binds import dagger.Module import dagger.multibindings.IntoMap import dagger.multibindings.StringKey /** Dagger module, that provides media output Volume Panel UI functionality. */ @Module interface MediaOutputModule { @Binds @IntoMap @StringKey(VolumePanelComponents.MEDIA_OUTPUT) fun bindVolumePanelUiComponent(component: MediaOutputComponent): VolumePanelUiComponent @Binds @IntoMap @StringKey(VolumePanelComponents.MEDIA_OUTPUT) fun bindComponentAvailabilityCriteria( criteria: MediaOutputAvailabilityCriteria ): ComponentAvailabilityCriteria }
packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt 0 → 100644 +185 −0 Original line number Diff line number Diff line /* * 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.volume.panel.component.mediaoutput.ui.composable import androidx.compose.animation.AnimatedContent import androidx.compose.animation.core.snap import androidx.compose.animation.core.tween import androidx.compose.animation.core.updateTransition import androidx.compose.animation.fadeIn import androidx.compose.animation.fadeOut import androidx.compose.animation.scaleIn import androidx.compose.animation.scaleOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.animation.togetherWith import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.RowScope import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.android.compose.animation.Expandable import com.android.systemui.common.ui.compose.Icon import com.android.systemui.common.ui.compose.toColor import com.android.systemui.volume.panel.component.mediaoutput.ui.viewmodel.ConnectedDeviceViewModel import com.android.systemui.volume.panel.component.mediaoutput.ui.viewmodel.DeviceIconViewModel import com.android.systemui.volume.panel.component.mediaoutput.ui.viewmodel.MediaOutputViewModel import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope import com.android.systemui.volume.panel.ui.composable.ComposeVolumePanelUiComponent import com.android.systemui.volume.panel.ui.composable.VolumePanelComposeScope import javax.inject.Inject @VolumePanelScope class MediaOutputComponent @Inject constructor( private val viewModel: MediaOutputViewModel, ) : ComposeVolumePanelUiComponent { @Composable override fun VolumePanelComposeScope.Content(modifier: Modifier) { val connectedDeviceViewModel: ConnectedDeviceViewModel? by viewModel.connectedDeviceViewModel.collectAsState() val deviceIconViewModel: DeviceIconViewModel? by viewModel.deviceIconViewModel.collectAsState() Expandable( modifier = Modifier.fillMaxWidth().height(80.dp), color = MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(28.dp), onClick = { viewModel.onBarClick(it) }, ) { Row { connectedDeviceViewModel?.let { ConnectedDeviceText(it) } deviceIconViewModel?.let { ConnectedDeviceIcon(it) } } } } @Composable private fun RowScope.ConnectedDeviceText(connectedDeviceViewModel: ConnectedDeviceViewModel) { Column( modifier = Modifier.weight(1f) .padding(start = 24.dp, top = 20.dp, bottom = 20.dp) .fillMaxHeight(), verticalArrangement = Arrangement.spacedBy(4.dp), ) { Text( connectedDeviceViewModel.label.toString(), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, overflow = TextOverflow.Ellipsis, ) connectedDeviceViewModel.deviceName?.let { Text( it.toString(), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface, maxLines = 1, overflow = TextOverflow.Ellipsis, ) } } } @Composable private fun ConnectedDeviceIcon(deviceIconViewModel: DeviceIconViewModel) { val transition = updateTransition(deviceIconViewModel, label = "MediaOutputIconTransition") Box( modifier = Modifier.padding(16.dp).fillMaxHeight().aspectRatio(1f), contentAlignment = Alignment.Center ) { transition.AnimatedContent( contentKey = { it.backgroundColor }, transitionSpec = { if (targetState is DeviceIconViewModel.IsPlaying) { scaleIn( initialScale = 0.9f, animationSpec = isPlayingInIconBackgroundSpec(), ) + fadeIn(animationSpec = isPlayingInIconBackgroundSpec()) togetherWith fadeOut(animationSpec = snap()) } else { fadeIn(animationSpec = snap(delayMillis = 900)) togetherWith scaleOut( targetScale = 0.9f, animationSpec = isPlayingOutSpec(), ) + fadeOut(animationSpec = isPlayingOutSpec()) } } ) { targetViewModel -> Expandable( modifier = Modifier.fillMaxSize(), color = targetViewModel.backgroundColor.toColor(), shape = RoundedCornerShape(12.dp), onClick = { viewModel.onDeviceClick(it) }, ) {} } transition.AnimatedContent( contentKey = { it.icon }, transitionSpec = { if (targetState is DeviceIconViewModel.IsPlaying) { fadeIn(animationSpec = snap(delayMillis = 700)) togetherWith slideOutVertically( targetOffsetY = { it }, animationSpec = isPlayingInIconSpec(), ) + fadeOut(animationSpec = isNotPlayingOutIconSpec()) } else { slideInVertically( initialOffsetY = { it }, animationSpec = isNotPlayingInIconSpec(), ) + fadeIn(animationSpec = isNotPlayingInIconSpec()) togetherWith fadeOut(animationSpec = isPlayingOutSpec()) } } ) { Icon( icon = it.icon, modifier = Modifier.padding(12.dp).fillMaxSize(), ) } } } } private fun <T> isPlayingOutSpec() = tween<T>(durationMillis = 400, delayMillis = 500) private fun <T> isPlayingInIconSpec() = tween<T>(durationMillis = 400, delayMillis = 300) private fun <T> isPlayingInIconBackgroundSpec() = tween<T>(durationMillis = 400, delayMillis = 700) private fun <T> isNotPlayingOutIconSpec() = tween<T>(durationMillis = 400, delayMillis = 300) private fun <T> isNotPlayingInIconSpec() = tween<T>(durationMillis = 400, delayMillis = 900)
packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt +14 −4 Original line number Diff line number Diff line Loading @@ -17,6 +17,7 @@ package com.android.systemui.volume.panel.ui.composable import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row Loading @@ -33,9 +34,16 @@ fun VolumePanelComposeScope.VerticalVolumePanelContent( modifier: Modifier = Modifier, ) { Column( modifier = modifier, modifier = modifier.animateContentSize(), verticalArrangement = Arrangement.spacedBy(20.dp), ) { for (component in layout.headerComponents) { AnimatedVisibility(component.isVisible) { with(component.component as ComposeVolumePanelUiComponent) { Content(Modifier.weight(1f)) } } } for (component in layout.contentComponents) { AnimatedVisibility(component.isVisible) { with(component.component as ComposeVolumePanelUiComponent) { Content(Modifier) } Loading @@ -44,9 +52,10 @@ fun VolumePanelComposeScope.VerticalVolumePanelContent( if (layout.footerComponents.isNotEmpty()) { Row( modifier = Modifier.fillMaxWidth().wrapContentHeight(), horizontalArrangement = Arrangement.spacedBy(20.dp) horizontalArrangement = Arrangement.spacedBy(20.dp), ) { for (component in layout.footerComponents) { AnimatedVisibility(component.isVisible) { with(component.component as ComposeVolumePanelUiComponent) { Content(Modifier.weight(1f)) } Loading @@ -55,3 +64,4 @@ fun VolumePanelComposeScope.VerticalVolumePanelContent( } } } }
packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt 0 → 100644 +121 −0 Original line number Diff line number Diff line /* * 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.volume.panel.component.mediaoutput.ui.viewmodel import android.content.applicationContext import android.media.session.MediaSession import android.media.session.PlaybackState import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.res.R import com.android.systemui.testKosmos import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever import com.android.systemui.volume.localMediaRepository import com.android.systemui.volume.mediaController import com.android.systemui.volume.mediaControllerRepository import com.android.systemui.volume.mediaOutputInteractor import com.android.systemui.volume.panel.mediaOutputActionsInteractor import com.android.systemui.volume.panel.volumePanelViewModel import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper(setAsMainLooper = true) class MediaOutputViewModelTest : SysuiTestCase() { private val kosmos = testKosmos() private val playbackStateBuilder = PlaybackState.Builder() private lateinit var underTest: MediaOutputViewModel @Before fun setup() { with(kosmos) { underTest = MediaOutputViewModel( applicationContext, testScope.backgroundScope, volumePanelViewModel, mediaOutputActionsInteractor, mediaOutputInteractor, ) with(context.orCreateTestableResources) { addOverride(R.string.media_output_label_title, "media_output_label_title") addOverride( R.string.media_output_title_without_playing, "media_output_title_without_playing" ) } whenever(mediaController.packageName).thenReturn("test.pkg") whenever(mediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {})) whenever(mediaController.playbackState).then { playbackStateBuilder.build() } mediaControllerRepository.setActiveLocalMediaController(mediaController) } } @Test fun playingSession_connectedDeviceViewMode_hasTheDevice() { with(kosmos) { testScope.runTest { playbackStateBuilder.setState(PlaybackState.STATE_PLAYING, 0, 0f) localMediaRepository.updateCurrentConnectedDevice( mock { whenever(name).thenReturn("test_device") } ) val connectedDeviceViewModel by collectLastValue(underTest.connectedDeviceViewModel) runCurrent() assertThat(connectedDeviceViewModel!!.label).isEqualTo("media_output_label_title") assertThat(connectedDeviceViewModel!!.deviceName).isEqualTo("test_device") } } } @Test fun notPlaying_connectedDeviceViewMode_hasTheDevice() { with(kosmos) { testScope.runTest { playbackStateBuilder.setState(PlaybackState.STATE_STOPPED, 0, 0f) localMediaRepository.updateCurrentConnectedDevice( mock { whenever(name).thenReturn("test_device") } ) val connectedDeviceViewModel by collectLastValue(underTest.connectedDeviceViewModel) runCurrent() assertThat(connectedDeviceViewModel!!.label) .isEqualTo("media_output_title_without_playing") assertThat(connectedDeviceViewModel!!.deviceName).isEqualTo("test_device") } } } }