diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt index e5c04609c0..fdb96015e7 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsFragment.kt @@ -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) } ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsState.kt index 298f7e2cff..88497c1d38 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsState.kt @@ -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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsViewModel.kt index e8e32be73f..bdd7e0fa53 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/internal/InternalSettingsViewModel.kt @@ -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, diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.kt index f3821d2979..7ac47a62ee 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/InternalValues.kt @@ -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. diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/CallLinkPreJoinActionProcessor.kt b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/CallLinkPreJoinActionProcessor.kt index e2d535bfa5..5656fe067f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/CallLinkPreJoinActionProcessor.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/CallLinkPreJoinActionProcessor.kt @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupNetworkUnavailableActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupNetworkUnavailableActionProcessor.java index f52c8336af..69eaf3ec59 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupNetworkUnavailableActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupNetworkUnavailableActionProcessor.java @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupPreJoinActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupPreJoinActionProcessor.java index 9b0df7fd20..b91c6e95ed 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupPreJoinActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupPreJoinActionProcessor.java @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingCallActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingCallActionProcessor.java index 0023f3f64c..7c0e109759 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingCallActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingCallActionProcessor.java @@ -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(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingGroupCallActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingGroupCallActionProcessor.java index 7fa6172d95..7516826838 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingGroupCallActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingGroupCallActionProcessor.java @@ -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) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/OutgoingCallActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/OutgoingCallActionProcessor.java index 0431e5066c..ee8a2f2f67 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/OutgoingCallActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/OutgoingCallActionProcessor.java @@ -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(), diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/RingRtcDynamicConfiguration.kt b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/RingRtcDynamicConfiguration.kt index 0dafb27edc..620809e5a3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/RingRtcDynamicConfiguration.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/RingRtcDynamicConfiguration.kt @@ -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() } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt index 6db826af91..1bd4096e01 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt @@ -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", diff --git a/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/AudioDeviceConfig.kt b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/AudioDeviceConfig.kt new file mode 100644 index 0000000000..845c326399 --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/webrtc/audio/AudioDeviceConfig.kt @@ -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 = emptyList(), + @JsonProperty("exclude") val exclude: List = 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 = 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 + } +} diff --git a/app/src/test/java/org/thoughtcrime/securesms/webrtc/audio/AudioDeviceConfigTest.kt b/app/src/test/java/org/thoughtcrime/securesms/webrtc/audio/AudioDeviceConfigTest.kt new file mode 100644 index 0000000000..99d2cc096d --- /dev/null +++ b/app/src/test/java/org/thoughtcrime/securesms/webrtc/audio/AudioDeviceConfigTest.kt @@ -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)) + } +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c62bf33050..e440850611 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -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 diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 3b8bf6cee5..e93dda656d 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -7188,12 +7188,12 @@ https://docs.gradle.org/current/userguide/dependency_verification.html - - - + + + - - + +