From b7b2bb0cddba2bb9027031b3a5bab4c16073bc5c Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Fri, 11 Mar 2022 21:44:57 -0500 Subject: [PATCH 1/8] 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 faab2d5799c4d5e57c7eb3987c33d958fe2fd627 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Fri, 11 Mar 2022 21:51:08 -0500 Subject: [PATCH 2/8] 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 5e8d51db9fb9fde47243f86eb5b80202debd69a8 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Fri, 11 Mar 2022 21:52:49 -0500 Subject: [PATCH 3/8] 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 e02dee8c01f4ba2fc5cf55ad23f97e3daa3c4f49 Mon Sep 17 00:00:00 2001 From: Stephen Paul Weber Date: Fri, 11 Mar 2022 23:00:49 -0500 Subject: [PATCH 4/8] Bad number can also cause IllegalArgumentException --- src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java index 7a4c2d371..7c4e31416 100644 --- a/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java +++ b/src/main/java/eu/siacs/conversations/ui/EnterJidDialog.java @@ -276,7 +276,7 @@ public class EnterJidDialog extends DialogFragment implements OnBackendConnected if (type != null && (type.equals("pstn") || type.equals("sms"))) { try { binding.jid.setText(PhoneNumberUtilWrapper.normalize(getActivity(), binding.jid.getText().toString())); - } catch (NumberParseException | NullPointerException e) { } + } catch (NumberParseException | IllegalArgumentException | NullPointerException e) { } } if (p == null) { From b218b57c940825dfc6bfb5bc203bf20e5bc8c066 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Fri, 11 Mar 2022 21:44:57 -0500 Subject: [PATCH 5/8] 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 6/8] 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 7/8] 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 8/8] 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) {