From 006c265f70d75cc04aeed4298a3dcbdb1c7d1361 Mon Sep 17 00:00:00 2001 From: Anton Potapov Date: Wed, 14 Feb 2024 22:05:07 +0000 Subject: [PATCH] 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 --- .../data/repository/AncSliceRepositoryTest.kt | 114 ++++++++++++++++++ .../anc/domain/AncAvailabilityCriteriaTest.kt | 89 ++++++++++++++ .../interactor/AncSliceInteractorTest.kt | 101 ++++++++++++++++ .../systemui/slice/SliceViewManagerExt.kt | 40 ++++++ .../systemui/volume/dagger/AncModule.kt | 40 ++++++ .../systemui/volume/dagger/VolumeModule.java | 1 + .../anc/data/repository/AncSliceRepository.kt | 108 +++++++++++++++++ .../anc/domain/AncAvailabilityCriteria.kt | 35 ++++++ .../domain/interactor/AncSliceInteractor.kt | 76 ++++++++++++ .../panel/component/anc/FakeSliceFactory.kt | 51 ++++++++ .../component/anc/VolumePanelAncKosmos.kt | 29 +++++ .../data/repository/FakeAncSliceRepository.kt | 33 +++++ 12 files changed, 717 insertions(+) create mode 100644 packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/anc/data/repository/AncSliceRepositoryTest.kt create mode 100644 packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/anc/domain/AncAvailabilityCriteriaTest.kt create mode 100644 packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/anc/domain/interactor/AncSliceInteractorTest.kt create mode 100644 packages/SystemUI/src/com/android/systemui/slice/SliceViewManagerExt.kt create mode 100644 packages/SystemUI/src/com/android/systemui/volume/dagger/AncModule.kt create mode 100644 packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/data/repository/AncSliceRepository.kt create mode 100644 packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/domain/AncAvailabilityCriteria.kt create mode 100644 packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/domain/interactor/AncSliceInteractor.kt create mode 100644 packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/anc/FakeSliceFactory.kt create mode 100644 packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/anc/VolumePanelAncKosmos.kt create mode 100644 packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/anc/data/repository/FakeAncSliceRepository.kt diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/anc/data/repository/AncSliceRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/anc/data/repository/AncSliceRepositoryTest.kt new file mode 100644 index 000000000000..e31cdcd82e7e --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/anc/data/repository/AncSliceRepositoryTest.kt @@ -0,0 +1,114 @@ +/* + * 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())).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( + ("" + + sliceUri + + "") + .toByteArray() + ) + } + val cachedBluetoothDevice: CachedBluetoothDevice = mock { + whenever(device).thenReturn(bluetoothDevice) + } + return mock { + whenever(cachedDevice).thenReturn(cachedBluetoothDevice) + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/anc/domain/AncAvailabilityCriteriaTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/anc/domain/AncAvailabilityCriteriaTest.kt new file mode 100644 index 000000000000..553aed8cfb05 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/anc/domain/AncAvailabilityCriteriaTest.kt @@ -0,0 +1,89 @@ +/* + * 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())).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() + } + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/anc/domain/interactor/AncSliceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/anc/domain/interactor/AncSliceInteractorTest.kt new file mode 100644 index 000000000000..53f0bc9ddb51 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/anc/domain/interactor/AncSliceInteractorTest.kt @@ -0,0 +1,101 @@ +/* + * 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() + } + } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/slice/SliceViewManagerExt.kt b/packages/SystemUI/src/com/android/systemui/slice/SliceViewManagerExt.kt new file mode 100644 index 000000000000..384acc493c4a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/slice/SliceViewManagerExt.kt @@ -0,0 +1,40 @@ +/* + * 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 = + ConflatedCallbackFlow.conflatedCallbackFlow { + val callback = SliceViewManager.SliceCallback { launch { send(it) } } + + val slice = bindSlice(sliceUri) + send(slice) + registerSliceCallback(sliceUri, callback) + awaitClose { unregisterSliceCallback(sliceUri, callback) } + } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/AncModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/AncModule.kt new file mode 100644 index 000000000000..66df45ca4cfa --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/AncModule.kt @@ -0,0 +1,40 @@ +/* + * 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)) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java index 32856373dbe9..c6aee428ce6a 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java @@ -57,6 +57,7 @@ import dagger.multibindings.IntoSet; @Module( includes = { AudioModule.class, + AncModule.class, CaptioningModule.class, MediaDevicesModule.class }, diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/data/repository/AncSliceRepository.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/data/repository/AncSliceRepository.kt new file mode 100644 index 000000000000..8f18aa8021ae --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/data/repository/AncSliceRepository.kt @@ -0,0 +1,108 @@ +/* + * 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 +} + +@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 { + 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 + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/domain/AncAvailabilityCriteria.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/domain/AncAvailabilityCriteria.kt new file mode 100644 index 000000000000..89b927480783 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/domain/AncAvailabilityCriteria.kt @@ -0,0 +1,35 @@ +/* + * 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 = ancSliceInteractor.ancSlice.map { it != null } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/domain/interactor/AncSliceInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/domain/interactor/AncSliceInteractor.kt new file mode 100644 index 000000000000..91af622074a0 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/anc/domain/interactor/AncSliceInteractor.kt @@ -0,0 +1,76 @@ +/* + * 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 = + 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 + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/anc/FakeSliceFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/anc/FakeSliceFactory.kt new file mode 100644 index 000000000000..fc406eaae0f1 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/anc/FakeSliceFactory.kt @@ -0,0 +1,51 @@ +/* + * 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) + } + } + ) + } + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/anc/VolumePanelAncKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/anc/VolumePanelAncKosmos.kt new file mode 100644 index 000000000000..f9b7e69eea7d --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/anc/VolumePanelAncKosmos.kt @@ -0,0 +1,29 @@ +/* + * 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) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/anc/data/repository/FakeAncSliceRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/anc/data/repository/FakeAncSliceRepository.kt new file mode 100644 index 000000000000..b66d7f974eca --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/anc/data/repository/FakeAncSliceRepository.kt @@ -0,0 +1,33 @@ +/* + * 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>() + + override fun ancSlice(width: Int): Flow = + sliceByWidth.getOrPut(width) { MutableStateFlow(null) } + + fun putSlice(width: Int, slice: Slice?) { + sliceByWidth.getOrPut(width) { MutableStateFlow(null) }.value = slice + } +} -- GitLab