1package eu.siacs.conversations.ui;
2
3import android.Manifest;
4import android.annotation.SuppressLint;
5import android.app.PictureInPictureParams;
6import android.content.Context;
7import android.content.Intent;
8import android.databinding.DataBindingUtil;
9import android.os.Build;
10import android.os.Bundle;
11import android.os.PowerManager;
12import android.os.SystemClock;
13import android.support.annotation.NonNull;
14import android.support.annotation.StringRes;
15import android.util.Log;
16import android.util.Rational;
17import android.view.View;
18import android.view.WindowManager;
19import android.widget.Toast;
20
21import com.google.common.base.Optional;
22import com.google.common.collect.ImmutableList;
23import com.google.common.collect.ImmutableSet;
24
25import org.webrtc.SurfaceViewRenderer;
26import org.webrtc.VideoTrack;
27
28import java.lang.ref.WeakReference;
29import java.util.Arrays;
30import java.util.List;
31import java.util.Set;
32
33import eu.siacs.conversations.Config;
34import eu.siacs.conversations.R;
35import eu.siacs.conversations.databinding.ActivityRtpSessionBinding;
36import eu.siacs.conversations.entities.Account;
37import eu.siacs.conversations.entities.Contact;
38import eu.siacs.conversations.services.AppRTCAudioManager;
39import eu.siacs.conversations.services.XmppConnectionService;
40import eu.siacs.conversations.utils.PermissionUtils;
41import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
42import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
43import eu.siacs.conversations.xmpp.jingle.Media;
44import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
45import rocks.xmpp.addr.Jid;
46
47import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
48import static java.util.Arrays.asList;
49
50public class RtpSessionActivity extends XmppActivity implements XmppConnectionService.OnJingleRtpConnectionUpdate {
51
52 public static final String EXTRA_WITH = "with";
53 public static final String EXTRA_SESSION_ID = "session_id";
54 public static final String EXTRA_LAST_REPORTED_STATE = "last_reported_state";
55 public static final String EXTRA_LAST_ACTION = "last_action";
56 public static final String ACTION_ACCEPT_CALL = "action_accept_call";
57 public static final String ACTION_MAKE_VOICE_CALL = "action_make_voice_call";
58 public static final String ACTION_MAKE_VIDEO_CALL = "action_make_video_call";
59 private static final List<RtpEndUserState> END_CARD = Arrays.asList(
60 RtpEndUserState.APPLICATION_ERROR,
61 RtpEndUserState.DECLINED_OR_BUSY,
62 RtpEndUserState.CONNECTIVITY_ERROR
63 );
64 private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session";
65 private static final int REQUEST_ACCEPT_CALL = 0x1111;
66 private WeakReference<JingleRtpConnection> rtpConnectionReference;
67
68 private ActivityRtpSessionBinding binding;
69 private PowerManager.WakeLock mProximityWakeLock;
70
71 private static Set<Media> actionToMedia(final String action) {
72 if (ACTION_MAKE_VIDEO_CALL.equals(action)) {
73 return ImmutableSet.of(Media.AUDIO, Media.VIDEO);
74 } else {
75 return ImmutableSet.of(Media.AUDIO);
76 }
77 }
78
79 @Override
80 public void onCreate(Bundle savedInstanceState) {
81 super.onCreate(savedInstanceState);
82 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
83 | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
84 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
85 | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
86 this.binding = DataBindingUtil.setContentView(this, R.layout.activity_rtp_session);
87 setSupportActionBar(binding.toolbar);
88 }
89
90 @Override
91 public void onStart() {
92 super.onStart();
93 Log.d(Config.LOGTAG, "RtpSessionActivity.onStart()");
94 }
95
96 private void endCall(View view) {
97 endCall();
98 }
99
100 private void endCall() {
101 if (this.rtpConnectionReference == null) {
102 final Intent intent = getIntent();
103 final Account account = extractAccount(intent);
104 final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH));
105 xmppConnectionService.getJingleConnectionManager().retractSessionProposal(account, with.asBareJid());
106 finish();
107 } else {
108 requireRtpConnection().endCall();
109 }
110 }
111
112 private void rejectCall(View view) {
113 requireRtpConnection().rejectCall();
114 finish();
115 }
116
117 private void acceptCall(View view) {
118 requestPermissionsAndAcceptCall();
119 }
120
121 private void requestPermissionsAndAcceptCall() {
122 final List<String> permissions;
123 if (getMedia().contains(Media.VIDEO)) {
124 permissions = ImmutableList.of(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO);
125 } else {
126 permissions = ImmutableList.of(Manifest.permission.RECORD_AUDIO);
127 }
128 if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CALL)) {
129 putScreenInCallMode();
130 checkRecorderAndAcceptCall();
131 }
132 }
133
134 private void checkRecorderAndAcceptCall() {
135 checkMicrophoneAvailability();
136 requireRtpConnection().acceptCall();
137 }
138
139 private void checkMicrophoneAvailability() {
140 new Thread(() -> {
141 final long start = SystemClock.elapsedRealtime();
142 final boolean isMicrophoneAvailable = AppRTCAudioManager.isMicrophoneAvailable();
143 final long stop = SystemClock.elapsedRealtime();
144 Log.d(Config.LOGTAG, "checking microphone availability took " + (stop - start) + "ms");
145 if (isMicrophoneAvailable) {
146 return;
147 }
148 runOnUiThread(() -> Toast.makeText(this, R.string.microphone_unavailable, Toast.LENGTH_SHORT).show());
149 }
150 ).start();
151 }
152
153 private void putScreenInCallMode() {
154 putScreenInCallMode(requireRtpConnection().getMedia());
155 }
156
157 private void putScreenInCallMode(final Set<Media> media) {
158 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
159 if (!media.contains(Media.VIDEO)) {
160 final JingleRtpConnection rtpConnection = rtpConnectionReference != null ? rtpConnectionReference.get() : null;
161 final AppRTCAudioManager audioManager = rtpConnection == null ? null : rtpConnection.getAudioManager();
162 if (audioManager == null || audioManager.getSelectedAudioDevice() == AppRTCAudioManager.AudioDevice.EARPIECE) {
163 acquireProximityWakeLock();
164 }
165 }
166 }
167
168 @SuppressLint("WakelockTimeout")
169 private void acquireProximityWakeLock() {
170 final PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
171 if (powerManager == null) {
172 Log.e(Config.LOGTAG, "power manager not available");
173 return;
174 }
175 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
176 if (this.mProximityWakeLock == null) {
177 this.mProximityWakeLock = powerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, PROXIMITY_WAKE_LOCK_TAG);
178 }
179 if (!this.mProximityWakeLock.isHeld()) {
180 Log.d(Config.LOGTAG, "acquiring proximity wake lock");
181 this.mProximityWakeLock.acquire();
182 }
183 }
184 }
185
186 private void releaseProximityWakeLock() {
187 if (this.mProximityWakeLock != null && mProximityWakeLock.isHeld()) {
188 Log.d(Config.LOGTAG, "releasing proximity wake lock");
189 this.mProximityWakeLock.release();
190 this.mProximityWakeLock = null;
191 }
192 }
193
194 private void putProximityWakeLockInProperState() {
195 if (requireRtpConnection().getAudioManager().getSelectedAudioDevice() == AppRTCAudioManager.AudioDevice.EARPIECE) {
196 acquireProximityWakeLock();
197 } else {
198 releaseProximityWakeLock();
199 }
200 }
201
202 @Override
203 protected void refreshUiReal() {
204
205 }
206
207 @Override
208 public void onNewIntent(final Intent intent) {
209 super.onNewIntent(intent);
210 setIntent(intent);
211 if (xmppConnectionService == null) {
212 Log.d(Config.LOGTAG, "RtpSessionActivity: background service wasn't bound in onNewIntent()");
213 return;
214 }
215 final Account account = extractAccount(intent);
216 final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH));
217 final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID);
218 if (sessionId != null) {
219 Log.d(Config.LOGTAG, "reinitializing from onNewIntent()");
220 initializeActivityWithRunningRtpSession(account, with, sessionId);
221 if (ACTION_ACCEPT_CALL.equals(intent.getAction())) {
222 Log.d(Config.LOGTAG, "accepting call from onNewIntent()");
223 requestPermissionsAndAcceptCall();
224 resetIntent(intent.getExtras());
225 }
226 } else {
227 throw new IllegalStateException("received onNewIntent without sessionId");
228 }
229 }
230
231 @Override
232 void onBackendConnected() {
233 final Intent intent = getIntent();
234 final String action = intent.getAction();
235 final Account account = extractAccount(intent);
236 final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH));
237 final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID);
238 if (sessionId != null) {
239 initializeActivityWithRunningRtpSession(account, with, sessionId);
240 if (ACTION_ACCEPT_CALL.equals(intent.getAction())) {
241 Log.d(Config.LOGTAG, "intent action was accept");
242 requestPermissionsAndAcceptCall();
243 resetIntent(intent.getExtras());
244 }
245 } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(action)) {
246 proposeJingleRtpSession(account, with, actionToMedia(action));
247 binding.with.setText(account.getRoster().getContact(with).getDisplayName());
248 } else if (Intent.ACTION_VIEW.equals(action)) {
249 final String extraLastState = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE);
250 if (extraLastState != null) {
251 Log.d(Config.LOGTAG, "restored last state from intent extra");
252 RtpEndUserState state = RtpEndUserState.valueOf(extraLastState);
253 updateButtonConfiguration(state);
254 updateStateDisplay(state);
255 }
256 binding.with.setText(account.getRoster().getContact(with).getDisplayName());
257 }
258 }
259
260 private void proposeJingleRtpSession(final Account account, final Jid with, final Set<Media> media) {
261 checkMicrophoneAvailability();
262 xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(account, with, media);
263 putScreenInCallMode(media);
264 }
265
266 @Override
267 public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
268 super.onRequestPermissionsResult(requestCode, permissions, grantResults);
269 if (PermissionUtils.allGranted(grantResults)) {
270 if (requestCode == REQUEST_ACCEPT_CALL) {
271 checkRecorderAndAcceptCall();
272 }
273 } else {
274 @StringRes int res;
275 final String firstDenied = getFirstDenied(grantResults, permissions);
276 if (Manifest.permission.RECORD_AUDIO.equals(firstDenied)) {
277 res = R.string.no_microphone_permission;
278 } else if (Manifest.permission.CAMERA.equals(firstDenied)) {
279 res = R.string.no_camera_permission;
280 } else {
281 throw new IllegalStateException("Invalid permission result request");
282 }
283 Toast.makeText(this, res, Toast.LENGTH_SHORT).show();
284 }
285 }
286
287 @Override
288 public void onStop() {
289 binding.remoteVideo.release();
290 binding.localVideo.release();
291 final WeakReference<JingleRtpConnection> weakReference = this.rtpConnectionReference;
292 final JingleRtpConnection jingleRtpConnection = weakReference == null ? null : weakReference.get();
293 if (jingleRtpConnection != null) {
294 releaseVideoTracks(jingleRtpConnection);
295 }
296 releaseProximityWakeLock();
297 super.onStop();
298 }
299
300 private void releaseVideoTracks(final JingleRtpConnection jingleRtpConnection) {
301 final Optional<VideoTrack> remoteVideo = jingleRtpConnection.getRemoteVideoTrack();
302 if (remoteVideo.isPresent()) {
303 remoteVideo.get().removeSink(binding.remoteVideo);
304 }
305 final Optional<VideoTrack> localVideo = jingleRtpConnection.geLocalVideoTrack();
306 if (localVideo.isPresent()) {
307 localVideo.get().removeSink(binding.localVideo);
308 }
309 }
310
311 @Override
312 public void onBackPressed() {
313 endCall();
314 super.onBackPressed();
315 }
316
317 @Override
318 public void onUserLeaveHint() {
319 Log.d(Config.LOGTAG, "user leave hint");
320 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
321 if (shouldBePictureInPicture()) {
322 enterPictureInPictureMode(
323 new PictureInPictureParams.Builder()
324 .setAspectRatio(new Rational(10, 16))
325 .build()
326 );
327 }
328 }
329
330 }
331
332 private boolean shouldBePictureInPicture() {
333 try {
334 final JingleRtpConnection rtpConnection = requireRtpConnection();
335 return rtpConnection.getMedia().contains(Media.VIDEO) && Arrays.asList(
336 RtpEndUserState.ACCEPTING_CALL,
337 RtpEndUserState.CONNECTING,
338 RtpEndUserState.CONNECTED
339 ).contains(rtpConnection.getEndUserState());
340 } catch (IllegalStateException e) {
341 return false;
342 }
343 }
344
345 private void initializeActivityWithRunningRtpSession(final Account account, Jid with, String sessionId) {
346 final WeakReference<JingleRtpConnection> reference = xmppConnectionService.getJingleConnectionManager()
347 .findJingleRtpConnection(account, with, sessionId);
348 if (reference == null || reference.get() == null) {
349 finish();
350 return;
351 }
352 this.rtpConnectionReference = reference;
353 final RtpEndUserState currentState = requireRtpConnection().getEndUserState();
354 if (currentState == RtpEndUserState.ENDED) {
355 finish();
356 return;
357 }
358 if (currentState == RtpEndUserState.INCOMING_CALL) {
359 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
360 }
361 if (JingleRtpConnection.STATES_SHOWING_ONGOING_CALL.contains(requireRtpConnection().getState())) {
362 putScreenInCallMode();
363 }
364 binding.with.setText(getWith().getDisplayName());
365 updateVideoViews(currentState);
366 updateStateDisplay(currentState);
367 updateButtonConfiguration(currentState);
368 }
369
370 private void reInitializeActivityWithRunningRapSession(final Account account, Jid with, String sessionId) {
371 runOnUiThread(() -> {
372 initializeActivityWithRunningRtpSession(account, with, sessionId);
373 });
374 final Intent intent = new Intent(Intent.ACTION_VIEW);
375 intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
376 intent.putExtra(EXTRA_WITH, with.toEscapedString());
377 intent.putExtra(EXTRA_SESSION_ID, sessionId);
378 setIntent(intent);
379 }
380
381 private void ensureSurfaceViewRendererIsSetup(final SurfaceViewRenderer surfaceViewRenderer) {
382 surfaceViewRenderer.setVisibility(View.VISIBLE);
383 try {
384 surfaceViewRenderer.init(requireRtpConnection().getEglBaseContext(), null);
385 } catch (IllegalStateException e) {
386 Log.d(Config.LOGTAG, "SurfaceViewRenderer was already initialized");
387 }
388 surfaceViewRenderer.setEnableHardwareScaler(true);
389 }
390
391 private void updateStateDisplay(final RtpEndUserState state) {
392 switch (state) {
393 case INCOMING_CALL:
394 if (getMedia().contains(Media.VIDEO)) {
395 setTitle(R.string.rtp_state_incoming_video_call);
396 } else {
397 setTitle(R.string.rtp_state_incoming_call);
398 }
399 break;
400 case CONNECTING:
401 setTitle(R.string.rtp_state_connecting);
402 break;
403 case CONNECTED:
404 setTitle(R.string.rtp_state_connected);
405 break;
406 case ACCEPTING_CALL:
407 setTitle(R.string.rtp_state_accepting_call);
408 break;
409 case ENDING_CALL:
410 setTitle(R.string.rtp_state_ending_call);
411 break;
412 case FINDING_DEVICE:
413 setTitle(R.string.rtp_state_finding_device);
414 break;
415 case RINGING:
416 setTitle(R.string.rtp_state_ringing);
417 break;
418 case DECLINED_OR_BUSY:
419 setTitle(R.string.rtp_state_declined_or_busy);
420 break;
421 case CONNECTIVITY_ERROR:
422 setTitle(R.string.rtp_state_connectivity_error);
423 break;
424 case APPLICATION_ERROR:
425 setTitle(R.string.rtp_state_application_failure);
426 break;
427 case ENDED:
428 throw new IllegalStateException("Activity should have called finishAndReleaseWakeLock();");
429 default:
430 throw new IllegalStateException(String.format("State %s has not been handled in UI", state));
431 }
432 }
433
434 private Set<Media> getMedia() {
435 return requireRtpConnection().getMedia();
436 }
437
438 @SuppressLint("RestrictedApi")
439 private void updateButtonConfiguration(final RtpEndUserState state) {
440 if (state == RtpEndUserState.ENDING_CALL || isPictureInPicture()) {
441 this.binding.rejectCall.setVisibility(View.INVISIBLE);
442 this.binding.endCall.setVisibility(View.INVISIBLE);
443 this.binding.acceptCall.setVisibility(View.INVISIBLE);
444 } else if (state == RtpEndUserState.INCOMING_CALL) {
445 this.binding.rejectCall.setOnClickListener(this::rejectCall);
446 this.binding.rejectCall.setImageResource(R.drawable.ic_call_end_white_48dp);
447 this.binding.rejectCall.setVisibility(View.VISIBLE);
448 this.binding.endCall.setVisibility(View.INVISIBLE);
449 this.binding.acceptCall.setOnClickListener(this::acceptCall);
450 this.binding.acceptCall.setImageResource(R.drawable.ic_call_white_48dp);
451 this.binding.acceptCall.setVisibility(View.VISIBLE);
452 } else if (state == RtpEndUserState.DECLINED_OR_BUSY) {
453 this.binding.rejectCall.setVisibility(View.INVISIBLE);
454 this.binding.endCall.setOnClickListener(this::exit);
455 this.binding.endCall.setImageResource(R.drawable.ic_clear_white_48dp);
456 this.binding.endCall.setVisibility(View.VISIBLE);
457 this.binding.acceptCall.setVisibility(View.INVISIBLE);
458 } else if (state == RtpEndUserState.CONNECTIVITY_ERROR || state == RtpEndUserState.APPLICATION_ERROR) {
459 this.binding.rejectCall.setOnClickListener(this::exit);
460 this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp);
461 this.binding.rejectCall.setVisibility(View.VISIBLE);
462 this.binding.endCall.setVisibility(View.INVISIBLE);
463 this.binding.acceptCall.setOnClickListener(this::retry);
464 this.binding.acceptCall.setImageResource(R.drawable.ic_replay_white_48dp);
465 this.binding.acceptCall.setVisibility(View.VISIBLE);
466 } else {
467 this.binding.rejectCall.setVisibility(View.INVISIBLE);
468 this.binding.endCall.setOnClickListener(this::endCall);
469 this.binding.endCall.setImageResource(R.drawable.ic_call_end_white_48dp);
470 this.binding.endCall.setVisibility(View.VISIBLE);
471 this.binding.acceptCall.setVisibility(View.INVISIBLE);
472 }
473 updateInCallButtonConfiguration(state);
474 }
475
476 private boolean isPictureInPicture() {
477 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
478 return isInPictureInPictureMode();
479 } else {
480 return false;
481 }
482 }
483
484 private void updateInCallButtonConfiguration() {
485 updateInCallButtonConfiguration(requireRtpConnection().getEndUserState());
486 }
487
488 @SuppressLint("RestrictedApi")
489 private void updateInCallButtonConfiguration(final RtpEndUserState state) {
490 if (state == RtpEndUserState.CONNECTED && !isPictureInPicture()) {
491 if (getMedia().contains(Media.VIDEO)) {
492 updateInCallButtonConfigurationVideo(requireRtpConnection().isVideoEnabled());
493 } else {
494 final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager();
495 updateInCallButtonConfigurationSpeaker(
496 audioManager.getSelectedAudioDevice(),
497 audioManager.getAudioDevices().size()
498 );
499 }
500 updateInCallButtonConfigurationMicrophone(requireRtpConnection().isMicrophoneEnabled());
501 } else {
502 this.binding.inCallActionLeft.setVisibility(View.GONE);
503 this.binding.inCallActionRight.setVisibility(View.GONE);
504 }
505 }
506
507 @SuppressLint("RestrictedApi")
508 private void updateInCallButtonConfigurationSpeaker(final AppRTCAudioManager.AudioDevice selectedAudioDevice, final int numberOfChoices) {
509 switch (selectedAudioDevice) {
510 case EARPIECE:
511 this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_off_black_24dp);
512 if (numberOfChoices >= 2) {
513 this.binding.inCallActionRight.setOnClickListener(this::switchToSpeaker);
514 } else {
515 this.binding.inCallActionRight.setOnClickListener(null);
516 this.binding.inCallActionRight.setClickable(false);
517 }
518 break;
519 case WIRED_HEADSET:
520 this.binding.inCallActionRight.setImageResource(R.drawable.ic_headset_black_24dp);
521 this.binding.inCallActionRight.setOnClickListener(null);
522 this.binding.inCallActionRight.setClickable(false);
523 break;
524 case SPEAKER_PHONE:
525 this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_up_black_24dp);
526 if (numberOfChoices >= 2) {
527 this.binding.inCallActionRight.setOnClickListener(this::switchToEarpiece);
528 } else {
529 this.binding.inCallActionRight.setOnClickListener(null);
530 this.binding.inCallActionRight.setClickable(false);
531 }
532 break;
533 case BLUETOOTH:
534 this.binding.inCallActionRight.setImageResource(R.drawable.ic_bluetooth_audio_black_24dp);
535 this.binding.inCallActionRight.setOnClickListener(null);
536 this.binding.inCallActionRight.setClickable(false);
537 break;
538 }
539 this.binding.inCallActionRight.setVisibility(View.VISIBLE);
540 }
541
542 @SuppressLint("RestrictedApi")
543 private void updateInCallButtonConfigurationVideo(final boolean videoEnabled) {
544 this.binding.inCallActionRight.setVisibility(View.VISIBLE);
545 if (videoEnabled) {
546 this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_black_24dp);
547 this.binding.inCallActionRight.setOnClickListener(this::disableVideo);
548 } else {
549 this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_off_black_24dp);
550 this.binding.inCallActionRight.setOnClickListener(this::enableVideo);
551 }
552 }
553
554 private void enableVideo(View view) {
555 requireRtpConnection().setVideoEnabled(true);
556 updateInCallButtonConfigurationVideo(true);
557 }
558
559 private void disableVideo(View view) {
560 requireRtpConnection().setVideoEnabled(false);
561 updateInCallButtonConfigurationVideo(false);
562
563 }
564
565 @SuppressLint("RestrictedApi")
566 private void updateInCallButtonConfigurationMicrophone(final boolean microphoneEnabled) {
567 if (microphoneEnabled) {
568 this.binding.inCallActionLeft.setImageResource(R.drawable.ic_mic_black_24dp);
569 this.binding.inCallActionLeft.setOnClickListener(this::disableMicrophone);
570 } else {
571 this.binding.inCallActionLeft.setImageResource(R.drawable.ic_mic_off_black_24dp);
572 this.binding.inCallActionLeft.setOnClickListener(this::enableMicrophone);
573 }
574 this.binding.inCallActionLeft.setVisibility(View.VISIBLE);
575 }
576
577 private void updateVideoViews(final RtpEndUserState state) {
578 if (END_CARD.contains(state) || state == RtpEndUserState.ENDING_CALL) {
579 binding.localVideo.setVisibility(View.GONE);
580 binding.remoteVideo.setVisibility(View.GONE);
581 binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
582 if (isPictureInPicture()) {
583 binding.appBarLayout.setVisibility(View.GONE);
584 binding.pipPlaceholder.setVisibility(View.VISIBLE);
585 if (state == RtpEndUserState.APPLICATION_ERROR || state == RtpEndUserState.CONNECTIVITY_ERROR) {
586 binding.pipWarning.setVisibility(View.VISIBLE);
587 binding.pipWaiting.setVisibility(View.GONE);
588 } else {
589 binding.pipWarning.setVisibility(View.GONE);
590 binding.pipWaiting.setVisibility(View.GONE);
591 }
592 } else {
593 binding.appBarLayout.setVisibility(View.VISIBLE);
594 binding.pipPlaceholder.setVisibility(View.GONE);
595 }
596 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
597 return;
598 }
599 if (isPictureInPicture() && (state == RtpEndUserState.CONNECTING || state == RtpEndUserState.ACCEPTING_CALL)) {
600 binding.localVideo.setVisibility(View.GONE);
601 binding.remoteVideo.setVisibility(View.GONE);
602 binding.appBarLayout.setVisibility(View.GONE);
603 binding.pipPlaceholder.setVisibility(View.VISIBLE);
604 binding.pipWarning.setVisibility(View.GONE);
605 binding.pipWaiting.setVisibility(View.VISIBLE);
606 binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
607 return;
608 }
609 final Optional<VideoTrack> localVideoTrack = requireRtpConnection().geLocalVideoTrack();
610 if (localVideoTrack.isPresent() && !isPictureInPicture()) {
611 ensureSurfaceViewRendererIsSetup(binding.localVideo);
612 //paint local view over remote view
613 binding.localVideo.setZOrderMediaOverlay(true);
614 binding.localVideo.setMirror(true);
615 localVideoTrack.get().addSink(binding.localVideo);
616 } else {
617 binding.localVideo.setVisibility(View.GONE);
618 }
619 final Optional<VideoTrack> remoteVideoTrack = requireRtpConnection().getRemoteVideoTrack();
620 if (remoteVideoTrack.isPresent()) {
621 ensureSurfaceViewRendererIsSetup(binding.remoteVideo);
622 remoteVideoTrack.get().addSink(binding.remoteVideo);
623 if (state == RtpEndUserState.CONNECTED) {
624 binding.appBarLayout.setVisibility(View.GONE);
625 getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
626 } else {
627 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
628 binding.remoteVideo.setVisibility(View.GONE);
629 }
630 if (isPictureInPicture() && !requireRtpConnection().isMicrophoneEnabled()) {
631 binding.pipLocalMicOffIndicator.setVisibility(View.VISIBLE);
632 } else {
633 binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
634 }
635 } else {
636 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
637 binding.remoteVideo.setVisibility(View.GONE);
638 binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
639 }
640 }
641
642 private void disableMicrophone(View view) {
643 JingleRtpConnection rtpConnection = requireRtpConnection();
644 rtpConnection.setMicrophoneEnabled(false);
645 updateInCallButtonConfiguration();
646 }
647
648 private void enableMicrophone(View view) {
649 JingleRtpConnection rtpConnection = requireRtpConnection();
650 rtpConnection.setMicrophoneEnabled(true);
651 updateInCallButtonConfiguration();
652 }
653
654 private void switchToEarpiece(View view) {
655 requireRtpConnection().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE);
656 acquireProximityWakeLock();
657 }
658
659 private void switchToSpeaker(View view) {
660 requireRtpConnection().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE);
661 releaseProximityWakeLock();
662 }
663
664 private void retry(View view) {
665 Log.d(Config.LOGTAG, "attempting retry");
666 final Intent intent = getIntent();
667 final Account account = extractAccount(intent);
668 final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH));
669 final String lastAction = intent.getStringExtra(EXTRA_LAST_ACTION);
670 final String action = intent.getAction();
671 final Set<Media> media = actionToMedia(lastAction == null ? action : lastAction);
672 this.rtpConnectionReference = null;
673 proposeJingleRtpSession(account, with, media);
674 }
675
676 private void exit(View view) {
677 finish();
678 }
679
680 private Contact getWith() {
681 final AbstractJingleConnection.Id id = requireRtpConnection().getId();
682 final Account account = id.account;
683 return account.getRoster().getContact(id.with);
684 }
685
686 private JingleRtpConnection requireRtpConnection() {
687 final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
688 if (connection == null) {
689 throw new IllegalStateException("No RTP connection found");
690 }
691 return connection;
692 }
693
694 @Override
695 public void onJingleRtpConnectionUpdate(Account account, Jid with, final String sessionId, RtpEndUserState state) {
696 Log.d(Config.LOGTAG, "onJingleRtpConnectionUpdate(" + state + ")");
697 if (END_CARD.contains(state)) {
698 Log.d(Config.LOGTAG, "end card reached");
699 releaseProximityWakeLock();
700 runOnUiThread(() -> getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON));
701 }
702 if (with.isBareJid()) {
703 updateRtpSessionProposalState(account, with, state);
704 return;
705 }
706 if (this.rtpConnectionReference == null) {
707 if (END_CARD.contains(state)) {
708 Log.d(Config.LOGTAG, "not reinitializing session");
709 return;
710 }
711 //this happens when going from proposed session to actual session
712 reInitializeActivityWithRunningRapSession(account, with, sessionId);
713 return;
714 }
715 final AbstractJingleConnection.Id id = requireRtpConnection().getId();
716 if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) {
717 if (state == RtpEndUserState.ENDED) {
718 finish();
719 return;
720 } else if (END_CARD.contains(state)) {
721 resetIntent(account, with, state, requireRtpConnection().getMedia());
722 }
723 runOnUiThread(() -> {
724 updateStateDisplay(state);
725 updateButtonConfiguration(state);
726 updateVideoViews(state);
727 });
728 } else {
729 Log.d(Config.LOGTAG, "received update for other rtp session");
730 //TODO if we only ever have one; we might just switch over? Maybe!
731 }
732 }
733
734 @Override
735 public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
736 Log.d(Config.LOGTAG, "onAudioDeviceChanged in activity: selected:" + selectedAudioDevice + ", available:" + availableAudioDevices);
737 try {
738 if (getMedia().contains(Media.VIDEO)) {
739 Log.d(Config.LOGTAG, "nothing to do; in video mode");
740 return;
741 }
742 final RtpEndUserState endUserState = requireRtpConnection().getEndUserState();
743 if (endUserState == RtpEndUserState.CONNECTED) {
744 final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager();
745 updateInCallButtonConfigurationSpeaker(
746 audioManager.getSelectedAudioDevice(),
747 audioManager.getAudioDevices().size()
748 );
749 } else if (END_CARD.contains(endUserState)) {
750 Log.d(Config.LOGTAG, "onAudioDeviceChanged() nothing to do because end card has been reached");
751 } else {
752 putProximityWakeLockInProperState();
753 }
754 } catch (IllegalStateException e) {
755 Log.d(Config.LOGTAG, "RTP connection was not available when audio device changed");
756 }
757 }
758
759 private void updateRtpSessionProposalState(final Account account, final Jid with, final RtpEndUserState state) {
760 final Intent currentIntent = getIntent();
761 final String withExtra = currentIntent == null ? null : currentIntent.getStringExtra(EXTRA_WITH);
762 if (withExtra == null) {
763 return;
764 }
765 if (Jid.ofEscaped(withExtra).asBareJid().equals(with)) {
766 runOnUiThread(() -> {
767 updateStateDisplay(state);
768 updateButtonConfiguration(state);
769 });
770 resetIntent(account, with, state, actionToMedia(currentIntent.getAction()));
771 }
772 }
773
774 private void resetIntent(final Bundle extras) {
775 final Intent intent = new Intent(Intent.ACTION_VIEW);
776 intent.putExtras(extras);
777 setIntent(intent);
778 }
779
780 private void resetIntent(final Account account, Jid with, final RtpEndUserState state, final Set<Media> media) {
781 final Intent intent = new Intent(Intent.ACTION_VIEW);
782 intent.putExtra(EXTRA_WITH, with.asBareJid().toEscapedString());
783 intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
784 intent.putExtra(EXTRA_LAST_REPORTED_STATE, state.toString());
785 intent.putExtra(EXTRA_LAST_ACTION, media.contains(Media.VIDEO) ? ACTION_MAKE_VIDEO_CALL : ACTION_MAKE_VOICE_CALL);
786 setIntent(intent);
787 }
788}