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}