diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreen.kt index d5b4b680b6..9a7284e31e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreen.kt @@ -125,7 +125,8 @@ fun CallScreen( callRecipient = callRecipient, isVideoCall = isRemoteVideoOffer, callStatus = callScreenState.callStatus, - callScreenControlsListener = callScreenControlsListener + callScreenControlsListener = callScreenControlsListener, + localParticipant = localParticipant ) return diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/IncomingCallScreen.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/IncomingCallScreen.kt index 5957fce785..b8d6845577 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/IncomingCallScreen.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/IncomingCallScreen.kt @@ -40,7 +40,9 @@ import org.signal.core.ui.compose.Previews import org.signal.glide.compose.GlideImage import org.thoughtcrime.securesms.R import org.thoughtcrime.securesms.avatar.AvatarImage +import org.thoughtcrime.securesms.events.CallParticipant import org.thoughtcrime.securesms.recipients.Recipient +import org.thoughtcrime.securesms.ringrtc.CameraState private val textShadow = Shadow( color = Color(0f, 0f, 0f, 0.25f), @@ -52,7 +54,8 @@ fun IncomingCallScreen( callRecipient: Recipient, callStatus: String?, isVideoCall: Boolean, - callScreenControlsListener: CallScreenControlsListener + callScreenControlsListener: CallScreenControlsListener, + localParticipant: CallParticipant = CallParticipant.EMPTY ) { val isLandscape = LocalConfiguration.current.orientation == Configuration.ORIENTATION_LANDSCAPE val callTypePadding = remember(isLandscape) { @@ -62,24 +65,37 @@ fun IncomingCallScreen( PaddingValues(top = 22.dp, bottom = 30.dp) } } + val showLocalVideo = localParticipant.isVideoEnabled Scaffold { contentPadding -> - GlideImage( - model = callRecipient.contactPhoto, - modifier = Modifier - .fillMaxSize() - .blur( - radiusX = 25.dp, - radiusY = 25.dp, - edgeTreatment = BlurredEdgeTreatment.Rectangle - ) - ) + if (showLocalVideo) { + RemoteParticipantContent( + participant = localParticipant, + renderInPip = false, + raiseHandAllowed = false, + mirrorVideo = localParticipant.cameraDirection == CameraState.Direction.FRONT, + showAudioIndicator = false, + onInfoMoreInfoClick = null, + modifier = Modifier.fillMaxSize() + ) + } else { + GlideImage( + model = callRecipient.contactPhoto, + modifier = Modifier + .fillMaxSize() + .blur( + radiusX = 25.dp, + radiusY = 25.dp, + edgeTreatment = BlurredEdgeTreatment.Rectangle + ) + ) + } Box( modifier = Modifier .fillMaxSize() - .background(color = Color.Black.copy(alpha = 0.4f)) + .background(color = Color.Black.copy(alpha = if (showLocalVideo) 0.2f else 0.4f)) ) {} CallScreenTopAppBar( diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/WebRtcCallActivity.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/WebRtcCallActivity.kt index 9d71213e0a..1453918c97 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/WebRtcCallActivity.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/WebRtcCallActivity.kt @@ -7,6 +7,7 @@ package org.thoughtcrime.securesms.components.webrtc.v2 import android.Manifest import android.annotation.SuppressLint +import android.app.KeyguardManager import android.app.PictureInPictureParams import android.content.Context import android.content.Intent @@ -249,6 +250,8 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re if (SignalStore.rateLimit.needsRecaptcha()) { RecaptchaProofBottomSheetFragment.show(supportFragmentManager) } + + updateIncomingRingingVanity() } override fun onNewIntent(intent: Intent) { @@ -263,6 +266,8 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re Log.i(TAG, "onPause") super.onPause() + disableIncomingRingingVanity() + if (!isInPipMode() || isFinishing) { EventBus.getDefault().unregister(this) } @@ -403,6 +408,9 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re recreate() return } + if (previousCallState != WebRtcViewModel.State.CALL_INCOMING) { + updateIncomingRingingVanity() + } } WebRtcViewModel.State.CALL_OUTGOING -> handleOutgoingCall(event) @@ -1051,6 +1059,24 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re } } + private fun updateIncomingRingingVanity() { + val event = previousEvent ?: return + if (event.state != WebRtcViewModel.State.CALL_INCOMING) return + + val keyguardManager = getSystemService(KeyguardManager::class.java) + val shouldEnable = keyguardManager == null || !keyguardManager.isKeyguardLocked + + Log.i(TAG, "updateIncomingRingingVanity(): shouldEnable=$shouldEnable, keyguardLocked=${keyguardManager?.isKeyguardLocked}") + AppDependencies.signalCallManager.setIncomingRingingVanity(shouldEnable) + } + + private fun disableIncomingRingingVanity() { + val event = previousEvent ?: return + if (event.state == WebRtcViewModel.State.CALL_INCOMING) { + AppDependencies.signalCallManager.setIncomingRingingVanity(false) + } + } + private fun initializeScreenshotSecurity() { if (TextSecurePreferences.isScreenSecurityEnabled(this)) { window.addFlags(WindowManager.LayoutParams.FLAG_SECURE) diff --git a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/Camera.java b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/Camera.java index 278d21d2e5..26dc436f31 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/Camera.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/Camera.java @@ -22,6 +22,7 @@ import org.webrtc.CameraVideoCapturer; import org.webrtc.CapturerObserver; import org.webrtc.SurfaceTextureHelper; import org.webrtc.VideoFrame; +import org.webrtc.VideoSink; import java.util.LinkedList; import java.util.List; @@ -47,6 +48,7 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa private boolean enabled; private boolean isInitialized; private int orientation; + @Nullable private volatile VideoSink vanitySink; public Camera(@NonNull Context context, @NonNull CameraEventListener cameraEventListener, @@ -139,6 +141,15 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa this.cameraEventListener = cameraEventListener; } + /** + * Set a vanity sink that receives camera frames directly from the capturer, + * bypassing the WebRTC VideoTrack pipeline. This allows local preview to work + * even when the video track is disabled (e.g., during incoming call ringing). + */ + public void setVanitySink(@Nullable VideoSink vanitySink) { + this.vanitySink = vanitySink; + } + public void dispose() { if (capturer != null) { capturer.dispose(); @@ -327,6 +338,10 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa @Override public void onFrameCaptured(VideoFrame videoFrame) { observer.onFrameCaptured(videoFrame); + VideoSink sink = vanitySink; + if (sink != null) { + sink.onFrame(videoFrame); + } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingCallActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingCallActionProcessor.java index 005e7bf2dd..fd6e811ace 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingCallActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingCallActionProcessor.java @@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.ringrtc.CallState; +import org.thoughtcrime.securesms.ringrtc.Camera; import org.thoughtcrime.securesms.ringrtc.RemotePeer; import org.thoughtcrime.securesms.service.webrtc.state.CallSetupState; import org.thoughtcrime.securesms.service.webrtc.state.VideoState; @@ -133,6 +134,17 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor { Log.i(TAG, "handleAcceptCall(): call_id: " + activePeer.getCallId()); + Camera camera = currentState.getVideoState().requireCamera(); + camera.setVanitySink(null); + + if (!answerWithVideo && currentState.getLocalDeviceState().getCameraState().isEnabled()) { + camera.setEnabled(false); + currentState = currentState.builder() + .changeLocalDeviceState() + .cameraState(camera.getCameraState()) + .build(); + } + currentState = currentState.builder() .changeCallSetupState(activePeer.getCallId()) .acceptWithVideo(answerWithVideo) @@ -157,6 +169,11 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor { Log.i(TAG, "handleDenyCall():"); + Camera camera = currentState.getVideoState().getCamera(); + if (camera != null) { + camera.setVanitySink(null); + } + webRtcInteractor.sendNotAcceptedCallEventSyncMessage(activePeer, false, currentState.getCallSetupState(activePeer).isRemoteVideoOffer()); @@ -170,6 +187,43 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor { } } + @Override + protected @NonNull WebRtcServiceState handleSetIncomingRingingVanity(@NonNull WebRtcServiceState currentState, boolean enabled) { + RemotePeer activePeer = currentState.getCallInfoState().requireActivePeer(); + boolean isVideoOffer = currentState.getCallSetupState(activePeer).isRemoteVideoOffer(); + + if (!isVideoOffer) { + return currentState; + } + + boolean cameraAlreadyEnabled = currentState.getLocalDeviceState().getCameraState().isEnabled(); + + if (enabled && cameraAlreadyEnabled) { + return currentState; + } + + if (!enabled && !cameraAlreadyEnabled) { + return currentState; + } + + Camera camera = currentState.getVideoState().requireCamera(); + + if (enabled) { + Log.i(TAG, "handleSetIncomingRingingVanity(): enabling vanity camera"); + camera.setVanitySink(currentState.getVideoState().requireLocalSink()); + camera.setEnabled(true); + } else { + Log.i(TAG, "handleSetIncomingRingingVanity(): disabling vanity camera"); + camera.setVanitySink(null); + camera.setEnabled(false); + } + + return currentState.builder() + .changeLocalDeviceState() + .cameraState(camera.getCameraState()) + .build(); + } + protected @NonNull WebRtcServiceState handleLocalRinging(@NonNull WebRtcServiceState currentState, @NonNull RemotePeer remotePeer) { Log.i(TAG, "handleLocalRinging(): call_id: " + remotePeer.getCallId()); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingGroupCallActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingGroupCallActionProcessor.java index 96de70adfe..3290caefc8 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingGroupCallActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/IncomingGroupCallActionProcessor.java @@ -22,6 +22,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.notifications.DoNotDisturbUtil; import org.thoughtcrime.securesms.notifications.NotificationChannels; import org.thoughtcrime.securesms.recipients.Recipient; +import org.thoughtcrime.securesms.ringrtc.Camera; import org.thoughtcrime.securesms.ringrtc.RemotePeer; import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState; import org.thoughtcrime.securesms.util.AppForegroundObserver; @@ -179,8 +180,41 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro .build(); } + @Override + protected @NonNull WebRtcServiceState handleSetIncomingRingingVanity(@NonNull WebRtcServiceState currentState, boolean enabled) { + boolean cameraAlreadyEnabled = currentState.getLocalDeviceState().getCameraState().isEnabled(); + + if (enabled && cameraAlreadyEnabled) { + return currentState; + } + + if (!enabled && !cameraAlreadyEnabled) { + return currentState; + } + + Camera camera = currentState.getVideoState().requireCamera(); + + if (enabled && !camera.isInitialized()) { + Log.i(TAG, "handleSetIncomingRingingVanity(): initializing vanity camera"); + return WebRtcVideoUtil.initializeVanityCamera(currentState); + } else if (enabled) { + Log.i(TAG, "handleSetIncomingRingingVanity(): enabling vanity camera"); + camera.setEnabled(true); + } else { + Log.i(TAG, "handleSetIncomingRingingVanity(): disabling vanity camera"); + camera.setEnabled(false); + } + + return currentState.builder() + .changeLocalDeviceState() + .cameraState(camera.getCameraState()) + .build(); + } + @Override protected @NonNull WebRtcServiceState handleAcceptCall(@NonNull WebRtcServiceState currentState, boolean answerWithVideo) { + currentState = WebRtcVideoUtil.reinitializeCamera(context, webRtcInteractor.getCameraEventListener(), currentState); + byte[] groupId = currentState.getCallInfoState().getCallRecipient().requireGroupId().getDecodedId(); GroupCall groupCall = webRtcInteractor.getCallManager().createGroupCall(groupId, SignalStore.internal().getGroupCallingServer(), @@ -220,7 +254,7 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro try { groupCall.setOutgoingVideoSource(currentState.getVideoState().requireLocalSink(), currentState.getVideoState().requireCamera()); - groupCall.setOutgoingVideoMuted(answerWithVideo); + groupCall.setOutgoingVideoMuted(!answerWithVideo); groupCall.setOutgoingAudioMuted(!currentState.getLocalDeviceState().isMicrophoneEnabled()); groupCall.setDataMode(NetworkUtil.getCallingDataMode(context, groupCall.getLocalDeviceState().getNetworkRoute().getLocalAdapterType())); @@ -229,6 +263,15 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro return groupCallFailure(currentState, "Unable to join group call", e); } + if (answerWithVideo) { + Camera camera = currentState.getVideoState().requireCamera(); + camera.setEnabled(true); + currentState = currentState.builder() + .changeLocalDeviceState() + .cameraState(camera.getCameraState()) + .build(); + } + return currentState.builder() .actionProcessor(MultiPeerActionProcessorFactory.GroupActionProcessorFactory.INSTANCE.createJoiningActionProcessor(webRtcInteractor)) .changeCallInfoState() diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java index 45d02259d1..c4d97b3884 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/SignalCallManager.java @@ -268,6 +268,10 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall. process((s, p) -> p.handleSetEnableVideo(s, enabled)); } + public void setIncomingRingingVanity(boolean enabled) { + process((s, p) -> p.handleSetIncomingRingingVanity(s, enabled)); + } + public void flipCamera() { process((s, p) -> p.handleSetCameraFlip(s)); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java index 35c64660ee..6c4cc49b41 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcActionProcessor.java @@ -586,6 +586,10 @@ public abstract class WebRtcActionProcessor { return currentState; } + protected @NonNull WebRtcServiceState handleSetIncomingRingingVanity(@NonNull WebRtcServiceState currentState, boolean enabled) { + Log.i(tag, "handleSetIncomingRingingVanity not processed"); + return currentState; + } protected @NonNull WebRtcServiceState handleSelfRaiseHand(@NonNull WebRtcServiceState currentState, boolean raised) { Log.i(tag, "raiseHand not processed");