From b218b57c940825dfc6bfb5bc203bf20e5bc8c066 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Fri, 11 Mar 2022 21:44:57 -0500 Subject: [PATCH 1/5] ConnectionService: fix unchecked type assignments --- src/cheogram/java/com/cheogram/android/ConnectionService.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cheogram/java/com/cheogram/android/ConnectionService.java b/src/cheogram/java/com/cheogram/android/ConnectionService.java index 671578dd8..c1dbb49cc 100644 --- a/src/cheogram/java/com/cheogram/android/ConnectionService.java +++ b/src/cheogram/java/com/cheogram/android/ConnectionService.java @@ -119,7 +119,7 @@ public class ConnectionService extends android.telecom.ConnectionService { .withSmallIcon(R.drawable.ic_notification).build() ); - Set permissions = new HashSet(); + Set permissions = new HashSet<>(); permissions.add(Manifest.permission.RECORD_AUDIO); permissionManager.checkPermissions(permissions, new PermissionManager.PermissionRequestListener() { @Override @@ -154,7 +154,7 @@ public class ConnectionService extends android.telecom.ConnectionService { protected Account account; protected Jid with; protected String sessionId = null; - protected Stack postDial = new Stack(); + protected Stack postDial = new Stack<>(); protected Icon gatewayIcon; protected WeakReference rtpConnection = null; From 788818fdda6983723371c1156e0241fa2f1d1e0a Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Fri, 11 Mar 2022 21:51:08 -0500 Subject: [PATCH 2/5] ConnectionService: Dialer UI integration for incoming calls For incoming calls, we fall back to the built-in call UI if the microphone permission is not granted. The reason is that if the Dialer UI is displayed over keyguard, then the user may not even be able to see the notification that we show in order for them to grant the permission. Having a small annoyance for the first incoming call is better than having the in-call UI hang. --- .../cheogram/android/ConnectionService.java | 34 +++++++++++ .../siacs/conversations/entities/Contact.java | 2 +- .../services/NotificationService.java | 61 +++++++++++++++++++ 3 files changed, 96 insertions(+), 1 deletion(-) diff --git a/src/cheogram/java/com/cheogram/android/ConnectionService.java b/src/cheogram/java/com/cheogram/android/ConnectionService.java index c1dbb49cc..57b2e541a 100644 --- a/src/cheogram/java/com/cheogram/android/ConnectionService.java +++ b/src/cheogram/java/com/cheogram/android/ConnectionService.java @@ -150,6 +150,29 @@ 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; @@ -203,6 +226,8 @@ 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(); @@ -235,6 +260,15 @@ 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 onDisconnect() { if (rtpConnection == null || rtpConnection.get() == null) { diff --git a/src/main/java/eu/siacs/conversations/entities/Contact.java b/src/main/java/eu/siacs/conversations/entities/Contact.java index 7e25f979c..bc1c809e4 100644 --- a/src/main/java/eu/siacs/conversations/entities/Contact.java +++ b/src/main/java/eu/siacs/conversations/entities/Contact.java @@ -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" diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index 75c9fa3cc..0fdba2675 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -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) { + 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) { + if (tryRingingWithDialerUI(id, media)) { + return; + } + showIncomingCallNotification(id, media); final NotificationManager notificationManager = (NotificationManager) mXmppConnectionService.getSystemService(Context.NOTIFICATION_SERVICE); final int currentInterruptionFilter; From 6ca2ffd6ea85f9090dfc9f818ce08e9de0b4d657 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Fri, 11 Mar 2022 21:52:49 -0500 Subject: [PATCH 3/5] ConnectionService: miscellaneous fixes * Fix a few potential errors due to the use of newer APIs (minSDK is still only 24) * Fix one remaining case of raw usage of generic types. --- .../java/com/cheogram/android/ConnectionService.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/cheogram/java/com/cheogram/android/ConnectionService.java b/src/cheogram/java/com/cheogram/android/ConnectionService.java index 57b2e541a..ef0e64788 100644 --- a/src/cheogram/java/com/cheogram/android/ConnectionService.java +++ b/src/cheogram/java/com/cheogram/android/ConnectionService.java @@ -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; @@ -246,6 +249,8 @@ public class ConnectionService extends android.telecom.ConnectionService { @Override public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices) { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O) return; + switch(selectedAudioDevice) { case SPEAKER_PHONE: setAudioRoute(CallAudioState.ROUTE_SPEAKER); @@ -310,9 +315,9 @@ public class ConnectionService extends android.telecom.ConnectionService { while (!postDial.empty()) { String next = postDial.pop(); if (next.equals(";")) { - Stack v = (Stack) postDial.clone(); + Vector v = new Vector<>(postDial); Collections.reverse(v); - setPostDialWait(String.join("", v)); + setPostDialWait(Joiner.on("").join(v)); return; } else if (next.equals(",")) { sleep(2000); From d8d49e03a0cad3d2447c116e761c3e482c7325e6 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 12 Mar 2022 13:08:42 -0500 Subject: [PATCH 4/5] ConnectionService: implement onReject() Forgot this for incoming calls --- .../java/com/cheogram/android/ConnectionService.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/cheogram/java/com/cheogram/android/ConnectionService.java b/src/cheogram/java/com/cheogram/android/ConnectionService.java index ef0e64788..be497a8b8 100644 --- a/src/cheogram/java/com/cheogram/android/ConnectionService.java +++ b/src/cheogram/java/com/cheogram/android/ConnectionService.java @@ -274,6 +274,13 @@ public class ConnectionService extends android.telecom.ConnectionService { rtpConnection.get().acceptCall(); } + @Override + public void onReject() { + this.rtpConnection = xmppConnectionService.getJingleConnectionManager().findJingleRtpConnection(account, with, sessionId); + rtpConnection.get().rejectCall(); + setDisconnected(new DisconnectCause(DisconnectCause.LOCAL)); + } + @Override public void onDisconnect() { if (rtpConnection == null || rtpConnection.get() == null) { From 437002d1898a343c1adcf511b3a7211c99d579df Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Sat, 12 Mar 2022 22:55:14 -0500 Subject: [PATCH 5/5] ConnectionService: handle disconnected state correctly onDisconnect() will only be called when the user manually requests a disconnect. When the call is disconnected by the rtp connection itself, the call won't be cleaned up and the system will be stuck in an in-call state. Handle this correctly by moving the clean-up code to a close() function, and calling that function instead when we disconnect. Note that we cannot call the cleanup routine destroy() from the onStateChanged() function, because it seems that this triggers a deadlock somewhere down the line, probably in the calling account-related telephony code. Doing a close() function mirrors what is done in the default TelephonyConnection implementation for RIL-based calls. --- .../cheogram/android/ConnectionService.java | 28 +++++++++++-------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/src/cheogram/java/com/cheogram/android/ConnectionService.java b/src/cheogram/java/com/cheogram/android/ConnectionService.java index be497a8b8..08091e2d7 100644 --- a/src/cheogram/java/com/cheogram/android/ConnectionService.java +++ b/src/cheogram/java/com/cheogram/android/ConnectionService.java @@ -136,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)); } }); @@ -237,13 +237,13 @@ public class ConnectionService extends android.telecom.ConnectionService { 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)); } } @@ -278,21 +278,27 @@ public class ConnectionService extends android.telecom.ConnectionService { public void onReject() { this.rtpConnection = xmppConnectionService.getJingleConnectionManager().findJingleRtpConnection(account, with, sessionId); rtpConnection.get().rejectCall(); - setDisconnected(new DisconnectCause(DisconnectCause.LOCAL)); + 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