Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 088ceae9 authored by Anton Potapov's avatar Anton Potapov
Browse files

Add Head tracking domain and data

Flag: aconfig new_volume_panel DISABLED
Test: atest SpatializerInteractorTest
Test: atest SpatialAudioComponentInteractorTest
Test: atest SpatialAudioAvailabilityCriteriaTest
Fixes: 323165738
Change-Id: I588e35f444368862ccf815612b997816fa8fad53
parent 91b5d8c4
Loading
Loading
Loading
Loading
+66 −10
Original line number Diff line number Diff line
@@ -18,33 +18,71 @@ package com.android.settingslib.media.data.repository

import android.media.AudioDeviceAttributes
import android.media.Spatializer
import androidx.concurrent.futures.DirectExecutor
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

interface SpatializerRepository {

    /** Returns true when head tracking is enabled and false the otherwise. */
    val isHeadTrackingAvailable: StateFlow<Boolean>

    /**
     * Returns true when Spatial audio feature is supported for the [audioDeviceAttributes] and
     * false the otherwise.
     */
    suspend fun isAvailableForDevice(audioDeviceAttributes: AudioDeviceAttributes): Boolean
    suspend fun isSpatialAudioAvailableForDevice(
        audioDeviceAttributes: AudioDeviceAttributes
    ): Boolean

    /** Returns a list [AudioDeviceAttributes] that are compatible with spatial audio. */
    suspend fun getCompatibleDevices(): Collection<AudioDeviceAttributes>
    suspend fun getSpatialAudioCompatibleDevices(): Collection<AudioDeviceAttributes>

    /** Adds a [audioDeviceAttributes] to [getSpatialAudioCompatibleDevices] list. */
    suspend fun addSpatialAudioCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes)

    /** Removes a [audioDeviceAttributes] from [getSpatialAudioCompatibleDevices] list. */
    suspend fun removeSpatialAudioCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes)

    /** Adds a [audioDeviceAttributes] to [getCompatibleDevices] list. */
    suspend fun addCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes)
    /** Checks if the head tracking is enabled for the [audioDeviceAttributes]. */
    suspend fun isHeadTrackingEnabled(audioDeviceAttributes: AudioDeviceAttributes): Boolean

    /** Removes a [audioDeviceAttributes] to [getCompatibleDevices] list. */
    suspend fun removeCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes)
    /** Sets head tracking [isEnabled] for the [audioDeviceAttributes]. */
    suspend fun setHeadTrackingEnabled(
        audioDeviceAttributes: AudioDeviceAttributes,
        isEnabled: Boolean,
    )
}

class SpatializerRepositoryImpl(
    private val spatializer: Spatializer,
    coroutineScope: CoroutineScope,
    private val backgroundContext: CoroutineContext,
) : SpatializerRepository {

    override suspend fun isAvailableForDevice(
    override val isHeadTrackingAvailable: StateFlow<Boolean> =
        callbackFlow {
                val listener =
                    Spatializer.OnHeadTrackerAvailableListener { _, available ->
                        launch { send(available) }
                    }
                spatializer.addOnHeadTrackerAvailableListener(DirectExecutor.INSTANCE, listener)
                awaitClose { spatializer.removeOnHeadTrackerAvailableListener(listener) }
            }
            .onStart { emit(spatializer.isHeadTrackerAvailable) }
            .flowOn(backgroundContext)
            .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), false)

    override suspend fun isSpatialAudioAvailableForDevice(
        audioDeviceAttributes: AudioDeviceAttributes
    ): Boolean {
        return withContext(backgroundContext) {
@@ -52,18 +90,36 @@ class SpatializerRepositoryImpl(
        }
    }

    override suspend fun getCompatibleDevices(): Collection<AudioDeviceAttributes> =
    override suspend fun getSpatialAudioCompatibleDevices(): Collection<AudioDeviceAttributes> =
        withContext(backgroundContext) { spatializer.compatibleAudioDevices }

    override suspend fun addCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes) {
    override suspend fun addSpatialAudioCompatibleDevice(
        audioDeviceAttributes: AudioDeviceAttributes
    ) {
        withContext(backgroundContext) {
            spatializer.addCompatibleAudioDevice(audioDeviceAttributes)
        }
    }

    override suspend fun removeCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes) {
    override suspend fun removeSpatialAudioCompatibleDevice(
        audioDeviceAttributes: AudioDeviceAttributes
    ) {
        withContext(backgroundContext) {
            spatializer.removeCompatibleAudioDevice(audioDeviceAttributes)
        }
    }

    override suspend fun isHeadTrackingEnabled(
        audioDeviceAttributes: AudioDeviceAttributes
    ): Boolean =
        withContext(backgroundContext) { spatializer.isHeadTrackerEnabled(audioDeviceAttributes) }

    override suspend fun setHeadTrackingEnabled(
        audioDeviceAttributes: AudioDeviceAttributes,
        isEnabled: Boolean,
    ) {
        withContext(backgroundContext) {
            spatializer.setHeadTrackerEnabled(isEnabled, audioDeviceAttributes)
        }
    }
}
+26 −8
Original line number Diff line number Diff line
@@ -18,22 +18,40 @@ package com.android.settingslib.media.domain.interactor

import android.media.AudioDeviceAttributes
import com.android.settingslib.media.data.repository.SpatializerRepository
import kotlinx.coroutines.flow.StateFlow

class SpatializerInteractor(private val repository: SpatializerRepository) {

    suspend fun isAvailable(audioDeviceAttributes: AudioDeviceAttributes): Boolean =
        repository.isAvailableForDevice(audioDeviceAttributes)
    /** Checks if head tracking is available. */
    val isHeadTrackingAvailable: StateFlow<Boolean>
        get() = repository.isHeadTrackingAvailable

