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}