ConnectionService.java

  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}