    suspend fun isSpatialAudioAvailable(audioDeviceAttributes: AudioDeviceAttributes): Boolean =
        repository.isSpatialAudioAvailableForDevice(audioDeviceAttributes)

    /** Checks if spatial audio is enabled for the [audioDeviceAttributes]. */
    suspend fun isEnabled(audioDeviceAttributes: AudioDeviceAttributes): Boolean =
        repository.getCompatibleDevices().contains(audioDeviceAttributes)
    suspend fun isSpatialAudioEnabled(audioDeviceAttributes: AudioDeviceAttributes): Boolean =
        repository.getSpatialAudioCompatibleDevices().contains(audioDeviceAttributes)

    /** Enblaes or disables spatial audio for [audioDeviceAttributes]. */
    suspend fun setEnabled(audioDeviceAttributes: AudioDeviceAttributes, isEnabled: Boolean) {
    /** Enables or disables spatial audio for [audioDeviceAttributes]. */
    suspend fun setSpatialAudioEnabled(
        audioDeviceAttributes: AudioDeviceAttributes,
        isEnabled: Boolean
    ) {
        if (isEnabled) {
            repository.addCompatibleDevice(audioDeviceAttributes)
            repository.addSpatialAudioCompatibleDevice(audioDeviceAttributes)
        } else {
            repository.removeCompatibleDevice(audioDeviceAttributes)
            repository.removeSpatialAudioCompatibleDevice(audioDeviceAttributes)
        }
    }

    /** Checks if head tracking is enabled for the [audioDeviceAttributes]. */
    suspend fun isHeadTrackingEnabled(audioDeviceAttributes: AudioDeviceAttributes): Boolean =
        repository.isHeadTrackingEnabled(audioDeviceAttributes)

    /** Enables or disables head tracking for the [audioDeviceAttributes]. */
    suspend fun setHeadTrackingEnabled(
        audioDeviceAttributes: AudioDeviceAttributes,
        isEnabled: Boolean,
    ) = repository.setHeadTrackingEnabled(audioDeviceAttributes, isEnabled)
}
+0 −45
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.settingslib.media.domain.interactor

import android.media.AudioDeviceAttributes
import com.android.settingslib.media.data.repository.SpatializerRepository

class FakeSpatializerRepository : SpatializerRepository {

    private val availabilityByDevice: MutableMap<AudioDeviceAttributes, Boolean> = mutableMapOf()
    private val compatibleDevices: MutableList<AudioDeviceAttributes> = mutableListOf()

    override suspend fun isAvailableForDevice(
        audioDeviceAttributes: AudioDeviceAttributes
    ): Boolean = availabilityByDevice.getOrDefault(audioDeviceAttributes, false)

    override suspend fun getCompatibleDevices(): Collection<AudioDeviceAttributes> =
        compatibleDevices

    override suspend fun addCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes) {
        compatibleDevices.add(audioDeviceAttributes)
    }

    override suspend fun removeCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes) {
        compatibleDevices.remove(audioDeviceAttributes)
    }

    fun setIsAvailable(audioDeviceAttributes: AudioDeviceAttributes, isAvailable: Boolean) {
        availabilityByDevice[audioDeviceAttributes] = isAvailable
    }
}
+92 −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.media.domain.interactor

import android.media.AudioDeviceAttributes
import android.media.AudioDeviceInfo
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.media.domain.interactor.SpatializerInteractor
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.testScope
import com.android.systemui.media.spatializerRepository
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Test
import org.junit.runner.RunWith

@SmallTest
@RunWith(AndroidJUnit4::class)
class SpatializerInteractorTest : SysuiTestCase() {

    private val kosmos = testKosmos()
    private val underTest = SpatializerInteractor(kosmos.spatializerRepository)

    @Test
    fun setSpatialAudioEnabledFalse_isEnabled_false() {
        with(kosmos) {
            testScope.runTest {
                underTest.setSpatialAudioEnabled(deviceAttributes, false)

                assertThat(underTest.isSpatialAudioEnabled(deviceAttributes)).isFalse()
            }
        }
    }

    @Test
    fun setSpatialAudioEnabledTrue_isEnabled_true() {
        with(kosmos) {
            testScope.runTest {
                underTest.setSpatialAudioEnabled(deviceAttributes, true)

                assertThat(underTest.isSpatialAudioEnabled(deviceAttributes)).isTrue()
            }
        }
    }

    @Test
    fun setHeadTrackingEnabledFalse_isEnabled_false() {
        with(kosmos) {
            testScope.runTest {
                underTest.setHeadTrackingEnabled(deviceAttributes, false)

                assertThat(underTest.isHeadTrackingEnabled(deviceAttributes)).isFalse()
            }
        }
    }

    @Test
    fun setHeadTrackingEnabledTrue_isEnabled_true() {
        with(kosmos) {
            testScope.runTest {
                underTest.setHeadTrackingEnabled(deviceAttributes, true)

                assertThat(underTest.isHeadTrackingEnabled(deviceAttributes)).isTrue()
            }
        }
    }

    private companion object {
        val deviceAttributes =
            AudioDeviceAttributes(
                AudioDeviceAttributes.ROLE_OUTPUT,
                AudioDeviceInfo.TYPE_BLE_HEADSET,
                "test_address",
            )
    }
}
+32 −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.spatial

import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
import com.android.systemui.media.spatializerInteractor
import com.android.systemui.volume.mediaOutputInteractor
import com.android.systemui.volume.panel.component.spatial.domain.interactor.SpatialAudioComponentInteractor

val Kosmos.spatialAudioComponentInteractor by
    Kosmos.Fixture {
        SpatialAudioComponentInteractor(
            mediaOutputInteractor,
            spatializerInteractor,
            testScope.backgroundScope
        )
    }
Loading