@@ -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);
}
}
@@ -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() {
@@ -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
+ }
+}