Add data model for local call participants

This is the counterpart of CallParticipantModel for the local
participant. For now it just stores whether audio and video are enabled
or not, and whether the local participant is speaking or not, but it
will be eventually extended with further properties.

It is also expected that the views, like the button with the microphone
state, will update themselves based on the model. Similarly the model
should be moved from the CallActivity to a class similar to
CallParticipant but for the local participant. In any case, all that is
something for the future; the immediate use of the model will be to know
when the local state changes to notify other participants.

Signed-off-by: Daniel Calviño Sánchez <danxuliu@gmail.com>
This commit is contained in:
Daniel Calviño Sánchez 2024-10-25 19:43:17 +02:00 committed by Marcel Hibbe
parent 36a29ed36e
commit cb52fb349f
No known key found for this signature in database
GPG Key ID: C793F8B59F43CE7B
5 changed files with 414 additions and 0 deletions

View File

@ -66,6 +66,7 @@ import com.nextcloud.talk.call.CallParticipantModel
import com.nextcloud.talk.call.MessageSender
import com.nextcloud.talk.call.MessageSenderMcu
import com.nextcloud.talk.call.MessageSenderNoMcu
import com.nextcloud.talk.call.MutableLocalCallParticipantModel
import com.nextcloud.talk.call.ReactionAnimator
import com.nextcloud.talk.chat.ChatActivity
import com.nextcloud.talk.data.user.model.User
@ -246,6 +247,7 @@ class CallActivity : CallBaseActivity() {
private val internalSignalingMessageSender = InternalSignalingMessageSender()
private var signalingMessageSender: SignalingMessageSender? = null
private var messageSender: MessageSender? = null
private val localCallParticipantModel: MutableLocalCallParticipantModel = MutableLocalCallParticipantModel()
private val offerAnswerNickProviders: MutableMap<String?, OfferAnswerNickProvider?> = HashMap()
private val callParticipantMessageListeners: MutableMap<String?, CallParticipantMessageListener> = HashMap()
private val selfPeerConnectionObserver: PeerConnectionObserver = CallActivitySelfPeerConnectionObserver()
@ -1123,6 +1125,7 @@ class CallActivity : CallBaseActivity() {
localStream!!.addTrack(localVideoTrack)
localVideoTrack!!.setEnabled(false)
localVideoTrack!!.addSink(binding!!.selfVideoRenderer)
localCallParticipantModel.isVideoEnabled = false
}
private fun microphoneInitialization() {
@ -1133,6 +1136,7 @@ class CallActivity : CallBaseActivity() {
localAudioTrack = peerConnectionFactory!!.createAudioTrack("NCa0", audioSource)
localAudioTrack!!.setEnabled(false)
localStream!!.addTrack(localAudioTrack)
localCallParticipantModel.isAudioEnabled = false
}
@SuppressLint("MissingPermission")
@ -1157,9 +1161,11 @@ class CallActivity : CallBaseActivity() {
if (microphoneOn && isCurrentlySpeaking && !isSpeakingLongTerm) {
isSpeakingLongTerm = true
localCallParticipantModel.isSpeaking = true
sendIsSpeakingMessage(true)
} else if (!isCurrentlySpeaking && isSpeakingLongTerm) {
isSpeakingLongTerm = false
localCallParticipantModel.isSpeaking = false
sendIsSpeakingMessage(false)
}
Thread.sleep(MICROPHONE_VALUE_SLEEP)
@ -1342,6 +1348,7 @@ class CallActivity : CallBaseActivity() {
}
if (localStream != null && localStream!!.videoTracks.size > 0) {
localStream!!.videoTracks[0].setEnabled(enable)
localCallParticipantModel.isVideoEnabled = enable
}
if (enable) {
binding!!.selfVideoRenderer.visibility = View.VISIBLE
@ -1358,6 +1365,7 @@ class CallActivity : CallBaseActivity() {
}
if (localStream != null && localStream!!.audioTracks.size > 0) {
localStream!!.audioTracks[0].setEnabled(enable)
localCallParticipantModel.isAudioEnabled = enable
}
}
if (isConnectionEstablished) {

View File

@ -0,0 +1,114 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez <danxuliu@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.call;
import android.os.Handler;
import java.util.Objects;
/**
* Read-only data model for local call participants.
* <p>
* Clients of the model can observe it with LocalCallParticipantModel.Observer to be notified when any value changes.
* 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
* may return the same value as before).
*/
public class LocalCallParticipantModel {
protected final LocalCallParticipantModelNotifier localCallParticipantModelNotifier =
new LocalCallParticipantModelNotifier();
protected Data<Boolean> audioEnabled;
protected Data<Boolean> speaking;
protected Data<Boolean> speakingWhileMuted;
protected Data<Boolean> videoEnabled;
public interface Observer {
void onChange();
}
protected class Data<T> {
private T value;
public Data() {
}
public Data(T value) {
this.value = value;
}
public T getValue() {
return value;
}
public void setValue(T value) {
if (Objects.equals(this.value, value)) {
return;
}
this.value = value;
localCallParticipantModelNotifier.notifyChange();
}
}
public LocalCallParticipantModel() {
this.audioEnabled = new Data<>(Boolean.FALSE);
this.speaking = new Data<>(Boolean.FALSE);
this.speakingWhileMuted = new Data<>(Boolean.FALSE);
this.videoEnabled = new Data<>(Boolean.FALSE);
}
public Boolean isAudioEnabled() {
return audioEnabled.getValue();
}
public Boolean isSpeaking() {
return speaking.getValue();
}
public Boolean isSpeakingWhileMuted() {
return speakingWhileMuted.getValue();
}
public Boolean isVideoEnabled() {
return videoEnabled.getValue();
}
/**
* Adds an Observer to be notified when any value changes.
*
* @param observer the Observer
* @see LocalCallParticipantModel#addObserver(Observer, Handler)
*/
public void addObserver(Observer observer) {
addObserver(observer, null);
}
/**
* Adds an observer to be notified when any value changes.
* <p>
* The observer will be notified on the thread associated to the given handler. If no handler is given the
* observer will be immediately notified on the same thread that changed the value; the observer will be
* immediately notified too if the thread of the handler is the same thread that changed the value.
* <p>
* An observer is expected to be added only once. If the same observer is added again it will be notified just
* once on the thread of the last handler.
*
* @param observer the Observer
* @param handler a Handler for the thread to be notified on
*/
public void addObserver(Observer observer, Handler handler) {
localCallParticipantModelNotifier.addObserver(observer, handler);
}
public void removeObserver(Observer observer) {
localCallParticipantModelNotifier.removeObserver(observer);
}
}

View File

@ -0,0 +1,73 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez <danxuliu@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.call;
import android.os.Handler;
import android.os.Looper;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
/**
* Helper class to register and notify LocalCallParticipantModel.Observers.
* <p>
* This class is only meant for internal use by LocalCallParticipantModel; observers must register themselves against a
* LocalCallParticipantModel rather than against a LocalCallParticipantModelNotifier.
*/
class LocalCallParticipantModelNotifier {
private final List<LocalCallParticipantModelObserverOn> localCallParticipantModelObserversOn = new ArrayList<>();
/**
* Helper class to associate a LocalCallParticipantModel.Observer with a Handler.
*/
private static class LocalCallParticipantModelObserverOn {
public final LocalCallParticipantModel.Observer observer;
public final Handler handler;
private LocalCallParticipantModelObserverOn(LocalCallParticipantModel.Observer observer, Handler handler) {
this.observer = observer;
this.handler = handler;
}
}
public synchronized void addObserver(LocalCallParticipantModel.Observer observer, Handler handler) {
if (observer == null) {
throw new IllegalArgumentException("LocalCallParticipantModel.Observer can not be null");
}
removeObserver(observer);
localCallParticipantModelObserversOn.add(new LocalCallParticipantModelObserverOn(observer, handler));
}
public synchronized void removeObserver(LocalCallParticipantModel.Observer observer) {
Iterator<LocalCallParticipantModelObserverOn> it = localCallParticipantModelObserversOn.iterator();
while (it.hasNext()) {
LocalCallParticipantModelObserverOn observerOn = it.next();
if (observerOn.observer == observer) {
it.remove();
return;
}
}
}
public synchronized void notifyChange() {
for (LocalCallParticipantModelObserverOn observerOn : new ArrayList<>(localCallParticipantModelObserversOn)) {
if (observerOn.handler == null || observerOn.handler.getLooper() == Looper.myLooper()) {
observerOn.observer.onChange();
} else {
observerOn.handler.post(() -> {
observerOn.observer.onChange();
});
}
}
}
}

View File

@ -0,0 +1,51 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez <danxuliu@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.call;
import java.util.Objects;
/**
* Mutable data model for local call participants.
* <p>
* Setting "speaking" will automatically set "speaking" or "speakingWhileMuted" as needed, depending on whether audio is
* enabled or not. Similarly, setting whether the audio is enabled or disabled will automatically switch between
* "speaking" and "speakingWhileMuted" as needed.
* <p>
* There is no synchronization when setting the values; if needed, it should be handled by the clients of the model.
*/
public class MutableLocalCallParticipantModel extends LocalCallParticipantModel {
public void setAudioEnabled(Boolean audioEnabled) {
if (Objects.equals(this.audioEnabled.getValue(), audioEnabled)) {
return;
}
if (audioEnabled == null || !audioEnabled) {
this.speakingWhileMuted.setValue(this.speaking.getValue());
this.speaking.setValue(Boolean.FALSE);
}
this.audioEnabled.setValue(audioEnabled);
if (audioEnabled != null && audioEnabled) {
this.speaking.setValue(this.speakingWhileMuted.getValue());
this.speakingWhileMuted.setValue(Boolean.FALSE);
}
}
public void setSpeaking(Boolean speaking) {
if (this.audioEnabled.getValue() != null && this.audioEnabled.getValue()) {
this.speaking.setValue(speaking);
} else {
this.speakingWhileMuted.setValue(speaking);
}
}
public void setVideoEnabled(Boolean videoEnabled) {
this.videoEnabled.setValue(videoEnabled);
}
}

View File

@ -0,0 +1,168 @@
/*
* Nextcloud Talk - Android Client
*
* SPDX-FileCopyrightText: 2024 Daniel Calviño Sánchez <danxuliu@gmail.com>
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.talk.call
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.mockito.Mockito
class LocalCallParticipantModelTest {
private var localCallParticipantModel: MutableLocalCallParticipantModel? = null
private var mockedLocalCallParticipantModelObserver: LocalCallParticipantModel.Observer? = null
@Before
fun setUp() {
localCallParticipantModel = MutableLocalCallParticipantModel()
mockedLocalCallParticipantModelObserver = Mockito.mock(LocalCallParticipantModel.Observer::class.java)
}
@Test
fun testSetAudioEnabled() {
localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver)
localCallParticipantModel!!.isAudioEnabled = true
assertTrue(localCallParticipantModel!!.isAudioEnabled)
assertFalse(localCallParticipantModel!!.isSpeaking)
assertFalse(localCallParticipantModel!!.isSpeakingWhileMuted)
Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.only())?.onChange()
}
@Test
fun testSetAudioEnabledWhileSpeakingWhileMuted() {
localCallParticipantModel!!.isSpeaking = true
localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver)
localCallParticipantModel!!.isAudioEnabled = true
assertTrue(localCallParticipantModel!!.isAudioEnabled)
assertTrue(localCallParticipantModel!!.isSpeaking)
assertFalse(localCallParticipantModel!!.isSpeakingWhileMuted)
Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.times(3))?.onChange()
}
@Test
fun testSetAudioEnabledTwiceWhileSpeakingWhileMuted() {
localCallParticipantModel!!.isSpeaking = true
localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver)
localCallParticipantModel!!.isAudioEnabled = true
localCallParticipantModel!!.isAudioEnabled = true
assertTrue(localCallParticipantModel!!.isAudioEnabled)
assertTrue(localCallParticipantModel!!.isSpeaking)
assertFalse(localCallParticipantModel!!.isSpeakingWhileMuted)
Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.times(3))?.onChange()
}
@Test
fun testSetAudioDisabled() {
localCallParticipantModel!!.isAudioEnabled = true
localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver)
localCallParticipantModel!!.isAudioEnabled = false
assertFalse(localCallParticipantModel!!.isAudioEnabled)
assertFalse(localCallParticipantModel!!.isSpeaking)
assertFalse(localCallParticipantModel!!.isSpeakingWhileMuted)
Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.only())?.onChange()
}
@Test
fun testSetAudioDisabledWhileSpeaking() {
localCallParticipantModel!!.isAudioEnabled = true
localCallParticipantModel!!.isSpeaking = true
localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver)
localCallParticipantModel!!.isAudioEnabled = false
assertFalse(localCallParticipantModel!!.isAudioEnabled)
assertFalse(localCallParticipantModel!!.isSpeaking)
assertTrue(localCallParticipantModel!!.isSpeakingWhileMuted)
Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.times(3))?.onChange()
}
@Test
fun testSetAudioDisabledTwiceWhileSpeaking() {
localCallParticipantModel!!.isAudioEnabled = true
localCallParticipantModel!!.isSpeaking = true
localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver)
localCallParticipantModel!!.isAudioEnabled = false
localCallParticipantModel!!.isAudioEnabled = false
assertFalse(localCallParticipantModel!!.isAudioEnabled)
assertFalse(localCallParticipantModel!!.isSpeaking)
assertTrue(localCallParticipantModel!!.isSpeakingWhileMuted)
Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.times(3))?.onChange()
}
@Test
fun testSetSpeakingWhileAudioEnabled() {
localCallParticipantModel!!.isAudioEnabled = true
localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver)
localCallParticipantModel!!.isSpeaking = true
assertTrue(localCallParticipantModel!!.isAudioEnabled)
assertTrue(localCallParticipantModel!!.isSpeaking)
assertFalse(localCallParticipantModel!!.isSpeakingWhileMuted)
Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.only())?.onChange()
}
@Test
fun testSetNotSpeakingWhileAudioEnabled() {
localCallParticipantModel!!.isAudioEnabled = true
localCallParticipantModel!!.isSpeaking = true
localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver)
localCallParticipantModel!!.isSpeaking = false
assertTrue(localCallParticipantModel!!.isAudioEnabled)
assertFalse(localCallParticipantModel!!.isSpeaking)
assertFalse(localCallParticipantModel!!.isSpeakingWhileMuted)
Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.only())?.onChange()
}
@Test
fun testSetSpeakingWhileAudioDisabled() {
localCallParticipantModel!!.isAudioEnabled = false
localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver)
localCallParticipantModel!!.isSpeaking = true
assertFalse(localCallParticipantModel!!.isAudioEnabled)
assertFalse(localCallParticipantModel!!.isSpeaking)
assertTrue(localCallParticipantModel!!.isSpeakingWhileMuted)
Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.only())?.onChange()
}
@Test
fun testSetNotSpeakingWhileAudioDisabled() {
localCallParticipantModel!!.isAudioEnabled = false
localCallParticipantModel!!.isSpeaking = true
localCallParticipantModel!!.addObserver(mockedLocalCallParticipantModelObserver)
localCallParticipantModel!!.isSpeaking = false
assertFalse(localCallParticipantModel!!.isAudioEnabled)
assertFalse(localCallParticipantModel!!.isSpeaking)
assertFalse(localCallParticipantModel!!.isSpeakingWhileMuted)
Mockito.verify(mockedLocalCallParticipantModelObserver, Mockito.only())?.onChange()
}
}