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.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 wake lock");
139            this.mProximityWakeLock.release();
140            this.mProximityWakeLock = null;
141        }
142    }
143
144    @Override
145    protected void refreshUiReal() {
146
147    }
148
149    @Override
150    public void onNewIntent(final Intent intent) {
151        super.onNewIntent(intent);
152        setIntent(intent);
153        if (xmppConnectionService == null) {
154            Log.d(Config.LOGTAG, "RtpSessionActivity: background service wasn't bound in onNewIntent()");
155            return;
156        }
157        final Account account = extractAccount(intent);
158        final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH));
159        final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID);
160        if (sessionId != null) {
161            Log.d(Config.LOGTAG, "reinitializing from onNewIntent()");
162            initializeActivityWithRunningRtpSession(account, with, sessionId);
163            if (ACTION_ACCEPT_CALL.equals(intent.getAction())) {
164                Log.d(Config.LOGTAG, "accepting call from onNewIntent()");
165                requestPermissionsAndAcceptCall();
166                resetIntent(intent.getExtras());
167            }
168        } else {
169            throw new IllegalStateException("received onNewIntent without sessionId");
170        }
171    }
172
173    @Override
174    void onBackendConnected() {
175        final Intent intent = getIntent();
176        final Account account = extractAccount(intent);
177        final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH));
178        final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID);
179        if (sessionId != null) {
180            initializeActivityWithRunningRtpSession(account, with, sessionId);
181            if (ACTION_ACCEPT_CALL.equals(intent.getAction())) {
182                Log.d(Config.LOGTAG, "intent action was accept");
183                requestPermissionsAndAcceptCall();
184                resetIntent(intent.getExtras());
185            }
186        } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(intent.getAction())) {
187            proposeJingleRtpSession(account, with);
188            binding.with.setText(account.getRoster().getContact(with).getDisplayName());
189        } else if (Intent.ACTION_VIEW.equals(intent.getAction())) {
190            final String extraLastState = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE);
191            if (extraLastState != null) {
192                Log.d(Config.LOGTAG, "restored last state from intent extra");
193                RtpEndUserState state = RtpEndUserState.valueOf(extraLastState);
194                updateButtonConfiguration(state);
195                updateStateDisplay(state);
196            }
197            binding.with.setText(account.getRoster().getContact(with).getDisplayName());
198        }
199    }
200
201    private void proposeJingleRtpSession(final Account account, final Jid with) {
202        xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(account, with);
203        putScreenInCallMode();
204    }
205
206    @Override
207    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
208        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
209        if (PermissionUtils.allGranted(grantResults)) {
210            if (requestCode == REQUEST_ACCEPT_CALL) {
211                requireRtpConnection().acceptCall();
212            }
213        } else {
214            @StringRes int res;
215            final String firstDenied = getFirstDenied(grantResults, permissions);
216            if (Manifest.permission.RECORD_AUDIO.equals(firstDenied)) {
217                res = R.string.no_microphone_permission;
218            } else if (Manifest.permission.CAMERA.equals(firstDenied)) {
219                res = R.string.no_camera_permission;
220            } else {
221                throw new IllegalStateException("Invalid permission result request");
222            }
223            Toast.makeText(this, res, Toast.LENGTH_SHORT).show();
224        }
225    }
226
227    @Override
228    public void onStop() {
229        releaseProximityWakeLock();
230        //TODO maybe we want to finish if call had ended
231        super.onStop();
232    }
233
234    @Override
235    public void onBackPressed() {
236        endCall();
237        super.onBackPressed();
238    }
239
240
241    private void initializeActivityWithRunningRtpSession(final Account account, Jid with, String sessionId) {
242        final WeakReference<JingleRtpConnection> reference = xmppConnectionService.getJingleConnectionManager()
243                .findJingleRtpConnection(account, with, sessionId);
244        if (reference == null || reference.get() == null) {
245            finish();
246            return;
247        }
248        this.rtpConnectionReference = reference;
249        final RtpEndUserState currentState = requireRtpConnection().getEndUserState();
250        if (currentState == RtpEndUserState.ENDED) {
251            finish();
252            return;
253        }
254        if (currentState == RtpEndUserState.INCOMING_CALL) {
255            getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
256        }
257        if (JingleRtpConnection.STATES_SHOWING_ONGOING_CALL.contains(requireRtpConnection().getState())) {
258            putScreenInCallMode();
259        }
260        binding.with.setText(getWith().getDisplayName());
261        updateStateDisplay(currentState);
262        updateButtonConfiguration(currentState);
263    }
264
265    private void reInitializeActivityWithRunningRapSession(final Account account, Jid with, String sessionId) {
266        runOnUiThread(() -> {
267            initializeActivityWithRunningRtpSession(account, with, sessionId);
268        });
269        final Intent intent = new Intent(Intent.ACTION_VIEW);
270        intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
271        intent.putExtra(EXTRA_WITH, with.toEscapedString());
272        intent.putExtra(EXTRA_SESSION_ID, sessionId);
273        setIntent(intent);
274    }
275
276    private void updateStateDisplay(final RtpEndUserState state) {
277        switch (state) {
278            case INCOMING_CALL:
279                binding.status.setText(R.string.rtp_state_incoming_call);
280                break;
281            case CONNECTING:
282                binding.status.setText(R.string.rtp_state_connecting);
283                break;
284            case CONNECTED:
285                binding.status.setText(R.string.rtp_state_connected);
286                break;
287            case ACCEPTING_CALL:
288                binding.status.setText(R.string.rtp_state_accepting_call);
289                break;
290            case ENDING_CALL:
291                binding.status.setText(R.string.rtp_state_ending_call);
292                break;
293            case FINDING_DEVICE:
294                binding.status.setText(R.string.rtp_state_finding_device);
295                break;
296            case RINGING:
297                binding.status.setText(R.string.rtp_state_ringing);
298                break;
299            case DECLINED_OR_BUSY:
300                binding.status.setText(R.string.rtp_state_declined_or_busy);
301                break;
302            case CONNECTIVITY_ERROR:
303                binding.status.setText(R.string.rtp_state_connectivity_error);
304                break;
305            case APPLICATION_ERROR:
306                binding.status.setText(R.string.rtp_state_application_failure);
307                break;
308            case ENDED:
309                throw new IllegalStateException("Activity should have called finishAndReleaseWakeLock();");
310            default:
311                throw new IllegalStateException(String.format("State %s has not been handled in UI", state));
312        }
313    }
314
315    @SuppressLint("RestrictedApi")
316    private void updateButtonConfiguration(final RtpEndUserState state) {
317        if (state == RtpEndUserState.INCOMING_CALL) {
318            this.binding.rejectCall.setOnClickListener(this::rejectCall);
319            this.binding.rejectCall.setImageResource(R.drawable.ic_call_end_white_48dp);
320            this.binding.rejectCall.setVisibility(View.VISIBLE);
321            this.binding.endCall.setVisibility(View.INVISIBLE);
322            this.binding.acceptCall.setOnClickListener(this::acceptCall);
323            this.binding.acceptCall.setImageResource(R.drawable.ic_call_white_48dp);
324            this.binding.acceptCall.setVisibility(View.VISIBLE);
325        } else if (state == RtpEndUserState.ENDING_CALL) {
326            this.binding.rejectCall.setVisibility(View.INVISIBLE);
327            this.binding.endCall.setVisibility(View.INVISIBLE);
328            this.binding.acceptCall.setVisibility(View.INVISIBLE);
329        } else if (state == RtpEndUserState.DECLINED_OR_BUSY) {
330            this.binding.rejectCall.setVisibility(View.INVISIBLE);
331            this.binding.endCall.setOnClickListener(this::exit);
332            this.binding.endCall.setImageResource(R.drawable.ic_clear_white_48dp);
333            this.binding.endCall.setVisibility(View.VISIBLE);
334            this.binding.acceptCall.setVisibility(View.INVISIBLE);
335        } else if (state == RtpEndUserState.CONNECTIVITY_ERROR || state == RtpEndUserState.APPLICATION_ERROR) {
336            this.binding.rejectCall.setOnClickListener(this::exit);
337            this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp);
338            this.binding.rejectCall.setVisibility(View.VISIBLE);
339            this.binding.endCall.setVisibility(View.INVISIBLE);
340            this.binding.acceptCall.setOnClickListener(this::retry);
341            this.binding.acceptCall.setImageResource(R.drawable.ic_replay_white_48dp);
342            this.binding.acceptCall.setVisibility(View.VISIBLE);
343        } else {
344            this.binding.rejectCall.setVisibility(View.INVISIBLE);
345            this.binding.endCall.setOnClickListener(this::endCall);
346            this.binding.endCall.setImageResource(R.drawable.ic_call_end_white_48dp);
347            this.binding.endCall.setVisibility(View.VISIBLE);
348            this.binding.acceptCall.setVisibility(View.INVISIBLE);
349        }
350        updateInCallButtonConfiguration(state);
351    }
352
353    private void updateInCallButtonConfiguration() {
354        updateInCallButtonConfiguration(requireRtpConnection().getEndUserState());
355    }
356
357    @SuppressLint("RestrictedApi")
358    private void updateInCallButtonConfiguration(final RtpEndUserState state) {
359        if (state == RtpEndUserState.CONNECTED) {
360            final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager();
361            updateInCallButtonConfiguration(
362                    audioManager.getSelectedAudioDevice(),
363                    audioManager.getAudioDevices().size(),
364                    requireRtpConnection().isMicrophoneEnabled()
365            );
366        } else {
367            this.binding.inCallActionLeft.setVisibility(View.GONE);
368            this.binding.inCallActionRight.setVisibility(View.GONE);
369        }
370    }
371
372    @SuppressLint("RestrictedApi")
373    private void updateInCallButtonConfiguration(final AppRTCAudioManager.AudioDevice selectedAudioDevice, final int numberOfChoices, final boolean microphoneEnabled) {
374        switch (selectedAudioDevice) {
375            case EARPIECE:
376                this.binding.inCallActionLeft.setImageResource(R.drawable.ic_volume_off_black_24dp);
377                if (numberOfChoices >= 2) {
378                    this.binding.inCallActionLeft.setOnClickListener(this::switchToSpeaker);
379                } else {
380                    this.binding.inCallActionLeft.setOnClickListener(null);
381                    this.binding.inCallActionLeft.setClickable(false);
382                }
383                break;
384            case WIRED_HEADSET:
385                this.binding.inCallActionLeft.setImageResource(R.drawable.ic_headset_black_24dp);
386                this.binding.inCallActionLeft.setOnClickListener(null);
387                this.binding.inCallActionLeft.setClickable(false);
388                break;
389            case SPEAKER_PHONE:
390                this.binding.inCallActionLeft.setImageResource(R.drawable.ic_volume_up_black_24dp);
391                if (numberOfChoices >= 2) {
392                    this.binding.inCallActionLeft.setOnClickListener(this::switchToEarpiece);
393                } else {
394                    this.binding.inCallActionLeft.setOnClickListener(null);
395                    this.binding.inCallActionLeft.setClickable(false);
396                }
397                break;
398            case BLUETOOTH:
399                this.binding.inCallActionLeft.setImageResource(R.drawable.ic_bluetooth_audio_black_24dp);
400                this.binding.inCallActionLeft.setOnClickListener(null);
401                this.binding.inCallActionLeft.setClickable(false);
402                break;
403        }
404        this.binding.inCallActionLeft.setVisibility(View.VISIBLE);
405        if (microphoneEnabled) {
406            this.binding.inCallActionRight.setImageResource(R.drawable.ic_mic_black_24dp);
407            this.binding.inCallActionRight.setOnClickListener(this::disableMicrophone);
408        } else {
409            this.binding.inCallActionRight.setImageResource(R.drawable.ic_mic_off_black_24dp);
410            this.binding.inCallActionRight.setOnClickListener(this::enableMicrophone);
411        }
412        this.binding.inCallActionRight.setVisibility(View.VISIBLE);
413    }
414
415    private void disableMicrophone(View view) {
416        JingleRtpConnection rtpConnection = requireRtpConnection();
417        rtpConnection.setMicrophoneEnabled(false);
418        updateInCallButtonConfiguration();
419    }
420
421    private void enableMicrophone(View view) {
422        JingleRtpConnection rtpConnection = requireRtpConnection();
423        rtpConnection.setMicrophoneEnabled(true);
424        updateInCallButtonConfiguration();
425    }
426
427    private void switchToEarpiece(View view) {
428        requireRtpConnection().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE);
429        acquireProximityWakeLock();
430    }
431
432    private void switchToSpeaker(View view) {
433        requireRtpConnection().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE);
434        releaseProximityWakeLock();
435    }
436
437    private void retry(View view) {
438        Log.d(Config.LOGTAG, "attempting retry");
439        final Intent intent = getIntent();
440        final Account account = extractAccount(intent);
441        final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH));
442        this.rtpConnectionReference = null;
443        proposeJingleRtpSession(account, with);
444    }
445
446    private void exit(View view) {
447        finish();
448    }
449
450    private Contact getWith() {
451        final AbstractJingleConnection.Id id = requireRtpConnection().getId();
452        final Account account = id.account;
453        return account.getRoster().getContact(id.with);
454    }
455
456    private JingleRtpConnection requireRtpConnection() {
457        final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
458        if (connection == null) {
459            throw new IllegalStateException("No RTP connection found");
460        }
461        return connection;
462    }
463
464    @Override
465    public void onJingleRtpConnectionUpdate(Account account, Jid with, final String sessionId, RtpEndUserState state) {
466        if (Arrays.asList(RtpEndUserState.APPLICATION_ERROR, RtpEndUserState.DECLINED_OR_BUSY, RtpEndUserState.DECLINED_OR_BUSY).contains(state)) {
467            releaseProximityWakeLock();
468        }
469        Log.d(Config.LOGTAG, "onJingleRtpConnectionUpdate(" + state + ")");
470        if (with.isBareJid()) {
471            updateRtpSessionProposalState(account, with, state);
472            return;
473        }
474        if (this.rtpConnectionReference == null) {
475            //this happens when going from proposed session to actual session
476            reInitializeActivityWithRunningRapSession(account, with, sessionId);
477            return;
478        }
479        final AbstractJingleConnection.Id id = requireRtpConnection().getId();
480        if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) {
481            if (state == RtpEndUserState.ENDED) {
482                finish();
483                return;
484            } else if (asList(RtpEndUserState.APPLICATION_ERROR, RtpEndUserState.DECLINED_OR_BUSY, RtpEndUserState.CONNECTIVITY_ERROR).contains(state)) {
485                resetIntent(account, with, state);
486            }
487            runOnUiThread(() -> {
488                updateStateDisplay(state);
489                updateButtonConfiguration(state);
490            });
491        } else {
492            Log.d(Config.LOGTAG, "received update for other rtp session");
493            //TODO if we only ever have one; we might just switch over? Maybe!
494        }
495    }
496
497    @Override
498    public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
499        Log.d(Config.LOGTAG, "onAudioDeviceChanged in activity: selected:" + selectedAudioDevice + ", available:" + availableAudioDevices);
500        try {
501            if (requireRtpConnection().getEndUserState() == RtpEndUserState.CONNECTED) {
502                final AppRTCAudioManager audioManager = requireRtpConnection().getAudioManager();
503                updateInCallButtonConfiguration(
504                        audioManager.getSelectedAudioDevice(),
505                        audioManager.getAudioDevices().size(),
506                        requireRtpConnection().isMicrophoneEnabled()
507                );
508            }
509        } catch (IllegalStateException e) {
510            Log.d(Config.LOGTAG, "RTP connection was not available when audio device changed");
511        }
512    }
513
514    private void updateRtpSessionProposalState(final Account account, final Jid with, final RtpEndUserState state) {
515        final Intent currentIntent = getIntent();
516        final String withExtra = currentIntent == null ? null : currentIntent.getStringExtra(EXTRA_WITH);
517        if (withExtra == null) {
518            return;
519        }
520        if (Jid.ofEscaped(withExtra).asBareJid().equals(with)) {
521            runOnUiThread(() -> {
522                updateStateDisplay(state);
523                updateButtonConfiguration(state);
524            });
525            resetIntent(account, with, state);
526        }
527    }
528
529    private void resetIntent(final Bundle extras) {
530        final Intent intent = new Intent(Intent.ACTION_VIEW);
531        intent.putExtras(extras);
532        setIntent(intent);
533    }
534
535    private void resetIntent(final Account account, Jid with, final RtpEndUserState state) {
536        final Intent intent = new Intent(Intent.ACTION_VIEW);
537        intent.putExtra(EXTRA_WITH, with.asBareJid().toEscapedString());
538        intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
539        intent.putExtra(EXTRA_LAST_REPORTED_STATE, state.toString());
540        setIntent(intent);
541    }
542}