mirror of
https://github.com/nextcloud/talk-android
synced 2025-03-07 06:39:45 +00:00
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:
parent
36a29ed36e
commit
cb52fb349f
@ -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) {
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user