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