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