diff --git a/src/cheogram/java/com/cheogram/android/ConnectionService.java b/src/cheogram/java/com/cheogram/android/ConnectionService.java index c5e304ec78030aeebe70f1fa553a5a7e3708c92a..687020552abbdb839621b8ecd8de46d3c5ad5eba 100644 --- a/src/cheogram/java/com/cheogram/android/ConnectionService.java +++ b/src/cheogram/java/com/cheogram/android/ConnectionService.java @@ -1,106 +1,202 @@ package com.cheogram.android; +import java.lang.ref.WeakReference; +import java.util.Collections; +import java.util.Set; +import java.util.Stack; + +import com.google.common.collect.ImmutableSet; + +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.telecom.DisconnectCause; +import android.telephony.PhoneNumberUtils; +import android.content.ComponentName; +import android.content.Context; import android.content.Intent; +import android.content.ServiceConnection; +import android.net.Uri; import android.os.Bundle; +import android.os.IBinder; import android.os.Parcel; +import android.util.Log; -import eu.siacs.conversations.xmpp.Jid; +import eu.siacs.conversations.entities.Account; +import eu.siacs.conversations.services.AppRTCAudioManager; +import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder; +import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.ui.RtpSessionActivity; +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; 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); - Connection connection = new CheogramConnection(); - connection.setAddress( - request.getAddress(), - TelecomManager.PRESENTATION_ALLOWED - ); - connection.setAudioModeIsVoip(true); - connection.setDialing(); - connection.setRingbackRequested(true); - connection.setConnectionCapabilities( - Connection.CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION - ); + + String rawTel = request.getAddress().getSchemeSpecificPart(); + String postDial = PhoneNumberUtils.extractPostDialPortion(rawTel); // TODO: jabber:iq:gateway - String tel = request.getAddress().getSchemeSpecificPart(). - replaceAll("[^\\+0-9]", ""); + String tel = PhoneNumberUtils.extractNetworkPortion(rawTel); if (tel.startsWith("1")) { tel = "+" + tel; } else if (!tel.startsWith("+")) { tel = "+1" + tel; } - // Instead of wiring the call up to the Android call UI, - // just show our UI for now. This means both are showing during a call. - final Intent intent = new Intent(this, RtpSessionActivity.class); - intent.setAction(RtpSessionActivity.ACTION_MAKE_VOICE_CALL); - Bundle extras = new Bundle(); - extras.putString( - RtpSessionActivity.EXTRA_ACCOUNT, - Jid.of(gateway[0]).toEscapedString() + if (xmppConnectionService.getJingleConnectionManager().isBusy() != null) { + return Connection.createFailedConnection( + new DisconnectCause(DisconnectCause.BUSY) + ); + } + + Account account = xmppConnectionService.findAccountByJid(Jid.of(gateway[0])); + Jid with = Jid.ofLocalAndDomain(tel, gateway[1]); + String sessionId = xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession( + account, + with, + ImmutableSet.of(Media.AUDIO) + ); + + Connection connection = new CheogramConnection(account, with, sessionId, postDial); + connection.setAddress( + Uri.fromParts("tel", tel, null), // Normalized tel as tel: URI + TelecomManager.PRESENTATION_ALLOWED + ); + connection.setCallerDisplayName( + account.getDisplayName(), + TelecomManager.PRESENTATION_ALLOWED ); - extras.putString( - RtpSessionActivity.EXTRA_WITH, - Jid.ofLocalAndDomain(tel, gateway[1]).toEscapedString() + connection.setAudioModeIsVoip(true); + connection.setRingbackRequested(true); + connection.setDialing(); + connection.setConnectionCapabilities( + Connection.CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION ); - extras.putBinder( - RtpSessionActivity.EXTRA_CONNECTION_BINDER, - new ConnectionBinder(connection) + + xmppConnectionService.setOnRtpConnectionUpdateListener( + (XmppConnectionService.OnJingleRtpConnectionUpdate) connection ); - intent.putExtras(extras); - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK); - startActivity(intent); return connection; } - public class ConnectionBinder extends android.os.Binder { - protected Connection connection; - - public static final int TRANSACT_ACTIVE = android.os.IBinder.FIRST_CALL_TRANSACTION + 1; - public static final int TRANSACT_DISCONNECT = TRANSACT_ACTIVE + 1; + public class CheogramConnection extends Connection implements XmppConnectionService.OnJingleRtpConnectionUpdate { + protected Account account; + protected Jid with; + protected String sessionId; + protected Stack postDial = new Stack(); + protected WeakReference rtpConnection = null; - ConnectionBinder(Connection connection) { + CheogramConnection(Account account, Jid with, String sessionId, String postDialString) { super(); - this.connection = connection; + this.account = account; + this.with = with; + this.sessionId = sessionId; + + if (postDialString != null) { + for (int i = postDialString.length() - 1; i >= 0; i--) { + postDial.push("" + postDialString.charAt(i)); + } + } + } + + @Override + public void onJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state) { + if (!sessionId.equals(this.sessionId)) return; + if (rtpConnection == null) { + this.with = with; // Store full JID of connection + rtpConnection = xmppConnectionService.getJingleConnectionManager().findJingleRtpConnection(account, with, sessionId); + } + + if (state == RtpEndUserState.CONNECTED) { + xmppConnectionService.setDiallerIntegrationActive(true); + setActive(); + + postDial(); + } else if (state == RtpEndUserState.DECLINED_OR_BUSY) { + setDisconnected(new DisconnectCause(DisconnectCause.BUSY)); + } else if (state == RtpEndUserState.ENDED) { + setDisconnected(new DisconnectCause(DisconnectCause.LOCAL)); + } else if (state == RtpEndUserState.RETRACTED) { + setDisconnected(new DisconnectCause(DisconnectCause.CANCELED)); + } else if (RtpSessionActivity.END_CARD.contains(state)) { + setDisconnected(new DisconnectCause(DisconnectCause.ERROR)); + } } @Override - protected boolean onTransact(int code, Parcel data, Parcel reply, int flags) { - switch(code) { - case TRANSACT_ACTIVE: - this.connection.setActive(); - connection.setRingbackRequested(false); - return true; - case TRANSACT_DISCONNECT: - this.connection.setDisconnected( - new DisconnectCause(DisconnectCause.UNKNOWN) - ); - return true; + public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices) { + 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: - return false; + setAudioRoute(CallAudioState.ROUTE_WIRED_OR_EARPIECE); } } - } - public class CheogramConnection extends Connection { @Override public void onDisconnect() { + if (rtpConnection == null || rtpConnection.get() == null) { + xmppConnectionService.getJingleConnectionManager().retractSessionProposal(account, with.asBareJid()); + } else { + rtpConnection.get().endCall(); + } destroy(); + xmppConnectionService.setDiallerIntegrationActive(false); + xmppConnectionService.removeRtpConnectionUpdateListener( + (XmppConnectionService.OnJingleRtpConnectionUpdate) this + ); } @Override @@ -110,7 +206,37 @@ public class ConnectionService extends android.telecom.ConnectionService { @Override public void onPlayDtmfTone(char c) { - // TODO + rtpConnection.get().applyDtmfTone("" + c); + } + + @Override + public void onPostDialContinue(boolean c) { + if (c) postDial(); + } + + 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(";")) { + Stack v = (Stack) postDial.clone(); + Collections.reverse(v); + setPostDialWait(String.join("", v)); + return; + } else if (next.equals(",")) { + sleep(2000); + } else { + rtpConnection.get().applyDtmfTone(next); + sleep(100); + } + } } } } diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index 555e35dceab3cdedc2bda5dfcb369d9609f45c0e..e2ede44666cbf2f733b55e8abf91e52e519130a0 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -4,6 +4,7 @@ import android.content.ComponentName; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; +import android.graphics.drawable.Icon; import android.net.Uri; import android.os.Bundle; import android.telecom.PhoneAccount; @@ -30,7 +31,9 @@ import eu.siacs.conversations.Config; import eu.siacs.conversations.R; import eu.siacs.conversations.android.AbstractPhoneContact; import eu.siacs.conversations.android.JabberIdContact; +import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.QuickConversationsService; +import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.utils.JidHelper; import eu.siacs.conversations.utils.UIHelper; import eu.siacs.conversations.xml.Element; @@ -578,11 +581,20 @@ public class Contact implements ListItem, Blockable { } // This Contact is a gateway to use for voice calls, register it with OS - public void registerAsPhoneAccount(Context ctx) { + public void registerAsPhoneAccount(XmppConnectionService ctx) { TelecomManager telecomManager = ctx.getSystemService(TelecomManager.class); PhoneAccount phoneAccount = PhoneAccount.builder( - phoneAccountHandle(), phoneAccountLabel() + phoneAccountHandle(), + account.getJid().asBareJid().toString() + ).setAddress( + Uri.fromParts("xmpp", account.getJid().asBareJid().toString(), null) + ).setIcon( + Icon.createWithBitmap(ctx.getAvatarService().get(this, AvatarService.getSystemUiAvatarSize(ctx) / 2, false)) + ).setHighlightColor( + 0x7401CF + ).setShortDescription( + getJid().asBareJid().toString() ).setCapabilities( PhoneAccount.CAPABILITY_CALL_PROVIDER ).build(); diff --git a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java index c6e79d3af47ae6db8c5063a675c4dacc970befa5..e0f1c2feea00f6e69d6f932327c0c91f1cd1f3d8 100644 --- a/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java +++ b/src/main/java/eu/siacs/conversations/services/XmppConnectionService.java @@ -286,9 +286,11 @@ public class XmppConnectionService extends Service { } }; private final AtomicBoolean isPhoneInCall = new AtomicBoolean(false); + private final AtomicBoolean diallerIntegrationActive = new AtomicBoolean(false); private final PhoneStateListener phoneStateListener = new PhoneStateListener() { @Override public void onCallStateChanged(final int state, final String phoneNumber) { + if (diallerIntegrationActive.get()) return; isPhoneInCall.set(state != TelephonyManager.CALL_STATE_IDLE); if (state == TelephonyManager.CALL_STATE_OFFHOOK) { mJingleConnectionManager.notifyPhoneCallStarted(); @@ -296,6 +298,10 @@ public class XmppConnectionService extends Service { } }; + public void setDiallerIntegrationActive(boolean active) { + diallerIntegrationActive.set(active); + } + private boolean destroyed = false; private int unreadCount = -1; diff --git a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java index 7a94829c3ac34da311c64f4cb24f7e3916fcd309..759256c80c0d47ac5f40b0453f87408aacaa5184 100644 --- a/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java +++ b/src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java @@ -71,8 +71,6 @@ import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; -import com.cheogram.android.ConnectionService.ConnectionBinder; - import static eu.siacs.conversations.utils.PermissionUtils.getFirstDenied; import static java.util.Arrays.asList; @@ -82,14 +80,13 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe public static final String EXTRA_SESSION_ID = "session_id"; public static final String EXTRA_LAST_REPORTED_STATE = "last_reported_state"; public static final String EXTRA_LAST_ACTION = "last_action"; - public static final String EXTRA_CONNECTION_BINDER = "connection_binder"; public static final String ACTION_ACCEPT_CALL = "action_accept_call"; public static final String ACTION_MAKE_VOICE_CALL = "action_make_voice_call"; public static final String ACTION_MAKE_VIDEO_CALL = "action_make_video_call"; private static final int CALL_DURATION_UPDATE_INTERVAL = 333; - private static final List END_CARD = Arrays.asList( + public static final List END_CARD = Arrays.asList( RtpEndUserState.APPLICATION_ERROR, RtpEndUserState.SECURITY_ERROR, RtpEndUserState.DECLINED_OR_BUSY, @@ -148,8 +145,6 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } } - protected android.os.IBinder connectionBinder = null; - @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -168,8 +163,6 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe boolean dialpadVisible = savedInstanceState.getBoolean("dialpad_visible"); binding.dialpad.setVisibility(dialpadVisible ? View.VISIBLE : View.GONE); } - - this.connectionBinder = getIntent().getExtras().getBinder(EXTRA_CONNECTION_BINDER); } @Override @@ -272,29 +265,6 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe } else { requireRtpConnection().endCall(); } - disconnectConnectionBinder(); - } - - private void disconnectConnectionBinder() { - if (connectionBinder != null) { - android.os.Parcel args = android.os.Parcel.obtain(); - try { - connectionBinder.transact(ConnectionBinder.TRANSACT_DISCONNECT, args, null, 0); - } catch (android.os.RemoteException e) {} - args.recycle(); - } - } - - private void activateConnectionBinder() { - // If we do this, the other UI takes over and kills our call - // So we can't activate that UI unless we are going to use it. - /*if (connectionBinder != null) { - android.os.Parcel args = android.os.Parcel.obtain(); - try { - connectionBinder.transact(ConnectionBinder.TRANSACT_ACTIVE, args, null, 0); - } catch (android.os.RemoteException e) {} - args.recycle(); - }*/ } private void retractSessionProposal() { @@ -1186,14 +1156,10 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe @Override public void onJingleRtpConnectionUpdate(Account account, Jid with, final String sessionId, RtpEndUserState state) { Log.d(Config.LOGTAG, "onJingleRtpConnectionUpdate(" + state + ")"); - if (state == RtpEndUserState.CONNECTED) { - activateConnectionBinder(); - } if (END_CARD.contains(state)) { Log.d(Config.LOGTAG, "end card reached"); releaseProximityWakeLock(); runOnUiThread(() -> getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)); - disconnectConnectionBinder(); } if (with.isBareJid()) { updateRtpSessionProposalState(account, with, state); diff --git a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java index b5667b645b800a42de0a434fe5bb5a9c4073c34e..806108c8531a95b6465ed6ed8bc03b9ff25c7f66 100644 --- a/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java +++ b/src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java @@ -172,7 +172,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { return Optional.absent(); } - private boolean hasMatchingRtpSession(final Account account, final Jid with, final Set media) { + private String hasMatchingRtpSession(final Account account, final Jid with, final Set media) { for (AbstractJingleConnection connection : this.connections.values()) { if (connection instanceof JingleRtpConnection) { final JingleRtpConnection rtpConnection = (JingleRtpConnection) connection; @@ -182,11 +182,11 @@ public class JingleConnectionManager extends AbstractConnectionManager { if (rtpConnection.getId().account == account && rtpConnection.getId().with.asBareJid().equals(with.asBareJid()) && rtpConnection.getMedia().equals(media)) { - return true; + return rtpConnection.getId().sessionId; } } } - return false; + return null; } private boolean isWithStrangerAndStrangerNotificationsAreOff(final Account account, Jid with) { @@ -573,7 +573,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { return id.sessionId; } - public void proposeJingleRtpSession(final Account account, final Jid with, final Set media) { + public String proposeJingleRtpSession(final Account account, final Jid with, final Set media) { synchronized (this.rtpSessionProposals) { for (Map.Entry entry : this.rtpSessionProposals.entrySet()) { RtpSessionProposal proposal = entry.getKey(); @@ -588,15 +588,16 @@ public class JingleConnectionManager extends AbstractConnectionManager { proposal.sessionId, endUserState ); - return; + return proposal.sessionId; } } } String busyCode = isBusy(); if (busyCode != null) { - if (hasMatchingRtpSession(account, with, media)) { - Log.d(Config.LOGTAG, "ignoring request to propose jingle session because the other party already created one for us"); - return; + String sessionId = hasMatchingRtpSession(account, with, media); + if (sessionId != null) { + Log.d(Config.LOGTAG, "ignoring request to propose jingle session because the other party already created one for us: " + sessionId); + return sessionId; } throw new IllegalStateException("There is already a running RTP session: " + busyCode); } @@ -610,6 +611,7 @@ public class JingleConnectionManager extends AbstractConnectionManager { ); final MessagePacket messagePacket = mXmppConnectionService.getMessageGenerator().sessionProposal(proposal); mXmppConnectionService.sendMessagePacket(account, messagePacket); + return proposal.sessionId; } }