kopia lustrzana https://github.com/ryukoposting/Signal-Android
Display audio levels for each participant in group calls.
rodzic
a9f208153c
commit
ec92d5ddb7
|
@ -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);
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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())) {
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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():");
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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());
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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(),
|
||||
)
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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" />
|
|
@ -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"
|
||||
|
|
Ładowanie…
Reference in New Issue