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                scheduleWaitingTone();
 93                break;
 94            case CONNECTED:
 95                scheduleConnected();
 96                break;
 97            case BUSY:
 98                scheduleBusy();
 99                break;
100            case ENDING_CALL:
101                scheduleEnding();
102                break;
103            case NULL:
104                if (noResetScheduled()) {
105                    resetAudioManager();
106                }
107                break;
108            default:
109                throw new IllegalStateException("Unable to handle transition to "+state);
110        }
111        this.state = state;
112    }
113
114    private static RtpEndUserState normalize(final RtpEndUserState endUserState) {
115        if (Arrays.asList(
116                        RtpEndUserState.CONNECTED,
117                        RtpEndUserState.RECONNECTING,
118                        RtpEndUserState.INCOMING_CONTENT_ADD)
119                .contains(endUserState)) {
120            return RtpEndUserState.CONNECTED;
121        } else {
122            return endUserState;
123        }
124    }
125
126    void setAppRtcAudioManagerHasControl(final boolean appRtcAudioManagerHasControl) {
127        this.appRtcAudioManagerHasControl = appRtcAudioManagerHasControl;
128    }
129
130    private void scheduleConnected() {
131        this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> {
132            startTone(ToneGenerator.TONE_PROP_PROMPT, 200);
133        }, 0, TimeUnit.SECONDS);
134    }
135
136    private void scheduleEnding() {
137        this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> {
138            startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375);
139        }, 0, TimeUnit.SECONDS);
140        this.currentResetFuture = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(this::resetAudioManager, 375, TimeUnit.MILLISECONDS);
141    }
142
143    private void scheduleBusy() {
144        this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(() -> {
145            startTone(ToneGenerator.TONE_CDMA_NETWORK_BUSY, 2500);
146        }, 0, TimeUnit.SECONDS);
147        this.currentResetFuture = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(this::resetAudioManager, 2500, TimeUnit.MILLISECONDS);
148    }
149
150    private void scheduleWaitingTone() {
151        this.currentTone = JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.scheduleAtFixedRate(() -> {
152            startTone(ToneGenerator.TONE_CDMA_NETWORK_USA_RINGBACK, 750);
153        }, 0, 3, TimeUnit.SECONDS);
154    }
155
156    private boolean noResetScheduled() {
157        return this.currentResetFuture == null || this.currentResetFuture.isDone();
158    }
159
160    private void cancelCurrentTone() {
161        if (currentTone != null) {
162            currentTone.cancel(true);
163        }
164        stopTone(toneGenerator);
165    }
166
167    private static void stopTone(final ToneGenerator toneGenerator) {
168        if (toneGenerator == null) {
169            return;
170        }
171        try {
172            toneGenerator.stopTone();
173        } catch (final RuntimeException e) {
174            Log.w(Config.LOGTAG,"tone has already stopped");
175        }
176    }
177
178    public void startTone(final int toneType, final int durationMs) {
179        if (this.toneGenerator != null) {
180            this.toneGenerator.release();
181
182        }
183        final AudioManager audioManager = ContextCompat.getSystemService(context, AudioManager.class);
184        final boolean ringerModeNormal = audioManager == null || audioManager.getRingerMode() == AudioManager.RINGER_MODE_NORMAL;
185        this.toneGenerator = getToneGenerator(ringerModeNormal);
186        if (toneGenerator != null) {
187            this.toneGenerator.startTone(toneType, durationMs);
188        }
189    }
190
191    private static ToneGenerator getToneGenerator(final boolean ringerModeNormal) {
192        try {
193            // when silent and on Android 12+ use STREAM_MUSIC
194            if (ringerModeNormal || Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
195                return new ToneGenerator(AudioManager.STREAM_VOICE_CALL,60);
196            } else {
197                return new ToneGenerator(AudioManager.STREAM_MUSIC,100);
198            }
199        } catch (final Exception e) {
200            Log.d(Config.LOGTAG,"could not create tone generator",e);
201            return null;
202        }
203    }
204
205    private void configureAudioManagerForCall(final Set<Media> media) {
206        if (appRtcAudioManagerHasControl) {
207            Log.d(Config.LOGTAG, ToneManager.class.getName() + ": do not configure audio manager because RTC has control");
208            return;
209        }
210        final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
211        if (audioManager == null) {
212            return;
213        }
214        final boolean isSpeakerPhone = media.contains(Media.VIDEO);
215        Log.d(Config.LOGTAG, ToneManager.class.getName() + ": putting AudioManager into communication mode. speaker=" + isSpeakerPhone);
216        audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
217        audioManager.setSpeakerphoneOn(isSpeakerPhone);
218    }
219
220    private void resetAudioManager() {
221        if (appRtcAudioManagerHasControl) {
222            Log.d(Config.LOGTAG, ToneManager.class.getName() + ": do not reset audio manager because RTC has control");
223            return;
224        }
225        final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
226        if (audioManager == null) {
227            return;
228        }
229        Log.d(Config.LOGTAG, ToneManager.class.getName() + ": putting AudioManager back into normal mode");
230        audioManager.setMode(AudioManager.MODE_NORMAL);
231        audioManager.setSpeakerphoneOn(false);
232    }
233
234    private enum ToneState {
235        NULL, RINGING, CONNECTED, BUSY, ENDING_CALL
236    }
237}