diff --git a/changelog.d/5811.feature b/changelog.d/5811.feature new file mode 100644 index 00000000000..12111e323be --- /dev/null +++ b/changelog.d/5811.feature @@ -0,0 +1 @@ +VoIP Screen Sharing Permission \ No newline at end of file diff --git a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt index bc716779ac4..07fab8a58df 100644 --- a/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt +++ b/vector/src/debug/java/im/vector/app/features/debug/features/DebugVectorFeatures.kt @@ -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 override(value: T?, key: Preferences.Key) = 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") } diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index eada6642168..20b7c4908ae 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -375,6 +375,12 @@ android:exported="false" android:foregroundServiceType="location" /> + + () { @@ -34,6 +36,8 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment(), 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(), 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(), 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()?.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" diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt index d1ed9618141..c84f733b9a7 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt index 256a373d440..52510f6f8bc 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewEvents.kt @@ -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() } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt index a80055d8e75..3b7baef155f 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt @@ -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 + ) } } diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt index 2d33cbf9b9b..2cd819b5f57 100644 --- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt +++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt @@ -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 { diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureService.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureService.kt new file mode 100644 index 00000000000..f1a4975751b --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureService.kt @@ -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 + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureServiceConnection.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureServiceConnection.kt new file mode 100644 index 00000000000..922e9676a86 --- /dev/null +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/ScreenCaptureServiceConnection.kt @@ -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 + } +} diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt index 90088c84750..bc8ae51a88c 100644 --- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt +++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt @@ -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) diff --git a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt index c8e9ee966ab..086a40636cf 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/DefaultNavigator.kt @@ -600,4 +600,9 @@ class DefaultNavigator @Inject constructor( roomEncryptionTrustLevel = threadTimelineArgs.roomEncryptionTrustLevel ))) } + + override fun openScreenSharingPermissionDialog(screenCaptureIntent: Intent, + activityResultLauncher: ActivityResultLauncher) { + activityResultLauncher.launch(screenCaptureIntent) + } } diff --git a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt index 310105bd955..41b53741680 100644 --- a/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt +++ b/vector/src/main/java/im/vector/app/features/navigation/Navigator.kt @@ -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 + ) } diff --git a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt index 161b58d53d2..e44f42d5cda 100755 --- a/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt +++ b/vector/src/main/java/im/vector/app/features/notifications/NotificationUtils.kt @@ -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)) diff --git a/vector/src/main/res/drawable/ic_share_screen.xml b/vector/src/main/res/drawable/ic_share_screen.xml new file mode 100644 index 00000000000..5e3caa69871 --- /dev/null +++ b/vector/src/main/res/drawable/ic_share_screen.xml @@ -0,0 +1,10 @@ + + + diff --git a/vector/src/main/res/layout/bottom_sheet_call_controls.xml b/vector/src/main/res/layout/bottom_sheet_call_controls.xml index e751ac412ae..516dc29fa3f 100644 --- a/vector/src/main/res/layout/bottom_sheet_call_controls.xml +++ b/vector/src/main/res/layout/bottom_sheet_call_controls.xml @@ -7,6 +7,15 @@ android:background="?colorSurface" android:orientation="vertical"> + + Back Turn HD off Turn HD on + Share screen + Stop screen sharing Send files Send sticker @@ -3030,4 +3032,8 @@ Notify the whole room Users Room notification + + + ${app_name} Screen Sharing + Screen sharing is in progress