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