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