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

376 lines
12 KiB
Java

package com.cheogram.android;
import java.lang.ref.WeakReference;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
import java.util.Stack;
import java.util.Vector;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import android.os.Build;
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.annotation.RequiresApi;
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.entities.Account;
import eu.siacs.conversations.services.AppRTCAudioManager;
import eu.siacs.conversations.services.AvatarService;
import eu.siacs.conversations.services.XmppConnectionService.XmppConnectionBinder;
import eu.siacs.conversations.services.XmppConnectionService;
import eu.siacs.conversations.ui.RtpSessionActivity;
import eu.siacs.conversations.utils.PhoneNumberUtilWrapper;
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;
@RequiresApi(Build.VERSION_CODES.M)
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);
String rawTel = request.getAddress().getSchemeSpecificPart();
String postDial = PhoneNumberUtils.extractPostDialPortion(rawTel);
String tel = PhoneNumberUtils.extractNetworkPortion(rawTel);
try {
tel = PhoneNumberUtilWrapper.normalize(this, tel);
} catch (IllegalArgumentException | NumberParseException e) {
return Connection.createFailedConnection(
new DisconnectCause(DisconnectCause.ERROR)
);
}
if (xmppConnectionService == null) {
return Connection.createFailedConnection(
new DisconnectCause(DisconnectCause.ERROR)
);
}
if (xmppConnectionService.getJingleConnectionManager().isBusy() != null) {
return Connection.createFailedConnection(
new DisconnectCause(DisconnectCause.BUSY)
);
}
Account account = xmppConnectionService.findAccountByJid(Jid.of(gateway[0]));
if (account == null) {
return Connection.createFailedConnection(
new DisconnectCause(DisconnectCause.ERROR)
);
}
Jid with = Jid.ofLocalAndDomain(tel, gateway[1]);
CheogramConnection connection = new CheogramConnection(account, with, postDial);
PermissionManager permissionManager = PermissionManager.getInstance(this);
permissionManager.setNotificationSettings(
new NotificationSettings.Builder()
.withMessage(R.string.microphone_permission_for_call)
.withSmallIcon(R.drawable.ic_notification).build()
);
Set<String> permissions = new HashSet<>();
permissions.add(Manifest.permission.RECORD_AUDIO);
permissionManager.checkPermissions(permissions, new PermissionManager.PermissionRequestListener() {
@Override
public void onPermissionGranted() {
connection.setSessionId(xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession(
account,
with,
ImmutableSet.of(Media.AUDIO)
));
}
@Override
public void onPermissionDenied(DeniedPermissions deniedPermissions) {
connection.close(new DisconnectCause(DisconnectCause.ERROR));
}
});
connection.setInitializing();
connection.setAddress(
Uri.fromParts("tel", tel, null), // Normalized tel as tel: URI
TelecomManager.PRESENTATION_ALLOWED
);
xmppConnectionService.setOnRtpConnectionUpdateListener(
(XmppConnectionService.OnJingleRtpConnectionUpdate) connection
);
return connection;
}
@Override
public Connection onCreateIncomingConnection(PhoneAccountHandle handle, ConnectionRequest request) {
Bundle extras = request.getExtras();
String accountJid = extras.getString("account");
String withJid = extras.getString("with");
String sessionId = extras.getString("sessionId");
Account account = xmppConnectionService.findAccountByJid(Jid.of(accountJid));
Jid with = Jid.of(withJid);
CheogramConnection connection = new CheogramConnection(account, with, null);
connection.setSessionId(sessionId);
connection.setAddress(
Uri.fromParts("tel", with.getLocal(), null),
TelecomManager.PRESENTATION_ALLOWED
);
connection.setRinging();
xmppConnectionService.setOnRtpConnectionUpdateListener(connection);
return connection;
}
public class CheogramConnection extends Connection implements XmppConnectionService.OnJingleRtpConnectionUpdate {
protected Account account;
protected Jid with;
protected String sessionId = null;
protected Stack<String> postDial = new Stack<>();
protected Icon gatewayIcon;
protected WeakReference<JingleRtpConnection> rtpConnection = null;
CheogramConnection(Account account, Jid with, String postDialString) {
super();
this.account = account;
this.with = with;
gatewayIcon = Icon.createWithBitmap(xmppConnectionService.getAvatarService().get(
account.getRoster().getContact(Jid.of(with.getDomain())),
AvatarService.getSystemUiAvatarSize(xmppConnectionService),
false
));
if (postDialString != null) {
for (int i = postDialString.length() - 1; i >= 0; i--) {
postDial.push("" + postDialString.charAt(i));
}
}
setCallerDisplayName(
account.getDisplayName(),
TelecomManager.PRESENTATION_ALLOWED
);
setAudioModeIsVoip(true);
setConnectionCapabilities(
Connection.CAPABILITY_CAN_SEND_RESPONSE_VIA_CONNECTION
);
}
public void setSessionId(final String sessionId) {
this.sessionId = sessionId;
}
@Override
public void onJingleRtpConnectionUpdate(final Account account, final Jid with, final String sessionId, final RtpEndUserState state) {
if (sessionId == null || !sessionId.equals(this.sessionId)) return;
if (rtpConnection == null) {
this.with = with; // Store full JID of connection
findRtpConnection();
}
String statusLabel = null;
if (state == RtpEndUserState.FINDING_DEVICE) {
setInitialized();
} else if (state == RtpEndUserState.RINGING) {
setDialing();
} else if (state == RtpEndUserState.INCOMING_CALL) {
setRinging();
} else if (state == RtpEndUserState.CONNECTING) {
xmppConnectionService.setDiallerIntegrationActive(true);
setActive();
statusLabel = getString(R.string.rtp_state_connecting);
} else if (state == RtpEndUserState.CONNECTED) {
xmppConnectionService.setDiallerIntegrationActive(true);
setActive();
postDial();
} else if (state == RtpEndUserState.DECLINED_OR_BUSY) {
close(new DisconnectCause(DisconnectCause.BUSY));
} else if (state == RtpEndUserState.ENDED) {
close(new DisconnectCause(DisconnectCause.LOCAL));
} else if (state == RtpEndUserState.RETRACTED) {
close(new DisconnectCause(DisconnectCause.CANCELED));
} else if (RtpSessionActivity.END_CARD.contains(state)) {
close(new DisconnectCause(DisconnectCause.ERROR));
}
setStatusHints(new StatusHints(statusLabel, gatewayIcon, null));
}
@Override
public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
if (Build.VERSION.SDK_INT < 26) return;
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
public void onAnswer() {
// For incoming calls, a connection update may not have been triggered before answering
// so we have to acquire the rtp connection object here
findRtpConnection();
if (rtpConnection == null || rtpConnection.get() == null) {
close(new DisconnectCause(DisconnectCause.CANCELED));
} else {
rtpConnection.get().acceptCall();
}
}
@Override
public void onReject() {
findRtpConnection();
if (rtpConnection != null && rtpConnection.get() != null) {
rtpConnection.get().rejectCall();
}
close(new DisconnectCause(DisconnectCause.LOCAL));
}
// Set the connection to the disconnected state and clean up the resources
// Note that we cannot do this from onStateChanged() because calling destroy
// there seems to trigger a deadlock somewhere in the telephony stack.
public void close(DisconnectCause reason) {
setDisconnected(reason);
destroy();
xmppConnectionService.setDiallerIntegrationActive(false);
xmppConnectionService.removeRtpConnectionUpdateListener(this);
}
@Override
public void onDisconnect() {
if (rtpConnection == null || rtpConnection.get() == null) {
xmppConnectionService.getJingleConnectionManager().retractSessionProposal(account, with.asBareJid());
close(new DisconnectCause(DisconnectCause.LOCAL));
} else {
rtpConnection.get().endCall();
}
}
@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 findRtpConnection() {
if (rtpConnection != null) return;
rtpConnection = xmppConnectionService.getJingleConnectionManager().findJingleRtpConnection(account, with, sessionId);
}
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(";")) {
Vector<String> v = new Vector<>(postDial);
Collections.reverse(v);
setPostDialWait(Joiner.on("").join(v));
return;
} else if (next.equals(",")) {
sleep(2000);
} else {
rtpConnection.get().applyDtmfTone(next);
sleep(100);
}
}
}
}
}