Merge pull request #5811 from vector-im/feature/ons/voip_screen_sharing_permission

VoIP Screen Sharing Permission
This commit is contained in:
Onuray Sahin 2022-04-21 16:03:35 +03:00 committed by GitHub
commit 8eaa2f8dfb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
19 changed files with 263 additions and 10 deletions

1
changelog.d/5811.feature Normal file
View File

@ -0,0 +1 @@
VoIP Screen Sharing Permission

View File

@ -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")
}

View File

@ -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

View File

@ -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
}

View File

@ -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)
}
}

View File

@ -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"

View File

@ -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()
}

View File

@ -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()
}

View File

@ -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
)
}
}

View File

@ -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 {

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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)

View File

@ -600,4 +600,9 @@ class DefaultNavigator @Inject constructor(
roomEncryptionTrustLevel = threadTimelineArgs.roomEncryptionTrustLevel
)))
}
override fun openScreenSharingPermissionDialog(screenCaptureIntent: Intent,
activityResultLauncher: ActivityResultLauncher<Intent>) {
activityResultLauncher.launch(screenCaptureIntent)
}
}

View File

@ -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>
)
}

View File

@ -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))

View File

@ -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>

View File

@ -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"

View File

@ -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>