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