From 125135c2505479a637658a13d8b1474d644cf997 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 4 Jul 2022 13:53:10 +0300 Subject: [PATCH 01/20] Add element call widget type. --- .../android/sdk/api/session/widgets/model/WidgetType.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt index ee098f9bf2..24f3a155ed 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt @@ -28,7 +28,8 @@ private val DEFINED_TYPES by lazy { WidgetType.StickerPicker, WidgetType.Grafana, WidgetType.Custom, - WidgetType.IntegrationManager + WidgetType.IntegrationManager, + WidgetType.ElementCall ) } @@ -47,6 +48,7 @@ sealed class WidgetType(open val preferred: String, open val legacy: String = pr object Grafana : WidgetType("m.grafana") object Custom : WidgetType("m.custom") object IntegrationManager : WidgetType("m.integration_manager") + object ElementCall : WidgetType("io.element.call") data class Fallback(override val preferred: String) : WidgetType(preferred) fun matches(type: String): Boolean { From 61f05e78a3add57c3dd3ddbcd589517aa7411d74 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Mon, 4 Jul 2022 17:35:07 +0300 Subject: [PATCH 02/20] Create custom widget args for element call. --- .../im/vector/app/features/navigation/DefaultNavigator.kt | 3 +++ .../im/vector/app/features/widgets/WidgetArgsBuilder.kt | 7 +++++++ 2 files changed, 10 insertions(+) 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 291eee307f..38edaa27e2 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 @@ -465,6 +465,9 @@ class DefaultNavigator @Inject constructor( val enableVideo = options?.get(JitsiCallViewModel.ENABLE_VIDEO_OPTION) == true context.startActivity(VectorJitsiActivity.newIntent(context, roomId = roomId, widgetId = widget.widgetId, enableVideo = enableVideo)) } + } else if (widget.type is WidgetType.ElementCall) { + val widgetArgs = widgetArgsBuilder.buildElementCallWidgetArgs(roomId, widget) + context.startActivity(WidgetActivity.newIntent(context, widgetArgs)) } else { val widgetArgs = widgetArgsBuilder.buildRoomWidgetArgs(roomId, widget) context.startActivity(WidgetActivity.newIntent(context, widgetArgs)) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetArgsBuilder.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetArgsBuilder.kt index 777bd9cc7e..83ea100cb6 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetArgsBuilder.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetArgsBuilder.kt @@ -78,6 +78,13 @@ class WidgetArgsBuilder @Inject constructor( ) } + fun buildElementCallWidgetArgs(roomId: String, widget: Widget): WidgetArgs { + return buildRoomWidgetArgs(roomId, widget) + .copy( + kind = WidgetKind.ELEMENT_CALL + ) + } + @Suppress("UNCHECKED_CAST") private fun Map.filterNotNull(): Map { return filterValues { it != null } as Map From b3a8052a519704a4a2fb5616ed218f0b02aed8c1 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 21 Jul 2022 12:30:55 +0300 Subject: [PATCH 03/20] Add element call widget type. --- .../java/im/vector/app/features/widgets/WidgetViewState.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewState.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewState.kt index 2d98f734dd..7619fea766 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewState.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewState.kt @@ -33,7 +33,8 @@ enum class WidgetStatus { enum class WidgetKind(@StringRes val nameRes: Int, val screenId: String?) { ROOM(R.string.room_widget_activity_title, null), STICKER_PICKER(R.string.title_activity_choose_sticker, WidgetType.StickerPicker.preferred), - INTEGRATION_MANAGER(0, null); + INTEGRATION_MANAGER(0, null), + ELEMENT_CALL(0, null); fun isAdmin(): Boolean { return this == STICKER_PICKER || this == INTEGRATION_MANAGER From 85aba894d6849b0416cb71e5b9012ec001a572f4 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Tue, 5 Jul 2022 16:06:20 +0300 Subject: [PATCH 04/20] Support picture-in-picture mode for element call widget. --- vector/src/main/AndroidManifest.xml | 3 ++- .../app/features/widgets/WidgetActivity.kt | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/vector/src/main/AndroidManifest.xml b/vector/src/main/AndroidManifest.xml index 1c104f3bbf..ca61e077fb 100644 --- a/vector/src/main/AndroidManifest.xml +++ b/vector/src/main/AndroidManifest.xml @@ -308,7 +308,8 @@ + android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" + android:supportsPictureInPicture="true" /> diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt index 954f622801..dc756ec7d7 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt @@ -17,8 +17,11 @@ package im.vector.app.features.widgets import android.app.Activity +import android.app.PictureInPictureParams import android.content.Context import android.content.Intent +import android.os.Build +import android.util.Rational import androidx.core.view.isVisible import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel @@ -119,6 +122,24 @@ class WidgetActivity : VectorBaseActivity() { } } + override fun onUserLeaveHint() { + super.onUserLeaveHint() + val widgetArgs: WidgetArgs? = intent?.extras?.getParcelable(Mavericks.KEY_ARG) + if (widgetArgs?.kind == WidgetKind.ELEMENT_CALL) { + enterPictureInPicture() + } + } + + private fun enterPictureInPicture() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + val aspectRatio = Rational(resources.getDimensionPixelSize(R.dimen.call_pip_width), resources.getDimensionPixelSize(R.dimen.call_pip_height)) + val params = PictureInPictureParams.Builder() + .setAspectRatio(aspectRatio) + .build() + enterPictureInPictureMode(params) + } + } + private fun handleClose(event: WidgetViewEvents.Close) { if (event.content != null) { val intent = createResultIntent(event.content) From a0eb2e733c2b4e8a861ce7818c6ee1be869bce3e Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 6 Jul 2022 14:54:24 +0300 Subject: [PATCH 05/20] Skip widget permissions for element call. --- .../app/features/widgets/WidgetActivity.kt | 47 +++++++++++-------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt index dc756ec7d7..399c2d392b 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt @@ -85,29 +85,36 @@ class WidgetActivity : VectorBaseActivity() { } } - permissionViewModel.observeViewEvents { - when (it) { - is RoomWidgetPermissionViewEvents.Close -> finish() + // Trust element call widget by default + if (widgetArgs.kind == WidgetKind.ELEMENT_CALL) { + if (supportFragmentManager.findFragmentByTag(WIDGET_FRAGMENT_TAG) == null) { + addFragment(views.fragmentContainer, WidgetFragment::class.java, widgetArgs, WIDGET_FRAGMENT_TAG) + } + } else { + permissionViewModel.observeViewEvents { + when (it) { + is RoomWidgetPermissionViewEvents.Close -> finish() + } } - } - viewModel.onEach(WidgetViewState::status) { ws -> - when (ws) { - WidgetStatus.UNKNOWN -> { - } - WidgetStatus.WIDGET_NOT_ALLOWED -> { - val dFrag = supportFragmentManager.findFragmentByTag(WIDGET_PERMISSION_FRAGMENT_TAG) as? RoomWidgetPermissionBottomSheet - if (dFrag != null && dFrag.dialog?.isShowing == true && !dFrag.isRemoving) { - return@onEach - } else { - RoomWidgetPermissionBottomSheet - .newInstance(widgetArgs) - .show(supportFragmentManager, WIDGET_PERMISSION_FRAGMENT_TAG) + viewModel.onEach(WidgetViewState::status) { ws -> + when (ws) { + WidgetStatus.UNKNOWN -> { } - } - WidgetStatus.WIDGET_ALLOWED -> { - if (supportFragmentManager.findFragmentByTag(WIDGET_FRAGMENT_TAG) == null) { - addFragment(views.fragmentContainer, WidgetFragment::class.java, widgetArgs, WIDGET_FRAGMENT_TAG) + WidgetStatus.WIDGET_NOT_ALLOWED -> { + val dFrag = supportFragmentManager.findFragmentByTag(WIDGET_PERMISSION_FRAGMENT_TAG) as? RoomWidgetPermissionBottomSheet + if (dFrag != null && dFrag.dialog?.isShowing == true && !dFrag.isRemoving) { + return@onEach + } else { + RoomWidgetPermissionBottomSheet + .newInstance(widgetArgs) + .show(supportFragmentManager, WIDGET_PERMISSION_FRAGMENT_TAG) + } + } + WidgetStatus.WIDGET_ALLOWED -> { + if (supportFragmentManager.findFragmentByTag(WIDGET_FRAGMENT_TAG) == null) { + addFragment(views.fragmentContainer, WidgetFragment::class.java, widgetArgs, WIDGET_FRAGMENT_TAG) + } } } } From f5ec7a312fcfd451538acd5bed1d71795477512e Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Wed, 6 Jul 2022 15:22:28 +0300 Subject: [PATCH 06/20] Auto grant WebView permissions if they are already granted system level. --- .../vector/app/core/utils/PermissionsTools.kt | 27 +++++++++++++++++++ .../app/features/widgets/WidgetFragment.kt | 2 +- .../features/widgets/webview/WidgetWebView.kt | 10 +++++-- 3 files changed, 36 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt index 9ad95d3c55..051a6cd8ce 100644 --- a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt +++ b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt @@ -19,6 +19,7 @@ package im.vector.app.core.utils import android.Manifest import android.app.Activity import android.content.pm.PackageManager +import android.webkit.PermissionRequest import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts @@ -137,6 +138,32 @@ fun checkPermissions( } } +/** + * Checks if required WebView permissions are already granted system level. + * @param activity the calling Activity that is requesting the permissions (or fragment parent) + * @param request WebView permission request of onPermissionRequest function + * @return true if WebView permissions are already granted, false otherwise + */ +fun checkWebViewPermissions(activity: Activity, request: PermissionRequest): Boolean { + return request.resources.all { + when (it) { + PermissionRequest.RESOURCE_AUDIO_CAPTURE -> { + PERMISSIONS_FOR_AUDIO_IP_CALL.all { permission -> + ContextCompat.checkSelfPermission(activity.applicationContext, permission) == PackageManager.PERMISSION_GRANTED + } + } + PermissionRequest.RESOURCE_VIDEO_CAPTURE -> { + PERMISSIONS_FOR_VIDEO_IP_CALL.all { permission -> + ContextCompat.checkSelfPermission(activity.applicationContext, permission) == PackageManager.PERMISSION_GRANTED + } + } + else -> { + false + } + } + } +} + /** * To be call after the permission request. * diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt index 5501031e92..a85e45b074 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt @@ -81,7 +81,7 @@ class WidgetFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - views.widgetWebView.setupForWidget(this) + views.widgetWebView.setupForWidget(requireActivity(), this) if (fragmentArgs.kind.isAdmin()) { viewModel.getPostAPIMediator().setWebView(views.widgetWebView) } diff --git a/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt b/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt index 0207987ca3..505645a668 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt @@ -17,18 +17,20 @@ package im.vector.app.features.widgets.webview import android.annotation.SuppressLint +import android.app.Activity import android.view.ViewGroup import android.webkit.CookieManager import android.webkit.PermissionRequest import android.webkit.WebChromeClient import android.webkit.WebView import im.vector.app.R +import im.vector.app.core.utils.checkWebViewPermissions import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.webview.VectorWebViewClient import im.vector.app.features.webview.WebEventListener @SuppressLint("NewApi") -fun WebView.setupForWidget(eventListener: WebEventListener) { +fun WebView.setupForWidget(activity: Activity, eventListener: WebEventListener) { // xml value seems ignored setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorSurface)) @@ -59,7 +61,11 @@ fun WebView.setupForWidget(eventListener: WebEventListener) { // Permission requests webChromeClient = object : WebChromeClient() { override fun onPermissionRequest(request: PermissionRequest) { - eventListener.onPermissionRequest(request) + if (checkWebViewPermissions(activity, request)) { + request.grant(request.resources) + } else { + eventListener.onPermissionRequest(request) + } } } webViewClient = VectorWebViewClient(eventListener) From fd3b082a2c5bc55ebcfc7dcc3e6ac780e9d7aeac Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 21 Jul 2022 13:13:14 +0300 Subject: [PATCH 07/20] Open element call widget. --- .../features/home/room/detail/RoomDetailViewEvents.kt | 1 + .../features/home/room/detail/RoomDetailViewState.kt | 2 ++ .../app/features/home/room/detail/TimelineFragment.kt | 10 ++++++++++ .../app/features/widgets/webview/WidgetWebView.kt | 2 ++ 4 files changed, 15 insertions(+) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt index dcfee2d919..3af849e965 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewEvents.kt @@ -84,4 +84,5 @@ sealed class RoomDetailViewEvents : VectorViewEvents { data class StartChatEffect(val type: ChatEffect) : RoomDetailViewEvents() object StopChatEffects : RoomDetailViewEvents() object RoomReplacementStarted : RoomDetailViewEvents() + object OpenElementCallWidget : RoomDetailViewEvents() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index 8500d1ed96..7aa7d5a877 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -102,6 +102,8 @@ data class RoomDetailViewState( // It can differs for a short period of time on the JitsiState as its computed async. fun hasActiveJitsiWidget() = activeRoomWidgets()?.any { it.type == WidgetType.Jitsi && it.isActive }.orFalse() + fun hasActiveElementCallWidget() = activeRoomWidgets()?.any { it.type == WidgetType.ElementCall && it.isActive }.orFalse() + fun isDm() = asyncRoomSummary()?.isDirect == true fun isThreadTimeline() = rootThreadEventId != null diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 31c1004ef9..1d77e4c83a 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -498,6 +498,7 @@ class TimelineFragment @Inject constructor( RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects() is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it) RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement() + RoomDetailViewEvents.OpenElementCallWidget -> handleOpenElementCallWidget() } } @@ -2653,6 +2654,15 @@ class TimelineFragment @Inject constructor( .show(childFragmentManager, "ROOM_WIDGETS_BOTTOM_SHEET") } + private fun handleOpenElementCallWidget() = withState(timelineViewModel) { state -> + state + .activeRoomWidgets() + ?.find { it.type == WidgetType.ElementCall } + ?.also { widget -> + navigator.openRoomWidget(requireContext(), state.roomId, widget) + } + } + override fun onTapToReturnToCall() { callManager.getCurrentCall()?.let { call -> VectorCallActivity.newIntent( diff --git a/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt b/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt index 505645a668..2f3b449b1a 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt @@ -58,6 +58,8 @@ fun WebView.setupForWidget(activity: Activity, eventListener: WebEventListener) settings.displayZoomControls = false + settings.mediaPlaybackRequiresUserGesture = false + // Permission requests webChromeClient = object : WebChromeClient() { override fun onPermissionRequest(request: PermissionRequest) { From 5c55263cf69fcbb1c25b1bad51f4094bd017e3ce Mon Sep 17 00:00:00 2001 From: Johannes Marbach Date: Thu, 7 Jul 2022 08:46:47 +0200 Subject: [PATCH 08/20] Suppress webview / checkbox permission dialog Signed-off-by: Johannes Marbach --- .../app/features/widgets/WidgetFragment.kt | 3 +- .../widgets/webview/WebviewPermissionUtils.kt | 49 +++++++++++++------ 2 files changed, 37 insertions(+), 15 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt index a85e45b074..a34e06aace 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt @@ -298,7 +298,8 @@ class WidgetFragment @Inject constructor( request = request, context = requireContext(), activity = requireActivity(), - activityResultLauncher = permissionResultLauncher + activityResultLauncher = permissionResultLauncher, + autoApprove = fragmentArgs.kind == WidgetKind.ELEMENT_CALL ) } diff --git a/vector/src/main/java/im/vector/app/features/widgets/webview/WebviewPermissionUtils.kt b/vector/src/main/java/im/vector/app/features/widgets/webview/WebviewPermissionUtils.kt index fa7b842ab9..44af4ec335 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/webview/WebviewPermissionUtils.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/webview/WebviewPermissionUtils.kt @@ -41,11 +41,22 @@ class WebviewPermissionUtils @Inject constructor( request: PermissionRequest, context: Context, activity: FragmentActivity, - activityResultLauncher: ActivityResultLauncher> + activityResultLauncher: ActivityResultLauncher>, + autoApprove: Boolean = false ) { + if (autoApprove) { + onPermissionsSelected( + permissions = request.resources.toList(), + request = request, + activity = activity, + activityResultLauncher = activityResultLauncher) + return + } + val allowedPermissions = request.resources.map { it to false }.toMutableList() + MaterialAlertDialogBuilder(context) .setTitle(title) .setMultiChoiceItems( @@ -54,21 +65,10 @@ class WebviewPermissionUtils @Inject constructor( allowedPermissions[which] = allowedPermissions[which].first to isChecked } .setPositiveButton(R.string.room_widget_resource_grant_permission) { _, _ -> - permissionRequest = request - selectedPermissions = allowedPermissions.mapNotNull { perm -> + val permissions = allowedPermissions.mapNotNull { perm -> perm.first.takeIf { perm.second } } - - val requiredAndroidPermissions = selectedPermissions.mapNotNull { permission -> - webPermissionToAndroidPermission(permission) - } - - // When checkPermissions returns false, some of the required Android permissions will - // have to be requested and the flow completes asynchronously via onPermissionResult - if (checkPermissions(requiredAndroidPermissions, activity, activityResultLauncher)) { - request.grant(selectedPermissions.toTypedArray()) - reset() - } + onPermissionsSelected(permissions, request, activity, activityResultLauncher) } .setNegativeButton(R.string.room_widget_resource_decline_permission) { _, _ -> request.deny() @@ -76,6 +76,27 @@ class WebviewPermissionUtils @Inject constructor( .show() } + private fun onPermissionsSelected( + permissions: List, + request: PermissionRequest, + activity: FragmentActivity, + activityResultLauncher: ActivityResultLauncher>, + ) { + permissionRequest = request + selectedPermissions = permissions + + val requiredAndroidPermissions = selectedPermissions.mapNotNull { permission -> + webPermissionToAndroidPermission(permission) + } + + // When checkPermissions returns false, some of the required Android permissions will + // have to be requested and the flow completes asynchronously via onPermissionResult + if (checkPermissions(requiredAndroidPermissions, activity, activityResultLauncher)) { + request.grant(selectedPermissions.toTypedArray()) + reset() + } + } + fun onPermissionResult(result: Map) { if (permissionRequest == null) { fatalError( From 95783506a243f199ce4f964852e0b00e288b1b8a Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 7 Jul 2022 13:19:40 +0300 Subject: [PATCH 09/20] Stop javascript for non element call widgets. --- .../java/im/vector/app/features/widgets/WidgetFragment.kt | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt index a34e06aace..390bd3c88b 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt @@ -131,9 +131,11 @@ class WidgetFragment @Inject constructor( override fun onPause() { super.onPause() - views.widgetWebView.let { - it.pauseTimers() - it.onPause() + if (fragmentArgs.kind != WidgetKind.ELEMENT_CALL) { + views.widgetWebView.let { + it.pauseTimers() + it.onPause() + } } } From 85b5713b244775f535c37e2274836d5b4e9fcf7a Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 8 Jul 2022 15:06:44 +0300 Subject: [PATCH 10/20] Add a hangup button in pip mode. --- .../app/features/widgets/WidgetAction.kt | 1 + .../app/features/widgets/WidgetActivity.kt | 54 +++++++++++++++++-- .../app/features/widgets/WidgetViewModel.kt | 5 ++ 3 files changed, 55 insertions(+), 5 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetAction.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetAction.kt index b72ea68b7f..f525af6109 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetAction.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetAction.kt @@ -26,4 +26,5 @@ sealed class WidgetAction : VectorViewModelAction { object DeleteWidget : WidgetAction() object RevokeWidget : WidgetAction() object OnTermsReviewed : WidgetAction() + object HangupElementCall : WidgetAction() } diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt index 399c2d392b..dc1bc2c4ff 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt @@ -17,11 +17,18 @@ package im.vector.app.features.widgets import android.app.Activity +import android.app.PendingIntent import android.app.PictureInPictureParams +import android.app.RemoteAction +import android.content.BroadcastReceiver import android.content.Context import android.content.Intent +import android.content.IntentFilter +import android.content.res.Configuration +import android.graphics.drawable.Icon import android.os.Build import android.util.Rational +import androidx.annotation.RequiresApi import androidx.core.view.isVisible import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel @@ -43,6 +50,10 @@ class WidgetActivity : VectorBaseActivity() { private const val WIDGET_FRAGMENT_TAG = "WIDGET_FRAGMENT_TAG" private const val WIDGET_PERMISSION_FRAGMENT_TAG = "WIDGET_PERMISSION_FRAGMENT_TAG" private const val EXTRA_RESULT = "EXTRA_RESULT" + private const val REQUEST_CODE_HANGUP = 1 + private const val ACTION_MEDIA_CONTROL = "MEDIA_CONTROL" + private const val EXTRA_CONTROL_TYPE = "EXTRA_CONTROL_TYPE" + private const val CONTROL_TYPE_HANGUP = 2 fun newIntent(context: Context, args: WidgetArgs): Intent { return Intent(context, WidgetActivity::class.java).apply { @@ -139,11 +150,44 @@ class WidgetActivity : VectorBaseActivity() { private fun enterPictureInPicture() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - val aspectRatio = Rational(resources.getDimensionPixelSize(R.dimen.call_pip_width), resources.getDimensionPixelSize(R.dimen.call_pip_height)) - val params = PictureInPictureParams.Builder() - .setAspectRatio(aspectRatio) - .build() - enterPictureInPictureMode(params) + createElementCallPipParams()?.let { + enterPictureInPictureMode(it) + } + } + } + + @RequiresApi(Build.VERSION_CODES.O) + private fun createElementCallPipParams(): PictureInPictureParams? { + val actions = mutableListOf() + val intent = Intent(ACTION_MEDIA_CONTROL).putExtra(EXTRA_CONTROL_TYPE, CONTROL_TYPE_HANGUP) + val pendingIntent = PendingIntent.getBroadcast(this, REQUEST_CODE_HANGUP, intent, 0) + val icon = Icon.createWithResource(this, R.drawable.ic_call_hangup) + actions.add(RemoteAction(icon, getString(R.string.call_notification_hangup), getString(R.string.call_notification_hangup), pendingIntent)) + + val aspectRatio = Rational(resources.getDimensionPixelSize(R.dimen.call_pip_width), resources.getDimensionPixelSize(R.dimen.call_pip_height)) + return PictureInPictureParams.Builder() + .setAspectRatio(aspectRatio) + .setActions(actions) + .build() + } + + private var hangupBroadcastReceiver: BroadcastReceiver? = null + override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration?) { + super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) + if (isInPictureInPictureMode) { + hangupBroadcastReceiver = object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent?.action == ACTION_MEDIA_CONTROL) { + val controlType = intent.getIntExtra(EXTRA_CONTROL_TYPE, 0) + if (controlType == CONTROL_TYPE_HANGUP) { + viewModel.handle(WidgetAction.HangupElementCall) + } + } + } + } + registerReceiver(hangupBroadcastReceiver, IntentFilter(ACTION_MEDIA_CONTROL)) + } else { + unregisterReceiver(hangupBroadcastReceiver) } } diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt index b3f4712815..17dc244f4c 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt @@ -147,9 +147,14 @@ class WidgetViewModel @AssistedInject constructor( WidgetAction.DeleteWidget -> handleDeleteWidget() WidgetAction.RevokeWidget -> handleRevokeWidget() WidgetAction.OnTermsReviewed -> loadFormattedUrl(forceFetchToken = false) + WidgetAction.HangupElementCall -> handleHangupElementCall() } } + private fun handleHangupElementCall() { + _viewEvents.post(WidgetViewEvents.Close()) + } + private fun handleRevokeWidget() { viewModelScope.launch { val widgetId = initialState.widgetId ?: return@launch From 7dfe5264cde3cf5669520d980f105057afcf77a1 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 21 Jul 2022 15:03:44 +0300 Subject: [PATCH 11/20] Fix picture in picture mode. --- .../vector/app/features/widgets/WidgetActivity.kt | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt index dc1bc2c4ff..90f1ab4bbb 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt @@ -29,9 +29,12 @@ import android.graphics.drawable.Icon import android.os.Build import android.util.Rational import androidx.annotation.RequiresApi +import androidx.core.app.PictureInPictureModeChangedInfo +import androidx.core.util.Consumer import androidx.core.view.isVisible import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel +import com.airbnb.mvrx.withState import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.extensions.addFragment @@ -99,6 +102,7 @@ class WidgetActivity : VectorBaseActivity() { // Trust element call widget by default if (widgetArgs.kind == WidgetKind.ELEMENT_CALL) { if (supportFragmentManager.findFragmentByTag(WIDGET_FRAGMENT_TAG) == null) { + addOnPictureInPictureModeChangedListener(pictureInPictureModeChangedInfoConsumer) addFragment(views.fragmentContainer, WidgetFragment::class.java, widgetArgs, WIDGET_FRAGMENT_TAG) } } else { @@ -148,6 +152,11 @@ class WidgetActivity : VectorBaseActivity() { } } + override fun onDestroy() { + removeOnPictureInPictureModeChangedListener(pictureInPictureModeChangedInfoConsumer) + super.onDestroy() + } + private fun enterPictureInPicture() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { createElementCallPipParams()?.let { @@ -172,8 +181,10 @@ class WidgetActivity : VectorBaseActivity() { } private var hangupBroadcastReceiver: BroadcastReceiver? = null - override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration?) { - super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig) + + private val pictureInPictureModeChangedInfoConsumer = Consumer { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return@Consumer + if (isInPictureInPictureMode) { hangupBroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { From 0d4697b7e1b7e7581308df4b67b76fe552a24573 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 21 Jul 2022 15:55:51 +0300 Subject: [PATCH 12/20] Show call icon if there is an active element call widget. --- .../app/features/home/room/detail/RoomDetailViewState.kt | 3 ++- .../vector/app/features/home/room/detail/TimelineFragment.kt | 4 ++-- .../vector/app/features/home/room/detail/TimelineViewModel.kt | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index 7aa7d5a877..b47b46c152 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -93,7 +93,8 @@ data class RoomDetailViewState( return asyncRoomSummary.invoke()?.isDirect ?: true || // When there is only one member, a warning will be displayed when the user // clicks on the menu item to start a call - asyncRoomSummary.invoke()?.joinedMembersCount == 1 + asyncRoomSummary.invoke()?.joinedMembersCount == 1 || + hasActiveElementCallWidget() } fun isSearchAvailable() = asyncRoomSummary()?.isEncrypted == false diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 1d77e4c83a..27ba12e4d7 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -1088,8 +1088,8 @@ class TimelineFragment @Inject constructor( val hasCallInRoom = callManager.getCallsByRoomId(state.roomId).isNotEmpty() || state.jitsiState.hasJoined val callButtonsEnabled = !hasCallInRoom && when (state.asyncRoomSummary.invoke()?.joinedMembersCount) { 1 -> false - 2 -> state.isAllowedToStartWebRTCCall - else -> state.isAllowedToManageWidgets + 2 -> state.isAllowedToStartWebRTCCall || state.hasActiveElementCallWidget() + else -> state.isAllowedToManageWidgets || state.hasActiveElementCallWidget() } setOf(R.id.voice_call, R.id.video_call).forEach { menu.findItem(it).icon?.alpha = if (callButtonsEnabled) 0xFF else 0x40 diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index e3ea8a0826..71b6b22aed 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -752,7 +752,7 @@ class TimelineViewModel @AssistedInject constructor( R.id.timeline_setting -> true R.id.invite -> state.canInvite R.id.open_matrix_apps -> true - R.id.voice_call -> state.isCallOptionAvailable() + R.id.voice_call -> state.isCallOptionAvailable() || state.hasActiveElementCallWidget() R.id.video_call -> state.isCallOptionAvailable() || state.jitsiState.confId == null || state.jitsiState.hasJoined // Show Join conference button only if there is an active conf id not joined. Otherwise fallback to default video disabled. ^ R.id.join_conference -> !state.isCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined From da780ac56f2720a547bb5e9872b514b4aad3fae0 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 21 Jul 2022 16:26:10 +0300 Subject: [PATCH 13/20] Open element call widget. --- .../app/features/home/room/detail/RoomDetailAction.kt | 2 ++ .../app/features/home/room/detail/RoomDetailViewState.kt | 3 +-- .../features/home/room/detail/StartCallActionsHandler.kt | 5 +++++ .../app/features/home/room/detail/TimelineFragment.kt | 9 ++++----- .../app/features/home/room/detail/TimelineViewModel.kt | 7 +++++++ 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt index 64670c73ac..c1e3b58a80 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailAction.kt @@ -117,4 +117,6 @@ sealed class RoomDetailAction : VectorViewModelAction { // Live Location object StopLiveLocationSharing : RoomDetailAction() + + object OpenElementCallWidget : RoomDetailAction() } diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt index b47b46c152..7aa7d5a877 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewState.kt @@ -93,8 +93,7 @@ data class RoomDetailViewState( return asyncRoomSummary.invoke()?.isDirect ?: true || // When there is only one member, a warning will be displayed when the user // clicks on the menu item to start a call - asyncRoomSummary.invoke()?.joinedMembersCount == 1 || - hasActiveElementCallWidget() + asyncRoomSummary.invoke()?.joinedMembersCount == 1 } fun isSearchAvailable() = asyncRoomSummary()?.isEncrypted == false diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt index ba691de5d2..8d2d086275 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/StartCallActionsHandler.kt @@ -47,6 +47,11 @@ class StartCallActionsHandler( } private fun handleCallRequest(isVideoCall: Boolean) = withState(timelineViewModel) { state -> + if (state.hasActiveElementCallWidget() && !isVideoCall) { + timelineViewModel.handle(RoomDetailAction.OpenElementCallWidget) + return@withState + } + val roomSummary = state.asyncRoomSummary.invoke() ?: return@withState when (roomSummary.joinedMembersCount) { 1 -> { diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt index 27ba12e4d7..a3acecdd38 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineFragment.kt @@ -1088,12 +1088,11 @@ class TimelineFragment @Inject constructor( val hasCallInRoom = callManager.getCallsByRoomId(state.roomId).isNotEmpty() || state.jitsiState.hasJoined val callButtonsEnabled = !hasCallInRoom && when (state.asyncRoomSummary.invoke()?.joinedMembersCount) { 1 -> false - 2 -> state.isAllowedToStartWebRTCCall || state.hasActiveElementCallWidget() - else -> state.isAllowedToManageWidgets || state.hasActiveElementCallWidget() - } - setOf(R.id.voice_call, R.id.video_call).forEach { - menu.findItem(it).icon?.alpha = if (callButtonsEnabled) 0xFF else 0x40 + 2 -> state.isAllowedToStartWebRTCCall + else -> state.isAllowedToManageWidgets } + menu.findItem(R.id.video_call).icon?.alpha = if (callButtonsEnabled) 0xFF else 0x40 + menu.findItem(R.id.voice_call).icon?.alpha = if (callButtonsEnabled || state.hasActiveElementCallWidget()) 0xFF else 0x40 val matrixAppsMenuItem = menu.findItem(R.id.open_matrix_apps) val widgetsCount = state.activeRoomWidgets.invoke()?.size ?: 0 diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt index 71b6b22aed..c73ed3b672 100644 --- a/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/home/room/detail/TimelineViewModel.kt @@ -467,6 +467,13 @@ class TimelineViewModel @AssistedInject constructor( } is RoomDetailAction.EndPoll -> handleEndPoll(action.eventId) RoomDetailAction.StopLiveLocationSharing -> handleStopLiveLocationSharing() + RoomDetailAction.OpenElementCallWidget -> handleOpenElementCallWidget() + } + } + + private fun handleOpenElementCallWidget() = withState { state -> + if (state.hasActiveElementCallWidget()) { + _viewEvents.post(RoomDetailViewEvents.OpenElementCallWidget) } } From 81f3e4a360a8b89fb4727f8630eb50797eff1baa Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 21 Jul 2022 16:29:34 +0300 Subject: [PATCH 14/20] Lint fixes. --- .../java/im/vector/app/features/widgets/WidgetActivity.kt | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt index 90f1ab4bbb..a068d57936 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt @@ -24,7 +24,6 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter -import android.content.res.Configuration import android.graphics.drawable.Icon import android.os.Build import android.util.Rational @@ -34,7 +33,6 @@ import androidx.core.util.Consumer import androidx.core.view.isVisible import com.airbnb.mvrx.Mavericks import com.airbnb.mvrx.viewModel -import com.airbnb.mvrx.withState import dagger.hilt.android.AndroidEntryPoint import im.vector.app.R import im.vector.app.core.extensions.addFragment @@ -184,7 +182,7 @@ class WidgetActivity : VectorBaseActivity() { private val pictureInPictureModeChangedInfoConsumer = Consumer { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return@Consumer - + if (isInPictureInPictureMode) { hangupBroadcastReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context?, intent: Intent?) { From b38911751e3c7ec30459bff7cb2c19e439317d54 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Thu, 21 Jul 2022 17:28:01 +0300 Subject: [PATCH 15/20] Changelog added. --- changelog.d/6616.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 changelog.d/6616.feature diff --git a/changelog.d/6616.feature b/changelog.d/6616.feature new file mode 100644 index 0000000000..d013771764 --- /dev/null +++ b/changelog.d/6616.feature @@ -0,0 +1 @@ +Support element call widget From d631c709d686249661fe664d160e0f4657101f7c Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 22 Jul 2022 14:33:45 +0300 Subject: [PATCH 16/20] Create use case to check webview permissions. --- .../api/session/widgets/model/WidgetType.kt | 2 +- .../utils/CheckWebViewPermissionsUseCase.kt | 52 +++++++++++++++++++ .../vector/app/core/utils/PermissionsTools.kt | 27 ---------- .../app/features/widgets/WidgetFragment.kt | 6 ++- .../features/widgets/webview/WidgetWebView.kt | 9 ++-- 5 files changed, 63 insertions(+), 33 deletions(-) create mode 100644 vector/src/main/java/im/vector/app/core/utils/CheckWebViewPermissionsUseCase.kt diff --git a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt index 24f3a155ed..f02fe4f9de 100644 --- a/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt +++ b/matrix-sdk-android/src/main/java/org/matrix/android/sdk/api/session/widgets/model/WidgetType.kt @@ -29,7 +29,7 @@ private val DEFINED_TYPES by lazy { WidgetType.Grafana, WidgetType.Custom, WidgetType.IntegrationManager, - WidgetType.ElementCall + WidgetType.ElementCall, ) } diff --git a/vector/src/main/java/im/vector/app/core/utils/CheckWebViewPermissionsUseCase.kt b/vector/src/main/java/im/vector/app/core/utils/CheckWebViewPermissionsUseCase.kt new file mode 100644 index 0000000000..df84e24f90 --- /dev/null +++ b/vector/src/main/java/im/vector/app/core/utils/CheckWebViewPermissionsUseCase.kt @@ -0,0 +1,52 @@ +/* + * 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.core.utils + +import android.app.Activity +import android.content.pm.PackageManager +import android.webkit.PermissionRequest +import androidx.core.content.ContextCompat +import javax.inject.Inject + +class CheckWebViewPermissionsUseCase @Inject constructor() { + + /** + * Checks if required WebView permissions are already granted system level. + * @param activity the calling Activity that is requesting the permissions (or fragment parent) + * @param request WebView permission request of onPermissionRequest function + * @return true if WebView permissions are already granted, false otherwise + */ + fun execute(activity: Activity, request: PermissionRequest): Boolean { + return request.resources.all { + when (it) { + PermissionRequest.RESOURCE_AUDIO_CAPTURE -> { + PERMISSIONS_FOR_AUDIO_IP_CALL.all { permission -> + ContextCompat.checkSelfPermission(activity.applicationContext, permission) == PackageManager.PERMISSION_GRANTED + } + } + PermissionRequest.RESOURCE_VIDEO_CAPTURE -> { + PERMISSIONS_FOR_VIDEO_IP_CALL.all { permission -> + ContextCompat.checkSelfPermission(activity.applicationContext, permission) == PackageManager.PERMISSION_GRANTED + } + } + else -> { + false + } + } + } + } +} diff --git a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt index 051a6cd8ce..9ad95d3c55 100644 --- a/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt +++ b/vector/src/main/java/im/vector/app/core/utils/PermissionsTools.kt @@ -19,7 +19,6 @@ package im.vector.app.core.utils import android.Manifest import android.app.Activity import android.content.pm.PackageManager -import android.webkit.PermissionRequest import androidx.activity.ComponentActivity import androidx.activity.result.ActivityResultLauncher import androidx.activity.result.contract.ActivityResultContracts @@ -138,32 +137,6 @@ fun checkPermissions( } } -/** - * Checks if required WebView permissions are already granted system level. - * @param activity the calling Activity that is requesting the permissions (or fragment parent) - * @param request WebView permission request of onPermissionRequest function - * @return true if WebView permissions are already granted, false otherwise - */ -fun checkWebViewPermissions(activity: Activity, request: PermissionRequest): Boolean { - return request.resources.all { - when (it) { - PermissionRequest.RESOURCE_AUDIO_CAPTURE -> { - PERMISSIONS_FOR_AUDIO_IP_CALL.all { permission -> - ContextCompat.checkSelfPermission(activity.applicationContext, permission) == PackageManager.PERMISSION_GRANTED - } - } - PermissionRequest.RESOURCE_VIDEO_CAPTURE -> { - PERMISSIONS_FOR_VIDEO_IP_CALL.all { permission -> - ContextCompat.checkSelfPermission(activity.applicationContext, permission) == PackageManager.PERMISSION_GRANTED - } - } - else -> { - false - } - } - } -} - /** * To be call after the permission request. * diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt index 390bd3c88b..9ac085fa89 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetFragment.kt @@ -43,6 +43,7 @@ import im.vector.app.core.extensions.registerStartForActivityResult import im.vector.app.core.platform.OnBackPressed import im.vector.app.core.platform.VectorBaseFragment import im.vector.app.core.platform.VectorMenuProvider +import im.vector.app.core.utils.CheckWebViewPermissionsUseCase import im.vector.app.core.utils.openUrlInExternalBrowser import im.vector.app.databinding.FragmentRoomWidgetBinding import im.vector.app.features.webview.WebEventListener @@ -65,7 +66,8 @@ data class WidgetArgs( ) : Parcelable class WidgetFragment @Inject constructor( - private val permissionUtils: WebviewPermissionUtils + private val permissionUtils: WebviewPermissionUtils, + private val checkWebViewPermissionsUseCase: CheckWebViewPermissionsUseCase, ) : VectorBaseFragment(), WebEventListener, @@ -81,7 +83,7 @@ class WidgetFragment @Inject constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - views.widgetWebView.setupForWidget(requireActivity(), this) + views.widgetWebView.setupForWidget(requireActivity(), checkWebViewPermissionsUseCase, this) if (fragmentArgs.kind.isAdmin()) { viewModel.getPostAPIMediator().setWebView(views.widgetWebView) } diff --git a/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt b/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt index 2f3b449b1a..ac9930866f 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/webview/WidgetWebView.kt @@ -24,13 +24,16 @@ import android.webkit.PermissionRequest import android.webkit.WebChromeClient import android.webkit.WebView import im.vector.app.R -import im.vector.app.core.utils.checkWebViewPermissions +import im.vector.app.core.utils.CheckWebViewPermissionsUseCase import im.vector.app.features.themes.ThemeUtils import im.vector.app.features.webview.VectorWebViewClient import im.vector.app.features.webview.WebEventListener @SuppressLint("NewApi") -fun WebView.setupForWidget(activity: Activity, eventListener: WebEventListener) { +fun WebView.setupForWidget(activity: Activity, + checkWebViewPermissionsUseCase: CheckWebViewPermissionsUseCase, + eventListener: WebEventListener, +) { // xml value seems ignored setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorSurface)) @@ -63,7 +66,7 @@ fun WebView.setupForWidget(activity: Activity, eventListener: WebEventListener) // Permission requests webChromeClient = object : WebChromeClient() { override fun onPermissionRequest(request: PermissionRequest) { - if (checkWebViewPermissions(activity, request)) { + if (checkWebViewPermissionsUseCase.execute(activity, request)) { request.grant(request.resources) } else { eventListener.onPermissionRequest(request) From 242c14a1569a33947309d1562aca6ff1b8515a1c Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 22 Jul 2022 14:34:06 +0300 Subject: [PATCH 17/20] Write test for the use case. --- .../CheckWebViewPermissionsUseCaseTest.kt | 120 ++++++++++++++++++ 1 file changed, 120 insertions(+) create mode 100644 vector/src/test/java/im/vector/app/core/utils/CheckWebViewPermissionsUseCaseTest.kt diff --git a/vector/src/test/java/im/vector/app/core/utils/CheckWebViewPermissionsUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/utils/CheckWebViewPermissionsUseCaseTest.kt new file mode 100644 index 0000000000..8d328d75c5 --- /dev/null +++ b/vector/src/test/java/im/vector/app/core/utils/CheckWebViewPermissionsUseCaseTest.kt @@ -0,0 +1,120 @@ +/* + * 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.core.utils + +import android.app.Activity +import android.content.pm.PackageManager +import android.net.Uri +import android.webkit.PermissionRequest +import androidx.core.content.ContextCompat.checkSelfPermission +import io.mockk.MockKAnnotations +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkStatic +import org.amshove.kluent.shouldBe +import org.junit.After +import org.junit.Before +import org.junit.Test + +class CheckWebViewPermissionsUseCaseTest { + + private val checkWebViewPermissionsUseCase = CheckWebViewPermissionsUseCase() + + private val activity = mockk().apply { + every { applicationContext } returns mockk() + } + + @Before + fun setup() { + MockKAnnotations.init(this) + mockkStatic("androidx.core.content.ContextCompat") + } + + @After + fun tearDown() { + unmockkStatic("androidx.core.content.ContextCompat") + } + + @Test + fun `given an audio permission is granted when the web client requests audio permission then use case returns true`() { + val permissionRequest = givenAPermissionRequest(arrayOf(PermissionRequest.RESOURCE_AUDIO_CAPTURE)) + + every { checkSelfPermission(activity.applicationContext, any()) } returns PackageManager.PERMISSION_GRANTED + + checkWebViewPermissionsUseCase.execute(activity, permissionRequest) shouldBe true + } + + @Test + fun `given a camera permission is granted when the web client requests video permission then use case returns true`() { + val permissionRequest = givenAPermissionRequest(arrayOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) + + every { checkSelfPermission(activity.applicationContext, any()) } returns PackageManager.PERMISSION_GRANTED + + checkWebViewPermissionsUseCase.execute(activity, permissionRequest) shouldBe true + } + + @Test + fun `given an audio and camera permissions are granted when the web client requests audio and video permissions then use case returns true`() { + val permissionRequest = givenAPermissionRequest(arrayOf(PermissionRequest.RESOURCE_AUDIO_CAPTURE, PermissionRequest.RESOURCE_VIDEO_CAPTURE)) + + every { checkSelfPermission(activity.applicationContext, any()) } returns PackageManager.PERMISSION_GRANTED + + checkWebViewPermissionsUseCase.execute(activity, permissionRequest) shouldBe true + } + + @Test + fun `given an audio permission is granted but camera isn't when the web client requests audio and video permissions then use case returns false`() { + val permissionRequest = givenAPermissionRequest(arrayOf(PermissionRequest.RESOURCE_AUDIO_CAPTURE, PermissionRequest.RESOURCE_VIDEO_CAPTURE)) + + PERMISSIONS_FOR_AUDIO_IP_CALL.forEach { + every { checkSelfPermission(activity.applicationContext, it) } returns PackageManager.PERMISSION_GRANTED + } + PERMISSIONS_FOR_VIDEO_IP_CALL.forEach { + every { checkSelfPermission(activity.applicationContext, it) } returns PackageManager.PERMISSION_DENIED + } + + checkWebViewPermissionsUseCase.execute(activity, permissionRequest) shouldBe false + } + + @Test + fun `given an audio and camera permissions are granted when the web client requests another permission then use case returns false`() { + val permissionRequest = givenAPermissionRequest(arrayOf(PermissionRequest.RESOURCE_AUDIO_CAPTURE, PermissionRequest.RESOURCE_MIDI_SYSEX)) + + every { checkSelfPermission(activity.applicationContext, any()) } returns PackageManager.PERMISSION_GRANTED + + checkWebViewPermissionsUseCase.execute(activity, permissionRequest) shouldBe false + } + + private fun givenAPermissionRequest(resources: Array): PermissionRequest { + return object : PermissionRequest() { + override fun getOrigin(): Uri { + return mockk() + } + + override fun getResources(): Array { + return resources + } + + override fun grant(resources: Array?) { + } + + override fun deny() { + } + } + } +} From 23a25cf24023c99a6e64a0049f3a13cc3629cdab Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 22 Jul 2022 14:36:02 +0300 Subject: [PATCH 18/20] Rename widget action. --- .../main/java/im/vector/app/features/widgets/WidgetAction.kt | 2 +- .../java/im/vector/app/features/widgets/WidgetActivity.kt | 2 +- .../java/im/vector/app/features/widgets/WidgetViewModel.kt | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetAction.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetAction.kt index f525af6109..d5d8e95aa6 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetAction.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetAction.kt @@ -26,5 +26,5 @@ sealed class WidgetAction : VectorViewModelAction { object DeleteWidget : WidgetAction() object RevokeWidget : WidgetAction() object OnTermsReviewed : WidgetAction() - object HangupElementCall : WidgetAction() + object CloseWidget : WidgetAction() } diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt index a068d57936..92b070c8d0 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt @@ -189,7 +189,7 @@ class WidgetActivity : VectorBaseActivity() { if (intent?.action == ACTION_MEDIA_CONTROL) { val controlType = intent.getIntExtra(EXTRA_CONTROL_TYPE, 0) if (controlType == CONTROL_TYPE_HANGUP) { - viewModel.handle(WidgetAction.HangupElementCall) + viewModel.handle(WidgetAction.CloseWidget) } } } diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt index 17dc244f4c..ecd6ca2fd6 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewModel.kt @@ -147,11 +147,11 @@ class WidgetViewModel @AssistedInject constructor( WidgetAction.DeleteWidget -> handleDeleteWidget() WidgetAction.RevokeWidget -> handleRevokeWidget() WidgetAction.OnTermsReviewed -> loadFormattedUrl(forceFetchToken = false) - WidgetAction.HangupElementCall -> handleHangupElementCall() + WidgetAction.CloseWidget -> handleCloseWidget() } } - private fun handleHangupElementCall() { + private fun handleCloseWidget() { _viewEvents.post(WidgetViewEvents.Close()) } From 792fca8400088fded16d5924ef9326c8aadf3c48 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 22 Jul 2022 14:48:58 +0300 Subject: [PATCH 19/20] Code review fix. --- .../java/im/vector/app/features/widgets/WidgetActivity.kt | 3 ++- .../java/im/vector/app/features/widgets/WidgetViewState.kt | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt index 92b070c8d0..0b78d8d2f1 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt @@ -41,6 +41,7 @@ import im.vector.app.databinding.ActivityWidgetBinding import im.vector.app.features.widgets.permissions.RoomWidgetPermissionBottomSheet import im.vector.app.features.widgets.permissions.RoomWidgetPermissionViewEvents import im.vector.app.features.widgets.permissions.RoomWidgetPermissionViewModel +import org.matrix.android.sdk.api.extensions.orFalse import org.matrix.android.sdk.api.session.events.model.Content import java.io.Serializable @@ -145,7 +146,7 @@ class WidgetActivity : VectorBaseActivity() { override fun onUserLeaveHint() { super.onUserLeaveHint() val widgetArgs: WidgetArgs? = intent?.extras?.getParcelable(Mavericks.KEY_ARG) - if (widgetArgs?.kind == WidgetKind.ELEMENT_CALL) { + if (widgetArgs?.kind?.supportsPictureInPictureMode().orFalse()) { enterPictureInPicture() } } diff --git a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewState.kt b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewState.kt index 7619fea766..cd2ed23980 100644 --- a/vector/src/main/java/im/vector/app/features/widgets/WidgetViewState.kt +++ b/vector/src/main/java/im/vector/app/features/widgets/WidgetViewState.kt @@ -39,6 +39,10 @@ enum class WidgetKind(@StringRes val nameRes: Int, val screenId: String?) { fun isAdmin(): Boolean { return this == STICKER_PICKER || this == INTEGRATION_MANAGER } + + fun supportsPictureInPictureMode(): Boolean { + return this == ELEMENT_CALL + } } data class WidgetViewState( From 5c253bbd280892371f28102d44dede815751ce95 Mon Sep 17 00:00:00 2001 From: Onuray Sahin Date: Fri, 22 Jul 2022 17:21:23 +0300 Subject: [PATCH 20/20] Code review fixes. --- .../CheckWebViewPermissionsUseCaseTest.kt | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/vector/src/test/java/im/vector/app/core/utils/CheckWebViewPermissionsUseCaseTest.kt b/vector/src/test/java/im/vector/app/core/utils/CheckWebViewPermissionsUseCaseTest.kt index 8d328d75c5..fe082ab5b6 100644 --- a/vector/src/test/java/im/vector/app/core/utils/CheckWebViewPermissionsUseCaseTest.kt +++ b/vector/src/test/java/im/vector/app/core/utils/CheckWebViewPermissionsUseCaseTest.kt @@ -17,15 +17,16 @@ package im.vector.app.core.utils import android.app.Activity +import android.content.Context import android.content.pm.PackageManager import android.net.Uri import android.webkit.PermissionRequest import androidx.core.content.ContextCompat.checkSelfPermission -import io.mockk.MockKAnnotations import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import io.mockk.unmockkStatic +import io.mockk.verify import org.amshove.kluent.shouldBe import org.junit.After import org.junit.Before @@ -41,7 +42,6 @@ class CheckWebViewPermissionsUseCaseTest { @Before fun setup() { - MockKAnnotations.init(this) mockkStatic("androidx.core.content.ContextCompat") } @@ -53,34 +53,33 @@ class CheckWebViewPermissionsUseCaseTest { @Test fun `given an audio permission is granted when the web client requests audio permission then use case returns true`() { val permissionRequest = givenAPermissionRequest(arrayOf(PermissionRequest.RESOURCE_AUDIO_CAPTURE)) - every { checkSelfPermission(activity.applicationContext, any()) } returns PackageManager.PERMISSION_GRANTED checkWebViewPermissionsUseCase.execute(activity, permissionRequest) shouldBe true + verifyPermissionsChecked(activity.applicationContext, PERMISSIONS_FOR_AUDIO_IP_CALL) } @Test fun `given a camera permission is granted when the web client requests video permission then use case returns true`() { val permissionRequest = givenAPermissionRequest(arrayOf(PermissionRequest.RESOURCE_VIDEO_CAPTURE)) - every { checkSelfPermission(activity.applicationContext, any()) } returns PackageManager.PERMISSION_GRANTED checkWebViewPermissionsUseCase.execute(activity, permissionRequest) shouldBe true + verifyPermissionsChecked(activity.applicationContext, PERMISSIONS_FOR_VIDEO_IP_CALL) } @Test fun `given an audio and camera permissions are granted when the web client requests audio and video permissions then use case returns true`() { val permissionRequest = givenAPermissionRequest(arrayOf(PermissionRequest.RESOURCE_AUDIO_CAPTURE, PermissionRequest.RESOURCE_VIDEO_CAPTURE)) - every { checkSelfPermission(activity.applicationContext, any()) } returns PackageManager.PERMISSION_GRANTED checkWebViewPermissionsUseCase.execute(activity, permissionRequest) shouldBe true + verifyPermissionsChecked(activity.applicationContext, PERMISSIONS_FOR_AUDIO_IP_CALL + PERMISSIONS_FOR_VIDEO_IP_CALL) } @Test fun `given an audio permission is granted but camera isn't when the web client requests audio and video permissions then use case returns false`() { val permissionRequest = givenAPermissionRequest(arrayOf(PermissionRequest.RESOURCE_AUDIO_CAPTURE, PermissionRequest.RESOURCE_VIDEO_CAPTURE)) - PERMISSIONS_FOR_AUDIO_IP_CALL.forEach { every { checkSelfPermission(activity.applicationContext, it) } returns PackageManager.PERMISSION_GRANTED } @@ -89,15 +88,22 @@ class CheckWebViewPermissionsUseCaseTest { } checkWebViewPermissionsUseCase.execute(activity, permissionRequest) shouldBe false + verifyPermissionsChecked(activity.applicationContext, PERMISSIONS_FOR_AUDIO_IP_CALL + PERMISSIONS_FOR_VIDEO_IP_CALL.first()) } @Test fun `given an audio and camera permissions are granted when the web client requests another permission then use case returns false`() { val permissionRequest = givenAPermissionRequest(arrayOf(PermissionRequest.RESOURCE_AUDIO_CAPTURE, PermissionRequest.RESOURCE_MIDI_SYSEX)) - every { checkSelfPermission(activity.applicationContext, any()) } returns PackageManager.PERMISSION_GRANTED checkWebViewPermissionsUseCase.execute(activity, permissionRequest) shouldBe false + verifyPermissionsChecked(activity.applicationContext, PERMISSIONS_FOR_AUDIO_IP_CALL) + } + + private fun verifyPermissionsChecked(context: Context, permissions: List) { + permissions.forEach { + verify { checkSelfPermission(context, it) } + } } private fun givenAPermissionRequest(resources: Array): PermissionRequest {