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