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_DIAL_TONE_LITE, 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 toneGenerator.stopTone();
165 }
166 }
167
168 private void startTone(final int toneType, final int durationMs) {
169 if (this.toneGenerator != null) {
170 this.toneGenerator.release();;
171
172 }
173 final AudioManager audioManager = ContextCompat.getSystemService(context, AudioManager.class);
174 final boolean ringerModeNormal = audioManager == null || audioManager.getRingerMode() == AudioManager.RINGER_MODE_NORMAL;
175 this.toneGenerator = getToneGenerator(ringerModeNormal);
176 if (toneGenerator != null) {
177 this.toneGenerator.startTone(toneType, durationMs);
178 }
179 }
180
181 private static ToneGenerator getToneGenerator(final boolean ringerModeNormal) {
182 try {
183 if (ringerModeNormal) {
184 return new ToneGenerator(AudioManager.STREAM_VOICE_CALL,60);
185 } else {
186 return new ToneGenerator(AudioManager.STREAM_MUSIC,100);
187 }
188 } catch (final Exception e) {
189 Log.d(Config.LOGTAG,"could not create tone generator",e);
190 return null;
191 }
192 }
193
194 private void configureAudioManagerForCall(final Set<Media> media) {
195 if (appRtcAudioManagerHasControl) {
196 Log.d(Config.LOGTAG, ToneManager.class.getName() + ": do not configure audio manager because RTC has control");
197 return;
198 }
199 final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
200 if (audioManager == null) {
201 return;
202 }
203 final boolean isSpeakerPhone = media.contains(Media.VIDEO);
204 Log.d(Config.LOGTAG, ToneManager.class.getName() + ": putting AudioManager into communication mode. speaker=" + isSpeakerPhone);
205 audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
206 audioManager.setSpeakerphoneOn(isSpeakerPhone);
207 }
208
209 private void resetAudioManager() {
210 if (appRtcAudioManagerHasControl) {
211 Log.d(Config.LOGTAG, ToneManager.class.getName() + ": do not reset audio manager because RTC has control");
212 return;
213 }
214 final AudioManager audioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
215 if (audioManager == null) {
216 return;
217 }
218 Log.d(Config.LOGTAG, ToneManager.class.getName() + ": putting AudioManager back into normal mode");
219 audioManager.setMode(AudioManager.MODE_NORMAL);
220 audioManager.setSpeakerphoneOn(false);
221 }
222
223 private enum ToneState {
224 NULL, RINGING, CONNECTED, BUSY, ENDING_CALL
225 }
226}