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