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