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