1package eu.siacs.conversations.ui;
2
3import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
4import static java.util.Arrays.asList;
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.ActivityInfo;
13import android.content.pm.PackageManager;
14import android.opengl.GLException;
15import android.os.Build;
16import android.os.Bundle;
17import android.os.Handler;
18import android.os.PowerManager;
19import android.util.Log;
20import android.util.Rational;
21import android.view.KeyEvent;
22import android.view.Menu;
23import android.view.MenuItem;
24import android.view.View;
25import android.view.WindowManager;
26import android.widget.Toast;
27import androidx.annotation.NonNull;
28import androidx.annotation.Nullable;
29import androidx.annotation.RequiresApi;
30import androidx.annotation.StringRes;
31import androidx.databinding.DataBindingUtil;
32import com.google.android.material.floatingactionbutton.FloatingActionButton;
33import com.google.common.base.Optional;
34import com.google.common.base.Preconditions;
35import com.google.common.base.Strings;
36import com.google.common.base.Throwables;
37import com.google.common.collect.ImmutableList;
38import com.google.common.collect.ImmutableSet;
39import com.google.common.util.concurrent.FutureCallback;
40import com.google.common.util.concurrent.Futures;
41
42import org.jetbrains.annotations.NotNull;
43import org.webrtc.RendererCommon;
44import org.webrtc.SurfaceViewRenderer;
45import org.webrtc.VideoTrack;
46
47import java.lang.ref.WeakReference;
48import java.util.Arrays;
49import java.util.Collections;
50import java.util.List;
51import java.util.Set;
52
53import eu.siacs.conversations.Config;
54import eu.siacs.conversations.R;
55import eu.siacs.conversations.databinding.ActivityRtpSessionBinding;
56import eu.siacs.conversations.entities.Account;
57import eu.siacs.conversations.entities.Contact;
58import eu.siacs.conversations.entities.Conversation;
59import eu.siacs.conversations.services.CallIntegration;
60import eu.siacs.conversations.services.CallIntegrationConnectionService;
61import eu.siacs.conversations.services.XmppConnectionService;
62import eu.siacs.conversations.ui.widget.DialpadView;
63import eu.siacs.conversations.ui.util.AvatarWorkerTask;
64import eu.siacs.conversations.ui.util.MainThreadExecutor;
65import eu.siacs.conversations.ui.util.Rationals;
66import eu.siacs.conversations.utils.PermissionUtils;
67import eu.siacs.conversations.utils.TimeFrameUtils;
68import eu.siacs.conversations.xmpp.Jid;
69import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
70import eu.siacs.conversations.xmpp.jingle.ContentAddition;
71import eu.siacs.conversations.xmpp.jingle.JingleConnectionManager;
72import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
73import eu.siacs.conversations.xmpp.jingle.Media;
74import eu.siacs.conversations.xmpp.jingle.OngoingRtpSession;
75import eu.siacs.conversations.xmpp.jingle.RtpCapability;
76import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
77
78public class RtpSessionActivity extends XmppActivity
79 implements XmppConnectionService.OnJingleRtpConnectionUpdate,
80 eu.siacs.conversations.ui.widget.SurfaceViewRenderer.OnAspectRatioChanged {
81
82 public static final String EXTRA_WITH = "with";
83 public static final String EXTRA_SESSION_ID = "session_id";
84 public static final String EXTRA_PROPOSED_SESSION_ID = "proposed_session_id";
85 public static final String EXTRA_LAST_REPORTED_STATE = "last_reported_state";
86 public static final String EXTRA_LAST_ACTION = "last_action";
87 public static final String ACTION_ACCEPT_CALL = "action_accept_call";
88 public static final String ACTION_MAKE_VOICE_CALL = "action_make_voice_call";
89 public static final String ACTION_MAKE_VIDEO_CALL = "action_make_video_call";
90
91 private static final int CALL_DURATION_UPDATE_INTERVAL = 250;
92 private static final int BUTTON_VISIBILITY_TIMEOUT = 10_000;
93
94 public static final List<RtpEndUserState> END_CARD =
95 Arrays.asList(
96 RtpEndUserState.APPLICATION_ERROR,
97 RtpEndUserState.SECURITY_ERROR,
98 RtpEndUserState.DECLINED_OR_BUSY,
99 RtpEndUserState.CONTACT_OFFLINE,
100 RtpEndUserState.CONNECTIVITY_ERROR,
101 RtpEndUserState.CONNECTIVITY_LOST_ERROR,
102 RtpEndUserState.RETRACTED);
103 private static final List<RtpEndUserState> STATES_SHOWING_HELP_BUTTON =
104 Arrays.asList(
105 RtpEndUserState.APPLICATION_ERROR,
106 RtpEndUserState.CONNECTIVITY_ERROR,
107 RtpEndUserState.SECURITY_ERROR);
108 private static final List<RtpEndUserState> STATES_SHOWING_SWITCH_TO_CHAT =
109 Arrays.asList(
110 RtpEndUserState.CONNECTING,
111 RtpEndUserState.CONNECTED,
112 RtpEndUserState.RECONNECTING,
113 RtpEndUserState.INCOMING_CONTENT_ADD);
114 private static final List<RtpEndUserState> STATES_CONSIDERED_CONNECTED =
115 Arrays.asList(RtpEndUserState.CONNECTED, RtpEndUserState.RECONNECTING);
116 private static final List<RtpEndUserState> STATES_SHOWING_PIP_PLACEHOLDER =
117 Arrays.asList(
118 RtpEndUserState.ACCEPTING_CALL,
119 RtpEndUserState.CONNECTING,
120 RtpEndUserState.RECONNECTING);
121 private static final List<RtpEndUserState> STATES_SHOWING_SPEAKER_CONFIGURATION =
122 new ImmutableList.Builder<RtpEndUserState>()
123 .add(RtpEndUserState.FINDING_DEVICE)
124 .add(RtpEndUserState.RINGING)
125 .add(RtpEndUserState.ACCEPTING_CALL)
126 .add(RtpEndUserState.CONNECTING)
127 .addAll(STATES_CONSIDERED_CONNECTED)
128 .build();
129 private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session";
130 private static final int REQUEST_ACCEPT_CALL = 0x1111;
131 private static final int REQUEST_ACCEPT_CONTENT = 0x1112;
132 private static final int REQUEST_ADD_CONTENT = 0x1113;
133 private WeakReference<JingleRtpConnection> rtpConnectionReference;
134
135 private ActivityRtpSessionBinding binding;
136 private PowerManager.WakeLock mProximityWakeLock;
137
138 private final Handler mHandler = new Handler();
139 private final Runnable mTickExecutor =
140 new Runnable() {
141 @Override
142 public void run() {
143 updateCallDuration();
144 mHandler.postDelayed(mTickExecutor, CALL_DURATION_UPDATE_INTERVAL);
145 }
146 };
147 private boolean buttonsHiddenAfterTimeout = false;
148 private final Runnable mVisibilityToggleExecutor = this::updateButtonInVideoCallVisibility;
149
150 public static Set<Media> actionToMedia(final String action) {
151 if (ACTION_MAKE_VIDEO_CALL.equals(action)) {
152 return ImmutableSet.of(Media.AUDIO, Media.VIDEO);
153 } else if (ACTION_MAKE_VOICE_CALL.equals(action)) {
154 return ImmutableSet.of(Media.AUDIO);
155 } else {
156 Log.w(
157 Config.LOGTAG,
158 "actionToMedia can not get media set from unknown action " + action);
159 return Collections.emptySet();
160 }
161 }
162
163 private static void addSink(
164 final VideoTrack videoTrack, final SurfaceViewRenderer surfaceViewRenderer) {
165 try {
166 videoTrack.addSink(surfaceViewRenderer);
167 } catch (final IllegalStateException e) {
168 Log.e(
169 Config.LOGTAG,
170 "possible race condition on trying to display video track. ignoring",
171 e);
172 }
173 }
174
175 @Override
176 public void onCreate(Bundle savedInstanceState) {
177 super.onCreate(savedInstanceState);
178 getWindow()
179 .addFlags(
180 WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
181 | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
182 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
183 | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
184 this.binding = DataBindingUtil.setContentView(this, R.layout.activity_rtp_session);
185 this.binding.remoteVideo.setOnClickListener(this::onVideoScreenClick);
186 this.binding.localVideo.setOnClickListener(this::onVideoScreenClick);
187 setSupportActionBar(binding.toolbar);
188
189 binding.dialpad.setClickConsumer(tag -> {
190 final JingleRtpConnection connection =
191 this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
192 if (connection != null) connection.applyDtmfTone(tag);
193 });
194
195 if (savedInstanceState != null) {
196 boolean dialpadVisible = savedInstanceState.getBoolean("dialpad_visible");
197 binding.dialpad.setVisibility(dialpadVisible ? View.VISIBLE : View.GONE);
198 }
199
200 Activities.setStatusAndNavigationBarColors(this, binding.getRoot());
201 }
202
203 private void onVideoScreenClick(final View view) {
204 resetVisibilityExecutorShowButtons();
205 }
206
207 @Override
208 public boolean onCreateOptionsMenu(final Menu menu) {
209 getMenuInflater().inflate(R.menu.activity_rtp_session, menu);
210 final MenuItem help = menu.findItem(R.id.action_help);
211 final MenuItem gotoChat = menu.findItem(R.id.action_goto_chat);
212 final MenuItem dialpad = menu.findItem(R.id.action_dialpad);
213 final MenuItem switchToVideo = menu.findItem(R.id.action_switch_to_video);
214 help.setVisible(Config.HELP != null && isHelpButtonVisible());
215 gotoChat.setVisible(isSwitchToConversationVisible());
216 switchToVideo.setVisible(isSwitchToVideoVisible());
217 dialpad.setVisible(isAudioOnlyConversation());
218 return super.onCreateOptionsMenu(menu);
219 }
220
221 @Override
222 public boolean onKeyDown(final int keyCode, final KeyEvent event) {
223 if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) {
224 if (xmppConnectionService != null) {
225 if (xmppConnectionService.getNotificationService().stopSoundAndVibration()) {
226 return true;
227 }
228 }
229 }
230 return super.onKeyDown(keyCode, event);
231 }
232
233 private boolean isHelpButtonVisible() {
234 try {
235 return STATES_SHOWING_HELP_BUTTON.contains(requireRtpConnection().getEndUserState());
236 } catch (IllegalStateException e) {
237 final Intent intent = getIntent();
238 final String state =
239 intent != null ? intent.getStringExtra(EXTRA_LAST_REPORTED_STATE) : null;
240 if (state != null) {
241 return STATES_SHOWING_HELP_BUTTON.contains(RtpEndUserState.valueOf(state));
242 } else {
243 return false;
244 }
245 }
246 }
247
248 private boolean isSwitchToConversationVisible() {
249 final JingleRtpConnection connection =
250 this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
251 return connection != null
252 && STATES_SHOWING_SWITCH_TO_CHAT.contains(connection.getEndUserState());
253 }
254
255 private boolean isAudioOnlyConversation() {
256 final JingleRtpConnection connection =
257 this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
258
259 return connection != null && !connection.getMedia().contains(Media.VIDEO);
260 }
261
262 private boolean isSwitchToVideoVisible() {
263 final JingleRtpConnection connection =
264 this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
265 if (connection == null) {
266 return false;
267 }
268 return connection.isSwitchToVideoAvailable();
269 }
270
271 private void switchToConversation() {
272 final Contact contact = getWith();
273 final Conversation conversation =
274 xmppConnectionService.findOrCreateConversation(
275 contact.getAccount(), contact.getJid(), false, true);
276 switchToConversation(conversation);
277 }
278
279 private void toggleDialpadVisibility() {
280 if (binding.dialpad.getVisibility() == View.VISIBLE) {
281 binding.dialpad.setVisibility(View.GONE);
282 }
283 else {
284 binding.dialpad.setVisibility(View.VISIBLE);
285 }
286 }
287
288 public boolean onOptionsItemSelected(final MenuItem item) {
289 final var itemItem = item.getItemId();
290 if (itemItem == R.id.action_help) {
291 launchHelpInBrowser();
292 return true;
293 } else if (itemItem == R.id.action_goto_chat) {
294 switchToConversation();
295 return true;
296 } else if (itemItem == R.id.action_dialpad) {
297 toggleDialpadVisibility();
298 return true;
299 } else if (itemItem == R.id.action_switch_to_video) {
300 requestPermissionAndSwitchToVideo();
301 return true;
302 } else {
303 return super.onOptionsItemSelected(item);
304 }
305 }
306
307 private void launchHelpInBrowser() {
308 final Intent intent = new Intent(Intent.ACTION_VIEW, Config.HELP);
309 try {
310 startActivity(intent);
311 } catch (final ActivityNotFoundException e) {
312 Toast.makeText(this, R.string.no_application_found_to_open_link, Toast.LENGTH_LONG)
313 .show();
314 }
315 }
316
317 private void endCall(View view) {
318 endCall();
319 }
320
321 private void endCall() {
322 if (this.rtpConnectionReference == null) {
323 retractSessionProposal();
324 finish();
325 } else {
326 try {
327 requireRtpConnection().endCall();
328 } catch (final IllegalStateException e) {
329 // No call, already done
330 finish();
331 }
332 }
333 }
334
335 private void retractSessionProposal() {
336 final Intent intent = getIntent();
337 final String action = intent.getAction();
338 final String lastAction = intent.getStringExtra(EXTRA_LAST_ACTION);
339 final Account account = extractAccount(intent);
340 final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH));
341 final String state = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE);
342 if (!Intent.ACTION_VIEW.equals(action)
343 || state == null
344 || !END_CARD.contains(RtpEndUserState.valueOf(state))) {
345 final Set<Media> media = actionToMedia(lastAction == null ? action : lastAction);
346 resetIntent(account, with, RtpEndUserState.RETRACTED, media);
347 }
348 xmppConnectionService
349 .getJingleConnectionManager()
350 .retractSessionProposal(account, with.asBareJid());
351 }
352
353 private void rejectCall(View view) {
354 requireRtpConnection().rejectCall();
355 finish();
356 }
357
358 private void acceptCall(View view) {
359 requestPermissionsAndAcceptCall();
360 }
361
362 private void acceptContentAdd() {
363 try {
364 final ContentAddition pendingContentAddition =
365 requireRtpConnection().getPendingContentAddition();
366 if (pendingContentAddition == null) {
367 Log.d(Config.LOGTAG, "content offer was gone after granting permission");
368 return;
369 }
370 requireRtpConnection().acceptContentAdd(pendingContentAddition.summary);
371 } catch (final IllegalStateException e) {
372 Toast.makeText(this, Strings.nullToEmpty(e.getMessage()), Toast.LENGTH_SHORT).show();
373 }
374 }
375
376 private void requestPermissionAndSwitchToVideo() {
377 final List<String> permissions = permissions(ImmutableSet.of(Media.VIDEO, Media.AUDIO));
378 if (PermissionUtils.hasPermission(this, permissions, REQUEST_ADD_CONTENT)) {
379 switchToVideo();
380 }
381 }
382
383 private void switchToVideo() {
384 try {
385 requireRtpConnection().addMedia(Media.VIDEO);
386 } catch (final IllegalStateException e) {
387 Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
388 }
389 }
390
391 private void acceptContentAdd(final ContentAddition contentAddition) {
392 if (contentAddition == null
393 || contentAddition.direction != ContentAddition.Direction.INCOMING) {
394 Log.d(Config.LOGTAG, "ignore press on content-accept button");
395 return;
396 }
397 requestPermissionAndAcceptContentAdd(contentAddition);
398 }
399
400 private void requestPermissionAndAcceptContentAdd(final ContentAddition contentAddition) {
401 final List<String> permissions = permissions(contentAddition.media());
402 if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CONTENT)) {
403 try {
404 requireRtpConnection().acceptContentAdd(contentAddition.summary);
405 } catch (final IllegalStateException e) {
406 Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
407 }
408 }
409 }
410
411 private void rejectContentAdd(final View view) {
412 requireRtpConnection().rejectContentAdd();
413 }
414
415 private void requestPermissionsAndAcceptCall() {
416 final List<String> permissions = permissions(getMedia());
417 if (PermissionUtils.hasPermission(this, permissions, REQUEST_ACCEPT_CALL)) {
418 putScreenInCallMode();
419 acceptCall();
420 }
421 }
422
423 private List<String> permissions(final Set<Media> media) {
424 final ImmutableList.Builder<String> permissions = ImmutableList.builder();
425 if (media.contains(Media.VIDEO)) {
426 permissions.add(Manifest.permission.CAMERA).add(Manifest.permission.RECORD_AUDIO);
427 } else {
428 permissions.add(Manifest.permission.RECORD_AUDIO);
429 }
430 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
431 permissions.add(Manifest.permission.BLUETOOTH_CONNECT);
432 }
433 return permissions.build();
434 }
435
436 private void acceptCall() {
437 try {
438 requireRtpConnection().acceptCall();
439 } catch (final IllegalStateException e) {
440 Toast.makeText(this, e.getMessage(), Toast.LENGTH_SHORT).show();
441 }
442 }
443
444 private void putScreenInCallMode() {
445 putScreenInCallMode(requireRtpConnection().getMedia());
446 }
447
448 private void putScreenInCallMode(final Set<Media> media) {
449 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
450 if (Media.audioOnly(media)) {
451 final JingleRtpConnection rtpConnection =
452 rtpConnectionReference != null ? rtpConnectionReference.get() : null;
453 final CallIntegration callIntegration =
454 rtpConnection == null ? null : rtpConnection.getCallIntegration();
455 if (callIntegration == null
456 || callIntegration.getSelectedAudioDevice()
457 == CallIntegration.AudioDevice.EARPIECE) {
458 acquireProximityWakeLock();
459 }
460 }
461 lockOrientation(media);
462 }
463
464 private void lockOrientation(final Set<Media> media) {
465 if (Media.audioOnly(media)) {
466 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
467 } else {
468 setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
469 }
470 }
471
472 @SuppressLint("WakelockTimeout")
473 private void acquireProximityWakeLock() {
474 final PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
475 if (powerManager == null) {
476 Log.e(Config.LOGTAG, "power manager not available");
477 return;
478 }
479 if (isFinishing()) {
480 Log.e(Config.LOGTAG, "do not acquire wakelock. activity is finishing");
481 return;
482 }
483 if (this.mProximityWakeLock == null) {
484 this.mProximityWakeLock =
485 powerManager.newWakeLock(
486 PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, PROXIMITY_WAKE_LOCK_TAG);
487 }
488 if (!this.mProximityWakeLock.isHeld()) {
489 Log.d(Config.LOGTAG, "acquiring proximity wake lock");
490 this.mProximityWakeLock.acquire();
491 }
492 }
493
494 private void releaseProximityWakeLock() {
495 if (this.mProximityWakeLock != null && mProximityWakeLock.isHeld()) {
496 Log.d(Config.LOGTAG, "releasing proximity wake lock");
497 this.mProximityWakeLock.release(PowerManager.RELEASE_FLAG_WAIT_FOR_NO_PROXIMITY);
498 this.mProximityWakeLock = null;
499 }
500 }
501
502 private void putProximityWakeLockInProperState(final CallIntegration.AudioDevice audioDevice) {
503 if (audioDevice == CallIntegration.AudioDevice.EARPIECE) {
504 acquireProximityWakeLock();
505 } else {
506 releaseProximityWakeLock();
507 }
508 }
509
510 @Override
511 protected void refreshUiReal() {}
512
513 @Override
514 public void onNewIntent(final Intent intent) {
515 Log.d(Config.LOGTAG, this.getClass().getName() + ".onNewIntent()");
516 super.onNewIntent(intent);
517 if (intent == null) {
518 return;
519 }
520 setIntent(intent);
521 if (xmppConnectionService == null) {
522 Log.d(
523 Config.LOGTAG,
524 "RtpSessionActivity: background service wasn't bound in onNewIntent()");
525 return;
526 }
527 initializeWithIntent(Event.ON_NEW_INTENT, intent);
528 }
529
530 @Override
531 protected void onBackendConnected() {
532 final var intent = getIntent();
533 if (intent == null) {
534 return;
535 }
536 initializeWithIntent(Event.ON_BACKEND_CONNECTED, intent);
537 }
538
539 private void initializeWithIntent(final Event event, @NonNull final Intent intent) {
540 final String action = intent.getAction();
541 Log.d(Config.LOGTAG, "initializeWithIntent(" + event + "," + action + ")");
542 final Account account = extractAccount(intent);
543 final var extraWith = intent.getStringExtra(EXTRA_WITH);
544 final Jid with = Strings.isNullOrEmpty(extraWith) ? null : Jid.of(extraWith);
545 if (with == null || account == null) {
546 Log.e(Config.LOGTAG, "intent is missing extras (account or with)");
547 return;
548 }
549 final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID);
550 if (sessionId != null) {
551 if (initializeActivityWithRunningRtpSession(account, with, sessionId)) {
552 return;
553 }
554 if (ACTION_ACCEPT_CALL.equals(intent.getAction())) {
555 Log.d(Config.LOGTAG, "intent action was accept");
556 requestPermissionsAndAcceptCall();
557 resetIntent(intent.getExtras());
558 }
559 } else if (Intent.ACTION_VIEW.equals(action)) {
560 final String proposedSessionId = intent.getStringExtra(EXTRA_PROPOSED_SESSION_ID);
561 final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession =
562 xmppConnectionService
563 .getJingleConnectionManager()
564 .getTerminalSessionState(with, proposedSessionId);
565 if (terminatedRtpSession != null) {
566 // termination (due to message error or 'busy' was faster than opening the activity
567 initializeWithTerminatedSessionState(account, with, terminatedRtpSession);
568 return;
569 }
570 final String extraLastState = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE);
571 final RtpEndUserState state =
572 extraLastState == null ? null : RtpEndUserState.valueOf(extraLastState);
573 final var contact = account.getRoster().getContact(with);
574 if (state != null) {
575 Log.d(Config.LOGTAG, "restored last state from intent extra");
576 updateButtonConfiguration(state);
577 updateVerifiedShield(false);
578 updateStateDisplay(state);
579 updateIncomingCallScreen(state);
580 updateSupportWarning(state, contact);
581 invalidateOptionsMenu();
582 }
583 setWith(state, contact);
584 if (xmppConnectionService
585 .getJingleConnectionManager()
586 .fireJingleRtpConnectionStateUpdates()) {
587 return;
588 }
589 if (END_CARD.contains(state)) {
590 return;
591 }
592 final String lastAction = intent.getStringExtra(EXTRA_LAST_ACTION);
593 final Set<Media> media = actionToMedia(lastAction);
594 if (xmppConnectionService
595 .getJingleConnectionManager()
596 .hasMatchingProposal(account, with)) {
597 putScreenInCallMode(media);
598 return;
599 }
600 Log.d(Config.LOGTAG, "restored state (" + state + ") was not an end card. finishing");
601 finish();
602 }
603 }
604
605 private void setWith(final RtpEndUserState state) {
606 setWith(state, getWith());
607 }
608
609 private void setWith(final RtpEndUserState state, final Contact contact) {
610 binding.with.setText(contact.getDisplayName());
611 if (Arrays.asList(RtpEndUserState.INCOMING_CALL, RtpEndUserState.ACCEPTING_CALL)
612 .contains(state)) {
613 binding.withJid.setText(contact.getJid().asBareJid().toString());
614 binding.withJid.setVisibility(View.VISIBLE);
615 } else {
616 binding.withJid.setVisibility(View.GONE);
617 }
618 }
619
620 @Override
621 public void onRequestPermissionsResult(
622 int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
623 super.onRequestPermissionsResult(requestCode, permissions, grantResults);
624 final PermissionUtils.PermissionResult permissionResult =
625 PermissionUtils.removeBluetoothConnect(permissions, grantResults);
626 if (PermissionUtils.allGranted(permissionResult.grantResults)) {
627 if (requestCode == REQUEST_ACCEPT_CALL) {
628 acceptCall();
629 } else if (requestCode == REQUEST_ACCEPT_CONTENT) {
630 acceptContentAdd();
631 } else if (requestCode == REQUEST_ADD_CONTENT) {
632 switchToVideo();
633 }
634 } else {
635 @StringRes int res;
636 final String firstDenied =
637 getFirstDenied(permissionResult.grantResults, permissionResult.permissions);
638 if (firstDenied == null) {
639 return;
640 }
641 if (Manifest.permission.RECORD_AUDIO.equals(firstDenied)) {
642 res = R.string.no_microphone_permission;
643 } else if (Manifest.permission.CAMERA.equals(firstDenied)) {
644 res = R.string.no_camera_permission;
645 } else {
646 throw new IllegalStateException("Invalid permission result request");
647 }
648 Toast.makeText(this, getString(res, getString(R.string.app_name)), Toast.LENGTH_SHORT)
649 .show();
650 }
651 }
652
653 @Override
654 public void onStart() {
655 super.onStart();
656 mHandler.postDelayed(mTickExecutor, CALL_DURATION_UPDATE_INTERVAL);
657 mHandler.postDelayed(mVisibilityToggleExecutor, BUTTON_VISIBILITY_TIMEOUT);
658 this.binding.remoteVideo.setOnAspectRatioChanged(this);
659 }
660
661 @Override
662 public void onResume() {
663 super.onResume();
664 resetVisibilityExecutorShowButtons();
665 }
666
667 @Override
668 public void onStop() {
669 mHandler.removeCallbacks(mTickExecutor);
670 mHandler.removeCallbacks(mVisibilityToggleExecutor);
671 binding.remoteVideo.release();
672 binding.remoteVideo.setOnAspectRatioChanged(null);
673 binding.localVideo.release();
674 final WeakReference<JingleRtpConnection> weakReference = this.rtpConnectionReference;
675 final JingleRtpConnection jingleRtpConnection =
676 weakReference == null ? null : weakReference.get();
677 if (jingleRtpConnection != null) {
678 releaseVideoTracks(jingleRtpConnection);
679 }
680 releaseProximityWakeLock();
681 super.onStop();
682 }
683
684 private void releaseVideoTracks(final JingleRtpConnection jingleRtpConnection) {
685 final Optional<VideoTrack> remoteVideo = jingleRtpConnection.getRemoteVideoTrack();
686 if (remoteVideo.isPresent()) {
687 remoteVideo.get().removeSink(binding.remoteVideo);
688 }
689 final Optional<VideoTrack> localVideo = jingleRtpConnection.getLocalVideoTrack();
690 if (localVideo.isPresent()) {
691 localVideo.get().removeSink(binding.localVideo);
692 }
693 }
694
695 @Override
696 public void onBackPressed() {
697 if (isConnected()) {
698 if (switchToPictureInPicture()) {
699 return;
700 }
701 } else {
702 endCall();
703 }
704 super.onBackPressed();
705 }
706
707 @Override
708 public void onUserLeaveHint() {
709 super.onUserLeaveHint();
710 if (switchToPictureInPicture()) {
711 return;
712 }
713 // TODO apparently this method is not getting called on Android 10 when using the task
714 // switcher
715 if (emptyReference(rtpConnectionReference) && xmppConnectionService != null) {
716 retractSessionProposal();
717 }
718 }
719
720 private boolean isConnected() {
721 final JingleRtpConnection connection =
722 this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
723 final RtpEndUserState endUserState =
724 connection == null ? null : connection.getEndUserState();
725 return STATES_CONSIDERED_CONNECTED.contains(endUserState)
726 || endUserState == RtpEndUserState.INCOMING_CONTENT_ADD;
727 }
728
729 private boolean switchToPictureInPicture() {
730 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && deviceSupportsPictureInPicture()) {
731 if (shouldBePictureInPicture()) {
732 startPictureInPicture();
733 return true;
734 }
735 }
736 return false;
737 }
738
739 @RequiresApi(api = Build.VERSION_CODES.O)
740 private void startPictureInPicture() {
741 try {
742 final Rational rational = this.binding.remoteVideo.getAspectRatio();
743 final Rational clippedRational = Rationals.clip(rational);
744 Log.d(
745 Config.LOGTAG,
746 "suggested rational " + rational + ". clipped to " + clippedRational);
747 enterPictureInPictureMode(
748 new PictureInPictureParams.Builder().setAspectRatio(clippedRational).build());
749 } catch (final IllegalStateException e) {
750 // this sometimes happens on Samsung phones (possibly when Knox is enabled)
751 Log.w(Config.LOGTAG, "unable to enter picture in picture mode", e);
752 }
753 }
754
755 @Override
756 public void onAspectRatioChanged(final Rational rational) {
757 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && isPictureInPicture()) {
758 final Rational clippedRational = Rationals.clip(rational);
759 Log.d(
760 Config.LOGTAG,
761 "suggested rational after aspect ratio change "
762 + rational
763 + ". clipped to "
764 + clippedRational);
765 setPictureInPictureParams(
766 new PictureInPictureParams.Builder().setAspectRatio(clippedRational).build());
767 }
768 }
769
770 private boolean deviceSupportsPictureInPicture() {
771 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
772 return getPackageManager().hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE);
773 } else {
774 return false;
775 }
776 }
777
778 private boolean shouldBePictureInPicture() {
779 try {
780 final JingleRtpConnection rtpConnection = requireRtpConnection();
781 return rtpConnection.getMedia().contains(Media.VIDEO)
782 && Arrays.asList(
783 RtpEndUserState.ACCEPTING_CALL,
784 RtpEndUserState.CONNECTING,
785 RtpEndUserState.CONNECTED)
786 .contains(rtpConnection.getEndUserState());
787 } catch (final IllegalStateException e) {
788 return false;
789 }
790 }
791
792 private boolean isInConnectedVideoCall() {
793 final JingleRtpConnection rtpConnection;
794 try {
795 rtpConnection = requireRtpConnection();
796 } catch (final IllegalStateException e) {
797 return false;
798 }
799 return rtpConnection.getMedia().contains(Media.VIDEO)
800 && rtpConnection.getEndUserState() == RtpEndUserState.CONNECTED;
801 }
802
803 private boolean initializeActivityWithRunningRtpSession(
804 final Account account, Jid with, String sessionId) {
805 final WeakReference<JingleRtpConnection> reference =
806 xmppConnectionService
807 .getJingleConnectionManager()
808 .findJingleRtpConnection(account, with, sessionId);
809 if (reference == null || reference.get() == null) {
810 final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession =
811 xmppConnectionService
812 .getJingleConnectionManager()
813 .getTerminalSessionState(with, sessionId);
814 if (terminatedRtpSession == null) {
815 throw new IllegalStateException(
816 "failed to initialize activity with running rtp session. session not"
817 + " found");
818 }
819 initializeWithTerminatedSessionState(account, with, terminatedRtpSession);
820 return true;
821 }
822 this.rtpConnectionReference = reference;
823 final RtpEndUserState currentState = requireRtpConnection().getEndUserState();
824 final boolean verified = requireRtpConnection().isVerified();
825 if (currentState == RtpEndUserState.ENDED) {
826 finish();
827 return true;
828 }
829 final Set<Media> media = getMedia();
830 final ContentAddition contentAddition = getPendingContentAddition();
831 if (currentState == RtpEndUserState.INCOMING_CALL) {
832 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
833 }
834 if (JingleRtpConnection.STATES_SHOWING_ONGOING_CALL.contains(
835 requireRtpConnection().getState())) {
836 putScreenInCallMode();
837 }
838 setWith(currentState);
839 updateVideoViews(currentState);
840 updateStateDisplay(currentState, media, contentAddition);
841 updateVerifiedShield(verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(currentState));
842 updateButtonConfiguration(currentState, media, contentAddition);
843 updateIncomingCallScreen(currentState);
844 invalidateOptionsMenu();
845 return false;
846 }
847
848 private void initializeWithTerminatedSessionState(
849 final Account account,
850 final Jid with,
851 final JingleConnectionManager.TerminatedRtpSession terminatedRtpSession) {
852 Log.d(Config.LOGTAG, "initializeWithTerminatedSessionState()");
853 if (terminatedRtpSession.state == RtpEndUserState.ENDED) {
854 finish();
855 return;
856 }
857 final RtpEndUserState state = terminatedRtpSession.state;
858 resetIntent(account, with, terminatedRtpSession.state, terminatedRtpSession.media);
859 updateButtonConfiguration(state);
860 updateStateDisplay(state);
861 updateIncomingCallScreen(state);
862 updateCallDuration();
863 updateVerifiedShield(false);
864 invalidateOptionsMenu();
865 final var contact = account.getRoster().getContact(with);
866 setWith(state, contact);
867 updateSupportWarning(state, contact);
868 }
869
870 private void reInitializeActivityWithRunningRtpSession(
871 final Account account, Jid with, String sessionId) {
872 runOnUiThread(() -> initializeActivityWithRunningRtpSession(account, with, sessionId));
873 resetIntent(account, with, sessionId);
874 }
875
876 private void resetIntent(final Account account, final Jid with, final String sessionId) {
877 final Intent intent = new Intent(Intent.ACTION_VIEW);
878 intent.putExtra(EXTRA_ACCOUNT, account.getJid().toString());
879 intent.putExtra(EXTRA_WITH, with.toString());
880 intent.putExtra(EXTRA_SESSION_ID, sessionId);
881 setIntent(intent);
882 }
883
884 private void ensureSurfaceViewRendererIsSetup(final SurfaceViewRenderer surfaceViewRenderer) {
885 surfaceViewRenderer.setVisibility(View.VISIBLE);
886 try {
887 surfaceViewRenderer.init(requireRtpConnection().getEglBaseContext(), null);
888 } catch (final IllegalStateException ignored) {
889 // SurfaceViewRenderer was already initialized
890 } catch (final RuntimeException e) {
891 if (Throwables.getRootCause(e) instanceof GLException glException) {
892 Log.w(Config.LOGTAG, "could not set up hardware renderer", glException);
893 }
894 }
895 surfaceViewRenderer.setEnableHardwareScaler(true);
896 }
897
898 private void updateStateDisplay(final RtpEndUserState state) {
899 updateStateDisplay(state, Collections.emptySet(), null);
900 }
901
902 private void updateStateDisplay(
903 final RtpEndUserState state,
904 final Set<Media> media,
905 final ContentAddition contentAddition) {
906 switch (state) {
907 case INCOMING_CALL -> {
908 Preconditions.checkArgument(!media.isEmpty(), "Media must not be empty");
909 if (media.contains(Media.VIDEO)) {
910 setTitle(R.string.rtp_state_incoming_video_call);
911 } else {
912 setTitle(R.string.rtp_state_incoming_call);
913 }
914 }
915 case INCOMING_CONTENT_ADD -> {
916 if (contentAddition != null && contentAddition.media().contains(Media.VIDEO)) {
917 setTitle(R.string.rtp_state_content_add_video);
918 } else {
919 setTitle(R.string.rtp_state_content_add);
920 }
921 }
922 case CONNECTING -> setTitle(R.string.rtp_state_connecting);
923 case CONNECTED -> setTitle(R.string.rtp_state_connected);
924 case RECONNECTING -> setTitle(R.string.rtp_state_reconnecting);
925 case ACCEPTING_CALL -> setTitle(R.string.rtp_state_accepting_call);
926 case ENDING_CALL -> setTitle(R.string.rtp_state_ending_call);
927 case FINDING_DEVICE -> setTitle(R.string.rtp_state_finding_device);
928 case RINGING -> setTitle(R.string.rtp_state_ringing);
929 case DECLINED_OR_BUSY -> setTitle(R.string.rtp_state_declined_or_busy);
930 case CONTACT_OFFLINE -> setTitle(R.string.rtp_state_contact_offline);
931 case CONNECTIVITY_ERROR -> setTitle(R.string.rtp_state_connectivity_error);
932 case CONNECTIVITY_LOST_ERROR -> setTitle(R.string.rtp_state_connectivity_lost_error);
933 case RETRACTED -> setTitle(R.string.rtp_state_retracted);
934 case APPLICATION_ERROR -> setTitle(R.string.rtp_state_application_failure);
935 case SECURITY_ERROR -> setTitle(R.string.rtp_state_security_error);
936 case ENDED ->
937 throw new IllegalStateException(
938 "Activity should have called finishAndReleaseWakeLock();");
939 default ->
940 throw new IllegalStateException(
941 String.format("State %s has not been handled in UI", state));
942 }
943 }
944
945 private void updateVerifiedShield(final boolean verified) {
946 if (isPictureInPicture()) {
947 this.binding.verified.setVisibility(View.GONE);
948 return;
949 }
950 this.binding.verified.setVisibility(verified ? View.VISIBLE : View.GONE);
951 }
952
953 private void updateIncomingCallScreen(final RtpEndUserState state) {
954 updateIncomingCallScreen(state, null);
955 }
956
957 private void updateIncomingCallScreen(final RtpEndUserState state, final Contact contact) {
958 if (state == RtpEndUserState.INCOMING_CALL || state == RtpEndUserState.ACCEPTING_CALL) {
959 final boolean show = getResources().getBoolean(R.bool.is_portrait_mode);
960 if (show) {
961 binding.contactPhoto.setVisibility(View.VISIBLE);
962 if (contact == null) {
963 AvatarWorkerTask.loadAvatar(
964 getWith(), binding.contactPhoto, R.dimen.publish_avatar_size);
965 } else {
966 AvatarWorkerTask.loadAvatar(
967 contact, binding.contactPhoto, R.dimen.publish_avatar_size);
968 }
969 } else {
970 binding.contactPhoto.setVisibility(View.GONE);
971 }
972 final Account account = contact == null ? getWith().getAccount() : contact.getAccount();
973 binding.usingAccount.setVisibility(View.VISIBLE);
974 binding.usingAccount.setText(
975 getString(R.string.using_account, account.getJid().asBareJid().toString()));
976 } else {
977 binding.usingAccount.setVisibility(View.GONE);
978 binding.contactPhoto.setVisibility(View.GONE);
979 }
980 }
981
982 private void updateSupportWarning(final RtpEndUserState state, final Contact contact) {
983 if (state == RtpEndUserState.CONNECTIVITY_ERROR
984 && getResources().getBoolean(R.bool.is_portrait_mode)) {
985 binding.supportWarning.setVisibility(
986 RtpCapability.check(contact) == RtpCapability.Capability.NONE
987 ? View.VISIBLE
988 : View.GONE);
989 } else {
990 binding.supportWarning.setVisibility(View.GONE);
991 }
992 }
993
994 private Set<Media> getMedia() {
995 return requireRtpConnection().getMedia();
996 }
997
998 public ContentAddition getPendingContentAddition() {
999 return requireRtpConnection().getPendingContentAddition();
1000 }
1001
1002 private void updateButtonConfiguration(final RtpEndUserState state) {
1003 updateButtonConfiguration(state, Collections.emptySet(), null);
1004 }
1005
1006 @SuppressLint("RestrictedApi")
1007 private void updateButtonConfiguration(
1008 final RtpEndUserState state,
1009 final Set<Media> media,
1010 final ContentAddition contentAddition) {
1011 if (state == RtpEndUserState.ENDING_CALL
1012 || isPictureInPicture()
1013 || this.buttonsHiddenAfterTimeout) {
1014 this.binding.rejectCall.setVisibility(View.INVISIBLE);
1015 this.binding.endCall.setVisibility(View.INVISIBLE);
1016 this.binding.acceptCall.setVisibility(View.INVISIBLE);
1017 } else if (state == RtpEndUserState.INCOMING_CALL) {
1018 this.binding.rejectCall.setContentDescription(getString(R.string.dismiss_call));
1019 this.binding.rejectCall.setOnClickListener(this::rejectCall);
1020 this.binding.rejectCall.setImageResource(R.drawable.ic_call_end_24dp);
1021 this.binding.rejectCall.setVisibility(View.VISIBLE);
1022 this.binding.endCall.setVisibility(View.INVISIBLE);
1023 this.binding.acceptCall.setContentDescription(getString(R.string.answer_call));
1024 this.binding.acceptCall.setOnClickListener(this::acceptCall);
1025 this.binding.acceptCall.setImageResource(R.drawable.ic_call_24dp);
1026 this.binding.acceptCall.setVisibility(View.VISIBLE);
1027 } else if (state == RtpEndUserState.INCOMING_CONTENT_ADD) {
1028 this.binding.rejectCall.setContentDescription(
1029 getString(R.string.reject_switch_to_video));
1030 this.binding.rejectCall.setOnClickListener(this::rejectContentAdd);
1031 this.binding.rejectCall.setImageResource(R.drawable.ic_clear_24dp);
1032 this.binding.rejectCall.setVisibility(View.VISIBLE);
1033 this.binding.endCall.setVisibility(View.INVISIBLE);
1034 this.binding.acceptCall.setContentDescription(getString(R.string.accept));
1035 this.binding.acceptCall.setOnClickListener((v -> acceptContentAdd(contentAddition)));
1036 this.binding.acceptCall.setImageResource(R.drawable.ic_check_24dp);
1037 this.binding.acceptCall.setVisibility(View.VISIBLE);
1038 } else if (asList(RtpEndUserState.DECLINED_OR_BUSY, RtpEndUserState.CONTACT_OFFLINE)
1039 .contains(state)) {
1040 this.binding.rejectCall.setContentDescription(getString(R.string.exit));
1041 this.binding.rejectCall.setOnClickListener(this::exit);
1042 this.binding.rejectCall.setImageResource(R.drawable.ic_clear_24dp);
1043 this.binding.rejectCall.setVisibility(View.VISIBLE);
1044 this.binding.endCall.setVisibility(View.INVISIBLE);
1045 this.binding.acceptCall.setContentDescription(getString(R.string.record_voice_mail));
1046 this.binding.acceptCall.setOnClickListener(this::recordVoiceMail);
1047 this.binding.acceptCall.setImageResource(R.drawable.ic_voicemail_24dp);
1048 this.binding.acceptCall.setVisibility(View.VISIBLE);
1049 } else if (asList(
1050 RtpEndUserState.CONNECTIVITY_ERROR,
1051 RtpEndUserState.CONNECTIVITY_LOST_ERROR,
1052 RtpEndUserState.APPLICATION_ERROR,
1053 RtpEndUserState.RETRACTED,
1054 RtpEndUserState.SECURITY_ERROR)
1055 .contains(state)) {
1056 this.binding.rejectCall.setContentDescription(getString(R.string.exit));
1057 this.binding.rejectCall.setOnClickListener(this::exit);
1058 this.binding.rejectCall.setImageResource(R.drawable.ic_clear_24dp);
1059 this.binding.rejectCall.setVisibility(View.VISIBLE);
1060 this.binding.endCall.setVisibility(View.INVISIBLE);
1061 this.binding.acceptCall.setContentDescription(getString(R.string.try_again));
1062 this.binding.acceptCall.setOnClickListener(this::retry);
1063 this.binding.acceptCall.setImageResource(R.drawable.ic_replay_24dp);
1064 this.binding.acceptCall.setVisibility(View.VISIBLE);
1065 } else {
1066 this.binding.rejectCall.setVisibility(View.INVISIBLE);
1067 this.binding.endCall.setContentDescription(getString(R.string.hang_up));
1068 this.binding.endCall.setOnClickListener(this::endCall);
1069 this.binding.endCall.setImageResource(R.drawable.ic_call_end_24dp);
1070 setVisibleAndShow(this.binding.endCall);
1071 this.binding.acceptCall.setVisibility(View.INVISIBLE);
1072 }
1073 updateInCallButtonConfiguration(state, media);
1074 }
1075
1076 private static void setVisibleAndShow(final FloatingActionButton button) {
1077 button.show();
1078 button.setVisibility(View.VISIBLE);
1079 }
1080
1081 private boolean isPictureInPicture() {
1082 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
1083 return isInPictureInPictureMode();
1084 } else {
1085 return false;
1086 }
1087 }
1088
1089 private void updateInCallButtonConfiguration() {
1090 updateInCallButtonConfiguration(
1091 requireRtpConnection().getEndUserState(), requireRtpConnection().getMedia());
1092 }
1093
1094 @SuppressLint("RestrictedApi")
1095 private void updateInCallButtonConfiguration(
1096 final RtpEndUserState state, final Set<Media> media) {
1097 final var showButtons = !isPictureInPicture() && !buttonsHiddenAfterTimeout;
1098 if (STATES_CONSIDERED_CONNECTED.contains(state) && showButtons) {
1099 Preconditions.checkArgument(!media.isEmpty(), "Media must not be empty");
1100 if (media.contains(Media.VIDEO)) {
1101 final JingleRtpConnection rtpConnection = requireRtpConnection();
1102 updateInCallButtonConfigurationVideo(
1103 rtpConnection.isVideoEnabled(), rtpConnection.isCameraSwitchable());
1104 } else {
1105 final CallIntegration callIntegration = requireRtpConnection().getCallIntegration();
1106 updateInCallButtonConfigurationSpeaker(
1107 callIntegration.getSelectedAudioDevice(),
1108 callIntegration.getAudioDevices().size());
1109 this.binding.inCallActionFarRight.setVisibility(View.GONE);
1110 }
1111 if (media.contains(Media.AUDIO)) {
1112 updateInCallButtonConfigurationMicrophone(
1113 requireRtpConnection().isMicrophoneEnabled());
1114 } else {
1115 this.binding.inCallActionLeft.setVisibility(View.GONE);
1116 }
1117 } else if (STATES_SHOWING_SPEAKER_CONFIGURATION.contains(state)
1118 && showButtons
1119 && Media.audioOnly(media)) {
1120 final CallIntegration callIntegration;
1121 try {
1122 callIntegration = requireCallIntegration();
1123 } catch (final IllegalStateException e) {
1124 Log.e(Config.LOGTAG, "can not update InCallButtonConfiguration in state " + state);
1125 return;
1126 }
1127 updateInCallButtonConfigurationSpeaker(
1128 callIntegration.getSelectedAudioDevice(),
1129 callIntegration.getAudioDevices().size());
1130 this.binding.inCallActionFarRight.setVisibility(View.GONE);
1131 } else {
1132 this.binding.inCallActionLeft.setVisibility(View.GONE);
1133 this.binding.inCallActionRight.setVisibility(View.GONE);
1134 this.binding.inCallActionFarRight.setVisibility(View.GONE);
1135 }
1136 }
1137
1138 @SuppressLint("RestrictedApi")
1139 private void updateInCallButtonConfigurationSpeaker(
1140 final CallIntegration.AudioDevice selectedAudioDevice, final int numberOfChoices) {
1141 switch (selectedAudioDevice) {
1142 case EARPIECE -> {
1143 this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_off_24dp);
1144 if (numberOfChoices >= 2) {
1145 this.binding.inCallActionRight.setContentDescription(
1146 getString(R.string.call_is_using_earpiece_tap_to_switch_to_speaker));
1147 this.binding.inCallActionRight.setOnClickListener(this::switchToSpeaker);
1148 } else {
1149 this.binding.inCallActionRight.setContentDescription(
1150 getString(R.string.call_is_using_earpiece));
1151 this.binding.inCallActionRight.setOnClickListener(null);
1152 this.binding.inCallActionRight.setClickable(false);
1153 }
1154 }
1155 case WIRED_HEADSET -> {
1156 this.binding.inCallActionRight.setContentDescription(
1157 getString(R.string.call_is_using_wired_headset));
1158 this.binding.inCallActionRight.setImageResource(R.drawable.ic_headset_mic_24dp);
1159 this.binding.inCallActionRight.setOnClickListener(null);
1160 this.binding.inCallActionRight.setClickable(false);
1161 }
1162 case SPEAKER_PHONE -> {
1163 this.binding.inCallActionRight.setImageResource(R.drawable.ic_volume_up_24dp);
1164 if (numberOfChoices >= 2) {
1165 this.binding.inCallActionRight.setContentDescription(
1166 getString(R.string.call_is_using_speaker_tap_to_switch_to_earpiece));
1167 this.binding.inCallActionRight.setOnClickListener(this::switchToEarpiece);
1168 } else {
1169 this.binding.inCallActionRight.setContentDescription(
1170 getString(R.string.call_is_using_speaker));
1171 this.binding.inCallActionRight.setOnClickListener(null);
1172 this.binding.inCallActionRight.setClickable(false);
1173 }
1174 }
1175 case BLUETOOTH -> {
1176 this.binding.inCallActionRight.setContentDescription(
1177 getString(R.string.call_is_using_bluetooth));
1178 this.binding.inCallActionRight.setImageResource(R.drawable.ic_bluetooth_audio_24dp);
1179 this.binding.inCallActionRight.setOnClickListener(null);
1180 this.binding.inCallActionRight.setClickable(false);
1181 }
1182 }
1183 setVisibleAndShow(this.binding.inCallActionRight);
1184 }
1185
1186 @SuppressLint("RestrictedApi")
1187 private void updateInCallButtonConfigurationVideo(
1188 final boolean videoEnabled, final boolean isCameraSwitchable) {
1189 setVisibleAndShow(this.binding.inCallActionRight);
1190 if (isCameraSwitchable) {
1191 this.binding.inCallActionFarRight.setImageResource(
1192 R.drawable.ic_flip_camera_android_24dp);
1193 setVisibleAndShow(this.binding.inCallActionFarRight);
1194 this.binding.inCallActionFarRight.setOnClickListener(this::switchCamera);
1195 this.binding.inCallActionFarRight.setContentDescription(
1196 getString(R.string.flip_camera));
1197 } else {
1198 this.binding.inCallActionFarRight.setVisibility(View.GONE);
1199 }
1200 if (videoEnabled) {
1201 this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_24dp);
1202 this.binding.inCallActionRight.setOnClickListener(this::disableVideo);
1203 this.binding.inCallActionRight.setContentDescription(
1204 getString(R.string.video_is_enabled_tap_to_disable));
1205 } else {
1206 this.binding.inCallActionRight.setImageResource(R.drawable.ic_videocam_off_24dp);
1207 this.binding.inCallActionRight.setOnClickListener(this::enableVideo);
1208 this.binding.inCallActionRight.setContentDescription(
1209 getString(R.string.video_is_disabled_tap_to_enable));
1210 }
1211 }
1212
1213 private void switchCamera(final View view) {
1214 resetVisibilityToggleExecutor();
1215 Futures.addCallback(
1216 requireRtpConnection().switchCamera(),
1217 new FutureCallback<>() {
1218 @Override
1219 public void onSuccess(@Nullable Boolean isFrontCamera) {
1220 binding.localVideo.setMirror(Boolean.TRUE.equals(isFrontCamera));
1221 }
1222
1223 @Override
1224 public void onFailure(@NonNull final Throwable throwable) {
1225 Log.d(
1226 Config.LOGTAG,
1227 "could not switch camera",
1228 Throwables.getRootCause(throwable));
1229 Toast.makeText(
1230 RtpSessionActivity.this,
1231 R.string.could_not_switch_camera,
1232 Toast.LENGTH_LONG)
1233 .show();
1234 }
1235 },
1236 MainThreadExecutor.getInstance());
1237 // TODO ^ replace with ContextCompat.getMainExecutor(getApplication())
1238 }
1239
1240 private void enableVideo(final View view) {
1241 resetVisibilityToggleExecutor();
1242 try {
1243 requireRtpConnection().setVideoEnabled(true);
1244 } catch (final IllegalStateException e) {
1245 Toast.makeText(this, R.string.unable_to_enable_video, Toast.LENGTH_SHORT).show();
1246 return;
1247 }
1248 updateInCallButtonConfigurationVideo(true, requireRtpConnection().isCameraSwitchable());
1249 }
1250
1251 private void disableVideo(final View view) {
1252 resetVisibilityToggleExecutor();
1253 final JingleRtpConnection rtpConnection = requireRtpConnection();
1254 final ContentAddition pending = rtpConnection.getPendingContentAddition();
1255 if (pending != null && pending.direction == ContentAddition.Direction.OUTGOING) {
1256 rtpConnection.retractContentAdd();
1257 return;
1258 }
1259 try {
1260 requireRtpConnection().setVideoEnabled(false);
1261 } catch (final IllegalStateException e) {
1262 Toast.makeText(this, R.string.could_not_disable_video, Toast.LENGTH_SHORT).show();
1263 return;
1264 }
1265 updateInCallButtonConfigurationVideo(false, requireRtpConnection().isCameraSwitchable());
1266 }
1267
1268 @SuppressLint("RestrictedApi")
1269 private void updateInCallButtonConfigurationMicrophone(final boolean microphoneEnabled) {
1270 if (microphoneEnabled) {
1271 this.binding.inCallActionLeft.setImageResource(R.drawable.ic_mic_24dp);
1272 this.binding.inCallActionLeft.setOnClickListener(this::disableMicrophone);
1273 } else {
1274 this.binding.inCallActionLeft.setImageResource(R.drawable.ic_mic_off_24dp);
1275 this.binding.inCallActionLeft.setOnClickListener(this::enableMicrophone);
1276 }
1277 setVisibleAndShow(this.binding.inCallActionLeft);
1278 }
1279
1280 private void updateCallDuration() {
1281 final JingleRtpConnection connection =
1282 this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
1283 if (connection == null || connection.getMedia().contains(Media.VIDEO)) {
1284 this.binding.duration.setVisibility(View.GONE);
1285 return;
1286 }
1287 if (connection.zeroDuration()) {
1288 this.binding.duration.setVisibility(View.GONE);
1289 } else {
1290 this.binding.duration.setText(
1291 TimeFrameUtils.formatElapsedTime(connection.getCallDuration(), false));
1292 this.binding.duration.setVisibility(View.VISIBLE);
1293 }
1294 }
1295
1296 private void resetVisibilityToggleExecutor() {
1297 mHandler.removeCallbacks(this.mVisibilityToggleExecutor);
1298 mHandler.postDelayed(this.mVisibilityToggleExecutor, BUTTON_VISIBILITY_TIMEOUT);
1299 }
1300
1301 private void updateButtonInVideoCallVisibility() {
1302 if (isInConnectedVideoCall()) {
1303 if (isPictureInPicture()) {
1304 return;
1305 }
1306 Log.d(Config.LOGTAG, "hiding in-call buttons after timeout was reached");
1307 hideInCallButtons();
1308 }
1309 }
1310
1311 private void hideInCallButtons() {
1312 binding.inCallActionLeft.hide();
1313 binding.endCall.hide();
1314 binding.inCallActionRight.hide();
1315 binding.inCallActionFarRight.hide();
1316 }
1317
1318 private void showInCallButtons() {
1319 this.buttonsHiddenAfterTimeout = false;
1320 final JingleRtpConnection rtpConnection;
1321 try {
1322 rtpConnection = requireRtpConnection();
1323 } catch (final IllegalStateException e) {
1324 return;
1325 }
1326 updateButtonConfiguration(
1327 rtpConnection.getEndUserState(),
1328 rtpConnection.getMedia(),
1329 rtpConnection.getPendingContentAddition());
1330 }
1331
1332 private void resetVisibilityExecutorShowButtons() {
1333 resetVisibilityToggleExecutor();
1334 showInCallButtons();
1335 }
1336
1337 private void updateVideoViews(final RtpEndUserState state) {
1338 if (END_CARD.contains(state) || state == RtpEndUserState.ENDING_CALL) {
1339 binding.localVideo.setVisibility(View.GONE);
1340 binding.localVideo.release();
1341 binding.remoteVideoWrapper.setVisibility(View.GONE);
1342 binding.remoteVideo.release();
1343 binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
1344 if (isPictureInPicture()) {
1345 binding.appBarLayout.setVisibility(View.GONE);
1346 binding.pipPlaceholder.setVisibility(View.VISIBLE);
1347 if (Arrays.asList(
1348 RtpEndUserState.APPLICATION_ERROR,
1349 RtpEndUserState.CONNECTIVITY_ERROR,
1350 RtpEndUserState.SECURITY_ERROR)
1351 .contains(state)) {
1352 binding.pipWarning.setVisibility(View.VISIBLE);
1353 binding.pipWaiting.setVisibility(View.GONE);
1354 } else {
1355 binding.pipWarning.setVisibility(View.GONE);
1356 binding.pipWaiting.setVisibility(View.GONE);
1357 }
1358 } else {
1359 binding.appBarLayout.setVisibility(View.VISIBLE);
1360 binding.pipPlaceholder.setVisibility(View.GONE);
1361 }
1362 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
1363 return;
1364 }
1365 if (isPictureInPicture() && STATES_SHOWING_PIP_PLACEHOLDER.contains(state)) {
1366 binding.localVideo.setVisibility(View.GONE);
1367 binding.remoteVideoWrapper.setVisibility(View.GONE);
1368 binding.appBarLayout.setVisibility(View.GONE);
1369 binding.pipPlaceholder.setVisibility(View.VISIBLE);
1370 binding.pipWarning.setVisibility(View.GONE);
1371 binding.pipWaiting.setVisibility(View.VISIBLE);
1372 binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
1373 return;
1374 }
1375 final Optional<VideoTrack> localVideoTrack = getLocalVideoTrack();
1376 if (localVideoTrack.isPresent() && !isPictureInPicture()) {
1377 ensureSurfaceViewRendererIsSetup(binding.localVideo);
1378 // paint local view over remote view
1379 binding.localVideo.setZOrderMediaOverlay(true);
1380 binding.localVideo.setMirror(requireRtpConnection().isFrontCamera());
1381 addSink(localVideoTrack.get(), binding.localVideo);
1382 } else {
1383 binding.localVideo.setVisibility(View.GONE);
1384 }
1385 final Optional<VideoTrack> remoteVideoTrack = getRemoteVideoTrack();
1386 if (remoteVideoTrack.isPresent()) {
1387 ensureSurfaceViewRendererIsSetup(binding.remoteVideo);
1388 addSink(remoteVideoTrack.get(), binding.remoteVideo);
1389 binding.remoteVideo.setScalingType(
1390 RendererCommon.ScalingType.SCALE_ASPECT_FILL,
1391 RendererCommon.ScalingType.SCALE_ASPECT_FIT);
1392 if (state == RtpEndUserState.CONNECTED) {
1393 binding.appBarLayout.setVisibility(View.GONE);
1394 getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
1395 binding.remoteVideoWrapper.setVisibility(View.VISIBLE);
1396 } else {
1397 binding.appBarLayout.setVisibility(View.VISIBLE);
1398 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
1399 binding.remoteVideoWrapper.setVisibility(View.GONE);
1400 }
1401 if (isPictureInPicture() && !requireRtpConnection().isMicrophoneEnabled()) {
1402 binding.pipLocalMicOffIndicator.setVisibility(View.VISIBLE);
1403 } else {
1404 binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
1405 }
1406 } else {
1407 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
1408 binding.remoteVideoWrapper.setVisibility(View.GONE);
1409 binding.pipLocalMicOffIndicator.setVisibility(View.GONE);
1410 }
1411 }
1412
1413 private Optional<VideoTrack> getLocalVideoTrack() {
1414 final JingleRtpConnection connection =
1415 this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
1416 if (connection == null) {
1417 return Optional.absent();
1418 }
1419 return connection.getLocalVideoTrack();
1420 }
1421
1422 private Optional<VideoTrack> getRemoteVideoTrack() {
1423 final JingleRtpConnection connection =
1424 this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
1425 if (connection == null) {
1426 return Optional.absent();
1427 }
1428 return connection.getRemoteVideoTrack();
1429 }
1430
1431 private void disableMicrophone(final View view) {
1432 setMicrophoneEnabled(false);
1433 }
1434
1435 private void enableMicrophone(final View view) {
1436 setMicrophoneEnabled(true);
1437 }
1438
1439 private void setMicrophoneEnabled(final boolean enabled) {
1440 resetVisibilityExecutorShowButtons();
1441 try {
1442 final JingleRtpConnection rtpConnection = requireRtpConnection();
1443 if (rtpConnection.setMicrophoneEnabled(enabled)) {
1444 updateInCallButtonConfiguration();
1445 }
1446 } catch (final IllegalStateException e) {
1447 Toast.makeText(this, R.string.could_not_modify_call, Toast.LENGTH_SHORT).show();
1448 }
1449 }
1450
1451 private void switchToEarpiece(final View view) {
1452 try {
1453 requireCallIntegration().setAudioDevice(CallIntegration.AudioDevice.EARPIECE);
1454 acquireProximityWakeLock();
1455 } catch (final IllegalStateException e) {
1456 Toast.makeText(this, R.string.could_not_modify_call, Toast.LENGTH_SHORT).show();
1457 }
1458 }
1459
1460 private void switchToSpeaker(final View view) {
1461 try {
1462 requireCallIntegration().setAudioDevice(CallIntegration.AudioDevice.SPEAKER_PHONE);
1463 releaseProximityWakeLock();
1464 } catch (final IllegalStateException e) {
1465 Toast.makeText(this, R.string.could_not_modify_call, Toast.LENGTH_SHORT).show();
1466 }
1467 }
1468
1469 private void retry(final View view) {
1470 final Intent intent = getIntent();
1471 final Account account = extractAccount(intent);
1472 final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH));
1473 final String lastAction = intent.getStringExtra(EXTRA_LAST_ACTION);
1474 final String action = intent.getAction();
1475 final Set<Media> media = actionToMedia(lastAction == null ? action : lastAction);
1476 this.rtpConnectionReference = null;
1477 Log.d(Config.LOGTAG, "attempting retry with " + with.toString());
1478 CallIntegrationConnectionService.placeCall(xmppConnectionService, account, with, media);
1479 }
1480
1481 private void exit(final View view) {
1482 finish();
1483 }
1484
1485 private void recordVoiceMail(final View view) {
1486 final Intent intent = getIntent();
1487 final Account account = extractAccount(intent);
1488 final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH));
1489 final Conversation conversation =
1490 xmppConnectionService.findOrCreateConversation(account, with, false, true);
1491 final Intent launchIntent = new Intent(this, ConversationsActivity.class);
1492 launchIntent.setAction(ConversationsActivity.ACTION_VIEW_CONVERSATION);
1493 launchIntent.putExtra(ConversationsActivity.EXTRA_CONVERSATION, conversation.getUuid());
1494 launchIntent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_CLEAR_TOP);
1495 launchIntent.putExtra(
1496 ConversationsActivity.EXTRA_POST_INIT_ACTION,
1497 ConversationsActivity.POST_ACTION_RECORD_VOICE);
1498 startActivity(launchIntent);
1499 finish();
1500 }
1501
1502 private Contact getWith() {
1503 final AbstractJingleConnection.Id id = requireRtpConnection().getId();
1504 final Account account = id.account;
1505 return account.getRoster().getContact(id.with);
1506 }
1507
1508 private JingleRtpConnection requireRtpConnection() {
1509 final JingleRtpConnection connection =
1510 this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
1511 if (connection == null) {
1512 throw new IllegalStateException("No RTP connection found");
1513 }
1514 return connection;
1515 }
1516
1517 private CallIntegration requireCallIntegration() {
1518 return requireOngoingRtpSession().getCallIntegration();
1519 }
1520
1521 private OngoingRtpSession requireOngoingRtpSession() {
1522 final JingleRtpConnection connection =
1523 this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
1524 if (connection != null) {
1525 return connection;
1526 }
1527 final Intent currentIntent = getIntent();
1528 final String withExtra =
1529 currentIntent == null ? null : currentIntent.getStringExtra(EXTRA_WITH);
1530 final var account = extractAccount(currentIntent);
1531 if (withExtra == null) {
1532 throw new IllegalStateException("Current intent has no EXTRA_WITH");
1533 }
1534 final var matching =
1535 xmppConnectionService
1536 .getJingleConnectionManager()
1537 .matchingProposal(account, Jid.of(withExtra));
1538 if (matching.isPresent()) {
1539 return matching.get();
1540 }
1541 throw new IllegalStateException("No matching session proposal");
1542 }
1543
1544 @Override
1545 public void onJingleRtpConnectionUpdate(
1546 Account account, Jid with, final String sessionId, RtpEndUserState state) {
1547 Log.d(Config.LOGTAG, "onJingleRtpConnectionUpdate(" + state + ")");
1548 if (END_CARD.contains(state)) {
1549 Log.d(Config.LOGTAG, "end card reached");
1550 releaseProximityWakeLock();
1551 runOnUiThread(
1552 () -> getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON));
1553 }
1554 if (with.isBareJid()) {
1555 // TODO check for ENDED
1556 updateRtpSessionProposalState(account, with, state);
1557 return;
1558 }
1559 if (emptyReference(this.rtpConnectionReference)) {
1560 if (END_CARD.contains(state)) {
1561 Log.d(Config.LOGTAG, "not reinitializing session");
1562 return;
1563 }
1564 // this happens when going from proposed session to actual session
1565 reInitializeActivityWithRunningRtpSession(account, with, sessionId);
1566 return;
1567 }
1568 final AbstractJingleConnection.Id id = requireRtpConnection().getId();
1569 final boolean verified = requireRtpConnection().isVerified();
1570 final Set<Media> media = getMedia();
1571 lockOrientation(media);
1572 final ContentAddition contentAddition = getPendingContentAddition();
1573 final Contact contact = getWith();
1574 if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) {
1575 if (state == RtpEndUserState.ENDED) {
1576 finish();
1577 return;
1578 }
1579 resetVisibilityToggleExecutor();
1580 runOnUiThread(
1581 () -> {
1582 updateStateDisplay(state, media, contentAddition);
1583 updateVerifiedShield(
1584 verified && STATES_SHOWING_SWITCH_TO_CHAT.contains(state));
1585 updateButtonConfiguration(state, media, contentAddition);
1586 updateVideoViews(state);
1587 updateIncomingCallScreen(state, contact);
1588 updateSupportWarning(state, contact);
1589 invalidateOptionsMenu();
1590 });
1591 if (END_CARD.contains(state)) {
1592 final JingleRtpConnection rtpConnection = requireRtpConnection();
1593 resetIntent(account, with, state, rtpConnection.getMedia());
1594 releaseVideoTracks(rtpConnection);
1595 this.rtpConnectionReference = null;
1596 }
1597 } else {
1598 Log.d(Config.LOGTAG, "received update for other rtp session");
1599 }
1600 }
1601
1602 @Override
1603 public void onAudioDeviceChanged(
1604 final CallIntegration.AudioDevice selectedAudioDevice,
1605 final Set<CallIntegration.AudioDevice> availableAudioDevices) {
1606 Log.d(
1607 Config.LOGTAG,
1608 "onAudioDeviceChanged in activity: selected:"
1609 + selectedAudioDevice
1610 + ", available:"
1611 + availableAudioDevices);
1612 try {
1613 final OngoingRtpSession ongoingRtpSession = requireOngoingRtpSession();
1614 final RtpEndUserState endUserState;
1615 if (ongoingRtpSession instanceof JingleRtpConnection jingleRtpConnection) {
1616 endUserState = jingleRtpConnection.getEndUserState();
1617 } else {
1618 // for session proposals all end user states are functionally the same
1619 endUserState = RtpEndUserState.RINGING;
1620 }
1621 final Set<Media> media = ongoingRtpSession.getMedia();
1622 if (END_CARD.contains(endUserState)) {
1623 Log.d(
1624 Config.LOGTAG,
1625 "onAudioDeviceChanged() nothing to do because end card has been reached");
1626 } else {
1627 if (Media.audioOnly(media)
1628 && STATES_SHOWING_SPEAKER_CONFIGURATION.contains(endUserState)) {
1629 final CallIntegration callIntegration = requireCallIntegration();
1630 updateInCallButtonConfigurationSpeaker(
1631 callIntegration.getSelectedAudioDevice(),
1632 callIntegration.getAudioDevices().size());
1633 }
1634 Log.d(
1635 Config.LOGTAG,
1636 "put proximity wake lock into proper state after device update");
1637 putProximityWakeLockInProperState(selectedAudioDevice);
1638 }
1639 } catch (final IllegalStateException e) {
1640 Log.d(Config.LOGTAG, "RTP connection was not available when audio device changed");
1641 }
1642 }
1643
1644 @Override
1645 protected void onSaveInstanceState(@NonNull @NotNull Bundle outState) {
1646 super.onSaveInstanceState(outState);
1647 outState.putBoolean("dialpad_visible", binding.dialpad.getVisibility() == View.VISIBLE);
1648 }
1649
1650 private void updateRtpSessionProposalState(
1651 final Account account, final Jid with, final RtpEndUserState state) {
1652 final Intent currentIntent = getIntent();
1653 final String withExtra =
1654 currentIntent == null ? null : currentIntent.getStringExtra(EXTRA_WITH);
1655 if (withExtra == null) {
1656 return;
1657 }
1658 final Set<Media> media = actionToMedia(currentIntent.getStringExtra(EXTRA_LAST_ACTION));
1659 if (Jid.of(withExtra).asBareJid().equals(with)) {
1660 runOnUiThread(
1661 () -> {
1662 updateVerifiedShield(false);
1663 updateStateDisplay(state);
1664 updateButtonConfiguration(state, media, null);
1665 updateIncomingCallScreen(state);
1666 updateSupportWarning(state, account.getRoster().getContact(with));
1667 invalidateOptionsMenu();
1668 });
1669 resetIntent(account, with, state, media);
1670 }
1671 }
1672
1673 private void resetIntent(final Bundle extras) {
1674 final Intent intent = new Intent(Intent.ACTION_VIEW);
1675 intent.putExtras(extras);
1676 setIntent(intent);
1677 }
1678
1679 private void resetIntent(
1680 final Account account, Jid with, final RtpEndUserState state, final Set<Media> media) {
1681 final Intent intent = new Intent(Intent.ACTION_VIEW);
1682 intent.putExtra(EXTRA_ACCOUNT, account.getJid().toString());
1683 if (RtpCapability.jmiSupport(account.getRoster().getContact(with))) {
1684 intent.putExtra(EXTRA_WITH, with.asBareJid().toString());
1685 } else {
1686 intent.putExtra(EXTRA_WITH, with.toString());
1687 }
1688 intent.putExtra(EXTRA_LAST_REPORTED_STATE, state.toString());
1689 intent.putExtra(
1690 EXTRA_LAST_ACTION,
1691 media.contains(Media.VIDEO) ? ACTION_MAKE_VIDEO_CALL : ACTION_MAKE_VOICE_CALL);
1692 setIntent(intent);
1693 }
1694
1695 private static boolean emptyReference(final WeakReference<?> weakReference) {
1696 return weakReference == null || weakReference.get() == null;
1697 }
1698
1699 private enum Event {
1700 ON_BACKEND_CONNECTED,
1701 ON_NEW_INTENT
1702 }
1703}