give TonManager control over audio mode to play dial tones on earpiece. fixes #3738

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java | 17 
src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java             | 65 
src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java           | 13 
3 files changed, 71 insertions(+), 24 deletions(-)

Detailed changes

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java 🔗

@@ -38,6 +38,7 @@ import eu.siacs.conversations.services.AbstractConnectionManager;
 import eu.siacs.conversations.services.XmppConnectionService;
 import eu.siacs.conversations.xml.Element;
 import eu.siacs.conversations.xml.Namespace;
+import eu.siacs.conversations.xmpp.Jid;
 import eu.siacs.conversations.xmpp.OnIqPacketReceived;
 import eu.siacs.conversations.xmpp.jingle.stanzas.Content;
 import eu.siacs.conversations.xmpp.jingle.stanzas.FileTransferDescription;
@@ -48,11 +49,10 @@ import eu.siacs.conversations.xmpp.jingle.stanzas.Reason;
 import eu.siacs.conversations.xmpp.jingle.stanzas.RtpDescription;
 import eu.siacs.conversations.xmpp.stanzas.IqPacket;
 import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
-import eu.siacs.conversations.xmpp.Jid;
 
 public class JingleConnectionManager extends AbstractConnectionManager {
     static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE = Executors.newSingleThreadScheduledExecutor();
-    final ToneManager toneManager = new ToneManager();
+    final ToneManager toneManager;
     private final HashMap<RtpSessionProposal, DeviceDiscoveryState> rtpSessionProposals = new HashMap<>();
     private final ConcurrentHashMap<AbstractJingleConnection.Id, AbstractJingleConnection> connections = new ConcurrentHashMap<>();
 
@@ -64,6 +64,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
 
     public JingleConnectionManager(XmppConnectionService service) {
         super(service);
+        this.toneManager = new ToneManager(service);
     }
 
     static String nextRandomId() {
@@ -333,11 +334,11 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                 }
             }
         } else if (addressedDirectly && "reject".equals(message.getName())) {
-            final RtpSessionProposal proposal = new RtpSessionProposal(account, from.asBareJid(), sessionId);
+            final RtpSessionProposal proposal = getRtpSessionProposal(account, from.asBareJid(), sessionId);
             synchronized (rtpSessionProposals) {
-                if (rtpSessionProposals.remove(proposal) != null) {
+                if (proposal != null && rtpSessionProposals.remove(proposal) != null) {
                     writeLogMissedOutgoing(account, proposal.with, proposal.sessionId, serverMsgId, timestamp);
-                    toneManager.transition(RtpEndUserState.DECLINED_OR_BUSY);
+                    toneManager.transition(RtpEndUserState.DECLINED_OR_BUSY, proposal.media);
                     mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, proposal.with, proposal.sessionId, RtpEndUserState.DECLINED_OR_BUSY);
                 } else {
                     Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": no rtp session proposal found for " + from + " to deliver reject");
@@ -511,7 +512,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
 
     private void retractSessionProposal(RtpSessionProposal rtpSessionProposal) {
         final Account account = rtpSessionProposal.account;
-        toneManager.transition(RtpEndUserState.ENDED);
+        toneManager.transition(RtpEndUserState.ENDED, rtpSessionProposal.media);
         Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retracting rtp session proposal with " + rtpSessionProposal.with);
         this.rtpSessionProposals.remove(rtpSessionProposal);
         final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionRetract(rtpSessionProposal);
@@ -527,7 +528,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                     final DeviceDiscoveryState preexistingState = entry.getValue();
                     if (preexistingState != null && preexistingState != DeviceDiscoveryState.FAILED) {
                         final RtpEndUserState endUserState = preexistingState.toEndUserState();
-                        toneManager.transition(endUserState);
+                        toneManager.transition(endUserState, media);
                         mXmppConnectionService.notifyJingleRtpConnectionUpdate(
                                 account,
                                 with,
@@ -623,7 +624,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
             }
             this.rtpSessionProposals.put(sessionProposal, target);
             final RtpEndUserState endUserState = target.toEndUserState();
-            toneManager.transition(endUserState);
+            toneManager.transition(endUserState, sessionProposal.media);
             mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, sessionProposal.with, sessionProposal.sessionId, endUserState);
             Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": flagging session " + sessionId + " as " + target);
         }

src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java 🔗

@@ -1,10 +1,10 @@
 package eu.siacs.conversations.xmpp.jingle;
 
+import android.content.Context;
 import android.media.AudioManager;
 import android.media.ToneGenerator;
 import android.util.Log;
 
-import java.util.Collections;
 import java.util.Set;
 import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
@@ -16,27 +16,22 @@ import static java.util.Arrays.asList;
 class ToneManager {
 
     private final ToneGenerator toneGenerator;
+    private final Context context;
 
     private ToneState state = null;
     private ScheduledFuture<?> currentTone;
+    private boolean appRtcAudioManagerHasControl = false;
 
-    ToneManager() {
+    ToneManager(final Context context) {
         ToneGenerator toneGenerator;
         try {
-            toneGenerator = new ToneGenerator(AudioManager.STREAM_MUSIC, 35);
+            toneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, 60);
         } catch (final RuntimeException e) {
             Log.e(Config.LOGTAG, "unable to instantiate ToneGenerator", e);
             toneGenerator = null;
         }
         this.toneGenerator = toneGenerator;
-    }
-
-    void transition(final RtpEndUserState state) {
-        transition(of(true, state, Collections.emptySet()));
-    }
-
-    void transition(final boolean isInitiator, final RtpEndUserState state, final Set<Media> media) {
-        transition(of(isInitiator, state, media));
+        this.context = context;
     }
 
     private static ToneState of(final boolean isInitiator, final RtpEndUserState state, final Set<Media> media) {
@@ -65,7 +60,15 @@ class ToneManager {
         return ToneState.NULL;
     }
 
-    private synchronized void transition(ToneState state) {
+    void transition(final RtpEndUserState state, final Set<Media> media) {
+        transition(of(true, state, media), media);
+    }
+
+    void transition(final boolean isInitiator, final RtpEndUserState state, final Set<Media> media) {
+        transition(of(isInitiator, state, media), media);
+    }
+
+    private synchronized void transition(ToneState state, final Set<Media> media) {
         if (this.state == state) {
             return;
         }
@@ -74,6 +77,9 @@ class ToneManager {
         }
         cancelCurrentTone();
         Log.d(Config.LOGTAG, getClass().getName() + ".transition(" + state + ")");
+        if (state != ToneState.NULL) {
+            configureAudioManagerForCall(media);
+        }
         switch (state) {
             case RINGING:
                 scheduleWaitingTone();
@@ -91,6 +97,10 @@ class ToneManager {
         this.state = state;
     }
 
+    void setAppRtcAudioManagerHasControl(final boolean appRtcAudioManagerHasControl) {
+        this.appRtcAudioManagerHasControl = appRtcAudioManagerHasControl;
+    }
+
     private void scheduleConnected() {
         this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> {
             startTone(ToneGenerator.TONE_PROP_PROMPT, 200);
@@ -101,12 +111,14 @@ class ToneManager {
         this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> {
             startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375);
         }, 0, TimeUnit.SECONDS);
+        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);
+        JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(this::resetAudioManager, 2500, TimeUnit.MILLISECONDS);
     }
 
     private void scheduleWaitingTone() {
@@ -132,6 +144,35 @@ class ToneManager {
         }
     }
 
+    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
     }

src/main/java/eu/siacs/conversations/xmpp/jingle/WebRTCWrapper.java 🔗

@@ -53,6 +53,7 @@ import javax.annotation.Nullable;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.services.AppRTCAudioManager;
+import eu.siacs.conversations.services.XmppConnectionService;
 
 public class WebRTCWrapper {
 
@@ -174,6 +175,7 @@ public class WebRTCWrapper {
     private PeerConnection peerConnection = null;
     private AudioTrack localAudioTrack = null;
     private AppRTCAudioManager appRTCAudioManager = null;
+    private ToneManager toneManager = null;
     private Context context = null;
     private EglBase eglBase = null;
     private CapturerChoice capturerChoice;
@@ -206,18 +208,20 @@ public class WebRTCWrapper {
         return null;
     }
 
-    public void setup(final Context context, final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference) throws InitializationException {
+    public void setup(final XmppConnectionService service, final AppRTCAudioManager.SpeakerPhonePreference speakerPhonePreference) throws InitializationException {
         try {
             PeerConnectionFactory.initialize(
-                    PeerConnectionFactory.InitializationOptions.builder(context).createInitializationOptions()
+                    PeerConnectionFactory.InitializationOptions.builder(service).createInitializationOptions()
             );
         } catch (final UnsatisfiedLinkError e) {
             throw new InitializationException(e);
         }
         this.eglBase = EglBase.create();
-        this.context = context;
+        this.context = service;
+        this.toneManager = service.getJingleConnectionManager().toneManager;
         mainHandler.post(() -> {
-            appRTCAudioManager = AppRTCAudioManager.create(context, speakerPhonePreference);
+            appRTCAudioManager = AppRTCAudioManager.create(service, speakerPhonePreference);
+            toneManager.setAppRtcAudioManagerHasControl(true);
             appRTCAudioManager.start(audioManagerEvents);
             eventCallback.onAudioDeviceChanged(appRTCAudioManager.getSelectedAudioDevice(), appRTCAudioManager.getAudioDevices());
         });
@@ -288,6 +292,7 @@ public class WebRTCWrapper {
             this.peerConnection = null;
         }
         if (audioManager != null) {
+            toneManager.setAppRtcAudioManagerHasControl(false);
             mainHandler.post(audioManager::stop);
         }
         this.localVideoTrack = null;