Detailed changes
@@ -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);
+ }
+ }
}
}
}
@@ -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();
@@ -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;
@@ -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);
@@ -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;
}
}