Display audio levels for each participant in group calls.

fork-5.53.8
Rashad Sookram 2022-04-01 17:09:56 -04:00 zatwierdzone przez Cody Henthorne
rodzic a9f208153c
commit ec92d5ddb7
19 zmienionych plików z 379 dodań i 32 usunięć

Wyświetl plik

@ -86,6 +86,9 @@ import java.util.List;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
import io.reactivex.rxjava3.disposables.Disposable;
import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE;
public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChangeDialog.Callback {
@ -113,6 +116,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
private WindowLayoutInfoConsumer windowLayoutInfoConsumer;
private ThrottledDebouncer requestNewSizesThrottle;
private Disposable ephemeralStateDisposable = Disposable.empty();
@Override
protected void attachBaseContext(@NonNull Context newBase) {
getDelegate().setLocalNightMode(AppCompatDelegate.MODE_NIGHT_YES);
@ -155,6 +160,18 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
requestNewSizesThrottle = new ThrottledDebouncer(TimeUnit.SECONDS.toMillis(1));
}
@Override
protected void onStart() {
super.onStart();
ephemeralStateDisposable = ApplicationDependencies.getSignalCallManager()
.ephemeralStates()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(state -> {
viewModel.updateFromEphemeralState(state);
});
}
@Override
public void onResume() {
Log.i(TAG, "onResume()");
@ -195,6 +212,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
Log.i(TAG, "onStop");
super.onStop();
ephemeralStateDisposable.dispose();
if (!isInPipMode() || isFinishing()) {
EventBus.getDefault().unregister(this);
requestNewSizesThrottle.clear();
@ -297,7 +316,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan
LiveDataUtil.combineLatest(viewModel.getCallParticipantsState(),
viewModel.getOrientationAndLandscapeEnabled(),
(s, o) -> new CallParticipantsViewState(s, o.first == PORTRAIT_BOTTOM_EDGE, o.second))
viewModel.getEphemeralState(),
(s, o, e) -> new CallParticipantsViewState(s, e, o.first == PORTRAIT_BOTTOM_EDGE, o.second))
.observe(this, p -> callScreen.updateCallParticipants(p));
viewModel.getCallParticipantListUpdate().observe(this, participantUpdateWindow::addCallParticipantListUpdate);
viewModel.getSafetyNumberChangeEvent().observe(this, this::handleSafetyNumberChangeEvent);

Wyświetl plik

@ -0,0 +1,130 @@
package org.thoughtcrime.securesms.components.webrtc
import android.animation.ValueAnimator
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.graphics.RectF
import android.util.AttributeSet
import android.view.View
import android.widget.FrameLayout
import org.signal.core.util.DimensionUnit
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.events.CallParticipant
import org.thoughtcrime.securesms.service.webrtc.WebRtcActionProcessor
import org.thoughtcrime.securesms.util.visible
/**
* An indicator shown for each participant in a call which shows the state of their audio.
*/
class AudioIndicatorView(context: Context, attrs: AttributeSet) : FrameLayout(context, attrs) {
companion object {
private const val SIDE_BAR_SHRINK_FACTOR = 0.75f
}
private val barPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.FILL
color = Color.WHITE
}
private val barRect = RectF()
private val barWidth = DimensionUnit.DP.toPixels(4f)
private val barRadius = DimensionUnit.DP.toPixels(32f)
private val barPadding = DimensionUnit.DP.toPixels(4f)
private var middleBarAnimation: ValueAnimator? = null
private var sideBarAnimation: ValueAnimator? = null
private var showAudioLevel = false
private var lastAudioLevel: CallParticipant.AudioLevel? = null
init {
inflate(context, R.layout.audio_indicator_view, this)
setWillNotDraw(false)
}
private val micMuted: View = findViewById(R.id.mic_muted)
fun bind(microphoneEnabled: Boolean, level: CallParticipant.AudioLevel?) {
micMuted.visible = !microphoneEnabled
val wasShowingAudioLevel = showAudioLevel
showAudioLevel = microphoneEnabled && level != null
if (showAudioLevel) {
val scaleFactor = when (level!!) {
CallParticipant.AudioLevel.LOWEST -> 0.2f
CallParticipant.AudioLevel.LOW -> 0.4f
CallParticipant.AudioLevel.MEDIUM -> 0.6f
CallParticipant.AudioLevel.HIGH -> 0.8f
CallParticipant.AudioLevel.HIGHEST -> 1.0f
}
middleBarAnimation?.end()
middleBarAnimation = createAnimation(middleBarAnimation, height * scaleFactor)
middleBarAnimation?.start()
sideBarAnimation?.end()
var finalHeight = height * scaleFactor
if (level != CallParticipant.AudioLevel.LOWEST) {
finalHeight *= SIDE_BAR_SHRINK_FACTOR
}
sideBarAnimation = createAnimation(sideBarAnimation, finalHeight)
sideBarAnimation?.start()
}
if (showAudioLevel != wasShowingAudioLevel || level != lastAudioLevel) {
invalidate()
}
lastAudioLevel = level
}
private fun createAnimation(current: ValueAnimator?, finalHeight: Float): ValueAnimator {
val currentHeight = current?.animatedValue as? Float ?: 0f
return ValueAnimator.ofFloat(currentHeight, finalHeight).apply {
duration = WebRtcActionProcessor.AUDIO_LEVELS_INTERVAL.toLong()
}
}
override fun onDraw(canvas: Canvas) {
super.onDraw(canvas)
val middleBarHeight = middleBarAnimation?.animatedValue as? Float
val sideBarHeight = sideBarAnimation?.animatedValue as? Float
if (showAudioLevel && middleBarHeight != null && sideBarHeight != null) {
val audioLevelWidth = 3 * barWidth + 2 * barPadding
val xOffsetBase = (width - audioLevelWidth) / 2
canvas.drawBar(
xOffset = xOffsetBase,
size = sideBarHeight
)
canvas.drawBar(
xOffset = barPadding + barWidth + xOffsetBase,
size = middleBarHeight
)
canvas.drawBar(
xOffset = 2 * (barPadding + barWidth) + xOffsetBase,
size = sideBarHeight
)
if (middleBarAnimation?.isRunning == true || sideBarAnimation?.isRunning == true) {
invalidate()
}
}
}
private fun Canvas.drawBar(xOffset: Float, size: Float) {
val yOffset = (height - size) / 2
barRect.set(xOffset, yOffset, xOffset + barWidth, height - yOffset)
drawRoundRect(barRect, barRadius, barRadius, barPaint)
}
}

Wyświetl plik

@ -62,7 +62,7 @@ public class CallParticipantView extends ConstraintLayout {
private ImageView pipAvatar;
private BadgeImageView pipBadge;
private ContactPhoto contactPhoto;
private View audioMuted;
private AudioIndicatorView audioIndicator;
private View infoOverlay;
private EmojiTextView infoMessage;
private Button infoMoreInfo;
@ -90,7 +90,7 @@ public class CallParticipantView extends ConstraintLayout {
pipAvatar = findViewById(R.id.call_participant_item_pip_avatar);
rendererFrame = findViewById(R.id.call_participant_renderer_frame);
renderer = findViewById(R.id.call_participant_renderer);
audioMuted = findViewById(R.id.call_participant_mic_muted);
audioIndicator = findViewById(R.id.call_participant_audio_indicator);
infoOverlay = findViewById(R.id.call_participant_info_overlay);
infoIcon = findViewById(R.id.call_participant_info_icon);
infoMessage = findViewById(R.id.call_participant_info_message);
@ -123,7 +123,7 @@ public class CallParticipantView extends ConstraintLayout {
rendererFrame.setVisibility(View.GONE);
renderer.setVisibility(View.GONE);
renderer.attachBroadcastVideoSink(null);
audioMuted.setVisibility(View.GONE);
audioIndicator.setVisibility(View.GONE);
avatar.setVisibility(View.GONE);
badge.setVisibility(View.GONE);
pipAvatar.setVisibility(View.GONE);
@ -159,7 +159,8 @@ public class CallParticipantView extends ConstraintLayout {
renderer.attachBroadcastVideoSink(null);
}
audioMuted.setVisibility(participant.isMicrophoneEnabled() ? View.GONE : View.VISIBLE);
audioIndicator.setVisibility(View.VISIBLE);
audioIndicator.bind(participant.isMicrophoneEnabled(), participant.getAudioLevel());
}
if (participantChanged || !Objects.equals(contactPhoto, participant.getRecipient().getContactPhoto())) {

Wyświetl plik

@ -13,6 +13,7 @@ import org.thoughtcrime.securesms.groups.ui.GroupMemberEntry
import org.thoughtcrime.securesms.recipients.Recipient
import org.thoughtcrime.securesms.ringrtc.CameraState
import org.thoughtcrime.securesms.service.webrtc.collections.ParticipantCollection
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcEphemeralState
import java.util.concurrent.TimeUnit
/**
@ -260,6 +261,15 @@ data class CallParticipantsState(
return oldState.copy(groupMembers = groupMembers)
}
@JvmStatic
fun update(oldState: CallParticipantsState, ephemeralState: WebRtcEphemeralState): CallParticipantsState {
return oldState.copy(
remoteParticipants = oldState.remoteParticipants.map { p -> p.copy(audioLevel = ephemeralState.remoteAudioLevels[p.callParticipantId]) },
localParticipant = oldState.localParticipant.copy(audioLevel = ephemeralState.localAudioLevel),
focusedParticipant = oldState.focusedParticipant.copy(audioLevel = ephemeralState.remoteAudioLevels[oldState.focusedParticipant.callParticipantId])
)
}
private fun determineLocalRenderMode(
oldState: CallParticipantsState,
localParticipant: CallParticipant = oldState.localParticipant,

Wyświetl plik

@ -292,7 +292,7 @@ public class WebRtcCallView extends ConstraintLayout {
rotatableControls.add(videoToggle);
rotatableControls.add(cameraDirectionToggle);
rotatableControls.add(decline);
rotatableControls.add(smallLocalRender.findViewById(R.id.call_participant_mic_muted));
rotatableControls.add(smallLocalRender.findViewById(R.id.call_participant_audio_indicator));
rotatableControls.add(ringToggle);
largeHeaderConstraints = new ConstraintSet();

Wyświetl plik

@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.recipients.LiveRecipient;
import org.thoughtcrime.securesms.recipients.Recipient;
import org.thoughtcrime.securesms.recipients.RecipientId;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcEphemeralState;
import org.thoughtcrime.securesms.util.DefaultValueLiveData;
import org.thoughtcrime.securesms.util.SingleLiveEvent;
import org.thoughtcrime.securesms.util.Util;
@ -66,6 +67,7 @@ public class WebRtcCallViewModel extends ViewModel {
private final MutableLiveData<Boolean> isLandscapeEnabled = new MutableLiveData<>();
private final LiveData<Integer> controlsRotation;
private final Observer<List<GroupMemberEntry.FullMember>> groupMemberStateUpdater = m -> participantsState.setValue(CallParticipantsState.update(participantsState.getValue(), m));
private final MutableLiveData<WebRtcEphemeralState> ephemeralState = new MutableLiveData<>();
private final Handler elapsedTimeHandler = new Handler(Looper.getMainLooper());
private final Runnable elapsedTimeRunnable = this::handleTick;
@ -159,6 +161,10 @@ public class WebRtcCallViewModel extends ViewModel {
return shouldShowSpeakerHint;
}
public LiveData<WebRtcEphemeralState> getEphemeralState() {
return ephemeralState;
}
public boolean canEnterPipMode() {
return canEnterPipMode;
}
@ -288,6 +294,11 @@ public class WebRtcCallViewModel extends ViewModel {
}
}
@MainThread
public void updateFromEphemeralState(@NonNull WebRtcEphemeralState state) {
ephemeralState.setValue(state);
}
private int resolveRotation(boolean isLandscapeEnabled, @NonNull Orientation orientation) {
if (isLandscapeEnabled) {
return 0;

Wyświetl plik

@ -16,6 +16,7 @@ data class CallParticipant constructor(
val isVideoEnabled: Boolean = false,
val isMicrophoneEnabled: Boolean = false,
val lastSpoke: Long = 0,
val audioLevel: AudioLevel? = null,
val isMediaKeysReceived: Boolean = true,
val addedToCallTime: Long = 0,
val isScreenSharing: Boolean = false,
@ -73,6 +74,33 @@ data class CallParticipant constructor(
PRIMARY, SECONDARY
}
enum class AudioLevel {
LOWEST,
LOW,
MEDIUM,
HIGH,
HIGHEST;
companion object {
/**
* Converts a raw audio level from RingRTC (value in [0, 32767]) to a level suitable for
* display in the UI.
*/
@JvmStatic
fun fromRawAudioLevel(raw: Int?): AudioLevel? {
return when {
raw == null -> null
raw < 500 -> LOWEST
raw < 2000 -> LOW
raw < 8000 -> MEDIUM
raw < 20000 -> HIGH
else -> HIGHEST
}
}
}
}
companion object {
@JvmField
val EMPTY: CallParticipant = CallParticipant()

Wyświetl plik

@ -1,6 +1,7 @@
package org.thoughtcrime.securesms.service.webrtc;
import android.os.ResultReceiver;
import android.util.LongSparseArray;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -11,13 +12,17 @@ import org.signal.core.util.logging.Log;
import org.signal.ringrtc.CallException;
import org.signal.ringrtc.GroupCall;
import org.signal.ringrtc.PeekInfo;
import org.thoughtcrime.securesms.events.CallParticipant;
import org.thoughtcrime.securesms.events.CallParticipantId;
import org.thoughtcrime.securesms.events.WebRtcViewModel;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.ringrtc.Camera;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcEphemeralState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.UUID;
@ -104,6 +109,31 @@ public class GroupConnectedActionProcessor extends GroupActionProcessor {
.build();
}
@Override
protected @NonNull WebRtcEphemeralState handleGroupAudioLevelsChanged(@NonNull WebRtcServiceState currentState, @NonNull WebRtcEphemeralState ephemeralState) {
GroupCall groupCall = currentState.getCallInfoState().requireGroupCall();
LongSparseArray<GroupCall.RemoteDeviceState> remoteDeviceStates = groupCall.getRemoteDeviceStates();
CallParticipant.AudioLevel localAudioLevel = CallParticipant.AudioLevel.fromRawAudioLevel(groupCall.getLocalDeviceState().getAudioLevel());
HashMap<CallParticipantId, CallParticipant.AudioLevel> remoteAudioLevels = new HashMap<>();
for (CallParticipant participant : currentState.getCallInfoState().getRemoteCallParticipants()) {
CallParticipantId callParticipantId = participant.getCallParticipantId();
Integer audioLevel = null;
if (remoteDeviceStates != null) {
GroupCall.RemoteDeviceState state = remoteDeviceStates.get(callParticipantId.getDemuxId());
if (state != null) {
audioLevel = state.getAudioLevel();
}
}
remoteAudioLevels.put(callParticipantId, CallParticipant.AudioLevel.fromRawAudioLevel(audioLevel));
}
return ephemeralState.copy(localAudioLevel, remoteAudioLevels);
}
@Override
protected @NonNull WebRtcServiceState handleGroupJoinedMembershipChanged(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleGroupJoinedMembershipChanged():");

Wyświetl plik

@ -45,7 +45,7 @@ public class GroupPreJoinActionProcessor extends GroupActionProcessor {
GroupCall groupCall = webRtcInteractor.getCallManager().createGroupCall(groupId,
SignalStore.internalValues().groupCallingServer(),
new byte[0],
null,
AUDIO_LEVELS_INTERVAL,
AudioProcessingMethodSelector.get(),
webRtcInteractor.getGroupCallObserver());

Wyświetl plik

@ -170,7 +170,7 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro
GroupCall groupCall = webRtcInteractor.getCallManager().createGroupCall(groupId,
SignalStore.internalValues().groupCallingServer(),
new byte[0],
null,
AUDIO_LEVELS_INTERVAL,
AudioProcessingMethodSelector.get(),
webRtcInteractor.getGroupCallObserver());

Wyświetl plik

@ -6,6 +6,7 @@ import android.content.Intent;
import android.os.Build;
import android.os.ResultReceiver;
import androidx.annotation.AnyThread;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -45,6 +46,7 @@ import org.thoughtcrime.securesms.recipients.RecipientUtil;
import org.thoughtcrime.securesms.ringrtc.CameraEventListener;
import org.thoughtcrime.securesms.ringrtc.CameraState;
import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcEphemeralState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.util.AppForegroundObserver;
import org.thoughtcrime.securesms.util.BubbleUtil;
@ -52,6 +54,7 @@ import org.thoughtcrime.securesms.util.NetworkUtil;
import org.thoughtcrime.securesms.util.RecipientAccessList;
import org.thoughtcrime.securesms.util.TextSecurePreferences;
import org.thoughtcrime.securesms.util.Util;
import org.thoughtcrime.securesms.util.rx.RxStore;
import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager;
import org.thoughtcrime.securesms.webrtc.locks.LockManager;
import org.webrtc.PeerConnection;
@ -80,6 +83,10 @@ import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.Collectors;
import io.reactivex.rxjava3.core.Flowable;
import io.reactivex.rxjava3.schedulers.Schedulers;
import kotlin.jvm.functions.Function1;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.GroupCallState.IDLE;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.CALL_INCOMING;
import static org.thoughtcrime.securesms.events.WebRtcViewModel.State.NETWORK_FAILURE;
@ -106,16 +113,18 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
private final Executor networkExecutor;
private final LockManager lockManager;
private WebRtcServiceState serviceState;
private boolean needsToSetSelfUuid = true;
private WebRtcServiceState serviceState;
private RxStore<WebRtcEphemeralState> ephemeralStateStore;
private boolean needsToSetSelfUuid = true;
public SignalCallManager(@NonNull Application application) {
this.context = application.getApplicationContext();
this.messageSender = ApplicationDependencies.getSignalServiceMessageSender();
this.accountManager = ApplicationDependencies.getSignalServiceAccountManager();
this.lockManager = new LockManager(this.context);
this.serviceExecutor = Executors.newSingleThreadExecutor();
this.networkExecutor = Executors.newSingleThreadExecutor();
this.context = application.getApplicationContext();
this.messageSender = ApplicationDependencies.getSignalServiceMessageSender();
this.accountManager = ApplicationDependencies.getSignalServiceAccountManager();
this.lockManager = new LockManager(this.context);
this.serviceExecutor = Executors.newSingleThreadExecutor();
this.networkExecutor = Executors.newSingleThreadExecutor();
this.ephemeralStateStore = new RxStore<>(new WebRtcEphemeralState(), Schedulers.from(serviceExecutor));
CallManager callManager = null;
try {
@ -133,6 +142,10 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
this)));
}
public @NonNull Flowable<WebRtcEphemeralState> ephemeralStates() {
return ephemeralStateStore.getStateFlowable();
}
@NonNull CallManager getRingRtcCallManager() {
//noinspection ConstantConditions
return callManager;
@ -173,6 +186,16 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
});
}
/**
* Processes the given update to {@link WebRtcEphemeralState}.
*
* @param transformer The transformation to apply to the state. Runs on the {@link #serviceExecutor}.
*/
@AnyThread
private void processStateless(@NonNull Function1<WebRtcEphemeralState, WebRtcEphemeralState> transformer) {
ephemeralStateStore.update(transformer);
}
public void startPreJoinCall(@NonNull Recipient recipient) {
process((s, p) -> p.handlePreJoinCall(s, new RemotePeer(recipient.getId())));
}
@ -766,7 +789,7 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall.
@Override
public void onAudioLevels(@NonNull GroupCall groupCall) {
// TODO: Implement audio level handling for group calls.
processStateless(s -> serviceState.getActionProcessor().handleGroupAudioLevelsChanged(serviceState, s));
}
@Override

Wyświetl plik

@ -38,6 +38,7 @@ import org.thoughtcrime.securesms.ringrtc.RemotePeer;
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.CallMetadata;
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.OfferMetadata;
import org.thoughtcrime.securesms.service.webrtc.WebRtcData.ReceivedOfferMetadata;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcEphemeralState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState;
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceStateBuilder;
import org.thoughtcrime.securesms.util.NetworkUtil;
@ -77,6 +78,8 @@ import static org.thoughtcrime.securesms.service.webrtc.WebRtcData.ReceivedAnswe
*/
public abstract class WebRtcActionProcessor {
public static final int AUDIO_LEVELS_INTERVAL = 200;
protected final Context context;
protected final WebRtcInteractor webRtcInteractor;
protected final String tag;
@ -680,6 +683,10 @@ public abstract class WebRtcActionProcessor {
return currentState;
}
protected @NonNull WebRtcEphemeralState handleGroupAudioLevelsChanged(@NonNull WebRtcServiceState currentState, @NonNull WebRtcEphemeralState ephemeralState) {
return ephemeralState;
}
protected @NonNull WebRtcServiceState handleGroupJoinedMembershipChanged(@NonNull WebRtcServiceState currentState) {
Log.i(tag, "handleGroupJoinedMembershipChanged not processed");
return currentState;

Wyświetl plik

@ -13,6 +13,8 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
* Represents the participants to be displayed in the grid at any given time.
@ -90,6 +92,10 @@ public class ParticipantCollection {
return participants;
}
public @NonNull ParticipantCollection map(@NonNull Function<CallParticipant, CallParticipant> mapper) {
return new ParticipantCollection(maxGridCellCount, participants.stream().map(mapper).collect(Collectors.toList()));
}
public int size() {
return participants.size();
}

Wyświetl plik

@ -0,0 +1,12 @@
package org.thoughtcrime.securesms.service.webrtc.state
import org.thoughtcrime.securesms.events.CallParticipant
import org.thoughtcrime.securesms.events.CallParticipantId
/**
* The state of the call system which contains data which changes frequently.
*/
data class WebRtcEphemeralState(
val localAudioLevel: CallParticipant.AudioLevel? = null,
val remoteAudioLevels: Map<CallParticipantId, CallParticipant.AudioLevel> = emptyMap(),
)

Wyświetl plik

@ -14,6 +14,7 @@ import com.annimon.stream.function.Predicate;
import org.signal.core.util.concurrent.SignalExecutors;
import org.thoughtcrime.securesms.util.concurrent.SerialMonoLifoExecutor;
import org.whispersystems.signalservice.api.util.Preconditions;
import java.util.LinkedHashSet;
import java.util.List;
@ -98,6 +99,20 @@ public final class LiveDataUtil {
return new CombineLiveData<>(a, b, combine);
}
/**
* Once there is non-null data on each input {@link LiveData}, the {@link Combine3} function is
* run and produces a live data of the combined data.
* <p>
* As each live data changes, the combine function is re-run, and a new value is emitted always
* with the latest, non-null values.
*/
public static <A, B, C, R> LiveData<R> combineLatest(@NonNull LiveData<A> a,
@NonNull LiveData<B> b,
@NonNull LiveData<C> c,
@NonNull Combine3<A, B, C, R> combine) {
return new Combine3LiveData<>(a, b, c, combine);
}
/**
* Merges the supplied live data streams.
*/
@ -285,4 +300,41 @@ public final class LiveDataUtil {
}
}
}
private static final class Combine3LiveData<A, B, C, R> extends MediatorLiveData<R> {
private A a;
private B b;
private C c;
Combine3LiveData(LiveData<A> liveDataA, LiveData<B> liveDataB, LiveData<C> liveDataC, Combine3<A, B, C, R> combine) {
Preconditions.checkArgument(liveDataA != liveDataB && liveDataB != liveDataC && liveDataA != liveDataC);
addSource(liveDataA, (a) -> {
if (a != null) {
this.a = a;
if (b != null && c != null) {
setValue(combine.apply(a, b, c));
}
}
});
addSource(liveDataB, (b) -> {
if (b != null) {
this.b = b;
if (a != null && c != null) {
setValue(combine.apply(a, b, c));
}
}
});
addSource(liveDataC, (c) -> {
if (c != null) {
this.c = c;
if (a != null && b != null) {
setValue(combine.apply(a, b, c));
}
}
});
}
}
}

