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				if (connection.getState() == Connection.STATE_DISCONNECTED) return;
149
150				connection.setSessionId(xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(
151					account,
152					with,
153					ImmutableSet.of(Media.AUDIO)
154				));
155			}
156
157			@Override
158			public void onPermissionDenied(DeniedPermissions deniedPermissions) {
159				connection.close(new DisconnectCause(DisconnectCause.ERROR));
160			}
161		});
162
163		connection.setInitializing();
164		connection.setAddress(
165			Uri.fromParts("tel", tel, null), // Normalized tel as tel: URI
166			TelecomManager.PRESENTATION_ALLOWED
167		);
168
169		xmppConnectionService.setOnRtpConnectionUpdateListener(
170			(XmppConnectionService.OnJingleRtpConnectionUpdate) connection
171		);
172
173		xmppConnectionService.setDiallerIntegrationActive(true);
174		return connection;
175	}
176
177	@Override
178	public Connection onCreateIncomingConnection(PhoneAccountHandle handle, ConnectionRequest request) {
179		Bundle extras = request.getExtras();
180		String accountJid = extras.getString("account");
181		String withJid = extras.getString("with");
182		String sessionId = extras.getString("sessionId");
183
184		Account account = xmppConnectionService.findAccountByJid(Jid.of(accountJid));
185		Jid with = Jid.of(withJid);
186
187		CheogramConnection connection = new CheogramConnection(account, with, null);
188		connection.setSessionId(sessionId);
189		connection.setAddress(
190			Uri.fromParts("tel", with.getLocal(), null),
191			TelecomManager.PRESENTATION_ALLOWED
192		);
193		connection.setRinging();
194
195		xmppConnectionService.setOnRtpConnectionUpdateListener(connection);
196
197		return connection;
198	}
199
200	public class CheogramConnection extends Connection implements XmppConnectionService.OnJingleRtpConnectionUpdate {
201		protected Account account;
202		protected Jid with;
203		protected String sessionId = null;
204		protected Stack<String> postDial = new Stack<>();
205		protected Icon gatewayIcon;
206		protected CallAudioState pendingState = null;
207		protected WeakReference<JingleRtpConnection> rtpConnection = null;
208
209		CheogramConnection(Account account, Jid with, String postDialString) {
210			super();
211			this.account = account;
212			this.with = with;
213
214			gatewayIcon = Icon.createWithBitmap(xmppConnectionService.getAvatarService().get(
215				account.getRoster().getContact(Jid.of(with.getDomain())),
216				AvatarService.getSystemUiAvatarSize(xmppConnectionService),
217				false
218			));
219
220			if (postDialString != null) {
221				for (int i = postDialString.length() - 1; i >= 0; i--) {
222					postDial.push("" + postDialString.charAt(i));
223				}
224			}
225
226			setCallerDisplayName(
227				account.getDisplayName(),
228				TelecomManager.PRESENTATION_ALLOWED
229			);
230			setAudioModeIsVoip(true);
231			setConnectionCapabilities(
232				Connection.CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION |
233				Connection.CAPABILITY_MUTE
234			);
235		}
236
237		public void setSessionId(final String sessionId) {
238			this.sessionId = sessionId;
239		}
240
241		@Override
242		public void onJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state) {
243			if (sessionId == null || !sessionId.equals(this.sessionId)) return;
244			if (rtpConnection == null) {
245				this.with = with; // Store full JID of connection
246				findRtpConnection();
247			}
248
249			String statusLabel = null;
250
251			if (state == RtpEndUserState.FINDING_DEVICE) {
252				setInitialized();
253			} else if (state == RtpEndUserState.RINGING) {
254				setDialing();
255			} else if (state == RtpEndUserState.INCOMING_CALL) {
256				setRinging();
257			} else if (state == RtpEndUserState.CONNECTING) {
258				xmppConnectionService.setDiallerIntegrationActive(true);
259				setActive();
260				statusLabel = getString(R.string.rtp_state_connecting);
261			} else if (state == RtpEndUserState.CONNECTED) {
262				xmppConnectionService.setDiallerIntegrationActive(true);
263				setActive();
264				postDial();
265			} else if (state == RtpEndUserState.DECLINED_OR_BUSY) {
266				close(new DisconnectCause(DisconnectCause.BUSY));
267			} else if (state == RtpEndUserState.ENDED) {
268				close(new DisconnectCause(DisconnectCause.LOCAL));
269			} else if (state == RtpEndUserState.RETRACTED) {
270				close(new DisconnectCause(DisconnectCause.CANCELED));
271			} else if (RtpSessionActivity.END_CARD.contains(state)) {
272				close(new DisconnectCause(DisconnectCause.ERROR));
273			}
274
275			setStatusHints(new StatusHints(statusLabel, gatewayIcon, null));
276		}
277
278		@Override
279		public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
280			if (Build.VERSION.SDK_INT < 26) return;
281
282			if (pendingState != null) {
283				Log.d("com.cheogram.android.CheogramConnection", "Try with pendingState: " + pendingState);
284				onCallAudioStateChanged(pendingState);
285				return;
286			}
287
288			Log.d("com.cheogram.android.CheogramConnection", "onAudioDeviceChanged: " + selectedAudioDevice);
289
290			switch(selectedAudioDevice) {
291				case SPEAKER_PHONE:
292					setAudioRoute(CallAudioState.ROUTE_SPEAKER);
293					break;
294				case WIRED_HEADSET:
295					setAudioRoute(CallAudioState.ROUTE_WIRED_HEADSET);
296					break;
297				case EARPIECE:
298					setAudioRoute(CallAudioState.ROUTE_EARPIECE);
299					break;
300				case BLUETOOTH:
301					setAudioRoute(CallAudioState.ROUTE_BLUETOOTH);
302					break;
303				default:
304					setAudioRoute(CallAudioState.ROUTE_WIRED_OR_EARPIECE);
305			}
306		}
307
308		@Override
309		public void onCallAudioStateChanged(CallAudioState state) {
310			pendingState = null;
311			if (rtpConnection == null || rtpConnection.get() == null || rtpConnection.get().getAudioManager() == null) {
312				pendingState = state;
313				return;
314			}
315
316			Log.d("com.cheogram.android.CheogramConnection", "onCallAudioStateChanged: " + state);
317
318			switch(state.getRoute()) {
319				case CallAudioState.ROUTE_SPEAKER:
320					rtpConnection.get().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.SPEAKER_PHONE);
321					break;
322				case CallAudioState.ROUTE_WIRED_HEADSET:
323					rtpConnection.get().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.WIRED_HEADSET);
324					break;
325				case CallAudioState.ROUTE_EARPIECE:
326					rtpConnection.get().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.EARPIECE);
327					break;
328				case CallAudioState.ROUTE_BLUETOOTH:
329					rtpConnection.get().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.BLUETOOTH);
330					break;
331				default:
332					rtpConnection.get().getAudioManager().setDefaultAudioDevice(AppRTCAudioManager.AudioDevice.NONE);
333			}
334
335			try {
336				rtpConnection.get().setMicrophoneEnabled(!state.isMuted());
337			} catch (final IllegalStateException e) {
338				pendingState = state;
339				Log.w("com.cheogram.android.CheogramConnection", "Could not set microphone mute to " + (state.isMuted() ? "true" : "false") + ": " + e.toString());
340			}
341		}
342
343		@Override
344		public void onAnswer() {
345			// For incoming calls, a connection update may not have been triggered before answering
346			// so we have to acquire the rtp connection object here
347			findRtpConnection();
348			if (rtpConnection == null || rtpConnection.get() == null) {
349				close(new DisconnectCause(DisconnectCause.CANCELED));
350			} else {
351				rtpConnection.get().acceptCall();
352			}
353		}
354
355		@Override
356		public void onReject() {
357			findRtpConnection();
358			if (rtpConnection != null && rtpConnection.get() != null) {
359				rtpConnection.get().rejectCall();
360			}
361			close(new DisconnectCause(DisconnectCause.LOCAL));
362		}
363
364		// Set the connection to the disconnected state and clean up the resources
365		// Note that we cannot do this from onStateChanged() because calling destroy
366		// there seems to trigger a deadlock somewhere in the telephony stack.
367		public void close(DisconnectCause reason) {
368			setDisconnected(reason);
369			destroy();
370			xmppConnectionService.setDiallerIntegrationActive(false);
371			xmppConnectionService.removeRtpConnectionUpdateListener(this);
372		}
373
374		@Override
375		public void onDisconnect() {
376			if (rtpConnection == null || rtpConnection.get() == null) {
377				xmppConnectionService.getJingleConnectionManager().retractSessionProposal(account, with.asBareJid());
378				close(new DisconnectCause(DisconnectCause.LOCAL));
379			} else {
380				rtpConnection.get().endCall();
381			}
382		}
383
384		@Override
385		public void onAbort() {
386			onDisconnect();
387		}
388
389		@Override
390		public void onPlayDtmfTone(char c) {
391			if (rtpConnection == null || rtpConnection.get() == null) {
392				postDial.push("" + c);
393				return;
394			}
395
396			rtpConnection.get().applyDtmfTone("" + c);
397		}
398
399		@Override
400		public void onPostDialContinue(boolean c) {
401			if (c) postDial();
402		}
403
404		protected void findRtpConnection() {
405			if (rtpConnection != null) return;
406
407			rtpConnection = xmppConnectionService.getJingleConnectionManager().findJingleRtpConnection(account, with, sessionId);
408		}
409
410		protected void sleep(int ms) {
411			try {
412				Thread.sleep(ms);
413			} catch (InterruptedException ex) {
414				Thread.currentThread().interrupt();
415			}
416		}
417
418		protected void postDial() {
419			while (!postDial.empty()) {
420				String next = postDial.pop();
421				if (next.equals(";")) {
422					Vector<String> v = new Vector<>(postDial);
423					Collections.reverse(v);
424					setPostDialWait(Joiner.on("").join(v));
425					return;
426				} else if (next.equals(",")) {
427					sleep(2000);
428				} else {
429					rtpConnection.get().applyDtmfTone(next);
430					sleep(100);
431				}
432			}
433		}
434	}
435}