Browse Source

Merge pull request #2995 from nextcloud/handle-received-signaling-messages-for-call-reactions

Handle received signaling messages for call reactions
Marcel Hibbe 2 years ago
parent
commit
51bedbba9c

+ 55 - 10
app/src/main/java/com/nextcloud/talk/activities/CallActivity.java

@@ -296,6 +296,10 @@ public class CallActivity extends CallBaseActivity {
 
 
     private Handler screenParticipantDisplayItemManagersHandler = new Handler(Looper.getMainLooper());
     private Handler screenParticipantDisplayItemManagersHandler = new Handler(Looper.getMainLooper());
 
 
+    private Map<String, CallParticipantEventDisplayer> callParticipantEventDisplayers = new HashMap<>();
+
+    private Handler callParticipantEventDisplayersHandler = new Handler(Looper.getMainLooper());
+
     private CallParticipantList.Observer callParticipantListObserver = new CallParticipantList.Observer() {
     private CallParticipantList.Observer callParticipantListObserver = new CallParticipantList.Observer() {
         @Override
         @Override
         public void onCallParticipantsChanged(Collection<Participant> joined, Collection<Participant> updated,
         public void onCallParticipantsChanged(Collection<Participant> joined, Collection<Participant> updated,
@@ -2248,6 +2252,11 @@ public class CallActivity extends CallBaseActivity {
         screenParticipantDisplayItemManagers.put(sessionId, screenParticipantDisplayItemManager);
         screenParticipantDisplayItemManagers.put(sessionId, screenParticipantDisplayItemManager);
         callParticipantModel.addObserver(screenParticipantDisplayItemManager, screenParticipantDisplayItemManagersHandler);
         callParticipantModel.addObserver(screenParticipantDisplayItemManager, screenParticipantDisplayItemManagersHandler);
 
 
+        CallParticipantEventDisplayer callParticipantEventDisplayer =
+            new CallParticipantEventDisplayer(callParticipantModel);
+        callParticipantEventDisplayers.put(sessionId, callParticipantEventDisplayer);
+        callParticipantModel.addObserver(callParticipantEventDisplayer, callParticipantEventDisplayersHandler);
+
         runOnUiThread(() -> {
         runOnUiThread(() -> {
             addParticipantDisplayItem(callParticipantModel, "video");
             addParticipantDisplayItem(callParticipantModel, "video");
         });
         });
@@ -2288,6 +2297,10 @@ public class CallActivity extends CallBaseActivity {
             screenParticipantDisplayItemManagers.remove(sessionId);
             screenParticipantDisplayItemManagers.remove(sessionId);
         callParticipant.getCallParticipantModel().removeObserver(screenParticipantDisplayItemManager);
         callParticipant.getCallParticipantModel().removeObserver(screenParticipantDisplayItemManager);
 
 
+        CallParticipantEventDisplayer callParticipantEventDisplayer =
+            callParticipantEventDisplayers.remove(sessionId);
+        callParticipant.getCallParticipantModel().removeObserver(callParticipantEventDisplayer);
+
         callParticipant.destroy();
         callParticipant.destroy();
 
 
         SignalingMessageReceiver.CallParticipantMessageListener listener = callParticipantMessageListeners.remove(sessionId);
         SignalingMessageReceiver.CallParticipantMessageListener listener = callParticipantMessageListeners.remove(sessionId);
@@ -2793,16 +2806,10 @@ public class CallActivity extends CallBaseActivity {
 
 
         @Override
         @Override
         public void onRaiseHand(boolean state, long timestamp) {
         public void onRaiseHand(boolean state, long timestamp) {
-            if (state) {
-                CallParticipant participant = callParticipants.get(sessionId);
-                if (participant != null) {
-                    String nick = participant.getCallParticipantModel().getNick();
-                    runOnUiThread(() -> Toast.makeText(
-                        context,
-                        String.format(context.getResources().getString(R.string.nc_call_raised_hand), nick),
-                        Toast.LENGTH_LONG).show());
-                }
-            }
+        }
+
+        @Override
+        public void onReaction(String reaction) {
         }
         }
 
 
         @Override
         @Override
@@ -2857,6 +2864,44 @@ public class CallActivity extends CallBaseActivity {
                 addParticipantDisplayItem(callParticipantModel, "screen");
                 addParticipantDisplayItem(callParticipantModel, "screen");
             }
             }
         }
         }
+
+        @Override
+        public void onReaction(String reaction) {
+        }
+    }
+
+    private class CallParticipantEventDisplayer implements CallParticipantModel.Observer {
+
+        private final CallParticipantModel callParticipantModel;
+
+        private boolean raisedHand;
+
+        private CallParticipantEventDisplayer(CallParticipantModel callParticipantModel) {
+            this.callParticipantModel = callParticipantModel;
+            this.raisedHand = callParticipantModel.getRaisedHand() != null ?
+                callParticipantModel.getRaisedHand().getState() : false;
+        }
+
+        @Override
+        public void onChange() {
+            if (callParticipantModel.getRaisedHand() == null || !callParticipantModel.getRaisedHand().getState()) {
+                raisedHand = false;
+
+                return;
+            }
+
+            if (raisedHand) {
+                return;
+            }
+            raisedHand = true;
+
+            String nick = callParticipantModel.getNick();
+            Toast.makeText(context, String.format(context.getResources().getString(R.string.nc_call_raised_hand), nick), Toast.LENGTH_LONG).show();
+        }
+
+        @Override
+        public void onReaction(String reaction) {
+        }
     }
     }
 
 
     private class InternalSignalingMessageSender implements SignalingMessageSender {
     private class InternalSignalingMessageSender implements SignalingMessageSender {

+ 10 - 1
app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.java

@@ -34,7 +34,16 @@ public class ParticipantDisplayItem {
 
 
     private final CallParticipantModel callParticipantModel;
     private final CallParticipantModel callParticipantModel;
 
 
-    private final CallParticipantModel.Observer callParticipantModelObserver = this::updateFromModel;
+    private final CallParticipantModel.Observer callParticipantModelObserver = new CallParticipantModel.Observer() {
+        @Override
+        public void onChange() {
+            updateFromModel();
+        }
+
+        @Override
+        public void onReaction(String reaction) {
+        }
+    };
 
 
     private String userId;
     private String userId;
     private PeerConnection.IceConnectionState iceConnectionState;
     private PeerConnection.IceConnectionState iceConnectionState;

+ 5 - 0
app/src/main/java/com/nextcloud/talk/call/CallParticipant.java

@@ -40,6 +40,11 @@ public class CallParticipant {
             callParticipantModel.setRaisedHand(state, timestamp);
             callParticipantModel.setRaisedHand(state, timestamp);
         }
         }
 
 
+        @Override
+        public void onReaction(String reaction) {
+            callParticipantModel.emitReaction(reaction);
+        }
+
         @Override
         @Override
         public void onUnshareScreen() {
         public void onUnshareScreen() {
         }
         }

+ 5 - 1
app/src/main/java/com/nextcloud/talk/call/CallParticipantModel.java

@@ -42,11 +42,15 @@ import java.util.Objects;
  * Getters called after receiving a notification are guaranteed to provide at least the value that triggered the
  * Getters called after receiving a notification are guaranteed to provide at least the value that triggered the
  * notification, but it may return even a more up to date one (so getting the value again on the following
  * notification, but it may return even a more up to date one (so getting the value again on the following
  * notification may return the same value as before).
  * notification may return the same value as before).
+ *
+ * Besides onChange(), which notifies about changes in the model values, CallParticipantModel.Observer provides
+ * additional methods to be notified about one-time events that are not reflected in the model values, like reactions.
  */
  */
 public class CallParticipantModel {
 public class CallParticipantModel {
 
 
     public interface Observer {
     public interface Observer {
         void onChange();
         void onChange();
+        void onReaction(String reaction);
     }
     }
 
 
     protected class Data<T> {
     protected class Data<T> {
@@ -68,7 +72,7 @@ public class CallParticipantModel {
         }
         }
     }
     }
 
 
-    private final CallParticipantModelNotifier callParticipantModelNotifier = new CallParticipantModelNotifier();
+    protected final CallParticipantModelNotifier callParticipantModelNotifier = new CallParticipantModelNotifier();
 
 
     protected final String sessionId;
     protected final String sessionId;
 
 

+ 12 - 0
app/src/main/java/com/nextcloud/talk/call/CallParticipantModelNotifier.java

@@ -83,4 +83,16 @@ class CallParticipantModelNotifier {
             }
             }
         }
         }
     }
     }
+
+    public synchronized void notifyReaction(String reaction) {
+        for (CallParticipantModelObserverOn observerOn : new ArrayList<>(callParticipantModelObserversOn)) {
+            if (observerOn.handler == null || observerOn.handler.getLooper() == Looper.myLooper()) {
+                observerOn.observer.onReaction(reaction);
+            } else {
+                observerOn.handler.post(() -> {
+                    observerOn.observer.onReaction(reaction);
+                });
+            }
+        }
+    }
 }
 }

