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