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