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;
8
9import com.google.common.collect.ImmutableSet;
10
11import android.telecom.CallAudioState;
12import android.telecom.Connection;
13import android.telecom.ConnectionRequest;
14import android.telecom.DisconnectCause;
15import android.telecom.PhoneAccount;
16import android.telecom.PhoneAccountHandle;
17import android.telecom.StatusHints;
18import android.telecom.TelecomManager;
19import android.telephony.PhoneNumberUtils;
20
21import android.Manifest;
22import androidx.core.content.ContextCompat;
23import android.content.ComponentName;
24import android.content.Context;
25import android.content.Intent;
26import android.content.pm.PackageManager;
27import android.content.ServiceConnection;
28import android.net.Uri;
29import android.os.Bundle;
30import android.os.IBinder;
31import android.os.Parcel;
32import android.util.Log;
33
34import com.intentfilter.androidpermissions.PermissionManager;
35import com.intentfilter.androidpermissions.models.DeniedPermissions;
36
37import eu.siacs.conversations.entities.Account;
38import eu.siacs.conversations.services.AppRTCAudioManager;
39import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
40import eu.siacs.conversations.services.XmppConnectionService;
41import eu.siacs.conversations.ui.RtpSessionActivity;
42import eu.siacs.conversations.xmpp.Jid;
43import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
44import eu.siacs.conversations.xmpp.jingle.Media;
45import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
46
47public class ConnectionService extends android.telecom.ConnectionService {
48 public XmppConnectionService xmppConnectionService = null;
49 protected ServiceConnection mConnection = new ServiceConnection() {
50 @Override
51 public void onServiceConnected(ComponentName className, IBinder service) {
52 XmppConnectionBinder binder = (XmppConnectionBinder) service;
53 xmppConnectionService = binder.getService();
54 }
55
56 @Override
57 public void onServiceDisconnected(ComponentName arg0) {
58 xmppConnectionService = null;
59 }
60 };
61
62 @Override
63 public void onCreate() {
64 // From XmppActivity.connectToBackend
65 Intent intent = new Intent(this, XmppConnectionService.class);
66 intent.setAction("ui");
67 try {
68 startService(intent);
69 } catch (IllegalStateException e) {
70 Log.w("com.cheogram.android.ConnectionService", "unable to start service from " + getClass().getSimpleName());
71 }
72 bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
73 }
74
75 @Override
76 public void onDestroy() {
77 unbindService(mConnection);
78 }
79
80 @Override
81 public Connection onCreateOutgoingConnection(
82 PhoneAccountHandle phoneAccountHandle,
83 ConnectionRequest request
84 ) {
85 String[] gateway = phoneAccountHandle.getId().split("/", 2);
86
87 String rawTel = request.getAddress().getSchemeSpecificPart();
88 String postDial = PhoneNumberUtils.extractPostDialPortion(rawTel);
89
90 // TODO: jabber:iq:gateway
91 String tel = PhoneNumberUtils.extractNetworkPortion(rawTel);
92 if (tel.startsWith("1")) {
93 tel = "+" + tel;
94 } else if (!tel.startsWith("+")) {
95 tel = "+1" + tel;
96 }
97
98 if (xmppConnectionService.getJingleConnectionManager().isBusy() != null) {
99 return Connection.createFailedConnection(
100 new DisconnectCause(DisconnectCause.BUSY)
101 );
102 }
103
104 Account account = xmppConnectionService.findAccountByJid(Jid.of(gateway[0]));
105 Jid with = Jid.ofLocalAndDomain(tel, gateway[1]);
106 CheogramConnection connection = new CheogramConnection(account, with, postDial);
107
108 PermissionManager permissionManager = PermissionManager.getInstance(this);
109 Set<String> permissions = new HashSet();
110 permissions.add(Manifest.permission.RECORD_AUDIO);
111 permissionManager.checkPermissions(permissions, new PermissionManager.PermissionRequestListener() {
112 @Override
113 public void onPermissionGranted() {
114 connection.setSessionId(xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(
115 account,
116 with,
117 ImmutableSet.of(Media.AUDIO)
118 ));
119 }
120
121 @Override
122 public void onPermissionDenied(DeniedPermissions deniedPermissions) {
123 connection.setDisconnected(new DisconnectCause(DisconnectCause.ERROR));
124 }
125 });
126
127 connection.setAddress(
128 Uri.fromParts("tel", tel, null), // Normalized tel as tel: URI
129 TelecomManager.PRESENTATION_ALLOWED
130 );
131 connection.setCallerDisplayName(
132 account.getDisplayName(),
133 TelecomManager.PRESENTATION_ALLOWED
134 );
135 connection.setAudioModeIsVoip(true);
136 connection.setRingbackRequested(true);
137 connection.setDialing();
138 connection.setConnectionCapabilities(
139 Connection.CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION
140 );
141
142 xmppConnectionService.setOnRtpConnectionUpdateListener(
143 (XmppConnectionService.OnJingleRtpConnectionUpdate) connection
144 );
145
146 return connection;
147 }
148
149 public class CheogramConnection extends Connection implements XmppConnectionService.OnJingleRtpConnectionUpdate {
150 protected Account account;
151 protected Jid with;
152 protected String sessionId = null;
153 protected Stack<String> postDial = new Stack();
154 protected WeakReference<JingleRtpConnection> rtpConnection = null;
155
156 CheogramConnection(Account account, Jid with, String postDialString) {
157 super();
158 this.account = account;
159 this.with = with;
160
161 if (postDialString != null) {
162 for (int i = postDialString.length() - 1; i >= 0; i--) {
163 postDial.push("" + postDialString.charAt(i));
164 }
165 }
166 }
167
168 public void setSessionId(final String sessionId) {
169 this.sessionId = sessionId;
170 }
171
172 @Override
173 public void onJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state) {
174 if (sessionId == null || !sessionId.equals(this.sessionId)) return;
175 if (rtpConnection == null) {
176 this.with = with; // Store full JID of connection
177 rtpConnection = xmppConnectionService.getJingleConnectionManager().findJingleRtpConnection(account, with, sessionId);
178 }
179
180 if (state == RtpEndUserState.CONNECTED) {
181 xmppConnectionService.setDiallerIntegrationActive(true);
182 setActive();
183
184 postDial();
185 } else if (state == RtpEndUserState.DECLINED_OR_BUSY) {
186 setDisconnected(new DisconnectCause(DisconnectCause.BUSY));
187 } else if (state == RtpEndUserState.ENDED) {
188 setDisconnected(new DisconnectCause(DisconnectCause.LOCAL));
189 } else if (state == RtpEndUserState.RETRACTED) {
190 setDisconnected(new DisconnectCause(DisconnectCause.CANCELED));
191 } else if (RtpSessionActivity.END_CARD.contains(state)) {
192 setDisconnected(new DisconnectCause(DisconnectCause.ERROR));
193 }
194 }
195
196 @Override
197 public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
198 switch(selectedAudioDevice) {
199 case SPEAKER_PHONE:
200 setAudioRoute(CallAudioState.ROUTE_SPEAKER);
201 case WIRED_HEADSET:
202 setAudioRoute(CallAudioState.ROUTE_WIRED_HEADSET);
203 case EARPIECE:
204 setAudioRoute(CallAudioState.ROUTE_EARPIECE);
205 case BLUETOOTH:
206 setAudioRoute(CallAudioState.ROUTE_BLUETOOTH);
207 default:
208 setAudioRoute(CallAudioState.ROUTE_WIRED_OR_EARPIECE);
209 }
210 }
211
212 @Override
213 public void onDisconnect() {
214 if (rtpConnection == null || rtpConnection.get() == null) {
215 xmppConnectionService.getJingleConnectionManager().retractSessionProposal(account, with.asBareJid());
216 } else {
217 rtpConnection.get().endCall();
218 }
219 destroy();
220 xmppConnectionService.setDiallerIntegrationActive(false);
221 xmppConnectionService.removeRtpConnectionUpdateListener(
222 (XmppConnectionService.OnJingleRtpConnectionUpdate) this
223 );
224 }
225
226 @Override
227 public void onAbort() {
228 onDisconnect();
229 }
230
231 @Override
232 public void onPlayDtmfTone(char c) {
233 rtpConnection.get().applyDtmfTone("" + c);
234 }
235
236 @Override
237 public void onPostDialContinue(boolean c) {
238 if (c) postDial();
239 }
240
241 protected void sleep(int ms) {
242 try {
243 Thread.sleep(ms);
244 } catch (InterruptedException ex) {
245 Thread.currentThread().interrupt();
246 }
247 }
248
249 protected void postDial() {
250 while (!postDial.empty()) {
251 String next = postDial.pop();
252 if (next.equals(";")) {
253 Stack v = (Stack) postDial.clone();
254 Collections.reverse(v);
255 setPostDialWait(String.join("", v));
256 return;
257 } else if (next.equals(",")) {
258 sleep(2000);
259 } else {
260 rtpConnection.get().applyDtmfTone(next);
261 sleep(100);
262 }
263 }
264 }
265 }
266}