Merge "Add Live Captioning repository" into main
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 0000000..5bcb82d
--- /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 0000000..858c8b3
--- /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 0000000..a5233e7
--- /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 0000000..fd253c6
--- /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 0000000..ea67eea
--- /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 5cb6fa8..2718839 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 @@
@Module(
includes = {
AudioModule.class,
+ CaptioningModule.class,
MediaDevicesModule.class
},
subcomponents = {