From 18f21c4f482a573b32173966d749c89e2c144316 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Daniel=20Calvi=C3=B1o=20S=C3=A1nchez?= <danxuliu@gmail.com>
Date: Fri, 25 Nov 2022 22:07:05 +0100
Subject: [PATCH] Update ParticipantDisplayItem from CallParticipantModel
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Instead of explicitly setting the values on the ParticipantDisplayItems
now the values are set on the CallParticipantModels, and the items are
automatically updated from their model when they change.

Different items are still used for the audio/video and screen shares of
the same participant, so the type is used to select from which
properties of the model is the item updated.

As the model may be updated from background threads it is explicitly
observed by the items from the main thread using a Handler shared by all
the items.

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
---
 .../talk/activities/CallActivity.java         | 189 +++++++++---------
 .../talk/adapters/ParticipantDisplayItem.java | 115 ++++++-----
 2 files changed, 150 insertions(+), 154 deletions(-)

diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java
index 1fd0d2c99..321b8257b 100644
--- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java
+++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java
@@ -60,6 +60,8 @@ import com.nextcloud.talk.adapters.ParticipantDisplayItem;
 import com.nextcloud.talk.adapters.ParticipantsAdapter;
 import com.nextcloud.talk.api.NcApi;
 import com.nextcloud.talk.application.NextcloudTalkApplication;
+import com.nextcloud.talk.call.CallParticipantModel;
+import com.nextcloud.talk.call.MutableCallParticipantModel;
 import com.nextcloud.talk.data.user.model.User;
 import com.nextcloud.talk.databinding.CallActivityBinding;
 import com.nextcloud.talk.events.ConfigurationChangeEvent;
