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