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