Add new remote config support for calling audio configuration.

This commit is contained in:
Jim Gustafson
2025-04-29 11:37:32 -07:00
committed by Cody Henthorne
parent 2421bbdabb
commit 570a475229
16 changed files with 777 additions and 150 deletions
@@ -567,15 +567,6 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
}
)
radioListPref(
title = DSLSettingsText.from("Audio processing method"),
listItems = CallManager.AudioProcessingMethod.entries.map { it.name }.toTypedArray(),
selected = CallManager.AudioProcessingMethod.entries.indexOf(state.callingAudioProcessingMethod),
onSelected = {
viewModel.setInternalCallingAudioProcessingMethod(CallManager.AudioProcessingMethod.entries[it])
}
)
radioListPref(
title = DSLSettingsText.from("Bandwidth mode"),
listItems = CallManager.DataMode.entries.map { it.name }.toTypedArray(),
@@ -594,10 +585,55 @@ class InternalSettingsFragment : DSLSettingsFragment(R.string.preferences__inter
)
switchPref(
title = DSLSettingsText.from("Enable Oboe ADM"),
isChecked = state.callingEnableOboeAdm,
title = DSLSettingsText.from("Set Audio Config:"),
isChecked = state.callingSetAudioConfig,
onClick = {
viewModel.setInternalCallingEnableOboeAdm(!state.callingEnableOboeAdm)
viewModel.setInternalCallingSetAudioConfig(!state.callingSetAudioConfig)
}
)
switchPref(
title = DSLSettingsText.from(" Use Oboe ADM"),
isChecked = state.callingUseOboeAdm,
isEnabled = state.callingSetAudioConfig,
onClick = {
viewModel.setInternalCallingUseOboeAdm(!state.callingUseOboeAdm)
}
)
switchPref(
title = DSLSettingsText.from(" Use Software AEC"),
isChecked = state.callingUseSoftwareAec,
isEnabled = state.callingSetAudioConfig,
onClick = {
viewModel.setInternalCallingUseSoftwareAec(!state.callingUseSoftwareAec)
}
)
switchPref(
title = DSLSettingsText.from(" Use Software NS"),
isChecked = state.callingUseSoftwareNs,
isEnabled = state.callingSetAudioConfig,
onClick = {
viewModel.setInternalCallingUseSoftwareNs(!state.callingUseSoftwareNs)
}
)
switchPref(
title = DSLSettingsText.from(" Use Input Low Latency"),
isChecked = state.callingUseInputLowLatency,
isEnabled = state.callingSetAudioConfig,
onClick = {
viewModel.setInternalCallingUseInputLowLatency(!state.callingUseInputLowLatency)
}
)
switchPref(
title = DSLSettingsText.from(" Use Input Voice Comm"),
isChecked = state.callingUseInputVoiceComm,
isEnabled = state.callingSetAudioConfig,
onClick = {
viewModel.setInternalCallingUseInputVoiceComm(!state.callingUseInputVoiceComm)
}
)
@@ -11,10 +11,14 @@ data class InternalSettingsState(
val allowCensorshipSetting: Boolean,
val forceWebsocketMode: Boolean,
val callingServer: String,
val callingAudioProcessingMethod: CallManager.AudioProcessingMethod,
val callingDataMode: CallManager.DataMode,
val callingDisableTelecom: Boolean,
val callingEnableOboeAdm: Boolean,
val callingSetAudioConfig: Boolean,
val callingUseOboeAdm: Boolean,
val callingUseSoftwareAec: Boolean,
val callingUseSoftwareNs: Boolean,
val callingUseInputLowLatency: Boolean,
val callingUseInputVoiceComm: Boolean,
val useBuiltInEmojiSet: Boolean,
val emojiVersion: EmojiFiles.Version?,
val removeSenderKeyMinimium: Boolean,
@@ -94,11 +94,6 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
refresh()
}
fun setInternalCallingAudioProcessingMethod(method: CallManager.AudioProcessingMethod) {
preferenceDataStore.putInt(InternalValues.CALLING_AUDIO_PROCESSING_METHOD, method.ordinal)
refresh()
}
fun setInternalCallingDataMode(dataMode: CallManager.DataMode) {
preferenceDataStore.putInt(InternalValues.CALLING_DATA_MODE, dataMode.ordinal)
refresh()
@@ -109,8 +104,33 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
refresh()
}
fun setInternalCallingEnableOboeAdm(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.CALLING_ENABLE_OBOE_ADM, enabled)
fun setInternalCallingSetAudioConfig(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.CALLING_SET_AUDIO_CONFIG, enabled)
refresh()
}
fun setInternalCallingUseOboeAdm(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.CALLING_USE_OBOE_ADM, enabled)
refresh()
}
fun setInternalCallingUseSoftwareAec(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.CALLING_USE_SOFTWARE_AEC, enabled)
refresh()
}
fun setInternalCallingUseSoftwareNs(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.CALLING_USE_SOFTWARE_NS, enabled)
refresh()
}
fun setInternalCallingUseInputLowLatency(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.CALLING_USE_INPUT_LOW_LATENCY, enabled)
refresh()
}
fun setInternalCallingUseInputVoiceComm(enabled: Boolean) {
preferenceDataStore.putBoolean(InternalValues.CALLING_USE_INPUT_VOICE_COMM, enabled)
refresh()
}
@@ -152,10 +172,14 @@ class InternalSettingsViewModel(private val repository: InternalSettingsReposito
allowCensorshipSetting = SignalStore.internal.allowChangingCensorshipSetting,
forceWebsocketMode = SignalStore.internal.isWebsocketModeForced,
callingServer = SignalStore.internal.groupCallingServer,
callingAudioProcessingMethod = SignalStore.internal.callingAudioProcessingMethod,
callingDataMode = SignalStore.internal.callingDataMode,
callingDisableTelecom = SignalStore.internal.callingDisableTelecom,
callingEnableOboeAdm = SignalStore.internal.callingEnableOboeAdm,
callingSetAudioConfig = SignalStore.internal.callingSetAudioConfig,
callingUseOboeAdm = SignalStore.internal.callingUseOboeAdm,
callingUseSoftwareAec = SignalStore.internal.callingUseSoftwareAec,
callingUseSoftwareNs = SignalStore.internal.callingUseSoftwareNs,
callingUseInputLowLatency = SignalStore.internal.callingUseInputLowLatency,
callingUseInputVoiceComm = SignalStore.internal.callingUseInputVoiceComm,
useBuiltInEmojiSet = SignalStore.internal.forceBuiltInEmoji,
emojiVersion = null,
removeSenderKeyMinimium = SignalStore.internal.removeSenderKeyMinimum,
@@ -1,6 +1,5 @@
package org.thoughtcrime.securesms.keyvalue
import org.signal.ringrtc.CallManager.AudioProcessingMethod
import org.signal.ringrtc.CallManager.DataMode
import org.thoughtcrime.securesms.BuildConfig
import org.thoughtcrime.securesms.util.Environment.Calling.defaultSfuUrl
@@ -16,10 +15,14 @@ class InternalValues internal constructor(store: KeyValueStore) : SignalStoreVal
const val REMOVE_SENDER_KEY_MINIMUM: String = "internal.remove_sender_key_minimum"
const val DELAY_RESENDS: String = "internal.delay_resends"
const val CALLING_SERVER: String = "internal.calling_server"
const val CALLING_AUDIO_PROCESSING_METHOD: String = "internal.calling_audio_processing_method"
const val CALLING_DATA_MODE: String = "internal.calling_bandwidth_mode"
const val CALLING_DISABLE_TELECOM: String = "internal.calling_disable_telecom"
const val CALLING_ENABLE_OBOE_ADM: String = "internal.calling_enable_oboe_adm"
const val CALLING_SET_AUDIO_CONFIG: String = "internal.calling_set_audio_config"
const val CALLING_USE_OBOE_ADM: String = "internal.calling_use_oboe_adm"
const val CALLING_USE_SOFTWARE_AEC: String = "internal.calling_use_software_aec"
const val CALLING_USE_SOFTWARE_NS: String = "internal.calling_use_software_ns"
const val CALLING_USE_INPUT_LOW_LATENCY: String = "internal.calling_use_input_low_latency"
const val CALLING_USE_INPUT_VOICE_COMM: String = "internal.calling_use_input_voice_comm"
const val SHAKE_TO_REPORT: String = "internal.shake_to_report"
const val DISABLE_STORAGE_SERVICE: String = "internal.disable_storage_service"
const val FORCE_WEBSOCKET_MODE: String = "internal.force_websocket_mode"
@@ -110,19 +113,6 @@ class InternalValues internal constructor(store: KeyValueStore) : SignalStoreVal
}
set(value) = putString(CALLING_SERVER, value)
/**
* Setting to override the default handling of hardware/software AEC.
*/
val callingAudioProcessingMethod: AudioProcessingMethod
get() {
return if (RemoteConfig.internalUser) {
val entryIndex = getInteger(CALLING_AUDIO_PROCESSING_METHOD, AudioProcessingMethod.Default.ordinal)
AudioProcessingMethod.entries[entryIndex]
} else {
AudioProcessingMethod.Default
}
}
/**
* Setting to override the default calling bandwidth mode.
*/
@@ -144,9 +134,34 @@ class InternalValues internal constructor(store: KeyValueStore) : SignalStoreVal
var callingDisableTelecom by booleanValue(CALLING_DISABLE_TELECOM, true).falseForExternalUsers()
/**
* Whether or not the Oboe ADM is used.
* Whether or not to override the audio settings from the remote configuration.
*/
var callingEnableOboeAdm by booleanValue(CALLING_ENABLE_OBOE_ADM, true).falseForExternalUsers()
var callingSetAudioConfig by booleanValue(CALLING_SET_AUDIO_CONFIG, true).falseForExternalUsers()
/**
* If overriding the audio settings, use the Oboe ADM or not.
*/
var callingUseOboeAdm by booleanValue(CALLING_USE_OBOE_ADM, true).defaultForExternalUsers()
/**
* If overriding the audio settings, use the Software AEC or not.
*/
var callingUseSoftwareAec by booleanValue(CALLING_USE_SOFTWARE_AEC, false).defaultForExternalUsers()
/**
* If overriding the audio settings, use the Software NS or not.
*/
var callingUseSoftwareNs by booleanValue(CALLING_USE_SOFTWARE_NS, false).defaultForExternalUsers()
/**
* If overriding the audio settings, use Low Latency for the input or not.
*/
var callingUseInputLowLatency by booleanValue(CALLING_USE_INPUT_LOW_LATENCY, true).defaultForExternalUsers()
/**
* If overriding the audio settings, use Voice Comm for the input or not.
*/
var callingUseInputVoiceComm by booleanValue(CALLING_USE_INPUT_VOICE_COMM, true).defaultForExternalUsers()
/**
* Whether or not the system is forced to be in 'websocket mode', where FCM is ignored and we use a foreground service to keep the app alive.
@@ -66,8 +66,7 @@ class CallLinkPreJoinActionProcessor(
callLink.credentials.adminPassBytes,
ByteArray(0),
AUDIO_LEVELS_INTERVAL,
RingRtcDynamicConfiguration.getAudioProcessingMethod(),
RingRtcDynamicConfiguration.shouldUseOboeAdm(),
RingRtcDynamicConfiguration.getAudioConfig(),
webRtcInteractor.groupCallObserver
)
} catch (e: InvalidInputException) {
@@ -52,8 +52,7 @@ public class GroupNetworkUnavailableActionProcessor extends WebRtcActionProcesso
SignalStore.internal().getGroupCallingServer(),
new byte[0],
null,
RingRtcDynamicConfiguration.getAudioProcessingMethod(),
RingRtcDynamicConfiguration.shouldUseOboeAdm(),
RingRtcDynamicConfiguration.getAudioConfig(),
webRtcInteractor.getGroupCallObserver());
if (groupCall == null) {
@@ -50,8 +50,7 @@ public class GroupPreJoinActionProcessor extends GroupActionProcessor {
SignalStore.internal().getGroupCallingServer(),
new byte[0],
AUDIO_LEVELS_INTERVAL,
RingRtcDynamicConfiguration.getAudioProcessingMethod(),
RingRtcDynamicConfiguration.shouldUseOboeAdm(),
RingRtcDynamicConfiguration.getAudioConfig(),
webRtcInteractor.getGroupCallObserver());
if (groupCall == null) {
@@ -103,8 +103,7 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor {
webRtcInteractor.getCallManager().proceed(activePeer.getCallId(),
context,
videoState.getLockableEglBase().require(),
RingRtcDynamicConfiguration.getAudioProcessingMethod(),
RingRtcDynamicConfiguration.shouldUseOboeAdm(),
RingRtcDynamicConfiguration.getAudioConfig(),
videoState.requireLocalSink(),
callParticipant.getVideoSink(),
videoState.requireCamera(),
@@ -186,8 +186,7 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro
SignalStore.internal().getGroupCallingServer(),
new byte[0],
AUDIO_LEVELS_INTERVAL,
RingRtcDynamicConfiguration.getAudioProcessingMethod(),
RingRtcDynamicConfiguration.shouldUseOboeAdm(),
RingRtcDynamicConfiguration.getAudioConfig(),
webRtcInteractor.getGroupCallObserver());
if (groupCall == null) {
@@ -152,8 +152,7 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor {
webRtcInteractor.getCallManager().proceed(activePeer.getCallId(),
context,
videoState.getLockableEglBase().require(),
RingRtcDynamicConfiguration.getAudioProcessingMethod(),
RingRtcDynamicConfiguration.shouldUseOboeAdm(),
RingRtcDynamicConfiguration.getAudioConfig(),
videoState.requireLocalSink(),
callParticipant.getVideoSink(),
videoState.requireCamera(),
@@ -2,73 +2,42 @@ package org.thoughtcrime.securesms.service.webrtc
import android.os.Build
import org.signal.core.util.asListContains
import org.signal.ringrtc.CallManager.AudioProcessingMethod
import org.signal.ringrtc.AudioConfig
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.util.RemoteConfig
import org.thoughtcrime.securesms.webrtc.audio.AudioDeviceConfig
/**
* Utility class to determine which AEC method RingRTC should use.
* Utility class to determine the audio configuration that RingRTC should use.
*/
object RingRtcDynamicConfiguration {
private val KNOWN_ISSUE_ROMS = "(lineage|calyxos)".toRegex(RegexOption.IGNORE_CASE)
@JvmStatic
fun getAudioProcessingMethod(): AudioProcessingMethod {
if (SignalStore.internal.callingAudioProcessingMethod != AudioProcessingMethod.Default) {
return SignalStore.internal.callingAudioProcessingMethod
}
return if (shouldUseOboeAdm()) {
when {
shouldUseSoftwareAecForOboe() || isKnownFaultyHardwareImplementation() -> AudioProcessingMethod.ForceSoftwareAec3
else -> AudioProcessingMethod.ForceHardware
}
} else {
when {
isHardwareBlocklisted() || isKnownFaultyHardwareImplementation() -> AudioProcessingMethod.ForceSoftwareAec3
isSoftwareBlocklisted() -> AudioProcessingMethod.ForceHardware
Build.VERSION.SDK_INT < 29 && RemoteConfig.useHardwareAecIfOlderThanApi29 -> AudioProcessingMethod.ForceHardware
Build.VERSION.SDK_INT < 29 -> AudioProcessingMethod.ForceSoftwareAec3
else -> AudioProcessingMethod.ForceHardware
}
}
}
private var lastFetchTime: Long = 0
fun isTelecomAllowedForDevice(): Boolean {
return RemoteConfig.telecomManufacturerAllowList.lowercase().asListContains(Build.MANUFACTURER.lowercase()) &&
!RemoteConfig.telecomModelBlocklist.lowercase().asListContains(Build.MODEL.lowercase())
}
private fun isHardwareBlocklisted(): Boolean {
return RemoteConfig.hardwareAecBlocklistModels.asListContains(Build.MODEL)
}
private fun isKnownFaultyHardwareImplementation(): Boolean {
return Build.PRODUCT.contains(KNOWN_ISSUE_ROMS) ||
Build.DISPLAY.contains(KNOWN_ISSUE_ROMS) ||
Build.HOST.contains(KNOWN_ISSUE_ROMS)
}
private fun isSoftwareBlocklisted(): Boolean {
return RemoteConfig.softwareAecBlocklistModels.asListContains(Build.MODEL)
}
@JvmStatic
fun shouldUseOboeAdm(): Boolean {
if (RemoteConfig.internalUser) {
return SignalStore.internal.callingEnableOboeAdm
fun getAudioConfig(): AudioConfig {
if (RemoteConfig.internalUser && SignalStore.internal.callingSetAudioConfig) {
// Use the internal audio settings.
var audioConfig = AudioConfig()
audioConfig.useOboe = SignalStore.internal.callingUseOboeAdm
audioConfig.useSoftwareAec = SignalStore.internal.callingUseSoftwareAec
audioConfig.useSoftwareNs = SignalStore.internal.callingUseSoftwareNs
audioConfig.useInputLowLatency = SignalStore.internal.callingUseInputLowLatency
audioConfig.useInputVoiceComm = SignalStore.internal.callingUseInputVoiceComm
return audioConfig
}
// For now, only allow the Oboe ADM to be used for custom ROMS.
return RemoteConfig.oboeDeployment && isKnownFaultyHardwareImplementation() && !shouldUseJavaAdm()
}
private fun shouldUseJavaAdm(): Boolean {
return RemoteConfig.useJavaAdmModels.asListContains(Build.MODEL)
}
private fun shouldUseSoftwareAecForOboe(): Boolean {
return RemoteConfig.useSoftwareAecForOboeModels.asListContains(Build.MODEL)
// Use the audio settings provided by the remote configuration.
if (lastFetchTime != SignalStore.remoteConfig.lastFetchTime) {
// The remote config has been updated.
AudioDeviceConfig.refresh()
lastFetchTime = SignalStore.remoteConfig.lastFetchTime
}
return AudioDeviceConfig.getCurrentConfig()
}
}
@@ -694,37 +694,9 @@ object RemoteConfig {
hotSwappable = false
)
/** A comma-separated list of models that should *not* use hardware AEC for calling. */
val hardwareAecBlocklistModels: String by remoteString(
key = "android.calling.hardwareAecBlockList",
defaultValue = "",
hotSwappable = true
)
/** A comma-separated list of models that should *not* use software AEC for calling. */
val softwareAecBlocklistModels: String by remoteString(
key = "android.calling.softwareAecBlockList",
defaultValue = "",
hotSwappable = true
)
/** Whether the Oboe ADM should be used or not. */
val oboeDeployment: Boolean by remoteBoolean(
key = "android.calling.oboeDeployment",
defaultValue = false,
hotSwappable = false
)
/** A comma-separated list of models that should use the Java ADM instead of the Oboe ADM. */
val useJavaAdmModels: String by remoteString(
key = "android.calling.useJavaAdmList",
defaultValue = "",
hotSwappable = true
)
/** A comma-separated list of models that should use software AEC for calling with the Oboe ADM. */
val useSoftwareAecForOboeModels: String by remoteString(
key = "android.calling.useSoftwareAecForOboe",
/** A json string representing rules necessary to build an audio configuration for a device. */
val callingAudioDeviceConfig: String by remoteString(
key = "android.calling.audioDeviceConfig",
defaultValue = "",
hotSwappable = true
)
@@ -757,13 +729,6 @@ object RemoteConfig {
hotSwappable = false
)
/** Whether or not hardware AEC should be used for calling on devices older than API 29. */
val useHardwareAecIfOlderThanApi29: Boolean by remoteBoolean(
key = "android.calling.useHardwareAecIfOlderThanApi29",
defaultValue = false,
hotSwappable = true
)
/** Prefetch count for stories from a given user. */
val storiesAutoDownloadMaximum: Int by remoteInt(
key = "android.stories.autoDownloadMaximum",
@@ -0,0 +1,191 @@
/*
* Copyright 2025 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.webrtc.audio
import android.content.pm.PackageManager
import android.media.audiofx.AcousticEchoCanceler
import android.media.audiofx.NoiseSuppressor
import android.os.Build
import androidx.annotation.VisibleForTesting
import com.fasterxml.jackson.annotation.JsonProperty
import org.signal.core.util.logging.Log
import org.signal.ringrtc.AudioConfig
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.util.JsonUtils
import org.thoughtcrime.securesms.util.RemoteConfig
import java.io.IOException
/**
* Remote config for audio devices to allow for more targeted configurations
*/
object AudioDeviceConfig {
private val TAG = Log.tag(AudioDeviceConfig::class.java)
private val CUSTOM_ROMS = "(lineage|calyxos)".toRegex(RegexOption.IGNORE_CASE)
private var currentConfig: AudioConfig? = null
@Synchronized
fun getCurrentConfig(): AudioConfig {
if (currentConfig == null) {
currentConfig = computeConfig()
}
return currentConfig!!
}
@Synchronized
fun refresh() {
currentConfig = computeConfig()
}
/**
* Defines rules that can be filtered and applied to a specific device based on the provided criteria
* @param target the targets to apply the rule to; can be specific devices and/or device classes
* @param settings the audio settings that the rule will set
* @param final forces an exit from the rule matching if true
*/
data class Rule(
@JsonProperty("target") val target: Target = Target(),
@JsonProperty("settings") val settings: Settings = Settings(),
@JsonProperty("final") val isFinal: Boolean = false
)
/**
* Defines the target devices for which a given rule should be applied
* @param include a list of device models to include; wildcard suffixes are supported
* @param exclude a list of device models to exclude; wildcard suffixes are supported
* @param custom whether or not a custom (non-AOSP) ROM should be considered
*/
data class Target(
@JsonProperty("include") val include: List<String> = emptyList(),
@JsonProperty("exclude") val exclude: List<String> = emptyList(),
@JsonProperty("custom") val isCustomRom: Boolean? = null
)
/**
* Defines the audio settings a rule can override, if specified
* @param oboe if true, use the Oboe ADM instead of the Java ADM
* @param softwareAec if true, use software AEC instead of the platform provided AEC
* @param softwareNs if true, use software NS instead of the platform provided NS
* @param inLowLatency if true, use low latency setting for input
* @param inVoiceComm if true, use voice communications setting for input
*/
data class Settings(
@JsonProperty("oboe") val oboe: Boolean? = null,
@JsonProperty("softwareAec") val softwareAec: Boolean? = null,
@JsonProperty("softwareNs") val softwareNs: Boolean? = null,
@JsonProperty("inLowLatency") val inLowLatency: Boolean? = null,
@JsonProperty("inVoiceComm") val inVoiceComm: Boolean? = null
)
@VisibleForTesting
fun computeConfig(): AudioConfig {
// Initialize the config with the default.
val config = AudioConfig()
val serialized = RemoteConfig.callingAudioDeviceConfig
if (serialized.isBlank()) {
Log.w(TAG, "No RemoteConfig.callingAudioDeviceConfig found! Using default.")
return applyOverrides(config)
}
val rules: List<Rule> = try {
JsonUtils.fromJsonArray(serialized, Rule::class.java)
} catch (e: IOException) {
Log.w(TAG, "Failed to parse callingAudioDeviceConfig json! Using default. " + e.message)
return applyOverrides(config)
}
for (rule in rules) {
if (matchesDevice(rule.target)) {
applySettings(rule.settings, config)
if (rule.isFinal) {
return applyOverrides(config)
}
}
}
return applyOverrides(config)
}
// Check if the target should apply to the device the code is running on.
private fun matchesDevice(target: Target): Boolean {
// Make sure the device is in the include list, if the list is present.
if (target.include.isNotEmpty() &&
!target.include.any { matchesModel(it) }
) {
return false
}
// If the device is in the exclude list, don't match it.
if (target.exclude.isNotEmpty() &&
target.exclude.any { matchesModel(it) }
) {
return false
}
// Check if the device needs to be a custom ROM or not, if the constraint is present.
if (target.isCustomRom != null &&
target.isCustomRom != isCustomRom()
) {
return false
}
return true
}
private fun matchesModel(model: String): Boolean {
return if (model.endsWith("*")) {
Build.MODEL.startsWith(model.substring(0, model.length - 1))
} else {
Build.MODEL.equals(model, ignoreCase = true)
}
}
private fun isCustomRom(): Boolean {
return Build.PRODUCT.contains(CUSTOM_ROMS) ||
Build.DISPLAY.contains(CUSTOM_ROMS) ||
Build.HOST.contains(CUSTOM_ROMS)
}
private fun applySettings(settings: Settings, config: AudioConfig) {
settings.oboe?.let { config.useOboe = it }
settings.softwareAec?.let { config.useSoftwareAec = it }
settings.softwareNs?.let { config.useSoftwareNs = it }
settings.inLowLatency?.let { config.useInputLowLatency = it }
settings.inVoiceComm?.let { config.useInputVoiceComm = it }
}
private fun applyOverrides(config: AudioConfig): AudioConfig {
if (!isCustomRom() && Build.VERSION.SDK_INT < 29) {
Log.w(TAG, "Device is less than API level 29, forcing software AEC/NS!")
config.useSoftwareAec = true
config.useSoftwareNs = true
}
if (!config.useSoftwareAec && !AcousticEchoCanceler.isAvailable()) {
Log.w(TAG, "Device does not implement AcousticEchoCanceler, overriding config!")
config.useSoftwareAec = true
}
if (!config.useSoftwareNs && !NoiseSuppressor.isAvailable()) {
Log.w(TAG, "Device does not implement NoiseSuppressor, overriding config!")
config.useSoftwareNs = true
}
val context = AppDependencies.application.applicationContext
if (config.useInputLowLatency &&
!context.packageManager.hasSystemFeature(PackageManager.FEATURE_AUDIO_LOW_LATENCY)
) {
Log.w(TAG, "Device does not implement FEATURE_AUDIO_LOW_LATENCY, overriding config!")
config.useInputLowLatency = false
}
return config
}
}
@@ -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))
}
}
+1 -1
View File
@@ -143,7 +143,7 @@ libsignal-client = { module = "org.signal:libsignal-client", version.ref = "libs
libsignal-android = { module = "org.signal:libsignal-android", version.ref = "libsignal-client" }
protobuf-gradle-plugin = { module = "com.google.protobuf:protobuf-gradle-plugin", version.ref = "protobuf-gradle-plugin" }
signal-aesgcmprovider = "org.signal:aesgcmprovider:0.0.4"
signal-ringrtc = "org.signal:ringrtc-android:2.52.0"
signal-ringrtc = "org.signal:ringrtc-android:2.52.1"
signal-android-database-sqlcipher = "org.signal:sqlcipher-android:4.6.0-S1"
# Third Party
+5 -5
View File
@@ -7188,12 +7188,12 @@ https://docs.gradle.org/current/userguide/dependency_verification.html
<sha256 value="2da2042da8308840e177bff4a14b8d12b7fa36c62a19b1120e3dda67a1098bc9" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.signal" name="ringrtc-android" version="2.52.0">
<artifact name="ringrtc-android-2.52.0.aar">
<sha256 value="886fcb3871721c3551db8e983c654425696b8fbbaddef56508a3a5a035d7ba24" origin="Generated by Gradle"/>
<component group="org.signal" name="ringrtc-android" version="2.52.1">
<artifact name="ringrtc-android-2.52.1.aar">
<sha256 value="7b1d05085a3a529819ceaf034d006941e65c8b2f576274cb0316ac6962aef801" origin="Generated by Gradle"/>
</artifact>
<artifact name="ringrtc-android-2.52.0.module">
<sha256 value="983fb4928fd18c8d530a2441d119337f4235d8a465402b888b96a2d4f1fb46a9" origin="Generated by Gradle"/>
<artifact name="ringrtc-android-2.52.1.module">
<sha256 value="6ce7395208568dc63fa21ba3a527ff49b1db05dfefe7024eb552b7b6ce40a988" origin="Generated by Gradle"/>
</artifact>
</component>
<component group="org.signal" name="sqlcipher-android" version="4.6.0-S1">