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