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 = {