RtpSessionActivity.java

  1package eu.siacs.conversations.ui;
  2
  3import android.Manifest;
  4import android.annotation.SuppressLint;
  5import android.content.Intent;
  6import android.databinding.DataBindingUtil;
  7import android.os.Bundle;
  8import android.support.annotation.NonNull;
  9import android.support.annotation.StringRes;
 10import android.util.Log;
 11import android.view.View;
 12import android.view.WindowManager;
 13import android.widget.Toast;
 14
 15import com.google.common.collect.ImmutableList;
 16
 17import java.lang.ref.WeakReference;
 18
 19import eu.siacs.conversations.Config;
 20import eu.siacs.conversations.R;
 21import eu.siacs.conversations.databinding.ActivityRtpSessionBinding;
 22import eu.siacs.conversations.entities.Account;
 23import eu.siacs.conversations.entities.Contact;
 24import eu.siacs.conversations.services.XmppConnectionService;
 25import eu.siacs.conversations.utils.PermissionUtils;
 26import eu.siacs.conversations.xmpp.jingle.AbstractJingleConnection;
 27import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
 28import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
 29import rocks.xmpp.addr.Jid;
 30
 31import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied;
 32import static java.util.Arrays.asList;
 33
 34//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
 35
 36public class RtpSessionActivity extends XmppActivity implements XmppConnectionService.OnJingleRtpConnectionUpdate {
 37
 38    private static final int REQUEST_ACCEPT_CALL = 0x1111;
 39
 40    public static final String EXTRA_WITH = "with";
 41    public static final String EXTRA_SESSION_ID = "session_id";
 42    public static final String EXTRA_LAST_REPORTED_STATE = "last_reported_state";
 43
 44    public static final String ACTION_ACCEPT_CALL = "action_accept_call";
 45    public static final String ACTION_MAKE_VOICE_CALL = "action_make_voice_call";
 46    public static final String ACTION_MAKE_VIDEO_CALL = "action_make_video_call";
 47
 48    private WeakReference<JingleRtpConnection> rtpConnectionReference;
 49
 50    private ActivityRtpSessionBinding binding;
 51
 52    @Override
 53    public void onCreate(Bundle savedInstanceState) {
 54        super.onCreate(savedInstanceState);
 55        getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
 56                | WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD
 57                | WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED
 58                | WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
 59        ;
 60        Log.d(Config.LOGTAG, "RtpSessionActivity.onCreate()");
 61        this.binding = DataBindingUtil.setContentView(this, R.layout.activity_rtp_session);
 62    }
 63
 64    @Override
 65    public void onStart() {
 66        super.onStart();
 67        Log.d(Config.LOGTAG, "RtpSessionActivity.onStart()");
 68    }
 69
 70    private void endCall(View view) {
 71        if (this.rtpConnectionReference == null) {
 72            final Intent intent = getIntent();
 73            final Account account = extractAccount(intent);
 74            final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH));
 75            xmppConnectionService.getJingleConnectionManager().retractSessionProposal(account, with.asBareJid());
 76            finish();
 77        } else {
 78            requireRtpConnection().endCall();
 79        }
 80    }
 81
 82    private void rejectCall(View view) {
 83        requireRtpConnection().rejectCall();
 84        finish();
 85    }
 86
 87    private void acceptCall(View view) {
 88        requestPermissionsAndAcceptCall();
 89    }
 90
 91    private void requestPermissionsAndAcceptCall() {
 92        if (PermissionUtils.hasPermission(this, ImmutableList.of(Manifest.permission.RECORD_AUDIO), REQUEST_ACCEPT_CALL)) {
 93            requireRtpConnection().acceptCall();
 94        }
 95    }
 96
 97    @Override
 98    protected void refreshUiReal() {
 99
100    }
101
102    @Override
103    public void onNewIntent(final Intent intent) {
104        super.onNewIntent(intent);
105        final Account account = extractAccount(intent);
106        final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH));
107        final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID);
108        if (sessionId != null) {
109            Log.d(Config.LOGTAG, "reinitializing from onNewIntent()");
110            initializeActivityWithRunningRapSession(account, with, sessionId);
111            if (ACTION_ACCEPT_CALL.equals(intent.getAction())) {
112                Log.d(Config.LOGTAG, "accepting call from onNewIntent()");
113                requestPermissionsAndAcceptCall();
114            }
115        } else {
116            throw new IllegalStateException("received onNewIntent without sessionId");
117        }
118    }
119
120    @Override
121    void onBackendConnected() {
122        final Intent intent = getIntent();
123        final Account account = extractAccount(intent);
124        final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH));
125        final String sessionId = intent.getStringExtra(EXTRA_SESSION_ID);
126        if (sessionId != null) {
127            initializeActivityWithRunningRapSession(account, with, sessionId);
128            if (ACTION_ACCEPT_CALL.equals(intent.getAction())) {
129                Log.d(Config.LOGTAG, "intent action was accept");
130                requestPermissionsAndAcceptCall();
131            }
132        } else if (asList(ACTION_MAKE_VIDEO_CALL, ACTION_MAKE_VOICE_CALL).contains(intent.getAction())) {
133            xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(account, with);
134            binding.with.setText(account.getRoster().getContact(with).getDisplayName());
135        } else if (Intent.ACTION_VIEW.equals(intent.getAction())) {
136            final String extraLastState = intent.getStringExtra(EXTRA_LAST_REPORTED_STATE);
137            if (extraLastState != null) {
138                Log.d(Config.LOGTAG, "restored last state from intent extra");
139                RtpEndUserState state = RtpEndUserState.valueOf(extraLastState);
140                updateButtonConfiguration(state);
141                updateStateDisplay(state);
142            }
143            binding.with.setText(account.getRoster().getContact(with).getDisplayName());
144        }
145    }
146
147    @Override
148    public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
149        super.onRequestPermissionsResult(requestCode, permissions, grantResults);
150        if (PermissionUtils.allGranted(grantResults)) {
151            if (requestCode == REQUEST_ACCEPT_CALL) {
152                requireRtpConnection().acceptCall();
153            }
154        } else {
155            @StringRes int res;
156            final String firstDenied = getFirstDenied(grantResults, permissions);
157            if (Manifest.permission.RECORD_AUDIO.equals(firstDenied)) {
158                res = R.string.no_microphone_permission;
159            } else if (Manifest.permission.CAMERA.equals(firstDenied)) {
160                res = R.string.no_camera_permission;
161            } else {
162                throw new IllegalStateException("Invalid permission result request");
163            }
164            Toast.makeText(this, res, Toast.LENGTH_SHORT).show();
165        }
166    }
167
168
169    private void initializeActivityWithRunningRapSession(final Account account, Jid with, String sessionId) {
170        final WeakReference<JingleRtpConnection> reference = xmppConnectionService.getJingleConnectionManager()
171                .findJingleRtpConnection(account, with, sessionId);
172        if (reference == null || reference.get() == null) {
173            finish();
174            return;
175        }
176        this.rtpConnectionReference = reference;
177        final RtpEndUserState currentState = requireRtpConnection().getEndUserState();
178        if (currentState == RtpEndUserState.ENDED) {
179            finish();
180            return;
181        }
182        binding.with.setText(getWith().getDisplayName());
183        updateStateDisplay(currentState);
184        updateButtonConfiguration(currentState);
185    }
186
187    private void reInitializeActivityWithRunningRapSession(final Account account, Jid with, String sessionId) {
188        runOnUiThread(() -> {
189            initializeActivityWithRunningRapSession(account, with, sessionId);
190        });
191        final Intent intent = new Intent(Intent.ACTION_VIEW);
192        intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
193        intent.putExtra(EXTRA_WITH, with.toEscapedString());
194        intent.putExtra(EXTRA_SESSION_ID, sessionId);
195        setIntent(intent);
196    }
197
198    private void updateStateDisplay(final RtpEndUserState state) {
199        switch (state) {
200            case INCOMING_CALL:
201                binding.status.setText(R.string.rtp_state_incoming_call);
202                break;
203            case CONNECTING:
204                binding.status.setText(R.string.rtp_state_connecting);
205                break;
206            case CONNECTED:
207                binding.status.setText(R.string.rtp_state_connected);
208                break;
209            case ACCEPTING_CALL:
210                binding.status.setText(R.string.rtp_state_accepting_call);
211                break;
212            case ENDING_CALL:
213                binding.status.setText(R.string.rtp_state_ending_call);
214                break;
215            case FINDING_DEVICE:
216                binding.status.setText(R.string.rtp_state_finding_device);
217                break;
218            case RINGING:
219                binding.status.setText(R.string.rtp_state_ringing);
220                break;
221            case DECLINED_OR_BUSY:
222                binding.status.setText(R.string.rtp_state_declined_or_busy);
223                break;
224            case CONNECTIVITY_ERROR:
225                binding.status.setText(R.string.rtp_state_connectivity_error);
226                break;
227            case APPLICATION_ERROR:
228                binding.status.setText(R.string.rtp_state_application_failure);
229                break;
230            case ENDED:
231                throw new IllegalStateException("Activity should have called finish()");
232            default:
233                throw new IllegalStateException(String.format("State %s has not been handled in UI", state));
234        }
235    }
236
237    @SuppressLint("RestrictedApi")
238    private void updateButtonConfiguration(final RtpEndUserState state) {
239        if (state == RtpEndUserState.INCOMING_CALL) {
240            this.binding.rejectCall.setOnClickListener(this::rejectCall);
241            this.binding.rejectCall.setImageResource(R.drawable.ic_call_end_white_48dp);
242            this.binding.rejectCall.setVisibility(View.VISIBLE);
243            this.binding.endCall.setVisibility(View.INVISIBLE);
244            this.binding.acceptCall.setOnClickListener(this::acceptCall);
245            this.binding.acceptCall.setImageResource(R.drawable.ic_call_white_48dp);
246            this.binding.acceptCall.setVisibility(View.VISIBLE);
247        } else if (state == RtpEndUserState.ENDING_CALL) {
248            this.binding.rejectCall.setVisibility(View.INVISIBLE);
249            this.binding.endCall.setVisibility(View.INVISIBLE);
250            this.binding.acceptCall.setVisibility(View.INVISIBLE);
251        } else if (state == RtpEndUserState.DECLINED_OR_BUSY) {
252            this.binding.rejectCall.setVisibility(View.INVISIBLE);
253            this.binding.endCall.setOnClickListener(this::exit);
254            this.binding.endCall.setImageResource(R.drawable.ic_clear_white_48dp);
255            this.binding.endCall.setVisibility(View.VISIBLE);
256            this.binding.acceptCall.setVisibility(View.INVISIBLE);
257        } else if (state == RtpEndUserState.CONNECTIVITY_ERROR || state == RtpEndUserState.APPLICATION_ERROR) {
258            this.binding.rejectCall.setOnClickListener(this::exit);
259            this.binding.rejectCall.setImageResource(R.drawable.ic_clear_white_48dp);
260            this.binding.rejectCall.setVisibility(View.VISIBLE);
261            this.binding.endCall.setVisibility(View.INVISIBLE);
262            this.binding.acceptCall.setOnClickListener(this::retry);
263            this.binding.acceptCall.setImageResource(R.drawable.ic_replay_white_48dp);
264            this.binding.acceptCall.setVisibility(View.VISIBLE);
265        } else {
266            this.binding.rejectCall.setVisibility(View.INVISIBLE);
267            this.binding.endCall.setOnClickListener(this::endCall);
268            this.binding.endCall.setImageResource(R.drawable.ic_call_end_white_48dp);
269            this.binding.endCall.setVisibility(View.VISIBLE);
270            this.binding.acceptCall.setVisibility(View.INVISIBLE);
271        }
272    }
273
274    private void retry(View view) {
275        Log.d(Config.LOGTAG, "attempting retry");
276        final Intent intent = getIntent();
277        final Account account = extractAccount(intent);
278        final Jid with = Jid.of(intent.getStringExtra(EXTRA_WITH));
279        this.rtpConnectionReference = null;
280        xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(account, with);
281    }
282
283    private void exit(View view) {
284        finish();
285    }
286
287    private Contact getWith() {
288        final AbstractJingleConnection.Id id = requireRtpConnection().getId();
289        final Account account = id.account;
290        return account.getRoster().getContact(id.with);
291    }
292
293    private JingleRtpConnection requireRtpConnection() {
294        final JingleRtpConnection connection = this.rtpConnectionReference != null ? this.rtpConnectionReference.get() : null;
295        if (connection == null) {
296            throw new IllegalStateException("No RTP connection found");
297        }
298        return connection;
299    }
300
301    @Override
302    public void onJingleRtpConnectionUpdate(Account account, Jid with, final String sessionId, RtpEndUserState state) {
303        Log.d(Config.LOGTAG, "onJingleRtpConnectionUpdate(" + state + ")");
304        if (with.isBareJid()) {
305            updateRtpSessionProposalState(account, with, state);
306            return;
307        }
308        if (this.rtpConnectionReference == null) {
309            //this happens when going from proposed session to actual session
310            reInitializeActivityWithRunningRapSession(account, with, sessionId);
311            return;
312        }
313        final AbstractJingleConnection.Id id = requireRtpConnection().getId();
314        if (account == id.account && id.with.equals(with) && id.sessionId.equals(sessionId)) {
315            if (state == RtpEndUserState.ENDED) {
316                finish();
317                return;
318            } else if (asList(RtpEndUserState.APPLICATION_ERROR, RtpEndUserState.DECLINED_OR_BUSY, RtpEndUserState.CONNECTIVITY_ERROR).contains(state)) {
319                resetIntent(account, with, state);
320            }
321            runOnUiThread(() -> {
322                updateStateDisplay(state);
323                updateButtonConfiguration(state);
324            });
325        } else {
326            Log.d(Config.LOGTAG, "received update for other rtp session");
327            //TODO if we only ever have one; we might just switch over? Maybe!
328        }
329    }
330
331    private void updateRtpSessionProposalState(final Account account, final Jid with, final RtpEndUserState state) {
332        final Intent currentIntent = getIntent();
333        final String withExtra = currentIntent == null ? null : currentIntent.getStringExtra(EXTRA_WITH);
334        if (withExtra == null) {
335            return;
336        }
337        if (Jid.ofEscaped(withExtra).asBareJid().equals(with)) {
338            runOnUiThread(() -> {
339                updateStateDisplay(state);
340                updateButtonConfiguration(state);
341            });
342            resetIntent(account, with, state);
343        }
344    }
345
346    private void resetIntent(final Account account, Jid with, final RtpEndUserState state) {
347        Log.d(Config.LOGTAG, "resetting intent");
348        final Intent intent = new Intent(Intent.ACTION_VIEW);
349        intent.putExtra(EXTRA_WITH, with.asBareJid().toEscapedString());
350        intent.putExtra(EXTRA_ACCOUNT, account.getJid().toEscapedString());
351        intent.putExtra(EXTRA_LAST_REPORTED_STATE, state.toString());
352        setIntent(intent);
353    }
354}