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 final var toneGenerator =
396 new ToneGenerator(AudioManager.STREAM_VOICE_CALL, DEFAULT_TONE_VOLUME);
397 toneGenerator.startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375);
398 this.destroyWithDelay(new DisconnectCause(DisconnectCause.LOCAL, null), 375);
399 }
400
401 public void accepted() {
402 Log.d(Config.LOGTAG, "CallIntegration.accepted()");
403 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N_MR1) {
404 this.destroyWith(new DisconnectCause(DisconnectCause.ANSWERED_ELSEWHERE, null));
405 } else {
406 this.destroyWith(new DisconnectCause(DisconnectCause.CANCELED, null));
407 }
408 }
409
410 public void error() {
411 Log.d(Config.LOGTAG, "CallIntegration.error()");
412 final var toneGenerator =
413 new ToneGenerator(AudioManager.STREAM_VOICE_CALL, DEFAULT_TONE_VOLUME);
414 toneGenerator.startTone(ToneGenerator.TONE_CDMA_CALLDROP_LITE, 375);
415 this.destroyWithDelay(new DisconnectCause(DisconnectCause.ERROR, null), 375);
416 }
417
418 public void retracted() {
419 Log.d(Config.LOGTAG, "CallIntegration.retracted()");
420 // an alternative cause would be LOCAL
421 this.destroyWith(new DisconnectCause(DisconnectCause.CANCELED, null));
422 }
423
424 public void rejected() {
425 Log.d(Config.LOGTAG, "CallIntegration.rejected()");
426 this.destroyWith(new DisconnectCause(DisconnectCause.REJECTED, null));
427 }
428
429 public void busy() {
430 Log.d(Config.LOGTAG, "CallIntegration.busy()");
431 final var toneGenerator = new ToneGenerator(AudioManager.STREAM_VOICE_CALL, 80);
432 toneGenerator.startTone(ToneGenerator.TONE_CDMA_NETWORK_BUSY, 2500);
433 this.destroyWithDelay(new DisconnectCause(DisconnectCause.BUSY, null), 2500);
434 }
435
436 private void destroyWithDelay(final DisconnectCause disconnectCause, final int delay) {
437 if (this.delayedDestructionInitiated.compareAndSet(false, true)) {
438 JingleConnectionManager.SCHEDULED_EXECUTOR_SERVICE.schedule(
439 () -> {
440 this.setDisconnected(disconnectCause);
441 this.destroyCallIntegration();
442 },
443 delay,
444 TimeUnit.MILLISECONDS);
445 } else {
446 Log.w(Config.LOGTAG, "CallIntegration destruction has already been scheduled!");
447 }
448 }
449
450 private void destroyWith(final DisconnectCause disconnectCause) {
451 if (this.getState() == STATE_DISCONNECTED || this.delayedDestructionInitiated.get()) {
452 Log.d(Config.LOGTAG, "CallIntegration has already been destroyed");
453 return;
454 }
455 this.setDisconnected(disconnectCause);
456 this.destroyCallIntegration();
457 Log.d(Config.LOGTAG, "destroyed!");
458 }
459
460 public static Uri address(final Jid contact) {
461 return Uri.parse(String.format("xmpp:%s", contact.toEscapedString()));
462 }
463
464 public void verifyDisconnected() {
465 if (this.getState() == STATE_DISCONNECTED || this.delayedDestructionInitiated.get()) {
466 return;
467 }
468 throw new AssertionError("CallIntegration has not been disconnected");
469 }
470
471 private void onAudioDeviceChanged(
472 final CallIntegration.AudioDevice selectedAudioDevice,
473 final Set<CallIntegration.AudioDevice> availableAudioDevices) {
474 if (isAudioRoutingRequested) {
475 configureInitialAudioDevice(availableAudioDevices);
476 }
477 final var callback = this.callback;
478 if (callback == null) {
479 return;
480 }
481 callback.onAudioDeviceChanged(selectedAudioDevice, availableAudioDevices);
482 }
483
484 private void configureInitialAudioDevice(final Set<AudioDevice> availableAudioDevices) {
485 final var initialAudioDevice = this.initialAudioDevice;
486 if (initialAudioDevice == null) {
487 Log.d(Config.LOGTAG, "skipping configureInitialAudioDevice()");
488 return;
489 }
490 final var target = this.initialAudioDevice;
491 if (this.initialAudioDeviceConfigured.compareAndSet(false, true)) {
492 if (availableAudioDevices.contains(target)
493 && !availableAudioDevices.contains(AudioDevice.BLUETOOTH)) {
494 setAudioDevice(target);
495 Log.d(Config.LOGTAG, "configured initial audio device: " + target);
496 } else {
497 Log.d(
498 Config.LOGTAG,
499 "not setting initial audio device. available devices: "
500 + availableAudioDevices);
501 }
502 }
503 }
504
505 private boolean selfManaged() {
506 return selfManaged(context);
507 }
508
509 public static boolean selfManaged(final Context context) {
510 return Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
511 && hasSystemFeature(context)
512 && isDeviceModelSupported();
513 }
514
515 public static boolean hasSystemFeature(final Context context) {
516 final var packageManager = context.getPackageManager();
517 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
518 return packageManager.hasSystemFeature(PackageManager.FEATURE_TELECOM);
519 } else {
520 //noinspection deprecation
521 return packageManager.hasSystemFeature(PackageManager.FEATURE_CONNECTION_SERVICE);
522 }
523 }
524
525 private static boolean isDeviceModelSupported() {
526 if (BROKEN_DEVICE_MODELS.contains(Build.DEVICE)) {
527 return false;
528 }
529 // all Realme devices at least up to and including Android 11 are broken
530 if ("realme".equalsIgnoreCase(Build.MANUFACTURER)
531 && Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) {
532 return false;
533 }
534 // we only know of one Umidigi device (BISON_GT2_5G) that doesn't work (audio is not being
535 // routed properly) However with those devices being extremely rare it's impossible to gauge
536 // how many might be effected and no Naomi Wu around to clarify with the company directly
537 if ("umidigi".equalsIgnoreCase(Build.MANUFACTURER)
538 && Build.VERSION.SDK_INT <= Build.VERSION_CODES.S) {
539 return false;
540 }
541 return true;
542 }
543
544 public static boolean notSelfManaged(final Context context) {
545 return !selfManaged(context);
546 }
547
548 public void setInitialAudioDevice(final AudioDevice audioDevice) {
549 Log.d(Config.LOGTAG, "setInitialAudioDevice(" + audioDevice + ")");
550 this.initialAudioDevice = audioDevice;
551 }
552
553 public void startAudioRouting() {
554 this.isAudioRoutingRequested = true;
555 if (selfManaged()) {
556 final var devices = getAudioDevices();
557 if (devices.isEmpty()) {
558 return;
559 }
560 configureInitialAudioDevice(devices);
561 return;
562 }
563 final var audioManager = requireAppRtcAudioManager();
564 audioManager.executeOnMain(
565 () -> {
566 audioManager.start();
567 this.onAudioDeviceChanged(
568 audioManager.getSelectedAudioDevice(), audioManager.getAudioDevices());
569 });
570 }
571
572 private void destroyCallIntegration() {
573 super.destroy();
574 this.isDestroyed.set(true);
575 }
576
577 public boolean isDestroyed() {
578 return this.isDestroyed.get();
579 }
580
581 public enum AudioDevice {
582 NONE,
583 SPEAKER_PHONE,
584 WIRED_HEADSET,
585 EARPIECE,
586 BLUETOOTH,
587 STREAMING
588 }
589
590 public static AudioDevice initialAudioDevice(final Set<Media> media) {
591 if (Media.audioOnly(media)) {
592 return AudioDevice.EARPIECE;
593 } else {
594 return AudioDevice.SPEAKER_PHONE;
595 }
596 }
597
598 public interface Callback {
599 void onCallIntegrationShowIncomingCallUi();
600
601 void onCallIntegrationDisconnect();
602
603 void onAudioDeviceChanged(
604 CallIntegration.AudioDevice selectedAudioDevice,
605 Set<CallIntegration.AudioDevice> availableAudioDevices);
606
607 void onCallIntegrationReject();
608
609 void onCallIntegrationAnswer();
610
611 void onCallIntegrationSilence();
612
613 void onCallIntegrationMicrophoneEnabled(boolean enabled);
614 }
615}