ToneManager.java

  1package eu.siacs.conversations.xmpp.jingle;
  2
  3import android.content.Context;
  4import android.media.AudioManager;
  5import android.media.ToneGenerator;
  6import android.util.Log;
  7
  8import java.util.Set;
  9import java.util.concurrent.ScheduledFuture;
 10import java.util.concurrent.TimeUnit;
 11
 12import eu.siacs.conversations.Config;
 13
 14import static java.util.Arrays.asList;
 15
 16class ToneManager {
 17
 18    private final ToneGenerator toneGenerator;
 19    private final Context context;
 20
 21    private ToneState state = null;
 22    private ScheduledFuture<?> currentTone;
 23    private ScheduledFuture<?> currentResetFuture;
 24    private boolean appRtcAudioManagerHasControl = false;
 25
 26    ToneManager(final Context context) {
 27        ToneGenerator toneGenerator;
 28        try {
 29            toneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, 60);
 30        } catch (final RuntimeException e) {
 31            Log.e(Config.LOGTAG, "unable to instantiate ToneGenerator", e);
 32            toneGenerator = null;
 33        }
 34        this.toneGenerator = toneGenerator;
 35        this.context = context;
 36    }
 37
 38    private static ToneState of(final boolean isInitiator, final RtpEndUserState state, final Set<Media> media) {
 39        if (isInitiator) {
 40            if (asList(RtpEndUserState.FINDING_DEVICE, RtpEndUserState.RINGING, RtpEndUserState.CONNECTING).contains(state)) {
 41                return ToneState.RINGING;
 42            }
 43            if (state == RtpEndUserState.DECLINED_OR_BUSY) {
 44                return ToneState.BUSY;
 45            }
 46        }
 47        if (state == RtpEndUserState.ENDING_CALL) {
 48            if (media.contains(Media.VIDEO)) {
 49                return ToneState.NULL;
 50            } else {
 51                return ToneState.ENDING_CALL;
 52            }
 53        }
 54        if (state == RtpEndUserState.CONNECTED || state == RtpEndUserState.RECONNECTING) {
 55            if (media.contains(Media.VIDEO)) {
 56                return ToneState.NULL;
 57            } else {
 58                return ToneState.CONNECTED;
 59            }
 60        }
 61        return ToneState.NULL;
 62    }
 63
 64    void transition(final RtpEndUserState state, final Set<Media> media) {
 65        transition(of(true, state, media), media);
 66    }
 67
 68    void transition(final boolean isInitiator, final RtpEndUserState state, final Set<Media> media) {
 69        transition(of(isInitiator, state, media), media);
 70    }
 71
 72    private synchronized void transition(ToneState state, final Set<Media> media) {
 73        if (this.state == state) {
 74            return;
 75        }
 76        if (state == ToneState.NULL && this.state == ToneState.ENDING_CALL) {
 77            return;
 78        }
 79        cancelCurrentTone();
 80        Log.d(Config.LOGTAG, getClass().getName() + ".transition(" + state + ")");
 81        if (state != ToneState.NULL) {
 82            configureAudioManagerForCall(media);
 83        }
 84        switch (state) {
 85            case RINGING:
 86                scheduleWaitingTone();
 87                break;
 88            case CONNECTED:
 89                scheduleConnected();
 90                break;
 91            case BUSY:
 92                scheduleBusy();
 93                break;
 94            case ENDING_CALL:
 95                scheduleEnding();
 96                break;
 97            case NULL:
 98                if (noResetScheduled()) {
 99                    resetAudioManager();
100                }
101                break;
102            default:
103                throw new IllegalStateException("Unable to handle transition to "+state);
104        }
105        this.state = state;
106    }
107
108    void setAppRtcAudioManagerHasControl(final boolean appRtcAudioManagerHasControl) {
109        this.appRtcAudioManagerHasControl = appRtcAudioManagerHasControl;
110    }
111
112    private void scheduleConnected() {
113        this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> {
114            startTone(ToneGenerator.TONE_PROP_PROMPT, 200);
115        }, 0, TimeUnit.SECONDS);
116    }
117
118    private void scheduleEnding() {
119        this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> {
120            startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375);
121        }, 0, TimeUnit.SECONDS);
122        this.currentResetFuture = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(this::resetAudioManager, 375, TimeUnit.MILLISECONDS);
123    }
124
125    private void scheduleBusy() {
126        this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> {
127            startTone(ToneGenerator.TONE_CDMA_NETWORK_BUSY, 2500);
128        }, 0, TimeUnit.SECONDS);
129        this.currentResetFuture = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(this::resetAudioManager, 2500, TimeUnit.MILLISECONDS);
130    }
131
132    private void scheduleWaitingTone() {
133        this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate(() -> {
134            startTone(ToneGenerator.TONE_CDMA_DIAL_TONE_LITE, 750);
135        }, 0, 3, TimeUnit.SECONDS);
136    }
137
138    private boolean noResetScheduled() {
139        return this.currentResetFuture == null || this.currentResetFuture.isDone();
140    }
141
142    private void cancelCurrentTone() {
143        if (currentTone != null) {
144            currentTone.cancel(true);
145        }
146        if (toneGenerator != null) {
147            toneGenerator.stopTone();
148        }
149    }
150
151    private void startTone(final int toneType, final int durationMs) {
152        if (toneGenerator != null) {
153            this.toneGenerator.startTone(toneType, durationMs);
154        } else {
155            Log.e(Config.LOGTAG, "failed to start tone. ToneGenerator doesn't exist");
156        }
157    }
158
159    private void configureAudioManagerForCall(final Set<Media> media) {
160        if (appRtcAudioManagerHasControl) {
161            Log.d(Config.LOGTAG, ToneManager.class.getName() + ": do not configure audio manager because RTC has control");
162            return;
163        }
164        final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
165        if (audioManager == null) {
166            return;
167        }
168        final boolean isSpeakerPhone = media.contains(Media.VIDEO);
169        Log.d(Config.LOGTAG, ToneManager.class.getName() + ": putting AudioManager into communication mode. speaker=" + isSpeakerPhone);
170        audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
171        audioManager.setSpeakerphoneOn(isSpeakerPhone);
172    }
173
174    private void resetAudioManager() {
175        if (appRtcAudioManagerHasControl) {
176            Log.d(Config.LOGTAG, ToneManager.class.getName() + ": do not reset audio manager because RTC has control");
177            return;
178        }
179        final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
180        if (audioManager == null) {
181            return;
182        }
183        Log.d(Config.LOGTAG, ToneManager.class.getName() + ": putting AudioManager back into normal mode");
184        audioManager.setMode(AudioManager.MODE_NORMAL);
185        audioManager.setSpeakerphoneOn(false);
186    }
187
188    private enum ToneState {
189        NULL, RINGING, CONNECTED, BUSY, ENDING_CALL
190    }
191}