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