From 6ff1a2366f4d47c6c7c63aa80ce1318a9f6bd986 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Fri, 11 Mar 2022 13:43:00 -0500 Subject: [PATCH 1/5] ConnectionService: reformat --- .../cheogram/android/ConnectionService.java | 471 +++++++++--------- 1 file changed, 232 insertions(+), 239 deletions(-) diff --git a/src/cheogram/java/com/cheogram/android/ConnectionService.java b/src/cheogram/java/com/cheogram/android/ConnectionService.java index 671578dd8..3ba683a5e 100644 --- a/src/cheogram/java/com/cheogram/android/ConnectionService.java +++ b/src/cheogram/java/com/cheogram/android/ConnectionService.java @@ -1,292 +1,285 @@ package com.cheogram.android; +import android.Manifest; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.graphics.drawable.Icon; +import android.net.Uri; +import android.os.IBinder; +import android.telecom.CallAudioState; +import android.telecom.Connection; +import android.telecom.ConnectionRequest; +import android.telecom.DisconnectCause; +import android.telecom.PhoneAccountHandle; +import android.telecom.StatusHints; +import android.telecom.TelecomManager; +import android.telephony.PhoneNumberUtils; +import android.util.Log; + +import com.google.common.collect.ImmutableSet; +import com.intentfilter.androidpermissions.NotificationSettings; +import com.intentfilter.androidpermissions.PermissionManager; +import com.intentfilter.androidpermissions.models.DeniedPermissions; + import java.lang.ref.WeakReference; import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.Stack; -import com.google.common.collect.ImmutableSet; - -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.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.services.XmppConnectionService.XmppConnectionBinder; 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; +import io.michaelrocks.libphonenumber.android.NumberParseException; 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(); - } + 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 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 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 void onDestroy() { + unbindService(mConnection); + } - @Override - public Connection onCreateOutgoingConnection( - PhoneAccountHandle phoneAccountHandle, - ConnectionRequest request - ) { - String[] gateway = phoneAccountHandle.getId().split("/", 2); + @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 rawTel = request.getAddress().getSchemeSpecificPart(); + String postDial = PhoneNumberUtils.extractPostDialPortion(rawTel); - String tel = PhoneNumberUtils.extractNetworkPortion(rawTel); - try { - tel = PhoneNumberUtilWrapper.normalize(this, tel); - } catch (NumberParseException e) { - return Connection.createFailedConnection( - new DisconnectCause(DisconnectCause.ERROR) - ); - } + String tel = PhoneNumberUtils.extractNetworkPortion(rawTel); + try { + tel = PhoneNumberUtilWrapper.normalize(this, tel); + } catch (NumberParseException e) { + return Connection.createFailedConnection( + new DisconnectCause(DisconnectCause.ERROR) + ); + } - if (xmppConnectionService.getJingleConnectionManager().isBusy() != null) { - return Connection.createFailedConnection( - new DisconnectCause(DisconnectCause.BUSY) - ); - } + if (xmppConnectionService.getJingleConnectionManager().isBusy() != null) { + return Connection.createFailedConnection( + new DisconnectCause(DisconnectCause.BUSY) + ); + } - Account account = xmppConnectionService.findAccountByJid(Jid.of(gateway[0])); - Jid with = Jid.ofLocalAndDomain(tel, gateway[1]); - CheogramConnection connection = new CheogramConnection(account, with, postDial); + Account account = xmppConnectionService.findAccountByJid(Jid.of(gateway[0])); + 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() - ); + PermissionManager permissionManager = PermissionManager.getInstance(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() { - connection.setSessionId(xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession( - account, - with, - ImmutableSet.of(Media.AUDIO) - )); - } + Set 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.setDisconnected(new DisconnectCause(DisconnectCause.ERROR)); - } - }); + @Override + public void onPermissionDenied(DeniedPermissions deniedPermissions) { + connection.setDisconnected(new DisconnectCause(DisconnectCause.ERROR)); + } + }); - connection.setInitializing(); - connection.setAddress( - Uri.fromParts("tel", tel, null), // Normalized tel as tel: URI - TelecomManager.PRESENTATION_ALLOWED - ); + connection.setInitializing(); + connection.setAddress( + Uri.fromParts("tel", tel, null), // Normalized tel as tel: URI + TelecomManager.PRESENTATION_ALLOWED + ); - xmppConnectionService.setOnRtpConnectionUpdateListener( - (XmppConnectionService.OnJingleRtpConnectionUpdate) connection - ); + xmppConnectionService.setOnRtpConnectionUpdateListener( + (XmppConnectionService.OnJingleRtpConnectionUpdate) connection + ); - return connection; - } + return connection; + } - public class CheogramConnection extends Connection implements XmppConnectionService.OnJingleRtpConnectionUpdate { - protected Account account; - protected Jid with; - protected String sessionId = null; - protected Stack postDial = new Stack(); - protected Icon gatewayIcon; - protected WeakReference rtpConnection = null; + public class CheogramConnection extends Connection implements XmppConnectionService.OnJingleRtpConnectionUpdate { + protected Account account; + protected Jid with; + protected String sessionId = null; + protected Stack postDial = new Stack(); + protected Icon gatewayIcon; + protected WeakReference rtpConnection = null; - CheogramConnection(Account account, Jid with, String postDialString) { - super(); - this.account = account; - this.with = with; + 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 - )); + 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)); - } - } + 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 - ); - } + 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; - } + 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 - rtpConnection = xmppConnectionService.getJingleConnectionManager().findJingleRtpConnection(account, with, 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 + rtpConnection = xmppConnectionService.getJingleConnectionManager().findJingleRtpConnection(account, with, sessionId); + } - setStatusHints(new StatusHints(null, gatewayIcon, null)); + setStatusHints(new StatusHints(null, gatewayIcon, null)); - if (state == RtpEndUserState.FINDING_DEVICE) { - setInitialized(); - } else if (state == RtpEndUserState.RINGING) { - setDialing(); - } else if (state == RtpEndUserState.CONNECTED) { - xmppConnectionService.setDiallerIntegrationActive(true); - setActive(); + if (state == RtpEndUserState.FINDING_DEVICE) { + setInitialized(); + } else if (state == RtpEndUserState.RINGING) { + setDialing(); + } else if (state == RtpEndUserState.CONNECTED) { + xmppConnectionService.setDiallerIntegrationActive(true); + setActive(); - postDial(); - } else if (state == RtpEndUserState.DECLINED_OR_BUSY) { - setDisconnected(new DisconnectCause(DisconnectCause.BUSY)); - } else if (state == RtpEndUserState.ENDED) { - setDisconnected(new DisconnectCause(DisconnectCause.LOCAL)); - } else if (state == RtpEndUserState.RETRACTED) { - setDisconnected(new DisconnectCause(DisconnectCause.CANCELED)); - } else if (RtpSessionActivity.END_CARD.contains(state)) { - setDisconnected(new DisconnectCause(DisconnectCause.ERROR)); - } - } + postDial(); + } else if (state == RtpEndUserState.DECLINED_OR_BUSY) { + setDisconnected(new DisconnectCause(DisconnectCause.BUSY)); + } else if (state == RtpEndUserState.ENDED) { + setDisconnected(new DisconnectCause(DisconnectCause.LOCAL)); + } else if (state == RtpEndUserState.RETRACTED) { + setDisconnected(new DisconnectCause(DisconnectCause.CANCELED)); + } else if (RtpSessionActivity.END_CARD.contains(state)) { + setDisconnected(new DisconnectCause(DisconnectCause.ERROR)); + } + } - @Override - public void onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices) { - 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 onAudioDeviceChanged(AppRTCAudioManager.AudioDevice selectedAudioDevice, Set availableAudioDevices) { + 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 onDisconnect() { - if (rtpConnection == null || rtpConnection.get() == null) { - xmppConnectionService.getJingleConnectionManager().retractSessionProposal(account, with.asBareJid()); - } else { - rtpConnection.get().endCall(); - } - destroy(); - xmppConnectionService.setDiallerIntegrationActive(false); - xmppConnectionService.removeRtpConnectionUpdateListener( - (XmppConnectionService.OnJingleRtpConnectionUpdate) this - ); - } + @Override + public void onDisconnect() { + if (rtpConnection == null || rtpConnection.get() == null) { + xmppConnectionService.getJingleConnectionManager().retractSessionProposal(account, with.asBareJid()); + } else { + rtpConnection.get().endCall(); + } + destroy(); + xmppConnectionService.setDiallerIntegrationActive(false); + xmppConnectionService.removeRtpConnectionUpdateListener( + (XmppConnectionService.OnJingleRtpConnectionUpdate) this + ); + } - @Override - public void onAbort() { - onDisconnect(); - } + @Override + public void onAbort() { + onDisconnect(); + } - @Override - public void onPlayDtmfTone(char c) { - rtpConnection.get().applyDtmfTone("" + c); - } + @Override + public void onPlayDtmfTone(char c) { + rtpConnection.get().applyDtmfTone("" + c); + } - @Override - public void onPostDialContinue(boolean c) { - if (c) postDial(); - } + @Override + public void onPostDialContinue(boolean c) { + if (c) postDial(); + } - protected void sleep(int ms) { - try { - Thread.sleep(ms); - } catch (InterruptedException ex) { - Thread.currentThread().interrupt(); - } - } + 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(";")) { - Stack v = (Stack) postDial.clone(); - Collections.reverse(v); - setPostDialWait(String.join("", v)); - return; - } else if (next.equals(",")) { - sleep(2000); - } else { - rtpConnection.get().applyDtmfTone(next); - sleep(100); - } - } - } - } + protected void postDial() { + while (!postDial.empty()) { + String next = postDial.pop(); + if (next.equals(";")) { + Stack v = (Stack) postDial.clone(); + Collections.reverse(v); + setPostDialWait(String.join("", v)); + return; + } else if (next.equals(",")) { + sleep(2000); + } else { + rtpConnection.get().applyDtmfTone(next); + sleep(100); + } + } + } + } } From 0872f2412880eab8133307fcc3647fa2c46cbb0e Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Fri, 11 Mar 2022 13:45:46 -0500 Subject: [PATCH 2/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 3ba683a5e..9d5e464c7 100644 --- a/src/cheogram/java/com/cheogram/android/ConnectionService.java +++ b/src/cheogram/java/com/cheogram/android/ConnectionService.java @@ -112,7 +112,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 @@ -147,7 +147,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 d3078dfd8ba77f68c16acfadf37be300b93dccec Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Fri, 11 Mar 2022 15:17:06 -0500 Subject: [PATCH 3/5] ConnectionService: Dialer UI integration for incoming calls --- .../cheogram/android/ConnectionService.java | 71 ++++++++++++++++--- .../siacs/conversations/entities/Contact.java | 2 +- .../services/NotificationService.java | 48 +++++++++++++ 3 files changed, 112 insertions(+), 9 deletions(-) diff --git a/src/cheogram/java/com/cheogram/android/ConnectionService.java b/src/cheogram/java/com/cheogram/android/ConnectionService.java index 9d5e464c7..6d4c14c80 100644 --- a/src/cheogram/java/com/cheogram/android/ConnectionService.java +++ b/src/cheogram/java/com/cheogram/android/ConnectionService.java @@ -7,6 +7,7 @@ import android.content.Intent; import android.content.ServiceConnection; import android.graphics.drawable.Icon; import android.net.Uri; +import android.os.Bundle; import android.os.IBinder; import android.telecom.CallAudioState; import android.telecom.Connection; @@ -57,9 +58,17 @@ public class ConnectionService extends android.telecom.ConnectionService { xmppConnectionService = null; } }; + private PermissionManager mPermissionManager; @Override public void onCreate() { + mPermissionManager = PermissionManager.getInstance(this); + mPermissionManager.setNotificationSettings( + new NotificationSettings.Builder() + .withMessage(R.string.microphone_permission_for_call) + .withSmallIcon(R.drawable.ic_notification).build() + ); + // From XmppActivity.connectToBackend Intent intent = new Intent(this, XmppConnectionService.class); intent.setAction("ui"); @@ -105,16 +114,9 @@ public class ConnectionService extends android.telecom.ConnectionService { 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 permissions = new HashSet<>(); permissions.add(Manifest.permission.RECORD_AUDIO); - permissionManager.checkPermissions(permissions, new PermissionManager.PermissionRequestListener() { + mPermissionManager.checkPermissions(permissions, new PermissionManager.PermissionRequestListener() { @Override public void onPermissionGranted() { connection.setSessionId(xmppConnectionService.getJingleConnectionManager().proposeJingleRtpSession( @@ -143,6 +145,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; @@ -196,6 +221,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(); @@ -228,6 +255,34 @@ 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 + Set permissions = new HashSet<>(); + permissions.add(Manifest.permission.RECORD_AUDIO); + mPermissionManager.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..62baeccd5 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) { + // There *could* be race conditions where the account is not registered yet + // when an incoming call is already received + 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 9cb59cc7356685c379a655e7c6e5d745ae582223 Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Fri, 11 Mar 2022 16:57:04 -0500 Subject: [PATCH 4/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 6d4c14c80..89d00962a 100644 --- a/src/cheogram/java/com/cheogram/android/ConnectionService.java +++ b/src/cheogram/java/com/cheogram/android/ConnectionService.java @@ -7,6 +7,7 @@ import android.content.Intent; import android.content.ServiceConnection; import android.graphics.drawable.Icon; import android.net.Uri; +import android.os.Build; import android.os.Bundle; import android.os.IBinder; import android.telecom.CallAudioState; @@ -19,6 +20,7 @@ import android.telecom.TelecomManager; import android.telephony.PhoneNumberUtils; import android.util.Log; +import com.google.common.base.Joiner; import com.google.common.collect.ImmutableSet; import com.intentfilter.androidpermissions.NotificationSettings; import com.intentfilter.androidpermissions.PermissionManager; @@ -29,6 +31,7 @@ import java.util.Collections; import java.util.HashSet; import java.util.Set; import java.util.Stack; +import java.util.Vector; import eu.siacs.conversations.R; import eu.siacs.conversations.entities.Account; @@ -241,6 +244,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); @@ -324,9 +329,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("").skipNulls().join(v)); return; } else if (next.equals(",")) { sleep(2000); From 0f581f889a654f5af4fd54f857c402e4a40ad3bf Mon Sep 17 00:00:00 2001 From: Peter Cai Date: Fri, 11 Mar 2022 17:43:41 -0500 Subject: [PATCH 5/5] do not use Dialer UI for incoming calls if audio permission is not granted We cannot always request new permissions when Dialer UI is shown. For example, Dialer UI can be shown over keyguard, which doesn't allow other dialogs (like the permission request dialog) to be displayed. However it doesn't return the failed response either, making ConnectionService kind of stuck. 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 | 19 +------------------ .../services/NotificationService.java | 8 ++++++++ 2 files changed, 9 insertions(+), 18 deletions(-) diff --git a/src/cheogram/java/com/cheogram/android/ConnectionService.java b/src/cheogram/java/com/cheogram/android/ConnectionService.java index 89d00962a..1a31faa21 100644 --- a/src/cheogram/java/com/cheogram/android/ConnectionService.java +++ b/src/cheogram/java/com/cheogram/android/ConnectionService.java @@ -268,24 +268,7 @@ public class ConnectionService extends android.telecom.ConnectionService { xmppConnectionService.getJingleConnectionManager() .findJingleRtpConnection(account, with, sessionId); - // Request recording permission only when answering - Set permissions = new HashSet<>(); - permissions.add(Manifest.permission.RECORD_AUDIO); - mPermissionManager.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 62baeccd5..b73eb4994 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,12 @@ 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 always request audio permission in Dialer UI + // e.g. when Dialer is shown over keyguard + 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");