CallIntegration.java

  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}