Merge pull request #2908 from vector-im/feature/bma/jitsi_pip

PIP support for Jitsi call
This commit is contained in:
Benoit Marty 2021-03-01 11:41:34 +01:00 committed by GitHub
commit c152964323
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 211 additions and 77 deletions

View file

@ -7,6 +7,7 @@ Features ✨:
Improvements 🙌:
- Fetch homeserver type and version and display in a new setting screen and add info in rageshakes (#2831)
- Improve initial sync performance (#983)
- PIP support for Jitsi call (#2418)
Bugfix 🐛:
- Try to fix crash about UrlPreview (#2640)

View file

@ -37,7 +37,7 @@
tools:node="remove" />
<!-- Jitsi SDK is now API23+ -->
<uses-sdk tools:overrideLibrary="org.jitsi.meet.sdk,com.oney.WebRTCModule,com.learnium.RNDeviceInfo,com.reactnativecommunity.asyncstorage,com.ocetnik.timer,com.calendarevents,com.reactnativecommunity.netinfo,com.kevinresol.react_native_default_preference,com.rnimmersive,com.corbt.keepawake,com.BV.LinearGradient,com.horcrux.svg"/>
<uses-sdk tools:overrideLibrary="org.jitsi.meet.sdk,com.oney.WebRTCModule,com.learnium.RNDeviceInfo,com.reactnativecommunity.asyncstorage,com.ocetnik.timer,com.calendarevents,com.reactnativecommunity.netinfo,com.kevinresol.react_native_default_preference,com.rnimmersive,com.corbt.keepawake,com.BV.LinearGradient,com.horcrux.svg" />
<!-- Adding CAMERA permission prevents Chromebooks to see the application on the PlayStore -->
<!-- Tell that the Camera is not mandatory to install the application -->
@ -83,8 +83,7 @@
android:resource="@xml/shortcuts" />
</activity-alias>
<activity
android:name=".features.home.HomeActivity" />
<activity android:name=".features.home.HomeActivity" />
<activity
android:name=".features.login.LoginActivity"
android:launchMode="singleTask"
@ -233,11 +232,15 @@
<activity
android:name=".features.attachments.preview.AttachmentsPreviewActivity"
android:theme="@style/AppTheme.AttachmentsPreview" />
<activity android:name=".features.call.VectorCallActivity"
android:excludeFromRecents="true"/>
<activity
android:name=".features.call.VectorCallActivity"
android:excludeFromRecents="true" />
<!-- PIP Support https://developer.android.com/guide/topics/ui/picture-in-picture -->
<activity
android:name=".features.call.conference.VectorJitsiActivity"
android:configChanges="orientation|screenSize" />
android:configChanges="orientation|smallestScreenSize|screenLayout|screenSize"
android:launchMode="singleTask"
android:supportsPictureInPicture="true" />
<activity android:name=".features.terms.ReviewTermsActivity" />
<activity android:name=".features.widgets.WidgetActivity" />
@ -247,27 +250,28 @@
<activity android:name=".features.call.transfer.CallTransferActivity" />
<!-- Single instance is very important for the custom scheme callback-->
<activity android:name=".features.auth.ReAuthActivity"
android:launchMode="singleInstance"
android:exported="false">
<activity
android:name=".features.auth.ReAuthActivity"
android:exported="false"
android:launchMode="singleInstance">
<!-- XXX: UIA SSO has only web fallback, i.e no url redirect, so for now we comment this out
hopefully, we would use it when finally available
-->
<!-- Add intent filter to handle redirection URL after SSO login in external browser -->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.VIEW" />-->
<!-- <intent-filter>-->
<!-- <action android:name="android.intent.action.VIEW" />-->
<!-- <category android:name="android.intent.category.DEFAULT" />-->
<!-- <category android:name="android.intent.category.BROWSABLE" />-->
<!-- <category android:name="android.intent.category.DEFAULT" />-->
<!-- <category android:name="android.intent.category.BROWSABLE" />-->
<!-- <data-->
<!-- android:host="reauth"-->
<!-- android:scheme="element" />-->
<!-- </intent-filter>-->
<!-- <data-->
<!-- android:host="reauth"-->
<!-- android:scheme="element" />-->
<!-- </intent-filter>-->
</activity>
<activity android:name=".features.devtools.RoomDevToolActivity"/>
<activity android:name=".features.devtools.RoomDevToolActivity" />
<!-- Services -->
<service

View file

@ -58,6 +58,7 @@ import im.vector.app.features.settings.VectorPreferences
import im.vector.app.features.themes.ThemeUtils
import im.vector.app.features.version.VersionProvider
import im.vector.app.push.fcm.FcmHelper
import org.jitsi.meet.sdk.log.JitsiMeetDefaultLogHandler
import org.matrix.android.sdk.api.Matrix
import org.matrix.android.sdk.api.MatrixConfiguration
import org.matrix.android.sdk.api.auth.AuthenticationService
@ -117,6 +118,11 @@ class VectorApplication :
vectorUncaughtExceptionHandler.activate(this)
rxConfig.setupRxPlugin()
// Remove Log handler statically added by Jitsi
Timber.forest()
.filterIsInstance(JitsiMeetDefaultLogHandler::class.java)
.forEach { Timber.uproot(it) }
if (BuildConfig.DEBUG) {
Timber.plant(Timber.DebugTree())
}

View file

@ -46,7 +46,7 @@ class ActiveConferenceView @JvmOverloads constructor(
}
var callback: Callback? = null
var jitsiWidget: Widget? = null
private var jitsiWidget: Widget? = null
private lateinit var views: ViewActiveConferenceViewBinding
@ -95,18 +95,12 @@ class ActiveConferenceView @JvmOverloads constructor(
val summary = state.asyncRoomSummary()
if (summary?.membership == Membership.JOIN) {
// We only display banner for 'live' widgets
val activeConf =
state.activeRoomWidgets()?.firstOrNull {
// for now only jitsi?
it.type == WidgetType.Jitsi
}
if (activeConf == null) {
isVisible = false
} else {
isVisible = true
jitsiWidget = activeConf
jitsiWidget = state.activeRoomWidgets()?.firstOrNull {
// for now only jitsi?
it.type == WidgetType.Jitsi
}
isVisible = jitsiWidget != null
// if sent by me or if i can moderate?
views.deleteWidgetButton.isVisible = state.isAllowedToManageWidgets
} else {

View file

@ -18,4 +18,12 @@ package im.vector.app.features.call.conference
import im.vector.app.core.platform.VectorViewModelAction
sealed class JitsiCallViewActions : VectorViewModelAction
sealed class JitsiCallViewActions : VectorViewModelAction {
data class SwitchTo(val args: VectorJitsiActivity.Args,
val withConfirmation: Boolean) : JitsiCallViewActions()
/**
* The ViewModel will either ask the View to finish, or to join another conf.
*/
object OnConferenceLeft: JitsiCallViewActions()
}

View file

@ -17,5 +17,21 @@
package im.vector.app.features.call.conference
import im.vector.app.core.platform.VectorViewEvents
import org.jitsi.meet.sdk.JitsiMeetUserInfo
sealed class JitsiCallViewEvents : VectorViewEvents
sealed class JitsiCallViewEvents : VectorViewEvents {
data class StartConference(
val enableVideo: Boolean,
val jitsiUrl: String,
val subject: String,
val confId: String,
val userInfo: JitsiMeetUserInfo
) : JitsiCallViewEvents()
data class ConfirmSwitchingConference(
val args: VectorJitsiActivity.Args
) : JitsiCallViewEvents()
object LeaveConference : JitsiCallViewEvents()
object Finish : JitsiCallViewEvents()
}

View file

@ -16,18 +16,25 @@
package im.vector.app.features.call.conference
import androidx.lifecycle.viewModelScope
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.MvRxViewModelFactory
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.Uninitialized
import com.airbnb.mvrx.ViewModelContext
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
import dagger.assisted.AssistedFactory
import dagger.assisted.AssistedInject
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.features.themes.ThemeProvider
import io.reactivex.disposables.Disposable
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import org.jitsi.meet.sdk.JitsiMeetUserInfo
import org.matrix.android.sdk.api.query.QueryStringValue
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.widgets.model.Widget
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
import org.matrix.android.sdk.api.util.toMatrixItem
import org.matrix.android.sdk.rx.asObservable
@ -35,7 +42,6 @@ import java.net.URL
class JitsiCallViewModel @AssistedInject constructor(
@Assisted initialState: JitsiCallViewState,
@Assisted val args: VectorJitsiActivity.Args,
private val session: Session,
private val jitsiMeetPropertiesFactory: JitsiWidgetPropertiesFactory,
private val themeProvider: ThemeProvider
@ -43,38 +49,35 @@ class JitsiCallViewModel @AssistedInject constructor(
@AssistedFactory
interface Factory {
fun create(initialState: JitsiCallViewState, args: VectorJitsiActivity.Args): JitsiCallViewModel
fun create(initialState: JitsiCallViewState): JitsiCallViewModel
}
private var currentWidgetObserver: Disposable? = null
private val widgetService = session.widgetService()
private var confIsStarted = false
private var pendingArgs: VectorJitsiActivity.Args? = null
init {
val me = session.getRoomMember(session.myUserId, args.roomId)?.toMatrixItem()
val userInfo = JitsiMeetUserInfo().apply {
displayName = me?.getBestName()
avatar = me?.avatarUrl?.let { session.contentUrlResolver().resolveFullSize(it) }?.let { URL(it) }
}
val roomName = session.getRoomSummary(args.roomId)?.displayName
observeWidget(initialState.roomId, initialState.widgetId)
}
setState {
copy(userInfo = userInfo)
}
widgetService.getRoomWidgetsLive(args.roomId, QueryStringValue.Equals(args.widgetId), WidgetType.Jitsi.values())
private fun observeWidget(roomId: String, widgetId: String) {
confIsStarted = false
currentWidgetObserver?.dispose()
currentWidgetObserver = widgetService.getRoomWidgetsLive(roomId, QueryStringValue.Equals(widgetId), WidgetType.Jitsi.values())
.asObservable()
.distinctUntilChanged()
.subscribe {
val jitsiWidget = it.firstOrNull()
if (jitsiWidget != null) {
val ppt = widgetService.getWidgetComputedUrl(jitsiWidget, themeProvider.isLightTheme())
?.let { url -> jitsiMeetPropertiesFactory.create(url) }
setState {
copy(
widget = Success(jitsiWidget),
jitsiUrl = "https://${ppt?.domain}",
confId = ppt?.confId ?: "",
subject = roomName ?: ""
)
copy(widget = Success(jitsiWidget))
}
if (!confIsStarted) {
confIsStarted = true
startConference(jitsiWidget)
}
} else {
setState {
@ -87,7 +90,69 @@ class JitsiCallViewModel @AssistedInject constructor(
.disposeOnClear()
}
private fun startConference(jitsiWidget: Widget) = withState { state ->
val me = session.getRoomMember(session.myUserId, state.roomId)?.toMatrixItem()
val userInfo = JitsiMeetUserInfo().apply {
displayName = me?.getBestName()
avatar = me?.avatarUrl?.let { session.contentUrlResolver().resolveFullSize(it) }?.let { URL(it) }
}
val roomName = session.getRoomSummary(state.roomId)?.displayName
val ppt = widgetService.getWidgetComputedUrl(jitsiWidget, themeProvider.isLightTheme())
?.let { url -> jitsiMeetPropertiesFactory.create(url) }
_viewEvents.post(JitsiCallViewEvents.StartConference(
enableVideo = state.enableVideo,
jitsiUrl = "https://${ppt?.domain}",
subject = roomName ?: "",
confId = ppt?.confId ?: "",
userInfo = userInfo
))
}
override fun handle(action: JitsiCallViewActions) {
when (action) {
is JitsiCallViewActions.SwitchTo -> handleSwitchTo(action)
JitsiCallViewActions.OnConferenceLeft -> handleOnConferenceLeft()
}.exhaustive
}
private fun handleSwitchTo(action: JitsiCallViewActions.SwitchTo) = withState { state ->
// Check if it is the same conf
if (action.args.roomId != state.roomId
|| action.args.widgetId != state.widgetId) {
if (action.withConfirmation) {
// Ask confirmation to switch, but wait a bit for the Activity to quit the PiP mode
viewModelScope.launch {
delay(500)
_viewEvents.post(JitsiCallViewEvents.ConfirmSwitchingConference(action.args))
}
} else {
// Ask the view to leave the conf, then the view will tell us when it's done, to join the new conf
pendingArgs = action.args
_viewEvents.post(JitsiCallViewEvents.LeaveConference)
}
}
}
private fun handleOnConferenceLeft() {
val safePendingArgs = pendingArgs
pendingArgs = null
if (safePendingArgs == null) {
// Quit
_viewEvents.post(JitsiCallViewEvents.Finish)
} else {
setState {
copy(
roomId = safePendingArgs.roomId,
widgetId = safePendingArgs.widgetId,
enableVideo = safePendingArgs.enableVideo,
widget = Uninitialized
)
}
observeWidget(safePendingArgs.roomId, safePendingArgs.widgetId)
}
}
companion object : MvRxViewModelFactory<JitsiCallViewModel, JitsiCallViewState> {
@ -97,8 +162,7 @@ class JitsiCallViewModel @AssistedInject constructor(
@JvmStatic
override fun create(viewModelContext: ViewModelContext, state: JitsiCallViewState): JitsiCallViewModel? {
val callActivity: VectorJitsiActivity = viewModelContext.activity()
val callArgs: VectorJitsiActivity.Args = viewModelContext.args()
return callActivity.viewModelFactory.create(state, callArgs)
return callActivity.viewModelFactory.create(state)
}
override fun initialState(viewModelContext: ViewModelContext): JitsiCallViewState? {

View file

@ -19,16 +19,11 @@ package im.vector.app.features.call.conference
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
import org.jitsi.meet.sdk.JitsiMeetUserInfo
import org.matrix.android.sdk.api.session.widgets.model.Widget
data class JitsiCallViewState(
val roomId: String = "",
val widgetId: String = "",
val enableVideo: Boolean = true,
val jitsiUrl: String = "",
val subject: String = "",
val confId: String = "",
val userInfo: JitsiMeetUserInfo = JitsiMeetUserInfo(),
val enableVideo: Boolean = false,
val widget: Async<Widget> = Uninitialized
) : MvRxState

View file

@ -20,9 +20,12 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.content.res.Configuration
import android.os.Bundle
import android.os.Parcelable
import android.widget.FrameLayout
import androidx.appcompat.app.AlertDialog
import androidx.core.view.isVisible
import androidx.localbroadcastmanager.content.LocalBroadcastManager
import com.airbnb.mvrx.Fail
@ -30,7 +33,9 @@ import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.Success
import com.airbnb.mvrx.viewModel
import com.facebook.react.modules.core.PermissionListener
import im.vector.app.R
import im.vector.app.core.di.ScreenComponent
import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivityJitsiBinding
import kotlinx.parcelize.Parcelize
@ -80,9 +85,39 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee
renderState(it)
}
jitsiViewModel.observeViewEvents {
when (it) {
is JitsiCallViewEvents.StartConference -> configureJitsiView(it)
is JitsiCallViewEvents.ConfirmSwitchingConference -> handleConfirmSwitching(it)
JitsiCallViewEvents.Finish -> finish()
JitsiCallViewEvents.LeaveConference -> handleLeaveConference()
}.exhaustive
}
registerForBroadcastMessages()
}
private fun handleLeaveConference() {
jitsiMeetView?.leave()
}
private fun handleConfirmSwitching(action: JitsiCallViewEvents.ConfirmSwitchingConference) {
AlertDialog.Builder(this)
.setTitle(R.string.dialog_title_warning)
.setMessage(R.string.jitsi_leave_conf_to_join_another_one_content)
.setPositiveButton(R.string.action_switch) { _, _ ->
jitsiViewModel.handle(JitsiCallViewActions.SwitchTo(action.args, false))
}
.setNegativeButton(R.string.cancel, null)
.show()
}
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean,
newConfig: Configuration) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
Timber.w("onPictureInPictureModeChanged($isInPictureInPictureMode)")
}
override fun initUiAndData() {
super.initUiAndData()
jitsiMeetView = JitsiMeetView(this)
@ -96,7 +131,6 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee
is Success -> {
views.jitsiProgressLayout.isVisible = false
jitsiMeetView?.isVisible = true
configureJitsiView(viewState)
}
else -> {
jitsiMeetView?.isVisible = false
@ -105,12 +139,12 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee
}
}
private fun configureJitsiView(viewState: JitsiCallViewState) {
private fun configureJitsiView(startConference: JitsiCallViewEvents.StartConference) {
val jitsiMeetConferenceOptions = JitsiMeetConferenceOptions.Builder()
.setVideoMuted(!viewState.enableVideo)
.setUserInfo(viewState.userInfo)
.setVideoMuted(!startConference.enableVideo)
.setUserInfo(startConference.userInfo)
.apply {
tryOrNull { URL(viewState.jitsiUrl) }?.let {
tryOrNull { URL(startConference.jitsiUrl) }?.let {
setServerURL(it)
}
}
@ -120,15 +154,15 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee
.setFeatureFlag("add-people.enabled", false)
.setFeatureFlag("video-share.enabled", false)
.setFeatureFlag("call-integration.enabled", false)
.setRoom(viewState.confId)
.setSubject(viewState.subject)
.setRoom(startConference.confId)
.setSubject(startConference.subject)
.build()
jitsiMeetView?.join(jitsiMeetConferenceOptions)
}
override fun onPause() {
override fun onStop() {
JitsiMeetActivityDelegate.onHostPause(this)
super.onPause()
super.onStop()
}
override fun onResume() {
@ -147,13 +181,23 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee
super.onDestroy()
}
// override fun onUserLeaveHint() {
// super.onUserLeaveHint()
// jitsiMeetView?.enterPictureInPicture()
// }
override fun onUserLeaveHint() {
super.onUserLeaveHint()
if (packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
jitsiMeetView?.enterPictureInPicture()
}
}
override fun onNewIntent(intent: Intent?) {
JitsiMeetActivityDelegate.onNewIntent(intent)
// Is it a switch to another conf?
intent?.takeIf { it.hasExtra(MvRx.KEY_ARG) }
?.let { intent.getParcelableExtra<Args>(MvRx.KEY_ARG) }
?.let {
jitsiViewModel.handle(JitsiCallViewActions.SwitchTo(it, true))
}
super.onNewIntent(intent)
}
@ -195,7 +239,7 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee
Timber.v("JitsiMeetViewListener.onConferenceTerminated()")
// Do not finish if there is an error
if (data["error"] == null) {
finish()
jitsiViewModel.handle(JitsiCallViewActions.OnConferenceLeft)
}
}
@ -203,7 +247,6 @@ class VectorJitsiActivity : VectorBaseActivity<ActivityJitsiBinding>(), JitsiMee
fun newIntent(context: Context, roomId: String, widgetId: String, enableVideo: Boolean): Intent {
return Intent(context, VectorJitsiActivity::class.java).apply {
putExtra(MvRx.KEY_ARG, Args(roomId, widgetId, enableVideo))
addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
}
}
}

View file

@ -133,6 +133,7 @@
<string name="action_close">Close</string>
<string name="action_copy">Copy</string>
<string name="action_add">Add</string>
<string name="action_switch">Switch</string>
<string name="action_unpublish">Unpublish</string>
<string name="copied_to_clipboard">Copied to clipboard</string>
<string name="disable">Disable</string>
@ -1318,6 +1319,8 @@
<string name="error_jitsi_not_supported_on_old_device">Sorry, conference calls with Jitsi are not supported on old devices (devices with Android OS below 6.0)</string>
<string name="jitsi_leave_conf_to_join_another_one_content">Leave the current conference and switch to the other one?</string>
<string name="room_widget_resource_permission_title">This widget wants to use the following resources:</string>
<string name="room_widget_resource_grant_permission">Allow</string>
<string name="room_widget_resource_decline_permission">Block All</string>