get rid of ToneManager and play sounds in CallIntegration instead

CallIntegration takes care of audio routing so it makes sense to play
and sounds here too
This commit is contained in:
Daniel Gultsch 2024-01-17 12:18:47 +01:00
parent 66cd50e163
commit d31b24d05a
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
7 changed files with 35 additions and 260 deletions

View file

@ -120,7 +120,7 @@ public final class Config {
public static final boolean DISABLE_PROXY_LOOKUP =
false; // disables STUN/TURN and Proxy65 look up (useful to debug IBB fallback)
public static final boolean USE_DIRECT_JINGLE_CANDIDATES = true;
public static final boolean USE_JINGLE_DIRECT_INIT = true;
public static final boolean USE_JINGLE_MESSAGE_INIT = true;
public static final boolean DISABLE_HTTP_UPLOAD = false;
public static final boolean EXTENDED_SM_LOGGING = false; // log stanza counts
public static final boolean BACKGROUND_STANZA_LOGGING =

View file

@ -1,6 +1,8 @@
package eu.siacs.conversations.services;
import android.content.Context;
import android.media.AudioManager;
import android.media.ToneGenerator;
import android.net.Uri;
import android.os.Build;
import android.telecom.CallAudioState;
@ -20,11 +22,13 @@ import com.google.common.collect.Lists;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.ui.util.MainThreadExecutor;
import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
import eu.siacs.conversations.xmpp.jingle.Media;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
public class CallIntegration extends Connection {
@ -32,6 +36,7 @@ public class CallIntegration extends Connection {
private final AppRTCAudioManager appRTCAudioManager;
private AudioDevice initialAudioDevice = null;
private final AtomicBoolean initialAudioDeviceConfigured = new AtomicBoolean(false);
private final AtomicBoolean delayedDestructionInitiated = new AtomicBoolean(false);
private List<CallEndpoint> availableEndpoints = Collections.emptyList();
@ -302,7 +307,9 @@ public class CallIntegration extends Connection {
public void success() {
Log.d(Config.LOGTAG, "CallIntegration.success()");
this.destroyWith(new DisconnectCause(DisconnectCause.LOCAL, null));
final var toneGenerator = new ToneGenerator(AudioManager.STREAM_MUSIC, 100);
toneGenerator.startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375);
this.destroyWithDelay(new DisconnectCause(DisconnectCause.LOCAL, null), 375);
}
public void accepted() {
@ -316,6 +323,9 @@ public class CallIntegration extends Connection {
public void error() {
Log.d(Config.LOGTAG, "CallIntegration.error()");
final var toneGenerator = new ToneGenerator(AudioManager.STREAM_MUSIC, 80);
toneGenerator.startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375);
this.destroyWithDelay(new DisconnectCause(DisconnectCause.ERROR, null), 375);
this.destroyWith(new DisconnectCause(DisconnectCause.ERROR, null));
}
@ -332,16 +342,33 @@ public class CallIntegration extends Connection {
public void busy() {
Log.d(Config.LOGTAG, "CallIntegration.busy()");
this.destroyWith(new DisconnectCause(DisconnectCause.BUSY, null));
final var toneGenerator = new ToneGenerator(AudioManager.STREAM_MUSIC, 80);
toneGenerator.startTone(ToneGenerator.TONE_CDMA_NETWORK_BUSY, 2500);
this.destroyWithDelay(new DisconnectCause(DisconnectCause.BUSY, null), 2500);
}
private void destroyWithDelay(final DisconnectCause disconnectCause, final int delay) {
if (this.delayedDestructionInitiated.compareAndSet(false, true)) {
JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(
() -> {
this.setDisconnected(disconnectCause);
this.destroy();
},
delay,
TimeUnit.MILLISECONDS);
} else {
Log.w(Config.LOGTAG, "CallIntegration destruction has already been scheduled!");
}
}
private void destroyWith(final DisconnectCause disconnectCause) {
if (this.getState() == STATE_DISCONNECTED) {
if (this.getState() == STATE_DISCONNECTED || this.delayedDestructionInitiated.get()) {
Log.d(Config.LOGTAG, "CallIntegration has already been destroyed");
return;
}
this.setDisconnected(disconnectCause);
this.destroy();
Log.d(Config.LOGTAG, "destroyed!");
}
public static Uri address(final Jid contact) {
@ -349,7 +376,7 @@ public class CallIntegration extends Connection {
}
public void verifyDisconnected() {
if (this.getState() == STATE_DISCONNECTED) {
if (this.getState() == STATE_DISCONNECTED || this.delayedDestructionInitiated.get()) {
return;
}
throw new AssertionError("CallIntegration has not been disconnected");

View file

@ -1633,7 +1633,7 @@ public class ConversationFragment extends XmppFragment
activity.xmppConnectionService.updateAccount(account);
}
final Contact contact = conversation.getContact();
if (Config.USE_JINGLE_DIRECT_INIT && RtpCapability.jmiSupport(contact)) {
if (Config.USE_JINGLE_MESSAGE_INIT && RtpCapability.jmiSupport(contact)) {
triggerRtpSession(contact.getAccount(), contact.getJid().asBareJid(), action);
} else {
final RtpCapability.Capability capability;

View file

@ -54,9 +54,8 @@ import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
public class JingleConnectionManager extends AbstractConnectionManager {
static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE =
public static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE =
Executors.newSingleThreadScheduledExecutor();
final ToneManager toneManager;
private final HashMap<RtpSessionProposal, DeviceDiscoveryState> rtpSessionProposals =
new HashMap<>();
private final ConcurrentHashMap<AbstractJingleConnection.Id, AbstractJingleConnection>
@ -67,7 +66,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
public JingleConnectionManager(XmppConnectionService service) {
super(service);
this.toneManager = new ToneManager(service);
}
static String nextRandomId() {
@ -490,7 +488,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
proposal.callIntegration.busy();
writeLogMissedOutgoing(
account, proposal.with, proposal.sessionId, serverMsgId, timestamp);
toneManager.transition(RtpEndUserState.DECLINED_OR_BUSY, proposal.media);
mXmppConnectionService.notifyJingleRtpConnectionUpdate(
account,
proposal.with,
@ -667,7 +664,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
private void retractSessionProposal(final RtpSessionProposal rtpSessionProposal) {
final Account account = rtpSessionProposal.account;
toneManager.transition(RtpEndUserState.ENDED, rtpSessionProposal.media);
Log.d(
Config.LOGTAG,
account.getJid().asBareJid()
@ -713,7 +709,6 @@ public class JingleConnectionManager extends AbstractConnectionManager {
if (preexistingState != null
&& preexistingState != DeviceDiscoveryState.FAILED) {
final RtpEndUserState endUserState = preexistingState.toEndUserState();
toneManager.transition(endUserState, media);
mXmppConnectionService.notifyJingleRtpConnectionUpdate(
account, with, proposal.sessionId, endUserState);
return proposal;

View file

@ -2727,7 +2727,6 @@ public class JingleRtpConnection extends AbstractJingleConnection
private void updateEndUserState() {
final RtpEndUserState endUserState = getEndUserState();
jingleConnectionManager.toneManager.transition(isInitiator(), endUserState, getMedia());
this.updateCallIntegrationState();
xmppConnectionService.notifyJingleRtpConnectionUpdate(
id.account, id.with, id.sessionId, endUserState);

View file

@ -1,238 +0,0 @@
package eu.siacs.conversations.xmpp.jingle;
import android.content.Context;
import android.media.AudioManager;
import android.media.ToneGenerator;
import android.os.Build;
import android.util.Log;
import java.util.Arrays;
import java.util.Set;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import eu.siacs.conversations.Config;
import static java.util.Arrays.asList;
import androidx.core.content.ContextCompat;
class ToneManager {
private ToneGenerator toneGenerator;
private final Context context;
private ToneState state = null;
private RtpEndUserState endUserState = null;
private ScheduledFuture<?> currentTone;
private ScheduledFuture<?> currentResetFuture;
private boolean appRtcAudioManagerHasControl = false;
ToneManager(final Context context) {
this.context = context;
}
private static ToneState of(final boolean isInitiator, final RtpEndUserState state, final Set<Media> media) {
if (isInitiator) {
if (asList(RtpEndUserState.FINDING_DEVICE, RtpEndUserState.RINGING, RtpEndUserState.CONNECTING).contains(state)) {
return ToneState.RINGING;
}
if (state == RtpEndUserState.DECLINED_OR_BUSY) {
return ToneState.BUSY;
}
}
if (state == RtpEndUserState.ENDING_CALL) {
if (media.contains(Media.VIDEO)) {
return ToneState.NULL;
} else {
return ToneState.ENDING_CALL;
}
}
if (Arrays.asList(
RtpEndUserState.CONNECTED,
RtpEndUserState.RECONNECTING,
RtpEndUserState.INCOMING_CONTENT_ADD)
.contains(state)) {
if (media.contains(Media.VIDEO)) {
return ToneState.NULL;
} else {
return ToneState.CONNECTED;
}
}
return ToneState.NULL;
}
void transition(final RtpEndUserState state, final Set<Media> media) {
transition(state, of(true, state, media), media);
}
void transition(final boolean isInitiator, final RtpEndUserState state, final Set<Media> media) {
transition(state, of(isInitiator, state, media), media);
}
private synchronized void transition(final RtpEndUserState endUserState, final ToneState state, final Set<Media> media) {
final RtpEndUserState normalizeEndUserState = normalize(endUserState);
if (this.endUserState == normalizeEndUserState) {
return;
}
this.endUserState = normalizeEndUserState;
if (this.state == state) {
return;
}
if (state == ToneState.NULL && this.state == ToneState.ENDING_CALL) {
return;
}
cancelCurrentTone();
Log.d(Config.LOGTAG, getClass().getName() + ".transition(" + state + ")");
if (state != ToneState.NULL) {
configureAudioManagerForCall(media);
}
switch (state) {
case RINGING:
// ringing can be removed as this is now handled by 'CallIntegration'
//scheduleWaitingTone();
break;
case CONNECTED:
scheduleConnected();
break;
case BUSY:
scheduleBusy();
break;
case ENDING_CALL:
scheduleEnding();
break;
case NULL:
if (noResetScheduled()) {
resetAudioManager();
}
break;
default:
throw new IllegalStateException("Unable to handle transition to "+state);
}
this.state = state;
}
private static RtpEndUserState normalize(final RtpEndUserState endUserState) {
if (Arrays.asList(
RtpEndUserState.CONNECTED,
RtpEndUserState.RECONNECTING,
RtpEndUserState.INCOMING_CONTENT_ADD)
.contains(endUserState)) {
return RtpEndUserState.CONNECTED;
} else {
return endUserState;
}
}
void setAppRtcAudioManagerHasControl(final boolean appRtcAudioManagerHasControl) {
this.appRtcAudioManagerHasControl = appRtcAudioManagerHasControl;
}
private void scheduleConnected() {
this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> {
startTone(ToneGenerator.TONE_PROP_PROMPT, 200);
}, 0, TimeUnit.SECONDS);
}
private void scheduleEnding() {
this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> {
startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375);
}, 0, TimeUnit.SECONDS);
this.currentResetFuture = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(this::resetAudioManager, 375, TimeUnit.MILLISECONDS);
}
private void scheduleBusy() {
this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> {
startTone(ToneGenerator.TONE_CDMA_NETWORK_BUSY, 2500);
}, 0, TimeUnit.SECONDS);
this.currentResetFuture = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(this::resetAudioManager, 2500, TimeUnit.MILLISECONDS);
}
private void scheduleWaitingTone() {
this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate(() -> {
startTone(ToneGenerator.TONE_CDMA_DIAL_TONE_LITE, 750);
}, 0, 3, TimeUnit.SECONDS);
}
private boolean noResetScheduled() {
return this.currentResetFuture == null || this.currentResetFuture.isDone();
}
private void cancelCurrentTone() {
if (currentTone != null) {
currentTone.cancel(true);
}
stopTone(toneGenerator);
}
private static void stopTone(final ToneGenerator toneGenerator) {
if (toneGenerator == null) {
return;
}
try {
toneGenerator.stopTone();
} catch (final RuntimeException e) {
Log.w(Config.LOGTAG,"tone has already stopped");
}
}
private void startTone(final int toneType, final int durationMs) {
if (this.toneGenerator != null) {
this.toneGenerator.release();;
}
final AudioManager audioManager = ContextCompat.getSystemService(context, AudioManager.class);
final boolean ringerModeNormal = audioManager == null || audioManager.getRingerMode() == AudioManager.RINGER_MODE_NORMAL;
this.toneGenerator = getToneGenerator(ringerModeNormal);
if (toneGenerator != null) {
this.toneGenerator.startTone(toneType, durationMs);
}
}
private static ToneGenerator getToneGenerator(final boolean ringerModeNormal) {
try {
// when silent and on Android 12+ use STREAM_MUSIC
if (ringerModeNormal || Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
return new ToneGenerator(AudioManager.STREAM_VOICE_CALL,60);
} else {
return new ToneGenerator(AudioManager.STREAM_MUSIC,100);
}
} catch (final Exception e) {
Log.d(Config.LOGTAG,"could not create tone generator",e);
return null;
}
}
private void configureAudioManagerForCall(final Set<Media> media) {
if (appRtcAudioManagerHasControl) {
Log.d(Config.LOGTAG, ToneManager.class.getName() + ": do not configure audio manager because RTC has control");
return;
}
final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
if (audioManager == null) {
return;
}
final boolean isSpeakerPhone = media.contains(Media.VIDEO);
Log.d(Config.LOGTAG, ToneManager.class.getName() + ": putting AudioManager into communication mode. speaker=" + isSpeakerPhone);
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
audioManager.setSpeakerphoneOn(isSpeakerPhone);
}
private void resetAudioManager() {
if (appRtcAudioManagerHasControl) {
Log.d(Config.LOGTAG, ToneManager.class.getName() + ": do not reset audio manager because RTC has control");
return;
}
final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
if (audioManager == null) {
return;
}
Log.d(Config.LOGTAG, ToneManager.class.getName() + ": putting AudioManager back into normal mode");
audioManager.setMode(AudioManager.MODE_NORMAL);
audioManager.setSpeakerphoneOn(false);
}
private enum ToneState {
NULL, RINGING, CONNECTED, BUSY, ENDING_CALL
}
}

View file

@ -2,8 +2,6 @@ package eu.siacs.conversations.xmpp.jingle;
import android.content.Context;
import android.os.Build;
import android.os.Handler;
import android.os.Looper;
import android.util.Log;
import com.google.common.base.Optional;
@ -15,8 +13,6 @@ import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.SettableFuture;
import eu.siacs.conversations.Config;
import eu.siacs.conversations.services.AppRTCAudioManager;
import eu.siacs.conversations.services.CallIntegration;
import eu.siacs.conversations.services.XmppConnectionService;
import org.webrtc.AudioSource;
@ -52,7 +48,6 @@ import java.util.concurrent.atomic.AtomicBoolean;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
@SuppressWarnings("UnstableApiUsage")
public class WebRTCWrapper {
private static final String EXTENDED_LOGGING_TAG = WebRTCWrapper.class.getSimpleName();
@ -205,7 +200,6 @@ public class WebRTCWrapper {
};
@Nullable private PeerConnectionFactory peerConnectionFactory = null;
@Nullable private PeerConnection peerConnection = null;
private ToneManager toneManager = null;
private Context context = null;
private EglBase eglBase = null;
private VideoSourceWrapper videoSourceWrapper;
@ -222,8 +216,7 @@ public class WebRTCWrapper {
}
}
public void setup(final XmppConnectionService service)
throws InitializationException {
public void setup(final XmppConnectionService service) throws InitializationException {
try {
PeerConnectionFactory.initialize(
PeerConnectionFactory.InitializationOptions.builder(service)
@ -238,7 +231,6 @@ public class WebRTCWrapper {
throw new InitializationException("Unable to create EGL base", e);
}
this.context = service;
this.toneManager = service.getJingleConnectionManager().toneManager;
}
synchronized void initializePeerConnection(