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