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 @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
128 @Override
129 public void onAvailableCallEndpointsChanged(@NonNull List<CallEndpoint> availableEndpoints) {
130 Log.d(Config.LOGTAG, "onAvailableCallEndpointsChanged(" + availableEndpoints + ")");
131 this.availableEndpoints = availableEndpoints;
132 this.onAudioDeviceChanged(
133 getAudioDeviceUpsideDownCake(getCurrentCallEndpoint()),
134 ImmutableSet.copyOf(
135 Lists.transform(
136 availableEndpoints,
137 CallIntegration::getAudioDeviceUpsideDownCake)));
138 }
139
140 @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
141 @Override
142 public void onCallEndpointChanged(@NonNull final CallEndpoint callEndpoint) {
143 Log.d(Config.LOGTAG, "onCallEndpointChanged()");
144 this.onAudioDeviceChanged(
145 getAudioDeviceUpsideDownCake(callEndpoint),
146 ImmutableSet.copyOf(
147 Lists.transform(
148 this.availableEndpoints,
149 CallIntegration::getAudioDeviceUpsideDownCake)));
150 }
151
152 @Override
153 public void onCallAudioStateChanged(final CallAudioState state) {
154 if (selfManaged() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
155 Log.d(Config.LOGTAG, "ignoring onCallAudioStateChange() on Upside Down Cake");
156 return;
157 }
158 setMicrophoneEnabled(!state.isMuted());
159 Log.d(Config.LOGTAG, "onCallAudioStateChange(" + state + ")");
160 this.onAudioDeviceChanged(getAudioDeviceOreo(state), getAudioDevicesOreo(state));
161 }
162
163 @Override
164 public void onMuteStateChanged(final boolean isMuted) {
165 Log.d(Config.LOGTAG, "onMuteStateChanged(" + isMuted + ")");
166 setMicrophoneEnabled(!isMuted);
167 }
168
169 private void setMicrophoneEnabled(final boolean enabled) {
170 this.isMicrophoneEnabled = enabled;
171 this.callback.onCallIntegrationMicrophoneEnabled(enabled);
172 }
173
174 public boolean isMicrophoneEnabled() {
175 return this.isMicrophoneEnabled;
176 }
177
178 public Set<AudioDevice> getAudioDevices() {
179 if (notSelfManaged(context)) {
180 return getAudioDevicesFallback();
181 } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
182 return getAudioDevicesUpsideDownCake();
183 } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
184 return getAudioDevicesOreo();
185 } else {
186 throw new AssertionError("Trying to get audio devices on unsupported version");
187 }
188 }
189
190 public AudioDevice getSelectedAudioDevice() {
191 if (notSelfManaged(context)) {
192 return getAudioDeviceFallback();
193 } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
194 return getAudioDeviceUpsideDownCake();
195 } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
196 return getAudioDeviceOreo();
197 } else {
198 throw new AssertionError("Trying to get selected audio device on unsupported version");
199 }
200 }
201
202 public void setAudioDevice(final AudioDevice audioDevice) {
203 if (notSelfManaged(context)) {
204 setAudioDeviceFallback(audioDevice);
205 } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
206 setAudioDeviceUpsideDownCake(audioDevice);
207 } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
208 setAudioDeviceOreo(audioDevice);
209 } else {
210 throw new AssertionError("Trying to set audio devices on unsupported version");
211 }
212 }
213
214 public void setAudioDeviceWhenAvailable(final AudioDevice audioDevice) {
215 final var available = getAudioDevices();
216 if (available.contains(audioDevice) && !available.contains(AudioDevice.BLUETOOTH)) {
217 this.setAudioDevice(audioDevice);
218 } else {
219 Log.d(
220 Config.LOGTAG,
221 "application requested to switch to "
222 + audioDevice
223 + " but we won't because available devices are "
224 + available);
225 }
226 }
227
228 @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
229 private Set<AudioDevice> getAudioDevicesUpsideDownCake() {
230 return ImmutableSet.copyOf(
231 Lists.transform(
232 this.availableEndpoints, CallIntegration::getAudioDeviceUpsideDownCake));
233 }
234
235 @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
236 private AudioDevice getAudioDeviceUpsideDownCake() {
237 return getAudioDeviceUpsideDownCake(getCurrentCallEndpoint());
238 }
239
240 @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
241 private static AudioDevice getAudioDeviceUpsideDownCake(final CallEndpoint callEndpoint) {
242 if (callEndpoint == null) {
243 return AudioDevice.NONE;
244 }
245 final var endpointType = callEndpoint.getEndpointType();
246 return switch (endpointType) {
247 case CallEndpoint.TYPE_BLUETOOTH -> AudioDevice.BLUETOOTH;
248 case CallEndpoint.TYPE_EARPIECE -> AudioDevice.EARPIECE;
249 case CallEndpoint.TYPE_SPEAKER -> AudioDevice.SPEAKER_PHONE;
250 case CallEndpoint.TYPE_WIRED_HEADSET -> AudioDevice.WIRED_HEADSET;
251 case CallEndpoint.TYPE_STREAMING -> AudioDevice.STREAMING;
252 case CallEndpoint.TYPE_UNKNOWN -> AudioDevice.NONE;
253 default -> throw new IllegalStateException("Unknown endpoint type " + endpointType);
254 };
255 }
256
257 @RequiresApi(api = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
258 private void setAudioDeviceUpsideDownCake(final AudioDevice audioDevice) {
259 final var callEndpointOptional =
260 Iterables.tryFind(
261 this.availableEndpoints,
262 e -> getAudioDeviceUpsideDownCake(e) == audioDevice);
263 if (callEndpointOptional.isPresent()) {
264 final var endpoint = callEndpointOptional.get();
265 requestCallEndpointChange(
266 endpoint,
267 MainThreadExecutor.getInstance(),
268 result -> Log.d(Config.LOGTAG, "switched to endpoint " + endpoint));
269 } else {
270 Log.w(Config.LOGTAG, "no endpoint found matching " + audioDevice);
271 }
272 }
273
274 private Set<AudioDevice> getAudioDevicesOreo() {
275 final var audioState = getCallAudioState();
276 if (audioState == null) {
277 Log.d(
278 Config.LOGTAG,
279 "no CallAudioState available. returning empty set for audio devices");
280 return Collections.emptySet();
281 }
282 return getAudioDevicesOreo(audioState);
283 }
284
285 private static Set<AudioDevice> getAudioDevicesOreo(final CallAudioState callAudioState) {
286 final ImmutableSet.Builder<AudioDevice> supportedAudioDevicesBuilder =
287 new ImmutableSet.Builder<>();
288 final var supportedRouteMask = callAudioState.getSupportedRouteMask();
289 if ((supportedRouteMask & CallAudioState.ROUTE_BLUETOOTH)
290 == CallAudioState.ROUTE_BLUETOOTH) {
291 supportedAudioDevicesBuilder.add(AudioDevice.BLUETOOTH);
292 }
293 if ((supportedRouteMask & CallAudioState.ROUTE_EARPIECE) == CallAudioState.ROUTE_EARPIECE) {
294 supportedAudioDevicesBuilder.add(AudioDevice.EARPIECE);
295 }
296 if ((supportedRouteMask & CallAudioState.ROUTE_SPEAKER) == CallAudioState.ROUTE_SPEAKER) {
297 supportedAudioDevicesBuilder.add(AudioDevice.SPEAKER_PHONE);
298 }
299 if ((supportedRouteMask & CallAudioState.ROUTE_WIRED_HEADSET)
300 == CallAudioState.ROUTE_WIRED_HEADSET) {
301 supportedAudioDevicesBuilder.add(AudioDevice.WIRED_HEADSET);
302 }
303 return supportedAudioDevicesBuilder.build();
304 }
305
306 private AudioDevice getAudioDeviceOreo() {
307 final var audioState = getCallAudioState();
308 if (audioState == null) {
309 Log.d(Config.LOGTAG, "no CallAudioState available. returning NONE as audio device");
310 return AudioDevice.NONE;
311 }
312 return getAudioDeviceOreo(audioState);
313 }
314
315 private static AudioDevice getAudioDeviceOreo(final CallAudioState audioState) {
316 // technically we get a mask here; maybe we should query the mask instead
317 return switch (audioState.getRoute()) {
318 case CallAudioState.ROUTE_BLUETOOTH -> AudioDevice.BLUETOOTH;
319 case CallAudioState.ROUTE_EARPIECE -> AudioDevice.EARPIECE;
320 case CallAudioState.ROUTE_SPEAKER -> AudioDevice.SPEAKER_PHONE;
321 case CallAudioState.ROUTE_WIRED_HEADSET -> AudioDevice.WIRED_HEADSET;
322 default -> AudioDevice.NONE;
323 };
324 }
325
326 @RequiresApi(api = Build.VERSION_CODES.O)
327 private void setAudioDeviceOreo(final AudioDevice audioDevice) {
328 switch (audioDevice) {
329 case EARPIECE -> setAudioRoute(CallAudioState.ROUTE_EARPIECE);
330 case BLUETOOTH -> setAudioRoute(CallAudioState.ROUTE_BLUETOOTH);
331 case WIRED_HEADSET -> setAudioRoute(CallAudioState.ROUTE_WIRED_HEADSET);
332 case SPEAKER_PHONE -> setAudioRoute(CallAudioState.ROUTE_SPEAKER);
333 }
334 }
335
336 private Set<AudioDevice> getAudioDevicesFallback() {
337 return requireAppRtcAudioManager().getAudioDevices();
338 }
339
340 private AudioDevice getAudioDeviceFallback() {
341 final var audioDevice = requireAppRtcAudioManager().getSelectedAudioDevice();
342 return audioDevice == null ? AudioDevice.NONE : audioDevice;
343 }
344
345 private void setAudioDeviceFallback(final AudioDevice audioDevice) {
346 final var audioManager = requireAppRtcAudioManager();
347 audioManager.executeOnMain(() -> audioManager.setDefaultAudioDevice(audioDevice));
348 }
349
350 @NonNull
351 private AppRTCAudioManager requireAppRtcAudioManager() {
352 if (this.appRTCAudioManager == null) {
353 throw new IllegalStateException(
354 "You are trying to access the fallback audio manager on a modern device");
355 }
356 return this.appRTCAudioManager;
357 }
358
359 @Override
360 public void onSilence() {
361 this.callback.onCallIntegrationSilence();
362 }
363
364 @Override
365 public void onStateChanged(final int state) {
366 Log.d(Config.LOGTAG, "onStateChanged(" + state + ")");
367 if (notSelfManaged(context)) {
368 if (state == STATE_DIALING) {
369 requireAppRtcAudioManager().startRingBack();
370 } else {
371 requireAppRtcAudioManager().stopRingBack();
372 }
373 }
374 if (state == STATE_ACTIVE) {
375 startTone(DEFAULT_TONE_VOLUME, ToneGenerator.TONE_CDMA_ANSWER, 100);
376 } else if (state == STATE_DISCONNECTED) {
377 final var audioManager = this.appRTCAudioManager;
378 if (audioManager != null) {
379 audioManager.executeOnMain(audioManager::stop);
380 }
381 }
382 }
383
384 public void success() {
385 Log.d(Config.LOGTAG, "CallIntegration.success()");
386 startTone(DEFAULT_TONE_VOLUME, ToneGenerator.TONE_CDMA_CONFIRM, 600);
387 this.destroyWithDelay(new DisconnectCause(DisconnectCause.LOCAL, null), 600);
388 }
389
390 public void accepted() {
391 Log.d(Config.LOGTAG, "CallIntegration.accepted()");
392 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
393 this.destroyWith(new DisconnectCause(DisconnectCause.ANSWERED_ELSEWHERE, null));
394 } else {
395 this.destroyWith(new DisconnectCause(DisconnectCause.CANCELED, null));
396 }
397 }
398
399 public void error() {
400 Log.d(Config.LOGTAG, "CallIntegration.error()");
401 startTone(DEFAULT_TONE_VOLUME, ToneGenerator.TONE_CDMA_CONFIRM, 600);
402 this.destroyWithDelay(new DisconnectCause(DisconnectCause.ERROR, null), 600);
403 }
404
405 public void retracted() {
406 Log.d(Config.LOGTAG, "CallIntegration.retracted()");
407 // an alternative cause would be LOCAL
408 this.destroyWith(new DisconnectCause(DisconnectCause.CANCELED, null));
409 }
410
411 public void rejected() {
412 Log.d(Config.LOGTAG, "CallIntegration.rejected()");
413 this.destroyWith(new DisconnectCause(DisconnectCause.REJECTED, null));
414 }
415
416 public void busy() {
417 Log.d(Config.LOGTAG, "CallIntegration.busy()");
418 startTone(80, ToneGenerator.TONE_CDMA_NETWORK_BUSY, 2500);
419 this.destroyWithDelay(new DisconnectCause(DisconnectCause.BUSY, null), 2500);
420 }
421
422 private void destroyWithDelay(final DisconnectCause disconnectCause, final int delay) {
423 if (this.delayedDestructionInitiated.compareAndSet(false, true)) {
424 JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(
425 () -> {
426 this.setDisconnected(disconnectCause);
427 this.destroyCallIntegration();
428 },
429 delay,
430 TimeUnit.MILLISECONDS);
431 } else {
432 Log.w(Config.LOGTAG, "CallIntegration destruction has already been scheduled!");
433 }
434 }
435
436 private void destroyWith(final DisconnectCause disconnectCause) {
437 if (this.getState() == STATE_DISCONNECTED || this.delayedDestructionInitiated.get()) {
438 Log.d(Config.LOGTAG, "CallIntegration has already been destroyed");
439 return;
440 }
441 this.setDisconnected(disconnectCause);
442 this.destroyCallIntegration();
443 Log.d(Config.LOGTAG, "destroyed!");
444 }
445
446 private void startTone(final int volume, final int toneType, final int durationMs) {
447 final ToneGenerator toneGenerator;
448 try {
449 toneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, volume);
450 } catch (final RuntimeException e) {
451 Log.e(Config.LOGTAG, "could not initialize tone generator", e);
452 return;
453 }
454 toneGenerator.startTone(toneType, durationMs);
455 }
456
457 public static Uri address(final Jid contact) {
458 return Uri.parse(String.format("xmpp:%s", contact.toString()));
459 }
460
461 public void verifyDisconnected() {
462 if (this.getState() == STATE_DISCONNECTED || this.delayedDestructionInitiated.get()) {
463 return;
464 }
465 throw new AssertionError("CallIntegration has not been disconnected");
466 }
467
468 private void onAudioDeviceChanged(
469 final CallIntegration.AudioDevice selectedAudioDevice,
470 final Set<CallIntegration.AudioDevice> availableAudioDevices) {
471 if (isAudioRoutingRequested) {
472 configureInitialAudioDevice(availableAudioDevices);
473 }
474 final var callback = this.callback;
475 if (callback == null) {
476 return;
477 }
478 callback.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
479 }
480
481 private void configureInitialAudioDevice(final Set<AudioDevice> availableAudioDevices) {
482 final var initialAudioDevice = this.initialAudioDevice;
483 if (initialAudioDevice == null) {
484 Log.d(Config.LOGTAG, "skipping configureInitialAudioDevice()");
485 return;
486 }
487 final var target = this.initialAudioDevice;
488 if (this.initialAudioDeviceConfigured.compareAndSet(false, true)) {
489 if (availableAudioDevices.contains(target)
490 && !availableAudioDevices.contains(AudioDevice.BLUETOOTH)) {
491 setAudioDevice(target);
492 Log.d(Config.LOGTAG, "configured initial audio device: " + target);
493 } else {
494 Log.d(
495 Config.LOGTAG,
496 "not setting initial audio device. available devices: "
497 + availableAudioDevices);
498 }
499 }
500 }
501
502 private boolean selfManaged() {
503 return selfManaged(context);
504 }
505
506 public static boolean selfManagedAvailable(final Context context) {
507 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
508 && Build.VERSION.SDK_INT < 35
509 && hasSystemFeature(context)
510 && isDeviceModelSupported();
511 }
512
513 public static boolean selfManaged(final Context context) {
514 return selfManagedAvailable(context) && new AppSettings(context).isCallIntegration();
515 }
516
517 public static boolean hasSystemFeature(final Context context) {
518 final var packageManager = context.getPackageManager();
519 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
520 return packageManager.hasSystemFeature(PackageManager.FEATURE_TELECOM);
521 } else {
522 //noinspection deprecation
523 return packageManager.hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE);
524 }
525 }
526
527 private static boolean isDeviceModelSupported() {
528 final var manufacturer = Strings.nullToEmpty(Build.MANUFACTURER).toLowerCase(Locale.ROOT);
529 if (BROKEN_DEVICE_MODELS.contains(Build.DEVICE)) {
530 return false;
531 }
532 if (BROKEN_MANUFACTURES_UP_TO_11.contains(manufacturer)
533 && Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
534 return false;
535 }
536 // we only know of one Umidigi device (BISON_GT2_5G) that doesn't work (audio is not being
537 // routed properly) However with those devices being extremely rare it's impossible to gauge
538 // how many might be effected and no Naomi Wu around to clarify with the company directly
539 if ("umidigi".equals(manufacturer) && Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
540 return false;
541 }
542 // SailfishOS's AppSupport do not support Call Integration
543 return !Build.MODEL.endsWith("(AppSupport)");
544 }
545
546 public static boolean notSelfManaged(final Context context) {
547 return !selfManaged(context);
548 }
549
550 public void setInitialAudioDevice(final AudioDevice audioDevice) {
551 Log.d(Config.LOGTAG, "setInitialAudioDevice(" + audioDevice + ")");
552 this.initialAudioDevice = audioDevice;
553 }
554
555 public void startAudioRouting() {
556 this.isAudioRoutingRequested = true;
557 if (selfManaged()) {
558 final var devices = getAudioDevices();
559 if (devices.isEmpty()) {
560 return;
561 }
562 configureInitialAudioDevice(devices);
563 return;
564 }
565 final var audioManager = requireAppRtcAudioManager();
566 audioManager.executeOnMain(
567 () -> {
568 audioManager.start();
569 this.onAudioDeviceChanged(
570 audioManager.getSelectedAudioDevice(), audioManager.getAudioDevices());
571 });
572 }
573
574 private void destroyCallIntegration() {
575 super.destroy();
576 this.isDestroyed.set(true);
577 }
578
579 public boolean isDestroyed() {
580 return this.isDestroyed.get();
581 }
582
583 public enum AudioDevice {
584 NONE,
585 SPEAKER_PHONE,
586 WIRED_HEADSET,
587 EARPIECE,
588 BLUETOOTH,
589 STREAMING
590 }
591
592 public static AudioDevice initialAudioDevice(final Set<Media> media) {
593 if (Media.audioOnly(media)) {
594 return AudioDevice.EARPIECE;
595 } else {
596 return AudioDevice.SPEAKER_PHONE;
597 }
598 }
599
600 public interface Callback {
601 void onCallIntegrationShowIncomingCallUi();
602
603 void onCallIntegrationDisconnect();
604
605 void onAudioDeviceChanged(
606 CallIntegration.AudioDevice selectedAudioDevice,
607 Set<CallIntegration.AudioDevice> availableAudioDevices);
608
609 void onCallIntegrationReject();
610
611 void onCallIntegrationAnswer();
612
613 void onCallIntegrationSilence();
614
615 void onCallIntegrationMicrophoneEnabled(boolean enabled);
616 }
617}