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