ToneManager.java

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