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