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