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