+ 4 - 0
app/src/main/java/com/nextcloud/talk/call/MutableCallParticipantModel.java

@@ -72,4 +72,8 @@ public class MutableCallParticipantModel extends CallParticipantModel {
     public void setScreenMediaStream(MediaStream screenMediaStream) {
     public void setScreenMediaStream(MediaStream screenMediaStream) {
         this.screenMediaStream.setValue(screenMediaStream);
         this.screenMediaStream.setValue(screenMediaStream);
     }
     }
+
+    public void emitReaction(String reaction) {
+        this.callParticipantModelNotifier.notifyReaction(reaction);
+    }
 }
 }

+ 4 - 2
app/src/main/java/com/nextcloud/talk/models/json/signaling/NCMessagePayload.kt

@@ -42,8 +42,10 @@ data class NCMessagePayload(
     @JsonField(name = ["state"])
     @JsonField(name = ["state"])
     var state: Boolean? = null,
     var state: Boolean? = null,
     @JsonField(name = ["timestamp"])
     @JsonField(name = ["timestamp"])
-    var timestamp: Long? = null
+    var timestamp: Long? = null,
+    @JsonField(name = ["reaction"])
+    var reaction: String? = null
 ) : Parcelable {
 ) : Parcelable {
     // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
     // This constructor is added to work with the 'com.bluelinelabs.logansquare.annotation.JsonObject'
-    constructor() : this(null, null, null, null, null, null, null)
+    constructor() : this(null, null, null, null, null, null, null, null)
 }
 }

