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;
  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}