1package eu.siacs.conversations.ui;
2
3import android.Manifest;
4import android.annotation.SuppressLint;
5import android.app.PictureInPictureParams;
6import android.content.ActivityNotFoundException;
7import android.content.Context;
8import android.content.Intent;
9import android.content.pm.PackageManager;
10import android.os.Build;
11import android.os.Bundle;
12import android.os.Handler;
13import android.os.PowerManager;
14import android.os.SystemClock;
15import android.util.Log;
16import android.util.Rational;
17import android.view.KeyEvent;
18import android.view.Menu;
19import android.view.MenuItem;
20import android.view.View;
21import android.view.WindowManager;
22import android.widget.Toast;
23
24import androidx.annotation.NonNull;
25import androidx.annotation.RequiresApi;
26import androidx.annotation.StringRes;
27import androidx.databinding.DataBindingUtil;
28
29import com.google.common.base.Optional;
30import com.google.common.base.Preconditions;
31import com.google.common.base.Throwables;
32import com.google.common.collect.ImmutableList;
33import com.google.common.collect.ImmutableSet;
34import com.google.common.util.concurrent.FutureCallback;
35import com.google.common.util.concurrent.Futures;
36
37import org.checkerframework.checker.nullness.compatqual.NullableDecl;
38import org.webrtc.SurfaceViewRenderer;
39import org.webrtc.VideoTrack;
40
41import java.lang.ref.WeakReference;
42import java.util.Arrays;
43import java.util.Collections;
44import java.util.List;
45import java.util.Set;
46
47import eu.siacs.conversations.Config;
48import eu.siacs.conversations.R;
49import eu.siacs.conversations.databinding.ActivityRtpSessionBinding;
50import eu.siacs.conversations.entities.Account;
51import eu.siacs.conversations.entities.Contact;
52import eu.siacs.conversations.entities.Conversation;
53import eu.siacs.conversations.services.AppRTCAudioManager;
54import eu.siacs.conversations.services.XmppConnectionService;
55import eu.siacs.conversations.ui.util.AvatarWorkerTask;
56import eu.siacs.conversations.ui.util.MainThreadExecutor;
57import eu.siacs.conversations.utils.PermissionUtils;
58import eu.siacs.conversations.utils.TimeFrameUtils;
59import eu.siacs.conversations.xml.Namespace;
60import eu.siacs.conversations.xmpp.Jid;
61import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
62import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
63import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
64import eu.siacs.conversations.xmpp.jingle.Media;
65import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
66
67import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
68import static java.util.Arrays.asList;
69
70public class RtpSessionActivity extends XmppActivity implements XmppConnectionService.OnJingleRtpConnectionUpdate {
71
72 public static final String EXTRA_WITH = "with";
73 public static final String EXTRA_SESSION_ID = "session_id";
74 public static final String EXTRA_LAST_REPORTED_STATE = "last_reported_state";
75 public static final String EXTRA_LAST_ACTION = "last_action";
76 public static final String ACTION_ACCEPT_CALL = "action_accept_call";
77 public static final String ACTION_MAKE_VOICE_CALL = "action_make_voice_call";
78 public static final String ACTION_MAKE_VIDEO_CALL = "action_make_video_call";
79
80 private static final int CALL_DURATION_UPDATE_INTERVAL = 333;
81
82 private static final List<RtpEndUserState> END_CARD = Arrays.asList(
83 RtpEndUserState.APPLICATION_ERROR,
84 RtpEndUserState.DECLINED_OR_BUSY,
85 RtpEndUserState.CONNECTIVITY_ERROR,
86 RtpEndUserState.CONNECTIVITY_LOST_ERROR,
87 RtpEndUserState.RETRACTED
88 );
89 private static final List<RtpEndUserState> STATES_SHOWING_HELP_BUTTON = Arrays.asList(
90 RtpEndUserState.APPLICATION_ERROR,
91 RtpEndUserState.CONNECTIVITY_ERROR
92 );
93 private static final List<RtpEndUserState> STATES_SHOWING_SWITCH_TO_CHAT = Arrays.asList(
94 RtpEndUserState.CONNECTING,
95 RtpEndUserState.CONNECTED
96 );
97 private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session";
98 private static final int REQUEST_ACCEPT_CALL = 0x1111;
99 private WeakReference<JingleRtpConnection> rtpConnectionReference;
100
101 private ActivityRtpSessionBinding binding;
102 private PowerManager.WakeLock mProximityWakeLock;
103
104 private final Handler mHandler = new Handler();
105 private final Runnable mTickExecutor = new Runnable() {
106 @Override
107 public void run() {
108 updateCallDuration();
109 mHandler.postDelayed(mTickExecutor, CALL_DURATION_UPDATE_INTERVAL);
110 }
111 };
112
113 private static Set<Media> actionToMedia(final String action) {
114 if (ACTION_MAKE_VIDEO_CALL.equals(action)) {
115 return ImmutableSet.of(Media.AUDIO, Media.VIDEO);
116 } else {
117 return ImmutableSet.of(Media.AUDIO);
118 }
119 }
120
121 private static void addSink(final VideoTrack videoTrack, final SurfaceViewRenderer surfaceViewRenderer) {
122 try {
123 videoTrack.addSink(surfaceViewRenderer);
124 } catch (final IllegalStateException e) {
125 Log.e(Config.LOGTAG, "possible race condition on trying to display video track. ignoring", e);
126 }
127 }
128
129 @Override
130 public void onCreate(Bundle savedInstanceState) {
131 super.onCreate(savedInstanceState);
132 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
133 | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
134 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
135 | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
136 this.binding = DataBindingUtil.setContentView(this, R.layout.activity_rtp_session);
137 setSupportActionBar(binding.toolbar);
138 }
139
140 @Override
141 public boolean onCreateOptionsMenu(final Menu menu) {
142 getMenuInflater().inflate(R.menu.activity_rtp_session, menu);
143 final MenuItem help = menu.findItem(R.id.action_help);
144 final MenuItem gotoChat = menu.findItem(R.id.action_goto_chat);
145 help.setVisible(isHelpButtonVisible());
146 gotoChat.setVisible(isSwitchToConversationVisible());
147 return super.onCreateOptionsMenu(menu);
148 }
149
150 @Override
151 public boolean onKeyDown(final int keyCode, final KeyEvent event) {
152 if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN){
153 if (xmppConnectionService != null) {
154 xmppConnectionService.getNotificationService().stopSoundAndVibration();
155 }
156 }
157 return super.onKeyDown(keyCode, event);
158 }
159
160 private boolean isHelpButtonVisible() {
161 try {
162 return STATES_SHOWING_HELP_BUTTON.contains(requireRtpConnection().getEndUserState());
163 } catch (IllegalStateException e) {
164 final Intent intent = getIntent();
165 final String state = intent != null ? intent.getStringExtra(EXTRA_LAST_REPORTED_STATE) : null;
166 if (state != null) {
167 return STATES_SHOWING_HELP_BUTTON.contains(RtpEndUserState.valueOf(state));
168 } else {
169 return false;
170 }
171 }
172 }
173
174 private boolean isSwitchToConversationVisible() {
175 final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
176 return connection != null && STATES_SHOWING_SWITCH_TO_CHAT.contains(connection.getEndUserState());
177 }
178
179 private void switchToConversation() {
180 final Contact contact = getWith();
181 final Conversation conversation = xmppConnectionService.findOrCreateConversation(contact.getAccount(), contact.getJid(), false, true);
182 switchToConversation(conversation);
183 }
184
185 public boolean onOptionsItemSelected(final MenuItem item) {
186 switch (item.getItemId()) {
187 case R.id.action_help:
188 launchHelpInBrowser();
189 break;
190 case R.id.action_goto_chat:
191 switchToConversation();
192 break;
193 }
194 return super.onOptionsItemSelected(item);
195 }
196
197 private void launchHelpInBrowser() {
198 final Intent intent = new Intent(Intent.ACTION_VIEW, Config.HELP);
199 try {
200 startActivity(intent);
201 } catch (final ActivityNotFoundException e) {
202 Toast.makeText(this, R.string.no_application_found_to_open_link, Toast.LENGTH_LONG).show();
203 }
204 }
205
206 private void endCall(View view) {
207 endCall();
208 }
209
210 private void endCall() {
211 if (this.rtpConnectionReference == null) {
212 retractSessionProposal();
213 finish();
214 } else {
215 requireRtpConnection().endCall();
216 }
217 }
218
219 private void retractSessionProposal() {
220 final Intent intent = getIntent();
221 final String action = intent.getAction();
222 final Account account = extractAccount(intent);
223 final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
224 final String state = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE);
225 if (!Intent.ACTION_VIEW.equals(action) || state == null || !END_CARD.contains(RtpEndUserState.valueOf(state))) {
226 resetIntent(account, with, RtpEndUserState.RETRACTED, actionToMedia(intent.getAction()));
227 }
228 xmppConnectionService.getJingleConnectionManager().retractSessionProposal(account, with.asBareJid());
229 }
230
231 private void rejectCall(View view) {
232 requireRtpConnection().rejectCall();
233 finish();
234 }
235
236 private void acceptCall(View view) {
237 requestPermissionsAndAcceptCall();
238 }
239
240 private void requestPermissionsAndAcceptCall() {
241 final List<String> permissions;
242 if (getMedia().contains(Media.VIDEO)) {
243 permissions = ImmutableList.of(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO);
244 } else {
245 permissions = ImmutableList.of(Manifest.permission.RECORD_AUDIO);
246 }
247 if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CALL)) {
248 putScreenInCallMode();
249 checkRecorderAndAcceptCall();
250 }
251 }
252
253 private void checkRecorderAndAcceptCall() {
254 checkMicrophoneAvailability();
255 try {
256 requireRtpConnection().acceptCall();
257 } catch (final IllegalStateException e) {
258 Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
259 }
260 }
261
262 private void checkMicrophoneAvailability() {
263 new Thread(() -> {
264 final long start = SystemClock.elapsedRealtime();
265 final boolean isMicrophoneAvailable = AppRTCAudioManager.isMicrophoneAvailable();
266 final long stop = SystemClock.elapsedRealtime();
267 Log.d(Config.LOGTAG, "checking microphone availability took " + (stop - start) + "ms");
268 if (isMicrophoneAvailable) {
269 return;
270 }
271 runOnUiThread(() -> Toast.makeText(this, R.string.microphone_unavailable, Toast.LENGTH_LONG).show());
272 }
273 ).start();
274 }
275
276 private void putScreenInCallMode() {
277 putScreenInCallMode(requireRtpConnection().getMedia());
278 }
279
280 private void putScreenInCallMode(final Set<Media> media) {
281 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
282 if (!media.contains(Media.VIDEO)) {
283 final JingleRtpConnection rtpConnection = rtpConnectionReference != null ? rtpConnectionReference.get() : null;
284 final AppRTCAudioManager audioManager = rtpConnection == null ? null : rtpConnection.getAudioManager();
285 if (audioManager == null || audioManager.getSelectedAudioDevice() == AppRTCAudioManager.AudioDevice.EARPIECE) {
286 acquireProximityWakeLock();
287 }
288 }
289 }
290
291 @SuppressLint("WakelockTimeout")
292 private void acquireProximityWakeLock() {
293 final PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
294 if (powerManager == null) {
295 Log.e(Config.LOGTAG, "power manager not available");
296 return;
297 }
298 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
299 if (this.mProximityWakeLock == null) {
300 this.mProximityWakeLock = powerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, PROXIMITY_WAKE_LOCK_TAG);
301 }
302 if (!this.mProximityWakeLock.isHeld()) {
303 Log.d(Config.LOGTAG, "acquiring proximity wake lock");
304 this.mProximityWakeLock.acquire();
305 }
306 }
307 }
308
309 private void releaseProximityWakeLock() {
310 if (this.mProximityWakeLock != null && mProximityWakeLock.isHeld()) {
311 Log.d(Config.LOGTAG, "releasing proximity wake lock");
312 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
313 this.mProximityWakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
314 } else {
315 this.mProximityWakeLock.release();
316 }
317 this.mProximityWakeLock = null;
318 }
319 }
320
321 private void putProximityWakeLockInProperState(final AppRTCAudioManager.AudioDevice audioDevice) {
322 if (audioDevice == AppRTCAudioManager.AudioDevice.EARPIECE) {
323 acquireProximityWakeLock();
324 } else {
325 releaseProximityWakeLock();
326 }
327 }
328
329 @Override
330 protected void refreshUiReal() {
331
332 }
333
334 @Override
335 public void onNewIntent(final Intent intent) {
336 Log.d(Config.LOGTAG, this.getClass().getName() + ".onNewIntent()");
337 super.onNewIntent(intent);
338 setIntent(intent);
339 if (xmppConnectionService == null) {
340 Log.d(Config.LOGTAG, "RtpSessionActivity: background service wasn't bound in onNewIntent()");
341 return;
342 }
343 final Account account = extractAccount(intent);
344 final String action = intent.getAction();
345 final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
346 final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID);
347 if (sessionId != null) {
348 Log.d(Config.LOGTAG, "reinitializing from onNewIntent()");
349 if (initializeActivityWithRunningRtpSession(account, with, sessionId)) {
350 return;
351 }
352 if (ACTION_ACCEPT_CALL.equals(intent.getAction())) {
353 Log.d(Config.LOGTAG, "accepting call from onNewIntent()");
354 requestPermissionsAndAcceptCall();
355 resetIntent(intent.getExtras());
356 }
357 } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(action)) {
358 proposeJingleRtpSession(account, with, actionToMedia(action));
359 binding.with.setText(account.getRoster().getContact(with).getDisplayName());
360 } else {
361 throw new IllegalStateException("received onNewIntent without sessionId");
362 }
363 }
364
365 @Override
366 void onBackendConnected() {
367 final Intent intent = getIntent();
368 final String action = intent.getAction();
369 final Account account = extractAccount(intent);
370 final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
371 final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID);
372 if (sessionId != null) {
373 if (initializeActivityWithRunningRtpSession(account, with, sessionId)) {
374 return;
375 }
376 if (ACTION_ACCEPT_CALL.equals(intent.getAction())) {
377 Log.d(Config.LOGTAG, "intent action was accept");
378 requestPermissionsAndAcceptCall();
379 resetIntent(intent.getExtras());
380 }
381 } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(action)) {
382 proposeJingleRtpSession(account, with, actionToMedia(action));
383 binding.with.setText(account.getRoster().getContact(with).getDisplayName());
384 } else if (Intent.ACTION_VIEW.equals(action)) {
385 final String extraLastState = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE);
386 final RtpEndUserState state = extraLastState == null ? null : RtpEndUserState.valueOf(extraLastState);
387 if (state != null) {
388 Log.d(Config.LOGTAG, "restored last state from intent extra");
389 updateButtonConfiguration(state);
390 updateStateDisplay(state);
391 updateProfilePicture(state);
392 invalidateOptionsMenu();
393 }
394 binding.with.setText(account.getRoster().getContact(with).getDisplayName());
395 if (xmppConnectionService.getJingleConnectionManager().fireJingleRtpConnectionStateUpdates()) {
396 return;
397 }
398 if (END_CARD.contains(state) || xmppConnectionService.getJingleConnectionManager().hasMatchingProposal(account, with)) {
399 return;
400 }
401 Log.d(Config.LOGTAG, "restored state (" + state + ") was not an end card. finishing");
402 finish();
403 }
404 }
405
406 private void proposeJingleRtpSession(final Account account, final Jid with, final Set<Media> media) {
407 checkMicrophoneAvailability();
408 if (with.isBareJid()) {
409 xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(account, with, media);
410 } else {
411 final String sessionId = xmppConnectionService.getJingleConnectionManager().initializeRtpSession(account, with, media);
412 initializeActivityWithRunningRtpSession(account, with, sessionId);
413 resetIntent(account, with, sessionId);
414 }
415 putScreenInCallMode(media);
416 }
417
418 @Override
419 public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
420 super.onRequestPermissionsResult(requestCode, permissions, grantResults);
421 if (PermissionUtils.allGranted(grantResults)) {
422 if (requestCode == REQUEST_ACCEPT_CALL) {
423 checkRecorderAndAcceptCall();
424 }
425 } else {
426 @StringRes int res;
427 final String firstDenied = getFirstDenied(grantResults, permissions);
428 if (Manifest.permission.RECORD_AUDIO.equals(firstDenied)) {
429 res = R.string.no_microphone_permission;
430 } else if (Manifest.permission.CAMERA.equals(firstDenied)) {
431 res = R.string.no_camera_permission;
432 } else {
433 throw new IllegalStateException("Invalid permission result request");
434 }
435 Toast.makeText(this, getString(res, getString(R.string.app_name)), Toast.LENGTH_SHORT).show();
436 }
437 }
438
439 @Override
440 public void onStart() {
441 super.onStart();
442 mHandler.postDelayed(mTickExecutor, CALL_DURATION_UPDATE_INTERVAL);
443 }
444
445 @Override
446 public void onStop() {
447 mHandler.removeCallbacks(mTickExecutor);
448 binding.remoteVideo.release();
449 binding.localVideo.release();
450 final WeakReference<JingleRtpConnection> weakReference = this.rtpConnectionReference;
451 final JingleRtpConnection jingleRtpConnection = weakReference == null ? null : weakReference.get();
452 if (jingleRtpConnection != null) {
453 releaseVideoTracks(jingleRtpConnection);
454 }
455 releaseProximityWakeLock();
456 super.onStop();
457 }
458
459 private void releaseVideoTracks(final JingleRtpConnection jingleRtpConnection) {
460 final Optional<VideoTrack> remoteVideo = jingleRtpConnection.getRemoteVideoTrack();
461 if (remoteVideo.isPresent()) {
462 remoteVideo.get().removeSink(binding.remoteVideo);
463 }
464 final Optional<VideoTrack> localVideo = jingleRtpConnection.getLocalVideoTrack();
465 if (localVideo.isPresent()) {
466 localVideo.get().removeSink(binding.localVideo);
467 }
468 }
469
470 @Override
471 public void onBackPressed() {
472 if (isConnected()) {
473 if (switchToPictureInPicture()) {
474 return;
475 }
476 } else {
477 endCall();
478 }
479 super.onBackPressed();
480 }
481
482 @Override
483 public void onUserLeaveHint() {
484 super.onUserLeaveHint();
485 if (switchToPictureInPicture()) {
486 return;
487 }
488 //TODO apparently this method is not getting called on Android 10 when using the task switcher
489 final boolean emptyReference = rtpConnectionReference == null || rtpConnectionReference.get() == null;
490 if (emptyReference && xmppConnectionService != null) {
491 retractSessionProposal();
492 }
493 }
494
495 private boolean isConnected() {
496 final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
497 return connection != null && connection.getEndUserState() == RtpEndUserState.CONNECTED;
498 }
499
500 private boolean switchToPictureInPicture() {
501 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && deviceSupportsPictureInPicture()) {
502 if (shouldBePictureInPicture()) {
503 startPictureInPicture();
504 return true;
505 }
506 }
507 return false;
508 }
509
510 @RequiresApi(api = Build.VERSION_CODES.O)
511 private void startPictureInPicture() {
512 try {
513 enterPictureInPictureMode(
514 new PictureInPictureParams.Builder()
515 .setAspectRatio(new Rational(10, 16))
516 .build()
517 );
518 } catch (final IllegalStateException e) {
519 //this sometimes happens on Samsung phones (possibly when Knox is enabled)
520 Log.w(Config.LOGTAG, "unable to enter picture in picture mode", e);
521 }
522 }
523
524 private boolean deviceSupportsPictureInPicture() {
525 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
526 return getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
527 } else {
528 return false;
529 }
530 }
531
532 private boolean shouldBePictureInPicture() {
533 try {
534 final JingleRtpConnection rtpConnection = requireRtpConnection();
535 return rtpConnection.getMedia().contains(Media.VIDEO) && Arrays.asList(
536 RtpEndUserState.ACCEPTING_CALL,
537 RtpEndUserState.CONNECTING,
538 RtpEndUserState.CONNECTED
539 ).contains(rtpConnection.getEndUserState());
540 } catch (final IllegalStateException e) {
541 return false;
542 }
543 }
544
545 private boolean initializeActivityWithRunningRtpSession(final Account account, Jid with, String sessionId) {
546 final WeakReference<JingleRtpConnection> reference = xmppConnectionService.getJingleConnectionManager()
547 .findJingleRtpConnection(account, with, sessionId);
548 if (reference == null || reference.get() == null) {
549 final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession = xmppConnectionService
550 .getJingleConnectionManager().getTerminalSessionState(with, sessionId);
551 if (terminatedRtpSession == null) {
552 throw new IllegalStateException("failed to initialize activity with running rtp session. session not found");
553 }
554 initializeWithTerminatedSessionState(account, with, terminatedRtpSession);
555 return true;
556 }
557 this.rtpConnectionReference = reference;
558 final RtpEndUserState currentState = requireRtpConnection().getEndUserState();
559 if (currentState == RtpEndUserState.ENDED) {
560 reference.get().throwStateTransitionException();
561 finish();
562 return true;
563 }
564 final Set<Media> media = getMedia();
565 if (currentState == RtpEndUserState.INCOMING_CALL) {
566 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
567 }
568 if (JingleRtpConnection.STATES_SHOWING_ONGOING_CALL.contains(requireRtpConnection().getState())) {
569 putScreenInCallMode();
570 }
571 binding.with.setText(getWith().getDisplayName());
572 updateVideoViews(currentState);
573 updateStateDisplay(currentState, media);
574 updateButtonConfiguration(currentState, media);
575 updateProfilePicture(currentState);
576 invalidateOptionsMenu();
577 return false;
578 }
579
580 private void initializeWithTerminatedSessionState(final Account account, final Jid with, final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession) {
581 Log.d(Config.LOGTAG, "initializeWithTerminatedSessionState()");
582 if (terminatedRtpSession.state == RtpEndUserState.ENDED) {
583 finish();
584 return;
585 }
586 RtpEndUserState state = terminatedRtpSession.state;
587 resetIntent(account, with, terminatedRtpSession.state, terminatedRtpSession.media);
588 updateButtonConfiguration(state);
589 updateStateDisplay(state);
590 updateProfilePicture(state);
591 updateCallDuration();
592 invalidateOptionsMenu();
593 binding.with.setText(account.getRoster().getContact(with).getDisplayName());
594 }
595
596 private void reInitializeActivityWithRunningRtpSession(final Account account, Jid with, String sessionId) {
597 runOnUiThread(() -> initializeActivityWithRunningRtpSession(account, with, sessionId));
598 resetIntent(account, with, sessionId);
599 }
600
601 private void resetIntent(final Account account, final Jid with, final String sessionId) {
602 final Intent intent = new Intent(Intent.ACTION_VIEW);
603 intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
604 intent.putExtra(EXTRA_WITH, with.toEscapedString());
605 intent.putExtra(EXTRA_SESSION_ID, sessionId);
606 setIntent(intent);
607 }
608
609 private void ensureSurfaceViewRendererIsSetup(final SurfaceViewRenderer surfaceViewRenderer) {
610 surfaceViewRenderer.setVisibility(View.VISIBLE);
611 try {
612 surfaceViewRenderer.init(requireRtpConnection().getEglBaseContext(), null);
613 } catch (IllegalStateException e) {
614 Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized");
615 }
616 surfaceViewRenderer.setEnableHardwareScaler(true);
617 }
618
619 private void updateStateDisplay(final RtpEndUserState state) {
620 updateStateDisplay(state, Collections.emptySet());
621 }
622
623 private void updateStateDisplay(final RtpEndUserState state, final Set<Media> media) {
624 switch (state) {
625 case INCOMING_CALL:
626 Preconditions.checkArgument(media.size() > 0, "Media must not be empty");
627 if (media.contains(Media.VIDEO)) {
628 setTitle(R.string.rtp_state_incoming_video_call);
629 } else {
630 setTitle(R.string.rtp_state_incoming_call);
631 }
632 break;
633 case CONNECTING:
634 setTitle(R.string.rtp_state_connecting);
635 break;
636 case CONNECTED:
637 setTitle(R.string.rtp_state_connected);
638 break;
639 case ACCEPTING_CALL:
640 setTitle(R.string.rtp_state_accepting_call);
641 break;
642 case ENDING_CALL:
643 setTitle(R.string.rtp_state_ending_call);
644 break;
645 case FINDING_DEVICE:
646 setTitle(R.string.rtp_state_finding_device);
647 break;
648 case RINGING:
649 setTitle(R.string.rtp_state_ringing);
650 break;
651 case DECLINED_OR_BUSY:
652 setTitle(R.string.rtp_state_declined_or_busy);
653 break;
654 case CONNECTIVITY_ERROR:
655 setTitle(R.string.rtp_state_connectivity_error);
656 break;
657 case CONNECTIVITY_LOST_ERROR:
658 setTitle(R.string.rtp_state_connectivity_lost_error);
659 break;
660 case RETRACTED:
661 setTitle(R.string.rtp_state_retracted);
662 break;
663 case APPLICATION_ERROR:
664 setTitle(R.string.rtp_state_application_failure);
665 break;
666 case ENDED:
667 throw new IllegalStateException("Activity should have called finishAndReleaseWakeLock();");
668 default:
669 throw new IllegalStateException(String.format("State %s has not been handled in UI", state));
670 }
671 }
672
673 private void updateProfilePicture(final RtpEndUserState state) {
674 updateProfilePicture(state, null);
675 }
676
677 private void updateProfilePicture(final RtpEndUserState state, final Contact contact) {
678 if (state == RtpEndUserState.INCOMING_CALL || state == RtpEndUserState.ACCEPTING_CALL) {
679 final boolean show = getResources().getBoolean(R.bool.show_avatar_incoming_call);
680 if (show) {
681 binding.contactPhoto.setVisibility(View.VISIBLE);
682 if (contact == null) {
683 AvatarWorkerTask.loadAvatar(getWith(), binding.contactPhoto, R.dimen.publish_avatar_size);
684 } else {
685 AvatarWorkerTask.loadAvatar(contact, binding.contactPhoto, R.dimen.publish_avatar_size);
686 }
687 } else {
688 binding.contactPhoto.setVisibility(View.GONE);
689 }
690 } else {
691 binding.contactPhoto.setVisibility(View.GONE);
692 }
693 }
694
695 private Set<Media> getMedia() {
696 return requireRtpConnection().getMedia();
697 }
698
699 private void updateButtonConfiguration(final RtpEndUserState state) {
700 updateButtonConfiguration(state, Collections.emptySet());
701 }
702
703 @SuppressLint("RestrictedApi")
704 private void updateButtonConfiguration(final RtpEndUserState state, final Set<Media> media) {
705 if (state == RtpEndUserState.ENDING_CALL || isPictureInPicture()) {
706 this.binding.rejectCall.setVisibility(View.INVISIBLE);
707 this.binding.endCall.setVisibility(View.INVISIBLE);
708 this.binding.acceptCall.setVisibility(View.INVISIBLE);
709 } else if (state == RtpEndUserState.INCOMING_CALL) {
710 this.binding.rejectCall.setContentDescription(getString(R.string.dismiss_call));
711 this.binding.rejectCall.setOnClickListener(this::rejectCall);
712 this.binding.rejectCall.setImageResource(R.drawable.ic_call_end_white_48dp);
713 this.binding.rejectCall.setVisibility(View.VISIBLE);
714 this.binding.endCall.setVisibility(View.INVISIBLE);
715 this.binding.acceptCall.setContentDescription(getString(R.string.answer_call));
716 this.binding.acceptCall.setOnClickListener(this::acceptCall);
717 this.binding.acceptCall.setImageResource(R.drawable.ic_call_white_48dp);
718 this.binding.acceptCall.setVisibility(View.VISIBLE);
719 } else if (state == RtpEndUserState.DECLINED_OR_BUSY) {
720 this.binding.rejectCall.setContentDescription(getString(R.string.exit));
721 this.binding.rejectCall.setOnClickListener(this::exit);
722 this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp);
723 this.binding.rejectCall.setVisibility(View.VISIBLE);
724 this.binding.endCall.setVisibility(View.INVISIBLE);
725 this.binding.acceptCall.setContentDescription(getString(R.string.record_voice_mail));
726 this.binding.acceptCall.setOnClickListener(this::recordVoiceMail);
727 this.binding.acceptCall.setImageResource(R.drawable.ic_voicemail_white_24dp);
728 this.binding.acceptCall.setVisibility(View.VISIBLE);
729 } else if (asList(
730 RtpEndUserState.CONNECTIVITY_ERROR,
731 RtpEndUserState.CONNECTIVITY_LOST_ERROR,
732 RtpEndUserState.APPLICATION_ERROR,
733 RtpEndUserState.RETRACTED
734 ).contains(state)) {
735 this.binding.rejectCall.setContentDescription(getString(R.string.exit));
736 this.binding.rejectCall.setOnClickListener(this::exit);
737 this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp);
738 this.binding.rejectCall.setVisibility(View.VISIBLE);
739 this.binding.endCall.setVisibility(View.INVISIBLE);
740 this.binding.acceptCall.setContentDescription(getString(R.string.try_again));
741 this.binding.acceptCall.setOnClickListener(this::retry);
742 this.binding.acceptCall.setImageResource(R.drawable.ic_replay_white_48dp);
743 this.binding.acceptCall.setVisibility(View.VISIBLE);
744 } else {
745 this.binding.rejectCall.setVisibility(View.INVISIBLE);
746 this.binding.endCall.setContentDescription(getString(R.string.hang_up));
747 this.binding.endCall.setOnClickListener(this::endCall);
748 this.binding.endCall.setImageResource(R.drawable.ic_call_end_white_48dp);
749 this.binding.endCall.setVisibility(View.VISIBLE);
750 this.binding.acceptCall.setVisibility(View.INVISIBLE);
751 }
752 updateInCallButtonConfiguration(state, media);
753 }
754
755 private boolean isPictureInPicture() {
756 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
757 return isInPictureInPictureMode();
758 } else {
759 return false;
760 }
761 }
762
763 private void updateInCallButtonConfiguration() {
764 updateInCallButtonConfiguration(requireRtpConnection().getEndUserState(), requireRtpConnection().getMedia());
765 }
766
767 @SuppressLint("RestrictedApi")
768 private void updateInCallButtonConfiguration(final RtpEndUserState state, final Set<Media> media) {
769 if (state == RtpEndUserState.CONNECTED && !isPictureInPicture()) {
770 Preconditions.checkArgument(media.size() > 0, "Media must not be empty");
771 if (media.contains(Media.VIDEO)) {
772 final JingleRtpConnection rtpConnection = requireRtpConnection();
773 updateInCallButtonConfigurationVideo(rtpConnection.isVideoEnabled(), rtpConnection.isCameraSwitchable());
774 } else {
775 final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager();
776 updateInCallButtonConfigurationSpeaker(
777 audioManager.getSelectedAudioDevice(),
778 audioManager.getAudioDevices().size()
779 );
780 this.binding.inCallActionFarRight.setVisibility(View.GONE);
781 }
782 if (media.contains(Media.AUDIO)) {
783 updateInCallButtonConfigurationMicrophone(requireRtpConnection().isMicrophoneEnabled());
784 } else {
785 this.binding.inCallActionLeft.setVisibility(View.GONE);
786 }
787 } else {
788 this.binding.inCallActionLeft.setVisibility(View.GONE);
789 this.binding.inCallActionRight.setVisibility(View.GONE);
790 this.binding.inCallActionFarRight.setVisibility(View.GONE);
791 }
792 }
793
794 @SuppressLint("RestrictedApi")
795 private void updateInCallButtonConfigurationSpeaker(final AppRTCAudioManager.AudioDevice selectedAudioDevice, final int numberOfChoices) {
796 switch (selectedAudioDevice) {
797 case EARPIECE:
798 this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_off_black_24dp);
799 if (numberOfChoices >= 2) {
800 this.binding.inCallActionRight.setOnClickListener(this::switchToSpeaker);
801 } else {
802 this.binding.inCallActionRight.setOnClickListener(null);
803 this.binding.inCallActionRight.setClickable(false);
804 }
805 break;
806 case WIRED_HEADSET:
807 this.binding.inCallActionRight.setImageResource(R.drawable.ic_headset_black_24dp);
808 this.binding.inCallActionRight.setOnClickListener(null);
809 this.binding.inCallActionRight.setClickable(false);
810 break;
811 case SPEAKER_PHONE:
812 this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_up_black_24dp);
813 if (numberOfChoices >= 2) {
814 this.binding.inCallActionRight.setOnClickListener(this::switchToEarpiece);
815 } else {
816 this.binding.inCallActionRight.setOnClickListener(null);
817 this.binding.inCallActionRight.setClickable(false);
818 }
819 break;
820 case BLUETOOTH:
821 this.binding.inCallActionRight.setImageResource(R.drawable.ic_bluetooth_audio_black_24dp);
822 this.binding.inCallActionRight.setOnClickListener(null);
823 this.binding.inCallActionRight.setClickable(false);
824 break;
825 }
826 this.binding.inCallActionRight.setVisibility(View.VISIBLE);
827 }
828
829 @SuppressLint("RestrictedApi")
830 private void updateInCallButtonConfigurationVideo(final boolean videoEnabled, final boolean isCameraSwitchable) {
831 this.binding.inCallActionRight.setVisibility(View.VISIBLE);
832 if (isCameraSwitchable) {
833 this.binding.inCallActionFarRight.setImageResource(R.drawable.ic_flip_camera_android_black_24dp);
834 this.binding.inCallActionFarRight.setVisibility(View.VISIBLE);
835 this.binding.inCallActionFarRight.setOnClickListener(this::switchCamera);
836 } else {
837 this.binding.inCallActionFarRight.setVisibility(View.GONE);
838 }
839 if (videoEnabled) {
840 this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_black_24dp);
841 this.binding.inCallActionRight.setOnClickListener(this::disableVideo);
842 } else {
843 this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_off_black_24dp);
844 this.binding.inCallActionRight.setOnClickListener(this::enableVideo);
845 }
846 }
847
848 private void switchCamera(final View view) {
849 Futures.addCallback(requireRtpConnection().switchCamera(), new FutureCallback<Boolean>() {
850 @Override
851 public void onSuccess(@NullableDecl Boolean isFrontCamera) {
852 binding.localVideo.setMirror(isFrontCamera);
853 }
854
855 @Override
856 public void onFailure(@NonNull final Throwable throwable) {
857 Log.d(Config.LOGTAG, "could not switch camera", Throwables.getRootCause(throwable));
858 Toast.makeText(RtpSessionActivity.this, R.string.could_not_switch_camera, Toast.LENGTH_LONG).show();
859 }
860 }, MainThreadExecutor.getInstance());
861 }
862
863 private void enableVideo(View view) {
864 requireRtpConnection().setVideoEnabled(true);
865 updateInCallButtonConfigurationVideo(true, requireRtpConnection().isCameraSwitchable());
866 }
867
868 private void disableVideo(View view) {
869 requireRtpConnection().setVideoEnabled(false);
870 updateInCallButtonConfigurationVideo(false, requireRtpConnection().isCameraSwitchable());
871
872 }
873
874 @SuppressLint("RestrictedApi")
875 private void updateInCallButtonConfigurationMicrophone(final boolean microphoneEnabled) {
876 if (microphoneEnabled) {
877 this.binding.inCallActionLeft.setImageResource(R.drawable.ic_mic_black_24dp);
878 this.binding.inCallActionLeft.setOnClickListener(this::disableMicrophone);
879 } else {
880 this.binding.inCallActionLeft.setImageResource(R.drawable.ic_mic_off_black_24dp);
881 this.binding.inCallActionLeft.setOnClickListener(this::enableMicrophone);
882 }
883 this.binding.inCallActionLeft.setVisibility(View.VISIBLE);
884 }
885
886 private void updateCallDuration() {
887 final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
888 if (connection == null || connection.getMedia().contains(Media.VIDEO)) {
889 this.binding.duration.setVisibility(View.GONE);
890 return;
891 }
892 final long rtpConnectionStarted = connection.getRtpConnectionStarted();
893 final long rtpConnectionEnded = connection.getRtpConnectionEnded();
894 if (rtpConnectionStarted != 0) {
895 final long ended = rtpConnectionEnded == 0 ? SystemClock.elapsedRealtime() : rtpConnectionEnded;
896 this.binding.duration.setText(TimeFrameUtils.formatTimePassed(rtpConnectionStarted, ended, false));
897 this.binding.duration.setVisibility(View.VISIBLE);
898 } else {
899 this.binding.duration.setVisibility(View.GONE);
900 }
901 }
902
903 private void updateVideoViews(final RtpEndUserState state) {
904 if (END_CARD.contains(state) || state == RtpEndUserState.ENDING_CALL) {
905 binding.localVideo.setVisibility(View.GONE);
906 binding.localVideo.release();
907 binding.remoteVideo.setVisibility(View.GONE);
908 binding.remoteVideo.release();
909 binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
910 if (isPictureInPicture()) {
911 binding.appBarLayout.setVisibility(View.GONE);
912 binding.pipPlaceholder.setVisibility(View.VISIBLE);
913 if (state == RtpEndUserState.APPLICATION_ERROR || state == RtpEndUserState.CONNECTIVITY_ERROR) {
914 binding.pipWarning.setVisibility(View.VISIBLE);
915 binding.pipWaiting.setVisibility(View.GONE);
916 } else {
917 binding.pipWarning.setVisibility(View.GONE);
918 binding.pipWaiting.setVisibility(View.GONE);
919 }
920 } else {
921 binding.appBarLayout.setVisibility(View.VISIBLE);
922 binding.pipPlaceholder.setVisibility(View.GONE);
923 }
924 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
925 return;
926 }
927 if (isPictureInPicture() && (state == RtpEndUserState.CONNECTING || state == RtpEndUserState.ACCEPTING_CALL)) {
928 binding.localVideo.setVisibility(View.GONE);
929 binding.remoteVideo.setVisibility(View.GONE);
930 binding.appBarLayout.setVisibility(View.GONE);
931 binding.pipPlaceholder.setVisibility(View.VISIBLE);
932 binding.pipWarning.setVisibility(View.GONE);
933 binding.pipWaiting.setVisibility(View.VISIBLE);
934 binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
935 return;
936 }
937 final Optional<VideoTrack> localVideoTrack = getLocalVideoTrack();
938 if (localVideoTrack.isPresent() && !isPictureInPicture()) {
939 ensureSurfaceViewRendererIsSetup(binding.localVideo);
940 //paint local view over remote view
941 binding.localVideo.setZOrderMediaOverlay(true);
942 binding.localVideo.setMirror(requireRtpConnection().isFrontCamera());
943 addSink(localVideoTrack.get(), binding.localVideo);
944 } else {
945 binding.localVideo.setVisibility(View.GONE);
946 }
947 final Optional<VideoTrack> remoteVideoTrack = getRemoteVideoTrack();
948 if (remoteVideoTrack.isPresent()) {
949 ensureSurfaceViewRendererIsSetup(binding.remoteVideo);
950 addSink(remoteVideoTrack.get(), binding.remoteVideo);
951 if (state == RtpEndUserState.CONNECTED) {
952 binding.appBarLayout.setVisibility(View.GONE);
953 getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
954 } else {
955 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
956 binding.remoteVideo.setVisibility(View.GONE);
957 }
958 if (isPictureInPicture() && !requireRtpConnection().isMicrophoneEnabled()) {
959 binding.pipLocalMicOffIndicator.setVisibility(View.VISIBLE);
960 } else {
961 binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
962 }
963 } else {
964 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
965 binding.remoteVideo.setVisibility(View.GONE);
966 binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
967 }
968 }
969
970 private Optional<VideoTrack> getLocalVideoTrack() {
971 final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
972 if (connection == null) {
973 return Optional.absent();
974 }
975 return connection.getLocalVideoTrack();
976 }
977
978 private Optional<VideoTrack> getRemoteVideoTrack() {
979 final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
980 if (connection == null) {
981 return Optional.absent();
982 }
983 return connection.getRemoteVideoTrack();
984 }
985
986 private void disableMicrophone(View view) {
987 final JingleRtpConnection rtpConnection = requireRtpConnection();
988 if (rtpConnection.setMicrophoneEnabled(false)) {
989 updateInCallButtonConfiguration();
990 }
991 }
992
993 private void enableMicrophone(View view) {
994 final JingleRtpConnection rtpConnection = requireRtpConnection();
995 if (rtpConnection.setMicrophoneEnabled(true)) {
996 updateInCallButtonConfiguration();
997 }
998 }
999
1000 private void switchToEarpiece(View view) {
1001 requireRtpConnection().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE);
1002 acquireProximityWakeLock();
1003 }
1004
1005 private void switchToSpeaker(View view) {
1006 requireRtpConnection().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE);
1007 releaseProximityWakeLock();
1008 }
1009
1010 private void retry(View view) {
1011 final Intent intent = getIntent();
1012 final Account account = extractAccount(intent);
1013 final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
1014 final String lastAction = intent.getStringExtra(EXTRA_LAST_ACTION);
1015 final String action = intent.getAction();
1016 final Set<Media> media = actionToMedia(lastAction == null ? action : lastAction);
1017 this.rtpConnectionReference = null;
1018 Log.d(Config.LOGTAG, "attempting retry with " + with.toEscapedString());
1019 proposeJingleRtpSession(account, with, media);
1020 }
1021
1022 private void exit(final View view) {
1023 finish();
1024 }
1025
1026 private void recordVoiceMail(final View view) {
1027 final Intent intent = getIntent();
1028 final Account account = extractAccount(intent);
1029 final Jid with = Jid.ofEscaped(intent.getStringExtra(EXTRA_WITH));
1030 final Conversation conversation = xmppConnectionService.findOrCreateConversation(account, with, false, true);
1031 final Intent launchIntent = new Intent(this, ConversationsActivity.class);
1032 launchIntent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
1033 launchIntent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid());
1034 launchIntent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_CLEAR_TOP);
1035 launchIntent.putExtra(ConversationsActivity.EXTRA_POST_INIT_ACTION, ConversationsActivity.POST_ACTION_RECORD_VOICE);
1036 startActivity(launchIntent);
1037 finish();
1038 }
1039
1040 private Contact getWith() {
1041 final AbstractJingleConnection.Id id = requireRtpConnection().getId();
1042 final Account account = id.account;
1043 return account.getRoster().getContact(id.with);
1044 }
1045
1046 private JingleRtpConnection requireRtpConnection() {
1047 final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
1048 if (connection == null) {
1049 throw new IllegalStateException("No RTP connection found");
1050 }
1051 return connection;
1052 }
1053
1054 @Override
1055 public void onJingleRtpConnectionUpdate(Account account, Jid with, final String sessionId, RtpEndUserState state) {
1056 Log.d(Config.LOGTAG, "onJingleRtpConnectionUpdate(" + state + ")");
1057 if (END_CARD.contains(state)) {
1058 Log.d(Config.LOGTAG, "end card reached");
1059 releaseProximityWakeLock();
1060 runOnUiThread(() -> getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON));
1061 }
1062 if (with.isBareJid()) {
1063 updateRtpSessionProposalState(account, with, state);
1064 return;
1065 }
1066 if (this.rtpConnectionReference == null) {
1067 if (END_CARD.contains(state)) {
1068 Log.d(Config.LOGTAG, "not reinitializing session");
1069 return;
1070 }
1071 //this happens when going from proposed session to actual session
1072 reInitializeActivityWithRunningRtpSession(account, with, sessionId);
1073 return;
1074 }
1075 final AbstractJingleConnection.Id id = requireRtpConnection().getId();
1076 final Set<Media> media = getMedia();
1077 final Contact contact = getWith();
1078 if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) {
1079 if (state == RtpEndUserState.ENDED) {
1080 finish();
1081 return;
1082 }
1083 runOnUiThread(() -> {
1084 updateStateDisplay(state, media);
1085 updateButtonConfiguration(state, media);
1086 updateVideoViews(state);
1087 updateProfilePicture(state, contact);
1088 invalidateOptionsMenu();
1089 });
1090 if (END_CARD.contains(state)) {
1091 final JingleRtpConnection rtpConnection = requireRtpConnection();
1092 resetIntent(account, with, state, rtpConnection.getMedia());
1093 releaseVideoTracks(rtpConnection);
1094 this.rtpConnectionReference = null;
1095 }
1096 } else {
1097 Log.d(Config.LOGTAG, "received update for other rtp session");
1098 }
1099 }
1100
1101 @Override
1102 public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
1103 Log.d(Config.LOGTAG, "onAudioDeviceChanged in activity: selected:" + selectedAudioDevice + ", available:" + availableAudioDevices);
1104 try {
1105 if (getMedia().contains(Media.VIDEO)) {
1106 Log.d(Config.LOGTAG, "nothing to do; in video mode");
1107 return;
1108 }
1109 final RtpEndUserState endUserState = requireRtpConnection().getEndUserState();
1110 if (endUserState == RtpEndUserState.CONNECTED) {
1111 final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager();
1112 updateInCallButtonConfigurationSpeaker(
1113 audioManager.getSelectedAudioDevice(),
1114 audioManager.getAudioDevices().size()
1115 );
1116 } else if (END_CARD.contains(endUserState)) {
1117 Log.d(Config.LOGTAG, "onAudioDeviceChanged() nothing to do because end card has been reached");
1118 } else {
1119 putProximityWakeLockInProperState(selectedAudioDevice);
1120 }
1121 } catch (IllegalStateException e) {
1122 Log.d(Config.LOGTAG, "RTP connection was not available when audio device changed");
1123 }
1124 }
1125
1126 private void updateRtpSessionProposalState(final Account account, final Jid with, final RtpEndUserState state) {
1127 final Intent currentIntent = getIntent();
1128 final String withExtra = currentIntent == null ? null : currentIntent.getStringExtra(EXTRA_WITH);
1129 if (withExtra == null) {
1130 return;
1131 }
1132 if (Jid.ofEscaped(withExtra).asBareJid().equals(with)) {
1133 runOnUiThread(() -> {
1134 updateStateDisplay(state);
1135 updateButtonConfiguration(state);
1136 updateProfilePicture(state);
1137 invalidateOptionsMenu();
1138 });
1139 resetIntent(account, with, state, actionToMedia(currentIntent.getAction()));
1140 }
1141 }
1142
1143 private void resetIntent(final Bundle extras) {
1144 final Intent intent = new Intent(Intent.ACTION_VIEW);
1145 intent.putExtras(extras);
1146 setIntent(intent);
1147 }
1148
1149 private void resetIntent(final Account account, Jid with, final RtpEndUserState state, final Set<Media> media) {
1150 final Intent intent = new Intent(Intent.ACTION_VIEW);
1151 intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
1152 if (account.getRoster().getContact(with).getPresences().anySupport(Namespace.JINGLE_MESSAGE)) {
1153 intent.putExtra(EXTRA_WITH, with.asBareJid().toEscapedString());
1154 } else {
1155 intent.putExtra(EXTRA_WITH, with.toEscapedString());
1156 }
1157 intent.putExtra(EXTRA_LAST_REPORTED_STATE, state.toString());
1158 intent.putExtra(EXTRA_LAST_ACTION, media.contains(Media.VIDEO) ? ACTION_MAKE_VIDEO_CALL : ACTION_MAKE_VOICE_CALL);
1159 setIntent(intent);
1160 }
1161}