Rework Spatial Audio component to work as a Toggle when head tracking is
unavailable
Flag: com.android.systemui.new_volume_panel
Test: atest VolumePanelScreenshotTest
Test: manual on the spatial audio enabled tablet
Fixes: 343361893
Change-Id: If9c426cd78c2ecf150ca4c69d9d9b631f19b716c
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/SpatialAudioModule.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/SpatialAudioModule.kt
index da29d58..48af8cd 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/SpatialAudioModule.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/SpatialAudioModule.kt
@@ -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
@@ -40,14 +37,8 @@
criteria: SpatialAudioAvailabilityCriteria
): ComponentAvailabilityCriteria
- companion object {
-
- @Provides
- @IntoMap
- @StringKey(VolumePanelComponents.SPATIAL_AUDIO)
- fun provideVolumePanelUiComponent(
- viewModel: SpatialAudioViewModel,
- popup: SpatialAudioPopup,
- ): VolumePanelUiComponent = ButtonComponent(viewModel.spatialAudioButton, popup::show)
- }
+ @Binds
+ @IntoMap
+ @StringKey(VolumePanelComponents.SPATIAL_AUDIO)
+ fun bindVolumePanelUiComponent(component: SpatialAudioComponent): VolumePanelUiComponent
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioComponent.kt
new file mode 100644
index 0000000..2d89b5c
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/spatialaudio/ui/composable/SpatialAudioComponent.kt
@@ -0,0 +1,60 @@
+/*
+ * 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) }
+ }
+}
diff --git a/packages/SystemUI/res-keyguard/drawable/ic_spatial_speaker.xml b/packages/SystemUI/res-keyguard/drawable/ic_spatial_speaker.xml
new file mode 100644
index 0000000..82b222e7
--- /dev/null
+++ b/packages/SystemUI/res-keyguard/drawable/ic_spatial_speaker.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ 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>
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioViewModel.kt
index 4b2d26a..6c6a1cc 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/ui/viewmodel/SpatialAudioViewModel.kt
@@ -48,15 +48,31 @@
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)
@@ -73,8 +89,12 @@
}
}
.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)
}
}
@@ -97,11 +117,21 @@
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)
)
}
@@ -109,7 +139,12 @@
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)
)
}
@@ -117,7 +152,12 @@
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)
)
}