Skip to content

Commit

Permalink
Merge pull request #6616 from vector-im/feature/ons/element_call_widget
Browse files Browse the repository at this point in the history
Support element call widget (PSG-627)
  • Loading branch information
onurays authored Jul 22, 2022
2 parents 99a906f + 5c253bb commit 75de805
Show file tree
Hide file tree
Showing 20 changed files with 397 additions and 49 deletions.
1 change: 1 addition & 0 deletions changelog.d/6616.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Support element call widget
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ private val DEFINED_TYPES by lazy {
WidgetType.StickerPicker,
WidgetType.Grafana,
WidgetType.Custom,
WidgetType.IntegrationManager
WidgetType.IntegrationManager,
WidgetType.ElementCall,
)
}

Expand All @@ -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 {
Expand Down
3 changes: 2 additions & 1 deletion vector/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -308,7 +308,8 @@
<activity android:name=".features.terms.ReviewTermsActivity" />
<activity
android:name=".features.widgets.WidgetActivity"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation" />
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation"
android:supportsPictureInPicture="true" />

<activity android:name=".features.pin.PinActivity" />
<activity android:name=".features.analytics.ui.consent.AnalyticsOptInActivity" />
Expand Down
Original file line number Diff line number Diff line change
@@ -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
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,4 +117,6 @@ sealed class RoomDetailAction : VectorViewModelAction {

// Live Location
object StopLiveLocationSharing : RoomDetailAction()

object OpenElementCallWidget : RoomDetailAction()
}
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,5 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
data class StartChatEffect(val type: ChatEffect) : RoomDetailViewEvents()
object StopChatEffects : RoomDetailViewEvents()
object RoomReplacementStarted : RoomDetailViewEvents()
object OpenElementCallWidget : RoomDetailViewEvents()
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 -> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -498,6 +498,7 @@ class TimelineFragment @Inject constructor(
RoomDetailViewEvents.StopChatEffects -> handleStopChatEffects()
is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it)
RoomDetailViewEvents.RoomReplacementStarted -> handleRoomReplacement()
RoomDetailViewEvents.OpenElementCallWidget -> handleOpenElementCallWidget()
}
}

Expand Down Expand Up @@ -1090,9 +1091,8 @@ class TimelineFragment @Inject constructor(
2 -> state.isAllowedToStartWebRTCCall
else -> state.isAllowedToManageWidgets
}
setOf(R.id.voice_call, R.id.video_call).forEach {
menu.findItem(it).icon?.alpha = if (callButtonsEnabled) 0xFF else 0x40
}
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
Expand Down Expand Up @@ -2653,6 +2653,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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -752,7 +759,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,5 @@ sealed class WidgetAction : VectorViewModelAction {
object DeleteWidget : WidgetAction()
object RevokeWidget : WidgetAction()
object OnTermsReviewed : WidgetAction()
object CloseWidget : WidgetAction()
}
122 changes: 102 additions & 20 deletions vector/src/main/java/im/vector/app/features/widgets/WidgetActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,19 @@
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.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
Expand All @@ -30,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

Expand All @@ -40,6 +52,10 @@ class WidgetActivity : VectorBaseActivity<ActivityWidgetBinding>() {
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 {
Expand Down Expand Up @@ -82,29 +98,37 @@ class WidgetActivity : VectorBaseActivity<ActivityWidgetBinding>() {
}
}

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) {
addOnPictureInPictureModeChangedListener(pictureInPictureModeChangedInfoConsumer)
addFragment(views.fragmentContainer, WidgetFragment::class.java, widgetArgs, WIDGET_FRAGMENT_TAG)
}
}

viewModel.onEach(WidgetViewState::status) { ws ->
when (ws) {
WidgetStatus.UNKNOWN -> {
} else {
permissionViewModel.observeViewEvents {
when (it) {
is RoomWidgetPermissionViewEvents.Close -> finish()
}
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)
}
}
}
}
Expand All @@ -119,6 +143,64 @@ class WidgetActivity : VectorBaseActivity<ActivityWidgetBinding>() {
}
}

override fun onUserLeaveHint() {
super.onUserLeaveHint()
val widgetArgs: WidgetArgs? = intent?.extras?.getParcelable(Mavericks.KEY_ARG)
if (widgetArgs?.kind?.supportsPictureInPictureMode().orFalse()) {
enterPictureInPicture()
}
}

override fun onDestroy() {
removeOnPictureInPictureModeChangedListener(pictureInPictureModeChangedInfoConsumer)
super.onDestroy()
}

private fun enterPictureInPicture() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createElementCallPipParams()?.let {
enterPictureInPictureMode(it)
}
}
}

@RequiresApi(Build.VERSION_CODES.O)
private fun createElementCallPipParams(): PictureInPictureParams? {
val actions = mutableListOf<RemoteAction>()
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

private val pictureInPictureModeChangedInfoConsumer = Consumer<PictureInPictureModeChangedInfo> {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return@Consumer

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.CloseWidget)
}
}
}
}
registerReceiver(hangupBroadcastReceiver, IntentFilter(ACTION_MEDIA_CONTROL))
} else {
unregisterReceiver(hangupBroadcastReceiver)
}
}

private fun handleClose(event: WidgetViewEvents.Close) {
if (event.content != null) {
val intent = createResultIntent(event.content)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String, String?>.filterNotNull(): Map<String, String> {
return filterValues { it != null } as Map<String, String>
Expand Down
Loading

0 comments on commit 75de805

Please sign in to comment.