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