package com.cheogram.android; import java.lang.ref.WeakReference; import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.Stack; import java.util.Vector; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableSet; import android.os.Build; import android.telecom.CallAudioState; import android.telecom.Connection; import android.telecom.ConnectionRequest; import android.telecom.DisconnectCause; import android.telecom.PhoneAccount; import android.telecom.PhoneAccountHandle; import android.telecom.StatusHints; import android.telecom.TelecomManager; import android.telephony.PhoneNumberUtils; import android.Manifest; import androidx.annotation.RequiresApi; import androidx.core.content.ContextCompat; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; import android.content.ServiceConnection; import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Bundle; import android.os.IBinder; import android.os.Parcel; import android.util.Log; import com.intentfilter.androidpermissions.PermissionManager; import com.intentfilter.androidpermissions.NotificationSettings; import com.intentfilter.androidpermissions.models.DeniedPermissions; import io.michaelrocks.libphonenumber.android.NumberParseException; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.services.AppRTCAudioManager; import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder; import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.RtpSessionActivity; import eu.siacs.conversations.utils.PhoneNumberUtilWrapper; import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; @RequiresApi(Build.VERSION_CODES.M) public class ConnectionService extends android.telecom.ConnectionService { public XmppConnectionService xmppConnectionService = null; protected ServiceConnection mConnection = new ServiceConnection() { @Override public void onServiceConnected(ComponentName className, IBinder service) { XmppConnectionBinder binder = (XmppConnectionBinder) service; xmppConnectionService = binder.getService(); } @Override public void onServiceDisconnected(ComponentName arg0) { xmppConnectionService = null; } }; @Override public void onCreate() { // From XmppActivity.connectToBackend Intent intent = new Intent(this, XmppConnectionService.class); intent.setAction("ui"); try { startService(intent); } catch (IllegalStateException e) { Log.w("com.cheogram.android.ConnectionService", "unable to start service from " + getClass().getSimpleName()); } bindService(intent, mConnection, Context.BIND_AUTO_CREATE); } @Override public void onDestroy() { unbindService(mConnection); } @Override public Connection onCreateOutgoingConnection( PhoneAccountHandle phoneAccountHandle, ConnectionRequest request ) { String[] gateway = phoneAccountHandle.getId().split("/", 2); String rawTel = request.getAddress().getSchemeSpecificPart(); String postDial = PhoneNumberUtils.extractPostDialPortion(rawTel); String tel = PhoneNumberUtils.extractNetworkPortion(rawTel); try { tel = PhoneNumberUtilWrapper.normalize(this, tel); } catch (IllegalArgumentException | NumberParseException e) { return Connection.createFailedConnection( new DisconnectCause(DisconnectCause.ERROR) ); } if (xmppConnectionService == null) { return Connection.createFailedConnection( new DisconnectCause(DisconnectCause.ERROR) ); } if (xmppConnectionService.getJingleConnectionManager().isBusy() != null) { return Connection.createFailedConnection( new DisconnectCause(DisconnectCause.BUSY) ); } Account account = xmppConnectionService.findAccountByJid(Jid.of(gateway[0])); if (account == null) { return Connection.createFailedConnection( new DisconnectCause(DisconnectCause.ERROR) ); } Jid with = Jid.ofLocalAndDomain(tel, gateway[1]); CheogramConnection connection = new CheogramConnection(account, with, postDial); PermissionManager permissionManager = PermissionManager.getInstance(this); permissionManager.setNotificationSettings( new NotificationSettings.Builder() .withMessage(R.string.microphone_permission_for_call) .withSmallIcon(R.drawable.ic_notification).build() ); Set permissions = new HashSet<>(); permissions.add(Manifest.permission.RECORD_AUDIO); permissionManager.checkPermissions(permissions, new PermissionManager.PermissionRequestListener() { @Override public void onPermissionGranted() { connection.setSessionId(xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession( account, with, ImmutableSet.of(Media.AUDIO) )); } @Override public void onPermissionDenied(DeniedPermissions deniedPermissions) { connection.close(new DisconnectCause(DisconnectCause.ERROR)); } }); connection.setInitializing(); connection.setAddress( Uri.fromParts("tel", tel, null), // Normalized tel as tel: URI TelecomManager.PRESENTATION_ALLOWED ); xmppConnectionService.setOnRtpConnectionUpdateListener( (XmppConnectionService.OnJingleRtpConnectionUpdate) connection ); return connection; } @Override public Connection onCreateIncomingConnection(PhoneAccountHandle handle, ConnectionRequest request) { Bundle extras = request.getExtras(); String accountJid = extras.getString("account"); String withJid = extras.getString("with"); String sessionId = extras.getString("sessionId"); Account account = xmppConnectionService.findAccountByJid(Jid.of(accountJid)); Jid with = Jid.of(withJid); CheogramConnection connection = new CheogramConnection(account, with, null); connection.setSessionId(sessionId); connection.setAddress( Uri.fromParts("tel", with.getLocal(), null), TelecomManager.PRESENTATION_ALLOWED ); connection.setRinging(); xmppConnectionService.setOnRtpConnectionUpdateListener(connection); return connection; } public class CheogramConnection extends Connection implements XmppConnectionService.OnJingleRtpConnectionUpdate { protected Account account; protected Jid with; protected String sessionId = null; protected Stack postDial = new Stack<>(); protected Icon gatewayIcon; protected WeakReference rtpConnection = null; CheogramConnection(Account account, Jid with, String postDialString) { super(); this.account = account; this.with = with; gatewayIcon = Icon.createWithBitmap(xmppConnectionService.getAvatarService().get( account.getRoster().getContact(Jid.of(with.getDomain())), AvatarService.getSystemUiAvatarSize(xmppConnectionService), false )); if (postDialString != null) { for (int i = postDialString.length() - 1; i >= 0; i--) { postDial.push("" + postDialString.charAt(i)); } } setCallerDisplayName( account.getDisplayName(), TelecomManager.PRESENTATION_ALLOWED ); setAudioModeIsVoip(true); setConnectionCapabilities( Connection.CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION ); } public void setSessionId(final String sessionId) { this.sessionId = sessionId; } @Override public void onJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state) { if (sessionId == null || !sessionId.equals(this.sessionId)) return; if (rtpConnection == null) { this.with = with; // Store full JID of connection findRtpConnection(); } String statusLabel = null; if (state == RtpEndUserState.FINDING_DEVICE) { setInitialized(); } else if (state == RtpEndUserState.RINGING) { setDialing(); } else if (state == RtpEndUserState.INCOMING_CALL) { setRinging(); } else if (state == RtpEndUserState.CONNECTING) { xmppConnectionService.setDiallerIntegrationActive(true); setActive(); statusLabel = getString(R.string.rtp_state_connecting); } else if (state == RtpEndUserState.CONNECTED) { xmppConnectionService.setDiallerIntegrationActive(true); setActive(); postDial(); } else if (state == RtpEndUserState.DECLINED_OR_BUSY) { close(new DisconnectCause(DisconnectCause.BUSY)); } else if (state == RtpEndUserState.ENDED) { close(new DisconnectCause(DisconnectCause.LOCAL)); } else if (state == RtpEndUserState.RETRACTED) { close(new DisconnectCause(DisconnectCause.CANCELED)); } else if (RtpSessionActivity.END_CARD.contains(state)) { close(new DisconnectCause(DisconnectCause.ERROR)); } setStatusHints(new StatusHints(statusLabel, gatewayIcon, null)); } @Override public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices) { if (Build.VERSION.SDK_INT < 26) return; switch(selectedAudioDevice) { case SPEAKER_PHONE: setAudioRoute(CallAudioState.ROUTE_SPEAKER); case WIRED_HEADSET: setAudioRoute(CallAudioState.ROUTE_WIRED_HEADSET); case EARPIECE: setAudioRoute(CallAudioState.ROUTE_EARPIECE); case BLUETOOTH: setAudioRoute(CallAudioState.ROUTE_BLUETOOTH); default: setAudioRoute(CallAudioState.ROUTE_WIRED_OR_EARPIECE); } } @Override public void onAnswer() { // For incoming calls, a connection update may not have been triggered before answering // so we have to acquire the rtp connection object here findRtpConnection(); if (rtpConnection == null || rtpConnection.get() == null) { close(new DisconnectCause(DisconnectCause.CANCELED)); } else { rtpConnection.get().acceptCall(); } } @Override public void onReject() { findRtpConnection(); if (rtpConnection != null && rtpConnection.get() != null) { rtpConnection.get().rejectCall(); } close(new DisconnectCause(DisconnectCause.LOCAL)); } // Set the connection to the disconnected state and clean up the resources // Note that we cannot do this from onStateChanged() because calling destroy // there seems to trigger a deadlock somewhere in the telephony stack. public void close(DisconnectCause reason) { setDisconnected(reason); destroy(); xmppConnectionService.setDiallerIntegrationActive(false); xmppConnectionService.removeRtpConnectionUpdateListener(this); } @Override public void onDisconnect() { if (rtpConnection == null || rtpConnection.get() == null) { xmppConnectionService.getJingleConnectionManager().retractSessionProposal(account, with.asBareJid()); close(new DisconnectCause(DisconnectCause.LOCAL)); } else { rtpConnection.get().endCall(); } } @Override public void onAbort() { onDisconnect(); } @Override public void onPlayDtmfTone(char c) { rtpConnection.get().applyDtmfTone("" + c); } @Override public void onPostDialContinue(boolean c) { if (c) postDial(); } protected void findRtpConnection() { if (rtpConnection != null) return; rtpConnection = xmppConnectionService.getJingleConnectionManager().findJingleRtpConnection(account, with, sessionId); } protected void sleep(int ms) { try { Thread.sleep(ms); } catch (InterruptedException ex) { Thread.currentThread().interrupt(); } } protected void postDial() { while (!postDial.empty()) { String next = postDial.pop(); if (next.equals(";")) { Vector v = new Vector<>(postDial); Collections.reverse(v); setPostDialWait(Joiner.on("").join(v)); return; } else if (next.equals(",")) { sleep(2000); } else { rtpConnection.get().applyDtmfTone(next); sleep(100); } } } } }