Compare commits

...

3 commits

3 changed files with 336 additions and 240 deletions

View file

@ -1,292 +1,340 @@
package com.cheogram.android; package com.cheogram.android;
import android.Manifest;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.telecom.CallAudioState;
import android.telecom.Connection;
import android.telecom.ConnectionRequest;
import android.telecom.DisconnectCause;
import android.telecom.PhoneAccountHandle;
import android.telecom.StatusHints;
import android.telecom.TelecomManager;
import android.telephony.PhoneNumberUtils;
import android.util.Log;
import com.google.common.collect.ImmutableSet;
import com.intentfilter.androidpermissions.NotificationSettings;
import com.intentfilter.androidpermissions.PermissionManager;
import com.intentfilter.androidpermissions.models.DeniedPermissions;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; import java.util.HashSet;
import java.util.Set; import java.util.Set;
import java.util.Stack; 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.telephony.PhoneNumberUtils;
import android.Manifest;
import androidx.core.content.ContextCompat;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.ServiceConnection;
import android.graphics.drawable.Icon;
import android.net.Uri;
import android.os.Bundle;
import android.os.IBinder;
import android.os.Parcel;
import android.util.Log;
import com.intentfilter.androidpermissions.PermissionManager;
import com.intentfilter.androidpermissions.NotificationSettings;
import com.intentfilter.androidpermissions.models.DeniedPermissions;
import io.michaelrocks.libphonenumber.android.NumberParseException;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.entities.Account; import eu.siacs.conversations.entities.Account;
import eu.siacs.conversations.services.AppRTCAudioManager; import eu.siacs.conversations.services.AppRTCAudioManager;
import eu.siacs.conversations.services.AvatarService; import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
import eu.siacs.conversations.services.XmppConnectionService; import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
import eu.siacs.conversations.ui.RtpSessionActivity; import eu.siacs.conversations.ui.RtpSessionActivity;
import eu.siacs.conversations.utils.PhoneNumberUtilWrapper; import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
import eu.siacs.conversations.xmpp.Jid; import eu.siacs.conversations.xmpp.Jid;
import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection; import eu.siacs.conversations.xmpp.jingle.JingleRtpConnection;
import eu.siacs.conversations.xmpp.jingle.Media; import eu.siacs.conversations.xmpp.jingle.Media;
import eu.siacs.conversations.xmpp.jingle.RtpEndUserState; import eu.siacs.conversations.xmpp.jingle.RtpEndUserState;
import io.michaelrocks.libphonenumber.android.NumberParseException;
public class ConnectionService extends android.telecom.ConnectionService { public class ConnectionService extends android.telecom.ConnectionService {
public XmppConnectionService xmppConnectionService = null; public XmppConnectionService xmppConnectionService = null;
protected ServiceConnection mConnection = new ServiceConnection() { protected ServiceConnection mConnection = new ServiceConnection() {
@Override @Override
public void onServiceConnected(ComponentName className, IBinder service) { public void onServiceConnected(ComponentName className, IBinder service) {
XmppConnectionBinder binder = (XmppConnectionBinder) service; XmppConnectionBinder binder = (XmppConnectionBinder) service;
xmppConnectionService = binder.getService(); xmppConnectionService = binder.getService();
} }
@Override @Override
public void onServiceDisconnected(ComponentName arg0) { public void onServiceDisconnected(ComponentName arg0) {
xmppConnectionService = null; xmppConnectionService = null;
} }
}; };
private PermissionManager mPermissionManager;
@Override @Override
public void onCreate() { public void onCreate() {
// From XmppActivity.connectToBackend mPermissionManager = PermissionManager.getInstance(this);
Intent intent = new Intent(this, XmppConnectionService.class); mPermissionManager.setNotificationSettings(
intent.setAction("ui"); new NotificationSettings.Builder()
try { .withMessage(R.string.microphone_permission_for_call)
startService(intent); .withSmallIcon(R.drawable.ic_notification).build()
} catch (IllegalStateException e) { );
Log.w("com.cheogram.android.ConnectionService", "unable to start service from " + getClass().getSimpleName());
}
bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
}
@Override // From XmppActivity.connectToBackend
public void onDestroy() { Intent intent = new Intent(this, XmppConnectionService.class);
unbindService(mConnection); 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 @Override
public Connection onCreateOutgoingConnection( public void onDestroy() {
PhoneAccountHandle phoneAccountHandle, unbindService(mConnection);
ConnectionRequest request }
) {
String[] gateway = phoneAccountHandle.getId().split("/", 2);
String rawTel = request.getAddress().getSchemeSpecificPart(); @Override
String postDial = PhoneNumberUtils.extractPostDialPortion(rawTel); public Connection onCreateOutgoingConnection(
PhoneAccountHandle phoneAccountHandle,
ConnectionRequest request
) {
String[] gateway = phoneAccountHandle.getId().split("/", 2);
String tel = PhoneNumberUtils.extractNetworkPortion(rawTel); String rawTel = request.getAddress().getSchemeSpecificPart();
try { String postDial = PhoneNumberUtils.extractPostDialPortion(rawTel);
tel = PhoneNumberUtilWrapper.normalize(this, tel);
} catch (NumberParseException e) {
return Connection.createFailedConnection(
new DisconnectCause(DisconnectCause.ERROR)
);
}
if (xmppConnectionService.getJingleConnectionManager().isBusy() != null) { String tel = PhoneNumberUtils.extractNetworkPortion(rawTel);
return Connection.createFailedConnection( try {
new DisconnectCause(DisconnectCause.BUSY) tel = PhoneNumberUtilWrapper.normalize(this, tel);
); } catch (NumberParseException e) {
} return Connection.createFailedConnection(
new DisconnectCause(DisconnectCause.ERROR)
);
}
Account account = xmppConnectionService.findAccountByJid(Jid.of(gateway[0])); if (xmppConnectionService.getJingleConnectionManager().isBusy() != null) {
Jid with = Jid.ofLocalAndDomain(tel, gateway[1]); return Connection.createFailedConnection(
CheogramConnection connection = new CheogramConnection(account, with, postDial); new DisconnectCause(DisconnectCause.BUSY)
);
}
PermissionManager permissionManager = PermissionManager.getInstance(this); Account account = xmppConnectionService.findAccountByJid(Jid.of(gateway[0]));
permissionManager.setNotificationSettings( Jid with = Jid.ofLocalAndDomain(tel, gateway[1]);
new NotificationSettings.Builder() CheogramConnection connection = new CheogramConnection(account, with, postDial);
.withMessage(R.string.microphone_permission_for_call)
.withSmallIcon(R.drawable.ic_notification).build()
);
Set<String> permissions = new HashSet(); Set<String> permissions = new HashSet<>();
permissions.add(Manifest.permission.RECORD_AUDIO); permissions.add(Manifest.permission.RECORD_AUDIO);
permissionManager.checkPermissions(permissions, new PermissionManager.PermissionRequestListener() { mPermissionManager.checkPermissions(permissions, new PermissionManager.PermissionRequestListener() {
@Override @Override
public void onPermissionGranted() { public void onPermissionGranted() {
connection.setSessionId(xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession( connection.setSessionId(xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(
account, account,
with, with,
ImmutableSet.of(Media.AUDIO) ImmutableSet.of(Media.AUDIO)
)); ));
} }
@Override @Override
public void onPermissionDenied(DeniedPermissions deniedPermissions) { public void onPermissionDenied(DeniedPermissions deniedPermissions) {
connection.setDisconnected(new DisconnectCause(DisconnectCause.ERROR)); connection.setDisconnected(new DisconnectCause(DisconnectCause.ERROR));
} }
}); });
connection.setInitializing(); connection.setInitializing();
connection.setAddress( connection.setAddress(
Uri.fromParts("tel", tel, null), // Normalized tel as tel: URI Uri.fromParts("tel", tel, null), // Normalized tel as tel: URI
TelecomManager.PRESENTATION_ALLOWED TelecomManager.PRESENTATION_ALLOWED
); );
xmppConnectionService.setOnRtpConnectionUpdateListener( xmppConnectionService.setOnRtpConnectionUpdateListener(
(XmppConnectionService.OnJingleRtpConnectionUpdate) connection (XmppConnectionService.OnJingleRtpConnectionUpdate) connection
); );
return connection; return connection;
} }
public class CheogramConnection extends Connection implements XmppConnectionService.OnJingleRtpConnectionUpdate { @Override
protected Account account; public Connection onCreateIncomingConnection(PhoneAccountHandle handle, ConnectionRequest request) {
protected Jid with; Bundle extras = request.getExtras();
protected String sessionId = null; String accountJid = extras.getString("account");
protected Stack<String> postDial = new Stack(); String withJid = extras.getString("with");
protected Icon gatewayIcon; String sessionId = extras.getString("sessionId");
protected WeakReference<JingleRtpConnection> rtpConnection = null;
CheogramConnection(Account account, Jid with, String postDialString) { Account account = xmppConnectionService.findAccountByJid(Jid.of(accountJid));
super(); Jid with = Jid.of(withJid);
this.account = account;
this.with = with;
gatewayIcon = Icon.createWithBitmap(xmppConnectionService.getAvatarService().get( CheogramConnection connection = new CheogramConnection(account, with, null);
account.getRoster().getContact(Jid.of(with.getDomain())), connection.setSessionId(sessionId);
AvatarService.getSystemUiAvatarSize(xmppConnectionService), connection.setAddress(
false Uri.fromParts("tel", with.getLocal(), null),
)); TelecomManager.PRESENTATION_ALLOWED
);
connection.setRinging();
if (postDialString != null) { xmppConnectionService.setOnRtpConnectionUpdateListener(connection);
for (int i = postDialString.length() - 1; i >= 0; i--) {
postDial.push("" + postDialString.charAt(i));
}
}
setCallerDisplayName( return connection;
account.getDisplayName(), }
TelecomManager.PRESENTATION_ALLOWED
);
setAudioModeIsVoip(true);
setConnectionCapabilities(
Connection.CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION
);
}
public void setSessionId(final String sessionId) { public class CheogramConnection extends Connection implements XmppConnectionService.OnJingleRtpConnectionUpdate {
this.sessionId = sessionId; protected Account account;
} protected Jid with;
protected String sessionId = null;
protected Stack<String> postDial = new Stack<>();
protected Icon gatewayIcon;
protected WeakReference<JingleRtpConnection> rtpConnection = null;
@Override CheogramConnection(Account account, Jid with, String postDialString) {
public void onJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state) { super();
if (sessionId == null || !sessionId.equals(this.sessionId)) return; this.account = account;
if (rtpConnection == null) { this.with = with;
this.with = with; // Store full JID of connection
rtpConnection = xmppConnectionService.getJingleConnectionManager().findJingleRtpConnection(account, with, sessionId);
}
setStatusHints(new StatusHints(null, gatewayIcon, null)); gatewayIcon = Icon.createWithBitmap(xmppConnectionService.getAvatarService().get(
account.getRoster().getContact(Jid.of(with.getDomain())),
AvatarService.getSystemUiAvatarSize(xmppConnectionService),
false
));
if (state == RtpEndUserState.FINDING_DEVICE) { if (postDialString != null) {
setInitialized(); for (int i = postDialString.length() - 1; i >= 0; i--) {
} else if (state == RtpEndUserState.RINGING) { postDial.push("" + postDialString.charAt(i));
setDialing(); }
} else if (state == RtpEndUserState.CONNECTED) { }
xmppConnectionService.setDiallerIntegrationActive(true);
setActive();
postDial(); setCallerDisplayName(
} else if (state == RtpEndUserState.DECLINED_OR_BUSY) { account.getDisplayName(),
setDisconnected(new DisconnectCause(DisconnectCause.BUSY)); TelecomManager.PRESENTATION_ALLOWED
} else if (state == RtpEndUserState.ENDED) { );
setDisconnected(new DisconnectCause(DisconnectCause.LOCAL)); setAudioModeIsVoip(true);
} else if (state == RtpEndUserState.RETRACTED) { setConnectionCapabilities(
setDisconnected(new DisconnectCause(DisconnectCause.CANCELED)); Connection.CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION
} else if (RtpSessionActivity.END_CARD.contains(state)) { );
setDisconnected(new DisconnectCause(DisconnectCause.ERROR)); }
}
}
@Override public void setSessionId(final String sessionId) {
public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) { this.sessionId = sessionId;
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:
setAudioRoute(CallAudioState.ROUTE_WIRED_OR_EARPIECE);
}
}
@Override @Override
public void onDisconnect() { public void onJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state) {
if (rtpConnection == null || rtpConnection.get() == null) { if (sessionId == null || !sessionId.equals(this.sessionId)) return;
xmppConnectionService.getJingleConnectionManager().retractSessionProposal(account, with.asBareJid()); if (rtpConnection == null) {
} else { this.with = with; // Store full JID of connection
rtpConnection.get().endCall(); rtpConnection = xmppConnectionService.getJingleConnectionManager().findJingleRtpConnection(account, with, sessionId);
} }
destroy();
xmppConnectionService.setDiallerIntegrationActive(false);
xmppConnectionService.removeRtpConnectionUpdateListener(
(XmppConnectionService.OnJingleRtpConnectionUpdate) this
);
}
@Override setStatusHints(new StatusHints(null, gatewayIcon, null));
public void onAbort() {
onDisconnect();
}
@Override if (state == RtpEndUserState.FINDING_DEVICE) {
public void onPlayDtmfTone(char c) { setInitialized();
rtpConnection.get().applyDtmfTone("" + c); } else if (state == RtpEndUserState.RINGING) {
} setDialing();
} else if (state == RtpEndUserState.INCOMING_CALL) {
setRinging();
} else if (state == RtpEndUserState.CONNECTED) {
xmppConnectionService.setDiallerIntegrationActive(true);
setActive();
@Override postDial();
public void onPostDialContinue(boolean c) { } else if (state == RtpEndUserState.DECLINED_OR_BUSY) {
if (c) postDial(); 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));
}
}
protected void sleep(int ms) { @Override
try { public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
Thread.sleep(ms); switch (selectedAudioDevice) {
} catch (InterruptedException ex) { case SPEAKER_PHONE:
Thread.currentThread().interrupt(); 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:
setAudioRoute(CallAudioState.ROUTE_WIRED_OR_EARPIECE);
}
}
protected void postDial() { @Override
while (!postDial.empty()) { public void onAnswer() {
String next = postDial.pop(); // For incoming calls, a connection update may not have been triggered before answering
if (next.equals(";")) { // so we have to acquire the rtp connection object here
Stack v = (Stack) postDial.clone(); this.rtpConnection =
Collections.reverse(v); xmppConnectionService.getJingleConnectionManager()
setPostDialWait(String.join("", v)); .findJingleRtpConnection(account, with, sessionId);
return;
} else if (next.equals(",")) { // Request recording permission only when answering
sleep(2000); Set<String> permissions = new HashSet<>();
} else { permissions.add(Manifest.permission.RECORD_AUDIO);
rtpConnection.get().applyDtmfTone(next); mPermissionManager.checkPermissions(permissions, new PermissionManager.PermissionRequestListener() {
sleep(100); @Override
} public void onPermissionGranted() {
} if (rtpConnection == null || rtpConnection.get() == null) {
} setDisconnected(new DisconnectCause(DisconnectCause.ERROR));
} } else {
rtpConnection.get().acceptCall();
}
}
@Override
public void onPermissionDenied(DeniedPermissions deniedPermissions) {
setDisconnected(new DisconnectCause(DisconnectCause.ERROR));
}
});
}
@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
public void onAbort() {
onDisconnect();
}
@Override
public void onPlayDtmfTone(char c) {
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);
}
}
}
}
} }

