1package eu.siacs.conversations.services;
2
3import android.content.Context;
4import android.content.pm.PackageManager;
5import android.media.AudioAttributes;
6import android.media.AudioManager;
7import android.media.ToneGenerator;
8import android.net.Uri;
9import android.os.Build;
10import android.telecom.CallAudioState;
11import android.telecom.CallEndpoint;
12import android.telecom.Connection;
13import android.telecom.DisconnectCause;
14import android.util.Log;
15
16import androidx.annotation.NonNull;
17import androidx.annotation.RequiresApi;
18
19import com.google.common.collect.ImmutableSet;
20import com.google.common.collect.Iterables;
21import com.google.common.collect.Lists;
22
23import eu.siacs.conversations.Config;
24import eu.siacs.conversations.R;
25import eu.siacs.conversations.ui.util.MainThreadExecutor;
26import eu.siacs.conversations.xmpp.Jid;
27import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
28import eu.siacs.conversations.xmpp.jingle.Media;
29
30import java.util.Arrays;
31import java.util.Collections;
32import java.util.List;
33import java.util.Set;
34import java.util.concurrent.TimeUnit;
35import java.util.concurrent.atomic.AtomicBoolean;
36
37public class CallIntegration extends Connection {
38
39 private static final List<String> BROKEN_DEVICE_MODELS =
40 Arrays.asList(
41 "OnePlus6" // OnePlus 6 (Android 8.1-11) Device is buggy and always starts the
42 // OS call screen even though we want to be self managed
43 );
44
45 public static final int DEFAULT_TONE_VOLUME = 60;
46 private static final int DEFAULT_MEDIA_PLAYER_VOLUME = 90;
47
48 private final Context context;
49
50 private final AppRTCAudioManager appRTCAudioManager;
51 private AudioDevice initialAudioDevice = null;
52
53 private boolean isAudioRoutingRequested = false;
54 private final AtomicBoolean initialAudioDeviceConfigured = new AtomicBoolean(false);
55 private final AtomicBoolean delayedDestructionInitiated = new AtomicBoolean(false);
56 private final AtomicBoolean isDestroyed = new AtomicBoolean(false);
57
58 private List<CallEndpoint> availableEndpoints = Collections.emptyList();
59 private boolean isMicrophoneEnabled = true;
60
61 private Callback callback = null;
62
63 public CallIntegration(final Context context) {
64 this.context = context.getApplicationContext();
65 if (selfManaged()) {
66 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
67 setConnectionProperties(Connection.PROPERTY_SELF_MANAGED);
68 } else {
69 throw new AssertionError(
70 "Trying to set connection properties on unsupported version");
71 }
72 this.appRTCAudioManager = null;
73 } else {
74 this.appRTCAudioManager = new AppRTCAudioManager(context);
75 this.appRTCAudioManager.setAudioManagerEvents(this::onAudioDeviceChanged);
76 }
77 setRingbackRequested(true);
78 setConnectionCapabilities(CAPABILITY_MUTE | CAPABILITY_RESPOND_VIA_TEXT);
79 }
80
81 public void setCallback(final Callback callback) {
82 this.callback = callback;
83 }
84
85 @Override
86 public void onShowIncomingCallUi() {
87 Log.d(Config.LOGTAG, "onShowIncomingCallUi");
88 this.callback.onCallIntegrationShowIncomingCallUi();
89 }
90
91 @Override
92 public void onAnswer() {
93 this.callback.onCallIntegrationAnswer();
94 }
95
96 @Override
97 public void onDisconnect() {
98 Log.d(Config.LOGTAG, "onDisconnect()");
99 this.callback.onCallIntegrationDisconnect();
100 }
101
102 @Override
103 public void onReject() {
104 this.callback.onCallIntegrationReject();
105 }
106
107 @Override
108 public void onReject(final String replyMessage) {
109 Log.d(Config.LOGTAG, "onReject(" + replyMessage + ")");
110 this.callback.onCallIntegrationReject();
111 }
112
113 @Override
114 public void onPlayDtmfTone(char c) {
115 this.callback.applyDtmfTone("" + c);
116 }
117
118 @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
119 @Override
120 public void onAvailableCallEndpointsChanged(@NonNull List<CallEndpoint> availableEndpoints) {
121 Log.d(Config.LOGTAG, "onAvailableCallEndpointsChanged(" + availableEndpoints + ")");
122 this.availableEndpoints = availableEndpoints;
123 this.onAudioDeviceChanged(
124 getAudioDeviceUpsideDownCake(getCurrentCallEndpoint()),
125 ImmutableSet.copyOf(
126 Lists.transform(
127 availableEndpoints,
128 CallIntegration::getAudioDeviceUpsideDownCake)));
129 }
130
131 @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
132 @Override
133 public void onCallEndpointChanged(@NonNull final CallEndpoint callEndpoint) {
134 Log.d(Config.LOGTAG, "onCallEndpointChanged()");
135 this.onAudioDeviceChanged(
136 getAudioDeviceUpsideDownCake(callEndpoint),
137 ImmutableSet.copyOf(
138 Lists.transform(
139 this.availableEndpoints,
140 CallIntegration::getAudioDeviceUpsideDownCake)));
141 }
142
143 @Override
144 public void onCallAudioStateChanged(final CallAudioState state) {
145 if (selfManaged() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
146 Log.d(Config.LOGTAG, "ignoring onCallAudioStateChange() on Upside Down Cake");
147 return;
148 }
149 setMicrophoneEnabled(!state.isMuted());
150 Log.d(Config.LOGTAG, "onCallAudioStateChange(" + state + ")");
151 this.onAudioDeviceChanged(getAudioDeviceOreo(state), getAudioDevicesOreo(state));
152 }
153
154 @Override
155 public void onMuteStateChanged(final boolean isMuted) {
156 Log.d(Config.LOGTAG, "onMuteStateChanged(" + isMuted + ")");
157 setMicrophoneEnabled(!isMuted);
158 }
159
160 private void setMicrophoneEnabled(final boolean enabled) {
161 this.isMicrophoneEnabled = enabled;
162 this.callback.onCallIntegrationMicrophoneEnabled(enabled);
163 }
164
165 public boolean isMicrophoneEnabled() {
166 return this.isMicrophoneEnabled;
167 }
168
169 public Set<AudioDevice> getAudioDevices() {
170 if (notSelfManaged(context)) {
171 return getAudioDevicesFallback();
172 } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
173 return getAudioDevicesUpsideDownCake();
174 } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
175 return getAudioDevicesOreo();
176 } else {
177 throw new AssertionError("Trying to get audio devices on unsupported version");
178 }
179 }
180
181 public AudioDevice getSelectedAudioDevice() {
182 if (notSelfManaged(context)) {
183 return getAudioDeviceFallback();
184 } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
185 return getAudioDeviceUpsideDownCake();
186 } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
187 return getAudioDeviceOreo();
188 } else {
189 throw new AssertionError("Trying to get selected audio device on unsupported version");
190 }
191 }
192
193 public void setAudioDevice(final AudioDevice audioDevice) {
194 if (notSelfManaged(context)) {
195 setAudioDeviceFallback(audioDevice);
196 } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
197 setAudioDeviceUpsideDownCake(audioDevice);
198 } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
199 setAudioDeviceOreo(audioDevice);
200 } else {
201 throw new AssertionError("Trying to set audio devices on unsupported version");
202 }
203 }
204
205 public void setAudioDeviceWhenAvailable(final AudioDevice audioDevice) {
206 final var available = getAudioDevices();
207 if (available.contains(audioDevice) && !available.contains(AudioDevice.BLUETOOTH)) {
208 this.setAudioDevice(audioDevice);
209 } else {
210 Log.d(
211 Config.LOGTAG,
212 "application requested to switch to "
213 + audioDevice
214 + " but we won't because available devices are "
215 + available);
216 }
217 }
218
219 @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
220 private Set<AudioDevice> getAudioDevicesUpsideDownCake() {
221 return ImmutableSet.copyOf(
222 Lists.transform(
223 this.availableEndpoints, CallIntegration::getAudioDeviceUpsideDownCake));
224 }
225
226 @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
227 private AudioDevice getAudioDeviceUpsideDownCake() {
228 return getAudioDeviceUpsideDownCake(getCurrentCallEndpoint());
229 }
230
231 @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
232 private static AudioDevice getAudioDeviceUpsideDownCake(final CallEndpoint callEndpoint) {
233 if (callEndpoint == null) {
234 return AudioDevice.NONE;
235 }
236 final var endpointType = callEndpoint.getEndpointType();
237 return switch (endpointType) {
238 case CallEndpoint.TYPE_BLUETOOTH -> AudioDevice.BLUETOOTH;
239 case CallEndpoint.TYPE_EARPIECE -> AudioDevice.EARPIECE;
240 case CallEndpoint.TYPE_SPEAKER -> AudioDevice.SPEAKER_PHONE;
241 case CallEndpoint.TYPE_WIRED_HEADSET -> AudioDevice.WIRED_HEADSET;
242 case CallEndpoint.TYPE_STREAMING -> AudioDevice.STREAMING;
243 case CallEndpoint.TYPE_UNKNOWN -> AudioDevice.NONE;
244 default -> throw new IllegalStateException("Unknown endpoint type " + endpointType);
245 };
246 }
247
248 @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
249 private void setAudioDeviceUpsideDownCake(final AudioDevice audioDevice) {
250 final var callEndpointOptional =
251 Iterables.tryFind(
252 this.availableEndpoints,
253 e -> getAudioDeviceUpsideDownCake(e) == audioDevice);
254 if (callEndpointOptional.isPresent()) {
255 final var endpoint = callEndpointOptional.get();
256 requestCallEndpointChange(
257 endpoint,
258 MainThreadExecutor.getInstance(),
259 result -> Log.d(Config.LOGTAG, "switched to endpoint " + endpoint));
260 } else {
261 Log.w(Config.LOGTAG, "no endpoint found matching " + audioDevice);
262 }
263 }
264
265 private Set<AudioDevice> getAudioDevicesOreo() {
266 final var audioState = getCallAudioState();
267 if (audioState == null) {
268 Log.d(
269 Config.LOGTAG,
270 "no CallAudioState available. returning empty set for audio devices");
271 return Collections.emptySet();
272 }
273 return getAudioDevicesOreo(audioState);
274 }
275
276 private static Set<AudioDevice> getAudioDevicesOreo(final CallAudioState callAudioState) {
277 final ImmutableSet.Builder<AudioDevice> supportedAudioDevicesBuilder =
278 new ImmutableSet.Builder<>();
279 final var supportedRouteMask = callAudioState.getSupportedRouteMask();
280 if ((supportedRouteMask & CallAudioState.ROUTE_BLUETOOTH)
281 == CallAudioState.ROUTE_BLUETOOTH) {
282 supportedAudioDevicesBuilder.add(AudioDevice.BLUETOOTH);
283 }
284 if ((supportedRouteMask & CallAudioState.ROUTE_EARPIECE) == CallAudioState.ROUTE_EARPIECE) {
285 supportedAudioDevicesBuilder.add(AudioDevice.EARPIECE);
286 }
287 if ((supportedRouteMask & CallAudioState.ROUTE_SPEAKER) == CallAudioState.ROUTE_SPEAKER) {
288 supportedAudioDevicesBuilder.add(AudioDevice.SPEAKER_PHONE);
289 }
290 if ((supportedRouteMask & CallAudioState.ROUTE_WIRED_HEADSET)
291 == CallAudioState.ROUTE_WIRED_HEADSET) {
292 supportedAudioDevicesBuilder.add(AudioDevice.WIRED_HEADSET);
293 }
294 return supportedAudioDevicesBuilder.build();
295 }
296
297 private AudioDevice getAudioDeviceOreo() {
298 final var audioState = getCallAudioState();
299 if (audioState == null) {
300 Log.d(Config.LOGTAG, "no CallAudioState available. returning NONE as audio device");
301 return AudioDevice.NONE;
302 }
303 return getAudioDeviceOreo(audioState);
304 }
305
306 private static AudioDevice getAudioDeviceOreo(final CallAudioState audioState) {
307 // technically we get a mask here; maybe we should query the mask instead
308 return switch (audioState.getRoute()) {
309 case CallAudioState.ROUTE_BLUETOOTH -> AudioDevice.BLUETOOTH;
310 case CallAudioState.ROUTE_EARPIECE -> AudioDevice.EARPIECE;
311 case CallAudioState.ROUTE_SPEAKER -> AudioDevice.SPEAKER_PHONE;
312 case CallAudioState.ROUTE_WIRED_HEADSET -> AudioDevice.WIRED_HEADSET;
313 default -> AudioDevice.NONE;
314 };
315 }
316
317 @RequiresApi(api = Build.VERSION_CODES.O)
318 private void setAudioDeviceOreo(final AudioDevice audioDevice) {
319 switch (audioDevice) {
320 case EARPIECE -> setAudioRoute(CallAudioState.ROUTE_EARPIECE);
321 case BLUETOOTH -> setAudioRoute(CallAudioState.ROUTE_BLUETOOTH);
322 case WIRED_HEADSET -> setAudioRoute(CallAudioState.ROUTE_WIRED_HEADSET);
323 case SPEAKER_PHONE -> setAudioRoute(CallAudioState.ROUTE_SPEAKER);
324 }
325 }
326
327 private Set<AudioDevice> getAudioDevicesFallback() {
328 return requireAppRtcAudioManager().getAudioDevices();
329 }
330
331 private AudioDevice getAudioDeviceFallback() {
332 final var audioDevice = requireAppRtcAudioManager().getSelectedAudioDevice();
333 return audioDevice == null ? AudioDevice.NONE : audioDevice;
334 }
335
336 private void setAudioDeviceFallback(final AudioDevice audioDevice) {
337 final var audioManager = requireAppRtcAudioManager();
338 audioManager.executeOnMain(() -> audioManager.setDefaultAudioDevice(audioDevice));
339 }
340
341 @NonNull
342 private AppRTCAudioManager requireAppRtcAudioManager() {
343 if (this.appRTCAudioManager == null) {
344 throw new IllegalStateException(
345 "You are trying to access the fallback audio manager on a modern device");
346 }
347 return this.appRTCAudioManager;
348 }
349
350 @Override
351 public void onSilence() {
352 this.callback.onCallIntegrationSilence();
353 }
354
355 @Override
356 public void onStateChanged(final int state) {
357 Log.d(Config.LOGTAG, "onStateChanged(" + state + ")");
358 if (notSelfManaged(context)) {
359 if (state == STATE_DIALING) {
360 requireAppRtcAudioManager().startRingBack();
361 } else {
362 requireAppRtcAudioManager().stopRingBack();
363 }
364 }
365 if (state == STATE_ACTIVE) {
366 playConnectedSound();
367 } else if (state == STATE_DISCONNECTED) {
368 final var audioManager = this.appRTCAudioManager;
369 if (audioManager != null) {
370 audioManager.executeOnMain(audioManager::stop);
371 }
372 }
373 }
374
375 private void playConnectedSound() {
376 final var audioAttributes =
377 new AudioAttributes.Builder()
378 .setLegacyStreamType(AudioManager.STREAM_VOICE_CALL)
379 .build();
380 final var mediaPlayer =
381 MediaPlayer.create(
382 context,
383 R.raw.connected,
384 audioAttributes,
385 AudioManager.AUDIO_SESSION_ID_GENERATE);
386 mediaPlayer.setVolume(
387 DEFAULT_MEDIA_PLAYER_VOLUME / 100f, DEFAULT_MEDIA_PLAYER_VOLUME / 100f);
388 mediaPlayer.start();
389 }
390
391 public void success() {
392 Log.d(Config.LOGTAG, "CallIntegration.success()");
393 final var toneGenerator =
394 new ToneGenerator(AudioManager.STREAM_VOICE_CALL, DEFAULT_TONE_VOLUME);
395 toneGenerator.startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375);
396 this.destroyWithDelay(new DisconnectCause(DisconnectCause.LOCAL, null), 375);
397 }
398
399 public void accepted() {
400 Log.d(Config.LOGTAG, "CallIntegration.accepted()");
401 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
402 this.destroyWith(new DisconnectCause(DisconnectCause.ANSWERED_ELSEWHERE, null));
403 } else {
404 this.destroyWith(new DisconnectCause(DisconnectCause.CANCELED, null));
405 }
406 }
407
408 public void error() {
409 Log.d(Config.LOGTAG, "CallIntegration.error()");
410 final var toneGenerator =
411 new ToneGenerator(AudioManager.STREAM_VOICE_CALL, DEFAULT_TONE_VOLUME);
412 toneGenerator.startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375);
413 this.destroyWithDelay(new DisconnectCause(DisconnectCause.ERROR, null), 375);
414 }
415
416 public void retracted() {
417 Log.d(Config.LOGTAG, "CallIntegration.retracted()");
418 // an alternative cause would be LOCAL
419 this.destroyWith(new DisconnectCause(DisconnectCause.CANCELED, null));
420 }
421
422 public void rejected() {
423 Log.d(Config.LOGTAG, "CallIntegration.rejected()");
424 this.destroyWith(new DisconnectCause(DisconnectCause.REJECTED, null));
425 }
426
427 public void busy() {
428 Log.d(Config.LOGTAG, "CallIntegration.busy()");
429 final var toneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, 80);
430 toneGenerator.startTone(ToneGenerator.TONE_CDMA_NETWORK_BUSY, 2500);
431 this.destroyWithDelay(new DisconnectCause(DisconnectCause.BUSY, null), 2500);
432 }
433
434 private void destroyWithDelay(final DisconnectCause disconnectCause, final int delay) {
435 if (this.delayedDestructionInitiated.compareAndSet(false, true)) {
436 JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(
437 () -> {
438 this.setDisconnected(disconnectCause);
439 this.destroyCallIntegration();
440 },
441 delay,
442 TimeUnit.MILLISECONDS);
443 } else {
444 Log.w(Config.LOGTAG, "CallIntegration destruction has already been scheduled!");
445 }
446 }
447
448 private void destroyWith(final DisconnectCause disconnectCause) {
449 if (this.getState() == STATE_DISCONNECTED || this.delayedDestructionInitiated.get()) {
450 Log.d(Config.LOGTAG, "CallIntegration has already been destroyed");
451 return;
452 }
453 this.setDisconnected(disconnectCause);
454 this.destroyCallIntegration();
455 Log.d(Config.LOGTAG, "destroyed!");
456 }
457
458 public static Uri address(final Jid contact) {
459 return Uri.parse(String.format("xmpp:%s", contact.toEscapedString()));
460 }
461
462 public void verifyDisconnected() {
463 if (this.getState() == STATE_DISCONNECTED || this.delayedDestructionInitiated.get()) {
464 return;
465 }
466 throw new AssertionError("CallIntegration has not been disconnected");
467 }
468
469 private void onAudioDeviceChanged(
470 final CallIntegration.AudioDevice selectedAudioDevice,
471 final Set<CallIntegration.AudioDevice> availableAudioDevices) {
472 if (isAudioRoutingRequested) {
473 configureInitialAudioDevice(availableAudioDevices);
474 }
475 final var callback = this.callback;
476 if (callback == null) {
477 return;
478 }
479 callback.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
480 }
481
482 private void configureInitialAudioDevice(final Set<AudioDevice> availableAudioDevices) {
483 final var initialAudioDevice = this.initialAudioDevice;
484 if (initialAudioDevice == null) {
485 Log.d(Config.LOGTAG, "skipping configureInitialAudioDevice()");
486 return;
487 }
488 final var target = this.initialAudioDevice;
489 if (this.initialAudioDeviceConfigured.compareAndSet(false, true)) {
490 if (availableAudioDevices.contains(target)
491 && !availableAudioDevices.contains(AudioDevice.BLUETOOTH)) {
492 setAudioDevice(target);
493 Log.d(Config.LOGTAG, "configured initial audio device: " + target);
494 } else {
495 Log.d(
496 Config.LOGTAG,
497 "not setting initial audio device. available devices: "
498 + availableAudioDevices);
499 }
500 }
501 }
502
503 private boolean selfManaged() {
504 return selfManaged(context);
505 }
506
507 public static boolean selfManaged(final Context context) {
508 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
509 && hasSystemFeature(context)
510 && isDeviceModelSupported();
511 }
512
513 public static boolean hasSystemFeature(final Context context) {
514 final var packageManager = context.getPackageManager();
515 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
516 return packageManager.hasSystemFeature(PackageManager.FEATURE_TELECOM);
517 } else {
518 //noinspection deprecation
519 return packageManager.hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE);
520 }
521 }
522
523 private static boolean isDeviceModelSupported() {
524 if (BROKEN_DEVICE_MODELS.contains(Build.DEVICE)) {
525 return false;
526 }
527 // all Realme devices at least up to and including Android 11 are broken
528 if ("realme".equalsIgnoreCase(Build.MANUFACTURER)
529 && Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
530 return false;
531 }
532 return true;
533 }
534
535 public static boolean notSelfManaged(final Context context) {
536 return !selfManaged(context);
537 }
538
539 public void setInitialAudioDevice(final AudioDevice audioDevice) {
540 Log.d(Config.LOGTAG, "setInitialAudioDevice(" + audioDevice + ")");
541 this.initialAudioDevice = audioDevice;
542 }
543
544 public void startAudioRouting() {
545 this.isAudioRoutingRequested = true;
546 if (selfManaged()) {
547 final var devices = getAudioDevices();
548 if (devices.isEmpty()) {
549 return;
550 }
551 configureInitialAudioDevice(devices);
552 return;
553 }
554 final var audioManager = requireAppRtcAudioManager();
555 audioManager.executeOnMain(
556 () -> {
557 audioManager.start();
558 this.onAudioDeviceChanged(
559 audioManager.getSelectedAudioDevice(), audioManager.getAudioDevices());
560 });
561 }
562
563 private void destroyCallIntegration() {
564 super.destroy();
565 this.isDestroyed.set(true);
566 }
567
568 public boolean isDestroyed() {
569 return this.isDestroyed.get();
570 }
571
572 /** AudioDevice is the names of possible audio devices that we currently support. */
573 public enum AudioDevice {
574 NONE,
575 SPEAKER_PHONE,
576 WIRED_HEADSET,
577 EARPIECE,
578 BLUETOOTH,
579 STREAMING
580 }
581
582 public static AudioDevice initialAudioDevice(final Set<Media> media) {
583 if (Media.audioOnly(media)) {
584 return AudioDevice.EARPIECE;
585 } else {
586 return AudioDevice.SPEAKER_PHONE;
587 }
588 }
589
590 public interface Callback {
591 void onCallIntegrationShowIncomingCallUi();
592
593 void onCallIntegrationDisconnect();
594
595 void onAudioDeviceChanged(
596 CallIntegration.AudioDevice selectedAudioDevice,
597 Set<CallIntegration.AudioDevice> availableAudioDevices);
598
599 void onCallIntegrationReject();
600
601 void onCallIntegrationAnswer();
602
603 void onCallIntegrationSilence();
604
605 void onCallIntegrationMicrophoneEnabled(boolean enabled);
606
607 boolean applyDtmfTone(final String dtmf);
608 }
609}