Merge branch 'for-singpolyma' of https://gitea.angry.im/PeterCxy/cheogram

Stephen Paul Weber created

* 'for-singpolyma' of https://gitea.angry.im/PeterCxy/cheogram:
  ConnectionService: handle disconnected state correctly
  ConnectionService: implement onReject()
  ConnectionService: miscellaneous fixes
  ConnectionService: Dialer UI integration for incoming calls
  ConnectionService: fix unchecked type assignments

Change summary

src/cheogram/java/com/cheogram/android/ConnectionService.java          | 80 
src/main/java/eu/siacs/conversations/entities/Contact.java             |  2 
src/main/java/eu/siacs/conversations/services/NotificationService.java | 61 
3 files changed, 128 insertions(+), 15 deletions(-)

Detailed changes

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<String> permissions = new HashSet();
+		Set<String> 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<String> postDial = new Stack();
+		protected Stack<String> postDial = new Stack<>();
 		protected Icon gatewayIcon;
 		protected WeakReference<JingleRtpConnection> 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<AppRTCAudioManager.AudioDevice> 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<String> v = new Vector<>(postDial);
 					Collections.reverse(v);
-					setPostDialWait(String.join("", v));
+					setPostDialWait(Joiner.on("").join(v));
 					return;
 				} else if (next.equals(",")) {
 					sleep(2000);

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"

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> 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> media) {
+        if (tryRingingWithDialerUI(id, media)) {
+            return;
+        }
+
         showIncomingCallNotification(id, media);
         final NotificationManager notificationManager = (NotificationManager) mXmppConnectionService.getSystemService(Context.NOTIFICATION_SERVICE);
         final int currentInterruptionFilter;