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