Loading packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/SpatialAudioModule.kt +5 −14 Original line number Diff line number Diff line Loading @@ -16,16 +16,13 @@ package com.android.systemui.volume.panel.component.spatialaudio import com.android.systemui.volume.panel.component.button.ui.composable.ButtonComponent import com.android.systemui.volume.panel.component.shared.model.VolumePanelComponents import com.android.systemui.volume.panel.component.spatial.domain.SpatialAudioAvailabilityCriteria import com.android.systemui.volume.panel.component.spatial.ui.viewmodel.SpatialAudioViewModel import com.android.systemui.volume.panel.component.spatialaudio.ui.composable.SpatialAudioPopup import com.android.systemui.volume.panel.component.spatialaudio.ui.composable.SpatialAudioComponent 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.Provides import dagger.multibindings.IntoMap import dagger.multibindings.StringKey Loading @@ -40,14 +37,8 @@ interface SpatialAudioModule { criteria: SpatialAudioAvailabilityCriteria ): ComponentAvailabilityCriteria companion object { @Provides @Binds @IntoMap @StringKey(VolumePanelComponents.SPATIAL_AUDIO) fun provideVolumePanelUiComponent( viewModel: SpatialAudioViewModel, popup: SpatialAudioPopup, ): VolumePanelUiComponent = ButtonComponent(viewModel.spatialAudioButton, popup::show) } fun bindVolumePanelUiComponent(component: SpatialAudioComponent): VolumePanelUiComponent } packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioComponent.kt 0 → 100644 +60 −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.spatialaudio.ui.composable import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.systemui.volume.panel.component.button.ui.composable.ButtonComponent import com.android.systemui.volume.panel.component.button.ui.composable.ToggleButtonComponent import com.android.systemui.volume.panel.component.spatial.domain.model.SpatialAudioEnabledModel import com.android.systemui.volume.panel.component.spatial.ui.viewmodel.SpatialAudioViewModel import com.android.systemui.volume.panel.ui.composable.ComposeVolumePanelUiComponent import com.android.systemui.volume.panel.ui.composable.VolumePanelComposeScope import javax.inject.Inject /** [ComposeVolumePanelUiComponent] that represents spatial audio button in the Volume Panel. */ class SpatialAudioComponent @Inject constructor( private val viewModel: SpatialAudioViewModel, private val popup: SpatialAudioPopup, ) : ComposeVolumePanelUiComponent { @Composable override fun VolumePanelComposeScope.Content(modifier: Modifier) { val shouldUsePopup by viewModel.shouldUsePopup.collectAsStateWithLifecycle() val buttonComponent: ComposeVolumePanelUiComponent = remember(shouldUsePopup) { if (shouldUsePopup) { ButtonComponent(viewModel.spatialAudioButton, popup::show) } else { ToggleButtonComponent(viewModel.spatialAudioButton) { if (it) { viewModel.setEnabled(SpatialAudioEnabledModel.SpatialAudioEnabled) } else { viewModel.setEnabled(SpatialAudioEnabledModel.Disabled) } } } } with(buttonComponent) { Content(modifier) } } } packages/SystemUI/res-keyguard/drawable/ic_spatial_speaker.xml 0 → 100644 +25 −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. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportHeight="960" android:viewportWidth="960"> <path android:fillColor="@android:color/white" android:pathData="M360,880q-134,0 -227,-93T40,560h80q0,100 70,170t170,70v80ZM360,740q-75,0 -127.5,-52.5T180,560h80q0,42 29,71t71,29v80ZM400,600q-33,0 -56.5,-23.5T320,520v-320q0,-33 23.5,-56.5T400,120h160q33,0 56.5,23.5T640,200v320q0,33 -23.5,56.5T560,600L400,600ZM400,520h160v-320L400,200v320ZM600,740v-80q42,0 71,-29t29,-71h80q0,75 -52.5,127.5T600,740ZM600,880v-80q100,0 170,-70t70,-170h80q0,134 -93,227T600,880ZM400,520h160,-160Z" /> </vector> packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioViewModel.kt +50 −10 Original line number Diff line number Diff line Loading @@ -48,15 +48,31 @@ constructor( private val uiEventLogger: UiEventLogger, ) { private val spatialSpeakerIcon = Icon.Resource(R.drawable.ic_spatial_speaker, contentDescription = null) val spatialAudioButton: StateFlow<ButtonViewModel?> = interactor.isEnabled .map { val isChecked = it is SpatialAudioEnabledModel.SpatialAudioEnabled it.toViewModel(isChecked) combine(interactor.isEnabled, interactor.isAvailable) { isEnabled, isAvailable -> isEnabled .toViewModel( isChecked = isEnabled is SpatialAudioEnabledModel.SpatialAudioEnabled, isHeadTrackingAvailable = isAvailable is SpatialAudioAvailabilityModel.SpatialAudio, ) .copy(label = context.getString(R.string.volume_panel_spatial_audio_title)) } .stateIn(scope, SharingStarted.Eagerly, null) val shouldUsePopup: StateFlow<Boolean> = interactor.isAvailable .map { // head tracking availability means there are three possible states for the spatial // audio: disabled, enabled regular, enabled with head tracking. // Show popup in this case instead of a togglealbe button. it is SpatialAudioAvailabilityModel.SpatialAudio } .stateIn(scope, SharingStarted.Eagerly, false) val isAvailable: StateFlow<Boolean> = availabilityCriteria.isAvailable().stateIn(scope, SharingStarted.Eagerly, true) Loading @@ -73,8 +89,12 @@ constructor( } } .map { isEnabled -> val isChecked = isEnabled == currentIsEnabled val buttonViewModel: ButtonViewModel = isEnabled.toViewModel(isChecked) val buttonViewModel: ButtonViewModel = isEnabled.toViewModel( isChecked = isEnabled == currentIsEnabled, isHeadTrackingAvailable = isAvailable is SpatialAudioAvailabilityModel.HeadTracking, ) SpatialAudioButtonViewModel(button = buttonViewModel, model = isEnabled) } } Loading @@ -97,11 +117,21 @@ constructor( scope.launch { interactor.setEnabled(model) } } private fun SpatialAudioEnabledModel.toViewModel(isChecked: Boolean): ButtonViewModel { private fun SpatialAudioEnabledModel.toViewModel( isChecked: Boolean, isHeadTrackingAvailable: Boolean, ): ButtonViewModel { // This method deliberately uses the same icon for the case when head tracking is disabled // to show a toggle button with a non-changing icon if (this is SpatialAudioEnabledModel.HeadTrackingEnabled) { return ButtonViewModel( isActive = isChecked, icon = Icon.Resource(R.drawable.ic_head_tracking, contentDescription = null), icon = if (isHeadTrackingAvailable) { Icon.Resource(R.drawable.ic_head_tracking, contentDescription = null) } else { spatialSpeakerIcon }, label = context.getString(R.string.volume_panel_spatial_audio_tracking) ) } Loading @@ -109,7 +139,12 @@ constructor( if (this is SpatialAudioEnabledModel.SpatialAudioEnabled) { return ButtonViewModel( isActive = isChecked, icon = Icon.Resource(R.drawable.ic_spatial_audio, contentDescription = null), icon = if (isHeadTrackingAvailable) { Icon.Resource(R.drawable.ic_spatial_audio, contentDescription = null) } else { spatialSpeakerIcon }, label = context.getString(R.string.volume_panel_spatial_audio_fixed) ) } Loading @@ -117,7 +152,12 @@ constructor( if (this is SpatialAudioEnabledModel.Disabled) { return ButtonViewModel( isActive = isChecked, icon = Icon.Resource(R.drawable.ic_spatial_audio_off, contentDescription = null), icon = if (isHeadTrackingAvailable) { Icon.Resource(R.drawable.ic_spatial_audio_off, contentDescription = null) } else { spatialSpeakerIcon }, label = context.getString(R.string.volume_panel_spatial_audio_off) ) } Loading Loading
packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/SpatialAudioModule.kt +5 −14 Original line number Diff line number Diff line Loading @@ -16,16 +16,13 @@ package com.android.systemui.volume.panel.component.spatialaudio import com.android.systemui.volume.panel.component.button.ui.composable.ButtonComponent import com.android.systemui.volume.panel.component.shared.model.VolumePanelComponents import com.android.systemui.volume.panel.component.spatial.domain.SpatialAudioAvailabilityCriteria import com.android.systemui.volume.panel.component.spatial.ui.viewmodel.SpatialAudioViewModel import com.android.systemui.volume.panel.component.spatialaudio.ui.composable.SpatialAudioPopup import com.android.systemui.volume.panel.component.spatialaudio.ui.composable.SpatialAudioComponent 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.Provides import dagger.multibindings.IntoMap import dagger.multibindings.StringKey Loading @@ -40,14 +37,8 @@ interface SpatialAudioModule { criteria: SpatialAudioAvailabilityCriteria ): ComponentAvailabilityCriteria companion object { @Provides @Binds @IntoMap @StringKey(VolumePanelComponents.SPATIAL_AUDIO) fun provideVolumePanelUiComponent( viewModel: SpatialAudioViewModel, popup: SpatialAudioPopup, ): VolumePanelUiComponent = ButtonComponent(viewModel.spatialAudioButton, popup::show) } fun bindVolumePanelUiComponent(component: SpatialAudioComponent): VolumePanelUiComponent }
packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioComponent.kt 0 → 100644 +60 −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.spatialaudio.ui.composable import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.android.systemui.volume.panel.component.button.ui.composable.ButtonComponent import com.android.systemui.volume.panel.component.button.ui.composable.ToggleButtonComponent import com.android.systemui.volume.panel.component.spatial.domain.model.SpatialAudioEnabledModel import com.android.systemui.volume.panel.component.spatial.ui.viewmodel.SpatialAudioViewModel import com.android.systemui.volume.panel.ui.composable.ComposeVolumePanelUiComponent import com.android.systemui.volume.panel.ui.composable.VolumePanelComposeScope import javax.inject.Inject /** [ComposeVolumePanelUiComponent] that represents spatial audio button in the Volume Panel. */ class SpatialAudioComponent @Inject constructor( private val viewModel: SpatialAudioViewModel, private val popup: SpatialAudioPopup, ) : ComposeVolumePanelUiComponent { @Composable override fun VolumePanelComposeScope.Content(modifier: Modifier) { val shouldUsePopup by viewModel.shouldUsePopup.collectAsStateWithLifecycle() val buttonComponent: ComposeVolumePanelUiComponent = remember(shouldUsePopup) { if (shouldUsePopup) { ButtonComponent(viewModel.spatialAudioButton, popup::show) } else { ToggleButtonComponent(viewModel.spatialAudioButton) { if (it) { viewModel.setEnabled(SpatialAudioEnabledModel.SpatialAudioEnabled) } else { viewModel.setEnabled(SpatialAudioEnabledModel.Disabled) } } } } with(buttonComponent) { Content(modifier) } } }
packages/SystemUI/res-keyguard/drawable/ic_spatial_speaker.xml 0 → 100644 +25 −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. --> <vector xmlns:android="http://schemas.android.com/apk/res/android" android:width="24dp" android:height="24dp" android:viewportHeight="960" android:viewportWidth="960"> <path android:fillColor="@android:color/white" android:pathData="M360,880q-134,0 -227,-93T40,560h80q0,100 70,170t170,70v80ZM360,740q-75,0 -127.5,-52.5T180,560h80q0,42 29,71t71,29v80ZM400,600q-33,0 -56.5,-23.5T320,520v-320q0,-33 23.5,-56.5T400,120h160q33,0 56.5,23.5T640,200v320q0,33 -23.5,56.5T560,600L400,600ZM400,520h160v-320L400,200v320ZM600,740v-80q42,0 71,-29t29,-71h80q0,75 -52.5,127.5T600,740ZM600,880v-80q100,0 170,-70t70,-170h80q0,134 -93,227T600,880ZM400,520h160,-160Z" /> </vector>
packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioViewModel.kt +50 −10 Original line number Diff line number Diff line Loading @@ -48,15 +48,31 @@ constructor( private val uiEventLogger: UiEventLogger, ) { private val spatialSpeakerIcon = Icon.Resource(R.drawable.ic_spatial_speaker, contentDescription = null) val spatialAudioButton: StateFlow<ButtonViewModel?> = interactor.isEnabled .map { val isChecked = it is SpatialAudioEnabledModel.SpatialAudioEnabled it.toViewModel(isChecked) combine(interactor.isEnabled, interactor.isAvailable) { isEnabled, isAvailable -> isEnabled .toViewModel( isChecked = isEnabled is SpatialAudioEnabledModel.SpatialAudioEnabled, isHeadTrackingAvailable = isAvailable is SpatialAudioAvailabilityModel.SpatialAudio, ) .copy(label = context.getString(R.string.volume_panel_spatial_audio_title)) } .stateIn(scope, SharingStarted.Eagerly, null) val shouldUsePopup: StateFlow<Boolean> = interactor.isAvailable .map { // head tracking availability means there are three possible states for the spatial // audio: disabled, enabled regular, enabled with head tracking. // Show popup in this case instead of a togglealbe button. it is SpatialAudioAvailabilityModel.SpatialAudio } .stateIn(scope, SharingStarted.Eagerly, false) val isAvailable: StateFlow<Boolean> = availabilityCriteria.isAvailable().stateIn(scope, SharingStarted.Eagerly, true) Loading @@ -73,8 +89,12 @@ constructor( } } .map { isEnabled -> val isChecked = isEnabled == currentIsEnabled val buttonViewModel: ButtonViewModel = isEnabled.toViewModel(isChecked) val buttonViewModel: ButtonViewModel = isEnabled.toViewModel( isChecked = isEnabled == currentIsEnabled, isHeadTrackingAvailable = isAvailable is SpatialAudioAvailabilityModel.HeadTracking, ) SpatialAudioButtonViewModel(button = buttonViewModel, model = isEnabled) } } Loading @@ -97,11 +117,21 @@ constructor( scope.launch { interactor.setEnabled(model) } } private fun SpatialAudioEnabledModel.toViewModel(isChecked: Boolean): ButtonViewModel { private fun SpatialAudioEnabledModel.toViewModel( isChecked: Boolean, isHeadTrackingAvailable: Boolean, ): ButtonViewModel { // This method deliberately uses the same icon for the case when head tracking is disabled // to show a toggle button with a non-changing icon if (this is SpatialAudioEnabledModel.HeadTrackingEnabled) { return ButtonViewModel( isActive = isChecked, icon = Icon.Resource(R.drawable.ic_head_tracking, contentDescription = null), icon = if (isHeadTrackingAvailable) { Icon.Resource(R.drawable.ic_head_tracking, contentDescription = null) } else { spatialSpeakerIcon }, label = context.getString(R.string.volume_panel_spatial_audio_tracking) ) } Loading @@ -109,7 +139,12 @@ constructor( if (this is SpatialAudioEnabledModel.SpatialAudioEnabled) { return ButtonViewModel( isActive = isChecked, icon = Icon.Resource(R.drawable.ic_spatial_audio, contentDescription = null), icon = if (isHeadTrackingAvailable) { Icon.Resource(R.drawable.ic_spatial_audio, contentDescription = null) } else { spatialSpeakerIcon }, label = context.getString(R.string.volume_panel_spatial_audio_fixed) ) } Loading @@ -117,7 +152,12 @@ constructor( if (this is SpatialAudioEnabledModel.Disabled) { return ButtonViewModel( isActive = isChecked, icon = Icon.Resource(R.drawable.ic_spatial_audio_off, contentDescription = null), icon = if (isHeadTrackingAvailable) { Icon.Resource(R.drawable.ic_spatial_audio_off, contentDescription = null) } else { spatialSpeakerIcon }, label = context.getString(R.string.volume_panel_spatial_audio_off) ) } Loading