1package com.cheogram.android;
2
3import java.lang.ref.WeakReference;
4import java.util.Collections;
5import java.util.HashSet;
6import java.util.Set;
7import java.util.Stack;
8import java.util.Vector;
9
10import com.google.common.base.Joiner;
11import com.google.common.collect.ImmutableSet;
12
13import android.os.Build;
14import android.telecom.CallAudioState;
15import android.telecom.Connection;
16import android.telecom.ConnectionRequest;
17import android.telecom.DisconnectCause;
18import android.telecom.PhoneAccount;
19import android.telecom.PhoneAccountHandle;
20import android.telecom.StatusHints;
21import android.telecom.TelecomManager;
22import android.telephony.PhoneNumberUtils;
23
24import android.Manifest;
25
26import androidx.annotation.RequiresApi;
27import androidx.core.content.ContextCompat;
28import android.content.ComponentName;
29import android.content.Context;
30import android.content.Intent;
31import android.content.pm.PackageManager;
32import android.content.ServiceConnection;
33import android.graphics.drawable.Icon;
34import android.net.Uri;
35import android.os.Bundle;
36import android.os.IBinder;
37import android.os.Parcel;
38import android.util.Log;
39
40import com.intentfilter.androidpermissions.PermissionManager;
41import com.intentfilter.androidpermissions.NotificationSettings;
42import com.intentfilter.androidpermissions.models.DeniedPermissions;
43import io.michaelrocks.libphonenumber.android.NumberParseException;
44
45import eu.siacs.conversations.R;
46import eu.siacs.conversations.entities.Account;
47import eu.siacs.conversations.persistance.FileBackend;
48import eu.siacs.conversations.services.AvatarService;
49import eu.siacs.conversations.services.CallIntegration;
50import eu.siacs.conversations.services.CallIntegrationConnectionService;
51import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
52import eu.siacs.conversations.services.XmppConnectionService;
53import eu.siacs.conversations.ui.RtpSessionActivity;
54import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
55import eu.siacs.conversations.xmpp.Jid;
56import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
57import eu.siacs.conversations.xmpp.jingle.Media;
58import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
59import static eu.siacs.conversations.receiver.SystemEventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE;
60
61@RequiresApi(Build.VERSION_CODES.M)
62public class ConnectionService extends android.telecom.ConnectionService {
63 public XmppConnectionService xmppConnectionService = null;
64 protected ServiceConnection mConnection = new ServiceConnection() {
65 @Override
66 public void onServiceConnected(ComponentName className, IBinder service) {
67 XmppConnectionBinder binder = (XmppConnectionBinder) service;
68 xmppConnectionService = binder.getService();
69 }
70
71 @Override
72 public void onServiceDisconnected(ComponentName arg0) {
73 xmppConnectionService = null;
74 }
75 };
76
77 @Override
78 public void onCreate() {
79 // From XmppActivity.connectToBackend
80 Intent intent = new Intent(this, XmppConnectionService.class);
81 intent.setAction(XmppConnectionService.ACTION_STARTING_CALL);
82 intent.putExtra(EXTRA_NEEDS_FOREGROUND_SERVICE, true);
83 try {
84 startService(intent);
85 } catch (IllegalStateException e) {
86 Log.w("com.cheogram.android.ConnectionService", "unable to start service from " + getClass().getSimpleName());
87 }
88 bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
89 }
90
91 @Override
92 public void onDestroy() {
93 unbindService(mConnection);
94 }
95
96 @Override
97 public Connection onCreateOutgoingConnection(
98 PhoneAccountHandle phoneAccountHandle,
99 ConnectionRequest request
100 ) {
101 String[] gateway = phoneAccountHandle.getId().split("/", 2);
102
103 String rawTel = "";
104 if (request.getAddress() != null) {
105 rawTel = request.getAddress().getSchemeSpecificPart();
106 }
107 String postDial = PhoneNumberUtils.extractPostDialPortion(rawTel);
108
109 String tel = PhoneNumberUtils.extractNetworkPortion(rawTel);
110 try {
111 tel = PhoneNumberUtilWrapper.normalize(this, tel, true);
112 } catch (IllegalArgumentException | NumberParseException e) {
113 return Connection.createFailedConnection(
114 new DisconnectCause(DisconnectCause.ERROR)
115 );
116 }
117
118 if (xmppConnectionService == null) {
119 return Connection.createFailedConnection(
120 new DisconnectCause(DisconnectCause.ERROR)
121 );
122 }
123
124 if (xmppConnectionService.getJingleConnectionManager().isBusy()) {
125 return Connection.createFailedConnection(
126 new DisconnectCause(DisconnectCause.BUSY)
127 );
128 }
129
130 Account account = xmppConnectionService.findAccountByJid(Jid.of(gateway[0]));
131 if (account == null) {
132 return Connection.createFailedConnection(
133 new DisconnectCause(DisconnectCause.ERROR)
134 );
135 }
136
137 Jid with = Jid.ofLocalAndDomain(tel, gateway[1]);
138 CheogramConnection connection = new CheogramConnection(account, with, postDial);
139
140 PermissionManager permissionManager = PermissionManager.getInstance(this);
141 permissionManager.setNotificationSettings(
142 new NotificationSettings.Builder()
143 .withMessage(R.string.microphone_permission_for_call)
144 .withSmallIcon(R.drawable.ic_notification).build()
145 );
146
147 Set<String> permissions = new HashSet<>();
148 permissions.add(Manifest.permission.RECORD_AUDIO);
149 permissionManager.checkPermissions(permissions, new PermissionManager.PermissionRequestListener() {
150 @Override
151 public void onPermissionGranted() {
152 if (connection.getState() == Connection.STATE_DISCONNECTED) return;
153
154 connection.setSessionId(xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(
155 account,
156 with,
157 ImmutableSet.of(Media.AUDIO)
158 ).sessionId);
159 }
160
161 @Override
162 public void onPermissionDenied(DeniedPermissions deniedPermissions) {
163 connection.close(new DisconnectCause(DisconnectCause.ERROR));
164 }
165 });
166
167 connection.setInitializing();
168 connection.setAddress(
169 Uri.fromParts("tel", tel, null), // Normalized tel as tel: URI
170 TelecomManager.PRESENTATION_ALLOWED
171 );
172
173 xmppConnectionService.setOnRtpConnectionUpdateListener(
174 (XmppConnectionService.OnJingleRtpConnectionUpdate) connection
175 );
176
177 xmppConnectionService.setDiallerIntegrationActive(true);
178 return connection;
179 }
180
181 @Override
182 public Connection onCreateIncomingConnection(PhoneAccountHandle handle, ConnectionRequest request) {
183 final var extras = request.getExtras();
184 final var extraExtras = extras.getBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS);
185 final var accountJid = extraExtras == null ? null : extraExtras.getString("account");
186 final var withJid = extraExtras == null ? null : extraExtras.getString("with");
187 final String sessionId = extraExtras == null ? null : extraExtras.getString(CallIntegrationConnectionService.EXTRA_SESSION_ID);
188
189 if (xmppConnectionService == null) {
190 return Connection.createFailedConnection(
191 new DisconnectCause(DisconnectCause.ERROR)
192 );
193 }
194
195 Account account = xmppConnectionService.findAccountByJid(Jid.of(accountJid));
196 Jid with = Jid.of(withJid);
197
198 CheogramConnection connection = new CheogramConnection(account, with, null);
199 connection.setSessionId(sessionId);
200 connection.setAddress(
201 Uri.fromParts("tel", with.getLocal(), null),
202 TelecomManager.PRESENTATION_ALLOWED
203 );
204 connection.setCallerDisplayName(
205 account.getRoster().getContact(with).getDisplayName(),
206 TelecomManager.PRESENTATION_ALLOWED
207 );
208 connection.setRinging();
209
210 xmppConnectionService.setOnRtpConnectionUpdateListener(connection);
211
212 return connection;
213 }
214
215 public class CheogramConnection extends Connection implements XmppConnectionService.OnJingleRtpConnectionUpdate {
216 protected Account account;
217 protected Jid with;
218 protected String sessionId = null;
219 protected Stack<String> postDial = new Stack<>();
220 protected Icon gatewayIcon;
221 protected CallAudioState pendingState = null;
222 protected WeakReference<JingleRtpConnection> rtpConnection = null;
223
224 CheogramConnection(Account account, Jid with, String postDialString) {
225 super();
226 this.account = account;
227 this.with = with;
228
229 gatewayIcon = Icon.createWithBitmap(FileBackend.drawDrawable(xmppConnectionService.getAvatarService().get(
230 account.getRoster().getContact(Jid.of(with.getDomain())),
231 AvatarService.getSystemUiAvatarSize(xmppConnectionService),
232 false
233 )));
234
235 if (postDialString != null) {
236 for (int i = postDialString.length() - 1; i >= 0; i--) {
237 postDial.push("" + postDialString.charAt(i));
238 }
239 }
240
241 setCallerDisplayName(
242 account.getDisplayName(),
243 TelecomManager.PRESENTATION_ALLOWED
244 );
245 setAudioModeIsVoip(true);
246 setConnectionCapabilities(
247 Connection.CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION |
248 Connection.CAPABILITY_MUTE
249 );
250 setRingbackRequested(true);
251 }
252
253 public void setSessionId(final String sessionId) {
254 this.sessionId = sessionId;
255 }
256
257 @Override
258 public void onJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state) {
259 Log.d("com.cheogram.android.CheogramConnection", "onJingleRtpConnectionUpdate: " + with + " " + sessionId + " (== " + this.sessionId + " )? " + state);
260 if (sessionId == null || !sessionId.equals(this.sessionId)) return;
261 if (rtpConnection == null) {
262 this.with = with; // Store full JID of connection
263 findRtpConnection();
264 }
265
266 String statusLabel = null;
267
268 if (state == RtpEndUserState.FINDING_DEVICE) {
269 setInitialized();
270 } else if (state == RtpEndUserState.RINGING) {
271 setDialing();
272 } else if (state == RtpEndUserState.INCOMING_CALL) {
273 setRinging();
274 } else if (state == RtpEndUserState.CONNECTING) {
275 xmppConnectionService.setDiallerIntegrationActive(true);
276 setActive();
277 statusLabel = getString(R.string.rtp_state_connecting);
278 } else if (state == RtpEndUserState.CONNECTED) {
279 xmppConnectionService.setDiallerIntegrationActive(true);
280 setActive();
281 postDial();
282 } else if (state == RtpEndUserState.DECLINED_OR_BUSY) {
283 close(new DisconnectCause(DisconnectCause.BUSY));
284 } else if (state == RtpEndUserState.ENDED) {
285 close(new DisconnectCause(DisconnectCause.LOCAL));
286 } else if (state == RtpEndUserState.RETRACTED) {
287 close(new DisconnectCause(DisconnectCause.CANCELED));
288 } else if (RtpSessionActivity.END_CARD.contains(state)) {
289 close(new DisconnectCause(DisconnectCause.ERROR));
290 }
291
292 setStatusHints(new StatusHints(statusLabel, gatewayIcon, null));
293 }
294
295 @Override
296 public void onAudioDeviceChanged(CallIntegration.AudioDevice selectedAudioDevice, Set<CallIntegration.AudioDevice> availableAudioDevices) {
297 if (Build.VERSION.SDK_INT < 26) return;
298
299 if (pendingState != null) {
300 Log.d("com.cheogram.android.CheogramConnection", "Try with pendingState: " + pendingState);
301 onCallAudioStateChanged(pendingState);
302 return;
303 }
304
305 Log.d("com.cheogram.android.CheogramConnection", "onAudioDeviceChanged: " + selectedAudioDevice);
306
307 switch(selectedAudioDevice) {
308 case SPEAKER_PHONE:
309 setAudioRoute(CallAudioState.ROUTE_SPEAKER);
310 break;
311 case WIRED_HEADSET:
312 setAudioRoute(CallAudioState.ROUTE_WIRED_HEADSET);
313 break;
314 case EARPIECE:
315 setAudioRoute(CallAudioState.ROUTE_EARPIECE);
316 break;
317 case BLUETOOTH:
318 setAudioRoute(CallAudioState.ROUTE_BLUETOOTH);
319 break;
320 default:
321 setAudioRoute(CallAudioState.ROUTE_WIRED_OR_EARPIECE);
322 }
323 }
324
325 @Override
326 public void onCallAudioStateChanged(CallAudioState state) {
327 pendingState = null;
328 if (rtpConnection == null || rtpConnection.get() == null) {
329 pendingState = state;
330 return;
331 }
332
333 Log.d("com.cheogram.android.CheogramConnection", "onCallAudioStateChanged: " + state);
334 rtpConnection.get().callIntegration.onCallAudioStateChanged(state);
335
336 try {
337 rtpConnection.get().setMicrophoneEnabled(!state.isMuted());
338 } catch (final IllegalStateException e) {
339 pendingState = state;
340 Log.w("com.cheogram.android.CheogramConnection", "Could not set microphone mute to " + (state.isMuted() ? "true" : "false") + ": " + e.toString());
341 }
342 }
343
344 @Override
345 public void onAnswer() {
346 // For incoming calls, a connection update may not have been triggered before answering
347 // so we have to acquire the rtp connection object here
348 findRtpConnection();
349 if (rtpConnection == null || rtpConnection.get() == null) {
350 close(new DisconnectCause(DisconnectCause.CANCELED));
351 } else {
352 rtpConnection.get().acceptCall();
353 }
354 }
355
356 @Override
357 public void onReject() {
358 findRtpConnection();
359 if (rtpConnection != null && rtpConnection.get() != null) {
360 try {
361 rtpConnection.get().rejectCall();
362 } catch (final IllegalStateException e) {
363 Log.w("com.cheogram.android.CheogramConnection", e.toString());
364 }
365 }
366 close(new DisconnectCause(DisconnectCause.LOCAL));
367 }
368
369 // Set the connection to the disconnected state and clean up the resources
370 // Note that we cannot do this from onStateChanged() because calling destroy
371 // there seems to trigger a deadlock somewhere in the telephony stack.
372 public void close(DisconnectCause reason) {
373 setDisconnected(reason);
374 destroy();
375 xmppConnectionService.setDiallerIntegrationActive(false);
376 xmppConnectionService.removeRtpConnectionUpdateListener(this);
377 }
378
379 @Override
380 public void onDisconnect() {
381 if (rtpConnection == null || rtpConnection.get() == null) {
382 xmppConnectionService.getJingleConnectionManager().retractSessionProposal(account, with.asBareJid());
383 close(new DisconnectCause(DisconnectCause.LOCAL));
384 } else {
385 rtpConnection.get().endCall();
386 }
387 }
388
389 @Override
390 public void onAbort() {
391 onDisconnect();
392 }
393
394 @Override
395 public void onPlayDtmfTone(char c) {
396 if (rtpConnection == null || rtpConnection.get() == null) {
397 postDial.push("" + c);
398 return;
399 }
400
401 rtpConnection.get().applyDtmfTone("" + c);
402 }
403
404 @Override
405 public void onPostDialContinue(boolean c) {
406 if (c) postDial();
407 }
408
409 protected void findRtpConnection() {
410 if (rtpConnection != null) return;
411
412 rtpConnection = xmppConnectionService.getJingleConnectionManager().findJingleRtpConnection(account, with, sessionId);
413 }
414
415 protected void sleep(int ms) {
416 try {
417 Thread.sleep(ms);
418 } catch (InterruptedException ex) {
419 Thread.currentThread().interrupt();
420 }
421 }
422
423 protected void postDial() {
424 while (!postDial.empty()) {
425 String next = postDial.pop();
426 if (next.equals(";")) {
427 Vector<String> v = new Vector<>(postDial);
428 Collections.reverse(v);
429 setPostDialWait(Joiner.on("").join(v));
430 return;
431 } else if (next.equals(",")) {
432 sleep(2000);
433 } else {
434 rtpConnection.get().applyDtmfTone(next);
435 sleep(100);
436 }
437 }
438 }
439 }
440}