1package eu.siacs.conversations.ui;
2
3import android.Manifest;
4import android.annotation.SuppressLint;
5import android.content.Context;
6import android.content.Intent;
7import android.databinding.DataBindingUtil;
8import android.os.Build;
9import android.os.Bundle;
10import android.os.PowerManager;
11import android.support.annotation.NonNull;
12import android.support.annotation.StringRes;
13import android.util.Log;
14import android.view.View;
15import android.view.WindowManager;
16import android.widget.Toast;
17
18import com.google.common.collect.ImmutableList;
19
20import java.lang.ref.WeakReference;
21import java.util.Arrays;
22import java.util.Set;
23
24import eu.siacs.conversations.Config;
25import eu.siacs.conversations.R;
26import eu.siacs.conversations.databinding.ActivityRtpSessionBinding;
27import eu.siacs.conversations.entities.Account;
28import eu.siacs.conversations.entities.Contact;
29import eu.siacs.conversations.services.AppRTCAudioManager;
30import eu.siacs.conversations.services.XmppConnectionService;
31import eu.siacs.conversations.utils.PermissionUtils;
32import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
33import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
34import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
35import rocks.xmpp.addr.Jid;
36
37import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
38import static java.util.Arrays.asList;
39
40//TODO if last state was BUSY (or RETRY); we want to reset action to view or something so we don’t automatically call again on recreate
41
42public class RtpSessionActivity extends XmppActivity implements XmppConnectionService.OnJingleRtpConnectionUpdate {
43
44 private static final String PROXIMITY_WAKE_LOCK_TAG = "conversations:in-rtp-session";
45
46 private static final int REQUEST_ACCEPT_CALL = 0x1111;
47
48 public static final String EXTRA_WITH = "with";
49 public static final String EXTRA_SESSION_ID = "session_id";
50 public static final String EXTRA_LAST_REPORTED_STATE = "last_reported_state";
51
52 public static final String ACTION_ACCEPT_CALL = "action_accept_call";
53 public static final String ACTION_MAKE_VOICE_CALL = "action_make_voice_call";
54 public static final String ACTION_MAKE_VIDEO_CALL = "action_make_video_call";
55
56 private WeakReference<JingleRtpConnection> rtpConnectionReference;
57
58 private ActivityRtpSessionBinding binding;
59 private PowerManager.WakeLock mProximityWakeLock;
60
61 @Override
62 public void onCreate(Bundle savedInstanceState) {
63 super.onCreate(savedInstanceState);
64 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
65 | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
66 | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
67 | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON);
68 Log.d(Config.LOGTAG, "RtpSessionActivity.onCreate()");
69 this.binding = DataBindingUtil.setContentView(this, R.layout.activity_rtp_session);
70 }
71
72 @Override
73 public void onStart() {
74 super.onStart();
75 Log.d(Config.LOGTAG, "RtpSessionActivity.onStart()");
76 }
77
78 private void endCall(View view) {
79 endCall();
80 }
81
82 private void endCall() {
83 if (this.rtpConnectionReference == null) {
84 final Intent intent = getIntent();
85 final Account account = extractAccount(intent);
86 final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH));
87 xmppConnectionService.getJingleConnectionManager().retractSessionProposal(account, with.asBareJid());
88 finish();
89 } else {
90 requireRtpConnection().endCall();
91 }
92 }
93
94 private void rejectCall(View view) {
95 requireRtpConnection().rejectCall();
96 finish();
97 }
98
99 private void acceptCall(View view) {
100 requestPermissionsAndAcceptCall();
101 }
102
103 private void requestPermissionsAndAcceptCall() {
104 if (PermissionUtils.hasPermission(this, ImmutableList.of(Manifest.permission.RECORD_AUDIO), REQUEST_ACCEPT_CALL)) {
105 putScreenInCallMode();
106 requireRtpConnection().acceptCall();
107 }
108 }
109
110 @SuppressLint("WakelockTimeout")
111 private void putScreenInCallMode() {
112 getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
113 final JingleRtpConnection rtpConnection = rtpConnectionReference != null ? rtpConnectionReference.get() : null;
114 if (rtpConnection == null || rtpConnection.getAudioManager().getSelectedAudioDevice() == AppRTCAudioManager.AudioDevice.EARPIECE) {
115 acquireProximityWakeLock();
116 }
117 }
118
119 private void acquireProximityWakeLock() {
120 final PowerManager powerManager = (PowerManager) getSystemService(Context.POWER_SERVICE);
121 if (powerManager == null) {
122 Log.e(Config.LOGTAG, "power manager not available");
123 return;
124 }
125 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
126 if (this.mProximityWakeLock == null) {
127 this.mProximityWakeLock = powerManager.newWakeLock(PowerManager.PROXIMITY_SCREEN_OFF_WAKE_LOCK, PROXIMITY_WAKE_LOCK_TAG);
128 }
129 if (!this.mProximityWakeLock.isHeld()) {
130 Log.d(Config.LOGTAG, "acquiring proximity wake lock");
131 this.mProximityWakeLock.acquire();
132 }
133 }
134 }
135
136 private void releaseProximityWakeLock() {
137 if (this.mProximityWakeLock != null && mProximityWakeLock.isHeld()) {
138 Log.d(Config.LOGTAG, "releasing proximity wake lock");
139 this.mProximityWakeLock.release();
140 this.mProximityWakeLock = null;
141 }
142 }
143
144 private void putProximityWakeLockInProperState() {
145 if (requireRtpConnection().getAudioManager().getSelectedAudioDevice() == AppRTCAudioManager.AudioDevice.EARPIECE) {
146 acquireProximityWakeLock();
147 } else {
148 releaseProximityWakeLock();
149 }
150 }
151
152 @Override
153 protected void refreshUiReal() {
154
155 }
156
157 @Override
158 public void onNewIntent(final Intent intent) {
159 super.onNewIntent(intent);
160 setIntent(intent);
161 if (xmppConnectionService == null) {
162 Log.d(Config.LOGTAG, "RtpSessionActivity: background service wasn't bound in onNewIntent()");
163 return;
164 }
165 final Account account = extractAccount(intent);
166 final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH));
167 final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID);
168 if (sessionId != null) {
169 Log.d(Config.LOGTAG, "reinitializing from onNewIntent()");
170 initializeActivityWithRunningRtpSession(account, with, sessionId);
171 if (ACTION_ACCEPT_CALL.equals(intent.getAction())) {
172 Log.d(Config.LOGTAG, "accepting call from onNewIntent()");
173 requestPermissionsAndAcceptCall();
174 resetIntent(intent.getExtras());
175 }
176 } else {
177 throw new IllegalStateException("received onNewIntent without sessionId");
178 }
179 }
180
181 @Override
182 void onBackendConnected() {
183 final Intent intent = getIntent();
184 final Account account = extractAccount(intent);
185 final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH));
186 final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID);
187 if (sessionId != null) {
188 initializeActivityWithRunningRtpSession(account, with, sessionId);
189 if (ACTION_ACCEPT_CALL.equals(intent.getAction())) {
190 Log.d(Config.LOGTAG, "intent action was accept");
191 requestPermissionsAndAcceptCall();
192 resetIntent(intent.getExtras());
193 }
194 } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(intent.getAction())) {
195 proposeJingleRtpSession(account, with);
196 binding.with.setText(account.getRoster().getContact(with).getDisplayName());
197 } else if (Intent.ACTION_VIEW.equals(intent.getAction())) {
198 final String extraLastState = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE);
199 if (extraLastState != null) {
200 Log.d(Config.LOGTAG, "restored last state from intent extra");
201 RtpEndUserState state = RtpEndUserState.valueOf(extraLastState);
202 updateButtonConfiguration(state);
203 updateStateDisplay(state);
204 }
205 binding.with.setText(account.getRoster().getContact(with).getDisplayName());
206 }
207 }
208
209 private void proposeJingleRtpSession(final Account account, final Jid with) {
210 xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(account, with);
211 //TODO maybe we don’t want to acquire a wake lock just yet and wait for audio manager to discover what speaker we are using
212 putScreenInCallMode();
213 }
214
215 @Override
216 public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
217 super.onRequestPermissionsResult(requestCode, permissions, grantResults);
218 if (PermissionUtils.allGranted(grantResults)) {
219 if (requestCode == REQUEST_ACCEPT_CALL) {
220 requireRtpConnection().acceptCall();
221 }
222 } else {
223 @StringRes int res;
224 final String firstDenied = getFirstDenied(grantResults, permissions);
225 if (Manifest.permission.RECORD_AUDIO.equals(firstDenied)) {
226 res = R.string.no_microphone_permission;
227 } else if (Manifest.permission.CAMERA.equals(firstDenied)) {
228 res = R.string.no_camera_permission;
229 } else {
230 throw new IllegalStateException("Invalid permission result request");
231 }
232 Toast.makeText(this, res, Toast.LENGTH_SHORT).show();
233 }
234 }
235
236 @Override
237 public void onStop() {
238 releaseProximityWakeLock();
239 //TODO maybe we want to finish if call had ended
240 super.onStop();
241 }
242
243 @Override
244 public void onBackPressed() {
245 endCall();
246 super.onBackPressed();
247 }
248
249
250 private void initializeActivityWithRunningRtpSession(final Account account, Jid with, String sessionId) {
251 final WeakReference<JingleRtpConnection> reference = xmppConnectionService.getJingleConnectionManager()
252 .findJingleRtpConnection(account, with, sessionId);
253 if (reference == null || reference.get() == null) {
254 finish();
255 return;
256 }
257 this.rtpConnectionReference = reference;
258 final RtpEndUserState currentState = requireRtpConnection().getEndUserState();
259 if (currentState == RtpEndUserState.ENDED) {
260 finish();
261 return;
262 }
263 if (currentState == RtpEndUserState.INCOMING_CALL) {
264 getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
265 }
266 if (JingleRtpConnection.STATES_SHOWING_ONGOING_CALL.contains(requireRtpConnection().getState())) {
267 putScreenInCallMode();
268 }
269 binding.with.setText(getWith().getDisplayName());
270 updateStateDisplay(currentState);
271 updateButtonConfiguration(currentState);
272 }
273
274 private void reInitializeActivityWithRunningRapSession(final Account account, Jid with, String sessionId) {
275 runOnUiThread(() -> {
276 initializeActivityWithRunningRtpSession(account, with, sessionId);
277 });
278 final Intent intent = new Intent(Intent.ACTION_VIEW);
279 intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
280 intent.putExtra(EXTRA_WITH, with.toEscapedString());
281 intent.putExtra(EXTRA_SESSION_ID, sessionId);
282 setIntent(intent);
283 }
284
285 private void updateStateDisplay(final RtpEndUserState state) {
286 switch (state) {
287 case INCOMING_CALL:
288 binding.status.setText(R.string.rtp_state_incoming_call);
289 break;
290 case CONNECTING:
291 binding.status.setText(R.string.rtp_state_connecting);
292 break;
293 case CONNECTED:
294 binding.status.setText(R.string.rtp_state_connected);
295 break;
296 case ACCEPTING_CALL:
297 binding.status.setText(R.string.rtp_state_accepting_call);
298 break;
299 case ENDING_CALL:
300 binding.status.setText(R.string.rtp_state_ending_call);
301 break;
302 case FINDING_DEVICE:
303 binding.status.setText(R.string.rtp_state_finding_device);
304 break;
305 case RINGING:
306 binding.status.setText(R.string.rtp_state_ringing);
307 break;
308 case DECLINED_OR_BUSY:
309 binding.status.setText(R.string.rtp_state_declined_or_busy);
310 break;
311 case CONNECTIVITY_ERROR:
312 binding.status.setText(R.string.rtp_state_connectivity_error);
313 break;
314 case APPLICATION_ERROR:
315 binding.status.setText(R.string.rtp_state_application_failure);
316 break;
317 case ENDED:
318 throw new IllegalStateException("Activity should have called finishAndReleaseWakeLock();");
319 default:
320 throw new IllegalStateException(String.format("State %s has not been handled in UI", state));
321 }
322 }
323
324 @SuppressLint("RestrictedApi")
325 private void updateButtonConfiguration(final RtpEndUserState state) {
326 if (state == RtpEndUserState.INCOMING_CALL) {
327 this.binding.rejectCall.setOnClickListener(this::rejectCall);
328 this.binding.rejectCall.setImageResource(R.drawable.ic_call_end_white_48dp);
329 this.binding.rejectCall.setVisibility(View.VISIBLE);
330 this.binding.endCall.setVisibility(View.INVISIBLE);
331 this.binding.acceptCall.setOnClickListener(this::acceptCall);
332 this.binding.acceptCall.setImageResource(R.drawable.ic_call_white_48dp);
333 this.binding.acceptCall.setVisibility(View.VISIBLE);
334 } else if (state == RtpEndUserState.ENDING_CALL) {
335 this.binding.rejectCall.setVisibility(View.INVISIBLE);
336 this.binding.endCall.setVisibility(View.INVISIBLE);
337 this.binding.acceptCall.setVisibility(View.INVISIBLE);
338 } else if (state == RtpEndUserState.DECLINED_OR_BUSY) {
339 this.binding.rejectCall.setVisibility(View.INVISIBLE);
340 this.binding.endCall.setOnClickListener(this::exit);
341 this.binding.endCall.setImageResource(R.drawable.ic_clear_white_48dp);
342 this.binding.endCall.setVisibility(View.VISIBLE);
343 this.binding.acceptCall.setVisibility(View.INVISIBLE);
344 } else if (state == RtpEndUserState.CONNECTIVITY_ERROR || state == RtpEndUserState.APPLICATION_ERROR) {
345 this.binding.rejectCall.setOnClickListener(this::exit);
346 this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp);
347 this.binding.rejectCall.setVisibility(View.VISIBLE);
348 this.binding.endCall.setVisibility(View.INVISIBLE);
349 this.binding.acceptCall.setOnClickListener(this::retry);
350 this.binding.acceptCall.setImageResource(R.drawable.ic_replay_white_48dp);
351 this.binding.acceptCall.setVisibility(View.VISIBLE);
352 } else {
353 this.binding.rejectCall.setVisibility(View.INVISIBLE);
354 this.binding.endCall.setOnClickListener(this::endCall);
355 this.binding.endCall.setImageResource(R.drawable.ic_call_end_white_48dp);
356 this.binding.endCall.setVisibility(View.VISIBLE);
357 this.binding.acceptCall.setVisibility(View.INVISIBLE);
358 }
359 updateInCallButtonConfiguration(state);
360 }
361
362 private void updateInCallButtonConfiguration() {
363 updateInCallButtonConfiguration(requireRtpConnection().getEndUserState());
364 }
365
366 @SuppressLint("RestrictedApi")
367 private void updateInCallButtonConfiguration(final RtpEndUserState state) {
368 if (state == RtpEndUserState.CONNECTED) {
369 final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager();
370 updateInCallButtonConfiguration(
371 audioManager.getSelectedAudioDevice(),
372 audioManager.getAudioDevices().size(),
373 requireRtpConnection().isMicrophoneEnabled()
374 );
375 } else {
376 this.binding.inCallActionLeft.setVisibility(View.GONE);
377 this.binding.inCallActionRight.setVisibility(View.GONE);
378 }
379 }
380
381 @SuppressLint("RestrictedApi")
382 private void updateInCallButtonConfiguration(final AppRTCAudioManager.AudioDevice selectedAudioDevice, final int numberOfChoices, final boolean microphoneEnabled) {
383 switch (selectedAudioDevice) {
384 case EARPIECE:
385 this.binding.inCallActionLeft.setImageResource(R.drawable.ic_volume_off_black_24dp);
386 if (numberOfChoices >= 2) {
387 this.binding.inCallActionLeft.setOnClickListener(this::switchToSpeaker);
388 } else {
389 this.binding.inCallActionLeft.setOnClickListener(null);
390 this.binding.inCallActionLeft.setClickable(false);
391 }
392 break;
393 case WIRED_HEADSET:
394 this.binding.inCallActionLeft.setImageResource(R.drawable.ic_headset_black_24dp);
395 this.binding.inCallActionLeft.setOnClickListener(null);
396 this.binding.inCallActionLeft.setClickable(false);
397 break;
398 case SPEAKER_PHONE:
399 this.binding.inCallActionLeft.setImageResource(R.drawable.ic_volume_up_black_24dp);
400 if (numberOfChoices >= 2) {
401 this.binding.inCallActionLeft.setOnClickListener(this::switchToEarpiece);
402 } else {
403 this.binding.inCallActionLeft.setOnClickListener(null);
404 this.binding.inCallActionLeft.setClickable(false);
405 }
406 break;
407 case BLUETOOTH:
408 this.binding.inCallActionLeft.setImageResource(R.drawable.ic_bluetooth_audio_black_24dp);
409 this.binding.inCallActionLeft.setOnClickListener(null);
410 this.binding.inCallActionLeft.setClickable(false);
411 break;
412 }
413 this.binding.inCallActionLeft.setVisibility(View.VISIBLE);
414 if (microphoneEnabled) {
415 this.binding.inCallActionRight.setImageResource(R.drawable.ic_mic_black_24dp);
416 this.binding.inCallActionRight.setOnClickListener(this::disableMicrophone);
417 } else {
418 this.binding.inCallActionRight.setImageResource(R.drawable.ic_mic_off_black_24dp);
419 this.binding.inCallActionRight.setOnClickListener(this::enableMicrophone);
420 }
421 this.binding.inCallActionRight.setVisibility(View.VISIBLE);
422 }
423
424 private void disableMicrophone(View view) {
425 JingleRtpConnection rtpConnection = requireRtpConnection();
426 rtpConnection.setMicrophoneEnabled(false);
427 updateInCallButtonConfiguration();
428 }
429
430 private void enableMicrophone(View view) {
431 JingleRtpConnection rtpConnection = requireRtpConnection();
432 rtpConnection.setMicrophoneEnabled(true);
433 updateInCallButtonConfiguration();
434 }
435
436 private void switchToEarpiece(View view) {
437 requireRtpConnection().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE);
438 acquireProximityWakeLock();
439 }
440
441 private void switchToSpeaker(View view) {
442 requireRtpConnection().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE);
443 releaseProximityWakeLock();
444 }
445
446 private void retry(View view) {
447 Log.d(Config.LOGTAG, "attempting retry");
448 final Intent intent = getIntent();
449 final Account account = extractAccount(intent);
450 final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH));
451 this.rtpConnectionReference = null;
452 proposeJingleRtpSession(account, with);
453 }
454
455 private void exit(View view) {
456 finish();
457 }
458
459 private Contact getWith() {
460 final AbstractJingleConnection.Id id = requireRtpConnection().getId();
461 final Account account = id.account;
462 return account.getRoster().getContact(id.with);
463 }
464
465 private JingleRtpConnection requireRtpConnection() {
466 final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
467 if (connection == null) {
468 throw new IllegalStateException("No RTP connection found");
469 }
470 return connection;
471 }
472
473 @Override
474 public void onJingleRtpConnectionUpdate(Account account, Jid with, final String sessionId, RtpEndUserState state) {
475 if (Arrays.asList(RtpEndUserState.APPLICATION_ERROR, RtpEndUserState.DECLINED_OR_BUSY, RtpEndUserState.DECLINED_OR_BUSY).contains(state)) {
476 releaseProximityWakeLock();
477 }
478 Log.d(Config.LOGTAG, "onJingleRtpConnectionUpdate(" + state + ")");
479 if (with.isBareJid()) {
480 updateRtpSessionProposalState(account, with, state);
481 return;
482 }
483 if (this.rtpConnectionReference == null) {
484 //this happens when going from proposed session to actual session
485 reInitializeActivityWithRunningRapSession(account, with, sessionId);
486 return;
487 }
488 final AbstractJingleConnection.Id id = requireRtpConnection().getId();
489 if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) {
490 if (state == RtpEndUserState.ENDED) {
491 finish();
492 return;
493 } else if (asList(RtpEndUserState.APPLICATION_ERROR, RtpEndUserState.DECLINED_OR_BUSY, RtpEndUserState.CONNECTIVITY_ERROR).contains(state)) {
494 resetIntent(account, with, state);
495 }
496 runOnUiThread(() -> {
497 updateStateDisplay(state);
498 updateButtonConfiguration(state);
499 });
500 } else {
501 Log.d(Config.LOGTAG, "received update for other rtp session");
502 //TODO if we only ever have one; we might just switch over? Maybe!
503 }
504 }
505
506 @Override
507 public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
508 Log.d(Config.LOGTAG, "onAudioDeviceChanged in activity: selected:" + selectedAudioDevice + ", available:" + availableAudioDevices);
509 try {
510 if (requireRtpConnection().getEndUserState() == RtpEndUserState.CONNECTED) {
511 final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager();
512 updateInCallButtonConfiguration(
513 audioManager.getSelectedAudioDevice(),
514 audioManager.getAudioDevices().size(),
515 requireRtpConnection().isMicrophoneEnabled()
516 );
517 }
518 putProximityWakeLockInProperState();
519 } catch (IllegalStateException e) {
520 Log.d(Config.LOGTAG, "RTP connection was not available when audio device changed");
521 }
522 }
523
524 private void updateRtpSessionProposalState(final Account account, final Jid with, final RtpEndUserState state) {
525 final Intent currentIntent = getIntent();
526 final String withExtra = currentIntent == null ? null : currentIntent.getStringExtra(EXTRA_WITH);
527 if (withExtra == null) {
528 return;
529 }
530 if (Jid.ofEscaped(withExtra).asBareJid().equals(with)) {
531 runOnUiThread(() -> {
532 updateStateDisplay(state);
533 updateButtonConfiguration(state);
534 });
535 resetIntent(account, with, state);
536 }
537 }
538
539 private void resetIntent(final Bundle extras) {
540 final Intent intent = new Intent(Intent.ACTION_VIEW);
541 intent.putExtras(extras);
542 setIntent(intent);
543 }
544
545 private void resetIntent(final Account account, Jid with, final RtpEndUserState state) {
546 final Intent intent = new Intent(Intent.ACTION_VIEW);
547 intent.putExtra(EXTRA_WITH, with.asBareJid().toEscapedString());
548 intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
549 intent.putExtra(EXTRA_LAST_REPORTED_STATE, state.toString());
550 setIntent(intent);
551 }
552}