@@ -231,7 +233,6 @@ public class CallActivity extends CallBaseActivity {
     private MediaStream localStream;
     private String credentials;
     private List<PeerConnectionWrapper> peerConnectionWrapperList = new ArrayList<>();
-    private Map<String, String> userIdsBySessionId = new HashMap<>();
 
     private boolean videoOn = false;
     private boolean microphoneOn = false;
@@ -267,6 +268,8 @@ public class CallActivity extends CallBaseActivity {
 
     private Map<String, PeerConnectionWrapper.PeerConnectionObserver> peerConnectionObservers = new HashMap<>();
 
+    private Map<String, MutableCallParticipantModel> callParticipantModels = new HashMap<>();
+
     private SignalingMessageReceiver.ParticipantListMessageListener participantListMessageListener = new SignalingMessageReceiver.ParticipantListMessageListener() {
 
         @Override
@@ -382,6 +385,7 @@ public class CallActivity extends CallBaseActivity {
             requestBluetoothPermission();
         }
         basicInitialization();
+        callParticipantModels = new HashMap<>();
         participantDisplayItems = new HashMap<>();
         initViews();
         if (!isConnectionEstablished()) {
@@ -1776,7 +1780,7 @@ public class CallActivity extends CallBaseActivity {
         Log.d(TAG, "processUsersInRoom");
         List<String> newSessions = new ArrayList<>();
         Set<String> oldSessions = new HashSet<>();
-        userIdsBySessionId = new HashMap<>();
+        Map<String, String> userIdsBySessionId = new HashMap<>();
 
         hasMCU = hasExternalSignalingServer && webSocketClient != null && webSocketClient.hasMCU();
         Log.d(TAG, "   hasMCU is " + hasMCU);
@@ -1856,15 +1860,16 @@ public class CallActivity extends CallBaseActivity {
 
             String userId = userIdsBySessionId.get(sessionId);
             if (userId != null) {
-                runOnUiThread(() -> {
-                    if (participantDisplayItems.get(sessionId + "-video") != null) {
-                        participantDisplayItems.get(sessionId + "-video").setUserId(userId);
-                    }
-                    if (participantDisplayItems.get(sessionId + "-screen") != null) {
-                        participantDisplayItems.get(sessionId + "-screen").setUserId(userId);
-                    }
-                });
+                callParticipantModels.get(sessionId).setUserId(userId);
             }
+
+            String nick;
+            if (hasExternalSignalingServer) {
+                nick = webSocketClient.getDisplayNameForSession(sessionId);
+            } else {
+                nick = offerAnswerNickProviders.get(sessionId) != null ? offerAnswerNickProviders.get(sessionId).getNick() : "";
+            }
+            callParticipantModels.get(sessionId).setNick(nick);
         }
 
         if (newSessions.size() > 0 && currentCallStatus != CallStatus.IN_CONVERSATION) {
@@ -1991,14 +1996,16 @@ public class CallActivity extends CallBaseActivity {
             peerConnectionWrapper.addObserver(peerConnectionObserver);
 
             if (!publisher) {
+                MutableCallParticipantModel mutableCallParticipantModel = callParticipantModels.get(sessionId);
+                if (mutableCallParticipantModel == null) {
+                    mutableCallParticipantModel = new MutableCallParticipantModel(sessionId);
+                    callParticipantModels.put(sessionId, mutableCallParticipantModel);
+                }
+
+                final CallParticipantModel callParticipantModel = mutableCallParticipantModel;
+
                 runOnUiThread(() -> {
-                    // userId is unknown here, but it will be got based on the session id, and the stream will be
-                    // updated once it is added to the connection.
-                    setupVideoStreamForLayout(
-                        null,
-                        sessionId,
-                        false,
-                        type);
+                    setupVideoStreamForLayout(callParticipantModel, type);
                 });
             }
 
@@ -2036,6 +2043,20 @@ public class CallActivity extends CallBaseActivity {
                         peerConnectionWrapper.removeObserver(peerConnectionObserver);
 
                         runOnUiThread(() -> removeMediaStream(sessionId, videoStreamType));
+
+                        MutableCallParticipantModel mutableCallParticipantModel = callParticipantModels.get(sessionId);
+                        if (mutableCallParticipantModel != null) {
+                            if ("screen".equals(videoStreamType)) {
+                                mutableCallParticipantModel.setScreenMediaStream(null);
+                                mutableCallParticipantModel.setScreenIceConnectionState(null);
+                            } else {
+                                mutableCallParticipantModel.setMediaStream(null);
+                                mutableCallParticipantModel.setIceConnectionState(null);
+                                mutableCallParticipantModel.setAudioAvailable(null);
+                                mutableCallParticipantModel.setVideoAvailable(null);
+                            }
+                        }
+
                         deletePeerConnection(peerConnectionWrapper);
                     }
                 }
@@ -2051,12 +2072,19 @@ public class CallActivity extends CallBaseActivity {
                 signalingMessageReceiver.removeListener(offerAnswerNickProvider.getVideoWebRtcMessageListener());
                 signalingMessageReceiver.removeListener(offerAnswerNickProvider.getScreenWebRtcMessageListener());
             }
+
+            callParticipantModels.remove(sessionId);
         }
     }
 
     private void removeMediaStream(String sessionId, String videoStreamType) {
         Log.d(TAG, "removeMediaStream");
-        participantDisplayItems.remove(sessionId + "-" + videoStreamType);
+        ParticipantDisplayItem participantDisplayItem = participantDisplayItems.remove(sessionId + "-" + videoStreamType);
+        if (participantDisplayItem == null) {
+            return;
+        }
+
+        participantDisplayItem.destroy();
 
         if (!isDestroyed()) {
             initGridAdapter();
@@ -2183,40 +2211,16 @@ public class CallActivity extends CallBaseActivity {
                                                          this);
     }
 
-    private void setupVideoStreamForLayout(@Nullable MediaStream mediaStream,
-                                           String session,
-                                           boolean videoStreamEnabled,
-                                           String videoStreamType) {
-        PeerConnectionWrapper peerConnectionWrapper = getPeerConnectionWrapperForSessionIdAndType(session,
-                                                                                                  videoStreamType);
-
-        PeerConnection.IceConnectionState iceConnectionState = null;
-        if (peerConnectionWrapper != null) {
-            iceConnectionState = peerConnectionWrapper.getPeerConnection().iceConnectionState();
-        }
-
-        String nick;
-        if (hasExternalSignalingServer) {
-            nick = webSocketClient.getDisplayNameForSession(session);
-        } else {
-            nick = offerAnswerNickProviders.get(session) != null ? offerAnswerNickProviders.get(session).getNick() : "";
-        }
-
-        String userId = userIdsBySessionId.get(session);
-
+    private void setupVideoStreamForLayout(CallParticipantModel callParticipantModel, String videoStreamType) {
         String defaultGuestNick = getResources().getString(R.string.nc_nick_guest);
 
         ParticipantDisplayItem participantDisplayItem = new ParticipantDisplayItem(baseUrl,
-                                                                                   userId,
-                                                                                   session,
-                                                                                   iceConnectionState,
-                                                                                   nick,
                                                                                    defaultGuestNick,
-                                                                                   mediaStream,
+                                                                                   rootEglBase,
                                                                                    videoStreamType,
-                                                                                   videoStreamEnabled,
-                                                                                   rootEglBase);
-        participantDisplayItems.put(session + "-" + videoStreamType, participantDisplayItem);
+                                                                                   callParticipantModel);
+        String sessionId = callParticipantModel.getSessionId();
+        participantDisplayItems.put(sessionId + "-" + videoStreamType, participantDisplayItem);
 
         initGridAdapter();
     }
@@ -2525,11 +2529,8 @@ public class CallActivity extends CallBaseActivity {
         private void onOfferOrAnswer(String nick) {
             this.nick = nick;
 
-            if (participantDisplayItems.get(sessionId + "-video") != null) {
-                participantDisplayItems.get(sessionId + "-video").setNick(nick);
-            }
-            if (participantDisplayItems.get(sessionId + "-screen") != null) {
-                participantDisplayItems.get(sessionId + "-screen").setNick(nick);
+            if (callParticipantModels.get(sessionId) != null) {
+                callParticipantModels.get(sessionId).setNick(nick);
             }
         }
 
@@ -2562,56 +2563,45 @@ public class CallActivity extends CallBaseActivity {
 
     private class CallActivityDataChannelMessageListener implements PeerConnectionWrapper.DataChannelMessageListener {
 
-        private final String participantDisplayItemId;
+        private final String sessionId;
 
         private CallActivityDataChannelMessageListener(String sessionId) {
-            // DataChannel messages are sent only in video peers, so the listener only acts on the "video" items.
-            this.participantDisplayItemId = sessionId + "-video";
+            this.sessionId = sessionId;
         }
 
         @Override
         public void onAudioOn() {
-            runOnUiThread(() -> {
-                if (participantDisplayItems.get(participantDisplayItemId) != null) {
-                    participantDisplayItems.get(participantDisplayItemId).setAudioEnabled(true);
-                }
-            });
+            if (callParticipantModels.get(sessionId) != null) {
+                callParticipantModels.get(sessionId).setAudioAvailable(true);
+            }
         }
 
         @Override
         public void onAudioOff() {
-            runOnUiThread(() -> {
-                if (participantDisplayItems.get(participantDisplayItemId) != null) {
-                    participantDisplayItems.get(participantDisplayItemId).setAudioEnabled(false);
-                }
-            });
+            if (callParticipantModels.get(sessionId) != null) {
+                callParticipantModels.get(sessionId).setAudioAvailable(false);
+            }
         }
 
         @Override
         public void onVideoOn() {
-            runOnUiThread(() -> {
-                if (participantDisplayItems.get(participantDisplayItemId) != null) {
-                    participantDisplayItems.get(participantDisplayItemId).setStreamEnabled(true);
-                }
-            });
+            if (callParticipantModels.get(sessionId) != null) {
+                callParticipantModels.get(sessionId).setVideoAvailable(true);
+            }
         }
 
         @Override
         public void onVideoOff() {
-            runOnUiThread(() -> {
-                if (participantDisplayItems.get(participantDisplayItemId) != null) {
-                    participantDisplayItems.get(participantDisplayItemId).setStreamEnabled(false);
-                }
-            });
+            if (callParticipantModels.get(sessionId) != null) {
+                callParticipantModels.get(sessionId).setVideoAvailable(false);
+            }
         }
 
         @Override
         public void onNickChanged(String nick) {
-            runOnUiThread(() -> {
-                if (participantDisplayItems.get(participantDisplayItemId) != null) {
-                    participantDisplayItems.get(participantDisplayItemId).setNick(nick);
-                }
-            });
+            if (callParticipantModels.get(sessionId) != null) {
+                callParticipantModels.get(sessionId).setNick(nick);
+            }
         }
     }
 
@@ -2619,12 +2609,10 @@ public class CallActivity extends CallBaseActivity {
 
         private final String sessionId;
         private final String videoStreamType;
-        private final String participantDisplayItemId;
 
         private CallActivityPeerConnectionObserver(String sessionId, String videoStreamType) {
             this.sessionId = sessionId;
             this.videoStreamType = videoStreamType;
-            this.participantDisplayItemId = sessionId + "-" + videoStreamType;
         }
 
         @Override
@@ -2638,29 +2626,38 @@ public class CallActivity extends CallBaseActivity {
         }
 
         private void handleStream(MediaStream mediaStream) {
-            runOnUiThread(() -> {
-                if (participantDisplayItems.get(participantDisplayItemId) == null) {
-                    return;
-                }
+            if (callParticipantModels.get(sessionId) == null) {
+                return;
+            }
 
-                boolean hasAtLeastOneVideoStream = false;
-                if (mediaStream != null) {
-                    hasAtLeastOneVideoStream = mediaStream.videoTracks != null && mediaStream.videoTracks.size() > 0;
-                }
+            if ("screen".equals(videoStreamType)) {
+                callParticipantModels.get(sessionId).setScreenMediaStream(mediaStream);
 
-                ParticipantDisplayItem participantDisplayItem = participantDisplayItems.get(participantDisplayItemId);
-                participantDisplayItem.setMediaStream(mediaStream);
-                participantDisplayItem.setStreamEnabled(hasAtLeastOneVideoStream);
-            });
+                return;
+            }
+
+            boolean hasAtLeastOneVideoStream = false;
+            if (mediaStream != null) {
+                hasAtLeastOneVideoStream = mediaStream.videoTracks != null && mediaStream.videoTracks.size() > 0;
+            }
+
+            callParticipantModels.get(sessionId).setMediaStream(mediaStream);
+            callParticipantModels.get(sessionId).setVideoAvailable(hasAtLeastOneVideoStream);
         }
 
         @Override
         public void onIceConnectionStateChanged(PeerConnection.IceConnectionState iceConnectionState) {
+            if (callParticipantModels.get(sessionId) != null) {
+                if ("screen".equals(videoStreamType)) {
+                    callParticipantModels.get(sessionId).setScreenIceConnectionState(iceConnectionState);
+                } else {
+                    callParticipantModels.get(sessionId).setIceConnectionState(iceConnectionState);
+                }
+            }
+
             runOnUiThread(() -> {
                 if (webSocketClient != null && webSocketClient.getSessionId() != null && webSocketClient.getSessionId().equals(sessionId)) {
                     updateSelfVideoViewIceConnectionState(iceConnectionState);
-                } else if (participantDisplayItems.get(participantDisplayItemId) != null) {
-                    participantDisplayItems.get(participantDisplayItemId).setIceConnectionState(iceConnectionState);
                 }
 
                 if (iceConnectionState == PeerConnection.IceConnectionState.CLOSED) {
diff --git a/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.java b/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.java
index 746f21710..6a9912ead 100644
--- a/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.java
+++ b/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.java
@@ -1,7 +1,10 @@
 package com.nextcloud.talk.adapters;
 
+import android.os.Handler;
+import android.os.Looper;
 import android.text.TextUtils;
 
+import com.nextcloud.talk.call.CallParticipantModel;
 import com.nextcloud.talk.utils.ApiUtils;
 
 import org.webrtc.EglBase;
@@ -14,6 +17,11 @@ public class ParticipantDisplayItem {
         void onChange();
     }
 
+    /**
+     * Shared handler to receive change notifications from the model on the main thread.
+     */
+    private static final Handler handler = new Handler(Looper.getMainLooper());
+
     private final ParticipantDisplayItemNotifier participantDisplayItemNotifier = new ParticipantDisplayItemNotifier();
 
     private final String baseUrl;
@@ -23,6 +31,10 @@ public class ParticipantDisplayItem {
     private final String session;
     private final String streamType;
 
+    private final CallParticipantModel callParticipantModel;
+
+    private final CallParticipantModel.Observer callParticipantModelObserver = this::updateFromModel;
+
     private String userId;
     private PeerConnection.IceConnectionState iceConnectionState;
     private String nick;
@@ -31,60 +43,48 @@ public class ParticipantDisplayItem {
     private boolean streamEnabled;
     private boolean isAudioEnabled;
 
-    public ParticipantDisplayItem(String baseUrl, String userId, String session, PeerConnection.IceConnectionState iceConnectionState, String nick, String defaultGuestNick, MediaStream mediaStream, String streamType, boolean streamEnabled, EglBase rootEglBase) {
+    public ParticipantDisplayItem(String baseUrl, String defaultGuestNick, EglBase rootEglBase, String streamType,
+                                  CallParticipantModel callParticipantModel) {
         this.baseUrl = baseUrl;
-        this.userId = userId;
-        this.session = session;
-        this.iceConnectionState = iceConnectionState;
-        this.nick = nick;
         this.defaultGuestNick = defaultGuestNick;
-        this.mediaStream = mediaStream;
-        this.streamType = streamType;
-        this.streamEnabled = streamEnabled;
         this.rootEglBase = rootEglBase;
 
-        this.updateUrlForAvatar();
+        this.session = callParticipantModel.getSessionId();
+        this.streamType = streamType;
+
+        this.callParticipantModel = callParticipantModel;
+        this.callParticipantModel.addObserver(callParticipantModelObserver, handler);
+
+        updateFromModel();
     }
 
-    public void setUserId(String userId) {
-        this.userId = userId;
+    public void destroy() {
+        this.callParticipantModel.removeObserver(callParticipantModelObserver);
+    }
+
+    private void updateFromModel() {
+        userId = callParticipantModel.getUserId();
+        nick = callParticipantModel.getNick();
 
         this.updateUrlForAvatar();
 
-        participantDisplayItemNotifier.notifyChange();
-    }
-
-    public boolean isConnected() {
-        return iceConnectionState == PeerConnection.IceConnectionState.CONNECTED ||
-            iceConnectionState == PeerConnection.IceConnectionState.COMPLETED;
-    }
-
-    public void setIceConnectionState(PeerConnection.IceConnectionState iceConnectionState) {
-        this.iceConnectionState = iceConnectionState;
-
-        participantDisplayItemNotifier.notifyChange();
-    }
-
-    public String getNick() {
-        if (TextUtils.isEmpty(userId) && TextUtils.isEmpty(nick)) {
-            return defaultGuestNick;
+        if ("screen".equals(streamType)) {
+            iceConnectionState = callParticipantModel.getScreenIceConnectionState();
+            mediaStream = callParticipantModel.getScreenMediaStream();
+            isAudioEnabled = true;
+            streamEnabled = true;
+        } else {
+            iceConnectionState = callParticipantModel.getIceConnectionState();
+            mediaStream = callParticipantModel.getMediaStream();
+            isAudioEnabled = callParticipantModel.isAudioAvailable() != null ?
+                callParticipantModel.isAudioAvailable() : false;
+            streamEnabled = callParticipantModel.isVideoAvailable() != null ?
+                callParticipantModel.isVideoAvailable() : false;
         }
 
-        return nick;
-    }
-
-    public void setNick(String nick) {
-        this.nick = nick;
-
-        this.updateUrlForAvatar();
-
         participantDisplayItemNotifier.notifyChange();
     }
 
-    public String getUrlForAvatar() {
-        return urlForAvatar;
-    }
-
     private void updateUrlForAvatar() {
         if (!TextUtils.isEmpty(userId)) {
             urlForAvatar = ApiUtils.getUrlForAvatar(baseUrl, userId, true);
@@ -93,26 +93,31 @@ public class ParticipantDisplayItem {
         }
     }
 
+    public boolean isConnected() {
+        return iceConnectionState == PeerConnection.IceConnectionState.CONNECTED ||
+            iceConnectionState == PeerConnection.IceConnectionState.COMPLETED;
+    }
+
+    public String getNick() {
+        if (TextUtils.isEmpty(userId) && TextUtils.isEmpty(nick)) {
+            return defaultGuestNick;
+        }
+
+        return nick;
+    }
+
+    public String getUrlForAvatar() {
+        return urlForAvatar;
+    }
+
     public MediaStream getMediaStream() {
         return mediaStream;
     }
 
-    public void setMediaStream(MediaStream mediaStream) {
-        this.mediaStream = mediaStream;
-
-        participantDisplayItemNotifier.notifyChange();
-    }
-
     public boolean isStreamEnabled() {
         return streamEnabled;
     }
 
-    public void setStreamEnabled(boolean streamEnabled) {
-        this.streamEnabled = streamEnabled;
-
-        participantDisplayItemNotifier.notifyChange();
-    }
-
     public EglBase getRootEglBase() {
         return rootEglBase;
     }
@@ -121,12 +126,6 @@ public class ParticipantDisplayItem {
         return isAudioEnabled;
     }
 
-    public void setAudioEnabled(boolean audioEnabled) {
-        isAudioEnabled = audioEnabled;
-
-        participantDisplayItemNotifier.notifyChange();
-    }
-
     public void addObserver(Observer observer) {
         participantDisplayItemNotifier.addObserver(observer);
     }