M src/cheogram/java/com/cheogram/android/ConnectionService.java => src/cheogram/java/com/cheogram/android/ConnectionService.java +66 -14
@@ 5,9 5,12 @@ 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;
@@ 119,7 122,7 @@ public class ConnectionService extends android.telecom.ConnectionService {
.withSmallIcon(R.drawable.ic_notification).build()
);
- Set<String> permissions = new HashSet();
+ Set<String> permissions = new HashSet<>();
permissions.add(Manifest.permission.RECORD_AUDIO);
permissionManager.checkPermissions(permissions, new PermissionManager.PermissionRequestListener() {
@Override
@@ 133,7 136,7 @@ public class ConnectionService extends android.telecom.ConnectionService {
@Override
public void onPermissionDenied(DeniedPermissions deniedPermissions) {
- connection.setDisconnected(new DisconnectCause(DisconnectCause.ERROR));
+ connection.close(new DisconnectCause(DisconnectCause.ERROR));
}
});
@@ 150,11 153,34 @@ public class ConnectionService extends android.telecom.ConnectionService {
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 Stack<String> postDial = new Stack<>();
protected Icon gatewayIcon;
protected WeakReference<JingleRtpConnection> rtpConnection = null;
@@ 203,24 229,28 @@ public class ConnectionService extends android.telecom.ConnectionService {
setInitialized();
} else if (state == RtpEndUserState.RINGING) {
setDialing();
+ } else if (state == RtpEndUserState.INCOMING_CALL) {
+ setRinging();
} else if (state == RtpEndUserState.CONNECTED) {
xmppConnectionService.setDiallerIntegrationActive(true);
setActive();
postDial();
} else if (state == RtpEndUserState.DECLINED_OR_BUSY) {
- setDisconnected(new DisconnectCause(DisconnectCause.BUSY));
+ close(new DisconnectCause(DisconnectCause.BUSY));
} else if (state == RtpEndUserState.ENDED) {
- setDisconnected(new DisconnectCause(DisconnectCause.LOCAL));
+ close(new DisconnectCause(DisconnectCause.LOCAL));
} else if (state == RtpEndUserState.RETRACTED) {
- setDisconnected(new DisconnectCause(DisconnectCause.CANCELED));
+ close(new DisconnectCause(DisconnectCause.CANCELED));
} else if (RtpSessionActivity.END_CARD.contains(state)) {
- setDisconnected(new DisconnectCause(DisconnectCause.ERROR));
+ close(new DisconnectCause(DisconnectCause.ERROR));
}
}
@Override
public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set<AppRTCAudioManager.AudioDevice> availableAudioDevices) {
+ if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O) return;
+
switch(selectedAudioDevice) {
case SPEAKER_PHONE:
setAudioRoute(CallAudioState.ROUTE_SPEAKER);
@@ 236,17 266,39 @@ public class ConnectionService extends android.telecom.ConnectionService {
}
@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
+ this.rtpConnection = xmppConnectionService.getJingleConnectionManager().findJingleRtpConnection(account, with, sessionId);
+
+ rtpConnection.get().acceptCall();
+ }
+
+ @Override
+ public void onReject() {
+ this.rtpConnection = xmppConnectionService.getJingleConnectionManager().findJingleRtpConnection(account, with, sessionId);
+ 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();
}
- destroy();
- xmppConnectionService.setDiallerIntegrationActive(false);
- xmppConnectionService.removeRtpConnectionUpdateListener(
- (XmppConnectionService.OnJingleRtpConnectionUpdate) this
- );
}
@Override
@@ 276,9 328,9 @@ public class ConnectionService extends android.telecom.ConnectionService {
while (!postDial.empty()) {
String next = postDial.pop();
if (next.equals(";")) {
- Stack v = (Stack) postDial.clone();
+ Vector<String> v = new Vector<>(postDial);
Collections.reverse(v);
- setPostDialWait(String.join("", v));
+ setPostDialWait(Joiner.on("").join(v));
return;
} else if (next.equals(",")) {
sleep(2000);
M src/main/java/eu/siacs/conversations/entities/Contact.java => src/main/java/eu/siacs/conversations/entities/Contact.java +1 -1
@@ 599,7 599,7 @@ public class Contact implements ListItem, Blockable {
"/" + getJid().asBareJid().toString();
}
- protected PhoneAccountHandle phoneAccountHandle() {
+ public PhoneAccountHandle phoneAccountHandle() {
ComponentName componentName = new ComponentName(
"com.cheogram.android",
"com.cheogram.android.ConnectionService"
M src/main/java/eu/siacs/conversations/services/NotificationService.java => src/main/java/eu/siacs/conversations/services/NotificationService.java +61 -0
@@ 1,5 1,6 @@
package eu.siacs.conversations.services;
+import android.Manifest;
import android.app.Notification;
import android.app.NotificationChannel;
import android.app.NotificationChannelGroup;
@@ 8,6 9,7 @@ import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
import android.content.res.Resources;
import android.graphics.Bitmap;
import android.graphics.Typeface;
@@ 16,9 18,12 @@ import android.media.Ringtone;
import android.media.RingtoneManager;
import android.net.Uri;
import android.os.Build;
+import android.os.Bundle;
import android.os.SystemClock;
import android.os.Vibrator;
import android.preference.PreferenceManager;
+import android.telecom.PhoneAccountHandle;
+import android.telecom.TelecomManager;
import android.text.SpannableString;
import android.text.style.StyleSpan;
import android.util.DisplayMetrics;
@@ 423,7 428,63 @@ public class NotificationService {
notify(DELIVERY_FAILED_NOTIFICATION_ID, summaryNotification);
}
+ private synchronized boolean tryRingingWithDialerUI(final AbstractJingleConnection.Id id, final Set<Media> media) {
+ if (mXmppConnectionService.checkSelfPermission(Manifest.permission.RECORD_AUDIO) != PackageManager.PERMISSION_GRANTED) {
+ // We cannot request audio permission in Dialer UI
+ // when Dialer is shown over keyguard, the user cannot even necessarily
+ // see notifications.
+ return false;
+ }
+
+ 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) {
+ // If the account is not registered or enabled, it could result in a security exception
+ // Just fall back to the built-in UI in this case.
+ Log.w(Config.LOGTAG, e);
+ return false;
+ }
+
+ return true;
+ }
+
public synchronized void startRinging(final AbstractJingleConnection.Id id, final Set<Media> media) {
+ if (tryRingingWithDialerUI(id, media)) {
+ return;
+ }
+
showIncomingCallNotification(id, media);
final NotificationManager notificationManager = (NotificationManager) mXmppConnectionService.getSystemService(Context.NOTIFICATION_SERVICE);
final int currentInterruptionFilter;