Merge pull request #5811 from vector-im/feature/ons/voip_screen_sharing_permission
VoIP Screen Sharing Permission
This commit is contained in:
commit
8eaa2f8dfb
|
@ -0,0 +1 @@
|
|||
VoIP Screen Sharing Permission
|
|
@ -60,6 +60,9 @@ class DebugVectorFeatures(
|
|||
override fun isLiveLocationEnabled(): Boolean = read(DebugFeatureKeys.liveLocationSharing)
|
||||
?: vectorFeatures.isLiveLocationEnabled()
|
||||
|
||||
override fun isScreenSharingEnabled(): Boolean = read(DebugFeatureKeys.screenSharing)
|
||||
?: vectorFeatures.isScreenSharingEnabled()
|
||||
|
||||
fun <T> override(value: T?, key: Preferences.Key<T>) = updatePreferences {
|
||||
if (value == null) {
|
||||
it.remove(key)
|
||||
|
@ -114,4 +117,5 @@ object DebugFeatureKeys {
|
|||
val onboardingPersonalize = booleanPreferencesKey("onboarding-personalize")
|
||||
val onboardingCombinedRegister = booleanPreferencesKey("onboarding-combined-register")
|
||||
val liveLocationSharing = booleanPreferencesKey("live-location-sharing")
|
||||
val screenSharing = booleanPreferencesKey("screen-sharing")
|
||||
}
|
||||
|
|
|
@ -375,6 +375,12 @@
|
|||
android:exported="false"
|
||||
android:foregroundServiceType="location" />
|
||||
|
||||
<service
|
||||
android:name=".features.call.webrtc.ScreenCaptureService"
|
||||
android:exported="false"
|
||||
android:foregroundServiceType="mediaProjection"
|
||||
tools:targetApi="Q" />
|
||||
|
||||
<!-- Receivers -->
|
||||
|
||||
<receiver
|
||||
|
|
|
@ -27,6 +27,7 @@ interface VectorFeatures {
|
|||
fun isOnboardingPersonalizeEnabled(): Boolean
|
||||
fun isOnboardingCombinedRegisterEnabled(): Boolean
|
||||
fun isLiveLocationEnabled(): Boolean
|
||||
fun isScreenSharingEnabled(): Boolean
|
||||
|
||||
enum class OnboardingVariant {
|
||||
LEGACY,
|
||||
|
@ -43,4 +44,5 @@ class DefaultVectorFeatures : VectorFeatures {
|
|||
override fun isOnboardingPersonalizeEnabled() = false
|
||||
override fun isOnboardingCombinedRegisterEnabled() = false
|
||||
override fun isLiveLocationEnabled(): Boolean = false
|
||||
override fun isScreenSharingEnabled(): Boolean = false
|
||||
}
|
||||
|
|
|
@ -27,6 +27,8 @@ import dagger.hilt.android.AndroidEntryPoint
|
|||
import im.vector.app.R
|
||||
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
|
||||
import im.vector.app.databinding.BottomSheetCallControlsBinding
|
||||
import im.vector.app.features.VectorFeatures
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetCallControlsBinding>() {
|
||||
|
@ -34,6 +36,8 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC
|
|||
return BottomSheetCallControlsBinding.inflate(inflater, container, false)
|
||||
}
|
||||
|
||||
@Inject lateinit var vectorFeatures: VectorFeatures
|
||||
|
||||
private val callViewModel: VectorCallViewModel by activityViewModel()
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
|
@ -66,6 +70,12 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC
|
|||
callViewModel.handle(VectorCallViewActions.InitiateCallTransfer)
|
||||
dismiss()
|
||||
}
|
||||
|
||||
views.callControlsShareScreen.isVisible = vectorFeatures.isScreenSharingEnabled()
|
||||
views.callControlsShareScreen.views.bottomSheetActionClickableZone.debouncedClicks {
|
||||
callViewModel.handle(VectorCallViewActions.ToggleScreenSharing)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
|
||||
private fun renderState(state: VectorCallViewState) {
|
||||
|
@ -95,5 +105,6 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment<BottomSheetC
|
|||
views.callControlsToggleHoldResume.leftIcon = ContextCompat.getDrawable(requireContext(), R.drawable.ic_call_hold_action)
|
||||
}
|
||||
views.callControlsTransfer.isVisible = state.canOpponentBeTransferred
|
||||
views.callControlsShareScreen.title = getString(if (state.isSharingScreen) R.string.call_stop_screen_sharing else R.string.call_start_screen_sharing)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ import android.content.Intent
|
|||
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.Color
|
||||
import android.media.projection.MediaProjectionManager
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
|
@ -56,6 +57,8 @@ import im.vector.app.features.call.dialpad.CallDialPadBottomSheet
|
|||
import im.vector.app.features.call.dialpad.DialPadFragment
|
||||
import im.vector.app.features.call.transfer.CallTransferActivity
|
||||
import im.vector.app.features.call.utils.EglUtils
|
||||
import im.vector.app.features.call.webrtc.ScreenCaptureService
|
||||
import im.vector.app.features.call.webrtc.ScreenCaptureServiceConnection
|
||||
import im.vector.app.features.call.webrtc.WebRtcCall
|
||||
import im.vector.app.features.call.webrtc.WebRtcCallManager
|
||||
import im.vector.app.features.displayname.getBestName
|
||||
|
@ -94,6 +97,7 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
|||
|
||||
@Inject lateinit var callManager: WebRtcCallManager
|
||||
@Inject lateinit var avatarRenderer: AvatarRenderer
|
||||
@Inject lateinit var screenCaptureServiceConnection: ScreenCaptureServiceConnection
|
||||
|
||||
private val callViewModel: VectorCallViewModel by viewModel()
|
||||
|
||||
|
@ -512,20 +516,22 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
|||
private fun handleViewEvents(event: VectorCallViewEvents?) {
|
||||
Timber.tag(loggerTag.value).v("handleViewEvents $event")
|
||||
when (event) {
|
||||
is VectorCallViewEvents.ConnectionTimeout -> {
|
||||
is VectorCallViewEvents.ConnectionTimeout -> {
|
||||
onErrorTimoutConnect(event.turn)
|
||||
}
|
||||
is VectorCallViewEvents.ShowDialPad -> {
|
||||
is VectorCallViewEvents.ShowDialPad -> {
|
||||
CallDialPadBottomSheet.newInstance(false).apply {
|
||||
callback = dialPadCallback
|
||||
}.show(supportFragmentManager, FRAGMENT_DIAL_PAD_TAG)
|
||||
}
|
||||
is VectorCallViewEvents.ShowCallTransferScreen -> {
|
||||
is VectorCallViewEvents.ShowCallTransferScreen -> {
|
||||
val callId = withState(callViewModel) { it.callId }
|
||||
navigator.openCallTransfer(this, callTransferActivityResultLauncher, callId)
|
||||
}
|
||||
is VectorCallViewEvents.FailToTransfer -> showSnackbar(getString(R.string.call_transfer_failure))
|
||||
else -> Unit
|
||||
is VectorCallViewEvents.FailToTransfer -> showSnackbar(getString(R.string.call_transfer_failure))
|
||||
is VectorCallViewEvents.ShowScreenSharingPermissionDialog -> handleShowScreenSharingPermissionDialog()
|
||||
is VectorCallViewEvents.StopScreenSharingService -> handleStopScreenSharingService()
|
||||
else -> Unit
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -628,6 +634,32 @@ class VectorCallActivity : VectorBaseActivity<ActivityCallBinding>(), CallContro
|
|||
}
|
||||
}
|
||||
|
||||
private val screenSharingPermissionActivityResultLauncher = registerStartForActivityResult { activityResult ->
|
||||
if (activityResult.resultCode == Activity.RESULT_OK) {
|
||||
callViewModel.handle(VectorCallViewActions.StartScreenSharing)
|
||||
// We need to start a foreground service with a sticky notification during screen sharing
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
ContextCompat.startForegroundService(
|
||||
this,
|
||||
Intent(this, ScreenCaptureService::class.java)
|
||||
)
|
||||
screenCaptureServiceConnection.bind()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleShowScreenSharingPermissionDialog() {
|
||||
getSystemService<MediaProjectionManager>()?.let {
|
||||
navigator.openScreenSharingPermissionDialog(it.createScreenCaptureIntent(), screenSharingPermissionActivityResultLauncher)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleStopScreenSharingService() {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
screenCaptureServiceConnection.stopScreenCapturing()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val EXTRA_MODE = "EXTRA_MODE"
|
||||
private const val FRAGMENT_DIAL_PAD_TAG = "FRAGMENT_DIAL_PAD_TAG"
|
||||
|
|
|
@ -40,4 +40,6 @@ sealed class VectorCallViewActions : VectorViewModelAction {
|
|||
object CallTransferSelectionCancelled : VectorCallViewActions()
|
||||
data class CallTransferSelectionResult(val callTransferResult: CallTransferResult) : VectorCallViewActions()
|
||||
object TransferCall : VectorCallViewActions()
|
||||
object ToggleScreenSharing : VectorCallViewActions()
|
||||
object StartScreenSharing : VectorCallViewActions()
|
||||
}
|
||||
|
|
|
@ -31,7 +31,6 @@ sealed class VectorCallViewEvents : VectorViewEvents {
|
|||
object ShowDialPad : VectorCallViewEvents()
|
||||
object ShowCallTransferScreen : VectorCallViewEvents()
|
||||
object FailToTransfer : VectorCallViewEvents()
|
||||
// data class CallAnswered(val content: CallAnswerContent) : VectorCallViewEvents()
|
||||
// data class CallHangup(val content: CallHangupContent) : VectorCallViewEvents()
|
||||
// object CallAccepted : VectorCallViewEvents()
|
||||
object ShowScreenSharingPermissionDialog : VectorCallViewEvents()
|
||||
object StopScreenSharingService : VectorCallViewEvents()
|
||||
}
|
||||
|
|
|
@ -256,7 +256,10 @@ class VectorCallViewModel @AssistedInject constructor(
|
|||
|
||||
override fun handle(action: VectorCallViewActions) = withState { state ->
|
||||
when (action) {
|
||||
VectorCallViewActions.EndCall -> call?.endCall()
|
||||
VectorCallViewActions.EndCall -> {
|
||||
call?.endCall()
|
||||
_viewEvents.post(VectorCallViewEvents.StopScreenSharingService)
|
||||
}
|
||||
VectorCallViewActions.AcceptCall -> {
|
||||
setState {
|
||||
copy(callState = Loading())
|
||||
|
@ -341,6 +344,31 @@ class VectorCallViewModel @AssistedInject constructor(
|
|||
setState { VectorCallViewState(action.callArgs) }
|
||||
setupCallWithCurrentState()
|
||||
}
|
||||
is VectorCallViewActions.ToggleScreenSharing -> {
|
||||
handleToggleScreenSharing(state.isSharingScreen)
|
||||
}
|
||||
is VectorCallViewActions.StartScreenSharing -> {
|
||||
call?.startSharingScreen()
|
||||
setState {
|
||||
copy(isSharingScreen = true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleToggleScreenSharing(isSharingScreen: Boolean) {
|
||||
if (isSharingScreen) {
|
||||
call?.stopSharingScreen()
|
||||
setState {
|
||||
copy(isSharingScreen = false)
|
||||
}
|
||||
_viewEvents.post(
|
||||
VectorCallViewEvents.StopScreenSharingService
|
||||
)
|
||||
} else {
|
||||
_viewEvents.post(
|
||||
VectorCallViewEvents.ShowScreenSharingPermissionDialog
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -42,7 +42,8 @@ data class VectorCallViewState(
|
|||
val callInfo: CallInfo? = null,
|
||||
val formattedDuration: String = "",
|
||||
val canOpponentBeTransferred: Boolean = false,
|
||||
val transferee: TransfereeState = TransfereeState.NoTransferee
|
||||
val transferee: TransfereeState = TransfereeState.NoTransferee,
|
||||
val isSharingScreen: Boolean = false
|
||||
) : MavericksState {
|
||||
|
||||
sealed class TransfereeState {
|
||||
|
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.call.webrtc
|
||||
|
||||
import android.content.Intent
|
||||
import android.os.Binder
|
||||
import android.os.IBinder
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import im.vector.app.core.services.VectorService
|
||||
import im.vector.app.features.notifications.NotificationUtils
|
||||
import javax.inject.Inject
|
||||
|
||||
@AndroidEntryPoint
|
||||
class ScreenCaptureService : VectorService() {
|
||||
|
||||
@Inject lateinit var notificationUtils: NotificationUtils
|
||||
private val binder = LocalBinder()
|
||||
|
||||
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||
showStickyNotification()
|
||||
|
||||
return START_STICKY
|
||||
}
|
||||
|
||||
private fun showStickyNotification() {
|
||||
val notificationId = System.currentTimeMillis().toInt()
|
||||
val notification = notificationUtils.buildScreenSharingNotification()
|
||||
startForeground(notificationId, notification)
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder {
|
||||
return binder
|
||||
}
|
||||
|
||||
fun stopService() {
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
inner class LocalBinder : Binder() {
|
||||
fun getService(): ScreenCaptureService = this@ScreenCaptureService
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright (c) 2022 New Vector Ltd
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package im.vector.app.features.call.webrtc
|
||||
|
||||
import android.content.ComponentName
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.ServiceConnection
|
||||
import android.os.IBinder
|
||||
import javax.inject.Inject
|
||||
|
||||
class ScreenCaptureServiceConnection @Inject constructor(
|
||||
private val context: Context
|
||||
) : ServiceConnection {
|
||||
|
||||
private var isBound = false
|
||||
private var screenCaptureService: ScreenCaptureService? = null
|
||||
|
||||
fun bind() {
|
||||
if (!isBound) {
|
||||
Intent(context, ScreenCaptureService::class.java).also { intent ->
|
||||
context.bindService(intent, this, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun stopScreenCapturing() {
|
||||
screenCaptureService?.stopService()
|
||||
}
|
||||
|
||||
override fun onServiceConnected(className: ComponentName, binder: IBinder) {
|
||||
screenCaptureService = (binder as ScreenCaptureService.LocalBinder).getService()
|
||||
isBound = true
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(className: ComponentName) {
|
||||
isBound = false
|
||||
screenCaptureService = null
|
||||
}
|
||||
}
|
|
@ -770,6 +770,14 @@ class WebRtcCall(
|
|||
return currentCaptureFormat
|
||||
}
|
||||
|
||||
fun startSharingScreen() {
|
||||
// TODO. Will be handled within the next PR.
|
||||
}
|
||||
|
||||
fun stopSharingScreen() {
|
||||
// TODO. Will be handled within the next PR.
|
||||
}
|
||||
|
||||
private suspend fun release() {
|
||||
listeners.clear()
|
||||
mxCall.removeListener(this)
|
||||
|
|
|
@ -600,4 +600,9 @@ class DefaultNavigator @Inject constructor(
|
|||
roomEncryptionTrustLevel = threadTimelineArgs.roomEncryptionTrustLevel
|
||||
)))
|
||||
}
|
||||
|
||||
override fun openScreenSharingPermissionDialog(screenCaptureIntent: Intent,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>) {
|
||||
activityResultLauncher.launch(screenCaptureIntent)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -168,4 +168,9 @@ interface Navigator {
|
|||
fun openThread(context: Context, threadTimelineArgs: ThreadTimelineArgs, eventIdToNavigate: String? = null)
|
||||
|
||||
fun openThreadList(context: Context, threadTimelineArgs: ThreadTimelineArgs)
|
||||
|
||||
fun openScreenSharingPermissionDialog(
|
||||
screenCaptureIntent: Intent,
|
||||
activityResultLauncher: ActivityResultLauncher<Intent>
|
||||
)
|
||||
}
|
||||
|
|
|
@ -535,6 +535,20 @@ class NotificationUtils @Inject constructor(private val context: Context,
|
|||
.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a notification that indicates the application is capturing the screen.
|
||||
*/
|
||||
fun buildScreenSharingNotification(): Notification {
|
||||
return NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID)
|
||||
.setContentTitle(stringProvider.getString(R.string.screen_sharing_notification_title))
|
||||
.setContentText(stringProvider.getString(R.string.screen_sharing_notification_description))
|
||||
.setSmallIcon(R.drawable.ic_share_screen)
|
||||
.setColor(ThemeUtils.getColor(context, android.R.attr.colorPrimary))
|
||||
.setCategory(NotificationCompat.CATEGORY_SERVICE)
|
||||
.setContentIntent(buildOpenHomePendingIntentForSummary())
|
||||
.build()
|
||||
}
|
||||
|
||||
fun buildDownloadFileNotification(uri: Uri, fileName: String, mimeType: String): Notification {
|
||||
return NotificationCompat.Builder(context, SILENT_NOTIFICATION_CHANNEL_ID)
|
||||
.setGroup(stringProvider.getString(R.string.app_name))
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M3.4,4C2.0745,4 1,5.0745 1,6.4V17.6C1,18.9255 2.0745,20 3.4,20H20.6C21.9255,20 23,18.9255 23,17.6V6.4C23,5.0745 21.9255,4 20.6,4H3.4ZM11.9999,16C11.6464,16 11.3599,15.7135 11.3599,15.36V10.2049L9.3841,12.2166C9.1364,12.4688 8.7348,12.4688 8.4872,12.2166C8.2395,11.9644 8.2395,11.5556 8.4872,11.3034L11.5514,8.1834C11.7991,7.9312 12.2007,7.9312 12.4484,8.1834L15.5126,11.3034C15.7603,11.5556 15.7603,11.9644 15.5126,12.2166C15.265,12.4688 14.8634,12.4688 14.6157,12.2166L12.6399,10.2049V15.36C12.6399,15.7135 12.3534,16 11.9999,16Z"
|
||||
android:fillColor="#737D8C"
|
||||
android:fillType="evenOdd"/>
|
||||
</vector>
|
|
@ -7,6 +7,15 @@
|
|||
android:background="?colorSurface"
|
||||
android:orientation="vertical">
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/callControlsShareScreen"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
app:actionTitle="@string/call_start_screen_sharing"
|
||||
app:leftIcon="@drawable/ic_share_screen"
|
||||
app:tint="?vctr_content_primary"
|
||||
app:titleTextColor="?vctr_content_primary" />
|
||||
|
||||
<im.vector.app.core.ui.views.BottomSheetActionButton
|
||||
android:id="@+id/callControlsSwitchCamera"
|
||||
android:layout_width="match_parent"
|
||||
|
|
|
@ -483,6 +483,8 @@
|
|||
<string name="call_camera_back">Back</string>
|
||||
<string name="call_format_turn_hd_off">Turn HD off</string>
|
||||
<string name="call_format_turn_hd_on">Turn HD on</string>
|
||||
<string name="call_start_screen_sharing">Share screen</string>
|
||||
<string name="call_stop_screen_sharing">Stop screen sharing</string>
|
||||
|
||||
<string name="option_send_files">Send files</string>
|
||||
<string name="option_send_sticker">Send sticker</string>
|
||||
|
@ -3030,4 +3032,8 @@
|
|||
<string name="room_message_notify_everyone">Notify the whole room</string>
|
||||
<string name="room_message_autocomplete_users">Users</string>
|
||||
<string name="room_message_autocomplete_notification">Room notification</string>
|
||||
|
||||
<!-- Screen sharing -->
|
||||
<string name="screen_sharing_notification_title">${app_name} Screen Sharing</string>
|
||||
<string name="screen_sharing_notification_description">Screen sharing is in progress</string>
|
||||
</resources>
|
||||
|
|
Loading…
Reference in New Issue