Wyświetl plik

@ -1,15 +1,19 @@
package org.thoughtcrime.securesms.util.rx
import io.reactivex.rxjava3.core.Flowable
import io.reactivex.rxjava3.core.Scheduler
import io.reactivex.rxjava3.processors.BehaviorProcessor
import io.reactivex.rxjava3.schedulers.Schedulers
import io.reactivex.rxjava3.subjects.PublishSubject
/**
* Rx replacement for Store.
* Actions are run on the computation thread.
* Actions are run on the computation thread by default.
*/
class RxStore<T : Any>(defaultValue: T) {
class RxStore<T : Any>(
defaultValue: T,
private val scheduler: Scheduler = Schedulers.computation()
) {
private val behaviorProcessor = BehaviorProcessor.createDefault(defaultValue)
private val actionSubject = PublishSubject.create<(T) -> T>().toSerialized()
@ -19,7 +23,7 @@ class RxStore<T : Any>(defaultValue: T) {
init {
actionSubject
.observeOn(Schedulers.computation())
.observeOn(scheduler)
.scan(defaultValue) { v, f -> f(v) }
.subscribe { behaviorProcessor.onNext(it) }
}

Wyświetl plik

@ -1,9 +1,14 @@
package org.thoughtcrime.securesms.webrtc
import org.thoughtcrime.securesms.components.webrtc.CallParticipantsState
import org.thoughtcrime.securesms.service.webrtc.state.WebRtcEphemeralState
data class CallParticipantsViewState(
val callParticipantsState: CallParticipantsState,
class CallParticipantsViewState(
callParticipantsState: CallParticipantsState,
ephemeralState: WebRtcEphemeralState,
val isPortrait: Boolean,
val isLandscapeEnabled: Boolean
)
) {
val callParticipantsState = CallParticipantsState.update(callParticipantsState, ephemeralState)
}

Wyświetl plik

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.AppCompatImageView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/mic_muted"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
app:srcCompat="@drawable/ic_mic_off_solid_18"
app:tint="@color/core_white" />

Wyświetl plik

@ -83,16 +83,14 @@
</FrameLayout>
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/call_participant_mic_muted"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="12dp"
android:layout_marginBottom="12dp"
<org.thoughtcrime.securesms.components.webrtc.AudioIndicatorView
android:id="@+id/call_participant_audio_indicator"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginStart="9dp"
android:layout_marginBottom="9dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:srcCompat="@drawable/ic_mic_off_solid_18"
app:tint="@color/core_white" />
app:layout_constraintStart_toStartOf="parent" />
<LinearLayout
android:id="@+id/call_participant_info_overlay"