Detailed changes
@@ -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"
@@ -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
+ }
+ }
+}
@@ -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" />
@@ -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;
@@ -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<>();
@@ -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);
}
@@ -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);
@@ -260,8 +260,6 @@ public class UriHandlerActivity extends AppCompatActivity {
break;
case Intent.ACTION_VIEW:
case Intent.ACTION_SENDTO:
- case Intent.ACTION_DIAL:
- case Intent.ACTION_CALL:
if (handleUri(data.getData())) {
finish();
}
@@ -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;
}