1package eu.siacs.conversations.ui;
2
3import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
4
5import static java.util.Arrays.asList;
6
7import android.Manifest;
8import android.annotation.SuppressLint;
9import android.app.PictureInPictureParams;
10import android.content.ActivityNotFoundException;
11import android.content.Context;
12import android.content.Intent;
13import android.content.pm.ActivityInfo;
14import android.content.pm.PackageManager;
15import android.opengl.GLException;
16import android.os.Build;
17import android.os.Bundle;
18import android.os.Handler;
19import android.os.PowerManager;
20import android.util.Log;
21import android.util.Rational;
22import android.view.KeyEvent;
23import android.view.Menu;
24import android.view.MenuItem;
25import android.view.View;
26import android.view.WindowManager;
27import android.widget.Toast;
28
29import androidx.annotation.NonNull;
30import androidx.annotation.Nullable;
31import androidx.annotation.RequiresApi;
32import androidx.annotation.StringRes;
33import androidx.databinding.DataBindingUtil;
34
35import com.google.android.material.floatingactionbutton.FloatingActionButton;
36import com.google.common.base.Optional;
37import com.google.common.base.Preconditions;
38import com.google.common.base.Strings;
39import com.google.common.base.Throwables;
40import com.google.common.collect.ImmutableList;
41import com.google.common.collect.ImmutableSet;
42import com.google.common.util.concurrent.FutureCallback;
43import com.google.common.util.concurrent.Futures;
44
45import org.jetbrains.annotations.NotNull;
46import org.webrtc.RendererCommon;
47import org.webrtc.SurfaceViewRenderer;
48import org.webrtc.VideoTrack;
49
50import java.lang.ref.WeakReference;
51import java.util.Arrays;
52import java.util.Collections;
53import java.util.List;
54import java.util.Set;
55
56import eu.siacs.conversations.Config;
57import eu.siacs.conversations.R;
58import eu.siacs.conversations.databinding.ActivityRtpSessionBinding;
59import eu.siacs.conversations.entities.Account;
60import eu.siacs.conversations.entities.Contact;
61import eu.siacs.conversations.entities.Conversation;
62import eu.siacs.conversations.services.CallIntegration;
63import eu.siacs.conversations.services.CallIntegrationConnectionService;
64import eu.siacs.conversations.services.XmppConnectionService;
65import eu.siacs.conversations.ui.widget.DialpadView;
66import eu.siacs.conversations.ui.util.AvatarWorkerTask;
67import eu.siacs.conversations.ui.util.MainThreadExecutor;
68import eu.siacs.conversations.ui.util.Rationals;
69import eu.siacs.conversations.utils.PermissionUtils;
70import eu.siacs.conversations.utils.TimeFrameUtils;
71import eu.siacs.conversations.xmpp.Jid;
72import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
73import eu.siacs.conversations.xmpp.jingle.ContentAddition;
74import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
75import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
76import eu.siacs.conversations.xmpp.jingle.Media;
77import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession;
78import eu.siacs.conversations.xmpp.jingle.RtpCapability;
79import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
80
81import org.webrtc.RendererCommon;
82import org.webrtc.SurfaceViewRenderer;
83import org.webrtc.VideoTrack;
84
85import java.lang.ref.WeakReference;
86import java.util.Arrays;
87import java.util.Collections;
88import java.util.List;
89import java.util.Set;
90
91public class RtpSessionActivity extends XmppActivity
92 implements XmppConnectionService.OnJingleRtpConnectionUpdate,
93 eu.siacs.conversations.ui.widget.SurfaceViewRenderer.OnAspectRatioChanged {
94
95 public static final String EXTRA_WITH = "with";
96 public static final String EXTRA_SESSION_ID = "session_id";
97 public static final String EXTRA_PROPOSED_SESSION_ID = "proposed_session_id";
98 public static final String EXTRA_LAST_REPORTED_STATE = "last_reported_state";
99 public static final String EXTRA_LAST_ACTION = "last_action";
100 public static final String ACTION_ACCEPT_CALL = "action_accept_call";
101 public static final String ACTION_MAKE_VOICE_CALL = "action_make_voice_call";
102 public static final String ACTION_MAKE_VIDEO_CALL = "action_make_video_call";
103
104 private static final int CALL_DURATION_UPDATE_INTERVAL = 250;
105 private static final int BUTTON_VISIBILITY_TIMEOUT = 10_000;
106
107 public static final List<RtpEndUserState> END_CARD =
108 Arrays.asList(
109 RtpEndUserState.APPLICATION_ERROR,
110 RtpEndUserState.SECURITY_ERROR,
111 RtpEndUserState.DECLINED_OR_BUSY,
112 RtpEndUserState.CONTACT_OFFLINE,
113 RtpEndUserState.CONNECTIVITY_ERROR,
114 RtpEndUserState.CONNECTIVITY_LOST_ERROR,
115 RtpEndUserState.RETRACTED);
116 private static final List<RtpEndUserState> STATES_SHOWING_HELP_BUTTON =
117 Arrays.asList(
118 RtpEndUserState.APPLICATION_ERROR,
119 RtpEndUserState.CONNECTIVITY_ERROR,
120 RtpEndUserState.SECURITY_ERROR);
121 private static final List<RtpEndUserState> STATES_SHOWING_SWITCH_TO_CHAT =
122 Arrays.asList(
123 RtpEndUserState.CONNECTING,
124 RtpEndUserState.CONNECTED,
125 RtpEndUserState.RECONNECTING,
126 RtpEndUserState.INCOMING_CONTENT_ADD);
127 private static final List<RtpEndUserState> STATES_CONSIDERED_CONNECTED =
128 Arrays.asList(RtpEndUserState.CONNECTED, RtpEndUserState.RECONNECTING);
129 private static final List<RtpEndUserState> STATES_SHOWING_PIP_PLACEHOLDER =
130 Arrays.asList(
131 RtpEndUserState.ACCEPTING_CALL,
132 RtpEndUserState.CONNECTING,
133 RtpEndUserState.RECONNECTING);
134 private static final List<RtpEndUserState> STATES_SHOWING_SPEAKER_CONFIGURATION =
135 new ImmutableList.Builder<RtpEndUserState>()
136 .add(RtpEndUserState.FINDING_DEVICE)
137 .add(RtpEndUserState.RINGING)
138 .add(RtpEndUserState.ACCEPTING_CALL)
139 .add(RtpEndUserState.CONNECTING)
140 .addAll(STATES_CONSIDERED_CONNECTED)
141 .build();
142 private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session";
143 private static final int REQUEST_ACCEPT_CALL = 0x1111;
144 private static final int REQUEST_ACCEPT_CONTENT = 0x1112;
145 private static final int REQUEST_ADD_CONTENT = 0x1113;
146 private WeakReference<JingleRtpConnection> rtpConnectionReference;
147
148 private ActivityRtpSessionBinding binding;
149 private PowerManager.WakeLock mProximityWakeLock;
150
151 private final Handler mHandler = new Handler();
152 private final Runnable mTickExecutor =
153 new Runnable() {
154 @Override
155 public void run() {
156 updateCallDuration();
157 mHandler.postDelayed(mTickExecutor, CALL_DURATION_UPDATE_INTERVAL);
158 }
159 };
160 private boolean buttonsHiddenAfterTimeout = false;
161 private final Runnable mVisibilityToggleExecutor = this::updateButtonInVideoCallVisibility;
162
163 public static Set<Media> actionToMedia(final String action) {
164 if (ACTION_MAKE_VIDEO_CALL.equals(action)) {
165 return ImmutableSet.of(Media.AUDIO, Media.VIDEO);
166 } else if (ACTION_MAKE_VOICE_CALL.equals(action)) {
167 return ImmutableSet.of(Media.AUDIO);
168 } else {
169 Log.w(
170 Config.LOGTAG,
171 "actionToMedia can not get media set from unknown action " + action);
172 return Collections.emptySet();
173 }
174 }
175
176 private static void addSink(
177 final VideoTrack videoTrack, final SurfaceViewRenderer surfaceViewRenderer) {
178 try {
179 videoTrack.addSink(surfaceViewRenderer);
180 } catch (final IllegalStateException e) {
181 Log.e(
182 Config.LOGTAG,
183 "possible race condition on trying to display video track. ignoring",
184 e);
185 }
186 }
187
188 @Override
189 public void onCreate(Bundle savedInstanceState) {
190 super.onCreate(savedInstanceState);
191 getWindow()
192 .addFlags(
193 WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
194 | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
195 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
196 | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
197 this.binding = DataBindingUtil.setContentView(this, R.layout.activity_rtp_session);
198 this.binding.remoteVideo.setOnClickListener(this::onVideoScreenClick);
199 this.binding.localVideo.setOnClickListener(this::onVideoScreenClick);
200 setSupportActionBar(binding.toolbar);
201
202 binding.dialpad.setClickConsumer(tag -> {
203 final JingleRtpConnection connection =
204 this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
205 if (connection != null) connection.applyDtmfTone(tag);
206 });
207
208 if (savedInstanceState != null) {
209 boolean dialpadVisible = savedInstanceState.getBoolean("dialpad_visible");
210 binding.dialpad.setVisibility(dialpadVisible ? View.VISIBLE : View.GONE);
211 }
212
213 Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
214 }
215
216 private void onVideoScreenClick(final View view) {
217 resetVisibilityExecutorShowButtons();
218 }
219
220 @Override
221 public boolean onCreateOptionsMenu(final Menu menu) {
222 getMenuInflater().inflate(R.menu.activity_rtp_session, menu);
223 final MenuItem help = menu.findItem(R.id.action_help);
224 final MenuItem gotoChat = menu.findItem(R.id.action_goto_chat);
225 final MenuItem dialpad = menu.findItem(R.id.action_dialpad);
226 final MenuItem switchToVideo = menu.findItem(R.id.action_switch_to_video);
227 help.setVisible(Config.HELP != null && isHelpButtonVisible());
228 gotoChat.setVisible(isSwitchToConversationVisible());
229 switchToVideo.setVisible(isSwitchToVideoVisible());
230 dialpad.setVisible(isAudioOnlyConversation());
231 return super.onCreateOptionsMenu(menu);
232 }
233
234 @Override
235 public boolean onKeyDown(final int keyCode, final KeyEvent event) {
236 if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
237 if (xmppConnectionService != null) {
238 if (xmppConnectionService.getNotificationService().stopSoundAndVibration()) {
239 return true;
240 }
241 }
242 }
243 return super.onKeyDown(keyCode, event);
244 }
245
246 private boolean isHelpButtonVisible() {
247 try {
248 return STATES_SHOWING_HELP_BUTTON.contains(requireRtpConnection().getEndUserState());
249 } catch (IllegalStateException e) {
250 final Intent intent = getIntent();
251 final String state =
252 intent != null ? intent.getStringExtra(EXTRA_LAST_REPORTED_STATE) : null;
253 if (state != null) {
254 return STATES_SHOWING_HELP_BUTTON.contains(RtpEndUserState.valueOf(state));
255 } else {
256 return false;
257 }
258 }
259 }
260
261 private boolean isSwitchToConversationVisible() {
262 final JingleRtpConnection connection =
263 this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
264 return connection != null
265 && STATES_SHOWING_SWITCH_TO_CHAT.contains(connection.getEndUserState());
266 }
267
268 private boolean isAudioOnlyConversation() {
269 final JingleRtpConnection connection =
270 this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
271
272 return connection != null && !connection.getMedia().contains(Media.VIDEO);
273 }
274
275 private boolean isSwitchToVideoVisible() {
276 final JingleRtpConnection connection =
277 this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
278 if (connection == null) {
279 return false;
280 }
281 return connection.isSwitchToVideoAvailable();
282 }
283
284 private void switchToConversation() {
285 final Contact contact = getWith();
286 final Conversation conversation =
287 xmppConnectionService.findOrCreateConversation(
288 contact.getAccount(), contact.getJid(), false, true);
289 switchToConversation(conversation);
290 }
291
292 private void toggleDialpadVisibility() {
293 if (binding.dialpad.getVisibility() == View.VISIBLE) {
294 binding.dialpad.setVisibility(View.GONE);
295 }
296 else {
297 binding.dialpad.setVisibility(View.VISIBLE);
298 }
299 }
300
301 public boolean onOptionsItemSelected(final MenuItem item) {
302 final var itemItem = item.getItemId();
303 if (itemItem == R.id.action_help) {
304 launchHelpInBrowser();
305 return true;
306 } else if (itemItem == R.id.action_goto_chat) {
307 switchToConversation();
308 return true;
309 } else if (itemItem == R.id.action_dialpad) {
310 toggleDialpadVisibility();
311 return true;
312 } else if (itemItem == R.id.action_switch_to_video) {
313 requestPermissionAndSwitchToVideo();
314 return true;
315 } else {
316 return super.onOptionsItemSelected(item);
317 }
318 }
319
320 private void launchHelpInBrowser() {
321 final Intent intent = new Intent(Intent.ACTION_VIEW, Config.HELP);
322 try {
323 startActivity(intent);
324 } catch (final ActivityNotFoundException e) {
325 Toast.makeText(this, R.string.no_application_found_to_open_link, Toast.LENGTH_LONG)
326 .show();
327 }
328 }
329
330 private void endCall(View view) {
331 endCall();
332 }
333
334 private void endCall() {
335 if (this.rtpConnectionReference == null) {
336 retractSessionProposal();
337 finish();
338 } else {
339 try {
340 requireRtpConnection().endCall();
341 } catch (final IllegalStateException e) {
342 // No call, already done
343 finish();
344 }
345 }
346 }
347
348 private void retractSessionProposal() {
349 final Intent intent = getIntent();
350 final String action = intent.getAction();
351 final String lastAction = intent.getStringExtra(EXTRA_LAST_ACTION);
352 final Account account = extractAccount(intent);
353 final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
354 final String state = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE);
355 if (!Intent.ACTION_VIEW.equals(action)
356 || state == null
357 || !END_CARD.contains(RtpEndUserState.valueOf(state))) {
358 final Set<Media> media = actionToMedia(lastAction == null ? action : lastAction);
359 resetIntent(account, with, RtpEndUserState.RETRACTED, media);
360 }
361 xmppConnectionService
362 .getJingleConnectionManager()
363 .retractSessionProposal(account, with.asBareJid());
364 }
365
366 private void rejectCall(View view) {
367 requireRtpConnection().rejectCall();
368 finish();
369 }
370
371 private void acceptCall(View view) {
372 requestPermissionsAndAcceptCall();
373 }
374
375 private void acceptContentAdd() {
376 try {
377 final ContentAddition pendingContentAddition =
378 requireRtpConnection().getPendingContentAddition();
379 if (pendingContentAddition == null) {
380 Log.d(Config.LOGTAG, "content offer was gone after granting permission");
381 return;
382 }
383 requireRtpConnection().acceptContentAdd(pendingContentAddition.summary);
384 } catch (final IllegalStateException e) {
385 Toast.makeText(this, Strings.nullToEmpty(e.getMessage()), Toast.LENGTH_SHORT).show();
386 }
387 }
388
389 private void requestPermissionAndSwitchToVideo() {
390 final List<String> permissions = permissions(ImmutableSet.of(Media.VIDEO, Media.AUDIO));
391 if (PermissionUtils.hasPermission(this, permissions, REQUEST_ADD_CONTENT)) {
392 switchToVideo();
393 }
394 }
395
396 private void switchToVideo() {
397 try {
398 requireRtpConnection().addMedia(Media.VIDEO);
399 } catch (final IllegalStateException e) {
400 Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
401 }
402 }
403
404 private void acceptContentAdd(final ContentAddition contentAddition) {
405 if (contentAddition == null
406 || contentAddition.direction != ContentAddition.Direction.INCOMING) {
407 Log.d(Config.LOGTAG, "ignore press on content-accept button");
408 return;
409 }
410 requestPermissionAndAcceptContentAdd(contentAddition);
411 }
412
413 private void requestPermissionAndAcceptContentAdd(final ContentAddition contentAddition) {
414 final List<String> permissions = permissions(contentAddition.media());
415 if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CONTENT)) {
416 try {
417 requireRtpConnection().acceptContentAdd(contentAddition.summary);
418 } catch (final IllegalStateException e) {
419 Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
420 }
421 }
422 }
423
424 private void rejectContentAdd(final View view) {
425 requireRtpConnection().rejectContentAdd();
426 }
427
428 private void requestPermissionsAndAcceptCall() {
429 final List<String> permissions = permissions(getMedia());
430 if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CALL)) {
431 putScreenInCallMode();
432 acceptCall();
433 }
434 }
435
436 private List<String> permissions(final Set<Media> media) {
437 final ImmutableList.Builder<String> permissions = ImmutableList.builder();
438 if (media.contains(Media.VIDEO)) {
439 permissions.add(Manifest.permission.CAMERA).add(Manifest.permission.RECORD_AUDIO);
440 } else {
441 permissions.add(Manifest.permission.RECORD_AUDIO);
442 }
443 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
444 permissions.add(Manifest.permission.BLUETOOTH_CONNECT);
445 }
446 return permissions.build();
447 }
448
449 private void acceptCall() {
450 try {
451 requireRtpConnection().acceptCall();
452 } catch (final IllegalStateException e) {
453 Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
454 }
455 }
456
457 private void putScreenInCallMode() {
458 putScreenInCallMode(requireRtpConnection().getMedia());
459 }
460
461 private void putScreenInCallMode(final Set<Media> media) {
462 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
463 if (Media.audioOnly(media)) {
464 final JingleRtpConnection rtpConnection =
465 rtpConnectionReference != null ? rtpConnectionReference.get() : null;
466 final CallIntegration callIntegration =
467 rtpConnection == null ? null : rtpConnection.getCallIntegration();
468 if (callIntegration == null
469 || callIntegration.getSelectedAudioDevice()
470 == CallIntegration.AudioDevice.EARPIECE) {
471 acquireProximityWakeLock();
472 }
473 }
474 lockOrientation(media);
475 }
476
477 private void lockOrientation(final Set<Media> media) {
478 if (Media.audioOnly(media)) {
479 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
480 } else {
481 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
482 }
483 }
484
485 @SuppressLint("WakelockTimeout")
486 private void acquireProximityWakeLock() {
487 final PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
488 if (powerManager == null) {
489 Log.e(Config.LOGTAG, "power manager not available");
490 return;
491 }
492 if (isFinishing()) {
493 Log.e(Config.LOGTAG, "do not acquire wakelock. activity is finishing");
494 return;
495 }
496 if (this.mProximityWakeLock == null) {
497 this.mProximityWakeLock =
498 powerManager.newWakeLock(
499 PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, PROXIMITY_WAKE_LOCK_TAG);
500 }
501 if (!this.mProximityWakeLock.isHeld()) {
502 Log.d(Config.LOGTAG, "acquiring proximity wake lock");
503 this.mProximityWakeLock.acquire();
504 }
505 }
506
507 private void releaseProximityWakeLock() {
508 if (this.mProximityWakeLock != null && mProximityWakeLock.isHeld()) {
509 Log.d(Config.LOGTAG, "releasing proximity wake lock");
510 this.mProximityWakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
511 this.mProximityWakeLock = null;
512 }
513 }
514
515 private void putProximityWakeLockInProperState(final CallIntegration.AudioDevice audioDevice) {
516 if (audioDevice == CallIntegration.AudioDevice.EARPIECE) {
517 acquireProximityWakeLock();
518 } else {
519 releaseProximityWakeLock();
520 }
521 }
522
523 @Override
524 protected void refreshUiReal() {}
525
526 @Override
527 public void onNewIntent(final Intent intent) {
528 Log.d(Config.LOGTAG, this.getClass().getName() + ".onNewIntent()");
529 super.onNewIntent(intent);
530 if (intent == null) {
531 return;
532 }
533 setIntent(intent);
534 if (xmppConnectionService == null) {
535 Log.d(
536 Config.LOGTAG,
537 "RtpSessionActivity: background service wasn't bound in onNewIntent()");
538 return;
539 }
540 initializeWithIntent(Event.ON_NEW_INTENT, intent);
541 }
542
543 @Override
544 protected void onBackendConnected() {
545 final var intent = getIntent();
546 if (intent == null) {
547 return;
548 }
549 initializeWithIntent(Event.ON_BACKEND_CONNECTED, intent);
550 }
551
552 private void initializeWithIntent(final Event event, @NonNull final Intent intent) {
553 final String action = intent.getAction();
554 Log.d(Config.LOGTAG, "initializeWithIntent(" + event + "," + action + ")");
555 final Account account = extractAccount(intent);
556 final var extraWith = intent.getStringExtra(EXTRA_WITH);
557 final Jid with = Strings.isNullOrEmpty(extraWith) ? null : Jid.ofEscaped(extraWith);
558 if (with == null || account == null) {
559 Log.e(Config.LOGTAG, "intent is missing extras (account or with)");
560 return;
561 }
562 final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID);
563 if (sessionId != null) {
564 if (initializeActivityWithRunningRtpSession(account, with, sessionId)) {
565 return;
566 }
567 if (ACTION_ACCEPT_CALL.equals(intent.getAction())) {
568 Log.d(Config.LOGTAG, "intent action was accept");
569 requestPermissionsAndAcceptCall();
570 resetIntent(intent.getExtras());
571 }
572 } else if (Intent.ACTION_VIEW.equals(action)) {
573 final String proposedSessionId = intent.getStringExtra(EXTRA_PROPOSED_SESSION_ID);
574 final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession =
575 xmppConnectionService
576 .getJingleConnectionManager()
577 .getTerminalSessionState(with, proposedSessionId);
578 if (terminatedRtpSession != null) {
579 // termination (due to message error or 'busy' was faster than opening the activity
580 initializeWithTerminatedSessionState(account, with, terminatedRtpSession);
581 return;
582 }
583 final String extraLastState = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE);
584 final RtpEndUserState state =
585 extraLastState == null ? null : RtpEndUserState.valueOf(extraLastState);
586 final var contact = account.getRoster().getContact(with);
587 if (state != null) {
588 Log.d(Config.LOGTAG, "restored last state from intent extra");
589 updateButtonConfiguration(state);
590 updateVerifiedShield(false);
591 updateStateDisplay(state);
592 updateIncomingCallScreen(state);
593 updateSupportWarning(state, contact);
594 invalidateOptionsMenu();
595 }
596 setWith(state, contact);
597 if (xmppConnectionService
598 .getJingleConnectionManager()
599 .fireJingleRtpConnectionStateUpdates()) {
600 return;
601 }
602 if (END_CARD.contains(state)) {
603 return;
604 }
605 final String lastAction = intent.getStringExtra(EXTRA_LAST_ACTION);
606 final Set<Media> media = actionToMedia(lastAction);
607 if (xmppConnectionService
608 .getJingleConnectionManager()
609 .hasMatchingProposal(account, with)) {
610 putScreenInCallMode(media);
611 return;
612 }
613 Log.d(Config.LOGTAG, "restored state (" + state + ") was not an end card. finishing");
614 finish();
615 }
616 }
617
618 private void setWith(final RtpEndUserState state) {
619 setWith(state, getWith());
620 }
621
622 private void setWith(final RtpEndUserState state, final Contact contact) {
623 binding.with.setText(contact.getDisplayName());
624 if (Arrays.asList(RtpEndUserState.INCOMING_CALL, RtpEndUserState.ACCEPTING_CALL)
625 .contains(state)) {
626 binding.withJid.setText(contact.getJid().asBareJid().toEscapedString());
627 binding.withJid.setVisibility(View.VISIBLE);
628 } else {
629 binding.withJid.setVisibility(View.GONE);
630 }
631 }
632
633 @Override
634 public void onRequestPermissionsResult(
635 int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
636 super.onRequestPermissionsResult(requestCode, permissions, grantResults);
637 final PermissionUtils.PermissionResult permissionResult =
638 PermissionUtils.removeBluetoothConnect(permissions, grantResults);
639 if (PermissionUtils.allGranted(permissionResult.grantResults)) {
640 if (requestCode == REQUEST_ACCEPT_CALL) {
641 acceptCall();
642 } else if (requestCode == REQUEST_ACCEPT_CONTENT) {
643 acceptContentAdd();
644 } else if (requestCode == REQUEST_ADD_CONTENT) {
645 switchToVideo();
646 }
647 } else {
648 @StringRes int res;
649 final String firstDenied =
650 getFirstDenied(permissionResult.grantResults, permissionResult.permissions);
651 if (firstDenied == null) {
652 return;
653 }
654 if (Manifest.permission.RECORD_AUDIO.equals(firstDenied)) {
655 res = R.string.no_microphone_permission;
656 } else if (Manifest.permission.CAMERA.equals(firstDenied)) {
657 res = R.string.no_camera_permission;
658 } else {
659 throw new IllegalStateException("Invalid permission result request");
660 }
661 Toast.makeText(this, getString(res, getString(R.string.app_name)), Toast.LENGTH_SHORT)
662 .show();
663 }
664 }
665
666 @Override
667 public void onStart() {
668 super.onStart();
669 mHandler.postDelayed(mTickExecutor, CALL_DURATION_UPDATE_INTERVAL);
670 mHandler.postDelayed(mVisibilityToggleExecutor, BUTTON_VISIBILITY_TIMEOUT);
671 this.binding.remoteVideo.setOnAspectRatioChanged(this);
672 }
673
674 @Override
675 public void onResume() {
676 super.onResume();
677 resetVisibilityExecutorShowButtons();
678 }
679
680 @Override
681 public void onStop() {
682 mHandler.removeCallbacks(mTickExecutor);
683 mHandler.removeCallbacks(mVisibilityToggleExecutor);
684 binding.remoteVideo.release();
685 binding.remoteVideo.setOnAspectRatioChanged(null);
686 binding.localVideo.release();
687 final WeakReference<JingleRtpConnection> weakReference = this.rtpConnectionReference;
688 final JingleRtpConnection jingleRtpConnection =
689 weakReference == null ? null : weakReference.get();
690 if (jingleRtpConnection != null) {
691 releaseVideoTracks(jingleRtpConnection);
692 }
693 releaseProximityWakeLock();
694 super.onStop();
695 }
696
697 private void releaseVideoTracks(final JingleRtpConnection jingleRtpConnection) {
698 final Optional<VideoTrack> remoteVideo = jingleRtpConnection.getRemoteVideoTrack();
699 if (remoteVideo.isPresent()) {
700 remoteVideo.get().removeSink(binding.remoteVideo);
701 }
702 final Optional<VideoTrack> localVideo = jingleRtpConnection.getLocalVideoTrack();
703 if (localVideo.isPresent()) {
704 localVideo.get().removeSink(binding.localVideo);
705 }
706 }
707
708 @Override
709 public void onBackPressed() {
710 if (isConnected()) {
711 if (switchToPictureInPicture()) {
712 return;
713 }
714 } else {
715 endCall();
716 }
717 super.onBackPressed();
718 }
719
720 @Override
721 public void onUserLeaveHint() {
722 super.onUserLeaveHint();
723 if (switchToPictureInPicture()) {
724 return;
725 }
726 // TODO apparently this method is not getting called on Android 10 when using the task
727 // switcher
728 if (emptyReference(rtpConnectionReference) && xmppConnectionService != null) {
729 retractSessionProposal();
730 }
731 }
732
733 private boolean isConnected() {
734 final JingleRtpConnection connection =
735 this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
736 final RtpEndUserState endUserState =
737 connection == null ? null : connection.getEndUserState();
738 return STATES_CONSIDERED_CONNECTED.contains(endUserState)
739 || endUserState == RtpEndUserState.INCOMING_CONTENT_ADD;
740 }
741
742 private boolean switchToPictureInPicture() {
743 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && deviceSupportsPictureInPicture()) {
744 if (shouldBePictureInPicture()) {
745 startPictureInPicture();
746 return true;
747 }
748 }
749 return false;
750 }
751
752 @RequiresApi(api = Build.VERSION_CODES.O)
753 private void startPictureInPicture() {
754 try {
755 final Rational rational = this.binding.remoteVideo.getAspectRatio();
756 final Rational clippedRational = Rationals.clip(rational);
757 Log.d(
758 Config.LOGTAG,
759 "suggested rational " + rational + ". clipped to " + clippedRational);
760 enterPictureInPictureMode(
761 new PictureInPictureParams.Builder().setAspectRatio(clippedRational).build());
762 } catch (final IllegalStateException e) {
763 // this sometimes happens on Samsung phones (possibly when Knox is enabled)
764 Log.w(Config.LOGTAG, "unable to enter picture in picture mode", e);
765 }
766 }
767
768 @Override
769 public void onAspectRatioChanged(final Rational rational) {
770 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isPictureInPicture()) {
771 final Rational clippedRational = Rationals.clip(rational);
772 Log.d(
773 Config.LOGTAG,
774 "suggested rational after aspect ratio change "
775 + rational
776 + ". clipped to "
777 + clippedRational);
778 setPictureInPictureParams(
779 new PictureInPictureParams.Builder().setAspectRatio(clippedRational).build());
780 }
781 }
782
783 private boolean deviceSupportsPictureInPicture() {
784 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
785 return getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
786 } else {
787 return false;
788 }
789 }
790
791 private boolean shouldBePictureInPicture() {
792 try {
793 final JingleRtpConnection rtpConnection = requireRtpConnection();
794 return rtpConnection.getMedia().contains(Media.VIDEO)
795 && Arrays.asList(
796 RtpEndUserState.ACCEPTING_CALL,
797 RtpEndUserState.CONNECTING,
798 RtpEndUserState.CONNECTED)
799 .contains(rtpConnection.getEndUserState());
800 } catch (final IllegalStateException e) {
801 return false;
802 }
803 }
804
805 private boolean isInConnectedVideoCall() {
806 final JingleRtpConnection rtpConnection;
807 try {
808 rtpConnection = requireRtpConnection();
809 } catch (final IllegalStateException e) {
810 return false;
811 }
812 return rtpConnection.getMedia().contains(Media.VIDEO)
813 && rtpConnection.getEndUserState() == RtpEndUserState.CONNECTED;
814 }
815
816 private boolean initializeActivityWithRunningRtpSession(
817 final Account account, Jid with, String sessionId) {
818 final WeakReference<JingleRtpConnection> reference =
819 xmppConnectionService
820 .getJingleConnectionManager()
821 .findJingleRtpConnection(account, with, sessionId);
822 if (reference == null || reference.get() == null) {
823 final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession =
824 xmppConnectionService
825 .getJingleConnectionManager()
826 .getTerminalSessionState(with, sessionId);
827 if (terminatedRtpSession == null) {
828 Log.e(Config.LOGTAG, "failed to initialize activity with running rtp session. session not found");
829 finish();
830 return true;
831 }
832 initializeWithTerminatedSessionState(account, with, terminatedRtpSession);
833 return true;
834 }
835 this.rtpConnectionReference = reference;
836 final RtpEndUserState currentState = requireRtpConnection().getEndUserState();
837 final boolean verified = requireRtpConnection().isVerified();
838 if (currentState == RtpEndUserState.ENDED) {
839 finish();
840 return true;
841 }
842 final Set<Media> media = getMedia();
843 final ContentAddition contentAddition = getPendingContentAddition();
844 if (currentState == RtpEndUserState.INCOMING_CALL) {
845 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
846 }
847 if (JingleRtpConnection.STATES_SHOWING_ONGOING_CALL.contains(
848 requireRtpConnection().getState())) {
849 putScreenInCallMode();
850 }
851 setWith(currentState);
852 updateVideoViews(currentState);
853 updateStateDisplay(currentState, media, contentAddition);
854 updateVerifiedShield(verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(currentState));
855 updateButtonConfiguration(currentState, media, contentAddition);
856 updateIncomingCallScreen(currentState);
857 invalidateOptionsMenu();
858 return false;
859 }
860
861 private void initializeWithTerminatedSessionState(
862 final Account account,
863 final Jid with,
864 final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession) {
865 Log.d(Config.LOGTAG, "initializeWithTerminatedSessionState()");
866 if (terminatedRtpSession.state == RtpEndUserState.ENDED) {
867 finish();
868 return;
869 }
870 final RtpEndUserState state = terminatedRtpSession.state;
871 resetIntent(account, with, terminatedRtpSession.state, terminatedRtpSession.media);
872 updateButtonConfiguration(state);
873 updateStateDisplay(state);
874 updateIncomingCallScreen(state);
875 updateCallDuration();
876 updateVerifiedShield(false);
877 invalidateOptionsMenu();
878 final var contact = account.getRoster().getContact(with);
879 setWith(state, contact);
880 updateSupportWarning(state, contact);
881 }
882
883 private void reInitializeActivityWithRunningRtpSession(
884 final Account account, Jid with, String sessionId) {
885 runOnUiThread(() -> initializeActivityWithRunningRtpSession(account, with, sessionId));
886 resetIntent(account, with, sessionId);
887 }
888
889 private void resetIntent(final Account account, final Jid with, final String sessionId) {
890 final Intent intent = new Intent(Intent.ACTION_VIEW);
891 intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
892 intent.putExtra(EXTRA_WITH, with.toEscapedString());
893 intent.putExtra(EXTRA_SESSION_ID, sessionId);
894 setIntent(intent);
895 }
896
897 private void ensureSurfaceViewRendererIsSetup(final SurfaceViewRenderer surfaceViewRenderer) {
898 surfaceViewRenderer.setVisibility(View.VISIBLE);
899 try {
900 surfaceViewRenderer.init(requireRtpConnection().getEglBaseContext(), null);
901 } catch (final IllegalStateException ignored) {
902 // SurfaceViewRenderer was already initialized
903 } catch (final RuntimeException e) {
904 if (Throwables.getRootCause(e) instanceof GLException glException) {
905 Log.w(Config.LOGTAG, "could not set up hardware renderer", glException);
906 }
907 }
908 surfaceViewRenderer.setEnableHardwareScaler(true);
909 }
910
911 private void updateStateDisplay(final RtpEndUserState state) {
912 updateStateDisplay(state, Collections.emptySet(), null);
913 }
914
915 private void updateStateDisplay(
916 final RtpEndUserState state,
917 final Set<Media> media,
918 final ContentAddition contentAddition) {
919 switch (state) {
920 case INCOMING_CALL -> {
921 Preconditions.checkArgument(!media.isEmpty(), "Media must not be empty");
922 if (media.contains(Media.VIDEO)) {
923 setTitle(R.string.rtp_state_incoming_video_call);
924 } else {
925 setTitle(R.string.rtp_state_incoming_call);
926 }
927 }
928 case INCOMING_CONTENT_ADD -> {
929 if (contentAddition != null && contentAddition.media().contains(Media.VIDEO)) {
930 setTitle(R.string.rtp_state_content_add_video);
931 } else {
932 setTitle(R.string.rtp_state_content_add);
933 }
934 }
935 case CONNECTING -> setTitle(R.string.rtp_state_connecting);
936 case CONNECTED -> setTitle(R.string.rtp_state_connected);
937 case RECONNECTING -> setTitle(R.string.rtp_state_reconnecting);
938 case ACCEPTING_CALL -> setTitle(R.string.rtp_state_accepting_call);
939 case ENDING_CALL -> setTitle(R.string.rtp_state_ending_call);
940 case FINDING_DEVICE -> setTitle(R.string.rtp_state_finding_device);
941 case RINGING -> setTitle(R.string.rtp_state_ringing);
942 case DECLINED_OR_BUSY -> setTitle(R.string.rtp_state_declined_or_busy);
943 case CONTACT_OFFLINE -> setTitle(R.string.rtp_state_contact_offline);
944 case CONNECTIVITY_ERROR -> setTitle(R.string.rtp_state_connectivity_error);
945 case CONNECTIVITY_LOST_ERROR -> setTitle(R.string.rtp_state_connectivity_lost_error);
946 case RETRACTED -> setTitle(R.string.rtp_state_retracted);
947 case APPLICATION_ERROR -> setTitle(R.string.rtp_state_application_failure);
948 case SECURITY_ERROR -> setTitle(R.string.rtp_state_security_error);
949 case ENDED -> throw new IllegalStateException(
950 "Activity should have called finishAndReleaseWakeLock();");
951 default -> throw new IllegalStateException(
952 String.format("State %s has not been handled in UI", state));
953 }
954 }
955
956 private void updateVerifiedShield(final boolean verified) {
957 if (isPictureInPicture()) {
958 this.binding.verified.setVisibility(View.GONE);
959 return;
960 }
961 this.binding.verified.setVisibility(verified ? View.VISIBLE : View.GONE);
962 }
963
964 private void updateIncomingCallScreen(final RtpEndUserState state) {
965 updateIncomingCallScreen(state, null);
966 }
967
968 private void updateIncomingCallScreen(final RtpEndUserState state, final Contact contact) {
969 if (state == RtpEndUserState.INCOMING_CALL || state == RtpEndUserState.ACCEPTING_CALL) {
970 final boolean show = getResources().getBoolean(R.bool.is_portrait_mode);
971 if (show) {
972 binding.contactPhoto.setVisibility(View.VISIBLE);
973 if (contact == null) {
974 AvatarWorkerTask.loadAvatar(
975 getWith(), binding.contactPhoto, R.dimen.publish_avatar_size);
976 } else {
977 AvatarWorkerTask.loadAvatar(
978 contact, binding.contactPhoto, R.dimen.publish_avatar_size);
979 }
980 } else {
981 binding.contactPhoto.setVisibility(View.GONE);
982 }
983 final Account account = contact == null ? getWith().getAccount() : contact.getAccount();
984 binding.usingAccount.setVisibility(View.VISIBLE);
985 binding.usingAccount.setText(
986 getString(
987 R.string.using_account,
988 account.getJid().asBareJid().toEscapedString()));
989 } else {
990 binding.usingAccount.setVisibility(View.GONE);
991 binding.contactPhoto.setVisibility(View.GONE);
992 }
993 }
994
995 private void updateSupportWarning(final RtpEndUserState state, final Contact contact) {
996 if (state == RtpEndUserState.CONNECTIVITY_ERROR
997 && getResources().getBoolean(R.bool.is_portrait_mode)) {
998 binding.supportWarning.setVisibility(
999 RtpCapability.check(contact) == RtpCapability.Capability.NONE
1000 ? View.VISIBLE
1001 : View.GONE);
1002 } else {
1003 binding.supportWarning.setVisibility(View.GONE);
1004 }
1005 }
1006
1007 private Set<Media> getMedia() {
1008 return requireRtpConnection().getMedia();
1009 }
1010
1011 public ContentAddition getPendingContentAddition() {
1012 return requireRtpConnection().getPendingContentAddition();
1013 }
1014
1015 private void updateButtonConfiguration(final RtpEndUserState state) {
1016 updateButtonConfiguration(state, Collections.emptySet(), null);
1017 }
1018
1019 @SuppressLint("RestrictedApi")
1020 private void updateButtonConfiguration(
1021 final RtpEndUserState state,
1022 final Set<Media> media,
1023 final ContentAddition contentAddition) {
1024 if (state == RtpEndUserState.ENDING_CALL
1025 || isPictureInPicture()
1026 || this.buttonsHiddenAfterTimeout) {
1027 this.binding.rejectCall.setVisibility(View.INVISIBLE);
1028 this.binding.endCall.setVisibility(View.INVISIBLE);
1029 this.binding.acceptCall.setVisibility(View.INVISIBLE);
1030 } else if (state == RtpEndUserState.INCOMING_CALL) {
1031 this.binding.rejectCall.setContentDescription(getString(R.string.dismiss_call));
1032 this.binding.rejectCall.setOnClickListener(this::rejectCall);
1033 this.binding.rejectCall.setImageResource(R.drawable.ic_call_end_24dp);
1034 this.binding.rejectCall.setVisibility(View.VISIBLE);
1035 this.binding.endCall.setVisibility(View.INVISIBLE);
1036 this.binding.acceptCall.setContentDescription(getString(R.string.answer_call));
1037 this.binding.acceptCall.setOnClickListener(this::acceptCall);
1038 this.binding.acceptCall.setImageResource(R.drawable.ic_call_24dp);
1039 this.binding.acceptCall.setVisibility(View.VISIBLE);
1040 } else if (state == RtpEndUserState.INCOMING_CONTENT_ADD) {
1041 this.binding.rejectCall.setContentDescription(
1042 getString(R.string.reject_switch_to_video));
1043 this.binding.rejectCall.setOnClickListener(this::rejectContentAdd);
1044 this.binding.rejectCall.setImageResource(R.drawable.ic_clear_24dp);
1045 this.binding.rejectCall.setVisibility(View.VISIBLE);
1046 this.binding.endCall.setVisibility(View.INVISIBLE);
1047 this.binding.acceptCall.setContentDescription(getString(R.string.accept));
1048 this.binding.acceptCall.setOnClickListener((v -> acceptContentAdd(contentAddition)));
1049 this.binding.acceptCall.setImageResource(R.drawable.ic_check_24dp);
1050 this.binding.acceptCall.setVisibility(View.VISIBLE);
1051 } else if (asList(RtpEndUserState.DECLINED_OR_BUSY, RtpEndUserState.CONTACT_OFFLINE)
1052 .contains(state)) {
1053 this.binding.rejectCall.setContentDescription(getString(R.string.exit));
1054 this.binding.rejectCall.setOnClickListener(this::exit);
1055 this.binding.rejectCall.setImageResource(R.drawable.ic_clear_24dp);
1056 this.binding.rejectCall.setVisibility(View.VISIBLE);
1057 this.binding.endCall.setVisibility(View.INVISIBLE);
1058 this.binding.acceptCall.setContentDescription(getString(R.string.record_voice_mail));
1059 this.binding.acceptCall.setOnClickListener(this::recordVoiceMail);
1060 this.binding.acceptCall.setImageResource(R.drawable.ic_voicemail_24dp);
1061 this.binding.acceptCall.setVisibility(View.VISIBLE);
1062 } else if (asList(
1063 RtpEndUserState.CONNECTIVITY_ERROR,
1064 RtpEndUserState.CONNECTIVITY_LOST_ERROR,
1065 RtpEndUserState.APPLICATION_ERROR,
1066 RtpEndUserState.RETRACTED,
1067 RtpEndUserState.SECURITY_ERROR)
1068 .contains(state)) {
1069 this.binding.rejectCall.setContentDescription(getString(R.string.exit));
1070 this.binding.rejectCall.setOnClickListener(this::exit);
1071 this.binding.rejectCall.setImageResource(R.drawable.ic_clear_24dp);
1072 this.binding.rejectCall.setVisibility(View.VISIBLE);
1073 this.binding.endCall.setVisibility(View.INVISIBLE);
1074 this.binding.acceptCall.setContentDescription(getString(R.string.try_again));
1075 this.binding.acceptCall.setOnClickListener(this::retry);
1076 this.binding.acceptCall.setImageResource(R.drawable.ic_replay_24dp);
1077 this.binding.acceptCall.setVisibility(View.VISIBLE);
1078 } else {
1079 this.binding.rejectCall.setVisibility(View.INVISIBLE);
1080 this.binding.endCall.setContentDescription(getString(R.string.hang_up));
1081 this.binding.endCall.setOnClickListener(this::endCall);
1082 this.binding.endCall.setImageResource(R.drawable.ic_call_end_24dp);
1083 setVisibleAndShow(this.binding.endCall);
1084 this.binding.acceptCall.setVisibility(View.INVISIBLE);
1085 }
1086 updateInCallButtonConfiguration(state, media);
1087 }
1088
1089 private static void setVisibleAndShow(final FloatingActionButton button) {
1090 button.show();
1091 button.setVisibility(View.VISIBLE);
1092 }
1093
1094 private boolean isPictureInPicture() {
1095 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1096 return isInPictureInPictureMode();
1097 } else {
1098 return false;
1099 }
1100 }
1101
1102 private void updateInCallButtonConfiguration() {
1103 updateInCallButtonConfiguration(
1104 requireRtpConnection().getEndUserState(), requireRtpConnection().getMedia());
1105 }
1106
1107 @SuppressLint("RestrictedApi")
1108 private void updateInCallButtonConfiguration(
1109 final RtpEndUserState state, final Set<Media> media) {
1110 final var showButtons = !isPictureInPicture() && !buttonsHiddenAfterTimeout;
1111 if (STATES_CONSIDERED_CONNECTED.contains(state) && showButtons) {
1112 Preconditions.checkArgument(!media.isEmpty(), "Media must not be empty");
1113 if (media.contains(Media.VIDEO)) {
1114 final JingleRtpConnection rtpConnection = requireRtpConnection();
1115 updateInCallButtonConfigurationVideo(
1116 rtpConnection.isVideoEnabled(), rtpConnection.isCameraSwitchable());
1117 } else {
1118 final CallIntegration callIntegration = requireRtpConnection().getCallIntegration();
1119 updateInCallButtonConfigurationSpeaker(
1120 callIntegration.getSelectedAudioDevice(),
1121 callIntegration.getAudioDevices().size());
1122 this.binding.inCallActionFarRight.setVisibility(View.GONE);
1123 }
1124 if (media.contains(Media.AUDIO)) {
1125 updateInCallButtonConfigurationMicrophone(
1126 requireRtpConnection().isMicrophoneEnabled());
1127 } else {
1128 this.binding.inCallActionLeft.setVisibility(View.GONE);
1129 }
1130 } else if (STATES_SHOWING_SPEAKER_CONFIGURATION.contains(state)
1131 && showButtons
1132 && Media.audioOnly(media)) {
1133 final CallIntegration callIntegration;
1134 try {
1135 callIntegration = requireCallIntegration();
1136 } catch (final IllegalStateException e) {
1137 Log.e(Config.LOGTAG, "can not update InCallButtonConfiguration in state " + state);
1138 return;
1139 }
1140 updateInCallButtonConfigurationSpeaker(
1141 callIntegration.getSelectedAudioDevice(),
1142 callIntegration.getAudioDevices().size());
1143 this.binding.inCallActionFarRight.setVisibility(View.GONE);
1144 } else {
1145 this.binding.inCallActionLeft.setVisibility(View.GONE);
1146 this.binding.inCallActionRight.setVisibility(View.GONE);
1147 this.binding.inCallActionFarRight.setVisibility(View.GONE);
1148 }
1149 }
1150
1151 @SuppressLint("RestrictedApi")
1152 private void updateInCallButtonConfigurationSpeaker(
1153 final CallIntegration.AudioDevice selectedAudioDevice, final int numberOfChoices) {
1154 switch (selectedAudioDevice) {
1155 case EARPIECE -> {
1156 this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_off_24dp);
1157 if (numberOfChoices >= 2) {
1158 this.binding.inCallActionRight.setContentDescription(
1159 getString(R.string.call_is_using_earpiece_tap_to_switch_to_speaker));
1160 this.binding.inCallActionRight.setOnClickListener(this::switchToSpeaker);
1161 } else {
1162 this.binding.inCallActionRight.setContentDescription(
1163 getString(R.string.call_is_using_earpiece));
1164 this.binding.inCallActionRight.setOnClickListener(null);
1165 this.binding.inCallActionRight.setClickable(false);
1166 }
1167 }
1168 case WIRED_HEADSET -> {
1169 this.binding.inCallActionRight.setContentDescription(
1170 getString(R.string.call_is_using_wired_headset));
1171 this.binding.inCallActionRight.setImageResource(R.drawable.ic_headset_mic_24dp);
1172 this.binding.inCallActionRight.setOnClickListener(null);
1173 this.binding.inCallActionRight.setClickable(false);
1174 }
1175 case SPEAKER_PHONE -> {
1176 this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_up_24dp);
1177 if (numberOfChoices >= 2) {
1178 this.binding.inCallActionRight.setContentDescription(
1179 getString(R.string.call_is_using_speaker_tap_to_switch_to_earpiece));
1180 this.binding.inCallActionRight.setOnClickListener(this::switchToEarpiece);
1181 } else {
1182 this.binding.inCallActionRight.setContentDescription(
1183 getString(R.string.call_is_using_speaker));
1184 this.binding.inCallActionRight.setOnClickListener(null);
1185 this.binding.inCallActionRight.setClickable(false);
1186 }
1187 }
1188 case BLUETOOTH -> {
1189 this.binding.inCallActionRight.setContentDescription(
1190 getString(R.string.call_is_using_bluetooth));
1191 this.binding.inCallActionRight.setImageResource(R.drawable.ic_bluetooth_audio_24dp);
1192 this.binding.inCallActionRight.setOnClickListener(null);
1193 this.binding.inCallActionRight.setClickable(false);
1194 }
1195 }
1196 setVisibleAndShow(this.binding.inCallActionRight);
1197 }
1198
1199 @SuppressLint("RestrictedApi")
1200 private void updateInCallButtonConfigurationVideo(
1201 final boolean videoEnabled, final boolean isCameraSwitchable) {
1202 setVisibleAndShow(this.binding.inCallActionRight);
1203 if (isCameraSwitchable) {
1204 this.binding.inCallActionFarRight.setImageResource(
1205 R.drawable.ic_flip_camera_android_24dp);
1206 setVisibleAndShow(this.binding.inCallActionFarRight);
1207 this.binding.inCallActionFarRight.setOnClickListener(this::switchCamera);
1208 this.binding.inCallActionFarRight.setContentDescription(
1209 getString(R.string.flip_camera));
1210 } else {
1211 this.binding.inCallActionFarRight.setVisibility(View.GONE);
1212 }
1213 if (videoEnabled) {
1214 this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_24dp);
1215 this.binding.inCallActionRight.setOnClickListener(this::disableVideo);
1216 this.binding.inCallActionRight.setContentDescription(
1217 getString(R.string.video_is_enabled_tap_to_disable));
1218 } else {
1219 this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_off_24dp);
1220 this.binding.inCallActionRight.setOnClickListener(this::enableVideo);
1221 this.binding.inCallActionRight.setContentDescription(
1222 getString(R.string.video_is_disabled_tap_to_enable));
1223 }
1224 }
1225
1226 private void switchCamera(final View view) {
1227 resetVisibilityToggleExecutor();
1228 Futures.addCallback(
1229 requireRtpConnection().switchCamera(),
1230 new FutureCallback<>() {
1231 @Override
1232 public void onSuccess(@Nullable Boolean isFrontCamera) {
1233 binding.localVideo.setMirror(Boolean.TRUE.equals(isFrontCamera));
1234 }
1235
1236 @Override
1237 public void onFailure(@NonNull final Throwable throwable) {
1238 Log.d(
1239 Config.LOGTAG,
1240 "could not switch camera",
1241 Throwables.getRootCause(throwable));
1242 Toast.makeText(
1243 RtpSessionActivity.this,
1244 R.string.could_not_switch_camera,
1245 Toast.LENGTH_LONG)
1246 .show();
1247 }
1248 },
1249 MainThreadExecutor.getInstance());
1250 }
1251
1252 private void enableVideo(final View view) {
1253 resetVisibilityToggleExecutor();
1254 try {
1255 requireRtpConnection().setVideoEnabled(true);
1256 } catch (final IllegalStateException e) {
1257 Toast.makeText(this, R.string.unable_to_enable_video, Toast.LENGTH_SHORT).show();
1258 return;
1259 }
1260 updateInCallButtonConfigurationVideo(true, requireRtpConnection().isCameraSwitchable());
1261 }
1262
1263 private void disableVideo(final View view) {
1264 resetVisibilityToggleExecutor();
1265 final JingleRtpConnection rtpConnection = requireRtpConnection();
1266 final ContentAddition pending = rtpConnection.getPendingContentAddition();
1267 if (pending != null && pending.direction == ContentAddition.Direction.OUTGOING) {
1268 rtpConnection.retractContentAdd();
1269 return;
1270 }
1271 try {
1272 requireRtpConnection().setVideoEnabled(false);
1273 } catch (final IllegalStateException e) {
1274 Toast.makeText(this, R.string.could_not_disable_video, Toast.LENGTH_SHORT).show();
1275 return;
1276 }
1277 updateInCallButtonConfigurationVideo(false, requireRtpConnection().isCameraSwitchable());
1278 }
1279
1280 @SuppressLint("RestrictedApi")
1281 private void updateInCallButtonConfigurationMicrophone(final boolean microphoneEnabled) {
1282 if (microphoneEnabled) {
1283 this.binding.inCallActionLeft.setImageResource(R.drawable.ic_mic_24dp);
1284 this.binding.inCallActionLeft.setOnClickListener(this::disableMicrophone);
1285 } else {
1286 this.binding.inCallActionLeft.setImageResource(R.drawable.ic_mic_off_24dp);
1287 this.binding.inCallActionLeft.setOnClickListener(this::enableMicrophone);
1288 }
1289 setVisibleAndShow(this.binding.inCallActionLeft);
1290 }
1291
1292 private void updateCallDuration() {
1293 final JingleRtpConnection connection =
1294 this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
1295 if (connection == null || connection.getMedia().contains(Media.VIDEO)) {
1296 this.binding.duration.setVisibility(View.GONE);
1297 return;
1298 }
1299 if (connection.zeroDuration()) {
1300 this.binding.duration.setVisibility(View.GONE);
1301 } else {
1302 this.binding.duration.setText(
1303 TimeFrameUtils.formatElapsedTime(connection.getCallDuration(), false));
1304 this.binding.duration.setVisibility(View.VISIBLE);
1305 }
1306 }
1307
1308 private void resetVisibilityToggleExecutor() {
1309 mHandler.removeCallbacks(this.mVisibilityToggleExecutor);
1310 mHandler.postDelayed(this.mVisibilityToggleExecutor, BUTTON_VISIBILITY_TIMEOUT);
1311 }
1312
1313 private void updateButtonInVideoCallVisibility() {
1314 if (isInConnectedVideoCall()) {
1315 if (isPictureInPicture()) {
1316 return;
1317 }
1318 Log.d(Config.LOGTAG, "hiding in-call buttons after timeout was reached");
1319 hideInCallButtons();
1320 }
1321 }
1322
1323 private void hideInCallButtons() {
1324 binding.inCallActionLeft.hide();
1325 binding.endCall.hide();
1326 binding.inCallActionRight.hide();
1327 binding.inCallActionFarRight.hide();
1328 }
1329
1330 private void showInCallButtons() {
1331 this.buttonsHiddenAfterTimeout = false;
1332 final JingleRtpConnection rtpConnection;
1333 try {
1334 rtpConnection = requireRtpConnection();
1335 } catch (final IllegalStateException e) {
1336 return;
1337 }
1338 updateButtonConfiguration(
1339 rtpConnection.getEndUserState(),
1340 rtpConnection.getMedia(),
1341 rtpConnection.getPendingContentAddition());
1342 }
1343
1344 private void resetVisibilityExecutorShowButtons() {
1345 resetVisibilityToggleExecutor();
1346 showInCallButtons();
1347 }
1348
1349 private void updateVideoViews(final RtpEndUserState state) {
1350 if (END_CARD.contains(state) || state == RtpEndUserState.ENDING_CALL) {
1351 binding.localVideo.setVisibility(View.GONE);
1352 binding.localVideo.release();
1353 binding.remoteVideoWrapper.setVisibility(View.GONE);
1354 binding.remoteVideo.release();
1355 binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
1356 if (isPictureInPicture()) {
1357 binding.appBarLayout.setVisibility(View.GONE);
1358 binding.pipPlaceholder.setVisibility(View.VISIBLE);
1359 if (Arrays.asList(
1360 RtpEndUserState.APPLICATION_ERROR,
1361 RtpEndUserState.CONNECTIVITY_ERROR,
1362 RtpEndUserState.SECURITY_ERROR)
1363 .contains(state)) {
1364 binding.pipWarning.setVisibility(View.VISIBLE);
1365 binding.pipWaiting.setVisibility(View.GONE);
1366 } else {
1367 binding.pipWarning.setVisibility(View.GONE);
1368 binding.pipWaiting.setVisibility(View.GONE);
1369 }
1370 } else {
1371 binding.appBarLayout.setVisibility(View.VISIBLE);
1372 binding.pipPlaceholder.setVisibility(View.GONE);
1373 }
1374 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
1375 return;
1376 }
1377 if (isPictureInPicture() && STATES_SHOWING_PIP_PLACEHOLDER.contains(state)) {
1378 binding.localVideo.setVisibility(View.GONE);
1379 binding.remoteVideoWrapper.setVisibility(View.GONE);
1380 binding.appBarLayout.setVisibility(View.GONE);
1381 binding.pipPlaceholder.setVisibility(View.VISIBLE);
1382 binding.pipWarning.setVisibility(View.GONE);
1383 binding.pipWaiting.setVisibility(View.VISIBLE);
1384 binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
1385 return;
1386 }
1387 final Optional<VideoTrack> localVideoTrack = getLocalVideoTrack();
1388 if (localVideoTrack.isPresent() && !isPictureInPicture()) {
1389 ensureSurfaceViewRendererIsSetup(binding.localVideo);
1390 // paint local view over remote view
1391 binding.localVideo.setZOrderMediaOverlay(true);
1392 binding.localVideo.setMirror(requireRtpConnection().isFrontCamera());
1393 addSink(localVideoTrack.get(), binding.localVideo);
1394 } else {
1395 binding.localVideo.setVisibility(View.GONE);
1396 }
1397 final Optional<VideoTrack> remoteVideoTrack = getRemoteVideoTrack();
1398 if (remoteVideoTrack.isPresent()) {
1399 ensureSurfaceViewRendererIsSetup(binding.remoteVideo);
1400 addSink(remoteVideoTrack.get(), binding.remoteVideo);
1401 binding.remoteVideo.setScalingType(
1402 RendererCommon.ScalingType.SCALE_ASPECT_FILL,
1403 RendererCommon.ScalingType.SCALE_ASPECT_FIT);
1404 if (state == RtpEndUserState.CONNECTED) {
1405 binding.appBarLayout.setVisibility(View.GONE);
1406 getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
1407 binding.remoteVideoWrapper.setVisibility(View.VISIBLE);
1408 } else {
1409 binding.appBarLayout.setVisibility(View.VISIBLE);
1410 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
1411 binding.remoteVideoWrapper.setVisibility(View.GONE);
1412 }
1413 if (isPictureInPicture() && !requireRtpConnection().isMicrophoneEnabled()) {
1414 binding.pipLocalMicOffIndicator.setVisibility(View.VISIBLE);
1415 } else {
1416 binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
1417 }
1418 } else {
1419 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
1420 binding.remoteVideoWrapper.setVisibility(View.GONE);
1421 binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
1422 }
1423 }
1424
1425 private Optional<VideoTrack> getLocalVideoTrack() {
1426 final JingleRtpConnection connection =
1427 this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
1428 if (connection == null) {
1429 return Optional.absent();
1430 }
1431 return connection.getLocalVideoTrack();
1432 }
1433
1434 private Optional<VideoTrack> getRemoteVideoTrack() {
1435 final JingleRtpConnection connection =
1436 this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
1437 if (connection == null) {
1438 return Optional.absent();
1439 }
1440 return connection.getRemoteVideoTrack();
1441 }
1442
1443 private void disableMicrophone(final View view) {
1444 setMicrophoneEnabled(false);
1445 }
1446
1447 private void enableMicrophone(final View view) {
1448 setMicrophoneEnabled(true);
1449 }
1450
1451 private void setMicrophoneEnabled(final boolean enabled) {
1452 resetVisibilityExecutorShowButtons();
1453 try {
1454 final JingleRtpConnection rtpConnection = requireRtpConnection();
1455 if (rtpConnection.setMicrophoneEnabled(enabled)) {
1456 updateInCallButtonConfiguration();
1457 }
1458 } catch (final IllegalStateException e) {
1459 Toast.makeText(this, R.string.could_not_modify_call, Toast.LENGTH_SHORT).show();
1460 }
1461 }
1462
1463 private void switchToEarpiece(final View view) {
1464 try {
1465 requireCallIntegration().setAudioDevice(CallIntegration.AudioDevice.EARPIECE);
1466 acquireProximityWakeLock();
1467 } catch (final IllegalStateException e) {
1468 Toast.makeText(this, R.string.could_not_modify_call, Toast.LENGTH_SHORT).show();
1469 }
1470 }
1471
1472 private void switchToSpeaker(final View view) {
1473 try {
1474 requireCallIntegration().setAudioDevice(CallIntegration.AudioDevice.SPEAKER_PHONE);
1475 releaseProximityWakeLock();
1476 } catch (final IllegalStateException e) {
1477 Toast.makeText(this, R.string.could_not_modify_call, Toast.LENGTH_SHORT).show();
1478 }
1479 }
1480
1481 private void retry(final View view) {
1482 final Intent intent = getIntent();
1483 final Account account = extractAccount(intent);
1484 final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
1485 final String lastAction = intent.getStringExtra(EXTRA_LAST_ACTION);
1486 final String action = intent.getAction();
1487 final Set<Media> media = actionToMedia(lastAction == null ? action : lastAction);
1488 this.rtpConnectionReference = null;
1489 Log.d(Config.LOGTAG, "attempting retry with " + with.toEscapedString());
1490 CallIntegrationConnectionService.placeCall(xmppConnectionService, account, with, media);
1491 }
1492
1493 private void exit(final View view) {
1494 finish();
1495 }
1496
1497 private void recordVoiceMail(final View view) {
1498 final Intent intent = getIntent();
1499 final Account account = extractAccount(intent);
1500 final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
1501 final Conversation conversation =
1502 xmppConnectionService.findOrCreateConversation(account, with, false, true);
1503 final Intent launchIntent = new Intent(this, ConversationsActivity.class);
1504 launchIntent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
1505 launchIntent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid());
1506 launchIntent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_CLEAR_TOP);
1507 launchIntent.putExtra(
1508 ConversationsActivity.EXTRA_POST_INIT_ACTION,
1509 ConversationsActivity.POST_ACTION_RECORD_VOICE);
1510 startActivity(launchIntent);
1511 finish();
1512 }
1513
1514 private Contact getWith() {
1515 final AbstractJingleConnection.Id id = requireRtpConnection().getId();
1516 final Account account = id.account;
1517 return account.getRoster().getContact(id.with);
1518 }
1519
1520 private JingleRtpConnection requireRtpConnection() {
1521 final JingleRtpConnection connection =
1522 this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
1523 if (connection == null) {
1524 throw new IllegalStateException("No RTP connection found");
1525 }
1526 return connection;
1527 }
1528
1529 private CallIntegration requireCallIntegration() {
1530 return requireOngoingRtpSession().getCallIntegration();
1531 }
1532
1533 private OngoingRtpSession requireOngoingRtpSession() {
1534 final JingleRtpConnection connection =
1535 this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
1536 if (connection != null) {
1537 return connection;
1538 }
1539 final Intent currentIntent = getIntent();
1540 final String withExtra =
1541 currentIntent == null ? null : currentIntent.getStringExtra(EXTRA_WITH);
1542 final var account = extractAccount(currentIntent);
1543 if (withExtra == null) {
1544 throw new IllegalStateException("Current intent has no EXTRA_WITH");
1545 }
1546 final var matching =
1547 xmppConnectionService
1548 .getJingleConnectionManager()
1549 .matchingProposal(account, Jid.of(withExtra));
1550 if (matching.isPresent()) {
1551 return matching.get();
1552 }
1553 throw new IllegalStateException("No matching session proposal");
1554 }
1555
1556 @Override
1557 public void onJingleRtpConnectionUpdate(
1558 Account account, Jid with, final String sessionId, RtpEndUserState state) {
1559 Log.d(Config.LOGTAG, "onJingleRtpConnectionUpdate(" + state + ")");
1560 if (END_CARD.contains(state)) {
1561 Log.d(Config.LOGTAG, "end card reached");
1562 releaseProximityWakeLock();
1563 runOnUiThread(
1564 () -> getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON));
1565 }
1566 if (with.isBareJid()) {
1567 // TODO check for ENDED
1568 updateRtpSessionProposalState(account, with, state);
1569 return;
1570 }
1571 if (emptyReference(this.rtpConnectionReference)) {
1572 if (END_CARD.contains(state)) {
1573 Log.d(Config.LOGTAG, "not reinitializing session");
1574 return;
1575 }
1576 // this happens when going from proposed session to actual session
1577 reInitializeActivityWithRunningRtpSession(account, with, sessionId);
1578 return;
1579 }
1580 final AbstractJingleConnection.Id id = requireRtpConnection().getId();
1581 final boolean verified = requireRtpConnection().isVerified();
1582 final Set<Media> media = getMedia();
1583 lockOrientation(media);
1584 final ContentAddition contentAddition = getPendingContentAddition();
1585 final Contact contact = getWith();
1586 if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) {
1587 if (state == RtpEndUserState.ENDED) {
1588 finish();
1589 return;
1590 }
1591 resetVisibilityToggleExecutor();
1592 runOnUiThread(
1593 () -> {
1594 updateStateDisplay(state, media, contentAddition);
1595 updateVerifiedShield(
1596 verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(state));
1597 updateButtonConfiguration(state, media, contentAddition);
1598 updateVideoViews(state);
1599 updateIncomingCallScreen(state, contact);
1600 updateSupportWarning(state, contact);
1601 invalidateOptionsMenu();
1602 });
1603 if (END_CARD.contains(state)) {
1604 final JingleRtpConnection rtpConnection = requireRtpConnection();
1605 resetIntent(account, with, state, rtpConnection.getMedia());
1606 releaseVideoTracks(rtpConnection);
1607 this.rtpConnectionReference = null;
1608 }
1609 } else {
1610 Log.d(Config.LOGTAG, "received update for other rtp session");
1611 }
1612 }
1613
1614 @Override
1615 public void onAudioDeviceChanged(
1616 final CallIntegration.AudioDevice selectedAudioDevice,
1617 final Set<CallIntegration.AudioDevice> availableAudioDevices) {
1618 Log.d(
1619 Config.LOGTAG,
1620 "onAudioDeviceChanged in activity: selected:"
1621 + selectedAudioDevice
1622 + ", available:"
1623 + availableAudioDevices);
1624 try {
1625 final OngoingRtpSession ongoingRtpSession = requireOngoingRtpSession();
1626 final RtpEndUserState endUserState;
1627 if (ongoingRtpSession instanceof JingleRtpConnection jingleRtpConnection) {
1628 endUserState = jingleRtpConnection.getEndUserState();
1629 } else {
1630 // for session proposals all end user states are functionally the same
1631 endUserState = RtpEndUserState.RINGING;
1632 }
1633 final Set<Media> media = ongoingRtpSession.getMedia();
1634 if (END_CARD.contains(endUserState)) {
1635 Log.d(
1636 Config.LOGTAG,
1637 "onAudioDeviceChanged() nothing to do because end card has been reached");
1638 } else {
1639 if (Media.audioOnly(media)
1640 && STATES_SHOWING_SPEAKER_CONFIGURATION.contains(endUserState)) {
1641 final CallIntegration callIntegration = requireCallIntegration();
1642 updateInCallButtonConfigurationSpeaker(
1643 callIntegration.getSelectedAudioDevice(),
1644 callIntegration.getAudioDevices().size());
1645 }
1646 Log.d(
1647 Config.LOGTAG,
1648 "put proximity wake lock into proper state after device update");
1649 putProximityWakeLockInProperState(selectedAudioDevice);
1650 }
1651 } catch (final IllegalStateException e) {
1652 Log.d(Config.LOGTAG, "RTP connection was not available when audio device changed");
1653 }
1654 }
1655
1656 @Override
1657 protected void onSaveInstanceState(@NonNull @NotNull Bundle outState) {
1658 super.onSaveInstanceState(outState);
1659 outState.putBoolean("dialpad_visible", binding.dialpad.getVisibility() == View.VISIBLE);
1660 }
1661
1662 private void updateRtpSessionProposalState(
1663 final Account account, final Jid with, final RtpEndUserState state) {
1664 final Intent currentIntent = getIntent();
1665 final String withExtra =
1666 currentIntent == null ? null : currentIntent.getStringExtra(EXTRA_WITH);
1667 if (withExtra == null) {
1668 return;
1669 }
1670 final Set<Media> media = actionToMedia(currentIntent.getStringExtra(EXTRA_LAST_ACTION));
1671 if (Jid.ofEscaped(withExtra).asBareJid().equals(with)) {
1672 runOnUiThread(
1673 () -> {
1674 updateVerifiedShield(false);
1675 updateStateDisplay(state);
1676 updateButtonConfiguration(state, media, null);
1677 updateIncomingCallScreen(state);
1678 updateSupportWarning(state, account.getRoster().getContact(with));
1679 invalidateOptionsMenu();
1680 });
1681 resetIntent(account, with, state, media);
1682 }
1683 }
1684
1685 private void resetIntent(final Bundle extras) {
1686 final Intent intent = new Intent(Intent.ACTION_VIEW);
1687 intent.putExtras(extras);
1688 setIntent(intent);
1689 }
1690
1691 private void resetIntent(
1692 final Account account, Jid with, final RtpEndUserState state, final Set<Media> media) {
1693 final Intent intent = new Intent(Intent.ACTION_VIEW);
1694 intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
1695 if (RtpCapability.jmiSupport(account.getRoster().getContact(with))) {
1696 intent.putExtra(EXTRA_WITH, with.asBareJid().toEscapedString());
1697 } else {
1698 intent.putExtra(EXTRA_WITH, with.toEscapedString());
1699 }
1700 intent.putExtra(EXTRA_LAST_REPORTED_STATE, state.toString());
1701 intent.putExtra(
1702 EXTRA_LAST_ACTION,
1703 media.contains(Media.VIDEO) ? ACTION_MAKE_VIDEO_CALL : ACTION_MAKE_VOICE_CALL);
1704 setIntent(intent);
1705 }
1706
1707 private static boolean emptyReference(final WeakReference<?> weakReference) {
1708 return weakReference == null || weakReference.get() == null;
1709 }
1710
1711 private enum Event {
1712 ON_BACKEND_CONNECTED,
1713 ON_NEW_INTENT
1714 }
1715}