From 0cd067892184c215615ecfbb3ca0b635b9e603e1 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Fri, 11 Mar 2022 21:51:08 -0500 Subject: [PATCH 1/3] ConnectionService: Dialer UI integration for incoming calls --- .../cheogram/android/ConnectionService.java | 66 ++++++++++++++++++- .../siacs/conversations/entities/Contact.java | 2 +- .../services/NotificationService.java | 48 ++++++++++++++ 3 files changed, 112 insertions(+), 4 deletions(-) diff --git a/src/cheogram/java/com/cheogram/android/ConnectionService.java b/src/cheogram/java/com/cheogram/android/ConnectionService.java index c1dbb49cc..1cf5e53d2 100644 --- a/src/cheogram/java/com/cheogram/android/ConnectionService.java +++ b/src/cheogram/java/com/cheogram/android/ConnectionService.java @@ -114,9 +114,9 @@ public class ConnectionService extends android.telecom.ConnectionService { PermissionManager permissionManager = PermissionManager.getInstance(this); permissionManager.setNotificationSettings( - new NotificationSettings.Builder() - .withMessage(R.string.microphone_permission_for_call) - .withSmallIcon(R.drawable.ic_notification).build() + new NotificationSettings.Builder() + .withMessage(R.string.microphone_permission_for_call) + .withSmallIcon(R.drawable.ic_notification).build() ); Set permissions = new HashSet<>(); @@ -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,41 @@ 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); + + // Request recording permission only when answering + PermissionManager permissionManager = PermissionManager.getInstance(ConnectionService.this); + permissionManager.setNotificationSettings( + new NotificationSettings.Builder() + .withMessage(R.string.microphone_permission_for_call) + .withSmallIcon(R.drawable.ic_notification).build() + ); + + Set permissions = new HashSet<>(); + permissions.add(Manifest.permission.RECORD_AUDIO); + permissionManager.checkPermissions(permissions, new PermissionManager.PermissionRequestListener() { + @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) { 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..d07a127a2 100644 --- a/src/main/java/eu/siacs/conversations/services/NotificationService.java +++ b/src/main/java/eu/siacs/conversations/services/NotificationService.java @@ -16,9 +16,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 +426,52 @@ public class NotificationService { notify(DELIVERY_FAILED_NOTIFICATION_ID, summaryNotification); } + private synchronized boolean tryRingingWithDialerUI(final AbstractJingleConnection.Id id, final Set 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) { + // 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 729b86a2fc49745a03a0afb9cefdf2a929b76fd2 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Fri, 11 Mar 2022 21:52:49 -0500 Subject: [PATCH 2/3] 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 1cf5e53d2..f4f8053f7 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); @@ -336,9 +341,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 bb281ab7e0919acf35f967f0644bfb63acd2419b Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Fri, 11 Mar 2022 22:04:58 -0500 Subject: [PATCH 3/3] do not use Dialer UI for incoming calls if audio permission is not granted We cannot request new permissions when Dialer UI is shown. For incoming calls, if the Dialer UI is displayed over keyguard, then the user may not even be able to see the permission notifications that we use for outgoing calls. If we just do not use the Dialer UI when the permission is not granted, it is at most a minor annoyance for the first time. After the user has accepted an incoming call even just once, the permission will be granted, and the Dialer integration will start to work just fine. --- .../cheogram/android/ConnectionService.java | 26 +------------------ .../services/NotificationService.java | 9 +++++++ 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/src/cheogram/java/com/cheogram/android/ConnectionService.java b/src/cheogram/java/com/cheogram/android/ConnectionService.java index f4f8053f7..a0239960d 100644 --- a/src/cheogram/java/com/cheogram/android/ConnectionService.java +++ b/src/cheogram/java/com/cheogram/android/ConnectionService.java @@ -273,31 +273,7 @@ public class ConnectionService extends android.telecom.ConnectionService { xmppConnectionService.getJingleConnectionManager() .findJingleRtpConnection(account, with, sessionId); - // Request recording permission only when answering - PermissionManager permissionManager = PermissionManager.getInstance(ConnectionService.this); - permissionManager.setNotificationSettings( - new NotificationSettings.Builder() - .withMessage(R.string.microphone_permission_for_call) - .withSmallIcon(R.drawable.ic_notification).build() - ); - - Set permissions = new HashSet<>(); - permissions.add(Manifest.permission.RECORD_AUDIO); - permissionManager.checkPermissions(permissions, new PermissionManager.PermissionRequestListener() { - @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)); - } - }); + rtpConnection.get().acceptCall(); } @Override diff --git a/src/main/java/eu/siacs/conversations/services/NotificationService.java b/src/main/java/eu/siacs/conversations/services/NotificationService.java index d07a127a2..2dd77ce91 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; @@ -427,6 +429,13 @@ public class NotificationService { } 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");