RtpSessionActivity.java

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