diff --git a/src/cheogram/java/com/cheogram/android/ConnectionService.java b/src/cheogram/java/com/cheogram/android/ConnectionService.java index 671578dd8..08091e2d7 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; @@ -119,7 +122,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 @@ -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 postDial = new Stack(); + protected Stack postDial = new Stack<>(); protected Icon gatewayIcon; protected WeakReference 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 availableAudioDevices) { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.O) return; + switch(selectedAudioDevice) { case SPEAKER_PHONE: setAudioRoute(CallAudioState.ROUTE_SPEAKER); @@ -235,18 +265,40 @@ 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 v = new Vector<>(postDial); Collections.reverse(v); - setPostDialWait(String.join("", v)); + setPostDialWait(Joiner.on("").join(v)); return; } else if (next.equals(",")) { sleep(2000); 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;