View file

@ -599,7 +599,7 @@ public class Contact implements ListItem, Blockable {
"/" + getJid().asBareJid().toString(); "/" + getJid().asBareJid().toString();
} }
protected PhoneAccountHandle phoneAccountHandle() { public PhoneAccountHandle phoneAccountHandle() {
ComponentName componentName = new ComponentName( ComponentName componentName = new ComponentName(
"com.cheogram.android", "com.cheogram.android",
"com.cheogram.android.ConnectionService" "com.cheogram.android.ConnectionService"

View file

@ -16,9 +16,12 @@ import android.media.Ringtone;
import android.media.RingtoneManager; import android.media.RingtoneManager;
import android.net.Uri; import android.net.Uri;
import android.os.Build; import android.os.Build;
import android.os.Bundle;
import android.os.SystemClock; import android.os.SystemClock;
import android.os.Vibrator; import android.os.Vibrator;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.telecom.PhoneAccountHandle;
import android.telecom.TelecomManager;
import android.text.SpannableString; import android.text.SpannableString;
import android.text.style.StyleSpan; import android.text.style.StyleSpan;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
@ -423,7 +426,52 @@ public class NotificationService {
notify(DELIVERY_FAILED_NOTIFICATION_ID, summaryNotification); notify(DELIVERY_FAILED_NOTIFICATION_ID, summaryNotification);
} }
private synchronized boolean tryRingingWithDialerUI(final AbstractJingleConnection.Id id, final Set<Media> media) {
if (media.size() != 1 || !media.contains(Media.AUDIO)) {
// Currently our ConnectionService only handles single audio calls
Log.w(Config.LOGTAG, "only audio calls can be handled by cheogram connection service");
return false;
}
PhoneAccountHandle handle = null;
for (Contact contact : id.account.getRoster().getContacts()) {
if (!contact.getJid().getDomain().equals(id.with.getDomain()))
continue;
if (!contact.getPresences().anyIdentity("gateway", "pstn"))
continue;
handle = contact.phoneAccountHandle();
break;
}
if (handle == null) {
Log.w(Config.LOGTAG, "Could not find phone account handle for " + id.account.getJid().toString());
return false;
}
Bundle callInfo = new Bundle();
callInfo.putString("account", id.account.getJid().toString());
callInfo.putString("with", id.with.toString());
callInfo.putString("sessionId", id.sessionId);
TelecomManager telecomManager = mXmppConnectionService.getSystemService(TelecomManager.class);
try {
telecomManager.addNewIncomingCall(handle, callInfo);
} catch (SecurityException e) {
// There *could* be race conditions where the account is not registered yet
// when an incoming call is already received
Log.w(Config.LOGTAG, e);
return false;
}
return true;
}
public synchronized void startRinging(final AbstractJingleConnection.Id id, final Set<Media> media) { public synchronized void startRinging(final AbstractJingleConnection.Id id, final Set<Media> media) {
if (tryRingingWithDialerUI(id, media)) {
return;
}
showIncomingCallNotification(id, media); showIncomingCallNotification(id, media);
final NotificationManager notificationManager = (NotificationManager) mXmppConnectionService.getSystemService(Context.NOTIFICATION_SERVICE); final NotificationManager notificationManager = (NotificationManager) mXmppConnectionService.getSystemService(Context.NOTIFICATION_SERVICE);
final int currentInterruptionFilter; final int currentInterruptionFilter;