diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index a6c14a384..975f25114 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/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 rtpSessionProposals = new HashMap<>(); - private final Map connections = new ConcurrentHashMap<>(); + private final ConcurrentHashMap connections = new ConcurrentHashMap<>(); private final Cache 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 getOngoingRtpConnection(final Contact contact) { - for(final Map.Entry entry : this.connections.entrySet()) { + for (final Map.Entry 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); } } diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java index 0dcac3326..7817c2d2e 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleRtpConnection.java +++ b/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 = contentMap == null ? Collections.emptySet() : contentMap.getMedia(); + jingleConnectionManager.toneManager.transition(isInitiator(), endUserState, media); + xmppConnectionService.notifyJingleRtpConnectionUpdate(id.account, id.with, id.sessionId, endUserState); } private void updateOngoingCallNotification() { diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/ToneManager.java new file mode 100644 index 000000000..d805e6b24 --- /dev/null +++ b/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) { + transition(of(isInitiator, state, media)); + } + + private static ToneState of(final boolean isInitiator, final RtpEndUserState state, final Set 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 + } +}