Merge branch 'phoneaccount'

Stephen Paul Weber created

* phoneaccount:
  any means none means false (ie there exist) unless upstream reports a reason
  Small fix to address cheogram adding "+1" when making calls from the default dialer. Now only adds "+1" or "+" when necessary.
  First version of dialer integration
  Revert "Intercept DIAL and CALL to tel: and rewrite to cheogram"

Change summary

src/cheogram/AndroidManifest.xml                                         |  11 
src/cheogram/java/com/cheogram/android/ConnectionService.java            | 116 
src/main/AndroidManifest.xml                                             |   8 
src/main/java/eu/siacs/conversations/entities/Contact.java               |  37 
src/main/java/eu/siacs/conversations/entities/Presences.java             |  16 
src/main/java/eu/siacs/conversations/services/XmppConnectionService.java |  13 
src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java          |  37 
src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java          |   2 
src/main/java/eu/siacs/conversations/utils/XmppUri.java                  |   2 
9 files changed, 230 insertions(+), 12 deletions(-)

Detailed changes

src/cheogram/AndroidManifest.xml 🔗

@@ -3,7 +3,18 @@
     xmlns:tools="http://schemas.android.com/tools"
     package="eu.siacs.conversations">
 
+    <uses-permission android:name="android.permission.BIND_TELECOM_CONNECTION_SERVICE" />
+
     <application tools:ignore="GoogleAppIndexingWarning">
+
+        <service android:name="com.cheogram.android.ConnectionService"
+            android:label="Cheogram"
+            android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
+            <intent-filter>
+                <action android:name="android.telecom.ConnectionService" />
+            </intent-filter>
+        </service>
+
         <activity
             android:name=".ui.ManageAccountActivity"
             android:label="@string/title_activity_manage_accounts"

src/cheogram/java/com/cheogram/android/ConnectionService.java 🔗

@@ -0,0 +1,116 @@
+package com.cheogram.android;
+
+import android.telecom.Connection;
+import android.telecom.ConnectionRequest;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.StatusHints;
+import android.telecom.TelecomManager;
+import android.telecom.DisconnectCause;
+
+import android.content.Intent;
+import android.os.Bundle;
+import android.os.Parcel;
+
+import eu.siacs.conversations.xmpp.Jid;
+import eu.siacs.conversations.ui.RtpSessionActivity;
+
+public class ConnectionService extends android.telecom.ConnectionService {
+	@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
+		);
+
+		// TODO: jabber:iq:gateway
+		String tel = request.getAddress().getSchemeSpecificPart().
+		           replaceAll("[^\\+0-9]", "");
+		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()
+		);
+		extras.putString(
+			RtpSessionActivity.EXTRA_WITH,
+			Jid.ofLocalAndDomain(tel, gateway[1]).toEscapedString()
+		);
+		extras.putBinder(
+			RtpSessionActivity.EXTRA_CONNECTION_BINDER,
+			new ConnectionBinder(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;
+
+		ConnectionBinder(Connection connection) {
+			super();
+			this.connection = connection;
+		}
+
+		@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;
+				default:
+					return false;
+			}
+		}
+	}
+
+	public class CheogramConnection extends Connection {
+		@Override
+		public void onDisconnect() {
+			destroy();
+		}
+
+		@Override
+		public void onAbort() {
+			onDisconnect();
+		}
+
+		@Override
+		public void onPlayDtmfTone(char c) {
+			// TODO
+		}
+	}
+}

src/main/AndroidManifest.xml 🔗

@@ -143,14 +143,6 @@
                 <data android:scheme="imto" />
                 <data android:host="jabber" />
             </intent-filter>
-            <intent-filter>
-                <action android:name="android.intent.action.VIEW" />
-                <action android:name="android.intent.action.CALL" />
-                <action android:name="android.intent.action.DIAL" />
-                <category android:name="android.intent.category.DEFAULT" />
-                <category android:name="android.intent.category.BROWSABLE" />
-                <data android:scheme="tel" />
-            </intent-filter>
             <intent-filter>
                 <action android:name="android.intent.action.SENDTO" />
 

src/main/java/eu/siacs/conversations/entities/Contact.java 🔗

@@ -1,9 +1,14 @@
 package eu.siacs.conversations.entities;
 
+import android.content.ComponentName;
 import android.content.ContentValues;
 import android.content.Context;
 import android.database.Cursor;
 import android.net.Uri;
+import android.os.Bundle;
+import android.telecom.PhoneAccount;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
 import android.text.TextUtils;
 
 import androidx.annotation.NonNull;
@@ -559,6 +564,38 @@ public class Contact implements ListItem, Blockable {
         return changed;
     }
 
