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.AvatarService;
 49import eu.siacs.conversations.services.CallIntegration;
 50import eu.siacs.conversations.services.CallIntegrationConnectionService;
 51import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
 52import eu.siacs.conversations.services.XmppConnectionService;
 53import eu.siacs.conversations.ui.RtpSessionActivity;
 54import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
 55import eu.siacs.conversations.xmpp.Jid;
 56import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
 57import eu.siacs.conversations.xmpp.jingle.Media;
 58import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
 59import static eu.siacs.conversations.receiver.SystemEventReceiver.EXTRA_NEEDS_FOREGROUND_SERVICE;
 60
 61@RequiresApi(Build.VERSION_CODES.M)
 62public class ConnectionService extends android.telecom.ConnectionService {
 63	public XmppConnectionService xmppConnectionService = null;
 64	protected ServiceConnection mConnection = new ServiceConnection() {
 65		@Override
 66			public void onServiceConnected(ComponentName className, IBinder service) {
 67				XmppConnectionBinder binder = (XmppConnectionBinder) service;
 68				xmppConnectionService = binder.getService();
 69			}
 70
 71		@Override
 72		public void onServiceDisconnected(ComponentName arg0) {
 73			xmppConnectionService = null;
 74		}
 75	};
 76
 77	@Override
 78	public void onCreate() {
 79		// From XmppActivity.connectToBackend
 80		Intent intent = new Intent(this, XmppConnectionService.class);
 81		intent.setAction(XmppConnectionService.ACTION_STARTING_CALL);
 82		intent.putExtra(EXTRA_NEEDS_FOREGROUND_SERVICE, true);
 83		try {
 84			startService(intent);
 85		} catch (IllegalStateException e) {
 86			Log.w("com.cheogram.android.ConnectionService", "unable to start service from " + getClass().getSimpleName());
 87		}
 88		bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
 89	}
 90
 91	@Override
 92	public void onDestroy() {
 93		unbindService(mConnection);
 94	}
 95
 96	@Override
 97	public Connection onCreateOutgoingConnection(
 98		PhoneAccountHandle phoneAccountHandle,
 99		ConnectionRequest request
100	) {
101		String[] gateway = phoneAccountHandle.getId().split("/", 2);
102
103		String rawTel = "";
104		if (request.getAddress() != null) {
105			rawTel = request.getAddress().getSchemeSpecificPart();
106		}
107		String postDial = PhoneNumberUtils.extractPostDialPortion(rawTel);
108
109		String tel = PhoneNumberUtils.extractNetworkPortion(rawTel);
110		try {
111			tel = PhoneNumberUtilWrapper.normalize(this, tel, true);
112		} catch (IllegalArgumentException | NumberParseException e) {
113			return Connection.createFailedConnection(
114				new DisconnectCause(DisconnectCause.ERROR)
115			);
116		}
117
118		if (xmppConnectionService == null) {
119			return Connection.createFailedConnection(
120				new DisconnectCause(DisconnectCause.ERROR)
121			);
122		}
123
124		if (xmppConnectionService.getJingleConnectionManager().isBusy()) {
125			return Connection.createFailedConnection(
126				new DisconnectCause(DisconnectCause.BUSY)
127			);
128		}
129
130		Account account = xmppConnectionService.findAccountByJid(Jid.of(gateway[0]));
131		if (account == null) {
132			return Connection.createFailedConnection(
133				new DisconnectCause(DisconnectCause.ERROR)
134			);
135		}
136
137		Jid with = Jid.ofLocalAndDomain(tel, gateway[1]);
138		CheogramConnection connection = new CheogramConnection(account, with, postDial);
139
140		PermissionManager permissionManager = PermissionManager.getInstance(this);
141		permissionManager.setNotificationSettings(
142			new NotificationSettings.Builder()
143				.withMessage(R.string.microphone_permission_for_call)
144				.withSmallIcon(R.drawable.ic_notification).build()
145		);
146
147		Set<String> permissions = new HashSet<>();
148		permissions.add(Manifest.permission.RECORD_AUDIO);
149		permissionManager.checkPermissions(permissions, new PermissionManager.PermissionRequestListener() {
150			@Override
151			public void onPermissionGranted() {
152				if (connection.getState() == Connection.STATE_DISCONNECTED) return;
153
154				connection.setSessionId(xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(
155					account,
156					with,
157					ImmutableSet.of(Media.AUDIO)
158				).sessionId);
159			}
160
161			@Override
162			public void onPermissionDenied(DeniedPermissions deniedPermissions) {
163				connection.close(new DisconnectCause(DisconnectCause.ERROR));
164			}
165		});
166
167		connection.setInitializing();
168		connection.setAddress(
169			Uri.fromParts("tel", tel, null), // Normalized tel as tel: URI
170			TelecomManager.PRESENTATION_ALLOWED
171		);
172
173		xmppConnectionService.setOnRtpConnectionUpdateListener(
174			(XmppConnectionService.OnJingleRtpConnectionUpdate) connection
175		);
176
177		xmppConnectionService.setDiallerIntegrationActive(true);
178		return connection;
179	}
180
181	@Override
182	public Connection onCreateIncomingConnection(PhoneAccountHandle handle, ConnectionRequest request) {
183		final var extras = request.getExtras();
184		final var extraExtras = extras.getBundle(TelecomManager.EXTRA_INCOMING_CALL_EXTRAS);
185		final var accountJid = extraExtras == null ? null : extraExtras.getString("account");
186		final var withJid = extraExtras == null ? null : extraExtras.getString("with");
187		final String sessionId = extraExtras == null ? null : extraExtras.getString(CallIntegrationConnectionService.EXTRA_SESSION_ID);
188
189		if (xmppConnectionService == null) {
190			return Connection.createFailedConnection(
191				new DisconnectCause(DisconnectCause.ERROR)
192			);
193		}
194
195		Account account = xmppConnectionService.findAccountByJid(Jid.of(accountJid));
196		Jid with = Jid.of(withJid);
197
198		CheogramConnection connection = new CheogramConnection(account, with, null);
199		connection.setSessionId(sessionId);
200		connection.setAddress(
201			Uri.fromParts("tel", with.getLocal(), null),
202			TelecomManager.PRESENTATION_ALLOWED
203		);
204		connection.setCallerDisplayName(
205			account.getRoster().getContact(with).getDisplayName(),
206			TelecomManager.PRESENTATION_ALLOWED
207		);
208		connection.setRinging();
209
210		xmppConnectionService.setOnRtpConnectionUpdateListener(connection);
211
212		return connection;
213	}
214
215	public class CheogramConnection extends Connection implements XmppConnectionService.OnJingleRtpConnectionUpdate {
216		protected Account account;
217		protected Jid with;
218		protected String sessionId = null;
219		protected Stack<String> postDial = new Stack<>();
220		protected Icon gatewayIcon;
221		protected CallAudioState pendingState = null;
222		protected WeakReference<JingleRtpConnection> rtpConnection = null;
223
224		CheogramConnection(Account account, Jid with, String postDialString) {
225			super();
226			this.account = account;
227			this.with = with;
228
229			gatewayIcon = Icon.createWithBitmap(FileBackend.drawDrawable(xmppConnectionService.getAvatarService().get(
230				account.getRoster().getContact(Jid.of(with.getDomain())),
231				AvatarService.getSystemUiAvatarSize(xmppConnectionService),
232				false
233			)));
234
235			if (postDialString != null) {
236				for (int i = postDialString.length() - 1; i >= 0; i--) {
237					postDial.push("" + postDialString.charAt(i));
238				}
239			}
240
241			setCallerDisplayName(
242				account.getDisplayName(),
243				TelecomManager.PRESENTATION_ALLOWED
244			);
245			setAudioModeIsVoip(true);
246			setConnectionCapabilities(
247				Connection.CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION |
248				Connection.CAPABILITY_MUTE
249			);
250        setRingbackRequested(true);
251		}
252
253		public void setSessionId(final String sessionId) {
254			this.sessionId = sessionId;
255		}
256
257		@Override
258		public void onJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state) {
259			Log.d("com.cheogram.android.CheogramConnection", "onJingleRtpConnectionUpdate: " + with + " " + sessionId + " (== " + this.sessionId + " )? " + state);
260			if (sessionId == null || !sessionId.equals(this.sessionId)) return;
261			if (rtpConnection == null) {
262				this.with = with; // Store full JID of connection
263				findRtpConnection();
264			}
265
266			String statusLabel = null;
267
268			if (state == RtpEndUserState.FINDING_DEVICE) {
269				setInitialized();
270			} else if (state == RtpEndUserState.RINGING) {
271				setDialing();
272			} else if (state == RtpEndUserState.INCOMING_CALL) {
273				setRinging();
274			} else if (state == RtpEndUserState.CONNECTING) {
275				xmppConnectionService.setDiallerIntegrationActive(true);
276				setActive();
277				statusLabel = getString(R.string.rtp_state_connecting);
278			} else if (state == RtpEndUserState.CONNECTED) {
279				xmppConnectionService.setDiallerIntegrationActive(true);
280				setActive();
281				postDial();
282			} else if (state == RtpEndUserState.DECLINED_OR_BUSY) {
283				close(new DisconnectCause(DisconnectCause.BUSY));
284			} else if (state == RtpEndUserState.ENDED) {
285				close(new DisconnectCause(DisconnectCause.LOCAL));
286			} else if (state == RtpEndUserState.RETRACTED) {
287				close(new DisconnectCause(DisconnectCause.CANCELED));
288			} else if (RtpSessionActivity.END_CARD.contains(state)) {
289				close(new DisconnectCause(DisconnectCause.ERROR));
290			}
291
292			setStatusHints(new StatusHints(statusLabel, gatewayIcon, null));
293		}
294
295		@Override
296		public void onAudioDeviceChanged(CallIntegration.AudioDevice selectedAudioDevice, Set<CallIntegration.AudioDevice> availableAudioDevices) {
297			if (Build.VERSION.SDK_INT < 26) return;
298
299			if (pendingState != null) {
300				Log.d("com.cheogram.android.CheogramConnection", "Try with pendingState: " + pendingState);
301				onCallAudioStateChanged(pendingState);
302				return;
303			}
304
305			Log.d("com.cheogram.android.CheogramConnection", "onAudioDeviceChanged: " + selectedAudioDevice);
306
307			switch(selectedAudioDevice) {
308				case SPEAKER_PHONE:
309					setAudioRoute(CallAudioState.ROUTE_SPEAKER);
310					break;
311				case WIRED_HEADSET:
312					setAudioRoute(CallAudioState.ROUTE_WIRED_HEADSET);
313					break;
314				case EARPIECE:
315					setAudioRoute(CallAudioState.ROUTE_EARPIECE);
316					break;
317				case BLUETOOTH:
318					setAudioRoute(CallAudioState.ROUTE_BLUETOOTH);
319					break;
320				default:
321					setAudioRoute(CallAudioState.ROUTE_WIRED_OR_EARPIECE);
322			}
323		}
324
325		@Override
326		public void onCallAudioStateChanged(CallAudioState state) {
327			pendingState = null;
328			if (rtpConnection == null || rtpConnection.get() == null) {
329				pendingState = state;
330				return;
331			}
332
333			Log.d("com.cheogram.android.CheogramConnection", "onCallAudioStateChanged: " + state);
334			rtpConnection.get().callIntegration.onCallAudioStateChanged(state);
335
336			try {
337				rtpConnection.get().setMicrophoneEnabled(!state.isMuted());
338			} catch (final IllegalStateException e) {
339				pendingState = state;
340				Log.w("com.cheogram.android.CheogramConnection", "Could not set microphone mute to " + (state.isMuted() ? "true" : "false") + ": " + e.toString());
341			}
342		}
343
344		@Override
345		public void onAnswer() {
346			// For incoming calls, a connection update may not have been triggered before answering
347			// so we have to acquire the rtp connection object here
348			findRtpConnection();
349			if (rtpConnection == null || rtpConnection.get() == null) {
350				close(new DisconnectCause(DisconnectCause.CANCELED));
351			} else {
352				rtpConnection.get().acceptCall();
353			}
354		}
355
356		@Override
357		public void onReject() {
358			findRtpConnection();
359			if (rtpConnection != null && rtpConnection.get() != null) {
360				try {
361					rtpConnection.get().rejectCall();
362				} catch (final IllegalStateException e) {
363					Log.w("com.cheogram.android.CheogramConnection", e.toString());
364				}
365			}
366			close(new DisconnectCause(DisconnectCause.LOCAL));
367		}
368
369		// Set the connection to the disconnected state and clean up the resources
370		// Note that we cannot do this from onStateChanged() because calling destroy
371		// there seems to trigger a deadlock somewhere in the telephony stack.
372		public void close(DisconnectCause reason) {
373			setDisconnected(reason);
374			destroy();
375			xmppConnectionService.setDiallerIntegrationActive(false);
376			xmppConnectionService.removeRtpConnectionUpdateListener(this);
377		}
378
379		@Override
380		public void onDisconnect() {
381			if (rtpConnection == null || rtpConnection.get() == null) {
382				xmppConnectionService.getJingleConnectionManager().retractSessionProposal(account, with.asBareJid());
383				close(new DisconnectCause(DisconnectCause.LOCAL));
384			} else {
385				rtpConnection.get().endCall();
386			}
387		}
388
389		@Override
390		public void onAbort() {
391			onDisconnect();
392		}
393
394		@Override
395		public void onPlayDtmfTone(char c) {
396			if (rtpConnection == null || rtpConnection.get() == null) {
397				postDial.push("" + c);
398				return;
399			}
400
401			rtpConnection.get().applyDtmfTone("" + c);
402		}
403
404		@Override
405		public void onPostDialContinue(boolean c) {
406			if (c) postDial();
407		}
408
409		protected void findRtpConnection() {
410			if (rtpConnection != null) return;
411
412			rtpConnection = xmppConnectionService.getJingleConnectionManager().findJingleRtpConnection(account, with, sessionId);
413		}
414
415		protected void sleep(int ms) {
416			try {
417				Thread.sleep(ms);
418			} catch (InterruptedException ex) {
419				Thread.currentThread().interrupt();
420			}
421		}
422
423		protected void postDial() {
424			while (!postDial.empty()) {
425				String next = postDial.pop();
426				if (next.equals(";")) {
427					Vector<String> v = new Vector<>(postDial);
428					Collections.reverse(v);
429					setPostDialWait(Joiner.on("").join(v));
430					return;
431				} else if (next.equals(",")) {
432					sleep(2000);
433				} else {
434					rtpConnection.get().applyDtmfTone(next);
435					sleep(100);
436				}
437			}
438		}
439	}
440}