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