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