+    protected String phoneAccountLabel() {
+        return account.getJid().asBareJid().toString() +
+            "/" + getJid().asBareJid().toString();
+    }
+
+    protected PhoneAccountHandle phoneAccountHandle() {
+        ComponentName componentName = new ComponentName(
+            "com.cheogram.android",
+            "com.cheogram.android.ConnectionService"
+        );
+        return new PhoneAccountHandle(componentName, phoneAccountLabel());
+    }
+
+    // This Contact is a gateway to use for voice calls, register it with OS
+    public void registerAsPhoneAccount(Context ctx) {
+        TelecomManager telecomManager = ctx.getSystemService(TelecomManager.class);
+
+        PhoneAccount phoneAccount = PhoneAccount.builder(
+            phoneAccountHandle(), phoneAccountLabel()
+        ).setCapabilities(
+            PhoneAccount.CAPABILITY_CALL_PROVIDER
+        ).build();
+
+        telecomManager.registerPhoneAccount(phoneAccount);
+    }
+
+    // Unregister any associated PSTN gateway integration
+    public void unregisterAsPhoneAccount(Context ctx) {
+        TelecomManager telecomManager = ctx.getSystemService(TelecomManager.class);
+        telecomManager.unregisterPhoneAccount(phoneAccountHandle());
+    }
+
     public static int getOption(Class<? extends AbstractPhoneContact> clazz) {
         if (clazz == JabberIdContact.class) {
             return Options.SYNCED_VIA_ADDRESSBOOK;

src/main/java/eu/siacs/conversations/entities/Presences.java 🔗

@@ -149,6 +149,22 @@ public class Presences {
         return false;
     }
 
+    public boolean anyIdentity(final String category, final String type) {
+        synchronized (this.presences) {
+            if (this.presences.size() == 0) {
+                // https://github.com/iNPUTmice/Conversations/issues/4230
+                return false;
+            }
+            for (Presence presence : this.presences.values()) {
+                ServiceDiscoveryResult disco = presence.getServiceDiscoveryResult();
+                if (disco != null && disco.hasIdentity(category, type)) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
     public Pair<Map<String, String>, Map<String, String>> toTypeAndNameMap() {
         Map<String, String> typeMap = new HashMap<>();
         Map<String, String> nameMap = new HashMap<>();

src/main/java/eu/siacs/conversations/services/XmppConnectionService.java 🔗

@@ -233,6 +233,10 @@ public class XmppConnectionService extends Service {
                 }
             }
         }
+
+        if (contact.getPresences().anyIdentity("gateway", "pstn")) {
+            contact.registerAsPhoneAccount(this);
+        }
     };
     private final PresenceGenerator mPresenceGenerator = new PresenceGenerator(this);
     private List<Account> accounts;
@@ -1968,6 +1972,7 @@ public class XmppConnectionService extends Service {
 
 
     public void syncRoster(final Account account) {
+        unregisterPhoneAccounts(account);
         mRosterSyncTaskManager.execute(account, () -> databaseBackend.writeRoster(account.getRoster()));
     }
 
@@ -3449,6 +3454,14 @@ public class XmppConnectionService extends Service {
         }
     }
 
+    protected void unregisterPhoneAccounts(final Account account) {
+        for (final Contact contact : account.getRoster().getContacts()) {
+            if (!contact.showInRoster()) {
+                contact.unregisterAsPhoneAccount(this);
+            }
+        }
+    }
+
     public void createContact(final Contact contact, final boolean autoGrant) {
         createContact(contact, autoGrant, null);
     }

src/main/java/eu/siacs/conversations/ui/RtpSessionActivity.java 🔗

@@ -71,12 +71,18 @@ 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;
+
 public class RtpSessionActivity extends XmppActivity implements XmppConnectionService.OnJingleRtpConnectionUpdate, eu.siacs.conversations.ui.widget.SurfaceViewRenderer.OnAspectRatioChanged {
 
     public static final String EXTRA_WITH = "with";
     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";
@@ -142,6 +148,8 @@ public class RtpSessionActivity extends XmppActivity implements XmppConnectionSe
         }
     }
 
+    protected android.os.IBinder connectionBinder = null;
+
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
@@ -160,6 +168,8 @@ 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
@@ -262,6 +272,29 @@ 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() {
@@ -1153,10 +1186,14 @@ 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/utils/XmppUri.java 🔗

@@ -179,8 +179,6 @@ public class XmppUri {
             } catch (final UnsupportedEncodingException ignored) {
                 jid = null;
             }
-        } else if ("tel".equalsIgnoreCase(scheme)) {
-            jid = uri.getSchemeSpecificPart().replaceAll("[^\\d\\+]+", "") + "@cheogram.com";
         } else {
             jid = null;
         }