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

Skip to content
Snippets Groups Projects
Commit 006c265f authored by Anton Potapov's avatar Anton Potapov
Browse files

Add ANC data and domain.

The same as ag/26232404, but with the fixed comments

Flag: aconfig new_volume_panel DISABLED
Test: atest AncSliceRepositoryTest
Test: atest AncSliceInteractorTest
Test: atest AncAvailabilityCriteriaTest
Bug: 324392591
Change-Id: Idc22bc5005d6ec9d2713d32f892ab1dffbd6061b
parent 34457980
No related branches found
No related tags found
No related merge requests found
Showing
with 717 additions and 0 deletions
/*
* 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.anc.data.repository
import android.bluetooth.BluetoothDevice
import android.net.Uri
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.media.BluetoothMediaDevice
import com.android.settingslib.media.MediaDevice
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
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.localMediaRepositoryFactory
import com.android.systemui.volume.panel.component.anc.FakeSliceFactory
import com.android.systemui.volume.panel.component.anc.sliceViewManager
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)
class AncSliceRepositoryTest : SysuiTestCase() {
private val kosmos = testKosmos()
private lateinit var underTest: AncSliceRepository
@Before
fun setup() {
with(kosmos) {
val slice = FakeSliceFactory.createSlice(hasError = false, hasSliceItem = true)
whenever(sliceViewManager.bindSlice(any<Uri>())).thenReturn(slice)
underTest =
AncSliceRepositoryImpl(
localMediaRepositoryFactory,
testScope.testScheduler,
sliceViewManager,
)
}
}
@Test
fun noConnectedDevice_noSlice() {
with(kosmos) {
testScope.runTest {
localMediaRepository.updateCurrentConnectedDevice(null)
val slice by collectLastValue(underTest.ancSlice(1))
runCurrent()
assertThat(slice).isNull()
}
}
}
@Test
fun connectedDevice_sliceReturned() {
with(kosmos) {
testScope.runTest {
localMediaRepository.updateCurrentConnectedDevice(createMediaDevice())
val slice by collectLastValue(underTest.ancSlice(1))
runCurrent()
assertThat(slice).isNotNull()
}
}
}
private fun createMediaDevice(sliceUri: String = "content://test.slice"): MediaDevice {
val bluetoothDevice: BluetoothDevice = mock {
whenever(getMetadata(any()))
.thenReturn(
("<HEARABLE_CONTROL_SLICE_WITH_WIDTH>" +
sliceUri +
"</HEARABLE_CONTROL_SLICE_WITH_WIDTH>")
.toByteArray()
)
}
val cachedBluetoothDevice: CachedBluetoothDevice = mock {
whenever(device).thenReturn(bluetoothDevice)
}
return mock<BluetoothMediaDevice> {
whenever(cachedDevice).thenReturn(cachedBluetoothDevice)
}
}
}
/*
* 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.anc.domain
import android.net.Uri
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.testKosmos
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
import com.android.systemui.volume.panel.component.anc.FakeSliceFactory
import com.android.systemui.volume.panel.component.anc.ancSliceInteractor
import com.android.systemui.volume.panel.component.anc.ancSliceRepository
import com.android.systemui.volume.panel.component.anc.sliceViewManager
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)
class AncAvailabilityCriteriaTest : SysuiTestCase() {
private val kosmos = testKosmos()
private lateinit var underTest: AncAvailabilityCriteria
@Before
fun setup() {
with(kosmos) {
whenever(sliceViewManager.bindSlice(any<Uri>())).thenReturn(mock {})
underTest = AncAvailabilityCriteria(ancSliceInteractor)
}
}
@Test
fun noSlice_unavailable() {
with(kosmos) {
testScope.runTest {
ancSliceRepository.putSlice(1, null)
val isAvailable by collectLastValue(underTest.isAvailable())
runCurrent()
assertThat(isAvailable).isFalse()
}
}
}
@Test
fun hasSlice_available() {
with(kosmos) {
testScope.runTest {
ancSliceRepository.putSlice(
1,
FakeSliceFactory.createSlice(hasError = false, hasSliceItem = true)
)
val isAvailable by collectLastValue(underTest.isAvailable())
runCurrent()
assertThat(isAvailable).isTrue()
}
}
}
}
/*
* 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.anc.domain.interactor
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.testKosmos
import com.android.systemui.volume.panel.component.anc.FakeSliceFactory
import com.android.systemui.volume.panel.component.anc.ancSliceRepository
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)
class AncSliceInteractorTest : SysuiTestCase() {
private val kosmos = testKosmos()
private lateinit var underTest: AncSliceInteractor
@Before
fun setup() {
with(kosmos) {
underTest = AncSliceInteractor(ancSliceRepository, testScope.backgroundScope)
}
}
@Test
fun errorSlice_returnsNull() {
with(kosmos) {
testScope.runTest {
ancSliceRepository.putSlice(
1,
FakeSliceFactory.createSlice(hasError = true, hasSliceItem = true)
)
val slice by collectLastValue(underTest.ancSlice)
runCurrent()
assertThat(slice).isNull()
}
}
}
@Test
fun noSliceItem_returnsNull() {
with(kosmos) {
testScope.runTest {
ancSliceRepository.putSlice(
1,
FakeSliceFactory.createSlice(hasError = false, hasSliceItem = false)
)
val slice by collectLastValue(underTest.ancSlice)
runCurrent()
assertThat(slice).isNull()
}
}
}
@Test
fun sliceItem_noError_returnsSlice() {
with(kosmos) {
testScope.runTest {
ancSliceRepository.putSlice(
1,
FakeSliceFactory.createSlice(hasError = false, hasSliceItem = true)
)
val slice by collectLastValue(underTest.ancSlice)
runCurrent()
assertThat(slice).isNotNull()
}
}
}
}
/*
* 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.slice
import android.net.Uri
import androidx.slice.Slice
import androidx.slice.SliceViewManager
import com.android.systemui.common.coroutine.ConflatedCallbackFlow
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.launch
/**
* Returns updating [Slice] for a [sliceUri]. It's null when there is no slice available for the
* provided Uri. This can change overtime because of external changes (like device being
* connected/disconnected).
*/
fun SliceViewManager.sliceForUri(sliceUri: Uri): Flow<Slice?> =
ConflatedCallbackFlow.conflatedCallbackFlow {
val callback = SliceViewManager.SliceCallback { launch { send(it) } }
val slice = bindSlice(sliceUri)
send(slice)
registerSliceCallback(sliceUri, callback)
awaitClose { unregisterSliceCallback(sliceUri, callback) }
}
/*
* 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.dagger
import android.content.Context
import androidx.slice.SliceViewManager
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.volume.panel.component.anc.data.repository.AncSliceRepository
import com.android.systemui.volume.panel.component.anc.data.repository.AncSliceRepositoryImpl
import dagger.Module
import dagger.Provides
/** Dagger module that provides ANC controlling backend. */
@Module
interface AncModule {
companion object {
@Provides
@SysUISingleton
fun provideAncSliceRepository(
@Application context: Context,
implFactory: AncSliceRepositoryImpl.Factory
): AncSliceRepository = implFactory.create(SliceViewManager.getInstance(context))
}
}
...@@ -57,6 +57,7 @@ import dagger.multibindings.IntoSet; ...@@ -57,6 +57,7 @@ import dagger.multibindings.IntoSet;
@Module( @Module(
includes = { includes = {
AudioModule.class, AudioModule.class,
AncModule.class,
CaptioningModule.class, CaptioningModule.class,
MediaDevicesModule.class MediaDevicesModule.class
}, },
......
/*
* 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.anc.data.repository
import android.bluetooth.BluetoothDevice
import android.net.Uri
import androidx.slice.Slice
import androidx.slice.SliceViewManager
import com.android.settingslib.bluetooth.BluetoothUtils
import com.android.settingslib.media.BluetoothMediaDevice
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.slice.sliceForUri
import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
/** Provides ANC slice data */
interface AncSliceRepository {
/**
* ANC slice with a given width. Emits null when there is no ANC slice available. This can mean
* that:
* - there is no supported device connected;
* - there is no slice provider for the uri;
*/
fun ancSlice(width: Int): Flow<Slice?>
}
@OptIn(ExperimentalCoroutinesApi::class)
class AncSliceRepositoryImpl
@AssistedInject
constructor(
mediaRepositoryFactory: LocalMediaRepositoryFactory,
@Background private val backgroundCoroutineContext: CoroutineContext,
@Assisted private val sliceViewManager: SliceViewManager,
) : AncSliceRepository {
private val localMediaRepository = mediaRepositoryFactory.create(null)
override fun ancSlice(width: Int): Flow<Slice?> {
return localMediaRepository.currentConnectedDevice
.map { (it as? BluetoothMediaDevice)?.cachedDevice?.device?.getExtraControlUri(width) }
.distinctUntilChanged()
.flatMapLatest { sliceUri ->
sliceUri ?: return@flatMapLatest flowOf(null)
sliceViewManager.sliceForUri(sliceUri)
}
.flowOn(backgroundCoroutineContext)
}
private fun BluetoothDevice.getExtraControlUri(width: Int): Uri? {
val uri: String? = BluetoothUtils.getControlUriMetaData(this)
uri ?: return null
return if (uri.isEmpty()) {
null
} else {
Uri.parse(
"$uri$width" +
"&version=${SliceParameters.VERSION}" +
"&is_collapsed=${SliceParameters.IS_COLLAPSED}"
)
}
}
@AssistedFactory
interface Factory {
fun create(sliceViewManager: SliceViewManager): AncSliceRepositoryImpl
}
private object SliceParameters {
/**
* Slice version
* 1) legacy slice
* 2) new slice
*/
const val VERSION = 2
/**
* Collapsed slice shows a single button, and expanded shows a row buttons. Supported since
* [VERSION]==2.
*/
const val IS_COLLAPSED = false
}
}
/*
* 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.anc.domain
import com.android.systemui.volume.panel.component.anc.domain.interactor.AncSliceInteractor
import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
/** Determines if ANC component is available for the Volume Panel. */
@VolumePanelScope
class AncAvailabilityCriteria
@Inject
constructor(
private val ancSliceInteractor: AncSliceInteractor,
) : ComponentAvailabilityCriteria {
override fun isAvailable(): Flow<Boolean> = ancSliceInteractor.ancSlice.map { it != null }
}
/*
* 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.anc.domain.interactor
import android.app.slice.Slice.HINT_ERROR
import android.app.slice.SliceItem.FORMAT_SLICE
import androidx.slice.Slice
import com.android.systemui.volume.panel.component.anc.data.repository.AncSliceRepository
import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.shareIn
/** Provides a valid slice from [AncSliceRepository]. */
@OptIn(ExperimentalCoroutinesApi::class)
@VolumePanelScope
class AncSliceInteractor
@Inject
constructor(
private val ancSliceRepository: AncSliceRepository,
scope: CoroutineScope,
) {
// Start with a positive width to check is the Slice is available.
private val width = MutableStateFlow(1)
/** Provides a valid ANC slice. */
val ancSlice: SharedFlow<Slice?> =
width
.flatMapLatest { width -> ancSliceRepository.ancSlice(width) }
.map { slice ->
if (slice?.isValidSlice() == true) {
slice
} else {
null
}
}
.shareIn(scope, SharingStarted.Eagerly, replay = 1)
/** Updates the width of the [ancSlice] */
fun changeWidth(newWidth: Int) {
width.value = newWidth
}
private fun Slice.isValidSlice(): Boolean {
if (hints.contains(HINT_ERROR)) {
return false
}
for (item in items) {
if (item.format == FORMAT_SLICE) {
return true
}
}
return false
}
}
/*
* 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.anc
import androidx.slice.Slice
import androidx.slice.SliceItem
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
object FakeSliceFactory {
fun createSlice(hasError: Boolean, hasSliceItem: Boolean): Slice {
return mock {
val sliceItem: SliceItem = mock {
whenever(format).thenReturn(android.app.slice.SliceItem.FORMAT_SLICE)
}
whenever(items)
.thenReturn(
buildList {
if (hasSliceItem) {
add(sliceItem)
}
}
)
whenever(hints)
.thenReturn(
buildList {
if (hasError) {
add(android.app.slice.Slice.HINT_ERROR)
}
}
)
}
}
}
/*
* 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.anc
import androidx.slice.SliceViewManager
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
import com.android.systemui.util.mockito.mock
import com.android.systemui.volume.panel.component.anc.data.repository.FakeAncSliceRepository
import com.android.systemui.volume.panel.component.anc.domain.interactor.AncSliceInteractor
var Kosmos.sliceViewManager: SliceViewManager by Kosmos.Fixture { mock {} }
val Kosmos.ancSliceRepository by Kosmos.Fixture { FakeAncSliceRepository() }
val Kosmos.ancSliceInteractor by
Kosmos.Fixture { AncSliceInteractor(ancSliceRepository, testScope.backgroundScope) }
/*
* 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.anc.data.repository
import androidx.slice.Slice
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
class FakeAncSliceRepository : AncSliceRepository {
private val sliceByWidth = mutableMapOf<Int, MutableStateFlow<Slice?>>()
override fun ancSlice(width: Int): Flow<Slice?> =
sliceByWidth.getOrPut(width) { MutableStateFlow(null) }
fun putSlice(width: Int, slice: Slice?) {
sliceByWidth.getOrPut(width) { MutableStateFlow(null) }.value = slice
}
}
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment