Merge branch 'dialer-integration-2'

Stephen Paul Weber created

* dialer-integration-2:
  Mostly rewrite dialler integration
  Much prettier calling account

Change summary

src/cheogram/java/com/cheogram/android/ConnectionService.java                 | 236 
src/main/java/eu/siacs/conversations/entities/Contact.java                    |  16 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java      |   6 
src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java               |  36 
src/main/java/eu/siacs/conversations/xmpp/jingle/JingleConnectionManager.java |  18 
5 files changed, 212 insertions(+), 100 deletions(-)

Detailed changes

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<String> postDial = new Stack();
+		protected WeakReference<JingleRtpConnection> 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<AppRTCAudioManager.AudioDevice> 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);
+				}
+			}
 		}
 	}
 }

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();

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;

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<RtpEndUserState> END_CARD = Arrays.asList(
+    public static final List<RtpEndUserState> 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);

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> media) {
+    private String hasMatchingRtpSession(final Account account, final Jid with, final Set<Media> 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> media) {
+    public String proposeJingleRtpSession(final Account account, final Jid with, final Set<Media> media) {
         synchronized (this.rtpSessionProposals) {
             for (Map.Entry<RtpSessionProposal, DeviceDiscoveryState> 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;
         }
     }