Add audio stream managing functionality to AudioRepository.
Bug: 318349141
Flag: aconfig new_volume_panel DISABLED
Test: atest AudioRepositoryTest
Change-Id: Ib244946dcd60fa4eb2a2b2b3afdd100a43e79df8
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
index 3355fb3..6761aa7 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioRepository.kt
@@ -16,29 +16,71 @@
package com.android.settingslib.volume.data.repository
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.media.AudioDeviceInfo
import android.media.AudioManager
+import android.media.AudioManager.OnCommunicationDeviceChangedListener
+import androidx.concurrent.futures.DirectExecutor
import com.android.internal.util.ConcurrentUtils
+import com.android.settingslib.volume.shared.model.AudioStream
+import com.android.settingslib.volume.shared.model.AudioStreamModel
+import com.android.settingslib.volume.shared.model.RingerMode
import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
+import kotlinx.coroutines.flow.filter
+import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
-/** Provides audio managing functionality and data. */
+/** Provides audio streams state and managing functionality. */
interface AudioRepository {
/** Current [AudioManager.getMode]. */
val mode: StateFlow<Int>
+
+ /**
+ * Ringtone mode.
+ *
+ * @see AudioManager.getRingerModeInternal
+ */
+ val ringerMode: StateFlow<RingerMode>
+
+ /**
+ * Communication device. Emits null when there is no communication device available.
+ *
+ * @see AudioDeviceInfo.getType
+ */
+ val communicationDevice: StateFlow<AudioDeviceInfo?>
+
+ /** State of the [AudioStream]. */
+ suspend fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel>
+
+ /** Current state of the [AudioStream]. */
+ suspend fun getCurrentAudioStream(audioStream: AudioStream): AudioStreamModel
+
+ suspend fun setVolume(audioStream: AudioStream, volume: Int)
+
+ suspend fun setMuted(audioStream: AudioStream, isMuted: Boolean)
}
class AudioRepositoryImpl(
+ private val context: Context,
private val audioManager: AudioManager,
- backgroundCoroutineContext: CoroutineContext,
- coroutineScope: CoroutineScope,
+ private val backgroundCoroutineContext: CoroutineContext,
+ private val coroutineScope: CoroutineScope,
) : AudioRepository {
override val mode: StateFlow<Int> =
@@ -50,4 +92,117 @@
}
.flowOn(backgroundCoroutineContext)
.stateIn(coroutineScope, SharingStarted.WhileSubscribed(), audioManager.mode)
+
+ private val audioManagerIntents: SharedFlow<String> =
+ callbackFlow {
+ val receiver =
+ object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent) {
+ intent.action?.let { action -> launch { send(action) } }
+ }
+ }
+ context.registerReceiver(
+ receiver,
+ IntentFilter().apply {
+ for (action in allActions) {
+ addAction(action)
+ }
+ }
+ )
+
+ awaitClose { context.unregisterReceiver(receiver) }
+ }
+ .shareIn(coroutineScope, SharingStarted.WhileSubscribed())
+
+ override val ringerMode: StateFlow<RingerMode> =
+ audioManagerIntents
+ .filter { ringerActions.contains(it) }
+ .map { RingerMode(audioManager.ringerModeInternal) }
+ .flowOn(backgroundCoroutineContext)
+ .stateIn(
+ coroutineScope,
+ SharingStarted.WhileSubscribed(),
+ RingerMode(audioManager.ringerModeInternal),
+ )
+
+ override val communicationDevice: StateFlow<AudioDeviceInfo?>
+ get() =
+ callbackFlow {
+ val listener = OnCommunicationDeviceChangedListener { trySend(Unit) }
+ audioManager.addOnCommunicationDeviceChangedListener(
+ DirectExecutor.INSTANCE,
+ listener
+ )
+
+ awaitClose { audioManager.removeOnCommunicationDeviceChangedListener(listener) }
+ }
+ .filterNotNull()
+ .map { audioManager.communicationDevice }
+ .flowOn(backgroundCoroutineContext)
+ .stateIn(
+ coroutineScope,
+ SharingStarted.WhileSubscribed(),
+ audioManager.communicationDevice,
+ )
+
+ override suspend fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> {
+ return audioManagerIntents
+ .filter { modelActions.contains(it) }
+ .map { getCurrentAudioStream(audioStream) }
+ .flowOn(backgroundCoroutineContext)
+ }
+
+ override suspend fun getCurrentAudioStream(audioStream: AudioStream): AudioStreamModel {
+ return withContext(backgroundCoroutineContext) {
+ AudioStreamModel(
+ audioStream = audioStream,
+ minVolume = getMinVolume(audioStream),
+ maxVolume = audioManager.getStreamMaxVolume(audioStream.value),
+ volume = audioManager.getStreamVolume(audioStream.value),
+ isAffectedByRingerMode =
+ audioManager.isStreamAffectedByRingerMode(audioStream.value),
+ isMuted = audioManager.isStreamMute(audioStream.value)
+ )
+ }
+ }
+
+ override suspend fun setVolume(audioStream: AudioStream, volume: Int) =
+ withContext(backgroundCoroutineContext) {
+ audioManager.setStreamVolume(audioStream.value, volume, 0)
+ }
+
+ override suspend fun setMuted(audioStream: AudioStream, isMuted: Boolean) =
+ withContext(backgroundCoroutineContext) {
+ if (isMuted) {
+ audioManager.adjustStreamVolume(audioStream.value, 0, AudioManager.ADJUST_MUTE)
+ } else {
+ audioManager.adjustStreamVolume(audioStream.value, 0, AudioManager.ADJUST_UNMUTE)
+ }
+ }
+
+ private fun getMinVolume(stream: AudioStream): Int =
+ try {
+ audioManager.getStreamMinVolume(stream.value)
+ } catch (e: IllegalArgumentException) {
+ // Fallback to STREAM_VOICE_CALL because
+ // CallVolumePreferenceController.java default
+ // return STREAM_VOICE_CALL in getAudioStream
+ audioManager.getStreamMinVolume(AudioManager.STREAM_VOICE_CALL)
+ }
+
+ private companion object {
+ val modelActions =
+ setOf(
+ AudioManager.STREAM_MUTE_CHANGED_ACTION,
+ AudioManager.MASTER_MUTE_CHANGED_ACTION,
+ AudioManager.VOLUME_CHANGED_ACTION,
+ AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION,
+ AudioManager.STREAM_DEVICES_CHANGED_ACTION,
+ )
+ val ringerActions =
+ setOf(
+ AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION,
+ )
+ val allActions = ringerActions + modelActions
+ }
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSystemRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSystemRepository.kt
new file mode 100644
index 0000000..2b12936
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSystemRepository.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.volume.data.repository
+
+import android.content.Context
+import android.media.AudioSystem
+
+/** Provides the current state of the audio system. */
+interface AudioSystemRepository {
+
+ val isSingleVolume: Boolean
+}
+
+class AudioSystemRepositoryImpl(private val context: Context) : AudioSystemRepository {
+
+ override val isSingleVolume: Boolean
+ get() = AudioSystem.isSingleVolume(context)
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStream.kt b/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStream.kt
new file mode 100644
index 0000000..58f3c2d
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStream.kt
@@ -0,0 +1,45 @@
+/*
+ * 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.volume.shared.model
+
+import android.media.AudioManager
+
+/** Type-safe wrapper for [AudioManager] audio stream. */
+@JvmInline
+value class AudioStream(val value: Int) {
+ init {
+ require(value in supportedStreamTypes) { "Unsupported stream=$value" }
+ }
+
+ private companion object {
+ val supportedStreamTypes =
+ setOf(
+ AudioManager.STREAM_VOICE_CALL,
+ AudioManager.STREAM_SYSTEM,
+ AudioManager.STREAM_RING,
+ AudioManager.STREAM_MUSIC,
+ AudioManager.STREAM_ALARM,
+ AudioManager.STREAM_NOTIFICATION,
+ AudioManager.STREAM_BLUETOOTH_SCO,
+ AudioManager.STREAM_SYSTEM_ENFORCED,
+ AudioManager.STREAM_DTMF,
+ AudioManager.STREAM_TTS,
+ AudioManager.STREAM_ACCESSIBILITY,
+ AudioManager.STREAM_ASSISTANT,
+ )
+ }
+}
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStreamModel.kt b/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStreamModel.kt
new file mode 100644
index 0000000..c1be1ee
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/AudioStreamModel.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.volume.shared.model
+
+/** Current state of the audio stream. */
+data class AudioStreamModel(
+ val audioStream: AudioStream,
+ val volume: Int,
+ val minVolume: Int,
+ val maxVolume: Int,
+ val isAffectedByRingerMode: Boolean,
+ val isMuted: Boolean,
+)
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/RingerMode.kt b/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/RingerMode.kt
new file mode 100644
index 0000000..9f03927
--- /dev/null
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/shared/model/RingerMode.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.volume.shared.model
+
+import android.media.AudioManager
+
+/** Type-safe wrapper for [AudioManager] ringer mode. */
+@JvmInline
+value class RingerMode(val value: Int) {
+
+ init {
+ require(value in supportedRingerModes) { "Unsupported stream=$value" }
+ }
+
+ private companion object {
+ val supportedRingerModes =
+ setOf(
+ AudioManager.RINGER_MODE_SILENT,
+ AudioManager.RINGER_MODE_VIBRATE,
+ AudioManager.RINGER_MODE_NORMAL,
+ AudioManager.RINGER_MODE_MAX,
+ )
+ }
+}
diff --git a/packages/SettingsLib/tests/integ/Android.bp b/packages/SettingsLib/tests/integ/Android.bp
index ce3a7ba..f303ab5 100644
--- a/packages/SettingsLib/tests/integ/Android.bp
+++ b/packages/SettingsLib/tests/integ/Android.bp
@@ -40,6 +40,8 @@
"android.test.runner",
"telephony-common",
"android.test.base",
+ "android.test.mock",
+ "truth",
],
platform_apis: true,
@@ -49,16 +51,23 @@
"androidx.test.core",
"androidx.test.rules",
"androidx.test.espresso.core",
+ "androidx.test.ext.junit",
"flag-junit",
- "mockito-target-minus-junit4",
+ "kotlinx_coroutines_test",
+ "mockito-target-extended-minus-junit4",
"platform-test-annotations",
"truth",
"SettingsLibDeviceStateRotationLock",
"SettingsLibSettingsSpinner",
"SettingsLibUsageProgressBarPreference",
"settingslib_media_flags_lib",
- "kotlinx_coroutines_test",
],
+ jni_libs: [
+ "libdexmakerjvmtiagent",
+ "libmultiplejvmtiagentsinterferenceagent",
+ "libstaticjvmtiagent",
+ ],
dxflags: ["--multi-dex"],
+ manifest: "AndroidManifest.xml",
}
diff --git a/packages/SettingsLib/tests/integ/AndroidManifest.xml b/packages/SettingsLib/tests/integ/AndroidManifest.xml
index 32048ca..9fb1c1f 100644
--- a/packages/SettingsLib/tests/integ/AndroidManifest.xml
+++ b/packages/SettingsLib/tests/integ/AndroidManifest.xml
@@ -25,7 +25,7 @@
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
- <application>
+ <application android:debuggable="true" android:testOnly="true">
<uses-library android:name="android.test.runner" />
<activity android:name=".drawer.SettingsDrawerActivityTest$TestActivity"/>
diff --git a/packages/SettingsLib/tests/integ/AndroidTest.xml b/packages/SettingsLib/tests/integ/AndroidTest.xml
index d0aee88..9de6019 100644
--- a/packages/SettingsLib/tests/integ/AndroidTest.xml
+++ b/packages/SettingsLib/tests/integ/AndroidTest.xml
@@ -16,6 +16,7 @@
<configuration description="Runs Tests for SettingsLib.">
<target_preparer class="com.android.tradefed.targetprep.TestAppInstallSetup">
<option name="test-file-name" value="SettingsLibTests.apk" />
+ <option name="install-arg" value="-t" />
</target_preparer>
<option name="test-suite-tag" value="apct" />
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt
new file mode 100644
index 0000000..7b70c64
--- /dev/null
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/AudioRepositoryTest.kt
@@ -0,0 +1,281 @@
+/*
+ * 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.volume.data.repository
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.media.AudioDeviceInfo
+import android.media.AudioManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.settingslib.volume.shared.model.AudioStream
+import com.android.settingslib.volume.shared.model.AudioStreamModel
+import com.android.settingslib.volume.shared.model.RingerMode
+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.ArgumentMatchers.any
+import org.mockito.ArgumentMatchers.anyInt
+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 AudioRepositoryTest {
+
+ @Captor private lateinit var receiverCaptor: ArgumentCaptor<BroadcastReceiver>
+ @Captor
+ private lateinit var modeListenerCaptor: ArgumentCaptor<AudioManager.OnModeChangedListener>
+ @Captor
+ private lateinit var communicationDeviceListenerCaptor:
+ ArgumentCaptor<AudioManager.OnCommunicationDeviceChangedListener>
+
+ @Mock private lateinit var context: Context
+ @Mock private lateinit var audioManager: AudioManager
+ @Mock private lateinit var communicationDevice: AudioDeviceInfo
+
+ private val volumeByStream: MutableMap<Int, Int> = mutableMapOf()
+ private val isAffectedByRingerModeByStream: MutableMap<Int, Boolean> = mutableMapOf()
+ private val isMuteByStream: MutableMap<Int, Boolean> = mutableMapOf()
+ private val testScope = TestScope()
+
+ private lateinit var underTest: AudioRepository
+
+ @Before
+ fun setup() {
+ MockitoAnnotations.initMocks(this)
+
+ `when`(audioManager.mode).thenReturn(AudioManager.MODE_RINGTONE)
+ `when`(audioManager.communicationDevice).thenReturn(communicationDevice)
+ `when`(audioManager.getStreamMinVolume(anyInt())).thenReturn(MIN_VOLUME)
+ `when`(audioManager.getStreamMaxVolume(anyInt())).thenReturn(MAX_VOLUME)
+ `when`(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_NORMAL)
+ `when`(audioManager.setStreamVolume(anyInt(), anyInt(), anyInt())).then {
+ volumeByStream[it.arguments[0] as Int] = it.arguments[1] as Int
+ triggerIntent(AudioManager.ACTION_VOLUME_CHANGED)
+ }
+ `when`(audioManager.adjustStreamVolume(anyInt(), anyInt(), anyInt())).then {
+ isMuteByStream[it.arguments[0] as Int] = it.arguments[2] == AudioManager.ADJUST_MUTE
+ triggerIntent(AudioManager.STREAM_MUTE_CHANGED_ACTION)
+ }
+ `when`(audioManager.getStreamVolume(anyInt())).thenAnswer {
+ volumeByStream.getOrDefault(it.arguments[0] as Int, 0)
+ }
+ `when`(audioManager.isStreamAffectedByRingerMode(anyInt())).thenAnswer {
+ isAffectedByRingerModeByStream.getOrDefault(it.arguments[0] as Int, false)
+ }
+ `when`(audioManager.isStreamMute(anyInt())).thenAnswer {
+ isMuteByStream.getOrDefault(it.arguments[0] as Int, false)
+ }
+
+ underTest =
+ AudioRepositoryImpl(
+ context,
+ audioManager,
+ testScope.testScheduler,
+ testScope.backgroundScope,
+ )
+ }
+
+ @Test
+ fun audioModeChanges_repositoryEmits() {
+ testScope.runTest {
+ val modes = mutableListOf<Int>()
+ underTest.mode.onEach { modes.add(it) }.launchIn(backgroundScope)
+ runCurrent()
+
+ triggerModeChange(AudioManager.MODE_IN_CALL)
+ runCurrent()
+
+ assertThat(modes).containsExactly(AudioManager.MODE_RINGTONE, AudioManager.MODE_IN_CALL)
+ }
+ }
+
+ @Test
+ fun ringerModeChanges_repositoryEmits() {
+ testScope.runTest {
+ val modes = mutableListOf<RingerMode>()
+ underTest.ringerMode.onEach { modes.add(it) }.launchIn(backgroundScope)
+ runCurrent()
+
+ `when`(audioManager.ringerModeInternal).thenReturn(AudioManager.RINGER_MODE_SILENT)
+ triggerIntent(AudioManager.INTERNAL_RINGER_MODE_CHANGED_ACTION)
+ runCurrent()
+
+ assertThat(modes)
+ .containsExactly(
+ RingerMode(AudioManager.RINGER_MODE_NORMAL),
+ RingerMode(AudioManager.RINGER_MODE_SILENT),
+ )
+ }
+ }
+
+ @Test
+ fun communicationDeviceChanges_repositoryEmits() {
+ testScope.runTest {
+ var device: AudioDeviceInfo? = null
+ underTest.communicationDevice.onEach { device = it }.launchIn(backgroundScope)
+ runCurrent()
+
+ triggerConnectedDeviceChange(communicationDevice)
+ runCurrent()
+
+ assertThat(device).isSameInstanceAs(communicationDevice)
+ }
+ }
+
+ @Test
+ fun adjustingVolume_changesTheStream() {
+ testScope.runTest {
+ val audioStream = AudioStream(AudioManager.STREAM_SYSTEM)
+ var streamModel: AudioStreamModel? = null
+ underTest
+ .getAudioStream(audioStream)
+ .onEach { streamModel = it }
+ .launchIn(backgroundScope)
+ runCurrent()
+
+ underTest.setVolume(audioStream, 50)
+ runCurrent()
+
+ assertThat(streamModel)
+ .isEqualTo(
+ AudioStreamModel(
+ audioStream = audioStream,
+ volume = 50,
+ minVolume = MIN_VOLUME,
+ maxVolume = MAX_VOLUME,
+ isAffectedByRingerMode = false,
+ isMuted = false,
+ )
+ )
+ }
+ }
+
+ @Test
+ fun adjustingVolume_currentModeIsUpToDate() {
+ testScope.runTest {
+ val audioStream = AudioStream(AudioManager.STREAM_SYSTEM)
+ var streamModel: AudioStreamModel? = null
+ underTest
+ .getAudioStream(audioStream)
+ .onEach { streamModel = it }
+ .launchIn(backgroundScope)
+ runCurrent()
+
+ underTest.setVolume(audioStream, 50)
+ runCurrent()
+
+ assertThat(underTest.getCurrentAudioStream(audioStream)).isEqualTo(streamModel)
+ }
+ }
+
+ @Test
+ fun muteStream_mutesTheStream() {
+ testScope.runTest {
+ val audioStream = AudioStream(AudioManager.STREAM_SYSTEM)
+ var streamModel: AudioStreamModel? = null
+ underTest
+ .getAudioStream(audioStream)
+ .onEach { streamModel = it }
+ .launchIn(backgroundScope)
+ runCurrent()
+
+ underTest.setMuted(audioStream, true)
+ runCurrent()
+
+ assertThat(streamModel)
+ .isEqualTo(
+ AudioStreamModel(
+ audioStream = audioStream,
+ volume = 0,
+ minVolume = MIN_VOLUME,
+ maxVolume = MAX_VOLUME,
+ isAffectedByRingerMode = false,
+ isMuted = true,
+ )
+ )
+ }
+ }
+
+ @Test
+ fun unmuteStream_unmutesTheStream() {
+ testScope.runTest {
+ val audioStream = AudioStream(AudioManager.STREAM_SYSTEM)
+ isMuteByStream[audioStream.value] = true
+ var streamModel: AudioStreamModel? = null
+ underTest
+ .getAudioStream(audioStream)
+ .onEach { streamModel = it }
+ .launchIn(backgroundScope)
+ runCurrent()
+
+ underTest.setMuted(audioStream, false)
+ runCurrent()
+
+ assertThat(streamModel)
+ .isEqualTo(
+ AudioStreamModel(
+ audioStream = audioStream,
+ volume = 0,
+ minVolume = MIN_VOLUME,
+ maxVolume = MAX_VOLUME,
+ isAffectedByRingerMode = false,
+ isMuted = false,
+ )
+ )
+ }
+ }
+
+ private fun triggerConnectedDeviceChange(communicationDevice: AudioDeviceInfo?) {
+ verify(audioManager)
+ .addOnCommunicationDeviceChangedListener(
+ any(),
+ communicationDeviceListenerCaptor.capture(),
+ )
+ communicationDeviceListenerCaptor.value.onCommunicationDeviceChanged(communicationDevice)
+ }
+
+ private fun triggerModeChange(mode: Int) {
+ verify(audioManager).addOnModeChangedListener(any(), modeListenerCaptor.capture())
+ modeListenerCaptor.value.onModeChanged(mode)
+ }
+
+ private fun triggerIntent(action: String) {
+ verify(context).registerReceiver(receiverCaptor.capture(), any())
+ receiverCaptor.value.onReceive(context, Intent(action))
+ }
+
+ private companion object {
+ const val MIN_VOLUME = 0
+ const val MAX_VOLUME = 100
+ }
+}
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioRepository.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioRepository.kt
index 686362f..dddf8e82 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioRepository.kt
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/FakeAudioRepository.kt
@@ -16,9 +16,15 @@
package com.android.settingslib.volume.data.repository
+import android.media.AudioDeviceInfo
+import com.android.settingslib.volume.shared.model.AudioStream
+import com.android.settingslib.volume.shared.model.AudioStreamModel
+import com.android.settingslib.volume.shared.model.RingerMode
+import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.update
class FakeAudioRepository : AudioRepository {
@@ -26,7 +32,59 @@
override val mode: StateFlow<Int>
get() = mutableMode.asStateFlow()
+ private val mutableRingerMode = MutableStateFlow(RingerMode(0))
+ override val ringerMode: StateFlow<RingerMode>
+ get() = mutableRingerMode.asStateFlow()
+
+ private val mutableCommunicationDevice = MutableStateFlow<AudioDeviceInfo?>(null)
+ override val communicationDevice: StateFlow<AudioDeviceInfo?>
+ get() = mutableCommunicationDevice.asStateFlow()
+
+ private val models: MutableMap<AudioStream, MutableStateFlow<AudioStreamModel>> = mutableMapOf()
+
+ private fun getAudioStreamModelState(
+ audioStream: AudioStream
+ ): MutableStateFlow<AudioStreamModel> =
+ models.getOrPut(audioStream) {
+ MutableStateFlow(
+ AudioStreamModel(
+ audioStream = audioStream,
+ volume = 0,
+ minVolume = 0,
+ maxVolume = 0,
+ isAffectedByRingerMode = false,
+ isMuted = false,
+ )
+ )
+ }
+
+ override suspend fun getAudioStream(audioStream: AudioStream): Flow<AudioStreamModel> =
+ getAudioStreamModelState(audioStream).asStateFlow()
+
+ override suspend fun getCurrentAudioStream(audioStream: AudioStream): AudioStreamModel =
+ getAudioStreamModelState(audioStream).value
+
+ override suspend fun setVolume(audioStream: AudioStream, volume: Int) {
+ getAudioStreamModelState(audioStream).update { it.copy(volume = volume) }
+ }
+
+ override suspend fun setMuted(audioStream: AudioStream, isMuted: Boolean) {
+ getAudioStreamModelState(audioStream).update { it.copy(isMuted = isMuted) }
+ }
+
fun setMode(newMode: Int) {
mutableMode.value = newMode
}
+
+ fun setRingerMode(newRingerMode: RingerMode) {
+ mutableRingerMode.value = newRingerMode
+ }
+
+ fun setCommunicationDevice(device: AudioDeviceInfo?) {
+ mutableCommunicationDevice.value = device
+ }
+
+ fun setAudioStreamModel(model: AudioStreamModel) {
+ getAudioStreamModelState(model.audioStream).update { model }
+ }
}
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/domain/interactor/AudioModeInteractorTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/domain/interactor/AudioModeInteractorTest.kt
index 3bc1edc..4dbf865 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/domain/interactor/AudioModeInteractorTest.kt
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/domain/interactor/AudioModeInteractorTest.kt
@@ -17,8 +17,9 @@
package com.android.settingslib.volume.domain.interactor
import android.media.AudioManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import androidx.test.runner.AndroidJUnit4
+import com.android.settingslib.BaseTest
import com.android.settingslib.volume.data.repository.FakeAudioRepository
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -33,7 +34,7 @@
@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
@SmallTest
-class AudioModeInteractorTest {
+class AudioModeInteractorTest : BaseTest() {
private val testScope = TestScope()
private val fakeAudioRepository = FakeAudioRepository()
diff --git a/packages/SettingsLib/tests/unit/Android.bp b/packages/SettingsLib/tests/unit/Android.bp
index 6d6e2ff..e2eda4f 100644
--- a/packages/SettingsLib/tests/unit/Android.bp
+++ b/packages/SettingsLib/tests/unit/Android.bp
@@ -32,5 +32,7 @@
"androidx.test.ext.junit",
"androidx.test.runner",
"truth",
+ "kotlinx_coroutines_test",
+ "mockito-target-minus-junit4",
],
}