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