play busy and dial tones

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java |  19 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java     |   6 
src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java             | 105 
3 files changed, 123 insertions(+), 7 deletions(-)

Detailed changes

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

@@ -50,9 +50,10 @@ import eu.siacs.conversations.xmpp.stanzas.MessagePacket;
 import rocks.xmpp.addr.Jid;
 
 public class JingleConnectionManager extends AbstractConnectionManager {
-    private final ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
+    public static final ScheduledExecutorService SCHEDULED_EXECUTOR_SERVICE = Executors.newSingleThreadScheduledExecutor();
+    public final ToneManager toneManager = new ToneManager();
     private final HashMap<RtpSessionProposal, DeviceDiscoveryState> rtpSessionProposals = new HashMap<>();
-    private final Map<AbstractJingleConnection.Id, AbstractJingleConnection> connections = new ConcurrentHashMap<>();
+    private final ConcurrentHashMap<AbstractJingleConnection.Id, AbstractJingleConnection> connections = new ConcurrentHashMap<>();
 
     private final Cache<PersistableSessionId, JingleRtpConnection.State> endedSessions = CacheBuilder.newBuilder()
             .expireAfterWrite(30, TimeUnit.MINUTES)
@@ -141,7 +142,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
     }
 
     ScheduledFuture<?> schedule(final Runnable runnable, final long delay, final TimeUnit timeUnit) {
-        return this.scheduledExecutorService.schedule(runnable, delay, timeUnit);
+        return this.SCHEDULED_EXECUTOR_SERVICE.schedule(runnable, delay, timeUnit);
     }
 
     void respondWithJingleError(final Account account, final IqPacket original, String jingleCondition, String condition, String conditionType) {
@@ -268,6 +269,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
             synchronized (rtpSessionProposals) {
                 if (rtpSessionProposals.remove(proposal) != null) {
                     writeLogMissedOutgoing(account, proposal.with, proposal.sessionId, serverMsgId, timestamp);
+                    toneManager.transition(true, RtpEndUserState.DECLINED_OR_BUSY);
                     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");
@@ -352,7 +354,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
     }
 
     public Optional<AbstractJingleConnection.Id> getOngoingRtpConnection(final Contact contact) {
-        for(final Map.Entry<AbstractJingleConnection.Id,AbstractJingleConnection> entry : this.connections.entrySet()) {
+        for (final Map.Entry<AbstractJingleConnection.Id, AbstractJingleConnection> entry : this.connections.entrySet()) {
             if (entry.getValue() instanceof JingleRtpConnection) {
                 final AbstractJingleConnection.Id id = entry.getKey();
                 if (id.account == contact.getAccount() && id.with.asBareJid().equals(contact.getJid().asBareJid())) {
@@ -423,6 +425,7 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                 }
             }
             if (matchingProposal != null) {
+                toneManager.transition(true, RtpEndUserState.ENDED);
                 Log.d(Config.LOGTAG, account.getJid().asBareJid() + ": retracting rtp session proposal with " + with);
                 this.rtpSessionProposals.remove(matchingProposal);
                 final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionRetract(matchingProposal);
@@ -439,11 +442,13 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                 if (proposal.account == account && with.asBareJid().equals(proposal.with)) {
                     final DeviceDiscoveryState preexistingState = entry.getValue();
                     if (preexistingState != null && preexistingState != DeviceDiscoveryState.FAILED) {
+                        final RtpEndUserState endUserState = preexistingState.toEndUserState();
+                        toneManager.transition(true, endUserState);
                         mXmppConnectionService.notifyJingleRtpConnectionUpdate(
                                 account,
                                 with,
                                 proposal.sessionId,
-                                preexistingState.toEndUserState()
+                                endUserState
                         );
                         return;
                     }
@@ -529,7 +534,9 @@ public class JingleConnectionManager extends AbstractConnectionManager {
                 return;
             }
             this.rtpSessionProposals.put(sessionProposal, target);
-            mXmppConnectionService.notifyJingleRtpConnectionUpdate(account, sessionProposal.with, sessionProposal.sessionId, target.toEndUserState());
+            final RtpEndUserState endUserState = target.toEndUserState();
+            toneManager.transition(true, endUserState);
+            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/JingleRtpConnection.java 🔗

@@ -1027,7 +1027,11 @@ public class JingleRtpConnection extends AbstractJingleConnection implements Web
     }
 
     private void updateEndUserState() {
-        xmppConnectionService.notifyJingleRtpConnectionUpdate(id.account, id.with, id.sessionId, getEndUserState());
+        final RtpEndUserState endUserState = getEndUserState();
+        final RtpContentMap contentMap = initiatorRtpContentMap;
+        final Set<Media> media = contentMap == null ? Collections.emptySet() : contentMap.getMedia();
+        jingleConnectionManager.toneManager.transition(isInitiator(), endUserState, media);
+        xmppConnectionService.notifyJingleRtpConnectionUpdate(id.account, id.with, id.sessionId, endUserState);
     }
 
     private void updateOngoingCallNotification() {

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

@@ -0,0 +1,105 @@
+package eu.siacs.conversations.xmpp.jingle;
+
+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;
+
+import eu.siacs.conversations.Config;
+
+import static java.util.Arrays.asList;
+
+public class ToneManager {
+
+    private final ToneGenerator toneGenerator;
+
+    private ToneState state = null;
+    private ScheduledFuture<?> currentTone;
+
+    public ToneManager() {
+        this.toneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, 35);
+    }
+
+    public void transition(final boolean isInitiator, final RtpEndUserState state) {
+        transition(of(isInitiator, state, Collections.emptySet()));
+    }
+
+    public void transition(final boolean isInitiator, final RtpEndUserState state, final Set<Media> media) {
+        transition(of(isInitiator, state, media));
+    }
+
+    private static ToneState of(final boolean isInitiator, final RtpEndUserState state, final Set<Media> media) {
+        if (isInitiator) {
+            if (asList(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;
+            }
+        }
+        return ToneState.NULL;
+    }
+
+    private synchronized void transition(ToneState state) {
+        if (this.state == state) {
+            return;
+        }
+        if (state == ToneState.NULL && this.state == ToneState.ENDING_CALL) {
+            return;
+        }
+        cancelCurrentTone();
+        Log.d(Config.LOGTAG, getClass().getName() + ".transition(" + state + ")");
+        switch (state) {
+            case RINGING:
+                scheduleWaitingTone();
+                break;
+            case BUSY:
+                scheduleBusy();
+                break;
+            case ENDING_CALL:
+                scheduleEnding();
+                break;
+        }
+        this.state = state;
+    }
+
+    private void scheduleEnding() {
+        this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> {
+            this.toneGenerator.startTone(ToneGenerator.TONE_CDMA_CONFIRM, 600);
+        }, 0, TimeUnit.SECONDS);
+    }
+
+    private void scheduleBusy() {
+        this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> {
+            this.toneGenerator.startTone(ToneGenerator.TONE_CDMA_NETWORK_BUSY, 2500);
+        }, 0, TimeUnit.SECONDS);
+    }
+
+    private void scheduleWaitingTone() {
+        this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate(() -> {
+            this.toneGenerator.startTone(ToneGenerator.TONE_CDMA_DIAL_TONE_LITE, 750);
+        }, 0, 3, TimeUnit.SECONDS);
+    }
+
+    private void cancelCurrentTone() {
+        if (currentTone != null) {
+            currentTone.cancel(true);
+        }
+        toneGenerator.stopTone();
+    }
+
+    private enum ToneState {
+        NULL, RINGING, BUSY, ENDING_CALL
+    }
+}