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