From 9dcf68581d63bb0e7f5965ec9e1c787bf05c4659 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Thu, 14 May 2026 11:17:17 -0400 Subject: [PATCH] Improve screen share capture dimension calculation and use remote config. --- .../settings/app/labs/LabsSettingsEvents.kt | 1 - .../settings/app/labs/LabsSettingsFragment.kt | 9 -- .../settings/app/labs/LabsSettingsState.kt | 3 +- .../app/labs/LabsSettingsViewModel.kt | 8 +- .../components/webrtc/WebRtcControls.java | 4 +- .../webrtc/v2/AdditionalActionsPopup.kt | 52 +++++++++--- .../components/webrtc/v2/CallControls.kt | 2 + .../components/webrtc/v2/CallScreen.kt | 5 +- .../securesms/keyvalue/LabsValues.kt | 3 - .../ringrtc/OutgoingVideoSourceRouter.kt | 1 - .../securesms/ringrtc/ScreenShareCapturer.kt | 83 +++++++++++++++---- .../securesms/util/RemoteConfig.kt | 11 +++ app/src/main/res/values/strings.xml | 4 +- 13 files changed, 132 insertions(+), 54 deletions(-) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsEvents.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsEvents.kt index 15141e21cb..76a4d217d6 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsEvents.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsEvents.kt @@ -13,5 +13,4 @@ sealed interface LabsSettingsEvents { data class ToggleBetterSearch(val enabled: Boolean) : LabsSettingsEvents data class ToggleAutoLowerHand(val enabled: Boolean) : LabsSettingsEvents data class ToggleStarredMessages(val enabled: Boolean) : LabsSettingsEvents - data class ToggleScreenShare(val enabled: Boolean) : LabsSettingsEvents } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsFragment.kt index 0cf1642f47..01fb0915fc 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsFragment.kt @@ -151,15 +151,6 @@ private fun LabsSettingsContent( onCheckChanged = { onEvent(LabsSettingsEvents.ToggleStarredMessages(it)) } ) } - - item { - Rows.ToggleRow( - checked = state.screenShare, - text = "Screen Sharing", - label = "Share your screen during calls. Adds a screen share option to the overflow menu in 1:1 and group calls.", - onCheckChanged = { onEvent(LabsSettingsEvents.ToggleScreenShare(it)) } - ) - } } } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsState.kt index 5e8141671c..b5a2fbbcb5 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsState.kt @@ -15,6 +15,5 @@ data class LabsSettingsState( val groupSuggestionsForMembers: Boolean = false, val betterSearch: Boolean = false, val autoLowerHand: Boolean = false, - val starredMessages: Boolean = false, - val screenShare: Boolean = false + val starredMessages: Boolean = false ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsViewModel.kt index 1743b127ac..d931769b0a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/app/labs/LabsSettingsViewModel.kt @@ -46,10 +46,6 @@ class LabsSettingsViewModel : ViewModel() { SignalStore.labs.starredMessages = event.enabled _state.value = _state.value.copy(starredMessages = event.enabled) } - is LabsSettingsEvents.ToggleScreenShare -> { - SignalStore.labs.screenShare = event.enabled - _state.value = _state.value.copy(screenShare = event.enabled) - } } } @@ -61,9 +57,7 @@ class LabsSettingsViewModel : ViewModel() { groupSuggestionsForMembers = SignalStore.labs.groupSuggestionsForMembers, betterSearch = SignalStore.labs.betterSearch, autoLowerHand = SignalStore.labs.autoLowerHand, - - starredMessages = SignalStore.labs.starredMessages, - screenShare = SignalStore.labs.screenShare + starredMessages = SignalStore.labs.starredMessages ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java index aa701d21a2..3a1a0764aa 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcControls.java @@ -167,7 +167,7 @@ public final class WebRtcControls { public boolean displayOverflow() { boolean connectedGroupCall = isGroupCall() && groupCallState == GroupCallState.CONNECTED && hasAtLeastOneRemote; - boolean connected1to1Call = !isGroupCall() && callState == CallState.ONGOING && SignalStore.labs().getScreenShare(); + boolean connected1to1Call = !isGroupCall() && callState == CallState.ONGOING && RemoteConfig.screenSharing(); return isAtLeastOutgoing() && (connectedGroupCall || connected1to1Call); } @@ -284,7 +284,7 @@ public final class WebRtcControls { return callState.isAtLeast(CallState.OUTGOING); } - private boolean isGroupCall() { + public boolean isGroupCall() { return groupCallState != GroupCallState.NONE; } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/AdditionalActionsPopup.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/AdditionalActionsPopup.kt index 218aeab6e4..e22cf1ba0d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/AdditionalActionsPopup.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/AdditionalActionsPopup.kt @@ -48,6 +48,7 @@ data class AdditionalActionsState( val isSelfHandRaised: Boolean = false, val isScreenSharing: Boolean = false, val displayScreenShareToggle: Boolean = false, + val isGroupCall: Boolean = true, @Stable val listener: AdditionalActionsListener = AdditionalActionsListener.Empty ) @@ -85,15 +86,18 @@ private fun AdditionalActionsPopupContent( Column( verticalArrangement = spacedBy(12.dp), modifier = Modifier - .width(IntrinsicSize.Min) + .width(IntrinsicSize.Max) .padding(12.dp) ) { - CallReactionScrubber( - reactions = state.reactions, - listener = state.listener - ) + if (state.isGroupCall) { + CallReactionScrubber( + reactions = state.reactions, + listener = state.listener + ) + } CallScreenMenu( + isGroupCall = state.isGroupCall, onRaiseHandClick = state.listener::onRaiseHandClick, isSelfHandRaised = state.isSelfHandRaised, isScreenSharing = state.isScreenSharing, @@ -141,6 +145,7 @@ private fun CallReactionScrubber( @Composable private fun CallScreenMenu( + isGroupCall: Boolean, isSelfHandRaised: Boolean, onRaiseHandClick: (Boolean) -> Unit, isScreenSharing: Boolean = false, @@ -152,11 +157,13 @@ private fun CallScreenMenu( .fillMaxWidth() .background(SignalTheme.colors.colorSurface2, RoundedCornerShape(18.dp)) ) { - CallScreenMenuOption( - imageVector = ImageVector.vectorResource(R.drawable.symbol_raise_hand_24), - title = if (isSelfHandRaised) stringResource(R.string.CallOverflowPopupWindow__lower_hand) else stringResource(R.string.CallOverflowPopupWindow__raise_hand), - onClick = { onRaiseHandClick(!isSelfHandRaised) } - ) + if (isGroupCall) { + CallScreenMenuOption( + imageVector = ImageVector.vectorResource(R.drawable.symbol_raise_hand_24), + title = if (isSelfHandRaised) stringResource(R.string.CallOverflowPopupWindow__lower_hand) else stringResource(R.string.CallOverflowPopupWindow__raise_hand), + onClick = { onRaiseHandClick(!isSelfHandRaised) } + ) + } if (displayScreenShareToggle) { CallScreenMenuOption( @@ -242,3 +249,28 @@ private fun CallScreenAdditionalActionsPopupPreview() { ) } } + +@NightPreview +@Composable +private fun CallScreenAdditionalActionsScreenSharingPreview() { + Previews.Preview { + AdditionalActionsPopupContent( + state = AdditionalActionsState( + isGroupCall = false, + displayScreenShareToggle = true, + isShown = false, + reactions = persistentListOf( + "\u2764\ufe0f", + "\ud83d\udc4d", + "\ud83d\udc4e", + "\ud83d\ude02", + "\ud83d\ude2e", + "\ud83d\ude22" + ), + isSelfHandRaised = false, + listener = AdditionalActionsListener.Empty, + triggerAlignedPopupState = TriggerAlignedPopupState.rememberTriggerAlignedPopupState() + ) + ) + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallControls.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallControls.kt index 9bb176da68..c12f7d4c70 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallControls.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallControls.kt @@ -211,6 +211,7 @@ data class CallControlsState( val displayGroupRingingToggle: Boolean = false, val isGroupRingingEnabled: Boolean = false, val isGroupRingingAllowed: Boolean = false, + val isGroupCall: Boolean = false, val displayAdditionalActions: Boolean = false, val displayStartCallButton: Boolean = false, val startCallButtonText: Int = R.string.WebRtcCallView__start_call, @@ -252,6 +253,7 @@ data class CallControlsState( displayMicToggle = webRtcControls.displayMuteAudio(), isMicEnabled = callParticipantsState.localParticipant.isMicrophoneEnabled, displayGroupRingingToggle = webRtcControls.displayRingToggle(), + isGroupCall = webRtcControls.isGroupCall, isGroupRingingEnabled = callParticipantsState.ringGroup, isGroupRingingAllowed = groupMemberCount <= RemoteConfig.maxGroupCallRingSize, displayAdditionalActions = webRtcControls.displayOverflow(), 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 154c302ee4..c0f216faa9 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 @@ -77,11 +77,11 @@ import org.thoughtcrime.securesms.events.CallParticipantId import org.thoughtcrime.securesms.events.GroupCallRaiseHandEvent import org.thoughtcrime.securesms.events.GroupCallReactionEvent import org.thoughtcrime.securesms.events.WebRtcViewModel -import org.thoughtcrime.securesms.keyvalue.SignalStore import org.thoughtcrime.securesms.recipients.Recipient import org.thoughtcrime.securesms.recipients.RecipientId import org.thoughtcrime.securesms.ringrtc.CameraState import org.thoughtcrime.securesms.service.webrtc.PendingParticipantCollection +import org.thoughtcrime.securesms.util.RemoteConfig import kotlin.math.max import kotlin.math.round import kotlin.time.Duration.Companion.milliseconds @@ -181,7 +181,8 @@ fun CallScreen( reactions = callScreenState.reactions, isSelfHandRaised = localParticipant.isHandRaised, isScreenSharing = callScreenState.isLocalScreenSharing, - displayScreenShareToggle = callControlsState.displayEndCallButton && SignalStore.labs.screenShare, + displayScreenShareToggle = callControlsState.displayEndCallButton && RemoteConfig.screenSharing, + isGroupCall = callControlsState.isGroupCall, listener = additionalActionsListener, triggerAlignedPopupState = additionalActionsPopupState ) diff --git a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/LabsValues.kt b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/LabsValues.kt index 943bc102d0..73f70e208f 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/LabsValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/LabsValues.kt @@ -11,7 +11,6 @@ class LabsValues internal constructor(store: KeyValueStore) : SignalStoreValues( const val BETTER_SEARCH: String = "labs.better_search" const val AUTO_LOWER_HAND: String = "labs.auto_lower_hand" const val STARRED_MESSAGES: String = "labs.starred_messages" - const val SCREEN_SHARE: String = "labs.screen_share" } public override fun onFirstEverAppLaunch() = Unit @@ -32,8 +31,6 @@ class LabsValues internal constructor(store: KeyValueStore) : SignalStoreValues( var starredMessages by booleanValue(STARRED_MESSAGES, true).falseForExternalUsers() - var screenShare by booleanValue(SCREEN_SHARE, true).falseForExternalUsers() - private fun SignalStoreValueDelegate.falseForExternalUsers(): SignalStoreValueDelegate { return this.map { actualValue -> RemoteConfig.internalUser && actualValue } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/OutgoingVideoSourceRouter.kt b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/OutgoingVideoSourceRouter.kt index bea895d96c..87d0cd7c93 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/OutgoingVideoSourceRouter.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/OutgoingVideoSourceRouter.kt @@ -56,7 +56,6 @@ class OutgoingVideoSourceRouter( override fun setOrientation(orientation: Int?) { camera.setOrientation(orientation) - screenShareCapturer?.onConfigurationChanged() } val cameraState: CameraState diff --git a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/ScreenShareCapturer.kt b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/ScreenShareCapturer.kt index 5e31b949fe..298273697c 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/ScreenShareCapturer.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/ScreenShareCapturer.kt @@ -2,7 +2,12 @@ package org.thoughtcrime.securesms.ringrtc import android.content.Context import android.content.Intent +import android.hardware.display.DisplayManager import android.media.projection.MediaProjection +import android.os.Build +import android.util.DisplayMetrics +import android.view.Display +import android.view.WindowManager import org.signal.core.util.logging.Log import org.signal.core.util.logging.Log.tag import org.thoughtcrime.securesms.components.webrtc.EglBaseWrapper @@ -22,6 +27,27 @@ class ScreenShareCapturer( private val sink: CapturerObserver ) { + companion object { + private val TAG = tag(ScreenShareCapturer::class.java) + + private const val MAX_DIMENSION = 1280 + private const val FRAME_RATE = 15 + } + + private val displayManager: DisplayManager by lazy { + context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager + } + + private val displayListener = object : DisplayManager.DisplayListener { + override fun onDisplayChanged(displayId: Int) { + if (displayId == Display.DEFAULT_DISPLAY) { + onDisplayChangedInternal() + } + } + override fun onDisplayAdded(displayId: Int) = Unit + override fun onDisplayRemoved(displayId: Int) = Unit + } + private var screenCapturer: ScreenCapturerAndroid? = null private var surfaceHelper: SurfaceTextureHelper? = null private var captureWidth: Int = 0 @@ -45,10 +71,15 @@ class ScreenShareCapturer( override fun onStop() { Log.i(TAG, "MediaProjection stopped") } + + override fun onCapturedContentResize(width: Int, height: Int) { + Log.i(TAG, "onCapturedContentResize($width, $height)") + applyCaptureFormat(width, height) + } } ) - val (width, height) = computeCaptureDimensions() + val (width, height) = scaleForEncoder(readDisplayBounds()) captureWidth = width captureHeight = height @@ -57,27 +88,37 @@ class ScreenShareCapturer( surfaceHelper = SurfaceTextureHelper.create("WebRTC-ScreenShareHelper", base!!.getEglBaseContext()) screenCapturer!!.initialize(surfaceHelper, context, sink) screenCapturer!!.startCapture(width, height, FRAME_RATE) + + if (Build.VERSION.SDK_INT < 34) { + displayManager.registerDisplayListener(displayListener, surfaceHelper!!.handler) + } } } - fun onConfigurationChanged() { + private fun onDisplayChangedInternal() { if (!isCapturing) return + applyCaptureFormat(readDisplayBounds()) + } - val (width, height) = computeCaptureDimensions() + private fun applyCaptureFormat(rawDimensions: Pair) { + applyCaptureFormat(rawDimensions.first, rawDimensions.second) + } + + private fun applyCaptureFormat(rawWidth: Int, rawHeight: Int) { + val (width, height) = scaleForEncoder(rawWidth to rawHeight) if (width == captureWidth && height == captureHeight) { return } - Log.i(TAG, "onConfigurationChanged(): capture dimensions " + width + "x" + height) + Log.i(TAG, "applyCaptureFormat(): capture dimensions " + width + "x" + height) captureWidth = width captureHeight = height screenCapturer?.changeCaptureFormat(width, height, FRAME_RATE) } - private fun computeCaptureDimensions(): Pair { - val metrics = context.resources.displayMetrics - var width = metrics.widthPixels - var height = metrics.heightPixels + private fun scaleForEncoder(raw: Pair): Pair { + var width = raw.first + var height = raw.second val maxDimension = max(width, height) if (maxDimension > MAX_DIMENSION) { @@ -93,6 +134,21 @@ class ScreenShareCapturer( return width to height } + @Suppress("DEPRECATION") + private fun readDisplayBounds(): Pair { + return if (Build.VERSION.SDK_INT >= 30) { + val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + val bounds = windowManager.maximumWindowMetrics.bounds + bounds.width() to bounds.height() + } else { + val displayManager = context.getSystemService(Context.DISPLAY_SERVICE) as DisplayManager + val display = displayManager.getDisplay(Display.DEFAULT_DISPLAY) + val metrics = DisplayMetrics() + display.getRealMetrics(metrics) + metrics.widthPixels to metrics.heightPixels + } + } + fun stop() { if (!isCapturing) { return @@ -100,6 +156,10 @@ class ScreenShareCapturer( Log.i(TAG, "stop()") + if (Build.VERSION.SDK_INT < 34) { + displayManager.unregisterDisplayListener(displayListener) + } + if (screenCapturer != null) { screenCapturer!!.stopCapture() screenCapturer!!.dispose() @@ -119,11 +179,4 @@ class ScreenShareCapturer( fun dispose() { stop() } - - companion object { - private val TAG = tag(ScreenShareCapturer::class.java) - - private const val MAX_DIMENSION = 1280 - private const val FRAME_RATE = 15 - } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt index e41e11ce0b..0d97edee0e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/util/RemoteConfig.kt @@ -1376,5 +1376,16 @@ object RemoteConfig { hotSwappable = true ) + /** + * Whether screen sharing is available during calls. + */ + @JvmStatic + @get:JvmName("screenSharing") + val screenSharing: Boolean by remoteBoolean( + key = "android.calling.screenSharing", + defaultValue = false, + hotSwappable = true + ) + // endregion } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 24bfa56922..b5a7aba3ed 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2706,9 +2706,9 @@ Cancel - Share screen (Labs) + Share screen - Stop sharing (Labs) + Stop sharing View