+ 6 - 0
app/src/main/java/com/nextcloud/talk/signaling/CallParticipantMessageNotifier.java

@@ -93,6 +93,12 @@ class CallParticipantMessageNotifier {
         }
         }
     }
     }
 
 
+    public void notifyReaction(String sessionId, String reaction) {
+        for (SignalingMessageReceiver.CallParticipantMessageListener listener : getListenersFor(sessionId)) {
+            listener.onReaction(reaction);
+        }
+    }
+
     public synchronized void notifyUnshareScreen(String sessionId) {
     public synchronized void notifyUnshareScreen(String sessionId) {
         for (SignalingMessageReceiver.CallParticipantMessageListener listener : getListenersFor(sessionId)) {
         for (SignalingMessageReceiver.CallParticipantMessageListener listener : getListenersFor(sessionId)) {
             listener.onUnshareScreen();
             listener.onUnshareScreen();

+ 52 - 0
app/src/main/java/com/nextcloud/talk/signaling/SignalingMessageReceiver.java

@@ -149,6 +149,7 @@ public abstract class SignalingMessageReceiver {
      */
      */
     public interface CallParticipantMessageListener {
     public interface CallParticipantMessageListener {
         void onRaiseHand(boolean state, long timestamp);
         void onRaiseHand(boolean state, long timestamp);
+        void onReaction(String reaction);
         void onUnshareScreen();
         void onUnshareScreen();
     }
     }
 
 
@@ -562,6 +563,57 @@ public abstract class SignalingMessageReceiver {
             return;
             return;
         }
         }
 
 
+        if ("reaction".equals(type)) {
+            // Message schema (external signaling server):
+            // {
+            //     "type": "message",
+            //     "message": {
+            //         "sender": {
+            //             ...
+            //         },
+            //         "data": {
+            //             "to": #STRING#,
+            //             "roomType": "video",
+            //             "type": "reaction",
+            //             "payload": {
+            //                 "reaction": #STRING#,
+            //             },
+            //             "from": #STRING#,
+            //         },
+            //     },
+            // }
+            //
+            // Message schema (internal signaling server):
+            // {
+            //     "type": "message",
+            //     "data": {
+            //         "to": #STRING#,
+            //         "roomType": "video",
+            //         "type": "reaction",
+            //         "payload": {
+            //             "reaction": #STRING#,
+            //         },
+            //         "from": #STRING#,
+            //     },
+            // }
+
+            NCMessagePayload payload = signalingMessage.getPayload();
+            if (payload == null) {
+                // Broken message, this should not happen.
+                return;
+            }
+
+            String reaction = payload.getReaction();
+            if (reaction == null) {
+                // Broken message, this should not happen.
+                return;
+            }
+
+            callParticipantMessageNotifier.notifyReaction(sessionId, reaction);
+
+            return;
+        }
+
         // "unshareScreen" messages are directly sent to the screen peer connection when the internal signaling
         // "unshareScreen" messages are directly sent to the screen peer connection when the internal signaling
         // server is used, and to the room when the external signaling server is used. However, the (relevant) data
         // server is used, and to the room when the external signaling server is used. However, the (relevant) data
         // of the received message ("from" and "type") is the same in both cases.
         // of the received message ("from" and "type") is the same in both cases.

+ 7 - 0
app/src/test/java/com/nextcloud/talk/call/CallParticipantModelTest.kt

@@ -55,4 +55,11 @@ class CallParticipantModelTest {
         callParticipantModel!!.setRaisedHand(true, 4815162342L)
         callParticipantModel!!.setRaisedHand(true, 4815162342L)
         Mockito.verify(mockedCallParticipantModelObserver, Mockito.only())?.onChange()
         Mockito.verify(mockedCallParticipantModelObserver, Mockito.only())?.onChange()
     }
     }
+
+    @Test
+    fun testEmitReaction() {
+        callParticipantModel!!.addObserver(mockedCallParticipantModelObserver)
+        callParticipantModel!!.emitReaction("theReaction")
+        Mockito.verify(mockedCallParticipantModelObserver, Mockito.only())?.onReaction("theReaction")
+    }
 }
 }

+ 20 - 0
app/src/test/java/com/nextcloud/talk/signaling/SignalingMessageReceiverCallParticipantTest.java

@@ -84,6 +84,26 @@ public class SignalingMessageReceiverCallParticipantTest {
         verify(mockedCallParticipantMessageListener, only()).onRaiseHand(true, 4815162342L);
         verify(mockedCallParticipantMessageListener, only()).onRaiseHand(true, 4815162342L);
     }
     }
 
 
+    @Test
+    public void testCallParticipantMessageReaction() {
+        SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener =
+            mock(SignalingMessageReceiver.CallParticipantMessageListener.class);
+
+        signalingMessageReceiver.addListener(mockedCallParticipantMessageListener, "theSessionId");
+
+        NCSignalingMessage signalingMessage = new NCSignalingMessage();
+        signalingMessage.setFrom("theSessionId");
+        signalingMessage.setType("reaction");
+        signalingMessage.setRoomType("theRoomType");
+        NCMessagePayload messagePayload = new NCMessagePayload();
+        messagePayload.setType("reaction");
+        messagePayload.setReaction("theReaction");
+        signalingMessage.setPayload(messagePayload);
+        signalingMessageReceiver.processSignalingMessage(signalingMessage);
+
+        verify(mockedCallParticipantMessageListener, only()).onReaction("theReaction");
+    }
+
     @Test
     @Test
     public void testCallParticipantMessageUnshareScreen() {
     public void testCallParticipantMessageUnshareScreen() {
         SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener =
         SignalingMessageReceiver.CallParticipantMessageListener mockedCallParticipantMessageListener =