From 39529af4e9ef68cc89e1e89fcccd2efbe8ff7a54 Mon Sep 17 00:00:00 2001 From: Cody Henthorne Date: Fri, 8 May 2026 13:13:44 -0400 Subject: [PATCH] Add screen share to 1:1 and group calling. --- app/src/main/AndroidManifest.xml | 3 +- .../settings/app/labs/LabsSettingsEvents.kt | 2 +- .../settings/app/labs/LabsSettingsFragment.kt | 9 + .../settings/app/labs/LabsSettingsState.kt | 4 +- .../app/labs/LabsSettingsViewModel.kt | 7 +- .../components/webrtc/WebRtcControls.java | 6 +- .../webrtc/v2/AdditionalActionsPopup.kt | 22 ++- .../components/webrtc/v2/CallControls.kt | 11 +- .../components/webrtc/v2/CallEvent.kt | 2 +- .../components/webrtc/v2/CallScreen.kt | 9 +- .../webrtc/v2/CallScreenControlsListener.kt | 2 + .../webrtc/v2/CallScreenMediator.kt | 1 + .../components/webrtc/v2/CallScreenState.kt | 11 +- .../webrtc/v2/ComposeCallScreenMediator.kt | 33 +++- .../webrtc/v2/SwipeToSpeakerHintPopup.kt | 31 +++- .../webrtc/v2/WebRtcCallActivity.kt | 38 +++- .../webrtc/v2/WebRtcCallViewModel.kt | 17 +- .../securesms/events/WebRtcViewModel.kt | 10 +- .../securesms/keyvalue/LabsValues.kt | 4 +- .../securesms/ringrtc/Camera.java | 83 +++++++-- .../ringrtc/OutgoingVideoSourceRouter.kt | 165 ++++++++++++++++++ .../securesms/ringrtc/ScreenShareCapturer.kt | 129 ++++++++++++++ .../service/SafeForegroundService.kt | 8 +- .../service/webrtc/ActiveCallManager.kt | 66 ++++--- .../CallSetupActionProcessorDelegate.java | 12 +- .../webrtc/ConnectedCallActionProcessor.java | 75 +++++++- .../webrtc/DeviceAwareActionProcessor.java | 6 +- .../service/webrtc/GroupActionProcessor.java | 2 +- .../webrtc/GroupConnectedActionProcessor.java | 89 +++++++++- .../webrtc/GroupJoiningActionProcessor.java | 10 +- .../webrtc/GroupPreJoinActionProcessor.java | 6 +- .../webrtc/IncomingCallActionProcessor.java | 30 ++-- .../IncomingGroupCallActionProcessor.java | 20 +-- .../webrtc/OutgoingCallActionProcessor.java | 4 +- .../webrtc/PreJoinActionProcessor.java | 4 +- .../service/webrtc/SignalCallManager.java | 12 ++ .../service/webrtc/WebRtcActionProcessor.java | 18 +- .../service/webrtc/WebRtcVideoUtil.java | 69 ++++---- .../service/webrtc/state/LocalDeviceState.kt | 5 +- .../service/webrtc/state/VideoState.java | 25 +-- .../state/WebRtcServiceStateBuilder.java | 18 +- .../res/drawable/symbol_screen_share_24.xml | 12 ++ app/src/main/res/values/strings.xml | 6 + 43 files changed, 904 insertions(+), 192 deletions(-) create mode 100644 app/src/main/java/org/thoughtcrime/securesms/ringrtc/OutgoingVideoSourceRouter.kt create mode 100644 app/src/main/java/org/thoughtcrime/securesms/ringrtc/ScreenShareCapturer.kt create mode 100644 app/src/main/res/drawable/symbol_screen_share_24.xml diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e0ffe7b0df..9970c2b67e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -38,6 +38,7 @@ + @@ -1403,7 +1404,7 @@ + android:foregroundServiceType="dataSync|microphone|camera|phoneCall|mediaProjection" /> { + SignalStore.labs.screenShare = event.enabled + _state.value = _state.value.copy(screenShare = event.enabled) + } } } @@ -58,7 +62,8 @@ class LabsSettingsViewModel : ViewModel() { betterSearch = SignalStore.labs.betterSearch, autoLowerHand = SignalStore.labs.autoLowerHand, - starredMessages = SignalStore.labs.starredMessages + starredMessages = SignalStore.labs.starredMessages, + screenShare = SignalStore.labs.screenShare ) } } 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 5342bcbf47..aa701d21a2 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 @@ -8,6 +8,8 @@ import androidx.annotation.Px; import androidx.annotation.StringRes; import org.thoughtcrime.securesms.R; +import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.util.RemoteConfig; import org.thoughtcrime.securesms.webrtc.audio.SignalAudioManager; import java.util.Set; @@ -164,7 +166,9 @@ public final class WebRtcControls { } public boolean displayOverflow() { - return isAtLeastOutgoing() && hasAtLeastOneRemote && isGroupCall() && groupCallState == GroupCallState.CONNECTED; + boolean connectedGroupCall = isGroupCall() && groupCallState == GroupCallState.CONNECTED && hasAtLeastOneRemote; + boolean connected1to1Call = !isGroupCall() && callState == CallState.ONGOING && SignalStore.labs().getScreenShare(); + return isAtLeastOutgoing() && (connectedGroupCall || connected1to1Call); } public boolean displayMuteAudio() { 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 d0366c5d99..218aeab6e4 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 @@ -46,6 +46,8 @@ data class AdditionalActionsState( val isShown: Boolean = false, val reactions: PersistentList = persistentListOf(), val isSelfHandRaised: Boolean = false, + val isScreenSharing: Boolean = false, + val displayScreenShareToggle: Boolean = false, @Stable val listener: AdditionalActionsListener = AdditionalActionsListener.Empty ) @@ -53,11 +55,13 @@ interface AdditionalActionsListener { fun onReactClick(reaction: String) fun onReactWithAnyClick() fun onRaiseHandClick(raised: Boolean) + fun onScreenShareClick(sharing: Boolean) object Empty : AdditionalActionsListener { override fun onReactClick(reaction: String) = Unit override fun onReactWithAnyClick() = Unit override fun onRaiseHandClick(raised: Boolean) = Unit + override fun onScreenShareClick(sharing: Boolean) = Unit } } @@ -91,7 +95,10 @@ private fun AdditionalActionsPopupContent( CallScreenMenu( onRaiseHandClick = state.listener::onRaiseHandClick, - isSelfHandRaised = state.isSelfHandRaised + isSelfHandRaised = state.isSelfHandRaised, + isScreenSharing = state.isScreenSharing, + displayScreenShareToggle = state.displayScreenShareToggle, + onScreenShareClick = state.listener::onScreenShareClick ) } } @@ -135,7 +142,10 @@ private fun CallReactionScrubber( @Composable private fun CallScreenMenu( isSelfHandRaised: Boolean, - onRaiseHandClick: (Boolean) -> Unit + onRaiseHandClick: (Boolean) -> Unit, + isScreenSharing: Boolean = false, + displayScreenShareToggle: Boolean = false, + onScreenShareClick: (Boolean) -> Unit = {} ) { Column( modifier = Modifier @@ -147,6 +157,14 @@ private fun CallScreenMenu( title = if (isSelfHandRaised) stringResource(R.string.CallOverflowPopupWindow__lower_hand) else stringResource(R.string.CallOverflowPopupWindow__raise_hand), onClick = { onRaiseHandClick(!isSelfHandRaised) } ) + + if (displayScreenShareToggle) { + CallScreenMenuOption( + imageVector = ImageVector.vectorResource(R.drawable.symbol_screen_share_24), + title = if (isScreenSharing) stringResource(R.string.CallOverflowPopupWindow__stop_screen_share) else stringResource(R.string.CallOverflowPopupWindow__share_screen), + onClick = { onScreenShareClick(!isScreenSharing) } + ) + } } } 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 ad741386af..9bb176da68 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 @@ -82,7 +82,7 @@ fun CallControls( } val hasCameraPermission = ContextCompat.checkSelfPermission(LocalContext.current, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED - if (callControlsState.displayVideoToggle) { + if (callControlsState.displayVideoToggle && !callControlsState.isLocalScreenSharing) { CallScreenTooltipBox( text = stringResource(R.string.WebRtcCallActivity__tap_here_to_turn_on_your_video), displayTooltip = displayVideoTooltip, @@ -214,7 +214,8 @@ data class CallControlsState( val displayAdditionalActions: Boolean = false, val displayStartCallButton: Boolean = false, val startCallButtonText: Int = R.string.WebRtcCallView__start_call, - val displayEndCallButton: Boolean = false + val displayEndCallButton: Boolean = false, + val isLocalScreenSharing: Boolean = false ) { val hasAnyControls: Boolean @@ -235,7 +236,8 @@ data class CallControlsState( callParticipantsState: CallParticipantsState, webRtcControls: WebRtcControls, groupMemberCount: Int, - isAudioDeviceChangePending: Boolean = false + isAudioDeviceChangePending: Boolean = false, + isLocalScreenSharing: Boolean = false ): CallControlsState { return CallControlsState( isEarpieceAvailable = webRtcControls.isEarpieceAvailableForAudioToggle, @@ -255,7 +257,8 @@ data class CallControlsState( displayAdditionalActions = webRtcControls.displayOverflow(), displayStartCallButton = webRtcControls.displayStartCallControls(), startCallButtonText = webRtcControls.startCallButtonText, - displayEndCallButton = webRtcControls.displayEndCall() + displayEndCallButton = webRtcControls.displayEndCall(), + isLocalScreenSharing = isLocalScreenSharing ) } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallEvent.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallEvent.kt index c60a04883e..06ce2ad117 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallEvent.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallEvent.kt @@ -22,7 +22,7 @@ sealed interface CallEvent { data class StartCall(val isVideoCall: Boolean) : CallEvent data class ShowGroupCallSafetyNumberChange(val identityRecords: List) : CallEvent data object SwitchToSpeaker : CallEvent - data object ShowSwipeToSpeakerHint : CallEvent + data object ShowSwipeToScreenShareHint : CallEvent data object ShowLargeGroupAutoMuteToast : CallEvent data class ShowRemoteMuteToast(private val muted: Recipient, private val mutedBy: Recipient) : CallEvent { fun getDescription(context: Context): String { 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 7706a46834..d522c7f03e 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,6 +77,7 @@ 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 @@ -171,11 +172,15 @@ fun CallScreen( val additionalActionsPopupState = TriggerAlignedPopupState.rememberTriggerAlignedPopupState() val additionalActionsState = remember( callScreenState.reactions, - localParticipant.isHandRaised + localParticipant.isHandRaised, + callScreenState.isLocalScreenSharing, + callControlsState.displayEndCallButton ) { AdditionalActionsState( reactions = callScreenState.reactions, isSelfHandRaised = localParticipant.isHandRaised, + isScreenSharing = callScreenState.isLocalScreenSharing, + displayScreenShareToggle = callControlsState.displayEndCallButton && SignalStore.labs.screenShare, listener = additionalActionsListener, triggerAlignedPopupState = additionalActionsPopupState ) @@ -505,7 +510,7 @@ fun CallScreen( ) SwipeToSpeakerHintPopup( - visible = callScreenState.displaySwipeToSpeakerHint, + hintType = callScreenState.swipeHint, onDismiss = onSwipeToSpeakerHintDismissed, modifier = Modifier .statusBarsPadding() diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenControlsListener.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenControlsListener.kt index 1dfae30395..7fed21b442 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenControlsListener.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenControlsListener.kt @@ -36,6 +36,7 @@ interface CallScreenControlsListener { fun onNavigateUpClicked() fun toggleControls() fun onAudioPermissionsRequested(onGranted: Runnable?) + fun onScreenShareChanged(sharing: Boolean) object Empty : CallScreenControlsListener { override fun onStartCall(isVideoCall: Boolean) = Unit @@ -58,5 +59,6 @@ interface CallScreenControlsListener { override fun onNavigateUpClicked() = Unit override fun toggleControls() = Unit override fun onAudioPermissionsRequested(onGranted: Runnable?) = Unit + override fun onScreenShareChanged(sharing: Boolean) = Unit } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenMediator.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenMediator.kt index 0cac639b50..f442089e7b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenMediator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenMediator.kt @@ -43,6 +43,7 @@ interface CallScreenMediator { fun enableRingGroup(canRing: Boolean) fun showSpeakerViewHint() fun hideSpeakerViewHint() + fun showScreenShareHint() fun showVideoTooltip(): Dismissible fun showCameraTooltip(): Dismissible fun onCallStateUpdate(callControlsChange: CallControlsChange) diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenState.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenState.kt index 86019576fc..cfd9c8c33b 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/CallScreenState.kt @@ -25,7 +25,7 @@ data class CallScreenState( val isDisplayingAudioToggleSheet: Boolean = false, val displaySwitchCameraTooltip: Boolean = false, val displayVideoTooltip: Boolean = false, - val displaySwipeToSpeakerHint: Boolean = false, + val swipeHint: SwipeHintType = SwipeHintType.NONE, val displayWifiToCellularPopup: Boolean = false, val remoteMuteToastMessage: String? = null, val displayAdditionalActionsDialog: Boolean = false, @@ -34,7 +34,14 @@ data class CallScreenState( val isParticipantUpdatePopupEnabled: Boolean = true, val isCallStateUpdatePopupEnabled: Boolean = false, val isWaitingToBeLetIn: Boolean = false, - val reactions: PersistentList = persistentListOf() + val reactions: PersistentList = persistentListOf(), + val isLocalScreenSharing: Boolean = false ) { fun isDisplayingControlMenu(): Boolean = isDisplayingAudioToggleSheet || displayAdditionalActionsDialog } + +enum class SwipeHintType { + NONE, + SPEAKER_VIEW, + SCREEN_SHARE +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/ComposeCallScreenMediator.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/ComposeCallScreenMediator.kt index 98d74a3c5d..341123ff3a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/ComposeCallScreenMediator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/ComposeCallScreenMediator.kt @@ -14,6 +14,7 @@ import androidx.activity.enableEdgeToEdge import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember +import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.lifecycle.ViewModel @@ -110,6 +111,7 @@ class ComposeCallScreenMediator(private val activity: WebRtcCallActivity, viewMo val recipient by viewModel.getRecipientFlow().collectAsStateWithLifecycle(Recipient.UNKNOWN) val webRtcCallState by callScreenViewModel.callState.collectAsStateWithLifecycle() val callScreenState by callScreenViewModel.callScreenState.collectAsStateWithLifecycle() + val isLocalScreenSharing by viewModel.isLocalScreenSharing.collectAsStateWithLifecycle() val callControlsState by viewModel.getCallControlsState().collectAsStateWithLifecycle(CallControlsState()) val callParticipantsViewState by callScreenViewModel.callParticipantsViewState.collectAsStateWithLifecycle() val callParticipantsState = remember(callParticipantsViewState) { callParticipantsViewState.callParticipantsState } @@ -173,6 +175,22 @@ class ComposeCallScreenMediator(private val activity: WebRtcCallActivity, viewMo } } + LaunchedEffect(isLocalScreenSharing) { + callScreenViewModel.callScreenState.update { it.copy(isLocalScreenSharing = isLocalScreenSharing) } + } + + LaunchedEffect(callScreenController, callScreenControlsListener) { + snapshotFlow { callScreenController.callParticipantsVerticalPagerState.settledPage } + .collect { page -> + val selected = if (page == 1) { + CallParticipantsState.SelectedPage.FOCUSED + } else { + CallParticipantsState.SelectedPage.GRID + } + callScreenControlsListener.onPageChanged(selected) + } + } + val controlAndInfoState by controlsAndInfoViewModel.state SignalTheme(isDarkMode = true) { @@ -217,7 +235,7 @@ class ComposeCallScreenMediator(private val activity: WebRtcCallActivity, viewMo onControlsToggled = onControlsToggled, onCallScreenDialogDismissed = { callScreenViewModel.dialog.update { CallScreenDialogType.NONE } }, onWifiToCellularPopupDismissed = { callScreenViewModel.callScreenState.update { it.copy(displayWifiToCellularPopup = false) } }, - onSwipeToSpeakerHintDismissed = { callScreenViewModel.callScreenState.update { it.copy(displaySwipeToSpeakerHint = false) } }, + onSwipeToSpeakerHintDismissed = { callScreenViewModel.callScreenState.update { it.copy(swipeHint = SwipeHintType.NONE) } }, onRemoteMuteToastDismissed = { callScreenViewModel.callScreenState.update { it.copy(remoteMuteToastMessage = null) } }, callParticipantUpdatePopupController = callParticipantUpdatePopupController, isSelfAdmin = controlAndInfoState.isSelfAdmin(), @@ -322,11 +340,15 @@ class ComposeCallScreenMediator(private val activity: WebRtcCallActivity, viewMo } override fun showSpeakerViewHint() { - callScreenViewModel.callScreenState.update { it.copy(displaySwipeToSpeakerHint = true) } + callScreenViewModel.callScreenState.update { it.copy(swipeHint = SwipeHintType.SPEAKER_VIEW) } } override fun hideSpeakerViewHint() { - callScreenViewModel.callScreenState.update { it.copy(displaySwipeToSpeakerHint = false) } + callScreenViewModel.callScreenState.update { it.copy(swipeHint = SwipeHintType.NONE) } + } + + override fun showScreenShareHint() { + callScreenViewModel.callScreenState.update { it.copy(swipeHint = SwipeHintType.SCREEN_SHARE) } } override fun showVideoTooltip(): Dismissible { @@ -393,6 +415,11 @@ class ComposeCallScreenMediator(private val activity: WebRtcCallActivity, viewMo callScreenViewModel.callScreenState.update { it.copy(displayAdditionalActionsDialog = false) } } + override fun onScreenShareClick(sharing: Boolean) { + callScreenViewModel.callScreenState.update { it.copy(displayAdditionalActionsDialog = false) } + controlsListener.value.onScreenShareChanged(sharing) + } + private fun handleFailure() { Toast.makeText(activity, R.string.CallLinkDetailsFragment__couldnt_save_changes, Toast.LENGTH_LONG).show() } diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/SwipeToSpeakerHintPopup.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/SwipeToSpeakerHintPopup.kt index 9f7ad89e11..a1ae22c2a3 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/SwipeToSpeakerHintPopup.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/SwipeToSpeakerHintPopup.kt @@ -5,6 +5,7 @@ package org.thoughtcrime.securesms.components.webrtc.v2 +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -25,16 +26,23 @@ import kotlin.time.Duration.Companion.seconds import org.signal.core.ui.R as CoreUiR /** - * Popup shown to hint the user that they can swipe to view screen share. + * Popup shown to hint the user that they should swipe between the grid view and + * the focused page for speaker/screen share when available. */ @Composable fun SwipeToSpeakerHintPopup( - visible: Boolean, + hintType: SwipeHintType, onDismiss: () -> Unit, modifier: Modifier = Modifier ) { + val textResId = when (hintType) { + SwipeHintType.SCREEN_SHARE -> R.string.CallToastPopupWindow__swipe_to_view_screen_share + SwipeHintType.SPEAKER_VIEW, + SwipeHintType.NONE -> R.string.CallToastPopupWindow__swipe_to_view_speaker + } + CallScreenPopup( - visible = visible, + visible = hintType != SwipeHintType.NONE, onDismiss = onDismiss, displayDuration = 3.seconds, modifier = modifier @@ -51,7 +59,7 @@ fun SwipeToSpeakerHintPopup( ) Text( - text = stringResource(R.string.CallToastPopupWindow__swipe_to_view_screen_share), + text = stringResource(textResId), color = colorResource(CoreUiR.color.signal_light_colorOnSecondaryContainer), modifier = Modifier.padding(start = 8.dp) ) @@ -63,9 +71,16 @@ fun SwipeToSpeakerHintPopup( @Composable private fun SwipeToSpeakerHintPopupPreview() { Previews.Preview { - SwipeToSpeakerHintPopup( - visible = true, - onDismiss = {} - ) + Column { + SwipeToSpeakerHintPopup( + hintType = SwipeHintType.SPEAKER_VIEW, + onDismiss = {} + ) + + SwipeToSpeakerHintPopup( + hintType = SwipeHintType.SCREEN_SHARE, + onDismiss = {} + ) + } } } 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 3389fb630f..564c413eb9 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 @@ -14,6 +14,8 @@ import android.content.Intent import android.content.pm.PackageManager import android.content.res.Configuration import android.media.AudioManager +import android.media.projection.MediaProjectionConfig +import android.media.projection.MediaProjectionManager import android.os.Build import android.os.Bundle import android.util.Rational @@ -22,6 +24,7 @@ import android.view.ViewGroup import android.view.Window import android.view.WindowManager import androidx.activity.OnBackPressedCallback +import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.viewModels import androidx.annotation.RequiresApi import androidx.appcompat.app.AppCompatDelegate @@ -38,6 +41,7 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.disposables.Disposable import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.launch import org.greenrobot.eventbus.EventBus @@ -123,6 +127,11 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re private var ephemeralStateDisposable = Disposable.empty() private val callPermissionsDialogController = CallPermissionsDialogController() private val eventBusSubscriber = EventBusSubscriber() + private val mediaProjectionLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK && result.data != null) { + AppDependencies.signalCallManager.startScreenShare(result.data!!) + } + } override fun attachBaseContext(newBase: Context) { delegate.localNightMode = AppCompatDelegate.MODE_NIGHT_YES @@ -210,6 +219,17 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re callScreen.setMicEnabled(viewModel.microphoneEnabled.value) } } + + lifecycleScope.launch { + viewModel + .isLocalScreenSharing + .drop(1) + .collect { sharing -> + if (!sharing && !lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) { + AppDependencies.signalCallManager.setEnableVideo(false) + } + } + } } override fun onRestoreInstanceState(savedInstanceState: Bundle) { @@ -312,7 +332,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re requestNewSizesThrottle.clear() } - if (!isChangingConfigurations && !isInMultiWindowModeCompat()) { + if (!isChangingConfigurations && !isInMultiWindowModeCompat() && !viewModel.isLocalScreenSharing.value) { AppDependencies.signalCallManager.setEnableVideo(false) } @@ -985,7 +1005,7 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re is CallEvent.StartCall -> startCall(event.isVideoCall) is CallEvent.ShowGroupCallSafetyNumberChange -> SafetyNumberBottomSheet.forGroupCall(event.identityRecords).show(supportFragmentManager) is CallEvent.SwitchToSpeaker -> callScreen.switchToSpeakerView() - is CallEvent.ShowSwipeToSpeakerHint -> callScreen.showSpeakerViewHint() + is CallEvent.ShowSwipeToScreenShareHint -> callScreen.showScreenShareHint() is CallEvent.ShowRemoteMuteToast -> callScreen.showRemoteMuteToast(event.getDescription(this)) is CallEvent.ShowLargeGroupAutoMuteToast -> { callScreen.onCallStateUpdate(CallControlsChange.MIC_OFF) @@ -1390,6 +1410,20 @@ class WebRtcCallActivity : BaseActivity(), SafetyNumberChangeDialog.Callback, Re override fun onAudioPermissionsRequested(onGranted: Runnable?) { askAudioPermissions { onGranted?.run() } } + + override fun onScreenShareChanged(sharing: Boolean) { + if (sharing) { + val mediaProjectionManager = getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager + val intent = if (Build.VERSION.SDK_INT >= 34) { + mediaProjectionManager.createScreenCaptureIntent(MediaProjectionConfig.createConfigForDefaultDisplay()) + } else { + mediaProjectionManager.createScreenCaptureIntent() + } + mediaProjectionLauncher.launch(intent) + } else { + AppDependencies.signalCallManager.stopScreenShare() + } + } } private inner class PendingParticipantsViewListener : PendingParticipantsListener { diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/WebRtcCallViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/WebRtcCallViewModel.kt index 65299b5424..440c90cfd2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/WebRtcCallViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/v2/WebRtcCallViewModel.kt @@ -70,6 +70,9 @@ class WebRtcCallViewModel : ViewModel() { private val ephemeralState = MutableStateFlow(null) private val remoteMutesReported = MutableStateFlow(HashSet()) + private val _isLocalScreenSharing = MutableStateFlow(false) + val isLocalScreenSharing: StateFlow = _isLocalScreenSharing + private val controlsWithFoldableState: Flow = combine(foldableState, webRtcControls, this::updateControlsFoldableState) private val realWebRtcControls: StateFlow = combine(isInPipMode, controlsWithFoldableState, this::getRealWebRtcControls) .stateIn(viewModelScope, SharingStarted.Eagerly, WebRtcControls.NONE) @@ -100,6 +103,7 @@ class WebRtcCallViewModel : ViewModel() { private var callConnectedTime = -1L private var answerWithVideoAvailable = false private var previousParticipantList = Collections.emptyList() + private var hasSeededParticipantList = false private var switchOnFirstScreenShare = true private var showScreenShareTip = true private var hasShownAutoMuteToast = false @@ -181,9 +185,10 @@ class WebRtcCallViewModel : ViewModel() { callParticipantsState, getWebRtcControls(), groupSize, - isAudioDeviceChangePending - ) { participantsState, controls, groupMemberCount, audioChangePending -> - CallControlsState.fromViewModelData(participantsState, controls, groupMemberCount, audioChangePending) + isAudioDeviceChangePending, + _isLocalScreenSharing + ) { participantsState, controls, groupMemberCount, audioChangePending, isLocalScreenSharing -> + CallControlsState.fromViewModelData(participantsState, controls, groupMemberCount, audioChangePending, isLocalScreenSharing) } } @@ -263,7 +268,7 @@ class WebRtcCallViewModel : ViewModel() { ) { showScreenShareTip = false viewModelScope.launch { - events.emit(CallEvent.ShowSwipeToSpeakerHint) + events.emit(CallEvent.ShowSwipeToScreenShareHint) } } @@ -319,6 +324,7 @@ class WebRtcCallViewModel : ViewModel() { val wasMicrophoneEnabled = internalMicrophoneEnabled.value internalMicrophoneEnabled.value = localParticipant.isMicrophoneEnabled isAudioDeviceChangePending.value = webRtcViewModel.isAudioDeviceChangePending + _isLocalScreenSharing.value = webRtcViewModel.isLocalScreenSharing if (internalMicrophoneEnabled.value) { remoteMutedBy.update { null } @@ -347,12 +353,13 @@ class WebRtcCallViewModel : ViewModel() { } if (webRtcViewModel.groupState.isConnected) { - if (!containsPlaceholders(previousParticipantList)) { + if (!containsPlaceholders(previousParticipantList) && hasSeededParticipantList) { val update = CallParticipantListUpdate.computeDeltaUpdate(previousParticipantList, webRtcViewModel.remoteParticipants) viewModelScope.launch { callParticipantListUpdate.emit(update) } } + hasSeededParticipantList = true for (remote in webRtcViewModel.remoteParticipants) { if (remote.remotelyMutedBy == null) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.kt b/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.kt index da158e5425..6414f7cf90 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/events/WebRtcViewModel.kt @@ -124,12 +124,14 @@ class WebRtcViewModel(state: WebRtcServiceState) { val isAudioDeviceChangePending: Boolean = state.localDeviceState.isAudioDeviceChangePending val localParticipant: CallParticipant = createLocal( - state.localDeviceState.cameraState, - (if (state.videoState.localSink != null) state.videoState.localSink else BroadcastVideoSink())!!, - state.localDeviceState.isMicrophoneEnabled, - state.localDeviceState.handRaisedTimestamp + cameraState = state.localDeviceState.cameraState, + renderer = state.videoState.localSink ?: BroadcastVideoSink(), + microphoneEnabled = state.localDeviceState.isMicrophoneEnabled, + handRaisedTimestamp = state.localDeviceState.handRaisedTimestamp ) + val isLocalScreenSharing: Boolean = state.localDeviceState.isScreenSharing + val remoteMutedBy: CallParticipant? = state.localDeviceState.remoteMutedBy val isCellularConnection: Boolean = when (state.localDeviceState.networkConnectionType) { 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 0bd9a95290..943bc102d0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/keyvalue/LabsValues.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/keyvalue/LabsValues.kt @@ -10,8 +10,8 @@ class LabsValues internal constructor(store: KeyValueStore) : SignalStoreValues( const val GROUP_SUGGESTIONS_FOR_MEMBERS: String = "labs.group_suggestions_for_members" 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,6 +32,8 @@ 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/Camera.java b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/Camera.java index 0ecdfca9f8..748235e258 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/Camera.java +++ b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/Camera.java @@ -39,6 +39,10 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa private static final String TAG = Log.tag(Camera.class); + private static final int CAPTURE_WIDTH = 1280; + private static final int CAPTURE_HEIGHT = 720; + private static final int CAPTURE_FPS = 30; + @NonNull private final Context context; @Nullable private final CameraVideoCapturer capturer; @Nullable private CameraEventListener cameraEventListener; @@ -47,6 +51,8 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa @NonNull private CameraState.Direction activeDirection; private boolean enabled; private boolean isInitialized; + private boolean capturing; + private boolean paused; private int orientation; @Nullable private volatile VideoSink vanitySink; @@ -120,24 +126,42 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa Log.i(TAG, "setEnabled(): " + enabled); this.enabled = enabled; + this.paused = false; if (capturer == null || !isInitialized) { return; } - try { - if (enabled) { - Log.i(TAG, "setEnabled(): starting capture"); - capturer.startCapture(1280, 720, 30); - } else { - Log.i(TAG, "setEnabled(): stopping capture"); - capturer.stopCapture(); - } - } catch (InterruptedException e) { - Log.w(TAG, "Got interrupted while trying to stop video capture", e); + if (enabled) { + startCapture(); + } else { + stopCapture(); } } + public void pauseCapture() { + if (capturer != null && capturing) { + paused = true; + stopCapture(); + } + } + + public void resumeCapture() { + paused = false; + if (capturer != null && isInitialized && enabled && !capturing) { + startCapture(); + } + } + + /** Whether the camera should be capturing based on user state, regardless of the current pause status. */ + public boolean shouldBeCapturing() { + return capturer != null && isInitialized && enabled; + } + + public boolean isCapturing() { + return capturing; + } + public void setCameraEventListener(@Nullable CameraEventListener cameraEventListener) { this.cameraEventListener = cameraEventListener; } @@ -151,10 +175,18 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa this.vanitySink = vanitySink; } + void deliverToVanitySink(@NonNull VideoFrame videoFrame) { + VideoSink sink = vanitySink; + if (sink != null) { + sink.onFrame(videoFrame); + } + } + public void dispose() { if (capturer != null) { capturer.dispose(); isInitialized = false; + capturing = false; } } @@ -170,14 +202,30 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa return new CameraState(getActiveDirection(), getCount()); } - @Nullable CameraVideoCapturer getCapturer() { - return capturer; - } - public boolean isInitialized() { return isInitialized; } + private void startCapture() { + Log.i(TAG, "startCapture()"); + try { + capturer.startCapture(CAPTURE_WIDTH, CAPTURE_HEIGHT, CAPTURE_FPS); + capturing = true; + } catch (Exception e) { + Log.w(TAG, "Error starting capture", e); + } + } + + private void stopCapture() { + Log.i(TAG, "stopCapture()"); + try { + capturer.stopCapture(); + } catch (InterruptedException e) { + Log.w(TAG, "Interrupted stopping capture", e); + } + capturing = false; + } + private @Nullable CameraVideoCapturer createVideoCapturer(@NonNull CameraEnumerator enumerator, @NonNull CameraState.Direction direction) { @@ -332,6 +380,9 @@ public class Camera implements CameraControl, CameraVideoCapturer.CameraSwitchHa @Override public void onCapturerStopped() { + if (paused) { + return; + } observer.onCapturerStopped(); if (cameraEventListener != null) cameraEventListener.onCameraStopped(); } @@ -339,10 +390,6 @@ 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/ringrtc/OutgoingVideoSourceRouter.kt b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/OutgoingVideoSourceRouter.kt new file mode 100644 index 0000000000..bea895d96c --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/OutgoingVideoSourceRouter.kt @@ -0,0 +1,165 @@ +package org.thoughtcrime.securesms.ringrtc + +import android.content.Context +import android.content.Intent +import org.signal.core.util.logging.Log +import org.signal.core.util.logging.Log.tag +import org.signal.ringrtc.CameraControl +import org.thoughtcrime.securesms.components.webrtc.EglBaseWrapper +import org.webrtc.CapturerObserver +import org.webrtc.VideoFrame +import org.webrtc.VideoSink +import kotlin.concurrent.Volatile + +/** + * Owns the outgoing video pipeline: a [Camera] and a lazily-created + * [ScreenShareCapturer], and the routing logic that decides which one's + * frames flow into the WebRTC [CapturerObserver] that RingRTC supplies. + */ +class OutgoingVideoSourceRouter( + private val context: Context, + private val eglBase: EglBaseWrapper, + cameraEventListener: CameraEventListener, + desiredCameraDirection: CameraState.Direction +) : CameraControl { + + companion object { + private val TAG = tag(OutgoingVideoSourceRouter::class.java) + } + + private val camera: Camera = Camera(context, cameraEventListener, eglBase, desiredCameraDirection) + + private var screenShareCapturer: ScreenShareCapturer? = null + private var downstream: CapturerObserver? = null + + @Volatile + var isScreenSharing: Boolean = false + private set + + override fun hasCapturer(): Boolean { + return camera.hasCapturer() + } + + override fun initCapturer(observer: CapturerObserver) { + Log.i(TAG, "initCapturer()") + this.downstream = observer + camera.initCapturer(CameraSideObserver()) + } + + override fun setEnabled(enable: Boolean) { + camera.setEnabled(enable) + } + + override fun flip() { + camera.flip() + } + + override fun setOrientation(orientation: Int?) { + camera.setOrientation(orientation) + screenShareCapturer?.onConfigurationChanged() + } + + val cameraState: CameraState + get() = camera.cameraState + + val isInitialized: Boolean + get() = camera.isInitialized + + fun setCameraEventListener(cameraEventListener: CameraEventListener?) { + camera.setCameraEventListener(cameraEventListener) + } + + fun setVanitySink(vanitySink: VideoSink?) { + camera.setVanitySink(vanitySink) + } + + fun startScreenShare(mediaProjectionData: Intent) { + if (isScreenSharing) { + Log.w(TAG, "Already screen sharing") + return + } + + if (downstream == null) { + Log.w(TAG, "Cannot start screen share before initCapturer()") + return + } + + Log.i(TAG, "startScreenShare()") + + isScreenSharing = true + + if (camera.isCapturing) { + camera.pauseCapture() + } + + if (screenShareCapturer == null) { + screenShareCapturer = ScreenShareCapturer(context, eglBase, ScreenSideObserver()) + } + + screenShareCapturer!!.start(mediaProjectionData) + } + + fun stopScreenShare() { + if (!isScreenSharing) { + return + } + + Log.i(TAG, "stopScreenShare()") + + screenShareCapturer?.stop() + + isScreenSharing = false + + if (camera.shouldBeCapturing()) { + camera.resumeCapture() + } + } + + fun dispose() { + screenShareCapturer?.dispose() + screenShareCapturer = null + isScreenSharing = false + camera.dispose() + } + + private inner class CameraSideObserver : CapturerObserver { + override fun onCapturerStarted(success: Boolean) { + if (!isScreenSharing) { + downstream?.onCapturerStarted(success) + } + } + + override fun onCapturerStopped() { + if (!isScreenSharing) { + downstream?.onCapturerStopped() + } + } + + override fun onFrameCaptured(videoFrame: VideoFrame) { + if (!isScreenSharing) { + downstream?.onFrameCaptured(videoFrame) + } + camera.deliverToVanitySink(videoFrame) + } + } + + private inner class ScreenSideObserver : CapturerObserver { + override fun onCapturerStarted(success: Boolean) { + if (isScreenSharing) { + downstream?.onCapturerStarted(success) + } + } + + override fun onCapturerStopped() { + if (isScreenSharing) { + downstream?.onCapturerStopped() + } + } + + override fun onFrameCaptured(videoFrame: VideoFrame?) { + if (isScreenSharing) { + downstream?.onFrameCaptured(videoFrame) + } + } + } +} diff --git a/app/src/main/java/org/thoughtcrime/securesms/ringrtc/ScreenShareCapturer.kt b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/ScreenShareCapturer.kt new file mode 100644 index 0000000000..5e31b949fe --- /dev/null +++ b/app/src/main/java/org/thoughtcrime/securesms/ringrtc/ScreenShareCapturer.kt @@ -0,0 +1,129 @@ +package org.thoughtcrime.securesms.ringrtc + +import android.content.Context +import android.content.Intent +import android.media.projection.MediaProjection +import org.signal.core.util.logging.Log +import org.signal.core.util.logging.Log.tag +import org.thoughtcrime.securesms.components.webrtc.EglBaseWrapper +import org.webrtc.CapturerObserver +import org.webrtc.EglBase +import org.webrtc.ScreenCapturerAndroid +import org.webrtc.SurfaceTextureHelper +import kotlin.math.max + +/** + * Captures the device screen via [MediaProjection] and forwards frames to a + * [CapturerObserver] sink. + */ +class ScreenShareCapturer( + private val context: Context, + private val eglBase: EglBaseWrapper, + private val sink: CapturerObserver +) { + + private var screenCapturer: ScreenCapturerAndroid? = null + private var surfaceHelper: SurfaceTextureHelper? = null + private var captureWidth: Int = 0 + private var captureHeight: Int = 0 + var isCapturing: Boolean = false + private set + + fun start(mediaProjectionData: Intent) { + if (isCapturing) { + Log.w(TAG, "Already capturing") + return + } + + Log.i(TAG, "start()") + isCapturing = true + + eglBase.performWithValidEglBase { base: EglBase? -> + screenCapturer = ScreenCapturerAndroid( + mediaProjectionData, + object : MediaProjection.Callback() { + override fun onStop() { + Log.i(TAG, "MediaProjection stopped") + } + } + ) + + val (width, height) = computeCaptureDimensions() + captureWidth = width + captureHeight = height + + Log.i(TAG, "start(): capture dimensions " + width + "x" + height) + + surfaceHelper = SurfaceTextureHelper.create("WebRTC-ScreenShareHelper", base!!.getEglBaseContext()) + screenCapturer!!.initialize(surfaceHelper, context, sink) + screenCapturer!!.startCapture(width, height, FRAME_RATE) + } + } + + fun onConfigurationChanged() { + if (!isCapturing) return + + val (width, height) = computeCaptureDimensions() + if (width == captureWidth && height == captureHeight) { + return + } + + Log.i(TAG, "onConfigurationChanged(): 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 + + val maxDimension = max(width, height) + if (maxDimension > MAX_DIMENSION) { + val scale = MAX_DIMENSION.toFloat() / maxDimension + width = (width * scale).toInt() + height = (height * scale).toInt() + } + + // Encoders require even dimensions + width = width and 1.inv() + height = height and 1.inv() + + return width to height + } + + fun stop() { + if (!isCapturing) { + return + } + + Log.i(TAG, "stop()") + + if (screenCapturer != null) { + screenCapturer!!.stopCapture() + screenCapturer!!.dispose() + screenCapturer = null + } + + if (surfaceHelper != null) { + surfaceHelper!!.dispose() + surfaceHelper = null + } + + captureWidth = 0 + captureHeight = 0 + isCapturing = false + } + + 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/service/SafeForegroundService.kt b/app/src/main/java/org/thoughtcrime/securesms/service/SafeForegroundService.kt index 26c715c5e3..3a0486d770 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/SafeForegroundService.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/SafeForegroundService.kt @@ -5,6 +5,7 @@ package org.thoughtcrime.securesms.service +import android.annotation.SuppressLint import android.app.Notification import android.app.Service import android.content.Context @@ -181,6 +182,7 @@ abstract class SafeForegroundService : Service() { super.onCreate() } + @SuppressLint("WrongConstant") override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { checkNotNull(intent) { "Must have an intent!" } @@ -188,8 +190,8 @@ abstract class SafeForegroundService : Service() { if (intent.action == ACTION_TIMEOUT) { Log.i(TAG, "Time limit for foreground services has been met. Skipping starting a foreground.") - } else if (Build.VERSION.SDK_INT >= 30 && serviceType != 0) { - startForeground(notificationId, getForegroundNotification(intent), serviceType) + } else if (Build.VERSION.SDK_INT >= 30 && serviceType(intent) != 0) { + startForeground(notificationId, getForegroundNotification(intent), serviceType(intent)) } else { startForeground(notificationId, getForegroundNotification(intent)) } @@ -248,7 +250,7 @@ abstract class SafeForegroundService : Service() { /** Special service type to use when calling start service if needed */ @RequiresApi(30) - open val serviceType: Int = 0 + open fun serviceType(intent: Intent): Int = 0 /** Notification to post as our foreground notification. */ abstract fun getForegroundNotification(intent: Intent): Notification diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ActiveCallManager.kt b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ActiveCallManager.kt index b5f1c46f55..03bc84989a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ActiveCallManager.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ActiveCallManager.kt @@ -288,12 +288,16 @@ class ActiveCallManager( private const val EXTRA_RECIPIENT_ID = "RECIPIENT_ID" private const val EXTRA_IS_VIDEO_CALL = "IS_VIDEO_CALL" private const val EXTRA_TYPE = "TYPE" + private const val EXTRA_SCREEN_SHARING = "SCREEN_SHARING" - fun update(context: Context, @CallNotificationBuilder.CallNotificationType type: Int, recipientId: RecipientId, isVideoCall: Boolean) { + @JvmStatic + @JvmOverloads + fun update(context: Context, @CallNotificationBuilder.CallNotificationType type: Int, recipientId: RecipientId, isVideoCall: Boolean, isScreenSharing: Boolean = false) { val extras = bundleOf( EXTRA_TYPE to type, EXTRA_RECIPIENT_ID to recipientId, - EXTRA_IS_VIDEO_CALL to isVideoCall + EXTRA_IS_VIDEO_CALL to isVideoCall, + EXTRA_SCREEN_SHARING to isScreenSharing ) if (!SafeForegroundService.update(context, ActiveCallForegroundService::class.java, extras)) { @@ -308,34 +312,14 @@ class ActiveCallManager( } } + private var isScreenSharing: Boolean = false + override val tag: String get() = TAG override val notificationId: Int get() = CallNotificationBuilder.WEBRTC_NOTIFICATION - @get:RequiresApi(30) - override val serviceType: Int - get() { - val telecom = Build.VERSION.SDK_INT >= 36 && AndroidTelecomUtil.hasActiveController() - - var type = if (telecom) { - ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL - } else { - ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC - } - - if (Permissions.hasAll(this, Manifest.permission.RECORD_AUDIO)) { - type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE - } - - if (Permissions.hasAll(this, Manifest.permission.CAMERA)) { - type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA - } - - return type - } - @Suppress("DEPRECATION") private var hangUpRtcOnDeviceCallAnswered: PhoneStateListener? = null private var notificationDisposable: Disposable = Disposable.disposed() @@ -385,6 +369,31 @@ class ActiveCallManager( } } + @RequiresApi(30) + override fun serviceType(intent: Intent): Int { + val telecom = Build.VERSION.SDK_INT >= 36 && AndroidTelecomUtil.hasActiveController() + + var type = if (telecom) { + ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL + } else { + ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC + } + + if (Permissions.hasAll(this, Manifest.permission.RECORD_AUDIO)) { + type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_MICROPHONE + } + + if (Permissions.hasAll(this, Manifest.permission.CAMERA)) { + type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_CAMERA + } + + if (intent.getBooleanExtra(EXTRA_SCREEN_SHARING, false)) { + type = type or ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION + } + + return type + } + override fun getForegroundNotification(intent: Intent): Notification { notificationDisposable.dispose() @@ -423,6 +432,15 @@ class ActiveCallManager( return createNotification(type, recipient, isVideoCall, skipAvatarLoad = requiresAsyncNotificationLoad) } + override fun onServiceUpdateCommandReceived(intent: Intent) { + val wasScreenSharing = isScreenSharing + isScreenSharing = intent.getBooleanExtra(EXTRA_SCREEN_SHARING, false) + + if (isScreenSharing && !wasScreenSharing) { + AppDependencies.signalCallManager.onScreenSharingServiceReady() + } + } + private fun createNotification(type: Int, recipient: Recipient, isVideoCall: Boolean, skipAvatarLoad: Boolean): Notification { return CallNotificationBuilder.getCallInProgressNotification( this, diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/CallSetupActionProcessorDelegate.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/CallSetupActionProcessorDelegate.java index 301c895da4..6eae821296 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/CallSetupActionProcessorDelegate.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/CallSetupActionProcessorDelegate.java @@ -7,7 +7,7 @@ import org.signal.ringrtc.CallException; import org.signal.ringrtc.CallManager; import org.thoughtcrime.securesms.dependencies.AppDependencies; import org.thoughtcrime.securesms.events.WebRtcViewModel; -import org.thoughtcrime.securesms.ringrtc.Camera; +import org.thoughtcrime.securesms.ringrtc.OutgoingVideoSourceRouter; import org.thoughtcrime.securesms.ringrtc.RemotePeer; import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState; import org.thoughtcrime.securesms.util.AppForegroundObserver; @@ -93,19 +93,19 @@ public class CallSetupActionProcessorDelegate extends WebRtcActionProcessor { protected @NonNull WebRtcServiceState handleSetEnableVideo(@NonNull WebRtcServiceState currentState, boolean enable) { Log.i(tag, "handleSetEnableVideo(): enable: " + enable); - Camera camera = currentState.getVideoState().requireCamera(); + OutgoingVideoSourceRouter router = currentState.getVideoState().requireRouter(); - if (camera.isInitialized()) { - camera.setEnabled(enable); + if (router.isInitialized()) { + router.setEnabled(enable); } currentState = currentState.builder() .changeLocalDeviceState() - .cameraState(camera.getCameraState()) + .cameraState(router.getCameraState()) .build(); //noinspection SimplifiableBooleanExpression - if ((enable && camera.isInitialized()) || !enable) { + if ((enable && router.isInitialized()) || !enable) { try { CallManager callManager = webRtcInteractor.getCallManager(); callManager.setVideoEnable(enable, false); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ConnectedCallActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ConnectedCallActionProcessor.java index f709d9fe14..8c0ea0aa24 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ConnectedCallActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/ConnectedCallActionProcessor.java @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.service.webrtc; +import android.content.Intent; import android.os.ResultReceiver; import androidx.annotation.NonNull; @@ -11,7 +12,11 @@ import org.signal.ringrtc.CallManager; import org.thoughtcrime.securesms.events.CallParticipant; import org.thoughtcrime.securesms.events.CallParticipantId; import org.thoughtcrime.securesms.events.WebRtcViewModel; +import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.ringrtc.OutgoingVideoSourceRouter; import org.thoughtcrime.securesms.ringrtc.RemotePeer; +import org.thoughtcrime.securesms.service.webrtc.state.LocalDeviceState; +import org.thoughtcrime.securesms.webrtc.CallNotificationBuilder; import org.thoughtcrime.securesms.service.webrtc.state.WebRtcEphemeralState; import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState; @@ -50,7 +55,7 @@ public class ConnectedCallActionProcessor extends DeviceAwareActionProcessor { currentState = currentState.builder() .changeLocalDeviceState() - .cameraState(currentState.getVideoState().requireCamera().getCameraState()) + .cameraState(currentState.getVideoState().requireRouter().getCameraState()) .build(); boolean localVideoEnabled = currentState.getLocalDeviceState().getCameraState().isEnabled(); @@ -122,6 +127,74 @@ public class ConnectedCallActionProcessor extends DeviceAwareActionProcessor { return activeCallDelegate.handleScreenSharingEnable(currentState, enable); } + @Override + protected @NonNull WebRtcServiceState handleSetLocalScreenShare(@NonNull WebRtcServiceState currentState, boolean enable, @Nullable Intent mediaProjectionData) { + Log.i(TAG, "handleSetLocalScreenShare(): enable: " + enable); + + OutgoingVideoSourceRouter router = currentState.getVideoState().requireRouter(); + + RecipientId recipientId = currentState.getCallInfoState().requireActivePeer().getRecipient().getId(); + + if (enable && mediaProjectionData != null) { + Log.i(tag, "Updating service for media projection, then can screen share"); + ActiveCallManager.ActiveCallForegroundService.update(context, CallNotificationBuilder.TYPE_ESTABLISHED, recipientId, true, true); + + return currentState.builder() + .changeLocalDeviceState() + .setMediaProjectionIntent(mediaProjectionData) + .build(); + } else { + router.stopScreenShare(); + + boolean cameraWasEnabled = currentState.getLocalDeviceState().getCameraState().isEnabled(); + ActiveCallManager.ActiveCallForegroundService.update(context, CallNotificationBuilder.TYPE_ESTABLISHED, recipientId, cameraWasEnabled, false); + + try { + webRtcInteractor.getCallManager().setVideoEnable(cameraWasEnabled, false); + } catch (CallException e) { + return callFailure(currentState, "setVideoEnable() after screen share failed: ", e); + } + + return currentState.builder() + .changeLocalDeviceState() + .isScreenSharing(false) + .setMediaProjectionIntent(null) + .build(); + } + } + + @Override + protected @NonNull WebRtcServiceState handleScreenSharingServiceReady(@NonNull WebRtcServiceState currentState) { + Log.i(tag, "handleScreenSharingServiceReady()"); + + LocalDeviceState localDeviceState = currentState.getLocalDeviceState(); + Intent mediaProjectionIntent = localDeviceState.getMediaProjectionIntent(); + + if (localDeviceState.isScreenSharing()) { + Log.w(tag, "handleScreenSharingServiceReady(): already screensharing!"); + return currentState; + } + + if (mediaProjectionIntent == null) { + Log.w(tag, "handleScreenSharingServiceReady(): Media intent is null, bailing"); + return currentState; + } + + OutgoingVideoSourceRouter router = currentState.getVideoState().requireRouter(); + + try { + webRtcInteractor.getCallManager().setVideoEnable(true, true); + } catch (CallException e) { + return callFailure(currentState, "setVideoEnable() for screen share failed: ", e); + } + router.startScreenShare(mediaProjectionIntent); + + return currentState.builder() + .changeLocalDeviceState() + .isScreenSharing(true) + .build(); + } + @Override protected @NonNull WebRtcServiceState handleLocalHangup(@NonNull WebRtcServiceState currentState) { return activeCallDelegate.handleLocalHangup(currentState); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/DeviceAwareActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/DeviceAwareActionProcessor.java index 1905da0ec0..418d866817 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/DeviceAwareActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/DeviceAwareActionProcessor.java @@ -61,11 +61,11 @@ public abstract class DeviceAwareActionProcessor extends WebRtcActionProcessor { protected @NonNull WebRtcServiceState handleSetCameraFlip(@NonNull WebRtcServiceState currentState) { Log.i(tag, "handleSetCameraFlip():"); - if (currentState.getLocalDeviceState().getCameraState().isEnabled() && currentState.getVideoState().getCamera() != null) { - currentState.getVideoState().getCamera().flip(); + if (currentState.getLocalDeviceState().getCameraState().isEnabled() && currentState.getVideoState().getRouter() != null) { + currentState.getVideoState().getRouter().flip(); return currentState.builder() .changeLocalDeviceState() - .cameraState(currentState.getVideoState().getCamera().getCameraState()) + .cameraState(currentState.getVideoState().getRouter().getCameraState()) .build(); } return currentState; diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupActionProcessor.java index a6f7fcef17..baddf6ea29 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupActionProcessor.java @@ -317,7 +317,7 @@ public class GroupActionProcessor extends DeviceAwareActionProcessor { .actionProcessor(actionProcessorFactory.createNetworkUnavailableActionProcessor(webRtcInteractor)) .changeVideoState() .eglBase(videoState.getLockableEglBase()) - .camera(videoState.getCamera()) + .router(videoState.getRouter()) .localSink(videoState.getLocalSink()) .commit() .changeCallInfoState() diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupConnectedActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupConnectedActionProcessor.java index 3f42f540da..2de305e98d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupConnectedActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupConnectedActionProcessor.java @@ -1,13 +1,12 @@ package org.thoughtcrime.securesms.service.webrtc; +import android.content.Intent; import android.os.ResultReceiver; import android.util.LongSparseArray; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import java.util.stream.Collectors; - import org.signal.core.util.logging.Log; import org.signal.ringrtc.CallException; import org.signal.ringrtc.GroupCall; @@ -19,11 +18,14 @@ import org.thoughtcrime.securesms.events.GroupCallSpeechEvent; import org.thoughtcrime.securesms.events.WebRtcViewModel; import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.recipients.Recipient; -import org.thoughtcrime.securesms.ringrtc.Camera; +import org.thoughtcrime.securesms.recipients.RecipientId; +import org.thoughtcrime.securesms.ringrtc.OutgoingVideoSourceRouter; import org.thoughtcrime.securesms.ringrtc.RemotePeer; +import org.thoughtcrime.securesms.service.webrtc.state.LocalDeviceState; 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.webrtc.CallNotificationBuilder; import java.util.ArrayList; import java.util.Collection; @@ -31,6 +33,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.UUID; +import java.util.stream.Collectors; /** * Process actions for when the call has at least once been connected and joined. @@ -87,19 +90,19 @@ public class GroupConnectedActionProcessor extends GroupActionProcessor { protected @NonNull WebRtcServiceState handleSetEnableVideo(@NonNull WebRtcServiceState currentState, boolean enable) { Log.i(tag, "handleSetEnableVideo():"); - GroupCall groupCall = currentState.getCallInfoState().requireGroupCall(); - Camera camera = currentState.getVideoState().requireCamera(); + GroupCall groupCall = currentState.getCallInfoState().requireGroupCall(); + OutgoingVideoSourceRouter router = currentState.getVideoState().requireRouter(); try { groupCall.setOutgoingVideoMuted(!enable, false); } catch (CallException e) { return groupCallFailure(currentState, "Unable set video muted", e); } - camera.setEnabled(enable); + router.setEnabled(enable); currentState = currentState.builder() .changeLocalDeviceState() - .cameraState(camera.getCameraState()) + .cameraState(router.getCameraState()) .build(); boolean localVideoEnabled = currentState.getLocalDeviceState().getCameraState().isEnabled(); @@ -111,6 +114,78 @@ public class GroupConnectedActionProcessor extends GroupActionProcessor { return currentState; } + @Override + protected @NonNull WebRtcServiceState handleSetLocalScreenShare(@NonNull WebRtcServiceState currentState, boolean enable, @Nullable Intent mediaProjectionData) { + Log.i(tag, "handleSetLocalScreenShare(): enable: " + enable); + + GroupCall groupCall = currentState.getCallInfoState().requireGroupCall(); + OutgoingVideoSourceRouter router = currentState.getVideoState().requireRouter(); + + RecipientId recipientId = currentState.getCallInfoState().getCallRecipient().getId(); + + if (enable && mediaProjectionData != null) { + Log.i(tag, "Updating service for media projection, then can screen share"); + ActiveCallManager.ActiveCallForegroundService.update(context, CallNotificationBuilder.TYPE_ESTABLISHED, recipientId, true, true); + + return currentState.builder() + .changeLocalDeviceState() + .setMediaProjectionIntent(mediaProjectionData) + .build(); + } else { + router.stopScreenShare(); + + boolean cameraWasEnabled = currentState.getLocalDeviceState().getCameraState().isEnabled(); + ActiveCallManager.ActiveCallForegroundService.update(context, CallNotificationBuilder.TYPE_ESTABLISHED, recipientId, true, false); + + try { + groupCall.setPresenting(false); + groupCall.setOutgoingVideoMuted(!cameraWasEnabled, false); + } catch (CallException e) { + return groupCallFailure(currentState, "Unable to restore video mute after screen share", e); + } + + return currentState.builder() + .changeLocalDeviceState() + .isScreenSharing(false) + .setMediaProjectionIntent(null) + .build(); + } + } + + @Override + protected @NonNull WebRtcServiceState handleScreenSharingServiceReady(@NonNull WebRtcServiceState currentState) { + Log.i(tag, "handleScreenSharingServiceReady()"); + + LocalDeviceState localDeviceState = currentState.getLocalDeviceState(); + Intent mediaProjectionIntent = localDeviceState.getMediaProjectionIntent(); + + if (localDeviceState.isScreenSharing()) { + Log.w(tag, "handleScreenSharingServiceReady(): already screensharing!"); + return currentState; + } + + if (mediaProjectionIntent == null) { + Log.w(tag, "handleScreenSharingServiceReady(): Media intent is null, bailing"); + return currentState; + } + + GroupCall groupCall = currentState.getCallInfoState().requireGroupCall(); + OutgoingVideoSourceRouter router = currentState.getVideoState().requireRouter(); + + try { + groupCall.setOutgoingVideoMuted(false, true); + groupCall.setPresenting(true); + } catch (CallException e) { + return groupCallFailure(currentState, "Unable to unmute video for screen share", e); + } + router.startScreenShare(mediaProjectionIntent); + + return currentState.builder() + .changeLocalDeviceState() + .isScreenSharing(true) + .build(); + } + @Override protected @NonNull WebRtcServiceState handleSetMuteAudio(@NonNull WebRtcServiceState currentState, boolean muted) { try { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupJoiningActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupJoiningActionProcessor.java index 0df0b6d042..ee1019b5e2 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupJoiningActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupJoiningActionProcessor.java @@ -9,7 +9,7 @@ import org.signal.core.util.logging.Log; import org.signal.ringrtc.CallException; import org.signal.ringrtc.GroupCall; import org.thoughtcrime.securesms.events.WebRtcViewModel; -import org.thoughtcrime.securesms.ringrtc.Camera; +import org.thoughtcrime.securesms.ringrtc.OutgoingVideoSourceRouter; import org.thoughtcrime.securesms.ringrtc.RemotePeer; import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState; import org.thoughtcrime.securesms.events.CallParticipant; @@ -142,19 +142,19 @@ public class GroupJoiningActionProcessor extends GroupActionProcessor { @Override protected @NonNull WebRtcServiceState handleSetEnableVideo(@NonNull WebRtcServiceState currentState, boolean enable) { - GroupCall groupCall = currentState.getCallInfoState().requireGroupCall(); - Camera camera = currentState.getVideoState().requireCamera(); + GroupCall groupCall = currentState.getCallInfoState().requireGroupCall(); + OutgoingVideoSourceRouter router = currentState.getVideoState().requireRouter(); try { groupCall.setOutgoingVideoMuted(!enable, false); } catch (CallException e) { return groupCallFailure(currentState, "Unable to set video muted", e); } - camera.setEnabled(enable); + router.setEnabled(enable); currentState = currentState.builder() .changeLocalDeviceState() - .cameraState(camera.getCameraState()) + .cameraState(router.getCameraState()) .build(); WebRtcUtil.enableSpeakerPhoneIfNeeded(webRtcInteractor, currentState); diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupPreJoinActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupPreJoinActionProcessor.java index bf27ab8be5..7687aa5716 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupPreJoinActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/GroupPreJoinActionProcessor.java @@ -196,7 +196,7 @@ public class GroupPreJoinActionProcessor extends GroupActionProcessor { webRtcInteractor.initializeAudioForCall(true); try { - groupCall.setOutgoingVideoSource(currentState.getVideoState().requireLocalSink(), currentState.getVideoState().requireCamera()); + groupCall.setOutgoingVideoSource(currentState.getVideoState().requireLocalSink(), currentState.getVideoState().requireRouter()); groupCall.setOutgoingVideoMuted(!currentState.getLocalDeviceState().getCameraState().isEnabled(), false); groupCall.setOutgoingAudioMuted(!currentState.getLocalDeviceState().isMicrophoneEnabled()); groupCall.setDataMode(NetworkUtil.getCallingDataMode(context, groupCall.getLocalDeviceState().getNetworkRoute().getLocalAdapterType())); @@ -220,13 +220,13 @@ public class GroupPreJoinActionProcessor extends GroupActionProcessor { protected @NonNull WebRtcServiceState handleSetEnableVideo(@NonNull WebRtcServiceState currentState, boolean enable) { Log.i(tag, "handleSetEnableVideo(): Changing for pre-join group call. enable: " + enable); - currentState.getVideoState().requireCamera().setEnabled(enable); + currentState.getVideoState().requireRouter().setEnabled(enable); return currentState.builder() .changeCallSetupState(RemotePeer.GROUP_CALL_ID) .enableVideoOnCreate(enable) .commit() .changeLocalDeviceState() - .cameraState(currentState.getVideoState().requireCamera().getCameraState()) + .cameraState(currentState.getVideoState().requireRouter().getCameraState()) .build(); } 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 17ad8b3ca2..ab3e6ed3f1 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,7 +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.OutgoingVideoSourceRouter; import org.thoughtcrime.securesms.ringrtc.RemotePeer; import org.thoughtcrime.securesms.service.webrtc.state.CallSetupState; import org.thoughtcrime.securesms.service.webrtc.state.VideoState; @@ -109,7 +109,7 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor { RingRtcDynamicConfiguration.getAudioConfig(), videoState.requireLocalSink(), callParticipant.getVideoSink(), - videoState.requireCamera(), + videoState.requireRouter(), callSetupState.getIceServers(), hideIp, NetworkUtil.getCallingDataMode(context), @@ -137,14 +137,14 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor { Log.i(TAG, "handleAcceptCall(): call_id: " + activePeer.getCallId()); - Camera camera = currentState.getVideoState().requireCamera(); - camera.setVanitySink(null); + OutgoingVideoSourceRouter router = currentState.getVideoState().requireRouter(); + router.setVanitySink(null); if (!answerWithVideo && currentState.getLocalDeviceState().getCameraState().isEnabled()) { - camera.setEnabled(false); + router.setEnabled(false); currentState = currentState.builder() .changeLocalDeviceState() - .cameraState(camera.getCameraState()) + .cameraState(router.getCameraState()) .build(); } @@ -172,9 +172,9 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor { Log.i(TAG, "handleDenyCall():"); - Camera camera = currentState.getVideoState().getCamera(); - if (camera != null) { - camera.setVanitySink(null); + OutgoingVideoSourceRouter router = currentState.getVideoState().getRouter(); + if (router != null) { + router.setVanitySink(null); } webRtcInteractor.sendNotAcceptedCallEventSyncMessage(activePeer, @@ -209,21 +209,21 @@ public class IncomingCallActionProcessor extends DeviceAwareActionProcessor { return currentState; } - Camera camera = currentState.getVideoState().requireCamera(); + OutgoingVideoSourceRouter router = currentState.getVideoState().requireRouter(); if (enabled) { Log.i(TAG, "handleSetIncomingRingingVanity(): enabling vanity camera"); - camera.setVanitySink(currentState.getVideoState().requireLocalSink()); - camera.setEnabled(true); + router.setVanitySink(currentState.getVideoState().requireLocalSink()); + router.setEnabled(true); } else { Log.i(TAG, "handleSetIncomingRingingVanity(): disabling vanity camera"); - camera.setVanitySink(null); - camera.setEnabled(false); + router.setVanitySink(null); + router.setEnabled(false); } return currentState.builder() .changeLocalDeviceState() - .cameraState(camera.getCameraState()) + .cameraState(router.getCameraState()) .build(); } 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 6916e6bce4..5ce19877fe 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,7 +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.OutgoingVideoSourceRouter; import org.thoughtcrime.securesms.ringrtc.RemotePeer; import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState; import org.thoughtcrime.securesms.util.AppForegroundObserver; @@ -196,22 +196,22 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro return currentState; } - Camera camera = currentState.getVideoState().requireCamera(); + OutgoingVideoSourceRouter router = currentState.getVideoState().requireRouter(); - if (enabled && !camera.isInitialized()) { + if (enabled && !router.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); + router.setEnabled(true); } else { Log.i(TAG, "handleSetIncomingRingingVanity(): disabling vanity camera"); - camera.setEnabled(false); + router.setEnabled(false); } return currentState.builder() .changeLocalDeviceState() - .cameraState(camera.getCameraState()) + .cameraState(router.getCameraState()) .build(); } @@ -259,7 +259,7 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro webRtcInteractor.initializeAudioForCall(true); try { - groupCall.setOutgoingVideoSource(currentState.getVideoState().requireLocalSink(), currentState.getVideoState().requireCamera()); + groupCall.setOutgoingVideoSource(currentState.getVideoState().requireLocalSink(), currentState.getVideoState().requireRouter()); groupCall.setOutgoingVideoMuted(!answerWithVideo, false); groupCall.setOutgoingAudioMuted(!currentState.getLocalDeviceState().isMicrophoneEnabled()); groupCall.setDataMode(NetworkUtil.getCallingDataMode(context, groupCall.getLocalDeviceState().getNetworkRoute().getLocalAdapterType())); @@ -270,11 +270,11 @@ public final class IncomingGroupCallActionProcessor extends DeviceAwareActionPro } if (answerWithVideo) { - Camera camera = currentState.getVideoState().requireCamera(); - camera.setEnabled(true); + OutgoingVideoSourceRouter router = currentState.getVideoState().requireRouter(); + router.setEnabled(true); currentState = currentState.builder() .changeLocalDeviceState() - .cameraState(camera.getCameraState()) + .cameraState(router.getCameraState()) .build(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/OutgoingCallActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/OutgoingCallActionProcessor.java index 7677168a9e..66d947994e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/OutgoingCallActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/OutgoingCallActionProcessor.java @@ -157,7 +157,7 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor { RingRtcDynamicConfiguration.getAudioConfig(), videoState.requireLocalSink(), callParticipant.getVideoSink(), - videoState.requireCamera(), + videoState.requireRouter(), callSetupState.getIceServers(), hideIp, NetworkUtil.getCallingDataMode(context), @@ -170,7 +170,7 @@ public class OutgoingCallActionProcessor extends DeviceAwareActionProcessor { return currentState.builder() .changeLocalDeviceState() - .cameraState(currentState.getVideoState().requireCamera().getCameraState()) + .cameraState(currentState.getVideoState().requireRouter().getCameraState()) .build(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/PreJoinActionProcessor.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/PreJoinActionProcessor.java index c3d3dd2b6d..c20c39cd17 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/PreJoinActionProcessor.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/PreJoinActionProcessor.java @@ -63,10 +63,10 @@ public class PreJoinActionProcessor extends DeviceAwareActionProcessor { protected @NonNull WebRtcServiceState handleSetEnableVideo(@NonNull WebRtcServiceState currentState, boolean enable) { Log.i(TAG, "handleSetEnableVideo(): Changing for pre-join call."); - currentState.getVideoState().getCamera().setEnabled(enable); + currentState.getVideoState().getRouter().setEnabled(enable); return currentState.builder() .changeLocalDeviceState() - .cameraState(currentState.getVideoState().getCamera().getCameraState()) + .cameraState(currentState.getVideoState().getRouter().getCameraState()) .build(); } 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 e5072b0d43..d5fa8b5f21 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 @@ -265,6 +265,18 @@ public final class SignalCallManager implements CallManager.Observer, GroupCall. process((s, p) -> p.handleSetEnableVideo(s, enabled)); } + public void startScreenShare(@NonNull android.content.Intent mediaProjectionData) { + process((s, p) -> p.handleSetLocalScreenShare(s, true, mediaProjectionData)); + } + + public void stopScreenShare() { + process((s, p) -> p.handleSetLocalScreenShare(s, false, null)); + } + + public void onScreenSharingServiceReady() { + process((s, p) -> p.handleScreenSharingServiceReady(s)); + } + public void setIncomingRingingVanity(boolean enabled) { process((s, p) -> p.handleSetIncomingRingingVanity(s, enabled)); } 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 407d8c9869..35edda9aed 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 @@ -34,7 +34,7 @@ import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.recipients.RecipientUtil; import org.thoughtcrime.securesms.ringrtc.CallState; -import org.thoughtcrime.securesms.ringrtc.Camera; +import org.thoughtcrime.securesms.ringrtc.OutgoingVideoSourceRouter; import org.thoughtcrime.securesms.ringrtc.CameraState; import org.thoughtcrime.securesms.ringrtc.RemotePeer; import org.thoughtcrime.securesms.service.webrtc.WebRtcData.CallMetadata; @@ -414,6 +414,16 @@ public abstract class WebRtcActionProcessor { return currentState; } + protected @NonNull WebRtcServiceState handleSetLocalScreenShare(@NonNull WebRtcServiceState currentState, boolean enable, @Nullable android.content.Intent mediaProjectionData) { + Log.i(tag, "handleSetLocalScreenShare not processed"); + return currentState; + } + + protected @NonNull WebRtcServiceState handleScreenSharingServiceReady(@NonNull WebRtcServiceState currentState) { + Log.i(tag, "handleScreenSharingServiceReady not processed"); + return currentState; + } + protected @NonNull WebRtcServiceState handleReceivedHangup(@NonNull WebRtcServiceState currentState, @NonNull CallMetadata callMetadata, @NonNull HangupMetadata hangupMetadata) @@ -638,9 +648,9 @@ public abstract class WebRtcActionProcessor { } protected @NonNull WebRtcServiceState handleOrientationChanged(@NonNull WebRtcServiceState currentState, boolean isLandscapeEnabled, int orientationDegrees) { - Camera camera = currentState.getVideoState().getCamera(); - if (camera != null) { - camera.setOrientation(orientationDegrees); + OutgoingVideoSourceRouter router = currentState.getVideoState().getRouter(); + if (router != null) { + router.setOrientation(orientationDegrees); } int sinkRotationDegrees = isLandscapeEnabled ? BroadcastVideoSink.DEVICE_ROTATION_IGNORE : orientationDegrees; diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcVideoUtil.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcVideoUtil.java index 9b54a1205d..c0e528d0df 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcVideoUtil.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/WebRtcVideoUtil.java @@ -7,9 +7,9 @@ import androidx.annotation.NonNull; import org.signal.core.util.ThreadUtil; import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink; import org.thoughtcrime.securesms.components.webrtc.EglBaseWrapper; -import org.thoughtcrime.securesms.ringrtc.Camera; import org.thoughtcrime.securesms.ringrtc.CameraEventListener; import org.thoughtcrime.securesms.ringrtc.CameraState; +import org.thoughtcrime.securesms.ringrtc.OutgoingVideoSourceRouter; import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceState; import org.thoughtcrime.securesms.service.webrtc.state.WebRtcServiceStateBuilder; import org.webrtc.CapturerObserver; @@ -17,8 +17,8 @@ import org.webrtc.VideoFrame; import org.webrtc.VideoSink; /** - * Helper for initializing, reinitializing, and deinitializing the camera and it's related - * infrastructure. + * Helper for initializing, reinitializing, and deinitializing the outgoing video + * pipeline (camera + screen share, owned by the {@link OutgoingVideoSourceRouter}). */ public final class WebRtcVideoUtil { @@ -32,22 +32,25 @@ public final class WebRtcVideoUtil { final WebRtcServiceStateBuilder builder = currentState.builder(); ThreadUtil.runOnMainSync(() -> { - EglBaseWrapper eglBase = EglBaseWrapper.acquireEglBase(eglBaseHolder); - BroadcastVideoSink localSink = new BroadcastVideoSink(eglBase, - true, - false, - currentState.getLocalDeviceState().getOrientation().getDegrees()); - Camera camera = new Camera(context, cameraEventListener, eglBase, CameraState.Direction.FRONT); + EglBaseWrapper eglBase = EglBaseWrapper.acquireEglBase(eglBaseHolder); + BroadcastVideoSink localSink = new BroadcastVideoSink(eglBase, + true, + false, + currentState.getLocalDeviceState().getOrientation().getDegrees()); + OutgoingVideoSourceRouter router = new OutgoingVideoSourceRouter(context, + eglBase, + cameraEventListener, + CameraState.Direction.FRONT); - camera.setOrientation(currentState.getLocalDeviceState().getOrientation().getDegrees()); + router.setOrientation(currentState.getLocalDeviceState().getOrientation().getDegrees()); builder.changeVideoState() .eglBase(eglBase) .localSink(localSink) - .camera(camera) + .router(router) .commit() .changeLocalDeviceState() - .cameraState(camera.getCameraState()) + .cameraState(router.getCameraState()) .commit(); }); @@ -61,23 +64,23 @@ public final class WebRtcVideoUtil { final WebRtcServiceStateBuilder builder = currentState.builder(); ThreadUtil.runOnMainSync(() -> { - Camera camera = currentState.getVideoState().requireCamera(); - camera.setCameraEventListener(null); - camera.setEnabled(false); - camera.dispose(); + OutgoingVideoSourceRouter oldRouter = currentState.getVideoState().requireRouter(); + oldRouter.setCameraEventListener(null); + oldRouter.setEnabled(false); + oldRouter.dispose(); - camera = new Camera(context, - cameraEventListener, - currentState.getVideoState().getLockableEglBase(), - currentState.getLocalDeviceState().getCameraState().getActiveDirection()); + OutgoingVideoSourceRouter router = new OutgoingVideoSourceRouter(context, + currentState.getVideoState().getLockableEglBase(), + cameraEventListener, + currentState.getLocalDeviceState().getCameraState().getActiveDirection()); - camera.setOrientation(currentState.getLocalDeviceState().getOrientation().getDegrees()); + router.setOrientation(currentState.getLocalDeviceState().getOrientation().getDegrees()); builder.changeVideoState() - .camera(camera) + .router(router) .commit() .changeLocalDeviceState() - .cameraState(camera.getCameraState()) + .cameraState(router.getCameraState()) .commit(); }); @@ -85,15 +88,15 @@ public final class WebRtcVideoUtil { } public static @NonNull WebRtcServiceState deinitializeVideo(@NonNull WebRtcServiceState currentState) { - Camera camera = currentState.getVideoState().getCamera(); - if (camera != null) { - camera.dispose(); + OutgoingVideoSourceRouter router = currentState.getVideoState().getRouter(); + if (router != null) { + router.dispose(); } return currentState.builder() .changeVideoState() .eglBase(null) - .camera(null) + .router(null) .localSink(null) .commit() .changeLocalDeviceState() @@ -102,11 +105,11 @@ public final class WebRtcVideoUtil { } public static @NonNull WebRtcServiceState initializeVanityCamera(@NonNull WebRtcServiceState currentState) { - Camera camera = currentState.getVideoState().requireCamera(); - VideoSink sink = currentState.getVideoState().requireLocalSink(); + OutgoingVideoSourceRouter router = currentState.getVideoState().requireRouter(); + VideoSink sink = currentState.getVideoState().requireLocalSink(); - if (camera.hasCapturer()) { - camera.initCapturer(new CapturerObserver() { + if (router.hasCapturer()) { + router.initCapturer(new CapturerObserver() { @Override public void onFrameCaptured(VideoFrame videoFrame) { sink.onFrame(videoFrame); @@ -118,12 +121,12 @@ public final class WebRtcVideoUtil { @Override public void onCapturerStopped() {} }); - camera.setEnabled(true); + router.setEnabled(true); } return currentState.builder() .changeLocalDeviceState() - .cameraState(camera.getCameraState()) + .cameraState(router.getCameraState()) .build(); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/LocalDeviceState.kt b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/LocalDeviceState.kt index 0aa600d9af..03667cb5be 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/LocalDeviceState.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/LocalDeviceState.kt @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.service.webrtc.state +import android.content.Intent import org.thoughtcrime.securesms.components.sensors.Orientation import org.thoughtcrime.securesms.events.CallParticipant import org.thoughtcrime.securesms.ringrtc.CameraState @@ -21,7 +22,9 @@ data class LocalDeviceState( var isAudioDeviceChangePending: Boolean = false, var networkConnectionType: PeerConnection.AdapterType = PeerConnection.AdapterType.UNKNOWN, var handRaisedTimestamp: Long = CallParticipant.HAND_LOWERED, - var remoteMutedBy: CallParticipant? = null + var remoteMutedBy: CallParticipant? = null, + var isScreenSharing: Boolean = false, + var mediaProjectionIntent: Intent? = null ) { fun duplicate(): LocalDeviceState { diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/VideoState.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/VideoState.java index f850b7ed80..377af5d861 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/VideoState.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/VideoState.java @@ -5,7 +5,7 @@ import androidx.annotation.Nullable; import org.thoughtcrime.securesms.components.webrtc.BroadcastVideoSink; import org.thoughtcrime.securesms.components.webrtc.EglBaseWrapper; -import org.thoughtcrime.securesms.ringrtc.Camera; +import org.thoughtcrime.securesms.ringrtc.OutgoingVideoSourceRouter; import java.util.Objects; @@ -13,22 +13,25 @@ import java.util.Objects; * Local device video state and infrastructure. */ public final class VideoState { - EglBaseWrapper eglBase; - BroadcastVideoSink localSink; - Camera camera; + EglBaseWrapper eglBase; + BroadcastVideoSink localSink; + OutgoingVideoSourceRouter router; VideoState() { this(null, null, null); } VideoState(@NonNull VideoState toCopy) { - this(toCopy.eglBase, toCopy.localSink, toCopy.camera); + this(toCopy.eglBase, toCopy.localSink, toCopy.router); } - VideoState(@NonNull EglBaseWrapper eglBase, @Nullable BroadcastVideoSink localSink, @Nullable Camera camera) { + VideoState(@Nullable EglBaseWrapper eglBase, + @Nullable BroadcastVideoSink localSink, + @Nullable OutgoingVideoSourceRouter router) + { this.eglBase = eglBase; this.localSink = localSink; - this.camera = camera; + this.router = router; } public @NonNull EglBaseWrapper getLockableEglBase() { @@ -43,11 +46,11 @@ public final class VideoState { return Objects.requireNonNull(localSink); } - public @Nullable Camera getCamera() { - return camera; + public @Nullable OutgoingVideoSourceRouter getRouter() { + return router; } - public @NonNull Camera requireCamera() { - return Objects.requireNonNull(camera); + public @NonNull OutgoingVideoSourceRouter requireRouter() { + return Objects.requireNonNull(router); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java index 16e6196896..0cbe328675 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java +++ b/app/src/main/java/org/thoughtcrime/securesms/service/webrtc/state/WebRtcServiceStateBuilder.java @@ -1,5 +1,7 @@ package org.thoughtcrime.securesms.service.webrtc.state; +import android.content.Intent; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -15,8 +17,8 @@ import org.thoughtcrime.securesms.events.GroupCallSpeechEvent; import org.thoughtcrime.securesms.events.WebRtcViewModel; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; -import org.thoughtcrime.securesms.ringrtc.Camera; import org.thoughtcrime.securesms.ringrtc.CameraState; +import org.thoughtcrime.securesms.ringrtc.OutgoingVideoSourceRouter; import org.thoughtcrime.securesms.ringrtc.RemotePeer; import org.thoughtcrime.securesms.service.webrtc.CallLinkDisconnectReason; import org.thoughtcrime.securesms.service.webrtc.WebRtcActionProcessor; @@ -158,6 +160,16 @@ public class WebRtcServiceStateBuilder { toBuild.setMicrophoneEnabled(false); return this; } + + public @NonNull LocalDeviceStateBuilder isScreenSharing(boolean isScreenSharing) { + toBuild.setScreenSharing(isScreenSharing); + return this; + } + + public @NonNull LocalDeviceStateBuilder setMediaProjectionIntent(@Nullable Intent mediaProjectionIntent) { + toBuild.setMediaProjectionIntent(mediaProjectionIntent); + return this; + } } public class CallSetupStateBuilder { @@ -263,8 +275,8 @@ public class WebRtcServiceStateBuilder { return this; } - public @NonNull VideoStateBuilder camera(@Nullable Camera camera) { - toBuild.camera = camera; + public @NonNull VideoStateBuilder router(@Nullable OutgoingVideoSourceRouter router) { + toBuild.router = router; return this; } } diff --git a/app/src/main/res/drawable/symbol_screen_share_24.xml b/app/src/main/res/drawable/symbol_screen_share_24.xml new file mode 100644 index 0000000000..e7f98f6281 --- /dev/null +++ b/app/src/main/res/drawable/symbol_screen_share_24.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3e51c1ddec..24bfa56922 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -2705,6 +2705,10 @@ Lower Cancel + + Share screen (Labs) + + Stop sharing (Labs) View @@ -2829,6 +2833,8 @@ Swipe to view screen share + + Swipe to view speaker Proxy server