mirror of
https://github.com/signalapp/Signal-Android.git
synced 2026-02-23 03:05:26 +00:00
Add new remote config support for calling audio configuration.
This commit is contained in:
committed by
Cody Henthorne
parent
2421bbdabb
commit
570a475229
@@ -0,0 +1,429 @@
|
||||
/*
|
||||
* Copyright 2025 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.webrtc.audio
|
||||
|
||||
import android.app.Application
|
||||
import android.content.pm.PackageManager
|
||||
import android.media.audiofx.AcousticEchoCanceler
|
||||
import android.media.audiofx.NoiseSuppressor
|
||||
import android.os.Build
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import io.mockk.every
|
||||
import io.mockk.mockkObject
|
||||
import io.mockk.mockkStatic
|
||||
import io.mockk.unmockkObject
|
||||
import io.mockk.unmockkStatic
|
||||
import org.junit.After
|
||||
import org.junit.Before
|
||||
import org.junit.BeforeClass
|
||||
import org.junit.Rule
|
||||
import org.junit.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.robolectric.RobolectricTestRunner
|
||||
import org.robolectric.annotation.Config
|
||||
import org.robolectric.util.ReflectionHelpers
|
||||
import org.signal.core.util.logging.Log
|
||||
import org.signal.ringrtc.AudioConfig
|
||||
import org.thoughtcrime.securesms.dependencies.AppDependencies
|
||||
import org.thoughtcrime.securesms.testutil.MockAppDependenciesRule
|
||||
import org.thoughtcrime.securesms.testutil.SystemOutLogger
|
||||
import org.thoughtcrime.securesms.util.RemoteConfig
|
||||
|
||||
@RunWith(RobolectricTestRunner::class)
|
||||
@Config(manifest = Config.NONE, application = Application::class)
|
||||
class AudioDeviceConfigTest {
|
||||
companion object {
|
||||
private val REFERENCE_CONFIG = """
|
||||
[
|
||||
{
|
||||
"target": {
|
||||
"include": [
|
||||
"FP4"
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"oboe": true,
|
||||
"inVoiceComm": false
|
||||
},
|
||||
"final": true
|
||||
},
|
||||
{
|
||||
"target": {
|
||||
"custom": false,
|
||||
"include": [
|
||||
"Redmi Note 5",
|
||||
"FP2",
|
||||
"M1901F7*",
|
||||
"ASUS_I006D",
|
||||
"motorola one power",
|
||||
"FP3",
|
||||
"S22 FLIP",
|
||||
"Mi Note 10",
|
||||
"SM-S215DL",
|
||||
"T810S",
|
||||
"CPH2067"
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"softwareAec": true,
|
||||
"softwareNs": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"target": {
|
||||
"custom": true,
|
||||
"include": [
|
||||
"ONEPLUS A5010",
|
||||
"POCO F1",
|
||||
"SM-A320FL",
|
||||
"ONEPLUS A3003",
|
||||
"ONEPLUS A5000",
|
||||
"SM-G900F",
|
||||
"SM-G800F"
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"softwareAec": true,
|
||||
"softwareNs": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"target": {
|
||||
"custom": true,
|
||||
"exclude": [
|
||||
"Pixel 3",
|
||||
"SM-G973F"
|
||||
]
|
||||
},
|
||||
"settings": {
|
||||
"oboe": true
|
||||
}
|
||||
}
|
||||
]
|
||||
""".trimMargin()
|
||||
|
||||
private val REFERENCE_CONFIG_COMPRESSED = """
|
||||
[{"target":{"include":["FP4"]},"settings":{"oboe":true,"inVoiceComm":false},"final":true},{"target":{"custom":false,"include":["Redmi Note 5","FP2","M1901F7*","ASUS_I006D","motorola one power","FP3","S22 FLIP","Mi Note 10","SM-S215DL","T810S","CPH2067"]},"settings":{"softwareAec":true,"softwareNs":true}},{"target":{"custom":true,"include":["ONEPLUS A5010","POCO F1","SM-A320FL","ONEPLUS A3003","ONEPLUS A5000","SM-G900F","SM-G800F"]},"settings":{"softwareAec":true,"softwareNs":true}},{"target":{"custom":true,"exclude":["Pixel 3","SM-G973F"]},"settings":{"oboe":true}}]
|
||||
""".trimMargin()
|
||||
|
||||
@JvmStatic
|
||||
@BeforeClass
|
||||
fun setUpClass() {
|
||||
Log.initialize(SystemOutLogger())
|
||||
}
|
||||
}
|
||||
|
||||
@get:Rule
|
||||
val appDependencies = MockAppDependenciesRule()
|
||||
|
||||
@Before
|
||||
fun setup() {
|
||||
mockkStatic(AppDependencies::class)
|
||||
mockkStatic(PackageManager::class)
|
||||
mockkObject(RemoteConfig)
|
||||
mockkStatic(AcousticEchoCanceler::class)
|
||||
mockkStatic(NoiseSuppressor::class)
|
||||
}
|
||||
|
||||
@After
|
||||
fun tearDown() {
|
||||
mockkStatic(AppDependencies::class)
|
||||
unmockkStatic(PackageManager::class)
|
||||
unmockkObject(RemoteConfig)
|
||||
unmockkStatic(AcousticEchoCanceler::class)
|
||||
unmockkStatic(NoiseSuppressor::class)
|
||||
}
|
||||
|
||||
private fun mockEnvironment(
|
||||
sdk: Int = 34,
|
||||
aec: Boolean = true,
|
||||
ns: Boolean = true,
|
||||
lowLatency: Boolean = true
|
||||
) {
|
||||
ReflectionHelpers.setStaticField(Build.VERSION::class.java, "SDK_INT", sdk)
|
||||
|
||||
every { AcousticEchoCanceler.isAvailable() } returns aec
|
||||
every { NoiseSuppressor.isAvailable() } returns ns
|
||||
every {
|
||||
AppDependencies.application.applicationContext.packageManager
|
||||
.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY)
|
||||
} returns lowLatency
|
||||
}
|
||||
|
||||
private fun createAudioConfig(
|
||||
useOboe: Boolean = false,
|
||||
useSoftwareAec: Boolean = false,
|
||||
useSoftwareNs: Boolean = false,
|
||||
useInputLowLatency: Boolean = true,
|
||||
useInputVoiceComm: Boolean = true
|
||||
): AudioConfig {
|
||||
return AudioConfig().apply {
|
||||
this.useOboe = useOboe
|
||||
this.useSoftwareAec = useSoftwareAec
|
||||
this.useSoftwareNs = useSoftwareNs
|
||||
this.useInputLowLatency = useInputLowLatency
|
||||
this.useInputVoiceComm = useInputVoiceComm
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `empty config`() {
|
||||
mockEnvironment()
|
||||
|
||||
every { RemoteConfig.callingAudioDeviceConfig } returns ""
|
||||
assertThat(AudioDeviceConfig.computeConfig()).isEqualTo(createAudioConfig())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invalid config`() {
|
||||
mockEnvironment()
|
||||
|
||||
every { RemoteConfig.callingAudioDeviceConfig } returns "bad"
|
||||
assertThat(AudioDeviceConfig.computeConfig()).isEqualTo(createAudioConfig())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `reference`() {
|
||||
mockEnvironment()
|
||||
|
||||
every { RemoteConfig.callingAudioDeviceConfig } returns REFERENCE_CONFIG
|
||||
|
||||
assertThat(AudioDeviceConfig.computeConfig()).isEqualTo(createAudioConfig())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `targeted device`() {
|
||||
mockEnvironment()
|
||||
|
||||
ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "FP4")
|
||||
|
||||
every { RemoteConfig.callingAudioDeviceConfig } returns REFERENCE_CONFIG
|
||||
|
||||
assertThat(AudioDeviceConfig.computeConfig()).isEqualTo(createAudioConfig(useOboe = true, useInputVoiceComm = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hardware aec block list`() {
|
||||
mockEnvironment()
|
||||
|
||||
ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "Redmi Note 5")
|
||||
|
||||
every { RemoteConfig.callingAudioDeviceConfig } returns REFERENCE_CONFIG
|
||||
|
||||
assertThat(AudioDeviceConfig.computeConfig()).isEqualTo(createAudioConfig(useSoftwareAec = true, useSoftwareNs = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hardware aec block list wildcard`() {
|
||||
mockEnvironment()
|
||||
|
||||
ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "M1901F7A")
|
||||
|
||||
every { RemoteConfig.callingAudioDeviceConfig } returns REFERENCE_CONFIG
|
||||
|
||||
assertThat(AudioDeviceConfig.computeConfig()).isEqualTo(createAudioConfig(useSoftwareAec = true, useSoftwareNs = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `custom hardware aec block list`() {
|
||||
mockEnvironment()
|
||||
|
||||
ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "ONEPLUS A5000")
|
||||
ReflectionHelpers.setStaticField(Build::class.java, "PRODUCT", "lorem,lineageos=1.0.0,ipsum")
|
||||
|
||||
every { RemoteConfig.callingAudioDeviceConfig } returns REFERENCE_CONFIG
|
||||
|
||||
assertThat(AudioDeviceConfig.computeConfig()).isEqualTo(createAudioConfig(useOboe = true, useSoftwareAec = true, useSoftwareNs = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `custom device`() {
|
||||
mockEnvironment()
|
||||
|
||||
ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "Some Device")
|
||||
ReflectionHelpers.setStaticField(Build::class.java, "PRODUCT", "calyxos2.0")
|
||||
|
||||
every { RemoteConfig.callingAudioDeviceConfig } returns REFERENCE_CONFIG
|
||||
|
||||
assertThat(AudioDeviceConfig.computeConfig()).isEqualTo(createAudioConfig(useOboe = true))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `custom excluded device`() {
|
||||
mockEnvironment()
|
||||
|
||||
ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "Pixel 3")
|
||||
ReflectionHelpers.setStaticField(Build::class.java, "DISPLAY", "special calyxos,2.0")
|
||||
|
||||
every { RemoteConfig.callingAudioDeviceConfig } returns REFERENCE_CONFIG
|
||||
|
||||
assertThat(AudioDeviceConfig.computeConfig()).isEqualTo(createAudioConfig())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `overrides`() {
|
||||
mockEnvironment(aec = false, ns = false, lowLatency = false)
|
||||
|
||||
every { RemoteConfig.callingAudioDeviceConfig } returns REFERENCE_CONFIG
|
||||
|
||||
assertThat(AudioDeviceConfig.computeConfig()).isEqualTo(createAudioConfig(useSoftwareAec = true, useSoftwareNs = true, useInputLowLatency = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `flip all`() {
|
||||
mockEnvironment()
|
||||
|
||||
every { RemoteConfig.callingAudioDeviceConfig } returns
|
||||
"""
|
||||
[
|
||||
{
|
||||
"settings": {
|
||||
"oboe": true,
|
||||
"softwareAec": true,
|
||||
"softwareNs": true,
|
||||
"inLowLatency": false,
|
||||
"inVoiceComm": false
|
||||
}
|
||||
}
|
||||
]
|
||||
""".trimMargin()
|
||||
|
||||
assertThat(AudioDeviceConfig.computeConfig()).isEqualTo(createAudioConfig(useOboe = true, useSoftwareAec = true, useSoftwareNs = true, useInputLowLatency = false, useInputVoiceComm = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `bail early`() {
|
||||
mockEnvironment()
|
||||
|
||||
every { RemoteConfig.callingAudioDeviceConfig } returns
|
||||
"""
|
||||
[
|
||||
{
|
||||
"final": true
|
||||
},
|
||||
{
|
||||
"settings": {
|
||||
"oboe": true,
|
||||
"softwareAec": true,
|
||||
"softwareNs": true,
|
||||
"inLowLatency": false,
|
||||
"inVoiceComm": false
|
||||
}
|
||||
}
|
||||
]
|
||||
""".trimMargin()
|
||||
|
||||
assertThat(AudioDeviceConfig.computeConfig()).isEqualTo(createAudioConfig())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `custom exclude wildcard`() {
|
||||
mockEnvironment()
|
||||
|
||||
ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "ABCDEFG")
|
||||
ReflectionHelpers.setStaticField(Build::class.java, "DISPLAY", "lineage")
|
||||
|
||||
every { RemoteConfig.callingAudioDeviceConfig } returns
|
||||
"""
|
||||
[
|
||||
{
|
||||
"target": {
|
||||
"custom": true,
|
||||
"exclude": ["ABC*"]
|
||||
},
|
||||
"settings": {
|
||||
"oboe": true
|
||||
}
|
||||
}
|
||||
]
|
||||
""".trimMargin()
|
||||
|
||||
assertThat(AudioDeviceConfig.computeConfig()).isEqualTo(createAudioConfig())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `ignore unknown`() {
|
||||
mockEnvironment()
|
||||
|
||||
every { RemoteConfig.callingAudioDeviceConfig } returns
|
||||
"""
|
||||
[
|
||||
{
|
||||
"unknownRule": {
|
||||
"unknown": true
|
||||
},
|
||||
"target": {
|
||||
"unknown": true
|
||||
},
|
||||
"settings": {
|
||||
"unknown": true
|
||||
}
|
||||
}
|
||||
]
|
||||
""".trimMargin()
|
||||
|
||||
assertThat(AudioDeviceConfig.computeConfig()).isEqualTo(createAudioConfig())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `invalid json`() {
|
||||
mockEnvironment()
|
||||
|
||||
every { RemoteConfig.callingAudioDeviceConfig } returns
|
||||
"""
|
||||
[
|
||||
{
|
||||
"final": {
|
||||
"unknown": true
|
||||
}
|
||||
}
|
||||
]
|
||||
""".trimMargin()
|
||||
|
||||
assertThat(AudioDeviceConfig.computeConfig()).isEqualTo(createAudioConfig())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check compressed`() {
|
||||
mockEnvironment()
|
||||
|
||||
every { RemoteConfig.callingAudioDeviceConfig } returns REFERENCE_CONFIG_COMPRESSED
|
||||
|
||||
// Non-custom device.
|
||||
assertThat(AudioDeviceConfig.computeConfig()).isEqualTo(createAudioConfig())
|
||||
|
||||
// Non-custom device requiring software aec.
|
||||
// Note: For legacy reasons, we assume that if software AEC is used, software NS should also be used.
|
||||
ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "T810S")
|
||||
assertThat(AudioDeviceConfig.computeConfig()).isEqualTo(createAudioConfig(useSoftwareAec = true, useSoftwareNs = true))
|
||||
|
||||
// Custom device.
|
||||
ReflectionHelpers.setStaticField(Build::class.java, "PRODUCT", "lorem,lineageos=1.0.0,ipsum")
|
||||
assertThat(AudioDeviceConfig.computeConfig()).isEqualTo(createAudioConfig(useOboe = true))
|
||||
|
||||
// Custom device requiring software aec.
|
||||
ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "ONEPLUS A5000")
|
||||
assertThat(AudioDeviceConfig.computeConfig()).isEqualTo(createAudioConfig(useOboe = true, useSoftwareAec = true, useSoftwareNs = true))
|
||||
|
||||
// Custom device using Java ADM.
|
||||
ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "SM-G973F")
|
||||
assertThat(AudioDeviceConfig.computeConfig()).isEqualTo(createAudioConfig())
|
||||
|
||||
// Device with specific settings, in this case custom or not.
|
||||
ReflectionHelpers.setStaticField(Build::class.java, "MODEL", "FP4")
|
||||
assertThat(AudioDeviceConfig.computeConfig()).isEqualTo(createAudioConfig(useOboe = true, useInputVoiceComm = false))
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `api override`() {
|
||||
mockEnvironment(sdk = 28)
|
||||
|
||||
every { RemoteConfig.callingAudioDeviceConfig } returns REFERENCE_CONFIG_COMPRESSED
|
||||
|
||||
assertThat(AudioDeviceConfig.computeConfig()).isEqualTo(createAudioConfig(useSoftwareAec = true, useSoftwareNs = true))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user