diff options
| author | 2024-02-05 21:15:25 +0000 | |
|---|---|---|
| committer | 2024-02-05 21:15:25 +0000 | |
| commit | ec6216831e872d0760f2a0c73dbc02c09db211c3 (patch) | |
| tree | b6269c2d65466cb97142ed18d2172cc3b48ab6eb | |
| parent | 72dc708c55a89080e0a6ae529b818fbe6e034e55 (diff) | |
| parent | 2c81d2c03296e33afeeec423cccbe832d2f823d3 (diff) | |
Merge "Add Live Captioning repository" into main
6 files changed, 343 insertions, 0 deletions
diff --git a/packages/SettingsLib/src/com/android/settingslib/view/accessibility/data/repository/CaptioningRepository.kt b/packages/SettingsLib/src/com/android/settingslib/view/accessibility/data/repository/CaptioningRepository.kt new file mode 100644 index 000000000000..5bcb82d6c579 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/view/accessibility/data/repository/CaptioningRepository.kt @@ -0,0 +1,110 @@ +/* + * 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.view.accessibility.data.repository + +import android.view.accessibility.CaptioningManager +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.ProducerScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.filterIsInstance +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext + +interface CaptioningRepository { + + /** The system audio caption enabled state. */ + val isSystemAudioCaptioningEnabled: StateFlow<Boolean> + + /** The system audio caption UI enabled state. */ + val isSystemAudioCaptioningUiEnabled: StateFlow<Boolean> + + /** Sets [isSystemAudioCaptioningEnabled]. */ + suspend fun setIsSystemAudioCaptioningEnabled(isEnabled: Boolean) +} + +class CaptioningRepositoryImpl( + private val captioningManager: CaptioningManager, + private val backgroundCoroutineContext: CoroutineContext, + coroutineScope: CoroutineScope, +) : CaptioningRepository { + + private val captioningChanges: SharedFlow<CaptioningChange> = + callbackFlow { + val listener = CaptioningChangeProducingListener(this) + captioningManager.addCaptioningChangeListener(listener) + awaitClose { captioningManager.removeCaptioningChangeListener(listener) } + } + .shareIn(coroutineScope, SharingStarted.WhileSubscribed(), replay = 0) + + override val isSystemAudioCaptioningEnabled: StateFlow<Boolean> = + captioningChanges + .filterIsInstance(CaptioningChange.IsSystemAudioCaptioningEnabled::class) + .map { it.isEnabled } + .stateIn( + coroutineScope, + SharingStarted.WhileSubscribed(), + captioningManager.isSystemAudioCaptioningEnabled + ) + + override val isSystemAudioCaptioningUiEnabled: StateFlow<Boolean> = + captioningChanges + .filterIsInstance(CaptioningChange.IsSystemUICaptioningEnabled::class) + .map { it.isEnabled } + .stateIn( + coroutineScope, + SharingStarted.WhileSubscribed(), + captioningManager.isSystemAudioCaptioningUiEnabled, + ) + + override suspend fun setIsSystemAudioCaptioningEnabled(isEnabled: Boolean) { + withContext(backgroundCoroutineContext) { + captioningManager.isSystemAudioCaptioningEnabled = isEnabled + } + } + + private sealed interface CaptioningChange { + + data class IsSystemAudioCaptioningEnabled(val isEnabled: Boolean) : CaptioningChange + + data class IsSystemUICaptioningEnabled(val isEnabled: Boolean) : CaptioningChange + } + + private class CaptioningChangeProducingListener( + private val scope: ProducerScope<CaptioningChange> + ) : CaptioningManager.CaptioningChangeListener() { + + override fun onSystemAudioCaptioningChanged(enabled: Boolean) { + emitChange(CaptioningChange.IsSystemAudioCaptioningEnabled(enabled)) + } + + override fun onSystemAudioCaptioningUiChanged(enabled: Boolean) { + emitChange(CaptioningChange.IsSystemUICaptioningEnabled(enabled)) + } + + private fun emitChange(change: CaptioningChange) { + scope.launch { scope.send(change) } + } + } +} diff --git a/packages/SettingsLib/src/com/android/settingslib/view/accessibility/domain/interactor/CaptioningInteractor.kt b/packages/SettingsLib/src/com/android/settingslib/view/accessibility/domain/interactor/CaptioningInteractor.kt new file mode 100644 index 000000000000..858c8b369a14 --- /dev/null +++ b/packages/SettingsLib/src/com/android/settingslib/view/accessibility/domain/interactor/CaptioningInteractor.kt @@ -0,0 +1,32 @@ +/* + * 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.view.accessibility.domain.interactor + +import com.android.settingslib.view.accessibility.data.repository.CaptioningRepository +import kotlinx.coroutines.flow.StateFlow + +class CaptioningInteractor(private val repository: CaptioningRepository) { + + val isSystemAudioCaptioningEnabled: StateFlow<Boolean> + get() = repository.isSystemAudioCaptioningEnabled + + val isSystemAudioCaptioningUiEnabled: StateFlow<Boolean> + get() = repository.isSystemAudioCaptioningUiEnabled + + suspend fun setIsSystemAudioCaptioningEnabled(enabled: Boolean) = + repository.setIsSystemAudioCaptioningEnabled(enabled) +} diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/view/accessibility/data/repository/CaptioningRepositoryTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/view/accessibility/data/repository/CaptioningRepositoryTest.kt new file mode 100644 index 000000000000..a5233e7d51d6 --- /dev/null +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/view/accessibility/data/repository/CaptioningRepositoryTest.kt @@ -0,0 +1,113 @@ +/* + * 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.view.accessibility.data.repository + +import android.view.accessibility.CaptioningManager +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentCaptor +import org.mockito.Captor +import org.mockito.Mock +import org.mockito.Mockito.verify +import org.mockito.Mockito.`when` +import org.mockito.MockitoAnnotations + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@Suppress("UnspecifiedRegisterReceiverFlag") +@RunWith(AndroidJUnit4::class) +class CaptioningRepositoryTest { + + @Captor + private lateinit var listenerCaptor: ArgumentCaptor<CaptioningManager.CaptioningChangeListener> + + @Mock private lateinit var captioningManager: CaptioningManager + + private lateinit var underTest: CaptioningRepository + + private val testScope = TestScope() + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + + underTest = + CaptioningRepositoryImpl( + captioningManager, + testScope.testScheduler, + testScope.backgroundScope + ) + } + + @Test + fun isSystemAudioCaptioningEnabled_change_repositoryEmits() { + testScope.runTest { + `when`(captioningManager.isEnabled).thenReturn(false) + val isSystemAudioCaptioningEnabled = mutableListOf<Boolean>() + underTest.isSystemAudioCaptioningEnabled + .onEach { isSystemAudioCaptioningEnabled.add(it) } + .launchIn(backgroundScope) + runCurrent() + + triggerOnSystemAudioCaptioningChange() + runCurrent() + + assertThat(isSystemAudioCaptioningEnabled) + .containsExactlyElementsIn(listOf(false, true)) + .inOrder() + } + } + + @Test + fun isSystemAudioCaptioningUiEnabled_change_repositoryEmits() { + testScope.runTest { + `when`(captioningManager.isSystemAudioCaptioningUiEnabled).thenReturn(false) + val isSystemAudioCaptioningUiEnabled = mutableListOf<Boolean>() + underTest.isSystemAudioCaptioningUiEnabled + .onEach { isSystemAudioCaptioningUiEnabled.add(it) } + .launchIn(backgroundScope) + runCurrent() + + triggerSystemAudioCaptioningUiChange() + runCurrent() + + assertThat(isSystemAudioCaptioningUiEnabled) + .containsExactlyElementsIn(listOf(false, true)) + .inOrder() + } + } + + private fun triggerSystemAudioCaptioningUiChange(enabled: Boolean = true) { + verify(captioningManager).addCaptioningChangeListener(listenerCaptor.capture()) + listenerCaptor.value.onSystemAudioCaptioningUiChanged(enabled) + } + + private fun triggerOnSystemAudioCaptioningChange(enabled: Boolean = true) { + verify(captioningManager).addCaptioningChangeListener(listenerCaptor.capture()) + listenerCaptor.value.onSystemAudioCaptioningChanged(enabled) + } +} diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/view/accessibility/data/repository/FakeCaptioningRepository.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/view/accessibility/data/repository/FakeCaptioningRepository.kt new file mode 100644 index 000000000000..fd253c6807f2 --- /dev/null +++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/view/accessibility/data/repository/FakeCaptioningRepository.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.settingslib.view.accessibility.data.repository + +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeCaptioningRepository : CaptioningRepository { + + private val mutableIsSystemAudioCaptioningEnabled = MutableStateFlow(false) + override val isSystemAudioCaptioningEnabled: StateFlow<Boolean> + get() = mutableIsSystemAudioCaptioningEnabled.asStateFlow() + + private val mutableIsSystemAudioCaptioningUiEnabled = MutableStateFlow(false) + override val isSystemAudioCaptioningUiEnabled: StateFlow<Boolean> + get() = mutableIsSystemAudioCaptioningUiEnabled.asStateFlow() + + override suspend fun setIsSystemAudioCaptioningEnabled(isEnabled: Boolean) { + mutableIsSystemAudioCaptioningEnabled.value = isEnabled + } + + fun setIsSystemAudioCaptioningUiEnabled(isSystemAudioCaptioningUiEnabled: Boolean) { + mutableIsSystemAudioCaptioningUiEnabled.value = isSystemAudioCaptioningUiEnabled + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/CaptioningModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/CaptioningModule.kt new file mode 100644 index 000000000000..ea67eea1dd8b --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/CaptioningModule.kt @@ -0,0 +1,47 @@ +/* + * 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.view.accessibility.CaptioningManager +import com.android.settingslib.view.accessibility.data.repository.CaptioningRepository +import com.android.settingslib.view.accessibility.data.repository.CaptioningRepositoryImpl +import com.android.settingslib.view.accessibility.domain.interactor.CaptioningInteractor +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.dagger.qualifiers.Background +import dagger.Module +import dagger.Provides +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineScope + +@Module +interface CaptioningModule { + + companion object { + + @Provides + fun provideCaptioningRepository( + captioningManager: CaptioningManager, + @Background coroutineContext: CoroutineContext, + @Application coroutineScope: CoroutineScope, + ): CaptioningRepository = + CaptioningRepositoryImpl(captioningManager, coroutineContext, coroutineScope) + + @Provides + fun provideCaptioningInteractor(repository: CaptioningRepository): CaptioningInteractor = + CaptioningInteractor(repository) + } +} 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 5cb6fa8c8046..2718839db651 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/VolumeModule.java @@ -62,6 +62,7 @@ import kotlinx.coroutines.CoroutineScope; @Module( includes = { AudioModule.class, + CaptioningModule.class, MediaDevicesModule.class }, subcomponents = { |