+ hyuwah/DraggableView
+
+ hyuwah/DraggableView is licensed under the MIT License
+ Copyright (c) 2018 Muhammad Wahyudin
+
+
+
+
+Copyright (c) 2018 Muhammad Wahyudin
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
+
+
com.github.piasy:BigImageViewer
diff --git a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt
index 4a3379cb5a..68b212c830 100644
--- a/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt
+++ b/vector/src/main/java/im/vector/app/core/di/VectorComponent.kt
@@ -31,6 +31,7 @@ import im.vector.app.core.network.WifiDetector
import im.vector.app.core.pushers.PushersManager
import im.vector.app.core.utils.AssetReader
import im.vector.app.core.utils.DimensionConverter
+import im.vector.app.features.call.conference.JitsiActiveConferenceHolder
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.configuration.VectorConfiguration
import im.vector.app.features.crypto.keysrequest.KeyRequestHandler
@@ -39,7 +40,6 @@ import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.CurrentSpaceSuggestedRoomListDataSource
import im.vector.app.features.home.room.detail.RoomDetailPendingActionStore
import im.vector.app.features.home.room.detail.timeline.helper.MatrixItemColorProvider
-import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
import im.vector.app.features.html.EventHtmlRenderer
import im.vector.app.features.html.VectorHtmlCompressor
import im.vector.app.features.invite.AutoAcceptInvites
@@ -165,7 +165,7 @@ interface VectorComponent {
fun webRtcCallManager(): WebRtcCallManager
- fun roomSummaryHolder(): RoomSummariesHolder
+ fun jitsiActiveConferenceHolder(): JitsiActiveConferenceHolder
@Component.Factory
interface Factory {
diff --git a/vector/src/main/java/im/vector/app/core/services/CallRingPlayer.kt b/vector/src/main/java/im/vector/app/core/services/CallRingPlayer.kt
index f23eb07424..faa921b99e 100644
--- a/vector/src/main/java/im/vector/app/core/services/CallRingPlayer.kt
+++ b/vector/src/main/java/im/vector/app/core/services/CallRingPlayer.kt
@@ -98,13 +98,10 @@ class CallRingPlayerOutgoing(
private var player: MediaPlayer? = null
fun start() {
- val audioManager: AudioManager? = applicationContext.getSystemService()
+ applicationContext.getSystemService()?.mode = AudioManager.MODE_IN_COMMUNICATION
player?.release()
player = createPlayer()
-
- // Check if sound is enabled
- val ringerMode = audioManager?.ringerMode
- if (player != null && ringerMode == AudioManager.RINGER_MODE_NORMAL) {
+ if (player != null) {
try {
if (player?.isPlaying == false) {
player?.start()
@@ -116,8 +113,6 @@ class CallRingPlayerOutgoing(
Timber.e(failure, "## VOIP Failed to start ringing outgoing")
player = null
}
- } else {
- Timber.v("## VOIP Can't play $player ode $ringerMode")
}
}
diff --git a/vector/src/main/java/im/vector/app/core/ui/views/ActiveConferenceView.kt b/vector/src/main/java/im/vector/app/core/ui/views/ActiveConferenceView.kt
deleted file mode 100644
index 256f2d963e..0000000000
--- a/vector/src/main/java/im/vector/app/core/ui/views/ActiveConferenceView.kt
+++ /dev/null
@@ -1,110 +0,0 @@
-/*
- * Copyright (c) 2020 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.ui.views
-
-import android.content.Context
-import android.text.SpannableString
-import android.text.method.LinkMovementMethod
-import android.text.style.ClickableSpan
-import android.util.AttributeSet
-import android.view.View
-import android.widget.RelativeLayout
-import androidx.core.view.isVisible
-import im.vector.app.R
-import im.vector.app.core.utils.tappableMatchingText
-import im.vector.app.databinding.ViewActiveConferenceViewBinding
-import im.vector.app.features.home.room.detail.RoomDetailViewState
-import im.vector.app.features.themes.ThemeUtils
-import org.matrix.android.sdk.api.session.room.model.Membership
-import org.matrix.android.sdk.api.session.widgets.model.Widget
-import org.matrix.android.sdk.api.session.widgets.model.WidgetType
-
-class ActiveConferenceView @JvmOverloads constructor(
- context: Context,
- attrs: AttributeSet? = null,
- defStyleAttr: Int = 0
-) : RelativeLayout(context, attrs, defStyleAttr) {
-
- interface Callback {
- fun onTapJoinAudio(jitsiWidget: Widget)
- fun onTapJoinVideo(jitsiWidget: Widget)
- fun onDelete(jitsiWidget: Widget)
- }
-
- var callback: Callback? = null
- private var jitsiWidget: Widget? = null
-
- private lateinit var views: ViewActiveConferenceViewBinding
-
- init {
- setupView()
- }
-
- private fun setupView() {
- inflate(context, R.layout.view_active_conference_view, this)
- views = ViewActiveConferenceViewBinding.bind(this)
- setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorPrimary))
-
- // "voice" and "video" texts are underlined and clickable
- val voiceString = context.getString(R.string.ongoing_conference_call_voice)
- val videoString = context.getString(R.string.ongoing_conference_call_video)
-
- val fullMessage = context.getString(R.string.ongoing_conference_call, voiceString, videoString)
-
- val styledText = SpannableString(fullMessage)
- styledText.tappableMatchingText(voiceString, object : ClickableSpan() {
- override fun onClick(widget: View) {
- jitsiWidget?.let {
- callback?.onTapJoinAudio(it)
- }
- }
- })
- styledText.tappableMatchingText(videoString, object : ClickableSpan() {
- override fun onClick(widget: View) {
- jitsiWidget?.let {
- callback?.onTapJoinVideo(it)
- }
- }
- })
-
- views.activeConferenceInfo.apply {
- text = styledText
- movementMethod = LinkMovementMethod.getInstance()
- }
-
- views.deleteWidgetButton.setOnClickListener {
- jitsiWidget?.let { callback?.onDelete(it) }
- }
- }
-
- fun render(state: RoomDetailViewState) {
- val summary = state.asyncRoomSummary()
- if (summary?.membership == Membership.JOIN) {
- // We only display banner for 'live' widgets
- 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 {
- isVisible = false
- }
- }
-}
diff --git a/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt b/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt
index d1332f18dc..2f7eecc22c 100644
--- a/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt
+++ b/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsView.kt
@@ -18,7 +18,9 @@ package im.vector.app.core.ui.views
import android.content.Context
import android.util.AttributeSet
-import android.widget.RelativeLayout
+import android.util.TypedValue
+import android.widget.FrameLayout
+import androidx.appcompat.content.res.AppCompatResources
import im.vector.app.R
import im.vector.app.databinding.ViewCurrentCallsBinding
import im.vector.app.features.call.webrtc.WebRtcCall
@@ -29,7 +31,7 @@ class CurrentCallsView @JvmOverloads constructor(
context: Context,
attrs: AttributeSet? = null,
defStyleAttr: Int = 0
-) : RelativeLayout(context, attrs, defStyleAttr) {
+) : FrameLayout(context, attrs, defStyleAttr) {
interface Callback {
fun onTapToReturnToCall()
@@ -42,25 +44,33 @@ class CurrentCallsView @JvmOverloads constructor(
inflate(context, R.layout.view_current_calls, this)
views = ViewCurrentCallsBinding.bind(this)
setBackgroundColor(ThemeUtils.getColor(context, R.attr.colorPrimary))
+ val outValue = TypedValue().also {
+ context.theme.resolveAttribute(android.R.attr.selectableItemBackground, it, true)
+ }
+ foreground = AppCompatResources.getDrawable(context, outValue.resourceId)
setOnClickListener { callback?.onTapToReturnToCall() }
}
fun render(calls: List, formattedDuration: String) {
- val connectedCalls = calls.filter {
- it.mxCall.state is CallState.Connected
- }
- val heldCalls = connectedCalls.filter {
- it.isLocalOnHold || it.remoteOnHold
- }
- if (connectedCalls.isEmpty()) return
- views.currentCallsInfo.text = if (connectedCalls.size == heldCalls.size) {
- resources.getQuantityString(R.plurals.call_only_paused, heldCalls.size, heldCalls.size)
- } else {
- if (heldCalls.isEmpty()) {
- resources.getString(R.string.call_only_active, formattedDuration)
- } else {
- resources.getQuantityString(R.plurals.call_one_active_and_other_paused, heldCalls.size, formattedDuration, heldCalls.size)
+ val tapToReturnFormat = if (calls.size == 1) {
+ val firstCall = calls.first()
+ when (firstCall.mxCall.state) {
+ is CallState.Idle,
+ is CallState.CreateOffer,
+ is CallState.LocalRinging,
+ is CallState.Dialing -> {
+ resources.getString(R.string.call_ringing)
+ }
+ is CallState.Answering -> {
+ resources.getString(R.string.call_connecting)
+ }
+ else -> {
+ resources.getString(R.string.call_one_active, formattedDuration)
+ }
}
+ } else {
+ resources.getQuantityString(R.plurals.call_active_status, calls.size, calls.size)
}
+ views.currentCallsInfo.text = resources.getString(R.string.call_tap_to_return, tapToReturnFormat)
}
}
diff --git a/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsViewPresenter.kt b/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsViewPresenter.kt
new file mode 100644
index 0000000000..5aee73ee69
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/core/ui/views/CurrentCallsViewPresenter.kt
@@ -0,0 +1,55 @@
+/*
+ * Copyright (c) 2021 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.ui.views
+
+import androidx.core.view.isVisible
+import im.vector.app.features.call.webrtc.WebRtcCall
+
+class CurrentCallsViewPresenter {
+
+ private var currentCallsView: CurrentCallsView? = null
+ private var currentCall: WebRtcCall? = null
+ private var calls: List = emptyList()
+
+ private val tickListener = object : WebRtcCall.Listener {
+ override fun onTick(formattedDuration: String) {
+ currentCallsView?.render(calls, formattedDuration)
+ }
+ }
+
+ fun updateCall(currentCall: WebRtcCall?, calls: List) {
+ this.currentCall?.removeListener(tickListener)
+ this.currentCall = currentCall
+ this.currentCall?.addListener(tickListener)
+ this.calls = calls
+ val hasActiveCall = currentCall != null
+ currentCallsView?.isVisible = hasActiveCall
+ currentCallsView?.render(calls, currentCall?.formattedDuration() ?: "")
+ }
+
+ fun bind(activeCallView: CurrentCallsView, interactionListener: CurrentCallsView.Callback) {
+ this.currentCallsView = activeCallView
+ this.currentCallsView?.callback = interactionListener
+ this.currentCall?.addListener(tickListener)
+ }
+
+ fun unBind() {
+ this.currentCallsView?.callback = null
+ this.currentCall?.removeListener(tickListener)
+ currentCallsView = null
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/core/ui/views/KnownCallsViewHolder.kt b/vector/src/main/java/im/vector/app/core/ui/views/KnownCallsViewHolder.kt
deleted file mode 100644
index d49cf929b6..0000000000
--- a/vector/src/main/java/im/vector/app/core/ui/views/KnownCallsViewHolder.kt
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (c) 2020 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.ui.views
-
-import androidx.core.view.isVisible
-import com.google.android.material.card.MaterialCardView
-import im.vector.app.core.epoxy.onClick
-import im.vector.app.features.call.utils.EglUtils
-import im.vector.app.features.call.webrtc.WebRtcCall
-import org.matrix.android.sdk.api.session.call.CallState
-import org.webrtc.RendererCommon
-import org.webrtc.SurfaceViewRenderer
-
-class KnownCallsViewHolder {
-
- private var activeCallPiP: SurfaceViewRenderer? = null
- private var currentCallsView: CurrentCallsView? = null
- private var pipWrapper: MaterialCardView? = null
- private var currentCall: WebRtcCall? = null
- private var calls: List = emptyList()
-
- private var activeCallPipInitialized = false
-
- private val tickListener = object : WebRtcCall.Listener {
- override fun onTick(formattedDuration: String) {
- currentCallsView?.render(calls, formattedDuration)
- }
- }
-
- fun updateCall(currentCall: WebRtcCall?, calls: List) {
- activeCallPiP?.let {
- this.currentCall?.detachRenderers(listOf(it))
- }
- this.currentCall?.removeListener(tickListener)
- this.currentCall = currentCall
- this.currentCall?.addListener(tickListener)
- this.calls = calls
- val hasActiveCall = currentCall?.mxCall?.state is CallState.Connected
- if (hasActiveCall) {
- val isVideoCall = currentCall?.mxCall?.isVideoCall == true
- if (isVideoCall) initIfNeeded()
- currentCallsView?.isVisible = !isVideoCall
- currentCallsView?.render(calls, currentCall?.formattedDuration() ?: "")
- pipWrapper?.isVisible = isVideoCall
- activeCallPiP?.isVisible = isVideoCall
- activeCallPiP?.let {
- currentCall?.attachViewRenderers(null, it, null)
- }
- } else {
- currentCallsView?.isVisible = false
- activeCallPiP?.isVisible = false
- pipWrapper?.isVisible = false
- activeCallPiP?.let {
- currentCall?.detachRenderers(listOf(it))
- }
- }
- }
-
- private fun initIfNeeded() {
- if (!activeCallPipInitialized && activeCallPiP != null) {
- activeCallPiP?.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL)
- EglUtils.rootEglBase?.let { eglBase ->
- activeCallPiP?.init(eglBase.eglBaseContext, null)
- activeCallPiP?.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_BALANCED)
- activeCallPiP?.setEnableHardwareScaler(true /* enabled */)
- activeCallPiP?.setZOrderMediaOverlay(true)
- activeCallPipInitialized = true
- }
- }
- }
-
- fun bind(activeCallPiP: SurfaceViewRenderer,
- activeCallView: CurrentCallsView,
- pipWrapper: MaterialCardView,
- interactionListener: CurrentCallsView.Callback) {
- this.activeCallPiP = activeCallPiP
- this.currentCallsView = activeCallView
- this.pipWrapper = pipWrapper
- this.currentCallsView?.callback = interactionListener
- pipWrapper.onClick {
- interactionListener.onTapToReturnToCall()
- }
- this.currentCall?.addListener(tickListener)
- }
-
- fun unBind() {
- activeCallPiP?.let {
- currentCall?.detachRenderers(listOf(it))
- }
- if (activeCallPipInitialized) {
- activeCallPiP?.release()
- }
- this.currentCallsView?.callback = null
- this.currentCall?.removeListener(tickListener)
- pipWrapper?.setOnClickListener(null)
- activeCallPiP = null
- currentCallsView = null
- pipWrapper = null
- }
-}
diff --git a/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt b/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt
index f23b26883a..f9e2338077 100644
--- a/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt
+++ b/vector/src/main/java/im/vector/app/features/call/CallControlsBottomSheet.kt
@@ -23,13 +23,9 @@ import android.view.ViewGroup
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import com.airbnb.mvrx.activityViewModel
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R
import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
import im.vector.app.databinding.BottomSheetCallControlsBinding
-import im.vector.app.features.call.audio.CallAudioManager
-
-import me.gujun.android.span.span
class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment() {
override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetCallControlsBinding {
@@ -45,10 +41,6 @@ class CallControlsBottomSheet : VectorBaseBottomSheetDialogFragment {
- showSoundDeviceChooser(it.available, it.current)
- }
- else -> {
- }
- }
- }
- }
-
- private fun showSoundDeviceChooser(available: Set, current: CallAudioManager.Device) {
- val soundDevices = available.map {
- when (it) {
- CallAudioManager.Device.WIRELESS_HEADSET -> span {
- text = getString(R.string.sound_device_wireless_headset)
- textStyle = if (current == it) "bold" else "normal"
- }
- CallAudioManager.Device.PHONE -> span {
- text = getString(R.string.sound_device_phone)
- textStyle = if (current == it) "bold" else "normal"
- }
- CallAudioManager.Device.SPEAKER -> span {
- text = getString(R.string.sound_device_speaker)
- textStyle = if (current == it) "bold" else "normal"
- }
- CallAudioManager.Device.HEADSET -> span {
- text = getString(R.string.sound_device_headset)
- textStyle = if (current == it) "bold" else "normal"
- }
- }
- }
- MaterialAlertDialogBuilder(requireContext())
- .setItems(soundDevices.toTypedArray()) { d, n ->
- d.cancel()
- when (soundDevices[n].toString()) {
- // TODO Make an adapter and handle multiple Bluetooth headsets. Also do not use translations.
- getString(R.string.sound_device_phone) -> {
- callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.PHONE))
- }
- getString(R.string.sound_device_speaker) -> {
- callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.SPEAKER))
- }
- getString(R.string.sound_device_headset) -> {
- callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.HEADSET))
- }
- getString(R.string.sound_device_wireless_headset) -> {
- callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(CallAudioManager.Device.WIRELESS_HEADSET))
- }
- }
- }
- .setNegativeButton(R.string.cancel, null)
- .show()
}
private fun renderState(state: VectorCallViewState) {
- views.callControlsSoundDevice.title = getString(R.string.call_select_sound_device)
- views.callControlsSoundDevice.subTitle = when (state.device) {
- CallAudioManager.Device.PHONE -> getString(R.string.sound_device_phone)
- CallAudioManager.Device.SPEAKER -> getString(R.string.sound_device_speaker)
- CallAudioManager.Device.HEADSET -> getString(R.string.sound_device_headset)
- CallAudioManager.Device.WIRELESS_HEADSET -> getString(R.string.sound_device_wireless_headset)
- }
-
views.callControlsSwitchCamera.isVisible = state.isVideoCall && state.canSwitchCamera
views.callControlsSwitchCamera.subTitle = getString(if (state.isFrontCamera) R.string.call_camera_front else R.string.call_camera_back)
-
if (state.isVideoCall) {
views.callControlsToggleSDHD.isVisible = true
if (state.isHD) {
diff --git a/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt b/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt
index 3742de6271..f0f75370e3 100644
--- a/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt
+++ b/vector/src/main/java/im/vector/app/features/call/CallControlsView.kt
@@ -36,16 +36,19 @@ class CallControlsView @JvmOverloads constructor(
init {
inflate(context, R.layout.view_call_controls, this)
views = ViewCallControlsBinding.bind(this)
-
+ views.audioSettingsIcon.setOnClickListener { didTapAudioSettings() }
views.ringingControlAccept.setOnClickListener { acceptIncomingCall() }
views.ringingControlDecline.setOnClickListener { declineIncomingCall() }
views.endCallIcon.setOnClickListener { endOngoingCall() }
views.muteIcon.setOnClickListener { toggleMute() }
views.videoToggleIcon.setOnClickListener { toggleVideo() }
- views.openChatIcon.setOnClickListener { returnToChat() }
views.moreIcon.setOnClickListener { moreControlOption() }
}
+ private fun didTapAudioSettings() {
+ interactionListener?.didTapAudioSettings()
+ }
+
private fun acceptIncomingCall() {
interactionListener?.didAcceptIncomingCall()
}
@@ -66,10 +69,6 @@ class CallControlsView @JvmOverloads constructor(
interactionListener?.didTapToggleVideo()
}
- private fun returnToChat() {
- interactionListener?.returnToChat()
- }
-
private fun moreControlOption() {
interactionListener?.didTapMore()
}
@@ -77,49 +76,36 @@ class CallControlsView @JvmOverloads constructor(
fun updateForState(state: VectorCallViewState) {
val callState = state.callState.invoke()
if (state.isAudioMuted) {
- views.muteIcon.setImageResource(R.drawable.ic_microphone_off)
+ views.muteIcon.setImageResource(R.drawable.ic_mic_off)
views.muteIcon.contentDescription = resources.getString(R.string.a11y_unmute_microphone)
} else {
- views.muteIcon.setImageResource(R.drawable.ic_microphone_on)
+ views.muteIcon.setImageResource(R.drawable.ic_mic_on)
views.muteIcon.contentDescription = resources.getString(R.string.a11y_mute_microphone)
}
if (state.isVideoEnabled) {
views.videoToggleIcon.setImageResource(R.drawable.ic_video)
views.videoToggleIcon.contentDescription = resources.getString(R.string.a11y_stop_camera)
} else {
- views.videoToggleIcon.setImageResource(R.drawable.ic_video_off)
+ views.videoToggleIcon.setImageResource(R.drawable.ic_video_off)
views.videoToggleIcon.contentDescription = resources.getString(R.string.a11y_start_camera)
}
when (callState) {
- is CallState.Idle,
- is CallState.Dialing,
- is CallState.Answering -> {
- views.ringingControls.isVisible = true
- views.ringingControlAccept.isVisible = false
- views.ringingControlDecline.isVisible = true
- views.connectedControls.isVisible = false
- }
is CallState.LocalRinging -> {
views.ringingControls.isVisible = true
views.ringingControlAccept.isVisible = true
views.ringingControlDecline.isVisible = true
views.connectedControls.isVisible = false
}
- is CallState.Connected -> {
- if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
- views.ringingControls.isVisible = false
- views.connectedControls.isVisible = true
- views.videoToggleIcon.isVisible = state.isVideoCall
- } else {
- views.ringingControls.isVisible = true
- views.ringingControlAccept.isVisible = false
- views.ringingControlDecline.isVisible = true
- views.connectedControls.isVisible = false
- }
+ is CallState.Connected,
+ is CallState.Dialing,
+ is CallState.Answering -> {
+ views.ringingControls.isVisible = false
+ views.connectedControls.isVisible = true
+ views.videoToggleIcon.isVisible = state.isVideoCall
+ views.moreIcon.isVisible = callState is CallState.Connected && callState.iceConnectionState == MxPeerConnectionState.CONNECTED
}
- is CallState.Ended,
- null -> {
+ else -> {
views.ringingControls.isVisible = false
views.connectedControls.isVisible = false
}
@@ -127,12 +113,12 @@ class CallControlsView @JvmOverloads constructor(
}
interface InteractionListener {
+ fun didTapAudioSettings()
fun didAcceptIncomingCall()
fun didDeclineIncomingCall()
fun didEndCall()
fun didTapToggleMute()
fun didTapToggleVideo()
- fun returnToChat()
fun didTapMore()
}
}
diff --git a/vector/src/main/java/im/vector/app/features/call/CallSoundDeviceChooserBottomSheet.kt b/vector/src/main/java/im/vector/app/features/call/CallSoundDeviceChooserBottomSheet.kt
new file mode 100644
index 0000000000..649b7fee3e
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/call/CallSoundDeviceChooserBottomSheet.kt
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2020 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.call
+
+import android.os.Bundle
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import com.airbnb.epoxy.SimpleEpoxyController
+import com.airbnb.mvrx.activityViewModel
+import im.vector.app.core.epoxy.bottomsheet.BottomSheetActionItem_
+import im.vector.app.core.extensions.configureWith
+import im.vector.app.core.platform.VectorBaseBottomSheetDialogFragment
+import im.vector.app.databinding.BottomSheetGenericListBinding
+import im.vector.app.features.call.audio.CallAudioManager
+import im.vector.app.features.home.room.list.actions.RoomListQuickActionsBottomSheet
+
+class CallSoundDeviceChooserBottomSheet : VectorBaseBottomSheetDialogFragment() {
+ override fun getBinding(inflater: LayoutInflater, container: ViewGroup?): BottomSheetGenericListBinding {
+ return BottomSheetGenericListBinding.inflate(inflater, container, false)
+ }
+
+ private val callViewModel: VectorCallViewModel by activityViewModel()
+ private val controller = SimpleEpoxyController()
+
+ override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ super.onViewCreated(view, savedInstanceState)
+ views.bottomSheetRecyclerView.configureWith(controller, hasFixedSize = false)
+ callViewModel.observeViewEvents {
+ when (it) {
+ is VectorCallViewEvents.ShowSoundDeviceChooser -> {
+ render(it.available, it.current)
+ }
+ else -> {
+ }
+ }
+ }
+ callViewModel.handle(VectorCallViewActions.SwitchSoundDevice)
+ }
+
+ private fun render(available: Set, current: CallAudioManager.Device) {
+ val models = available.map { device ->
+ val title = when (device) {
+ is CallAudioManager.Device.WirelessHeadset -> device.name ?: getString(device.titleRes)
+ else -> getString(device.titleRes)
+ }
+ BottomSheetActionItem_().apply {
+ id(device.titleRes)
+ text(title)
+ iconRes(device.drawableRes)
+ selected(current == device)
+ listener {
+ callViewModel.handle(VectorCallViewActions.ChangeAudioDevice(device))
+ dismiss()
+ }
+ }
+ }
+ controller.setModels(models)
+ }
+
+ companion object {
+ fun newInstance(): RoomListQuickActionsBottomSheet {
+ return RoomListQuickActionsBottomSheet()
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/call/SharedKnownCallsViewModel.kt b/vector/src/main/java/im/vector/app/features/call/SharedKnownCallsViewModel.kt
index b33edd09e0..fb5e48af98 100644
--- a/vector/src/main/java/im/vector/app/features/call/SharedKnownCallsViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/call/SharedKnownCallsViewModel.kt
@@ -32,14 +32,12 @@ class SharedKnownCallsViewModel @Inject constructor(
val callListener = object : WebRtcCall.Listener {
override fun onStateUpdate(call: MxCall) {
- // post it-self
- liveKnownCalls.postValue(liveKnownCalls.value)
+ liveKnownCalls.postValue(callManager.getCalls())
}
override fun onHoldUnhold() {
super.onHoldUnhold()
- // post it-self
- liveKnownCalls.postValue(liveKnownCalls.value)
+ liveKnownCalls.postValue(callManager.getCalls())
}
}
diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt
index 6f6010be15..f71dcc0635 100644
--- a/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/call/VectorCallActivity.kt
@@ -17,13 +17,17 @@
package im.vector.app.features.call
import android.app.KeyguardManager
+import android.app.PictureInPictureParams
import android.content.Context
import android.content.Intent
import android.content.Intent.FLAG_ACTIVITY_CLEAR_TOP
+import android.content.res.Configuration
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.os.Parcelable
+import android.util.Rational
+import android.view.MenuItem
import android.view.View
import android.view.WindowManager
import androidx.annotation.StringRes
@@ -35,9 +39,11 @@ import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.viewModel
import com.airbnb.mvrx.withState
+import com.google.android.material.card.MaterialCardView
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import im.vector.app.R
import im.vector.app.core.di.ScreenComponent
+import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.utils.PERMISSIONS_FOR_AUDIO_IP_CALL
import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL
@@ -52,6 +58,8 @@ import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.RoomDetailActivity
import im.vector.app.features.home.room.detail.RoomDetailArgs
+import io.github.hyuwah.draggableviewlib.DraggableView
+import io.github.hyuwah.draggableviewlib.setupDraggable
import io.reactivex.android.schedulers.AndroidSchedulers
import kotlinx.parcelize.Parcelize
import org.matrix.android.sdk.api.extensions.orFalse
@@ -87,7 +95,6 @@ class VectorCallActivity : VectorBaseActivity(), CallContro
}
private val callViewModel: VectorCallViewModel by viewModel()
- private lateinit var callArgs: CallArgs
@Inject lateinit var callManager: WebRtcCallManager
@Inject lateinit var viewModelFactory: VectorCallViewModel.Factory
@@ -99,6 +106,8 @@ class VectorCallActivity : VectorBaseActivity(), CallContro
}
private var rootEglBase: EglBase? = null
+ private var pipDraggrableView: DraggableView? = null
+ private var otherCallDraggableView: DraggableView? = null
var surfaceRenderersAreInitialized = false
@@ -115,13 +124,6 @@ class VectorCallActivity : VectorBaseActivity(), CallContro
window.navigationBarColor = Color.BLACK
super.onCreate(savedInstanceState)
- if (intent.hasExtra(MvRx.KEY_ARG)) {
- callArgs = intent.getParcelableExtra(MvRx.KEY_ARG)!!
- } else {
- Timber.tag(loggerTag.value).e("missing callArgs for VectorCall Activity")
- finish()
- }
-
Timber.tag(loggerTag.value).v("EXTRA_MODE is ${intent.getStringExtra(EXTRA_MODE)}")
if (intent.getStringExtra(EXTRA_MODE) == INCOMING_RINGING) {
turnScreenOnAndKeyguardOff()
@@ -129,6 +131,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro
if (savedInstanceState != null) {
(supportFragmentManager.findFragmentByTag(FRAGMENT_DIAL_PAD_TAG) as? CallDialPadBottomSheet)?.callback = dialPadCallback
}
+ setSupportActionBar(views.callToolbar)
configureCallViews()
callViewModel.subscribe(this) {
@@ -149,25 +152,89 @@ class VectorCallActivity : VectorBaseActivity(), CallContro
}
.disposeOnDestroy()
- if (callArgs.isVideoCall) {
- if (checkPermissions(PERMISSIONS_FOR_VIDEO_IP_CALL, this, permissionCameraLauncher, R.string.permissions_rationale_msg_camera_and_audio)) {
- start()
- }
- } else {
- if (checkPermissions(PERMISSIONS_FOR_AUDIO_IP_CALL, this, permissionCameraLauncher, R.string.permissions_rationale_msg_record_audio)) {
- start()
+ callViewModel.selectSubscribe(this, VectorCallViewState::callId, VectorCallViewState::isVideoCall) { _, isVideoCall ->
+ if (isVideoCall) {
+ if (checkPermissions(PERMISSIONS_FOR_VIDEO_IP_CALL, this, permissionCameraLauncher, R.string.permissions_rationale_msg_camera_and_audio)) {
+ setupRenderersIfNeeded()
+ }
+ } else {
+ if (checkPermissions(PERMISSIONS_FOR_AUDIO_IP_CALL, this, permissionCameraLauncher, R.string.permissions_rationale_msg_record_audio)) {
+ setupRenderersIfNeeded()
+ }
}
}
}
+ override fun onNewIntent(intent: Intent?) {
+ super.onNewIntent(intent)
+ intent?.takeIf { it.hasExtra(MvRx.KEY_ARG) }
+ ?.let { intent.getParcelableExtra(MvRx.KEY_ARG) }
+ ?.let {
+ callViewModel.handle(VectorCallViewActions.SwitchCall(it))
+ }
+ }
+
+ override fun getMenuRes() = R.menu.vector_call
+
+ override fun onUserLeaveHint() {
+ enterPictureInPictureIfRequired()
+ }
+
+ override fun onBackPressed() {
+ if (!enterPictureInPictureIfRequired()) {
+ super.onBackPressed()
+ }
+ }
+
+ private fun enterPictureInPictureIfRequired(): Boolean = withState(callViewModel) {
+ if (!it.isVideoCall) {
+ false
+ } else 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()
+ renderPiPMode(it)
+ enterPictureInPictureMode(params)
+ } else {
+ false
+ }
+ }
+
+ private fun isInPictureInPictureModeSafe(): Boolean {
+ return Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && isInPictureInPictureMode
+ }
+
+ override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean, newConfig: Configuration) = withState(callViewModel) {
+ renderState(it)
+ }
+
+ override fun onOptionsItemSelected(item: MenuItem): Boolean {
+ if (item.itemId == R.id.menu_call_open_chat) {
+ returnToChat()
+ return true
+ } else if (item.itemId == android.R.id.home) {
+ // We check here as we want PiP in some cases
+ onBackPressed()
+ return true
+ }
+ return super.onOptionsItemSelected(item)
+ }
+
override fun onDestroy() {
- callManager.getCallById(callArgs.callId)?.detachRenderers(listOf(views.pipRenderer, views.fullscreenRenderer))
+ detachRenderersIfNeeded()
+ turnScreenOffAndKeyguardOn()
+ super.onDestroy()
+ }
+
+ private fun detachRenderersIfNeeded() {
+ val callId = withState(callViewModel) { it.callId }
+ callManager.getCallById(callId)?.detachRenderers(listOf(views.pipRenderer, views.fullscreenRenderer))
if (surfaceRenderersAreInitialized) {
views.pipRenderer.release()
views.fullscreenRenderer.release()
+ surfaceRenderersAreInitialized = false
}
- turnScreenOffAndKeyguardOn()
- super.onDestroy()
}
private fun renderState(state: VectorCallViewState) {
@@ -176,53 +243,57 @@ class VectorCallActivity : VectorBaseActivity(), CallContro
finish()
return
}
+ if (isInPictureInPictureModeSafe()) {
+ renderPiPMode(state)
+ } else {
+ renderFullScreenMode(state)
+ }
+ }
+ private fun renderFullScreenMode(state: VectorCallViewState) {
+ views.callToolbar.isVisible = true
+ views.callControlsView.isVisible = true
views.callControlsView.updateForState(state)
val callState = state.callState.invoke()
- views.callConnectingProgress.isVisible = false
views.callActionText.setOnClickListener(null)
views.callActionText.isVisible = false
views.smallIsHeldIcon.isVisible = false
when (callState) {
is CallState.Idle,
is CallState.CreateOffer,
- is CallState.Dialing -> {
- views.callVideoGroup.isInvisible = true
+ is CallState.LocalRinging,
+ is CallState.Dialing -> {
+ views.fullscreenRenderer.isVisible = false
+ views.pipRendererWrapper.isVisible = false
views.callInfoGroup.isVisible = true
- views.callStatusText.setText(R.string.call_ring)
+ views.callToolbar.setSubtitle(R.string.call_ringing)
configureCallInfo(state)
}
-
- is CallState.LocalRinging -> {
- views.callVideoGroup.isInvisible = true
+ is CallState.Answering -> {
+ views.fullscreenRenderer.isVisible = false
+ views.pipRendererWrapper.isVisible = false
views.callInfoGroup.isVisible = true
- views.callStatusText.text = null
+ views.callToolbar.setSubtitle(R.string.call_connecting)
configureCallInfo(state)
}
-
- is CallState.Answering -> {
- views.callVideoGroup.isInvisible = true
- views.callInfoGroup.isVisible = true
- views.callStatusText.setText(R.string.call_connecting)
- views.callConnectingProgress.isVisible = true
- configureCallInfo(state)
- }
- is CallState.Connected -> {
+ is CallState.Connected -> {
+ views.callToolbar.subtitle = state.formattedDuration
if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
if (state.isLocalOnHold || state.isRemoteOnHold) {
views.smallIsHeldIcon.isVisible = true
- views.callVideoGroup.isInvisible = true
+ views.fullscreenRenderer.isVisible = false
+ views.pipRendererWrapper.isVisible = false
views.callInfoGroup.isVisible = true
configureCallInfo(state, blurAvatar = true)
if (state.isRemoteOnHold) {
views.callActionText.setText(R.string.call_resume_action)
views.callActionText.isVisible = true
views.callActionText.setOnClickListener { callViewModel.handle(VectorCallViewActions.ToggleHoldResume) }
- views.callStatusText.setText(R.string.call_held_by_you)
+ views.callToolbar.setSubtitle(R.string.call_held_by_you)
} else {
views.callActionText.isInvisible = true
state.callInfo?.opponentUserItem?.let {
- views.callStatusText.text = getString(R.string.call_held_by_user, it.getBestName())
+ views.callToolbar.subtitle = getString(R.string.call_held_by_user, it.getBestName())
}
}
} else if (state.transferee !is VectorCallViewState.TransfereeState.NoTransferee) {
@@ -234,43 +305,90 @@ class VectorCallActivity : VectorBaseActivity(), CallContro
views.callActionText.text = getString(R.string.call_transfer_transfer_to_title, transfereeName)
views.callActionText.isVisible = true
views.callActionText.setOnClickListener { callViewModel.handle(VectorCallViewActions.TransferCall) }
- views.callStatusText.text = state.formattedDuration
configureCallInfo(state)
} else {
- views.callStatusText.text = state.formattedDuration
configureCallInfo(state)
- if (callArgs.isVideoCall) {
- views.callVideoGroup.isVisible = true
+ if (state.isVideoCall) {
+ views.fullscreenRenderer.isVisible = true
+ views.pipRendererWrapper.isVisible = true
views.callInfoGroup.isVisible = false
views.pipRenderer.isVisible = !state.isVideoCaptureInError && state.otherKnownCallInfo == null
} else {
- views.callVideoGroup.isInvisible = true
+ views.fullscreenRenderer.isVisible = false
+ views.pipRendererWrapper.isVisible = false
views.callInfoGroup.isVisible = true
}
}
} else {
// This state is not final, if you change network, new candidates will be sent
- views.callVideoGroup.isInvisible = true
+ views.fullscreenRenderer.isVisible = false
+ views.pipRendererWrapper.isVisible = false
views.callInfoGroup.isVisible = true
configureCallInfo(state)
- views.callStatusText.setText(R.string.call_connecting)
- views.callConnectingProgress.isVisible = true
+ views.callToolbar.setSubtitle(R.string.call_connecting)
}
}
- is CallState.Ended -> {
- views.callVideoGroup.isInvisible = true
+ is CallState.Ended -> {
+ views.fullscreenRenderer.isVisible = false
+ views.pipRendererWrapper.isVisible = false
views.callInfoGroup.isVisible = true
- views.callStatusText.setText(R.string.call_ended)
+ views.callToolbar.setSubtitle(R.string.call_ended)
configureCallInfo(state)
}
- else -> {
- views.callVideoGroup.isInvisible = true
+ else -> {
+ views.fullscreenRenderer.isVisible = false
+ views.pipRendererWrapper.isVisible = false
views.callInfoGroup.isInvisible = true
}
}
}
+ private fun renderPiPMode(state: VectorCallViewState) {
+ val callState = state.callState.invoke()
+ views.callToolbar.isVisible = false
+ views.callControlsView.isVisible = false
+ views.pipRendererWrapper.isVisible = false
+ views.pipRenderer.isVisible = false
+ views.callActionText.isVisible = false
+ when (callState) {
+ is CallState.Idle,
+ is CallState.CreateOffer,
+ is CallState.LocalRinging,
+ is CallState.Dialing,
+ is CallState.Answering -> {
+ views.fullscreenRenderer.isVisible = false
+ views.callInfoGroup.isVisible = false
+ }
+ is CallState.Connected -> {
+ if (callState.iceConnectionState == MxPeerConnectionState.CONNECTED) {
+ if (state.isLocalOnHold || state.isRemoteOnHold) {
+ views.smallIsHeldIcon.isVisible = true
+ views.fullscreenRenderer.isVisible = false
+ views.callInfoGroup.isVisible = true
+ configureCallInfo(state, blurAvatar = true)
+ } else {
+ configureCallInfo(state)
+ views.fullscreenRenderer.isVisible = true
+ views.callInfoGroup.isVisible = false
+ }
+ } else {
+ views.callInfoGroup.isVisible = false
+ }
+ }
+ else -> {
+ views.fullscreenRenderer.isVisible = false
+ views.callInfoGroup.isVisible = false
+ }
+ }
+ }
+
private fun handleCallEnded(callState: CallState.Ended) {
+ if (isInPictureInPictureModeSafe()) {
+ val startIntent = Intent(this, VectorCallActivity::class.java).apply {
+ flags = Intent.FLAG_ACTIVITY_REORDER_TO_FRONT
+ }
+ startActivity(startIntent)
+ }
when (callState.reason) {
EndCallReason.USER_BUSY -> {
showEndCallDialog(R.string.call_ended_user_busy_title, R.string.call_ended_user_busy_description)
@@ -300,9 +418,14 @@ class VectorCallActivity : VectorBaseActivity(), CallContro
val colorFilter = ContextCompat.getColor(this, R.color.bg_call_screen_blur)
avatarRenderer.renderBlur(it, views.bgCallView, sampling = 20, rounded = false, colorFilter = colorFilter, addPlaceholder = false)
if (state.transferee is VectorCallViewState.TransfereeState.NoTransferee) {
- views.participantNameText.text = it.getBestName()
+ views.participantNameText.setTextOrHide(null)
+ views.callToolbar.title = if (state.isVideoCall) {
+ getString(R.string.video_call_with_participant, it.getBestName())
+ } else {
+ getString(R.string.audio_call_with_participant, it.getBestName())
+ }
} else {
- views.participantNameText.text = getString(R.string.call_transfer_consulting_with, it.getBestName())
+ views.participantNameText.setTextOrHide(getString(R.string.call_transfer_consulting_with, it.getBestName()))
}
if (blurAvatar) {
avatarRenderer.renderBlur(it, views.otherMemberAvatar, sampling = 2, rounded = true, colorFilter = colorFilter, addPlaceholder = true)
@@ -310,7 +433,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro
avatarRenderer.render(it, views.otherMemberAvatar)
}
}
- if (state.otherKnownCallInfo?.opponentUserItem == null) {
+ if (state.otherKnownCallInfo?.opponentUserItem == null || isInPictureInPictureModeSafe()) {
views.otherKnownCallLayout.isVisible = false
} else {
val otherCall = callManager.getCallById(state.otherKnownCallInfo.callId)
@@ -324,7 +447,7 @@ class VectorCallActivity : VectorBaseActivity(), CallContro
addPlaceholder = true
)
views.otherKnownCallLayout.isVisible = true
- views.otherSmallIsHeldIcon.isVisible = otherCall?.let { it.isLocalOnHold || it.remoteOnHold }.orFalse()
+ views.otherSmallIsHeldIcon.isVisible = otherCall?.let { it.isLocalOnHold || it.isRemoteOnHold }.orFalse()
}
}
@@ -333,44 +456,60 @@ class VectorCallActivity : VectorBaseActivity(), CallContro
views.otherKnownCallLayout.setOnClickListener {
withState(callViewModel) {
val otherCall = callManager.getCallById(it.otherKnownCallInfo?.callId ?: "") ?: return@withState
- startActivity(newIntent(this, otherCall, null))
- finish()
+ val callArgs = CallArgs(
+ signalingRoomId = otherCall.nativeRoomId,
+ callId = otherCall.callId,
+ participantUserId = otherCall.mxCall.opponentUserId,
+ isIncomingCall = !otherCall.mxCall.isOutgoing,
+ isVideoCall = otherCall.mxCall.isVideoCall
+ )
+ callViewModel.handle(VectorCallViewActions.SwitchCall(callArgs))
}
}
+ views.pipRendererWrapper.setOnClickListener {
+ callViewModel.handle(VectorCallViewActions.ToggleCamera)
+ }
+ pipDraggrableView = views.pipRendererWrapper.setupDraggable()
+ .setStickyMode(DraggableView.Mode.STICKY_XY)
+ .build()
+
+ otherCallDraggableView = views.otherKnownCallLayout.setupDraggable()
+ .setStickyMode(DraggableView.Mode.STICKY_XY)
+ .build()
}
private val permissionCameraLauncher = registerForPermissionsResult { allGranted, _ ->
if (allGranted) {
- start()
+ setupRenderersIfNeeded()
} else {
// TODO display something
finish()
}
}
- private fun start() {
+ private fun setupRenderersIfNeeded() {
+ detachRenderersIfNeeded()
rootEglBase = EglUtils.rootEglBase ?: return Unit.also {
Timber.tag(loggerTag.value).v("rootEglBase is null")
finish()
}
// Init Picture in Picture renderer
- views.pipRenderer.init(rootEglBase!!.eglBaseContext, null)
- views.pipRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
-
+ views.pipRenderer.apply {
+ init(rootEglBase!!.eglBaseContext, null)
+ setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_BALANCED)
+ setEnableHardwareScaler(true)
+ setZOrderMediaOverlay(true)
+ }
// Init Full Screen renderer
views.fullscreenRenderer.init(rootEglBase!!.eglBaseContext, null)
views.fullscreenRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL)
-
- views.pipRenderer.setZOrderMediaOverlay(true)
- views.pipRenderer.setEnableHardwareScaler(true /* enabled */)
views.fullscreenRenderer.setEnableHardwareScaler(true /* enabled */)
- callManager.getCallById(callArgs.callId)?.attachViewRenderers(views.pipRenderer, views.fullscreenRenderer,
- intent.getStringExtra(EXTRA_MODE)?.takeIf { isFirstCreation() })
-
- views.pipRenderer.setOnClickListener {
- callViewModel.handle(VectorCallViewActions.ToggleCamera)
+ val callId = withState(callViewModel) { it.callId }
+ callManager.getCallById(callId)?.also { webRtcCall ->
+ webRtcCall.attachViewRenderers(views.pipRenderer, views.fullscreenRenderer, intent.getStringExtra(EXTRA_MODE))
+ intent.removeExtra(EXTRA_MODE)
}
surfaceRenderersAreInitialized = true
}
@@ -387,7 +526,8 @@ class VectorCallActivity : VectorBaseActivity(), CallContro
}.show(supportFragmentManager, FRAGMENT_DIAL_PAD_TAG)
}
is VectorCallViewEvents.ShowCallTransferScreen -> {
- navigator.openCallTransfer(this, callArgs.callId)
+ val callId = withState(callViewModel) { it.callId }
+ navigator.openCallTransfer(this, callId)
}
null -> {
}
@@ -406,37 +546,8 @@ class VectorCallActivity : VectorBaseActivity(), CallContro
.show()
}
- companion object {
- private const val EXTRA_MODE = "EXTRA_MODE"
- private const val FRAGMENT_DIAL_PAD_TAG = "FRAGMENT_DIAL_PAD_TAG"
-
- const val OUTGOING_CREATED = "OUTGOING_CREATED"
- const val INCOMING_RINGING = "INCOMING_RINGING"
- const val INCOMING_ACCEPT = "INCOMING_ACCEPT"
-
- fun newIntent(context: Context, call: WebRtcCall, mode: String?): Intent {
- return Intent(context, VectorCallActivity::class.java).apply {
- // what could be the best flags?
- flags = Intent.FLAG_ACTIVITY_NEW_TASK
- putExtra(MvRx.KEY_ARG, CallArgs(call.nativeRoomId, call.callId, call.mxCall.opponentUserId, !call.mxCall.isOutgoing, call.mxCall.isVideoCall))
- putExtra(EXTRA_MODE, mode)
- }
- }
-
- fun newIntent(context: Context,
- callId: String,
- signalingRoomId: String,
- otherUserId: String,
- isIncomingCall: Boolean,
- isVideoCall: Boolean,
- mode: String?): Intent {
- return Intent(context, VectorCallActivity::class.java).apply {
- // what could be the best flags?
- flags = FLAG_ACTIVITY_CLEAR_TOP
- putExtra(MvRx.KEY_ARG, CallArgs(signalingRoomId, callId, otherUserId, isIncomingCall, isVideoCall))
- putExtra(EXTRA_MODE, mode)
- }
- }
+ override fun didTapAudioSettings() {
+ CallSoundDeviceChooserBottomSheet().show(supportFragmentManager, "SoundDeviceChooser")
}
override fun didAcceptIncomingCall() {
@@ -459,8 +570,9 @@ class VectorCallActivity : VectorBaseActivity(), CallContro
callViewModel.handle(VectorCallViewActions.ToggleVideo)
}
- override fun returnToChat() {
- val args = RoomDetailArgs(callArgs.signalingRoomId)
+ private fun returnToChat() {
+ val roomId = withState(callViewModel) { it.roomId }
+ val args = RoomDetailArgs(roomId)
val intent = RoomDetailActivity.newIntent(this, args).apply {
flags = FLAG_ACTIVITY_CLEAR_TOP
}
@@ -508,4 +620,37 @@ class VectorCallActivity : VectorBaseActivity(), CallContro
)
}
}
+
+ companion object {
+ private const val EXTRA_MODE = "EXTRA_MODE"
+ private const val FRAGMENT_DIAL_PAD_TAG = "FRAGMENT_DIAL_PAD_TAG"
+
+ const val OUTGOING_CREATED = "OUTGOING_CREATED"
+ const val INCOMING_RINGING = "INCOMING_RINGING"
+ const val INCOMING_ACCEPT = "INCOMING_ACCEPT"
+
+ fun newIntent(context: Context, call: WebRtcCall, mode: String?): Intent {
+ return Intent(context, VectorCallActivity::class.java).apply {
+ // what could be the best flags?
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ putExtra(MvRx.KEY_ARG, CallArgs(call.nativeRoomId, call.callId, call.mxCall.opponentUserId, !call.mxCall.isOutgoing, call.mxCall.isVideoCall))
+ putExtra(EXTRA_MODE, mode)
+ }
+ }
+
+ fun newIntent(context: Context,
+ callId: String,
+ signalingRoomId: String,
+ otherUserId: String,
+ isIncomingCall: Boolean,
+ isVideoCall: Boolean,
+ mode: String?): Intent {
+ return Intent(context, VectorCallActivity::class.java).apply {
+ // what could be the best flags?
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ putExtra(MvRx.KEY_ARG, CallArgs(signalingRoomId, callId, otherUserId, isIncomingCall, isVideoCall))
+ putExtra(EXTRA_MODE, mode)
+ }
+ }
+ }
}
diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt
index a332153aaa..1834c05e41 100644
--- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt
+++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewActions.kt
@@ -29,6 +29,8 @@ sealed class VectorCallViewActions : VectorViewModelAction {
data class ChangeAudioDevice(val device: CallAudioManager.Device) : VectorCallViewActions()
object OpenDialPad: VectorCallViewActions()
data class SendDtmfDigit(val digit: String) : VectorCallViewActions()
+ data class SwitchCall(val callArgs: CallArgs) : VectorCallViewActions()
+
object SwitchSoundDevice : VectorCallViewActions()
object HeadSetButtonPressed : VectorCallViewActions()
object ToggleCamera : VectorCallViewActions()
diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt
index 4b3a8e84bc..63ba83bdbc 100644
--- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewModel.kt
@@ -60,7 +60,7 @@ class VectorCallViewModel @AssistedInject constructor(
setState {
copy(
isLocalOnHold = call?.isLocalOnHold ?: false,
- isRemoteOnHold = call?.remoteOnHold ?: false
+ isRemoteOnHold = call?.isRemoteOnHold ?: false
)
}
}
@@ -144,7 +144,7 @@ class VectorCallViewModel @AssistedInject constructor(
override fun onAudioDevicesChange() {
val currentSoundDevice = callManager.audioManager.selectedDevice ?: return
- if (currentSoundDevice == CallAudioManager.Device.PHONE) {
+ if (currentSoundDevice == CallAudioManager.Device.Phone) {
proximityManager.start()
} else {
proximityManager.stop()
@@ -172,7 +172,12 @@ class VectorCallViewModel @AssistedInject constructor(
}
init {
- val webRtcCall = callManager.getCallById(initialState.callId)
+ setupCallWithCurrentState()
+ }
+
+ private fun setupCallWithCurrentState() = withState { state ->
+ call?.removeListener(callListener)
+ val webRtcCall = callManager.getCallById(state.callId)
if (webRtcCall == null) {
setState {
copy(callState = Fail(IllegalArgumentException("No call")))
@@ -182,17 +187,19 @@ class VectorCallViewModel @AssistedInject constructor(
callManager.addCurrentCallListener(currentCallListener)
webRtcCall.addListener(callListener)
val currentSoundDevice = callManager.audioManager.selectedDevice
- if (currentSoundDevice == CallAudioManager.Device.PHONE) {
+ if (currentSoundDevice == CallAudioManager.Device.Phone) {
proximityManager.start()
}
setState {
copy(
+ isAudioMuted = webRtcCall.micMuted,
+ isVideoEnabled = !webRtcCall.videoMuted,
isVideoCall = webRtcCall.mxCall.isVideoCall,
callState = Success(webRtcCall.mxCall.state),
callInfo = webRtcCall.extractCallInfo(),
- device = currentSoundDevice ?: CallAudioManager.Device.PHONE,
+ device = currentSoundDevice ?: CallAudioManager.Device.Phone,
isLocalOnHold = webRtcCall.isLocalOnHold,
- isRemoteOnHold = webRtcCall.remoteOnHold,
+ isRemoteOnHold = webRtcCall.isRemoteOnHold,
availableDevices = callManager.audioManager.availableDevices,
isFrontCamera = webRtcCall.currentCameraType() == CameraType.FRONT,
canSwitchCamera = webRtcCall.canSwitchCamera(),
@@ -225,6 +232,7 @@ class VectorCallViewModel @AssistedInject constructor(
override fun onCleared() {
callManager.removeCurrentCallListener(currentCallListener)
call?.removeListener(callListener)
+ call = null
proximityManager.stop()
super.onCleared()
}
@@ -302,9 +310,13 @@ class VectorCallViewModel @AssistedInject constructor(
VectorCallViewEvents.ShowCallTransferScreen
)
}
- VectorCallViewActions.TransferCall -> {
+ VectorCallViewActions.TransferCall -> {
handleCallTransfer()
}
+ is VectorCallViewActions.SwitchCall -> {
+ setState { VectorCallViewState(action.callArgs) }
+ setupCallWithCurrentState()
+ }
}.exhaustive
}
diff --git a/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt b/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt
index 3e7791cc08..a351806e1a 100644
--- a/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt
+++ b/vector/src/main/java/im/vector/app/features/call/VectorCallViewState.kt
@@ -35,7 +35,7 @@ data class VectorCallViewState(
val isHD: Boolean = false,
val isFrontCamera: Boolean = true,
val canSwitchCamera: Boolean = true,
- val device: CallAudioManager.Device = CallAudioManager.Device.PHONE,
+ val device: CallAudioManager.Device = CallAudioManager.Device.Phone,
val availableDevices: Set = emptySet(),
val callState: Async = Uninitialized,
val otherKnownCallInfo: CallInfo? = null,
diff --git a/vector/src/main/java/im/vector/app/features/call/audio/API21AudioDeviceDetector.kt b/vector/src/main/java/im/vector/app/features/call/audio/API21AudioDeviceDetector.kt
index 4f54f703b4..eafd1eab20 100644
--- a/vector/src/main/java/im/vector/app/features/call/audio/API21AudioDeviceDetector.kt
+++ b/vector/src/main/java/im/vector/app/features/call/audio/API21AudioDeviceDetector.kt
@@ -50,13 +50,17 @@ internal class API21AudioDeviceDetector(private val context: Context,
private fun getAvailableSoundDevices(): Set {
return HashSet().apply {
- if (isBluetoothHeadsetOn()) add(CallAudioManager.Device.WIRELESS_HEADSET)
- if (isWiredHeadsetOn()) {
- add(CallAudioManager.Device.HEADSET)
- } else {
- add(CallAudioManager.Device.PHONE)
+ if (isBluetoothHeadsetOn()) {
+ connectedBlueToothHeadset?.connectedDevices?.forEach {
+ add(CallAudioManager.Device.WirelessHeadset(it.name))
+ }
}
- add(CallAudioManager.Device.SPEAKER)
+ if (isWiredHeadsetOn()) {
+ add(CallAudioManager.Device.Headset)
+ } else {
+ add(CallAudioManager.Device.Phone)
+ }
+ add(CallAudioManager.Device.Speaker)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/call/audio/API23AudioDeviceDetector.kt b/vector/src/main/java/im/vector/app/features/call/audio/API23AudioDeviceDetector.kt
index 7174554d5f..fb17338fd1 100644
--- a/vector/src/main/java/im/vector/app/features/call/audio/API23AudioDeviceDetector.kt
+++ b/vector/src/main/java/im/vector/app/features/call/audio/API23AudioDeviceDetector.kt
@@ -33,10 +33,10 @@ internal class API23AudioDeviceDetector(private val audioManager: AudioManager,
val deviceInfos = audioManager.getDevices(AudioManager.GET_DEVICES_OUTPUTS)
for (info in deviceInfos) {
when (info.type) {
- AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> devices.add(CallAudioManager.Device.WIRELESS_HEADSET)
- AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> devices.add(CallAudioManager.Device.PHONE)
- AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> devices.add(CallAudioManager.Device.SPEAKER)
- AudioDeviceInfo.TYPE_WIRED_HEADPHONES, AudioDeviceInfo.TYPE_WIRED_HEADSET, TYPE_USB_HEADSET -> devices.add(CallAudioManager.Device.HEADSET)
+ AudioDeviceInfo.TYPE_BLUETOOTH_SCO -> devices.add(CallAudioManager.Device.WirelessHeadset(info.productName.toString()))
+ AudioDeviceInfo.TYPE_BUILTIN_EARPIECE -> devices.add(CallAudioManager.Device.Phone)
+ AudioDeviceInfo.TYPE_BUILTIN_SPEAKER -> devices.add(CallAudioManager.Device.Speaker)
+ AudioDeviceInfo.TYPE_WIRED_HEADPHONES, AudioDeviceInfo.TYPE_WIRED_HEADSET, TYPE_USB_HEADSET -> devices.add(CallAudioManager.Device.Headset)
}
}
callAudioManager.replaceDevices(devices)
diff --git a/vector/src/main/java/im/vector/app/features/call/audio/CallAudioManager.kt b/vector/src/main/java/im/vector/app/features/call/audio/CallAudioManager.kt
index 36a11b5923..f4f56f9844 100644
--- a/vector/src/main/java/im/vector/app/features/call/audio/CallAudioManager.kt
+++ b/vector/src/main/java/im/vector/app/features/call/audio/CallAudioManager.kt
@@ -19,7 +19,10 @@ package im.vector.app.features.call.audio
import android.content.Context
import android.media.AudioManager
import android.os.Build
+import androidx.annotation.DrawableRes
+import androidx.annotation.StringRes
import androidx.core.content.getSystemService
+import im.vector.app.R
import org.matrix.android.sdk.api.extensions.orFalse
import timber.log.Timber
import java.util.HashSet
@@ -31,11 +34,11 @@ class CallAudioManager(private val context: Context, val configChange: (() -> Un
private var audioDeviceDetector: AudioDeviceDetector? = null
private var audioDeviceRouter: AudioDeviceRouter? = null
- enum class Device {
- PHONE,
- SPEAKER,
- HEADSET,
- WIRELESS_HEADSET
+ sealed class Device(@StringRes val titleRes: Int, @DrawableRes val drawableRes: Int) {
+ object Phone : Device(R.string.sound_device_phone, R.drawable.ic_sound_device_phone)
+ object Speaker : Device(R.string.sound_device_speaker, R.drawable.ic_sound_device_speaker)
+ object Headset : Device(R.string.sound_device_headset, R.drawable.ic_sound_device_headphone)
+ data class WirelessHeadset(val name: String?) : Device(R.string.sound_device_wireless_headset, R.drawable.ic_sound_device_wireless)
}
enum class Mode {
@@ -133,19 +136,19 @@ class CallAudioManager(private val context: Context, val configChange: (() -> Un
userSelectedDevice = null
return true
}
- val bluetoothAvailable = _availableDevices.contains(Device.WIRELESS_HEADSET)
- val headsetAvailable = _availableDevices.contains(Device.HEADSET)
+ val availableBluetoothDevice = _availableDevices.firstOrNull { it is Device.WirelessHeadset }
+ val headsetAvailable = _availableDevices.contains(Device.Headset)
// Pick the desired device based on what's available and the mode.
var audioDevice: Device
- audioDevice = if (bluetoothAvailable) {
- Device.WIRELESS_HEADSET
+ audioDevice = if (availableBluetoothDevice != null) {
+ availableBluetoothDevice
} else if (headsetAvailable) {
- Device.HEADSET
+ Device.Headset
} else if (mode == Mode.VIDEO_CALL) {
- Device.SPEAKER
+ Device.Speaker
} else {
- Device.PHONE
+ Device.Phone
}
// Consider the user's selection
if (userSelectedDevice != null && _availableDevices.contains(userSelectedDevice)) {
diff --git a/vector/src/main/java/im/vector/app/features/call/audio/DefaultAudioDeviceRouter.kt b/vector/src/main/java/im/vector/app/features/call/audio/DefaultAudioDeviceRouter.kt
index c252cc9f89..fd85ce075f 100644
--- a/vector/src/main/java/im/vector/app/features/call/audio/DefaultAudioDeviceRouter.kt
+++ b/vector/src/main/java/im/vector/app/features/call/audio/DefaultAudioDeviceRouter.kt
@@ -31,8 +31,8 @@ class DefaultAudioDeviceRouter(private val audioManager: AudioManager,
private var focusRequestCompat: AudioFocusRequestCompat? = null
override fun setAudioRoute(device: CallAudioManager.Device) {
- audioManager.isSpeakerphoneOn = device === CallAudioManager.Device.SPEAKER
- setBluetoothAudioRoute(device === CallAudioManager.Device.WIRELESS_HEADSET)
+ audioManager.isSpeakerphoneOn = device is CallAudioManager.Device.Speaker
+ setBluetoothAudioRoute(device is CallAudioManager.Device.WirelessHeadset)
}
override fun setMode(mode: CallAudioManager.Mode): Boolean {
diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiActiveConferenceHolder.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiActiveConferenceHolder.kt
new file mode 100644
index 0000000000..1a9fc5ea10
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiActiveConferenceHolder.kt
@@ -0,0 +1,46 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.call.conference
+
+import android.content.Context
+import androidx.lifecycle.ProcessLifecycleOwner
+import org.jitsi.meet.sdk.BroadcastEvent
+import org.matrix.android.sdk.api.extensions.orFalse
+import javax.inject.Inject
+import javax.inject.Singleton
+
+@Singleton
+class JitsiActiveConferenceHolder @Inject constructor(context: Context) {
+
+ private var activeConference: String? = null
+
+ init {
+ ProcessLifecycleOwner.get().lifecycle.addObserver(JitsiBroadcastEventObserver(context, this::onBroadcastEvent))
+ }
+
+ fun isJoined(confId: String?): Boolean {
+ return confId != null && activeConference?.endsWith(confId).orFalse()
+ }
+
+ private fun onBroadcastEvent(broadcastEvent: BroadcastEvent) {
+ when (broadcastEvent.type) {
+ BroadcastEvent.Type.CONFERENCE_JOINED -> activeConference = broadcastEvent.extractConferenceUrl()
+ BroadcastEvent.Type.CONFERENCE_TERMINATED -> activeConference = null
+ else -> Unit
+ }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiBroadcastEvent.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiBroadcastEvent.kt
new file mode 100644
index 0000000000..00ad7c540e
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiBroadcastEvent.kt
@@ -0,0 +1,84 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.call.conference
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.LifecycleObserver
+import androidx.lifecycle.OnLifecycleEvent
+import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import com.facebook.react.bridge.JavaOnlyMap
+import org.jitsi.meet.sdk.BroadcastEmitter
+import org.jitsi.meet.sdk.BroadcastEvent
+import org.jitsi.meet.sdk.JitsiMeet
+import org.matrix.android.sdk.api.extensions.tryOrNull
+
+private const val CONFERENCE_URL_DATA_KEY = "url"
+
+fun BroadcastEvent.extractConferenceUrl(): String? {
+ return when (type) {
+ BroadcastEvent.Type.CONFERENCE_TERMINATED,
+ BroadcastEvent.Type.CONFERENCE_WILL_JOIN,
+ BroadcastEvent.Type.CONFERENCE_JOINED -> data[CONFERENCE_URL_DATA_KEY] as? String
+ else -> null
+ }
+}
+
+class JitsiBroadcastEmitter(private val context: Context) {
+
+ fun emitConferenceEnded() {
+ val broadcastEventData = JavaOnlyMap.of(CONFERENCE_URL_DATA_KEY, JitsiMeet.getCurrentConference())
+ BroadcastEmitter(context).sendBroadcast(BroadcastEvent.Type.CONFERENCE_TERMINATED.name, broadcastEventData)
+ }
+}
+
+class JitsiBroadcastEventObserver(private val context: Context,
+ private val onBroadcastEvent: (BroadcastEvent) -> Unit) : LifecycleObserver {
+
+ // See https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-android-sdk#listening-for-broadcasted-events
+ private val broadcastReceiver = object : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ intent?.let { onBroadcastReceived(it) }
+ }
+ }
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
+ fun unregisterForBroadcastMessages() {
+ tryOrNull("Unable to unregister receiver") {
+ LocalBroadcastManager.getInstance(context).unregisterReceiver(broadcastReceiver)
+ }
+ }
+
+ @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
+ fun registerForBroadcastMessages() {
+ val intentFilter = IntentFilter()
+ for (type in BroadcastEvent.Type.values()) {
+ intentFilter.addAction(type.action)
+ }
+ tryOrNull("Unable to register receiver") {
+ LocalBroadcastManager.getInstance(context).registerReceiver(broadcastReceiver, intentFilter)
+ }
+ }
+
+ private fun onBroadcastReceived(intent: Intent) {
+ val event = BroadcastEvent(intent)
+ onBroadcastEvent(event)
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt b/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt
index 7b01824c6c..117da8fd7c 100644
--- a/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt
+++ b/vector/src/main/java/im/vector/app/features/call/conference/JitsiService.kt
@@ -108,8 +108,7 @@ class JitsiService @Inject constructor(
this.avatar = userAvatar?.let { URL(it) }
}
val roomName = session.getRoomSummary(roomId)?.displayName
- val properties = session.widgetService().getWidgetComputedUrl(jitsiWidget, themeProvider.isLightTheme())
- ?.let { url -> jitsiWidgetPropertiesFactory.create(url) } ?: throw IllegalStateException()
+ val properties = extractProperties(jitsiWidget) ?: throw IllegalStateException()
val token = if (jitsiWidget.isOpenIdJWTAuthenticationRequired()) {
getOpenIdJWTToken(roomId, properties.domain, userDisplayName ?: session.myUserId, userAvatar ?: "")
@@ -126,6 +125,11 @@ class JitsiService @Inject constructor(
)
}
+ fun extractProperties(jitsiWidget: Widget): JitsiWidgetProperties? {
+ return session.widgetService().getWidgetComputedUrl(jitsiWidget, themeProvider.isLightTheme())
+ ?.let { url -> jitsiWidgetPropertiesFactory.create(url) }
+ }
+
private fun Widget.isOpenIdJWTAuthenticationRequired(): Boolean {
return widgetContent.data[JITSI_AUTH_KEY] == JITSI_OPEN_ID_TOKEN_JWT_AUTH
}
diff --git a/vector/src/main/java/im/vector/app/features/call/conference/RemoveJitsiWidgetView.kt b/vector/src/main/java/im/vector/app/features/call/conference/RemoveJitsiWidgetView.kt
new file mode 100644
index 0000000000..391471d2f2
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/call/conference/RemoveJitsiWidgetView.kt
@@ -0,0 +1,162 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.call.conference
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.res.ColorStateList
+import android.util.AttributeSet
+import android.view.MotionEvent
+import androidx.constraintlayout.widget.ConstraintLayout
+import androidx.core.content.ContextCompat
+import androidx.core.view.isVisible
+import androidx.core.widget.ImageViewCompat
+import im.vector.app.R
+import im.vector.app.databinding.ViewRemoveJitsiWidgetBinding
+import im.vector.app.features.home.room.detail.RoomDetailViewState
+import org.matrix.android.sdk.api.session.room.model.Membership
+
+@SuppressLint("ClickableViewAccessibility") class RemoveJitsiWidgetView @JvmOverloads constructor(
+ context: Context,
+ attrs: AttributeSet? = null,
+ defStyleAttr: Int = 0
+) : ConstraintLayout(context, attrs, defStyleAttr) {
+
+ private sealed class State {
+ object Unmount : State()
+ object Idle : State()
+ data class Sliding(val initialX: Float, val translationX: Float, val hasReachedActivationThreshold: Boolean) : State()
+ object Progress : State()
+ }
+
+ private val views: ViewRemoveJitsiWidgetBinding
+ private var state: State = State.Unmount
+ var onCompleteSliding: (() -> Unit)? = null
+
+ init {
+ inflate(context, R.layout.view_remove_jitsi_widget, this)
+ views = ViewRemoveJitsiWidgetBinding.bind(this)
+ views.removeJitsiSlidingContainer.setOnTouchListener { _, event ->
+ val currentState = state
+ return@setOnTouchListener when (event.action) {
+ MotionEvent.ACTION_DOWN -> {
+ if (currentState == State.Idle) {
+ val initialX = views.removeJitsiSlidingContainer.x - event.rawX
+ updateState(State.Sliding(initialX, 0f, false))
+ }
+ true
+ }
+ MotionEvent.ACTION_UP,
+ MotionEvent.ACTION_CANCEL -> {
+ if (currentState is State.Sliding) {
+ if (currentState.hasReachedActivationThreshold) {
+ updateState(State.Progress)
+ } else {
+ updateState(State.Idle)
+ }
+ }
+ true
+ }
+ MotionEvent.ACTION_MOVE -> {
+ if (currentState is State.Sliding) {
+ val translationX = (currentState.initialX + event.rawX).coerceAtLeast(0f)
+ val hasReachedActivationThreshold = translationX >= views.root.width / 4
+ updateState(State.Sliding(currentState.initialX, translationX, hasReachedActivationThreshold))
+ }
+ true
+ }
+ else -> false
+ }
+ }
+ renderInternalState(state)
+ }
+
+ fun render(roomDetailViewState: RoomDetailViewState) {
+ val summary = roomDetailViewState.asyncRoomSummary()
+ val newState = if (summary?.membership != Membership.JOIN
+ || roomDetailViewState.isWebRTCCallOptionAvailable()
+ || !roomDetailViewState.isAllowedToManageWidgets
+ || roomDetailViewState.jitsiState.widgetId == null) {
+ State.Unmount
+ } else if (roomDetailViewState.jitsiState.deleteWidgetInProgress) {
+ State.Progress
+ } else {
+ State.Idle
+ }
+ // Don't force Idle if we are already sliding
+ if (state is State.Sliding && newState is State.Idle) {
+ return
+ } else {
+ updateState(newState)
+ }
+ }
+
+ private fun updateState(newState: State) {
+ if (newState == state) {
+ return
+ }
+ renderInternalState(newState)
+ state = newState
+ if (state == State.Progress) {
+ onCompleteSliding?.invoke()
+ }
+ }
+
+ private fun renderInternalState(state: State) {
+ isVisible = state != State.Unmount
+ when (state) {
+ State.Progress -> {
+ isVisible = true
+ views.updateVisibilities(true)
+ views.updateHangupColors(true)
+ }
+ State.Idle -> {
+ isVisible = true
+ views.updateVisibilities(false)
+ views.removeJitsiSlidingContainer.translationX = 0f
+ views.updateHangupColors(false)
+ }
+ is State.Sliding -> {
+ isVisible = true
+ views.updateVisibilities(false)
+ views.removeJitsiSlidingContainer.translationX = state.translationX
+ views.updateHangupColors(state.hasReachedActivationThreshold)
+ }
+ else -> Unit
+ }
+ }
+
+ private fun ViewRemoveJitsiWidgetBinding.updateVisibilities(isProgress: Boolean) {
+ removeJitsiProgressContainer.isVisible = isProgress
+ removeJitsiHangupContainer.isVisible = !isProgress
+ removeJitsiSlidingContainer.isVisible = !isProgress
+ }
+
+ private fun ViewRemoveJitsiWidgetBinding.updateHangupColors(activated: Boolean) {
+ val iconTintColor: Int
+ val bgColor: Int
+ if (activated) {
+ bgColor = ContextCompat.getColor(context, R.color.palette_vermilion)
+ iconTintColor = ContextCompat.getColor(context, R.color.palette_white)
+ } else {
+ bgColor = ContextCompat.getColor(context, android.R.color.transparent)
+ iconTintColor = ContextCompat.getColor(context, R.color.palette_vermilion)
+ }
+ removeJitsiHangupContainer.setBackgroundColor(bgColor)
+ ImageViewCompat.setImageTintList(removeJitsiHangupIcon, ColorStateList.valueOf(iconTintColor))
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt
index 11b84f4f44..a7a6f99cfc 100644
--- a/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/call/conference/VectorJitsiActivity.kt
@@ -16,18 +16,17 @@
package im.vector.app.features.call.conference
-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.Build
import android.os.Bundle
import android.os.Parcelable
import android.widget.FrameLayout
import android.widget.Toast
import androidx.core.view.isVisible
-import androidx.localbroadcastmanager.content.LocalBroadcastManager
+import androidx.lifecycle.Lifecycle
import com.airbnb.mvrx.Fail
import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.Success
@@ -41,6 +40,7 @@ import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.databinding.ActivityJitsiBinding
import kotlinx.parcelize.Parcelize
import org.jitsi.meet.sdk.BroadcastEvent
+import org.jitsi.meet.sdk.JitsiMeet
import org.jitsi.meet.sdk.JitsiMeetActivityDelegate
import org.jitsi.meet.sdk.JitsiMeetActivityInterface
import org.jitsi.meet.sdk.JitsiMeetConferenceOptions
@@ -71,13 +71,6 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee
injector.inject(this)
}
- // See https://jitsi.github.io/handbook/docs/dev-guide/dev-guide-android-sdk#listening-for-broadcasted-events
- private val broadcastReceiver = object : BroadcastReceiver() {
- override fun onReceive(context: Context?, intent: Intent?) {
- intent?.let { onBroadcastReceived(it) }
- }
- }
-
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -94,8 +87,47 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee
JitsiCallViewEvents.LeaveConference -> handleLeaveConference()
}.exhaustive
}
+ lifecycle.addObserver(JitsiBroadcastEventObserver(this, this::onBroadcastEvent))
+ }
- registerForBroadcastMessages()
+ override fun onResume() {
+ super.onResume()
+ JitsiMeetActivityDelegate.onHostResume(this)
+ }
+
+ override fun initUiAndData() {
+ super.initUiAndData()
+ jitsiMeetView = JitsiMeetView(this)
+ val params = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
+ views.jitsiLayout.addView(jitsiMeetView, params)
+ }
+
+ override fun onStop() {
+ JitsiMeetActivityDelegate.onHostPause(this)
+ super.onStop()
+ }
+
+ override fun onDestroy() {
+ val currentConf = JitsiMeet.getCurrentConference()
+ jitsiMeetView?.leave()
+ jitsiMeetView?.dispose()
+ // Fake emitting CONFERENCE_TERMINATED event when currentConf is not null (probably when closing the PiP screen).
+ if (currentConf != null) {
+ JitsiBroadcastEmitter(this).emitConferenceEnded()
+ }
+ JitsiMeetActivityDelegate.onHostDestroy(this)
+ super.onDestroy()
+ }
+
+ override fun onBackPressed() {
+ JitsiMeetActivityDelegate.onBackPressed()
+ }
+
+ override fun onUserLeaveHint() {
+ super.onUserLeaveHint()
+ if (packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
+ jitsiMeetView?.enterPictureInPicture()
+ }
}
private fun handleLeaveConference() {
@@ -116,14 +148,16 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee
override fun onPictureInPictureModeChanged(isInPictureInPictureMode: Boolean,
newConfig: Configuration) {
super.onPictureInPictureModeChanged(isInPictureInPictureMode, newConfig)
+ checkIfActivityShouldBeFinished()
Timber.w("onPictureInPictureModeChanged($isInPictureInPictureMode)")
}
- override fun initUiAndData() {
- super.initUiAndData()
- jitsiMeetView = JitsiMeetView(this)
- val params = FrameLayout.LayoutParams(FrameLayout.LayoutParams.MATCH_PARENT, FrameLayout.LayoutParams.MATCH_PARENT)
- views.jitsiLayout.addView(jitsiMeetView, params)
+ private fun checkIfActivityShouldBeFinished() {
+ // OnStop is called when PiP mode is closed directly from the ui
+ // If stopped is called and PiP mode is not active, we should finish the activity and remove the task as Android creates a new one for PiP.
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && !lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED) && !isInPictureInPictureMode) {
+ finishAndRemoveTask()
+ }
}
private fun renderState(viewState: JitsiCallViewState) {
@@ -167,34 +201,6 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee
jitsiMeetView?.join(jitsiMeetConferenceOptions)
}
- override fun onStop() {
- JitsiMeetActivityDelegate.onHostPause(this)
- super.onStop()
- }
-
- override fun onResume() {
- JitsiMeetActivityDelegate.onHostResume(this)
- super.onResume()
- }
-
- override fun onBackPressed() {
- JitsiMeetActivityDelegate.onBackPressed()
- super.onBackPressed()
- }
-
- override fun onDestroy() {
- JitsiMeetActivityDelegate.onHostDestroy(this)
- unregisterForBroadcastMessages()
- super.onDestroy()
- }
-
- override fun onUserLeaveHint() {
- super.onUserLeaveHint()
- if (packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) {
- jitsiMeetView?.enterPictureInPicture()
- }
- }
-
override fun onNewIntent(intent: Intent?) {
JitsiMeetActivityDelegate.onNewIntent(intent)
@@ -217,24 +223,7 @@ class VectorJitsiActivity : VectorBaseActivity(), JitsiMee
JitsiMeetActivityDelegate.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
- private fun registerForBroadcastMessages() {
- val intentFilter = IntentFilter()
- for (type in BroadcastEvent.Type.values()) {
- intentFilter.addAction(type.action)
- }
- tryOrNull("Unable to register receiver") {
- LocalBroadcastManager.getInstance(this).registerReceiver(broadcastReceiver, intentFilter)
- }
- }
-
- private fun unregisterForBroadcastMessages() {
- tryOrNull("Unable to unregister receiver") {
- LocalBroadcastManager.getInstance(this).unregisterReceiver(broadcastReceiver)
- }
- }
-
- private fun onBroadcastReceived(intent: Intent) {
- val event = BroadcastEvent(intent)
+ private fun onBroadcastEvent(event: BroadcastEvent) {
Timber.v("Broadcast received: ${event.type}")
when (event.type) {
BroadcastEvent.Type.CONFERENCE_TERMINATED -> onConferenceTerminated(event.data)
diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt
index 04a979fb94..2d39fda2e3 100644
--- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt
+++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCall.kt
@@ -175,7 +175,7 @@ class WebRtcCall(
private set
var videoMuted = false
private set
- var remoteOnHold = false
+ var isRemoteOnHold = false
private set
var isLocalOnHold = false
private set
@@ -357,7 +357,7 @@ class WebRtcCall(
fun attachViewRenderers(localViewRenderer: SurfaceViewRenderer?, remoteViewRenderer: SurfaceViewRenderer, mode: String?) {
sessionScope?.launch(dispatcher) {
- Timber.tag(loggerTag.value).v("attachViewRenderers localRendeder $localViewRenderer / $remoteViewRenderer")
+ Timber.tag(loggerTag.value).v("attachViewRenderers localRenderer $localViewRenderer / $remoteViewRenderer")
localSurfaceRenderers.addIfNeeded(localViewRenderer)
remoteSurfaceRenderers.addIfNeeded(remoteViewRenderer)
when (mode) {
@@ -614,12 +614,12 @@ class WebRtcCall(
}
private fun updateMuteStatus() {
- val micShouldBeMuted = micMuted || remoteOnHold
+ val micShouldBeMuted = micMuted || isRemoteOnHold
localAudioTrack?.setEnabled(!micShouldBeMuted)
- remoteAudioTrack?.setEnabled(!remoteOnHold)
- val vidShouldBeMuted = videoMuted || remoteOnHold
+ remoteAudioTrack?.setEnabled(!isRemoteOnHold)
+ val vidShouldBeMuted = videoMuted || isRemoteOnHold
localVideoTrack?.setEnabled(!vidShouldBeMuted)
- remoteVideoTrack?.setEnabled(!remoteOnHold)
+ remoteVideoTrack?.setEnabled(!isRemoteOnHold)
}
/**
@@ -645,16 +645,16 @@ class WebRtcCall(
fun updateRemoteOnHold(onHold: Boolean) {
sessionScope?.launch(dispatcher) {
- if (remoteOnHold == onHold) return@launch
+ if (isRemoteOnHold == onHold) return@launch
val direction: RtpTransceiver.RtpTransceiverDirection
if (onHold) {
wasLocalOnHold = isLocalOnHold
- remoteOnHold = true
+ isRemoteOnHold = true
isLocalOnHold = true
direction = RtpTransceiver.RtpTransceiverDirection.SEND_ONLY
timer.pause()
} else {
- remoteOnHold = false
+ isRemoteOnHold = false
isLocalOnHold = wasLocalOnHold
onCallBecomeActive(this@WebRtcCall)
direction = RtpTransceiver.RtpTransceiverDirection.SEND_RECV
diff --git a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallExt.kt b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallExt.kt
index ef9ef3ef9a..ac9d169633 100644
--- a/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallExt.kt
+++ b/vector/src/main/java/im/vector/app/features/call/webrtc/WebRtcCallExt.kt
@@ -16,17 +16,20 @@
package im.vector.app.features.call.webrtc
+import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.util.MatrixItem
import org.matrix.android.sdk.api.util.toMatrixItem
fun WebRtcCall.getOpponentAsMatrixItem(session: Session): MatrixItem? {
- return session.getRoomSummary(nativeRoomId)?.let { roomSummary ->
+ return session.getRoom(nativeRoomId)?.let { room ->
+ val roomSummary = room.roomSummary() ?: return@let null
// Fallback to RoomSummary if there is no other member.
- if (roomSummary.otherMemberIds.isEmpty()) {
+ if (roomSummary.otherMemberIds.isEmpty().orFalse()) {
roomSummary.toMatrixItem()
} else {
- roomSummary.otherMemberIds.first().let { session.getUser(it)?.toMatrixItem() }
+ val userId = roomSummary.otherMemberIds.first()
+ return room.getRoomMember(userId)?.toMatrixItem()
}
}
}
diff --git a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolActivity.kt b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolActivity.kt
index c502d92a4c..36ccef1fca 100644
--- a/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolActivity.kt
+++ b/vector/src/main/java/im/vector/app/features/devtools/RoomDevToolActivity.kt
@@ -147,10 +147,6 @@ class RoomDevToolActivity : SimpleFragmentActivity(), RoomDevToolViewModel.Facto
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
- if (item.itemId == android.R.id.home) {
- onBackPressed()
- return true
- }
if (item.itemId == R.id.menuItemEdit) {
viewModel.handle(RoomDevToolAction.MenuEdit)
return true
diff --git a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt
index 683ee40508..9b71d1c90c 100644
--- a/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/HomeDetailFragment.kt
@@ -22,7 +22,6 @@ import android.view.Menu
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
-import androidx.core.view.get
import androidx.core.view.isVisible
import androidx.core.view.iterator
import androidx.fragment.app.Fragment
@@ -39,8 +38,8 @@ import im.vector.app.core.platform.VectorBaseActivity
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.resources.ColorProvider
import im.vector.app.core.ui.views.CurrentCallsView
+import im.vector.app.core.ui.views.CurrentCallsViewPresenter
import im.vector.app.core.ui.views.KeysBackupBanner
-import im.vector.app.core.ui.views.KnownCallsViewHolder
import im.vector.app.databinding.FragmentHomeDetailBinding
import im.vector.app.features.call.SharedKnownCallsViewModel
import im.vector.app.features.call.VectorCallActivity
@@ -117,7 +116,7 @@ class HomeDetailFragment @Inject constructor(
return FragmentHomeDetailBinding.inflate(inflater, container, false)
}
- private val activeCallViewHolder = KnownCallsViewHolder()
+ private val currentCallsViewPresenter = CurrentCallsViewPresenter()
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
@@ -190,11 +189,16 @@ class HomeDetailFragment @Inject constructor(
sharedCallActionViewModel
.liveKnownCalls
.observe(viewLifecycleOwner, {
- activeCallViewHolder.updateCall(callManager.getCurrentCall(), callManager.getCalls())
+ currentCallsViewPresenter.updateCall(callManager.getCurrentCall(), callManager.getCalls())
invalidateOptionsMenu()
})
}
+ override fun onDestroyView() {
+ currentCallsViewPresenter.unBind()
+ super.onDestroyView()
+ }
+
override fun onResume() {
super.onResume()
// update notification tab if needed
@@ -291,12 +295,7 @@ class HomeDetailFragment @Inject constructor(
}
private fun setupActiveCallView() {
- activeCallViewHolder.bind(
- views.activeCallPiP,
- views.activeCallView,
- views.activeCallPiPWrap,
- this
- )
+ currentCallsViewPresenter.bind(views.currentCallsView, this)
}
private fun setupToolbar() {
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 6b031159b8..94388dcfeb 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
@@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail
import android.net.Uri
import android.view.View
import im.vector.app.core.platform.VectorViewModelAction
+import org.jitsi.meet.sdk.BroadcastEvent
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
@@ -89,9 +90,14 @@ sealed class RoomDetailAction : VectorViewModelAction {
object ManageIntegrations : RoomDetailAction()
data class AddJitsiWidget(val withVideo: Boolean) : RoomDetailAction()
data class RemoveWidget(val widgetId: String) : RoomDetailAction()
+
+ object JoinJitsiCall: RoomDetailAction()
+ object LeaveJitsiCall: RoomDetailAction()
+
data class EnsureNativeWidgetAllowed(val widget: Widget,
val userJustAccepted: Boolean,
val grantedEvents: RoomDetailViewEvents) : RoomDetailAction()
+ data class UpdateJoinJitsiCallStatus(val jitsiEvent: BroadcastEvent): RoomDetailAction()
data class OpenOrCreateDm(val userId: String) : RoomDetailAction()
data class JumpToReadReceipt(val userId: String) : RoomDetailAction()
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt
index 057b4f2703..847b06fb45 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailFragment.kt
@@ -16,6 +16,7 @@
package im.vector.app.features.home.room.detail
+import android.animation.ArgbEvaluator
import android.annotation.SuppressLint
import android.app.Activity
import android.content.Intent
@@ -65,6 +66,7 @@ import com.airbnb.mvrx.MvRx
import com.airbnb.mvrx.args
import com.airbnb.mvrx.fragmentViewModel
import com.airbnb.mvrx.withState
+import com.google.android.material.button.MaterialButton
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.jakewharton.rxbinding3.view.focusChanges
import com.jakewharton.rxbinding3.widget.textChanges
@@ -88,10 +90,9 @@ import im.vector.app.core.intent.getMimeTypeFromUri
import im.vector.app.core.platform.VectorBaseFragment
import im.vector.app.core.platform.showOptimizedSnackbar
import im.vector.app.core.resources.ColorProvider
-import im.vector.app.core.ui.views.ActiveConferenceView
import im.vector.app.core.ui.views.CurrentCallsView
+import im.vector.app.core.ui.views.CurrentCallsViewPresenter
import im.vector.app.core.ui.views.FailedMessagesWarningView
-import im.vector.app.core.ui.views.KnownCallsViewHolder
import im.vector.app.core.ui.views.NotificationAreaView
import im.vector.app.core.utils.Debouncer
import im.vector.app.core.utils.DimensionConverter
@@ -123,6 +124,8 @@ import im.vector.app.features.attachments.preview.AttachmentsPreviewArgs
import im.vector.app.features.attachments.toGroupedContentAttachmentData
import im.vector.app.features.call.SharedKnownCallsViewModel
import im.vector.app.features.call.VectorCallActivity
+import im.vector.app.features.call.conference.JitsiBroadcastEmitter
+import im.vector.app.features.call.conference.JitsiBroadcastEventObserver
import im.vector.app.features.call.conference.JitsiCallViewModel
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.command.Command
@@ -182,6 +185,7 @@ import nl.dionsegijn.konfetti.models.Shape
import nl.dionsegijn.konfetti.models.Size
import org.billcarsonfr.jsonviewer.JSonViewerDialog
import org.commonmark.parser.Parser
+import org.jitsi.meet.sdk.BroadcastEvent
import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.content.ContentAttachmentData
@@ -213,6 +217,7 @@ import java.net.URL
import java.util.UUID
import java.util.concurrent.TimeUnit
import javax.inject.Inject
+import android.animation.ValueAnimator
@Parcelize
data class RoomDetailArgs(
@@ -307,7 +312,7 @@ class RoomDetailFragment @Inject constructor(
private lateinit var attachmentTypeSelector: AttachmentTypeSelectorView
private var lockSendButton = false
- private val knownCallsViewHolder = KnownCallsViewHolder()
+ private val currentCallsViewPresenter = CurrentCallsViewPresenter()
private lateinit var emojiPopup: EmojiPopup
@@ -321,6 +326,7 @@ class RoomDetailFragment @Inject constructor(
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
+ lifecycle.addObserver(JitsiBroadcastEventObserver(vectorBaseActivity, this::onBroadcastJitsiEvent))
super.onViewCreated(view, savedInstanceState)
sharedActionViewModel = activityViewModelProvider.get(MessageSharedActionViewModel::class.java)
knownCallsViewModel = activityViewModelProvider.get(SharedKnownCallsViewModel::class.java)
@@ -344,9 +350,9 @@ class RoomDetailFragment @Inject constructor(
setupJumpToReadMarkerView()
setupActiveCallView()
setupJumpToBottomView()
- setupConfBannerView()
setupEmojiPopup()
setupFailedMessagesWarningView()
+ setupRemoveJitsiWidgetView()
setupVoiceMessageView()
views.roomToolbarContentView.debouncedClicks {
@@ -363,7 +369,7 @@ class RoomDetailFragment @Inject constructor(
knownCallsViewModel
.liveKnownCalls
.observe(viewLifecycleOwner, {
- knownCallsViewHolder.updateCall(callManager.getCurrentCall(), it)
+ currentCallsViewPresenter.updateCall(callManager.getCurrentCall(), it)
invalidateOptionsMenu()
})
@@ -412,6 +418,7 @@ class RoomDetailFragment @Inject constructor(
RoomDetailViewEvents.OpenActiveWidgetBottomSheet -> onViewWidgetsClicked()
is RoomDetailViewEvents.ShowInfoOkDialog -> showDialogWithMessage(it.message)
is RoomDetailViewEvents.JoinJitsiConference -> joinJitsiRoom(it.widget, it.withVideo)
+ RoomDetailViewEvents.LeaveJitsiConference -> leaveJitsiConference()
RoomDetailViewEvents.ShowWaitingView -> vectorBaseActivity.showWaitingView()
RoomDetailViewEvents.HideWaitingView -> vectorBaseActivity.hideWaitingView()
is RoomDetailViewEvents.RequestNativeWidgetPermission -> requestNativeWidgetPermission(it)
@@ -436,6 +443,26 @@ class RoomDetailFragment @Inject constructor(
}
}
+ private fun setupRemoveJitsiWidgetView() {
+ views.removeJitsiWidgetView.onCompleteSliding = {
+ withState(roomDetailViewModel) {
+ val jitsiWidgetId = it.jitsiState.widgetId ?: return@withState
+ if (it.jitsiState.hasJoined) {
+ leaveJitsiConference()
+ }
+ roomDetailViewModel.handle(RoomDetailAction.RemoveWidget(jitsiWidgetId))
+ }
+ }
+ }
+
+ private fun leaveJitsiConference() {
+ JitsiBroadcastEmitter(vectorBaseActivity).emitConferenceEnded()
+ }
+
+ private fun onBroadcastJitsiEvent(jitsiEvent: BroadcastEvent) {
+ roomDetailViewModel.handle(RoomDetailAction.UpdateJoinJitsiCallStatus(jitsiEvent))
+ }
+
private fun onCannotRecord() {
// Update the UI, cancel the animation
views.voiceMessageRecorderView.initVoiceRecordingViews()
@@ -559,31 +586,6 @@ class RoomDetailFragment @Inject constructor(
)
}
- private fun setupConfBannerView() {
- views.activeConferenceView.callback = object : ActiveConferenceView.Callback {
- override fun onTapJoinAudio(jitsiWidget: Widget) {
- // need to check if allowed first
- roomDetailViewModel.handle(RoomDetailAction.EnsureNativeWidgetAllowed(
- widget = jitsiWidget,
- userJustAccepted = false,
- grantedEvents = RoomDetailViewEvents.JoinJitsiConference(jitsiWidget, false))
- )
- }
-
- override fun onTapJoinVideo(jitsiWidget: Widget) {
- roomDetailViewModel.handle(RoomDetailAction.EnsureNativeWidgetAllowed(
- widget = jitsiWidget,
- userJustAccepted = false,
- grantedEvents = RoomDetailViewEvents.JoinJitsiConference(jitsiWidget, true))
- )
- }
-
- override fun onDelete(jitsiWidget: Widget) {
- roomDetailViewModel.handle(RoomDetailAction.RemoveWidget(jitsiWidget.widgetId))
- }
- }
- }
-
private fun setupEmojiPopup() {
emojiPopup = EmojiPopup
.Builder
@@ -769,7 +771,7 @@ class RoomDetailFragment @Inject constructor(
override fun onDestroyView() {
timelineEventController.callback = null
timelineEventController.removeModelBuildListener(modelBuildListener)
- views.activeCallView.callback = null
+ currentCallsViewPresenter.unBind()
modelBuildListener = null
autoCompleter.clear()
debouncer.cancelAll()
@@ -780,7 +782,6 @@ class RoomDetailFragment @Inject constructor(
}
override fun onDestroy() {
- knownCallsViewHolder.unBind()
roomDetailViewModel.handle(RoomDetailAction.ExitTrackingUnreadMessagesState)
super.onDestroy()
}
@@ -816,12 +817,7 @@ class RoomDetailFragment @Inject constructor(
}
private fun setupActiveCallView() {
- knownCallsViewHolder.bind(
- views.activeCallPiP,
- views.activeCallView,
- views.activeCallPiPWrap,
- this
- )
+ currentCallsViewPresenter.bind(views.currentCallsView, this)
}
private fun navigateToEvent(action: RoomDetailViewEvents.NavigateToEvent) {
@@ -872,6 +868,22 @@ class RoomDetailFragment @Inject constructor(
onOptionsItemSelected(menuItem)
}
}
+ val joinConfItem = menu.findItem(R.id.join_conference)
+ joinConfItem.actionView.findViewById(R.id.join_conference_button).also { joinButton ->
+ joinButton.setOnClickListener { roomDetailViewModel.handle(RoomDetailAction.JoinJitsiCall) }
+ val colorFrom = ContextCompat.getColor(joinButton.context, R.color.palette_element_green)
+ val colorTo = ContextCompat.getColor(joinButton.context, R.color.join_conference_animated_color)
+ // Animate button color to highlight
+ ValueAnimator.ofObject(ArgbEvaluator(), colorFrom, colorTo).apply {
+ repeatMode = ValueAnimator.REVERSE
+ repeatCount = ValueAnimator.INFINITE
+ duration = 500
+ addUpdateListener { animator ->
+ val color = animator.animatedValue as Int
+ joinButton.setBackgroundColor(color)
+ }
+ }.start()
+ }
}
override fun onPrepareOptionsMenu(menu: Menu) {
@@ -880,7 +892,8 @@ class RoomDetailFragment @Inject constructor(
}
withState(roomDetailViewModel) { state ->
// Set the visual state of the call buttons (voice/video) to enabled/disabled according to user permissions
- val callButtonsEnabled = when (state.asyncRoomSummary.invoke()?.joinedMembersCount) {
+ 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
@@ -891,14 +904,8 @@ class RoomDetailFragment @Inject constructor(
val matrixAppsMenuItem = menu.findItem(R.id.open_matrix_apps)
val widgetsCount = state.activeRoomWidgets.invoke()?.size ?: 0
- if (widgetsCount > 0) {
- val actionView = matrixAppsMenuItem.actionView
- actionView
- .findViewById(R.id.action_view_icon_image)
- .setColorFilter(colorProvider.getColorFromAttribute(R.attr.colorPrimary))
- actionView.findViewById(R.id.cart_badge).setTextOrHide("$widgetsCount")
- matrixAppsMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
- } else {
+ val hasOnlyJitsiWidget = widgetsCount == 1 && state.hasActiveJitsiWidget()
+ if (widgetsCount == 0 || hasOnlyJitsiWidget) {
// icon should be default color no badge
val actionView = matrixAppsMenuItem.actionView
actionView
@@ -906,6 +913,13 @@ class RoomDetailFragment @Inject constructor(
.setColorFilter(ThemeUtils.getColor(requireContext(), R.attr.vctr_content_secondary))
actionView.findViewById(R.id.cart_badge).isVisible = false
matrixAppsMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER)
+ } else {
+ val actionView = matrixAppsMenuItem.actionView
+ actionView
+ .findViewById(R.id.action_view_icon_image)
+ .setColorFilter(colorProvider.getColorFromAttribute(R.attr.colorPrimary))
+ actionView.findViewById(R.id.cart_badge).setTextOrHide("$widgetsCount")
+ matrixAppsMenuItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS)
}
}
}
@@ -932,10 +946,6 @@ class RoomDetailFragment @Inject constructor(
callActionsHandler.onVideoCallClicked()
true
}
- R.id.hangup_call -> {
- roomDetailViewModel.handle(RoomDetailAction.EndCall)
- true
- }
R.id.search -> {
handleSearchAction()
true
@@ -1362,7 +1372,7 @@ class RoomDetailFragment @Inject constructor(
invalidateOptionsMenu()
val summary = state.asyncRoomSummary()
renderToolbar(summary, state.typingMessage)
- views.activeConferenceView.render(state)
+ views.removeJitsiWidgetView.render(state)
views.failedMessagesWarningView.render(state.hasFailedSending)
val inviter = state.asyncInviter()
if (summary?.membership == Membership.JOIN) {
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 d62c5f6003..2802ee2f83 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
@@ -45,6 +45,7 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
data class NavigateToEvent(val eventId: String) : RoomDetailViewEvents()
data class JoinJitsiConference(val widget: Widget, val withVideo: Boolean) : RoomDetailViewEvents()
+ object LeaveJitsiConference : RoomDetailViewEvents()
object OpenInvitePeople : RoomDetailViewEvents()
object OpenSetRoomAvatarDialog : RoomDetailViewEvents()
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
index 0ac638034d..bcb8ef0f1f 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/RoomDetailViewModel.kt
@@ -38,6 +38,7 @@ import im.vector.app.core.extensions.exhaustive
import im.vector.app.core.mvrx.runCatchingToAsync
import im.vector.app.core.platform.VectorViewModel
import im.vector.app.core.resources.StringProvider
+import im.vector.app.features.call.conference.JitsiActiveConferenceHolder
import im.vector.app.features.attachments.toContentAttachmentData
import im.vector.app.features.call.conference.JitsiService
import im.vector.app.features.call.lookup.CallProtocolsChecker
@@ -51,7 +52,6 @@ import im.vector.app.features.home.room.detail.composer.VoiceMessageHelper
import im.vector.app.features.home.room.detail.composer.rainbow.RainbowGenerator
import im.vector.app.features.home.room.detail.sticker.StickerPickerActionHandler
import im.vector.app.features.home.room.detail.timeline.factory.TimelineFactory
-import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
import im.vector.app.features.home.room.typing.TypingHelper
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
@@ -66,6 +66,7 @@ import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.commonmark.parser.Parser
import org.commonmark.renderer.html.HtmlRenderer
+import org.jitsi.meet.sdk.BroadcastEvent
import org.matrix.android.sdk.api.MatrixCallback
import org.matrix.android.sdk.api.MatrixPatterns
import org.matrix.android.sdk.api.extensions.tryOrNull
@@ -115,12 +116,12 @@ class RoomDetailViewModel @AssistedInject constructor(
private val session: Session,
private val supportedVerificationMethodsProvider: SupportedVerificationMethodsProvider,
private val stickerPickerActionHandler: StickerPickerActionHandler,
- private val roomSummariesHolder: RoomSummariesHolder,
private val typingHelper: TypingHelper,
private val callManager: WebRtcCallManager,
private val chatEffectManager: ChatEffectManager,
private val directRoomHelper: DirectRoomHelper,
private val jitsiService: JitsiService,
+ private val activeConferenceHolder: JitsiActiveConferenceHolder,
private val voiceMessageHelper: VoiceMessageHelper,
private val voicePlayerHelper: VoicePlayerHelper,
timelineFactory: TimelineFactory
@@ -241,9 +242,25 @@ class RoomDetailViewModel @AssistedInject constructor(
.map { widgets ->
widgets.filter { it.isActive }
}
- .execute {
- copy(activeRoomWidgets = it)
+ .execute { widgets ->
+ copy(activeRoomWidgets = widgets)
}
+
+ asyncSubscribe(RoomDetailViewState::activeRoomWidgets) { widgets ->
+ setState {
+ val jitsiWidget = widgets.firstOrNull { it.type == WidgetType.Jitsi }
+ val jitsiConfId = jitsiWidget?.let {
+ jitsiService.extractProperties(it)?.confId
+ }
+ copy(
+ jitsiState = jitsiState.copy(
+ confId = jitsiConfId,
+ widgetId = jitsiWidget?.widgetId,
+ hasJoined = activeConferenceHolder.isJoined(jitsiConfId)
+ )
+ )
+ }
+ }
}
private fun observeMyRoomMember() {
@@ -308,6 +325,9 @@ class RoomDetailViewModel @AssistedInject constructor(
is RoomDetailAction.EndCall -> handleEndCall()
is RoomDetailAction.ManageIntegrations -> handleManageIntegrations()
is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action)
+ is RoomDetailAction.UpdateJoinJitsiCallStatus -> handleJitsiCallJoinStatus(action)
+ is RoomDetailAction.JoinJitsiCall -> handleJoinJitsiCall()
+ is RoomDetailAction.LeaveJitsiCall -> handleLeaveJitsiCall()
is RoomDetailAction.RemoveWidget -> handleDeleteWidget(action.widgetId)
is RoomDetailAction.EnsureNativeWidgetAllowed -> handleCheckWidgetAllowed(action)
is RoomDetailAction.CancelSend -> handleCancel(action)
@@ -340,6 +360,33 @@ class RoomDetailViewModel @AssistedInject constructor(
}.exhaustive
}
+ private fun handleJitsiCallJoinStatus(action: RoomDetailAction.UpdateJoinJitsiCallStatus) = withState { state ->
+ if (state.jitsiState.confId == null) {
+ // If jitsi widget is removed while on the call
+ if (state.jitsiState.hasJoined) {
+ setState { copy(jitsiState = jitsiState.copy(hasJoined = false)) }
+ }
+ return@withState
+ }
+ when (action.jitsiEvent.type) {
+ BroadcastEvent.Type.CONFERENCE_JOINED,
+ BroadcastEvent.Type.CONFERENCE_TERMINATED -> {
+ setState { copy(jitsiState = jitsiState.copy(hasJoined = activeConferenceHolder.isJoined(jitsiState.confId))) }
+ }
+ else -> Unit
+ }
+ }
+
+ private fun handleLeaveJitsiCall() {
+ _viewEvents.post(RoomDetailViewEvents.LeaveJitsiConference)
+ }
+
+ private fun handleJoinJitsiCall() = withState { state ->
+ val jitsiWidget = state.activeRoomWidgets()?.firstOrNull { it.widgetId == state.jitsiState.widgetId } ?: return@withState
+ val action = RoomDetailAction.EnsureNativeWidgetAllowed(jitsiWidget, false, RoomDetailViewEvents.JoinJitsiConference(jitsiWidget, true))
+ handleCheckWidgetAllowed(action)
+ }
+
private fun handleAcceptCall(action: RoomDetailAction.AcceptCall) {
callManager.getCallById(action.callId)?.also {
_viewEvents.post(RoomDetailViewEvents.DisplayAndAcceptCall(it))
@@ -448,10 +495,15 @@ class RoomDetailViewModel @AssistedInject constructor(
}
}
- private fun handleDeleteWidget(widgetId: String) {
- _viewEvents.post(RoomDetailViewEvents.ShowWaitingView)
+ private fun handleDeleteWidget(widgetId: String) = withState { state ->
+ val isJitsiWidget = state.jitsiState.widgetId == widgetId
viewModelScope.launch(Dispatchers.IO) {
try {
+ if (isJitsiWidget) {
+ setState { copy(jitsiState = jitsiState.copy(deleteWidgetInProgress = true)) }
+ } else {
+ _viewEvents.post(RoomDetailViewEvents.ShowWaitingView)
+ }
session.widgetService().destroyRoomWidget(room.roomId, widgetId)
// local echo
setState {
@@ -467,7 +519,11 @@ class RoomDetailViewModel @AssistedInject constructor(
} catch (failure: Throwable) {
_viewEvents.post(RoomDetailViewEvents.ShowMessage(stringProvider.getString(R.string.failed_to_remove_widget)))
} finally {
- _viewEvents.post(RoomDetailViewEvents.HideWaitingView)
+ if (isJitsiWidget) {
+ setState { copy(jitsiState = jitsiState.copy(deleteWidgetInProgress = false)) }
+ } else {
+ _viewEvents.post(RoomDetailViewEvents.HideWaitingView)
+ }
}
}
}
@@ -682,9 +738,10 @@ class RoomDetailViewModel @AssistedInject constructor(
R.id.timeline_setting -> true
R.id.invite -> state.canInvite
R.id.open_matrix_apps -> true
- R.id.voice_call,
- R.id.video_call -> callManager.getCallsByRoomId(state.roomId).isEmpty()
- R.id.hangup_call -> callManager.getCallsByRoomId(state.roomId).isNotEmpty()
+ R.id.voice_call -> state.isWebRTCCallOptionAvailable()
+ R.id.video_call -> state.isWebRTCCallOptionAvailable() || 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.isWebRTCCallOptionAvailable() && state.jitsiState.confId != null && !state.jitsiState.hasJoined
R.id.search -> true
R.id.dev_tools -> vectorPreferences.developerMode()
else -> false
@@ -1515,7 +1572,6 @@ class RoomDetailViewModel @AssistedInject constructor(
private fun observeSummaryState() {
asyncSubscribe(RoomDetailViewState::asyncRoomSummary) { summary ->
- roomSummariesHolder.set(summary)
setState {
val typingMessage = typingHelper.getTypingMessage(summary.typingUsers)
copy(
@@ -1563,7 +1619,6 @@ class RoomDetailViewModel @AssistedInject constructor(
}
override fun onCleared() {
- roomSummariesHolder.remove(room.roomId)
timeline.dispose()
timeline.removeAllListeners()
if (vectorPreferences.sendTypingNotifs()) {
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 d10456b7c2..1c75429d11 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
@@ -19,6 +19,7 @@ package im.vector.app.features.home.room.detail
import com.airbnb.mvrx.Async
import com.airbnb.mvrx.MvRxState
import com.airbnb.mvrx.Uninitialized
+import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.members.ChangeMembershipState
import org.matrix.android.sdk.api.session.room.model.RoomMemberSummary
@@ -26,6 +27,7 @@ import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
import org.matrix.android.sdk.api.session.sync.SyncState
import org.matrix.android.sdk.api.session.widgets.model.Widget
+import org.matrix.android.sdk.api.session.widgets.model.WidgetType
/**
* Describes the current send mode:
@@ -55,6 +57,14 @@ sealed class UnreadState {
data class HasUnread(val firstUnreadEventId: String) : UnreadState()
}
+data class JitsiState(
+ val hasJoined: Boolean = false,
+ // Not null if we have an active jitsi widget on the room
+ val confId: String? = null,
+ val widgetId: String? = null,
+ val deleteWidgetInProgress: Boolean = false
+)
+
data class RoomDetailViewState(
val roomId: String,
val eventId: String?,
@@ -75,7 +85,8 @@ data class RoomDetailViewState(
val canInvite: Boolean = true,
val isAllowedToManageWidgets: Boolean = false,
val isAllowedToStartWebRTCCall: Boolean = true,
- val hasFailedSending: Boolean = false
+ val hasFailedSending: Boolean = false,
+ val jitsiState: JitsiState = JitsiState()
) : MvRxState {
constructor(args: RoomDetailArgs) : this(
@@ -85,5 +96,11 @@ data class RoomDetailViewState(
highlightedEventId = args.eventId
)
+ fun isWebRTCCallOptionAvailable() = (asyncRoomSummary.invoke()?.joinedMembersCount ?: 0) <= 2
+
+ // This checks directly on the active room widgets.
+ // 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 isDm() = asyncRoomSummary()?.isDirect == true
}
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 b71b90ace3..92a75b449a 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
@@ -26,7 +26,6 @@ import im.vector.app.core.utils.PERMISSIONS_FOR_VIDEO_IP_CALL
import im.vector.app.core.utils.checkPermissions
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.settings.VectorPreferences
-import org.matrix.android.sdk.api.session.widgets.model.WidgetType
class StartCallActionsHandler(
private val roomId: String,
@@ -36,7 +35,7 @@ class StartCallActionsHandler(
private val roomDetailViewModel: RoomDetailViewModel,
private val startCallActivityResultLauncher: ActivityResultLauncher>,
private val showDialogWithMessage: (String) -> Unit,
- private val onTapToReturnToCall: () -> Unit) {
+ private val onTapToReturnToCall: () -> Unit) {
fun onVideoCallClicked() {
handleCallRequest(true)
@@ -61,16 +60,8 @@ class StartCallActionsHandler(
}
2 -> {
val currentCall = callManager.getCurrentCall()
- if (currentCall != null) {
- // resume existing if same room, if not prompt to kill and then restart new call?
- if (currentCall.signalingRoomId == roomId) {
- onTapToReturnToCall()
- }
- // else {
- // TODO might not work well, and should prompt
- // webRtcPeerConnectionManager.endCall()
- // safeStartCall(it, isVideoCall)
- // }
+ if (currentCall?.signalingRoomId == roomId) {
+ onTapToReturnToCall()
} else if (!state.isAllowedToStartWebRTCCall) {
showDialogWithMessage(fragment.getString(
if (state.isDm()) {
@@ -96,9 +87,8 @@ class StartCallActionsHandler(
}
))
} else {
- if (state.activeRoomWidgets()?.filter { it.type == WidgetType.Jitsi }?.any() == true) {
- // A conference is already in progress!
- showDialogWithMessage(fragment.getString(R.string.conference_call_in_progress))
+ if (state.hasActiveJitsiWidget()) {
+ // A conference is already in progress, return
} else {
MaterialAlertDialogBuilder(fragment.requireContext())
.setTitle(if (isVideoCall) R.string.video_meeting else R.string.audio_meeting)
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
index e1dae11c1c..8be319f2a8 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/TimelineEventController.kt
@@ -31,8 +31,7 @@ import im.vector.app.core.epoxy.LoadingItem_
import im.vector.app.core.extensions.localDateTime
import im.vector.app.core.extensions.nextOrNull
import im.vector.app.core.extensions.prevOrNull
-import im.vector.app.core.resources.UserPreferencesProvider
-import im.vector.app.features.call.webrtc.WebRtcCallManager
+import im.vector.app.features.home.room.detail.JitsiState
import im.vector.app.features.home.room.detail.RoomDetailAction
import im.vector.app.features.home.room.detail.RoomDetailViewState
import im.vector.app.features.home.room.detail.UnreadState
@@ -40,6 +39,7 @@ import im.vector.app.features.home.room.detail.timeline.factory.MergedHeaderItem
import im.vector.app.features.home.room.detail.timeline.factory.ReadReceiptsItemFactory
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactory
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactoryParams
+import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventsGroups
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.helper.ContentUploadStateTrackerBinder
import im.vector.app.features.home.room.detail.timeline.helper.TimelineControllerInterceptorHelper
@@ -47,14 +47,13 @@ import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventDiff
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityStateChangedListener
import im.vector.app.features.home.room.detail.timeline.helper.TimelineMediaSizeProvider
-import im.vector.app.features.home.room.detail.timeline.item.AbsMessageItem
import im.vector.app.features.home.room.detail.timeline.item.BasedMergedItem
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem_
+import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptsItem
-import im.vector.app.features.home.room.detail.timeline.item.SendStateDecoration
import im.vector.app.features.home.room.detail.timeline.url.PreviewUrlRetriever
import im.vector.app.features.media.ImageContentRenderer
import im.vector.app.features.media.VideoContentRenderer
@@ -65,6 +64,7 @@ import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.Membership
import org.matrix.android.sdk.api.session.room.model.ReadReceipt
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
+import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageAudioContent
import org.matrix.android.sdk.api.session.room.model.message.MessageImageInfoContent
import org.matrix.android.sdk.api.session.room.model.message.MessageVideoContent
@@ -80,14 +80,30 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val mergedHeaderItemFactory: MergedHeaderItemFactory,
private val session: Session,
- private val callManager: WebRtcCallManager,
@TimelineEventControllerHandler
private val backgroundHandler: Handler,
- private val userPreferencesProvider: UserPreferencesProvider,
private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper,
private val readReceiptsItemFactory: ReadReceiptsItemFactory
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor {
+ /**
+ * This is a partial state of the RoomDetailViewState
+ */
+ data class PartialState(
+ val unreadState: UnreadState = UnreadState.Unknown,
+ val highlightedEventId: String? = null,
+ val jitsiState: JitsiState = JitsiState(),
+ val roomSummary: RoomSummary? = null
+ ) {
+
+ constructor(state: RoomDetailViewState) : this(
+ unreadState = state.unreadState,
+ highlightedEventId = state.highlightedEventId,
+ jitsiState = state.jitsiState,
+ roomSummary = state.asyncRoomSummary()
+ )
+ }
+
interface Callback :
BaseCallback,
ReactionPillCallback,
@@ -149,14 +165,15 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
// Map eventId to adapter position
private val adapterPositionMapping = HashMap()
+ private val timelineEventsGroups = TimelineEventsGroups()
+ private val receiptsByEvent = HashMap>()
private val modelCache = arrayListOf()
private var currentSnapshot: List = emptyList()
private var inSubmitList: Boolean = false
private var hasReachedInvite: Boolean = false
private var hasUTD: Boolean = false
- private var unreadState: UnreadState = UnreadState.Unknown
private var positionOfReadMarker: Int? = null
- private var eventIdToHighlight: String? = null
+ private var partialState: PartialState = PartialState()
var callback: Callback? = null
var timeline: Timeline? = null
@@ -174,7 +191,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
// it's sent by the same user so we are sure we have up to date information.
val invalidatedSenderId: String? = currentSnapshot.getOrNull(position)?.senderInfo?.userId
val prevDisplayableEventIndex = currentSnapshot.subList(0, position).indexOfLast {
- timelineEventVisibilityHelper.shouldShowEvent(it, eventIdToHighlight)
+ timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId)
}
if (prevDisplayableEventIndex != -1 && currentSnapshot[prevDisplayableEventIndex].senderInfo.userId == invalidatedSenderId) {
modelCache[prevDisplayableEventIndex] = null
@@ -215,9 +232,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private val interceptorHelper = TimelineControllerInterceptorHelper(
::positionOfReadMarker,
- adapterPositionMapping,
- userPreferencesProvider,
- callManager
+ adapterPositionMapping
)
init {
@@ -226,29 +241,22 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
}
override fun intercept(models: MutableList>) = synchronized(modelCache) {
- interceptorHelper.intercept(models, unreadState, timeline, callback)
+ interceptorHelper.intercept(models, partialState.unreadState, timeline, callback)
}
- fun update(viewState: RoomDetailViewState) {
- var requestModelBuild = false
- if (eventIdToHighlight != viewState.highlightedEventId) {
+ fun update(viewState: RoomDetailViewState) = synchronized(modelCache) {
+ val newPartialState = PartialState(viewState)
+ if (partialState.highlightedEventId != newPartialState.highlightedEventId) {
// Clear cache to force a refresh
- synchronized(modelCache) {
- for (i in 0 until modelCache.size) {
- if (modelCache[i]?.eventId == viewState.highlightedEventId
- || modelCache[i]?.eventId == eventIdToHighlight) {
- modelCache[i] = null
- }
+ for (i in 0 until modelCache.size) {
+ if (modelCache[i]?.eventId == viewState.highlightedEventId
+ || modelCache[i]?.eventId == partialState.highlightedEventId) {
+ modelCache[i] = null
}
}
- eventIdToHighlight = viewState.highlightedEventId
- requestModelBuild = true
}
- if (this.unreadState != viewState.unreadState) {
- this.unreadState = viewState.unreadState
- requestModelBuild = true
- }
- if (requestModelBuild) {
+ if (newPartialState != partialState) {
+ partialState = newPartialState
requestModelBuild()
}
}
@@ -346,31 +354,33 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
if (modelCache.isEmpty()) {
return
}
- val receiptsByEvents = getReadReceiptsByShownEvent()
- val lastSentEventWithoutReadReceipts = searchLastSentEventWithoutReadReceipts(receiptsByEvents)
+ preprocessReverseEvents()
+ val lastSentEventWithoutReadReceipts = searchLastSentEventWithoutReadReceipts(receiptsByEvent)
(0 until modelCache.size).forEach { position ->
val event = currentSnapshot[position]
val nextEvent = currentSnapshot.nextOrNull(position)
val prevEvent = currentSnapshot.prevOrNull(position)
val nextDisplayableEvent = currentSnapshot.subList(position + 1, currentSnapshot.size).firstOrNull {
- timelineEventVisibilityHelper.shouldShowEvent(it, eventIdToHighlight)
+ timelineEventVisibilityHelper.shouldShowEvent(it, partialState.highlightedEventId)
}
- val params = TimelineItemFactoryParams(
- event = event,
- prevEvent = prevEvent,
- nextEvent = nextEvent,
- nextDisplayableEvent = nextDisplayableEvent,
- highlightedEventId = eventIdToHighlight,
- lastSentEventIdWithoutReadReceipts = lastSentEventWithoutReadReceipts,
- callback = callback
- )
// Should be build if not cached or if model should be refreshed
- if (modelCache[position] == null || modelCache[position]?.shouldTriggerBuild == true) {
+ if (modelCache[position] == null || modelCache[position]?.isCacheable == false) {
+ val timelineEventsGroup = timelineEventsGroups.getOrNull(event)
+ val params = TimelineItemFactoryParams(
+ event = event,
+ prevEvent = prevEvent,
+ nextEvent = nextEvent,
+ nextDisplayableEvent = nextDisplayableEvent,
+ partialState = partialState,
+ lastSentEventIdWithoutReadReceipts = lastSentEventWithoutReadReceipts,
+ callback = callback,
+ eventsGroup = timelineEventsGroup
+ )
modelCache[position] = buildCacheItem(params)
}
val itemCachedData = modelCache[position] ?: return@forEach
// Then update with additional models if needed
- modelCache[position] = itemCachedData.enrichWithModels(event, nextEvent, position, receiptsByEvents)
+ modelCache[position] = itemCachedData.enrichWithModels(event, nextEvent, position, receiptsByEvent)
}
}
@@ -384,12 +394,13 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
it.id(event.localId)
it.setOnVisibilityStateChanged(TimelineEventVisibilityStateChangedListener(callback, event))
}
- val shouldTriggerBuild = eventModel is AbsMessageItem && eventModel.attributes.informationData.sendStateDecoration == SendStateDecoration.SENT
+ val isCacheable = eventModel is ItemWithEvents && eventModel.isCacheable()
return CacheItemData(
localId = event.localId,
eventId = event.root.eventId,
eventModel = eventModel,
- shouldTriggerBuild = shouldTriggerBuild)
+ isCacheable = isCacheable
+ )
}
private fun CacheItemData.enrichWithModels(event: TimelineEvent,
@@ -399,10 +410,11 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
val wantsDateSeparator = wantsDateSeparator(event, nextEvent)
val mergedHeaderModel = mergedHeaderItemFactory.create(event,
nextEvent = nextEvent,
+ partialState = partialState,
items = this@TimelineEventController.currentSnapshot,
addDaySeparator = wantsDateSeparator,
currentPosition = position,
- eventIdToHighlight = eventIdToHighlight,
+ eventIdToHighlight = partialState.highlightedEventId,
callback = callback
) {
requestModelBuild()
@@ -431,7 +443,7 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
return null
}
// If the event is not shown, we go to the next one
- if (!timelineEventVisibilityHelper.shouldShowEvent(event, eventIdToHighlight)) {
+ if (!timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId)) {
continue
}
// If the event is sent by us, we update the holder with the eventId and stop the search
@@ -442,19 +454,18 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
return null
}
- private fun getReadReceiptsByShownEvent(): Map> {
- val receiptsByEvent = HashMap>()
- if (!userPreferencesProvider.shouldShowReadReceipts()) {
- return receiptsByEvent
- }
- var lastShownEventId: String? = null
+ private fun preprocessReverseEvents() {
+ receiptsByEvent.clear()
+ timelineEventsGroups.clear()
val itr = currentSnapshot.listIterator(currentSnapshot.size)
+ var lastShownEventId: String? = null
while (itr.hasPrevious()) {
val event = itr.previous()
+ timelineEventsGroups.addOrIgnore(event)
val currentReadReceipts = ArrayList(event.readReceipts).filter {
it.user.userId != session.myUserId
}
- if (timelineEventVisibilityHelper.shouldShowEvent(event, eventIdToHighlight)) {
+ if (timelineEventVisibilityHelper.shouldShowEvent(event, partialState.highlightedEventId)) {
lastShownEventId = event.eventId
}
if (lastShownEventId == null) {
@@ -463,7 +474,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
val existingReceipts = receiptsByEvent.getOrPut(lastShownEventId) { ArrayList() }
existingReceipts.addAll(currentReadReceipts)
}
- return receiptsByEvent
}
private fun buildDaySeparatorItem(originServerTs: Long?): DaySeparatorItem {
@@ -536,6 +546,6 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
val eventModel: EpoxyModel<*>? = null,
val mergedHeaderModel: BasedMergedItem<*>? = null,
val formattedDayModel: DaySeparatorItem? = null,
- val shouldTriggerBuild: Boolean = false
+ val isCacheable: Boolean = true
)
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
index b9c368ebdc..6e6c7c1dbe 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/action/MessageActionsViewModel.kt
@@ -36,6 +36,7 @@ import im.vector.app.features.html.VectorHtmlCompressor
import im.vector.app.features.powerlevel.PowerLevelsObservableFactory
import im.vector.app.features.reactions.data.EmojiDataSource
import im.vector.app.features.settings.VectorPreferences
+import org.matrix.android.sdk.api.extensions.orFalse
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.crypto.keysbackup.KeysBackupState
import org.matrix.android.sdk.api.session.events.model.EventType
@@ -207,7 +208,7 @@ class MessageActionsViewModel @AssistedInject constructor(@Assisted
EventType.CALL_CANDIDATES,
EventType.CALL_HANGUP,
EventType.CALL_ANSWER -> {
- noticeEventFormatter.format(timelineEvent)
+ noticeEventFormatter.format(timelineEvent, room?.roomSummary()?.isDirect.orFalse())
}
else -> null
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt
index 9697fb6672..97f2618fe6 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/CallItemFactory.kt
@@ -16,127 +16,122 @@
package im.vector.app.features.home.room.detail.timeline.factory
import im.vector.app.core.epoxy.VectorEpoxyModel
-import im.vector.app.features.call.vectorCallService
-import im.vector.app.features.call.webrtc.WebRtcCallManager
+import im.vector.app.core.resources.UserPreferencesProvider
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
+import im.vector.app.features.home.room.detail.timeline.helper.CallSignalingEventsGroup
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
-import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
import org.matrix.android.sdk.api.session.Session
import org.matrix.android.sdk.api.session.events.model.EventType
-import org.matrix.android.sdk.api.session.events.model.toModel
-import org.matrix.android.sdk.api.session.room.model.call.CallAnswerContent
-import org.matrix.android.sdk.api.session.room.model.call.CallHangupContent
-import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
-import org.matrix.android.sdk.api.session.room.model.call.CallRejectContent
-import org.matrix.android.sdk.api.session.room.model.call.CallSignalingContent
-import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class CallItemFactory @Inject constructor(
private val session: Session,
+ private val userPreferencesProvider: UserPreferencesProvider,
private val messageColorProvider: MessageColorProvider,
private val messageInformationDataFactory: MessageInformationDataFactory,
private val messageItemAttributesFactory: MessageItemAttributesFactory,
private val avatarSizeProvider: AvatarSizeProvider,
- private val roomSummariesHolder: RoomSummariesHolder,
- private val callManager: WebRtcCallManager
-) {
+ private val noticeItemFactory: NoticeItemFactory) {
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
val event = params.event
if (event.root.eventId == null) return null
- val roomId = event.roomId
+ val showHiddenEvents = userPreferencesProvider.shouldShowHiddenEvents()
+ val callEventGrouper = params.eventsGroup?.let { CallSignalingEventsGroup(it) } ?: return null
+ val roomSummary = params.partialState.roomSummary ?: return null
val informationData = messageInformationDataFactory.create(params)
- val callSignalingContent = event.getCallSignalingContent() ?: return null
- val callId = callSignalingContent.callId ?: return null
- val call = callManager.getCallById(callId)
- val callKind = when {
- call == null -> CallTileTimelineItem.CallKind.UNKNOWN
- call.mxCall.isVideoCall -> CallTileTimelineItem.CallKind.VIDEO
- else -> CallTileTimelineItem.CallKind.AUDIO
- }
- return when (event.root.getClearType()) {
+ val callKind = if (callEventGrouper.isVideo()) CallTileTimelineItem.CallKind.VIDEO else CallTileTimelineItem.CallKind.AUDIO
+ val callItem = when (event.root.getClearType()) {
EventType.CALL_ANSWER -> {
- createCallTileTimelineItem(
- roomId = roomId,
- callId = callId,
- callStatus = CallTileTimelineItem.CallStatus.IN_CALL,
- callKind = callKind,
- callback = params.callback,
- highlight = params.isHighlighted,
- informationData = informationData,
- isStillActive = call != null
- )
+ if (callEventGrouper.isInCall()) {
+ createCallTileTimelineItem(
+ roomSummary = roomSummary,
+ callId = callEventGrouper.callId,
+ callStatus = CallTileTimelineItem.CallStatus.IN_CALL,
+ callKind = callKind,
+ callback = params.callback,
+ highlight = params.isHighlighted,
+ informationData = informationData,
+ isStillActive = callEventGrouper.isInCall(),
+ formattedDuration = callEventGrouper.formattedDuration()
+ )
+ } else {
+ null
+ }
}
EventType.CALL_INVITE -> {
- createCallTileTimelineItem(
- roomId = roomId,
- callId = callId,
- callStatus = CallTileTimelineItem.CallStatus.INVITED,
- callKind = callKind,
- callback = params.callback,
- highlight = params.isHighlighted,
- informationData = informationData,
- isStillActive = call != null
- )
+ if (callEventGrouper.isRinging()) {
+ createCallTileTimelineItem(
+ roomSummary = roomSummary,
+ callId = callEventGrouper.callId,
+ callStatus = CallTileTimelineItem.CallStatus.INVITED,
+ callKind = callKind,
+ callback = params.callback,
+ highlight = params.isHighlighted,
+ informationData = informationData,
+ isStillActive = callEventGrouper.isRinging(),
+ formattedDuration = callEventGrouper.formattedDuration()
+ )
+ } else {
+ null
+ }
}
EventType.CALL_REJECT -> {
createCallTileTimelineItem(
- roomId = roomId,
- callId = callId,
+ roomSummary = roomSummary,
+ callId = callEventGrouper.callId,
callStatus = CallTileTimelineItem.CallStatus.REJECTED,
callKind = callKind,
callback = params.callback,
highlight = params.isHighlighted,
informationData = informationData,
- isStillActive = false
+ isStillActive = false,
+ formattedDuration = callEventGrouper.formattedDuration()
)
}
EventType.CALL_HANGUP -> {
createCallTileTimelineItem(
- roomId = roomId,
- callId = callId,
- callStatus = CallTileTimelineItem.CallStatus.ENDED,
+ roomSummary = roomSummary,
+ callId = callEventGrouper.callId,
+ callStatus = if (callEventGrouper.callWasMissed()) CallTileTimelineItem.CallStatus.MISSED else CallTileTimelineItem.CallStatus.ENDED,
callKind = callKind,
callback = params.callback,
highlight = params.isHighlighted,
informationData = informationData,
- isStillActive = false
+ isStillActive = false,
+ formattedDuration = callEventGrouper.formattedDuration()
)
}
else -> null
}
- }
-
- private fun TimelineEvent.getCallSignalingContent(): CallSignalingContent? {
- return when (root.getClearType()) {
- EventType.CALL_INVITE -> root.getClearContent().toModel()
- EventType.CALL_HANGUP -> root.getClearContent().toModel()
- EventType.CALL_REJECT -> root.getClearContent().toModel()
- EventType.CALL_ANSWER -> root.getClearContent().toModel()
- else -> null
+ return if (callItem == null && showHiddenEvents) {
+ // Fallback to notice item for showing hidden events
+ noticeItemFactory.create(params)
+ } else {
+ callItem
}
}
private fun createCallTileTimelineItem(
- roomId: String,
+ roomSummary: RoomSummary,
callId: String,
callKind: CallTileTimelineItem.CallKind,
callStatus: CallTileTimelineItem.CallStatus,
informationData: MessageInformationData,
highlight: Boolean,
isStillActive: Boolean,
+ formattedDuration: String,
callback: TimelineEventController.Callback?
): CallTileTimelineItem? {
- val correctedRoomId = session.vectorCallService.userMapper.nativeRoomForVirtualRoom(roomId) ?: roomId
- val userOfInterest = roomSummariesHolder.get(correctedRoomId)?.toMatrixItem() ?: return null
+ val userOfInterest = roomSummary.toMatrixItem()
val attributes = messageItemAttributesFactory.create(null, informationData, callback).let {
CallTileTimelineItem.Attributes(
callId = callId,
@@ -144,6 +139,7 @@ class CallItemFactory @Inject constructor(
callStatus = callStatus,
informationData = informationData,
avatarRenderer = it.avatarRenderer,
+ formattedDuration = formattedDuration,
messageColorProvider = messageColorProvider,
itemClickListener = it.itemClickListener,
itemLongClickListener = it.itemLongClickListener,
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt
index cb2a067540..f552266a1c 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/MergedHeaderItemFactory.kt
@@ -22,7 +22,6 @@ import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
import im.vector.app.features.home.room.detail.timeline.helper.MergedTimelineEventVisibilityStateChangedListener
-import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventVisibilityHelper
import im.vector.app.features.home.room.detail.timeline.helper.canBeMerged
import im.vector.app.features.home.room.detail.timeline.helper.isRoomConfiguration
@@ -47,8 +46,7 @@ import javax.inject.Inject
class MergedHeaderItemFactory @Inject constructor(private val activeSessionHolder: ActiveSessionHolder,
private val avatarRenderer: AvatarRenderer,
private val avatarSizeProvider: AvatarSizeProvider,
- private val roomSummariesHolder: RoomSummariesHolder,
-private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
+ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
private val collapsedEventIds = linkedSetOf()
private val mergeItemCollapseStates = HashMap()
@@ -60,6 +58,7 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
fun create(event: TimelineEvent,
nextEvent: TimelineEvent?,
items: List,
+ partialState: TimelineEventController.PartialState,
addDaySeparator: Boolean,
currentPosition: Int,
eventIdToHighlight: String?,
@@ -70,18 +69,17 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
&& event.isRoomConfiguration(nextEvent.root.getClearContent()?.toModel()?.creator)) {
// It's the first item before room.create
// Collapse all room configuration events
- buildRoomCreationMergedSummary(currentPosition, items, event, eventIdToHighlight, requestModelBuild, callback)
+ buildRoomCreationMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback)
} else if (!event.canBeMerged() || (nextEvent?.root?.getClearType() == event.root.getClearType() && !addDaySeparator)) {
null
} else {
- buildMembershipEventsMergedSummary(currentPosition, items, event, eventIdToHighlight, requestModelBuild, callback)
+ buildMembershipEventsMergedSummary(currentPosition, items, partialState, event, eventIdToHighlight, requestModelBuild, callback)
}
}
- private fun isDirectRoom(roomId: String) = roomSummariesHolder.get(roomId)?.isDirect.orFalse()
-
private fun buildMembershipEventsMergedSummary(currentPosition: Int,
items: List,
+ partialState: TimelineEventController.PartialState,
event: TimelineEvent,
eventIdToHighlight: String?,
requestModelBuild: () -> Unit,
@@ -102,7 +100,7 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
memberName = mergedEvent.senderInfo.disambiguatedDisplayName,
localId = mergedEvent.localId,
eventId = mergedEvent.root.eventId ?: "",
- isDirectRoom = isDirectRoom(event.roomId)
+ isDirectRoom = partialState.isDirectRoom()
)
mergedData.add(data)
}
@@ -141,6 +139,7 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
private fun buildRoomCreationMergedSummary(currentPosition: Int,
items: List,
+ partialState: TimelineEventController.PartialState,
event: TimelineEvent,
eventIdToHighlight: String?,
requestModelBuild: () -> Unit,
@@ -173,7 +172,7 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
memberName = mergedEvent.senderInfo.disambiguatedDisplayName,
localId = mergedEvent.localId,
eventId = mergedEvent.root.eventId ?: "",
- isDirectRoom = isDirectRoom(event.roomId)
+ isDirectRoom = partialState.isDirectRoom()
)
mergedData.add(data)
}
@@ -206,7 +205,7 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
isEncryptionAlgorithmSecure = encryptionAlgorithm == MXCRYPTO_ALGORITHM_MEGOLM,
callback = callback,
currentUserId = currentUserId,
- roomSummary = roomSummariesHolder.get(event.roomId),
+ roomSummary = partialState.roomSummary,
canChangeAvatar = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_AVATAR) ?: false,
canChangeTopic = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_TOPIC) ?: false,
canChangeName = powerLevelsHelper?.isUserAllowedToSend(currentUserId, true, EventType.STATE_ROOM_NAME) ?: false
@@ -223,6 +222,10 @@ private val timelineEventVisibilityHelper: TimelineEventVisibilityHelper) {
} else null
}
+ private fun TimelineEventController.PartialState.isDirectRoom(): Boolean {
+ return roomSummary?.isDirect.orFalse()
+ }
+
fun isCollapsed(localId: Long): Boolean {
return collapsedEventIds.contains(localId)
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt
index e757b6b47b..ed6620dcd4 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/NoticeItemFactory.kt
@@ -22,6 +22,7 @@ import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvide
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
import im.vector.app.features.home.room.detail.timeline.item.NoticeItem
import im.vector.app.features.home.room.detail.timeline.item.NoticeItem_
+import org.matrix.android.sdk.api.extensions.orFalse
import javax.inject.Inject
class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEventFormatter,
@@ -31,7 +32,7 @@ class NoticeItemFactory @Inject constructor(private val eventFormatter: NoticeEv
fun create(params: TimelineItemFactoryParams): NoticeItem? {
val event = params.event
- val formattedText = eventFormatter.format(event) ?: return null
+ val formattedText = eventFormatter.format(event, isDm = params.partialState.roomSummary?.isDirect.orFalse()) ?: return null
val informationData = informationDataFactory.create(params)
val attributes = NoticeItem.Attributes(
avatarRenderer = avatarRenderer,
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt
index 0e595ba30e..cdfedb2925 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/TimelineItemFactoryParams.kt
@@ -17,6 +17,7 @@
package im.vector.app.features.home.room.detail.timeline.factory
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
+import im.vector.app.features.home.room.detail.timeline.helper.TimelineEventsGroup
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
data class TimelineItemFactoryParams(
@@ -24,9 +25,14 @@ data class TimelineItemFactoryParams(
val prevEvent: TimelineEvent? = null,
val nextEvent: TimelineEvent? = null,
val nextDisplayableEvent: TimelineEvent? = null,
- val highlightedEventId: String? = null,
+ val partialState: TimelineEventController.PartialState = TimelineEventController.PartialState(),
val lastSentEventIdWithoutReadReceipts: String? = null,
- val callback: TimelineEventController.Callback? = null
+ val callback: TimelineEventController.Callback? = null,
+ val eventsGroup: TimelineEventsGroup? = null
) {
+
+ val highlightedEventId: String?
+ get() = partialState.highlightedEventId
+
val isHighlighted = highlightedEventId == event.eventId
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt
index 1fc57489a5..52f72810c9 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/factory/WidgetItemFactory.kt
@@ -16,34 +16,28 @@
package im.vector.app.features.home.room.detail.timeline.factory
-import im.vector.app.ActiveSessionDataSource
-import im.vector.app.R
import im.vector.app.core.epoxy.VectorEpoxyModel
-import im.vector.app.core.resources.StringProvider
+import im.vector.app.core.resources.UserPreferencesProvider
+import im.vector.app.features.home.AvatarRenderer
+import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
import im.vector.app.features.home.room.detail.timeline.helper.AvatarSizeProvider
+import im.vector.app.features.home.room.detail.timeline.helper.JitsiWidgetEventsGroup
import im.vector.app.features.home.room.detail.timeline.helper.MessageInformationDataFactory
-import im.vector.app.features.home.room.detail.timeline.helper.MessageItemAttributesFactory
-import im.vector.app.features.home.room.detail.timeline.item.WidgetTileTimelineItem
-import im.vector.app.features.home.room.detail.timeline.item.WidgetTileTimelineItem_
-import org.matrix.android.sdk.api.extensions.orFalse
-import org.matrix.android.sdk.api.session.events.model.Event
+import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem
+import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem_
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.widgets.model.WidgetContent
import org.matrix.android.sdk.api.session.widgets.model.WidgetType
+import org.matrix.android.sdk.api.util.toMatrixItem
import javax.inject.Inject
class WidgetItemFactory @Inject constructor(
- private val sp: StringProvider,
- private val messageItemAttributesFactory: MessageItemAttributesFactory,
private val informationDataFactory: MessageInformationDataFactory,
private val noticeItemFactory: NoticeItemFactory,
private val avatarSizeProvider: AvatarSizeProvider,
- private val activeSessionDataSource: ActiveSessionDataSource
-) {
- private val currentUserId: String?
- get() = activeSessionDataSource.currentValue?.orNull()?.myUserId
-
- private fun Event.isSentByCurrentUser() = senderId != null && senderId == currentUserId
+ private val messageColorProvider: MessageColorProvider,
+ private val avatarRenderer: AvatarRenderer,
+ private val userPreferencesProvider: UserPreferencesProvider) {
fun create(params: TimelineItemFactoryParams): VectorEpoxyModel<*>? {
val event = params.event
@@ -51,62 +45,54 @@ class WidgetItemFactory @Inject constructor(
val previousWidgetContent: WidgetContent? = event.root.resolvedPrevContent().toModel()
return when (WidgetType.fromString(widgetContent.type ?: previousWidgetContent?.type ?: "")) {
- WidgetType.Jitsi -> createJitsiItem(params, widgetContent, previousWidgetContent)
+ WidgetType.Jitsi -> createJitsiItem(params, widgetContent)
// There is lot of other widget types we could improve here
else -> noticeItemFactory.create(params)
}
}
- private fun createJitsiItem(params: TimelineItemFactoryParams,
- widgetContent: WidgetContent,
- previousWidgetContent: WidgetContent?): VectorEpoxyModel<*> {
- val timelineEvent = params.event
+ private fun createJitsiItem(params: TimelineItemFactoryParams, widgetContent: WidgetContent): VectorEpoxyModel<*>? {
val informationData = informationDataFactory.create(params)
- val attributes = messageItemAttributesFactory.create(null, informationData, params.callback)
-
- val disambiguatedDisplayName = timelineEvent.senderInfo.disambiguatedDisplayName
- val message = if (widgetContent.isActive()) {
- val widgetName = widgetContent.getHumanName()
- if (previousWidgetContent?.isActive().orFalse()) {
- // Widget has been modified
- if (timelineEvent.root.isSentByCurrentUser()) {
- sp.getString(R.string.notice_widget_jitsi_modified_by_you, widgetName)
- } else {
- sp.getString(R.string.notice_widget_jitsi_modified, disambiguatedDisplayName, widgetName)
- }
+ val userOfInterest = params.partialState.roomSummary?.toMatrixItem() ?: return null
+ val isActiveTile = widgetContent.isActive()
+ val jitsiWidgetEventsGroup = params.eventsGroup?.let { JitsiWidgetEventsGroup(it) } ?: return null
+ val isCallStillActive = jitsiWidgetEventsGroup.isStillActive()
+ val showHiddenEvents = userPreferencesProvider.shouldShowHiddenEvents()
+ if (isActiveTile && !isCallStillActive) {
+ return if (showHiddenEvents) {
+ noticeItemFactory.create(params)
} else {
- // Widget has been added
- if (timelineEvent.root.isSentByCurrentUser()) {
- sp.getString(R.string.notice_widget_jitsi_added_by_you, widgetName)
- } else {
- sp.getString(R.string.notice_widget_jitsi_added, disambiguatedDisplayName, widgetName)
- }
- }
- } else {
- // Widget has been removed
- val widgetName = previousWidgetContent?.getHumanName()
- if (timelineEvent.root.isSentByCurrentUser()) {
- sp.getString(R.string.notice_widget_jitsi_removed_by_you, widgetName)
- } else {
- sp.getString(R.string.notice_widget_jitsi_removed, disambiguatedDisplayName, widgetName)
+ null
}
}
-
- return WidgetTileTimelineItem_()
- .attributes(
- WidgetTileTimelineItem.Attributes(
- title = message,
- drawableStart = R.drawable.ic_video,
- informationData = informationData,
- avatarRenderer = attributes.avatarRenderer,
- messageColorProvider = attributes.messageColorProvider,
- itemLongClickListener = attributes.itemLongClickListener,
- itemClickListener = attributes.itemClickListener,
- reactionPillCallback = attributes.reactionPillCallback,
- readReceiptsCallback = attributes.readReceiptsCallback,
- emojiTypeFace = attributes.emojiTypeFace
- )
- )
+ val callStatus = if (isActiveTile && params.event.root.stateKey == params.partialState.jitsiState.widgetId) {
+ if (params.partialState.jitsiState.hasJoined) {
+ CallTileTimelineItem.CallStatus.IN_CALL
+ } else {
+ CallTileTimelineItem.CallStatus.INVITED
+ }
+ } else {
+ CallTileTimelineItem.CallStatus.ENDED
+ }
+ val attributes = CallTileTimelineItem.Attributes(
+ callId = jitsiWidgetEventsGroup.callId,
+ callKind = CallTileTimelineItem.CallKind.CONFERENCE,
+ callStatus = callStatus,
+ informationData = informationData,
+ avatarRenderer = avatarRenderer,
+ messageColorProvider = messageColorProvider,
+ itemClickListener = null,
+ itemLongClickListener = null,
+ reactionPillCallback = params.callback,
+ readReceiptsCallback = params.callback,
+ userOfInterest = userOfInterest,
+ callback = params.callback,
+ isStillActive = isCallStillActive,
+ formattedDuration = ""
+ )
+ return CallTileTimelineItem_()
+ .attributes(attributes)
+ .highlighted(params.isHighlighted)
.leftGuideline(avatarSizeProvider.leftGuideline)
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
index e6fbc5294b..5a9af975ed 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/DisplayableEventFormatter.kt
@@ -41,7 +41,7 @@ class DisplayableEventFormatter @Inject constructor(
private val noticeEventFormatter: NoticeEventFormatter
) {
- fun format(timelineEvent: TimelineEvent, appendAuthor: Boolean): CharSequence {
+ fun format(timelineEvent: TimelineEvent, isDm: Boolean, appendAuthor: Boolean): CharSequence {
if (timelineEvent.root.isRedacted()) {
return noticeEventFormatter.formatRedactedEvent(timelineEvent.root)
}
@@ -135,7 +135,7 @@ class DisplayableEventFormatter @Inject constructor(
}
else -> {
return span {
- text = noticeEventFormatter.format(timelineEvent) ?: ""
+ text = noticeEventFormatter.format(timelineEvent, isDm) ?: ""
textStyle = "italic"
}
}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
index b1b96df9ea..c80a92d568 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/format/NoticeEventFormatter.kt
@@ -19,7 +19,6 @@ package im.vector.app.features.home.room.detail.timeline.format
import im.vector.app.ActiveSessionDataSource
import im.vector.app.R
import im.vector.app.core.resources.StringProvider
-import im.vector.app.features.home.room.detail.timeline.helper.RoomSummariesHolder
import im.vector.app.features.roomprofile.permissions.RoleFormatter
import im.vector.app.features.settings.VectorPreferences
import org.matrix.android.sdk.api.extensions.appendNl
@@ -40,7 +39,6 @@ import org.matrix.android.sdk.api.session.room.model.RoomJoinRulesContent
import org.matrix.android.sdk.api.session.room.model.RoomMemberContent
import org.matrix.android.sdk.api.session.room.model.RoomNameContent
import org.matrix.android.sdk.api.session.room.model.RoomServerAclContent
-import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.RoomThirdPartyInviteContent
import org.matrix.android.sdk.api.session.room.model.RoomTopicContent
import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
@@ -58,7 +56,6 @@ class NoticeEventFormatter @Inject constructor(
private val roomHistoryVisibilityFormatter: RoomHistoryVisibilityFormatter,
private val roleFormatter: RoleFormatter,
private val vectorPreferences: VectorPreferences,
- private val roomSummariesHolder: RoomSummariesHolder,
private val sp: StringProvider
) {
@@ -67,28 +64,25 @@ class NoticeEventFormatter @Inject constructor(
private fun Event.isSentByCurrentUser() = senderId != null && senderId == currentUserId
- private fun RoomSummary?.isDm() = this?.isDirect.orFalse()
-
- fun format(timelineEvent: TimelineEvent): CharSequence? {
- val rs = roomSummariesHolder.get(timelineEvent.roomId)
+ fun format(timelineEvent: TimelineEvent, isDm: Boolean): CharSequence? {
return when (val type = timelineEvent.root.getClearType()) {
- EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
- EventType.STATE_ROOM_CREATE -> formatRoomCreateEvent(timelineEvent.root, rs)
+ EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm)
+ EventType.STATE_ROOM_CREATE -> formatRoomCreateEvent(timelineEvent.root, isDm)
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_AVATAR -> formatRoomAvatarEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
- EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
- EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
+ EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm)
+ EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm)
EventType.STATE_ROOM_ALIASES -> formatRoomAliasesEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_CANONICAL_ALIAS -> formatRoomCanonicalAliasEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_HISTORY_VISIBILITY ->
- formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
+ formatRoomHistoryVisibilityEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm)
EventType.STATE_ROOM_SERVER_ACL -> formatRoomServerAclEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
- EventType.STATE_ROOM_GUEST_ACCESS -> formatRoomGuestAccessEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
+ EventType.STATE_ROOM_GUEST_ACCESS -> formatRoomGuestAccessEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm)
EventType.STATE_ROOM_ENCRYPTION -> formatRoomEncryptionEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.STATE_ROOM_WIDGET,
EventType.STATE_ROOM_WIDGET_LEGACY -> formatWidgetEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
- EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, rs)
+ EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName, isDm)
EventType.STATE_ROOM_POWER_LEVELS -> formatRoomPowerLevels(timelineEvent.root, timelineEvent.senderInfo.disambiguatedDisplayName)
EventType.CALL_INVITE,
EventType.CALL_CANDIDATES,
@@ -176,20 +170,20 @@ class NoticeEventFormatter @Inject constructor(
}
}
- fun format(event: Event, senderName: String?, rs: RoomSummary?): CharSequence? {
+ fun format(event: Event, senderName: String?, isDm: Boolean): CharSequence? {
return when (val type = event.getClearType()) {
- EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(event, senderName, rs)
+ EventType.STATE_ROOM_JOIN_RULES -> formatJoinRulesEvent(event, senderName, isDm)
EventType.STATE_ROOM_NAME -> formatRoomNameEvent(event, senderName)
EventType.STATE_ROOM_TOPIC -> formatRoomTopicEvent(event, senderName)
EventType.STATE_ROOM_AVATAR -> formatRoomAvatarEvent(event, senderName)
- EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName, rs)
- EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(event, senderName, rs)
- EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName, rs)
+ EventType.STATE_ROOM_MEMBER -> formatRoomMemberEvent(event, senderName, isDm)
+ EventType.STATE_ROOM_THIRD_PARTY_INVITE -> formatRoomThirdPartyInvite(event, senderName, isDm)
+ EventType.STATE_ROOM_HISTORY_VISIBILITY -> formatRoomHistoryVisibilityEvent(event, senderName, isDm)
EventType.CALL_INVITE,
EventType.CALL_HANGUP,
EventType.CALL_REJECT,
EventType.CALL_ANSWER -> formatCallEvent(type, event, senderName)
- EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName, rs)
+ EventType.STATE_ROOM_TOMBSTONE -> formatRoomTombstoneEvent(event, senderName, isDm)
else -> {
Timber.v("Type $type not handled by this formatter")
null
@@ -201,14 +195,14 @@ class NoticeEventFormatter @Inject constructor(
return "Debug: event type \"${event.getClearType()}\""
}
- private fun formatRoomCreateEvent(event: Event, rs: RoomSummary?): CharSequence? {
+ private fun formatRoomCreateEvent(event: Event, isDm: Boolean): CharSequence? {
return event.getClearContent().toModel()
?.takeIf { it.creator.isNullOrBlank().not() }
?.let {
if (event.isSentByCurrentUser()) {
- sp.getString(if (rs.isDm()) R.string.notice_direct_room_created_by_you else R.string.notice_room_created_by_you)
+ sp.getString(if (isDm) R.string.notice_direct_room_created_by_you else R.string.notice_room_created_by_you)
} else {
- sp.getString(if (rs.isDm()) R.string.notice_direct_room_created else R.string.notice_room_created, it.creator)
+ sp.getString(if (isDm) R.string.notice_direct_room_created else R.string.notice_room_created, it.creator)
}
}
}
@@ -230,11 +224,11 @@ class NoticeEventFormatter @Inject constructor(
}
}
- private fun formatRoomTombstoneEvent(event: Event, senderName: String?, rs: RoomSummary?): CharSequence? {
+ private fun formatRoomTombstoneEvent(event: Event, senderName: String?, isDm: Boolean): CharSequence? {
return if (event.isSentByCurrentUser()) {
- sp.getString(if (rs.isDm()) R.string.notice_direct_room_update_by_you else R.string.notice_room_update_by_you)
+ sp.getString(if (isDm) R.string.notice_direct_room_update_by_you else R.string.notice_room_update_by_you)
} else {
- sp.getString(if (rs.isDm()) R.string.notice_direct_room_update else R.string.notice_room_update, senderName)
+ sp.getString(if (isDm) R.string.notice_direct_room_update else R.string.notice_room_update, senderName)
}
}
@@ -272,20 +266,20 @@ class NoticeEventFormatter @Inject constructor(
}
}
- private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?, rs: RoomSummary?): CharSequence? {
+ private fun formatRoomHistoryVisibilityEvent(event: Event, senderName: String?, isDm: Boolean): CharSequence? {
val historyVisibility = event.getClearContent().toModel()?.historyVisibility ?: return null
val historyVisibilitySuffix = roomHistoryVisibilityFormatter.getNoticeSuffix(historyVisibility)
return if (event.isSentByCurrentUser()) {
- sp.getString(if (rs.isDm()) R.string.notice_made_future_direct_room_visibility_by_you else R.string.notice_made_future_room_visibility_by_you,
+ sp.getString(if (isDm) R.string.notice_made_future_direct_room_visibility_by_you else R.string.notice_made_future_room_visibility_by_you,
historyVisibilitySuffix)
} else {
- sp.getString(if (rs.isDm()) R.string.notice_made_future_direct_room_visibility else R.string.notice_made_future_room_visibility,
+ sp.getString(if (isDm) R.string.notice_made_future_direct_room_visibility else R.string.notice_made_future_room_visibility,
senderName, historyVisibilitySuffix)
}
}
- private fun formatRoomThirdPartyInvite(event: Event, senderName: String?, rs: RoomSummary?): CharSequence? {
+ private fun formatRoomThirdPartyInvite(event: Event, senderName: String?, isDm: Boolean): CharSequence? {
val content = event.getClearContent().toModel()
val prevContent = event.resolvedPrevContent()?.toModel()
@@ -294,24 +288,24 @@ class NoticeEventFormatter @Inject constructor(
// Revoke case
if (event.isSentByCurrentUser()) {
sp.getString(
- if (rs.isDm()) {
+ if (isDm) {
R.string.notice_direct_room_third_party_revoked_invite_by_you
} else {
R.string.notice_room_third_party_revoked_invite_by_you
},
prevContent.displayName)
} else {
- sp.getString(if (rs.isDm()) R.string.notice_direct_room_third_party_revoked_invite else R.string.notice_room_third_party_revoked_invite,
+ sp.getString(if (isDm) R.string.notice_direct_room_third_party_revoked_invite else R.string.notice_room_third_party_revoked_invite,
senderName, prevContent.displayName)
}
}
content != null -> {
// Invitation case
if (event.isSentByCurrentUser()) {
- sp.getString(if (rs.isDm()) R.string.notice_direct_room_third_party_invite_by_you else R.string.notice_room_third_party_invite_by_you,
+ sp.getString(if (isDm) R.string.notice_direct_room_third_party_invite_by_you else R.string.notice_room_third_party_invite_by_you,
content.displayName)
} else {
- sp.getString(if (rs.isDm()) R.string.notice_direct_room_third_party_invite else R.string.notice_room_third_party_invite,
+ sp.getString(if (isDm) R.string.notice_direct_room_third_party_invite else R.string.notice_room_third_party_invite,
senderName, content.displayName)
}
}
@@ -358,7 +352,7 @@ class NoticeEventFormatter @Inject constructor(
}
EventType.CALL_REJECT ->
if (event.isSentByCurrentUser()) {
- sp.getString(R.string.call_tile_you_declined, "")
+ sp.getString(R.string.call_tile_you_declined_this_call)
} else {
sp.getString(R.string.call_tile_other_declined, senderName)
}
@@ -366,13 +360,13 @@ class NoticeEventFormatter @Inject constructor(
}
}
- private fun formatRoomMemberEvent(event: Event, senderName: String?, rs: RoomSummary?): String? {
+ private fun formatRoomMemberEvent(event: Event, senderName: String?, isDm: Boolean): String? {
val eventContent: RoomMemberContent? = event.getClearContent().toModel()
val prevEventContent: RoomMemberContent? = event.resolvedPrevContent().toModel()
val isMembershipEvent = prevEventContent?.membership != eventContent?.membership
|| eventContent?.membership == Membership.LEAVE
return if (isMembershipEvent) {
- buildMembershipNotice(event, senderName, eventContent, prevEventContent, rs)
+ buildMembershipNotice(event, senderName, eventContent, prevEventContent, isDm)
} else {
buildProfileNotice(event, senderName, eventContent, prevEventContent)
}
@@ -554,25 +548,25 @@ class NoticeEventFormatter @Inject constructor(
}
}
- private fun formatRoomGuestAccessEvent(event: Event, senderName: String?, rs: RoomSummary?): String? {
+ private fun formatRoomGuestAccessEvent(event: Event, senderName: String?, isDm: Boolean): String? {
val eventContent: RoomGuestAccessContent? = event.getClearContent().toModel()
return when (eventContent?.guestAccess) {
GuestAccess.CanJoin ->
if (event.isSentByCurrentUser()) {
sp.getString(
- if (rs.isDm()) R.string.notice_direct_room_guest_access_can_join_by_you else R.string.notice_room_guest_access_can_join_by_you
+ if (isDm) R.string.notice_direct_room_guest_access_can_join_by_you else R.string.notice_room_guest_access_can_join_by_you
)
} else {
- sp.getString(if (rs.isDm()) R.string.notice_direct_room_guest_access_can_join else R.string.notice_room_guest_access_can_join,
+ sp.getString(if (isDm) R.string.notice_direct_room_guest_access_can_join else R.string.notice_room_guest_access_can_join,
senderName)
}
GuestAccess.Forbidden ->
if (event.isSentByCurrentUser()) {
sp.getString(
- if (rs.isDm()) R.string.notice_direct_room_guest_access_forbidden_by_you else R.string.notice_room_guest_access_forbidden_by_you
+ if (isDm) R.string.notice_direct_room_guest_access_forbidden_by_you else R.string.notice_room_guest_access_forbidden_by_you
)
} else {
- sp.getString(if (rs.isDm()) R.string.notice_direct_room_guest_access_forbidden else R.string.notice_room_guest_access_forbidden,
+ sp.getString(if (isDm) R.string.notice_direct_room_guest_access_forbidden else R.string.notice_room_guest_access_forbidden,
senderName)
}
else -> null
@@ -656,7 +650,7 @@ class NoticeEventFormatter @Inject constructor(
senderName: String?,
eventContent: RoomMemberContent?,
prevEventContent: RoomMemberContent?,
- rs: RoomSummary?): String? {
+ isDm: Boolean): String? {
val senderDisplayName = senderName ?: event.senderId ?: ""
val targetDisplayName = eventContent?.displayName ?: prevEventContent?.displayName ?: event.stateKey ?: ""
return when (eventContent?.membership) {
@@ -706,17 +700,17 @@ class NoticeEventFormatter @Inject constructor(
Membership.JOIN ->
eventContent.safeReason?.let { reason ->
if (event.isSentByCurrentUser()) {
- sp.getString(if (rs.isDm()) R.string.notice_direct_room_join_with_reason_by_you else R.string.notice_room_join_with_reason_by_you,
+ sp.getString(if (isDm) R.string.notice_direct_room_join_with_reason_by_you else R.string.notice_room_join_with_reason_by_you,
reason)
} else {
- sp.getString(if (rs.isDm()) R.string.notice_direct_room_join_with_reason else R.string.notice_room_join_with_reason,
+ sp.getString(if (isDm) R.string.notice_direct_room_join_with_reason else R.string.notice_room_join_with_reason,
senderDisplayName, reason)
}
} ?: run {
if (event.isSentByCurrentUser()) {
- sp.getString(if (rs.isDm()) R.string.notice_direct_room_join_by_you else R.string.notice_room_join_by_you)
+ sp.getString(if (isDm) R.string.notice_direct_room_join_by_you else R.string.notice_room_join_by_you)
} else {
- sp.getString(if (rs.isDm()) R.string.notice_direct_room_join else R.string.notice_room_join,
+ sp.getString(if (isDm) R.string.notice_direct_room_join else R.string.notice_room_join,
senderDisplayName)
}
}
@@ -738,7 +732,7 @@ class NoticeEventFormatter @Inject constructor(
eventContent.safeReason?.let { reason ->
if (event.isSentByCurrentUser()) {
sp.getString(
- if (rs.isDm()) {
+ if (isDm) {
R.string.notice_direct_room_leave_with_reason_by_you
} else {
R.string.notice_room_leave_with_reason_by_you
@@ -746,14 +740,14 @@ class NoticeEventFormatter @Inject constructor(
reason
)
} else {
- sp.getString(if (rs.isDm()) R.string.notice_direct_room_leave_with_reason else R.string.notice_room_leave_with_reason,
+ sp.getString(if (isDm) R.string.notice_direct_room_leave_with_reason else R.string.notice_room_leave_with_reason,
senderDisplayName, reason)
}
} ?: run {
if (event.isSentByCurrentUser()) {
- sp.getString(if (rs.isDm()) R.string.notice_direct_room_leave_by_you else R.string.notice_room_leave_by_you)
+ sp.getString(if (isDm) R.string.notice_direct_room_leave_by_you else R.string.notice_room_leave_by_you)
} else {
- sp.getString(if (rs.isDm()) R.string.notice_direct_room_leave else R.string.notice_room_leave,
+ sp.getString(if (isDm) R.string.notice_direct_room_leave else R.string.notice_room_leave,
senderDisplayName)
}
}
@@ -818,14 +812,14 @@ class NoticeEventFormatter @Inject constructor(
}
}
- private fun formatJoinRulesEvent(event: Event, senderName: String?, rs: RoomSummary?): CharSequence? {
+ private fun formatJoinRulesEvent(event: Event, senderName: String?, isDm: Boolean): CharSequence? {
val content = event.getClearContent().toModel() ?: return null
return when (content.joinRules) {
RoomJoinRules.INVITE ->
if (event.isSentByCurrentUser()) {
- sp.getString(if (rs.isDm()) R.string.direct_room_join_rules_invite_by_you else R.string.room_join_rules_invite_by_you)
+ sp.getString(if (isDm) R.string.direct_room_join_rules_invite_by_you else R.string.room_join_rules_invite_by_you)
} else {
- sp.getString(if (rs.isDm()) R.string.direct_room_join_rules_invite else R.string.room_join_rules_invite,
+ sp.getString(if (isDm) R.string.direct_room_join_rules_invite else R.string.room_join_rules_invite,
senderName)
}
RoomJoinRules.PUBLIC ->
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
index 221149aced..da75a808d8 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/MessageInformationDataFactory.kt
@@ -34,6 +34,7 @@ import org.matrix.android.sdk.api.session.events.model.EventType
import org.matrix.android.sdk.api.session.events.model.isAttachmentMessage
import org.matrix.android.sdk.api.session.events.model.toModel
import org.matrix.android.sdk.api.session.room.model.ReferencesAggregatedContent
+import org.matrix.android.sdk.api.session.room.model.RoomSummary
import org.matrix.android.sdk.api.session.room.model.message.MessageVerificationRequestContent
import org.matrix.android.sdk.api.session.room.send.SendState
import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
@@ -48,7 +49,6 @@ import javax.inject.Inject
* This class compute if data of an event (such has avatar, display name, ...) should be displayed, depending on the previous event in the timeline
*/
class MessageInformationDataFactory @Inject constructor(private val session: Session,
- private val roomSummariesHolder: RoomSummariesHolder,
private val dateFormatter: VectorDateFormatter,
private val visibilityHelper: TimelineEventVisibilityHelper,
private val vectorPreferences: VectorPreferences) {
@@ -74,7 +74,8 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
|| nextDisplayableEvent.isEdition()
val time = dateFormatter.format(event.root.originServerTs, DateFormatKind.MESSAGE_SIMPLE)
- val e2eDecoration = getE2EDecoration(event)
+ val roomSummary = params.partialState.roomSummary
+ val e2eDecoration = getE2EDecoration(roomSummary, event)
// SendState Decoration
val isSentByMe = event.root.senderId == session.myUserId
@@ -140,8 +141,7 @@ class MessageInformationDataFactory @Inject constructor(private val session: Ses
}
}
- private fun getE2EDecoration(event: TimelineEvent): E2EDecoration {
- val roomSummary = roomSummariesHolder.get(event.roomId)
+ private fun getE2EDecoration(roomSummary: RoomSummary?, event: TimelineEvent): E2EDecoration {
return if (
event.root.sendState == SendState.SYNCED
&& roomSummary?.isEncrypted.orFalse()
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/RoomSummariesHolder.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/RoomSummariesHolder.kt
deleted file mode 100644
index ac953f91f7..0000000000
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/RoomSummariesHolder.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-/*
- * Copyright (c) 2020 New Vector Ltd
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package im.vector.app.features.home.room.detail.timeline.helper
-
-import org.matrix.android.sdk.api.session.room.model.RoomSummary
-import javax.inject.Inject
-import javax.inject.Singleton
-
-/*
- You can use this to share room summary instances within the app.
- You should probably use this only in the context of the timeline
- */
-@Singleton
-class RoomSummariesHolder @Inject constructor() {
-
- private var roomSummaries = HashMap()
-
- fun set(roomSummary: RoomSummary) {
- roomSummaries[roomSummary.roomId] = roomSummary
- }
-
- fun get(roomId: String) = roomSummaries[roomId]
-
- fun remove(roomId: String) = roomSummaries.remove(roomId)
-
- fun clear() {
- roomSummaries.clear()
- }
-}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt
index 3121f031e2..736da63ee2 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineControllerInterceptorHelper.kt
@@ -19,12 +19,8 @@ package im.vector.app.features.home.room.detail.timeline.helper
import com.airbnb.epoxy.EpoxyModel
import com.airbnb.epoxy.VisibilityState
import im.vector.app.core.epoxy.LoadingItem_
-import im.vector.app.core.epoxy.TimelineEmptyItem_
-import im.vector.app.core.resources.UserPreferencesProvider
-import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.room.detail.UnreadState
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
-import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem
import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem
import im.vector.app.features.home.room.detail.timeline.item.ItemWithEvents
import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_
@@ -34,9 +30,7 @@ import kotlin.reflect.KMutableProperty0
private const val DEFAULT_PREFETCH_THRESHOLD = 30
class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMutableProperty0,
- private val adapterPositionMapping: MutableMap,
- private val userPreferencesProvider: UserPreferencesProvider,
- private val callManager: WebRtcCallManager
+ private val adapterPositionMapping: MutableMap
) {
private var previousModelsSize = 0
@@ -50,14 +44,12 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut
) {
positionOfReadMarker.set(null)
adapterPositionMapping.clear()
- val callIds = mutableSetOf()
// Add some prefetch loader if needed
models.addBackwardPrefetchIfNeeded(timeline, callback)
models.addForwardPrefetchIfNeeded(timeline, callback)
val modelsIterator = models.listIterator()
- val showHiddenEvents = userPreferencesProvider.shouldShowHiddenEvents()
var index = 0
val firstUnreadEventId = (unreadState as? UnreadState.HasUnread)?.firstUnreadEventId
var atLeastOneVisibleItemSinceLastDaySeparator = false
@@ -83,11 +75,6 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut
return@forEach
}
atLeastOneVisibleItemSinceLastDaySeparator = false
- } else if (epoxyModel is CallTileTimelineItem) {
- val hasBeenRemoved = modelsIterator.removeCallItemIfNeeded(epoxyModel, callIds, showHiddenEvents)
- if (!hasBeenRemoved) {
- atLeastOneVisibleItemSinceLastDaySeparator = true
- }
}
if (appendReadMarker) {
modelsIterator.addReadMarkerItem(callback)
@@ -109,29 +96,6 @@ class TimelineControllerInterceptorHelper(private val positionOfReadMarker: KMut
add(readMarker)
}
- private fun MutableListIterator>.removeCallItemIfNeeded(
- epoxyModel: CallTileTimelineItem,
- callIds: MutableSet,
- showHiddenEvents: Boolean
- ): Boolean {
- val callId = epoxyModel.attributes.callId
- // We should remove the call tile if we already have one for this call or
- // if this is an active call tile without an actual call (which can happen with permalink)
- val shouldRemoveCallItem = callIds.contains(callId)
- || (!callManager.getAdvertisedCalls().contains(callId) && epoxyModel.attributes.callStatus.isActive())
- val removed = shouldRemoveCallItem && !showHiddenEvents
- if (removed) {
- remove()
- val emptyItem = TimelineEmptyItem_()
- .id(epoxyModel.id())
- .eventId(epoxyModel.attributes.informationData.eventId)
- .notBlank(false)
- add(emptyItem)
- }
- callIds.add(callId)
- return removed
- }
-
private fun MutableList>.addBackwardPrefetchIfNeeded(timeline: Timeline?, callback: TimelineEventController.Callback?) {
val shouldAddBackwardPrefetch = timeline?.hasMoreToLoad(Timeline.Direction.BACKWARDS) ?: false
if (shouldAddBackwardPrefetch) {
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt
new file mode 100644
index 0000000000..3910204293
--- /dev/null
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/helper/TimelineEventsGroups.kt
@@ -0,0 +1,133 @@
+/*
+ * Copyright (c) 2021 New Vector Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package im.vector.app.features.home.room.detail.timeline.helper
+
+import im.vector.app.core.utils.TextUtils
+import org.matrix.android.sdk.api.extensions.orFalse
+import org.matrix.android.sdk.api.session.events.model.EventType
+import org.matrix.android.sdk.api.session.events.model.toModel
+import org.matrix.android.sdk.api.session.room.model.call.CallInviteContent
+import org.matrix.android.sdk.api.session.room.timeline.TimelineEvent
+import org.matrix.android.sdk.api.session.widgets.model.WidgetContent
+import org.threeten.bp.Duration
+
+class TimelineEventsGroup(val groupId: String) {
+
+ val events: Set
+ get() = _events
+
+ private val _events = HashSet()
+
+ fun add(timelineEvent: TimelineEvent) {
+ _events.add(timelineEvent)
+ }
+}
+
+class TimelineEventsGroups {
+
+ private val groups = HashMap()
+
+ fun addOrIgnore(event: TimelineEvent) {
+ val groupId = event.getGroupIdOrNull() ?: return
+ groups.getOrPut(groupId) { TimelineEventsGroup(groupId) }.add(event)
+ }
+
+ fun getOrNull(event: TimelineEvent): TimelineEventsGroup? {
+ val groupId = event.getGroupIdOrNull() ?: return null
+ return groups[groupId]
+ }
+
+ private fun TimelineEvent.getGroupIdOrNull(): String? {
+ val type = root.getClearType()
+ val content = root.getClearContent()
+ return if (EventType.isCallEvent(type)) {
+ (content?.get("call_id") as? String)
+ } else if (type == EventType.STATE_ROOM_WIDGET || type == EventType.STATE_ROOM_WIDGET_LEGACY) {
+ root.stateKey
+ } else {
+ null
+ }
+ }
+
+ fun clear() {
+ groups.clear()
+ }
+}
+
+class JitsiWidgetEventsGroup(private val group: TimelineEventsGroup) {
+
+ val callId: String = group.groupId
+
+ fun isStillActive(): Boolean {
+ return group.events.none {
+ it.root.getClearContent().toModel()?.isActive() == false
+ }
+ }
+}
+
+class CallSignalingEventsGroup(private val group: TimelineEventsGroup) {
+
+ val callId: String = group.groupId
+
+ fun isVideo(): Boolean {
+ val invite = getInvite() ?: return false
+ return invite.root.getClearContent().toModel()?.isVideo().orFalse()
+ }
+
+ fun isRinging(): Boolean {
+ return getAnswer() == null && getHangup() == null && getReject() == null
+ }
+
+ fun isInCall(): Boolean {
+ return getHangup() == null && getReject() == null
+ }
+
+ fun formattedDuration(): String {
+ val start = getAnswer()?.root?.originServerTs
+ val end = getHangup()?.root?.originServerTs
+ return if (start == null || end == null) {
+ ""
+ } else {
+ val durationInMillis = (end - start).coerceAtLeast(0L)
+ val duration = Duration.ofMillis(durationInMillis)
+ TextUtils.formatDuration(duration)
+ }
+ }
+
+ /**
+ * Returns true if there are only events from one side.
+ */
+ fun callWasMissed(): Boolean {
+ return group.events.distinctBy { it.senderInfo.userId }.size == 1
+ }
+
+ private fun getAnswer(): TimelineEvent? {
+ return group.events.firstOrNull { it.root.getClearType() == EventType.CALL_ANSWER }
+ }
+
+ private fun getInvite(): TimelineEvent? {
+ return group.events.firstOrNull { it.root.getClearType() == EventType.CALL_INVITE }
+ }
+
+ private fun getHangup(): TimelineEvent? {
+ return group.events.firstOrNull { it.root.getClearType() == EventType.CALL_HANGUP }
+ }
+
+ private fun getReject(): TimelineEvent? {
+ return group.events.firstOrNull { it.root.getClearType() == EventType.CALL_REJECT }
+ }
+}
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt
index fd5eea1b49..b53495fdaf 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/AbsMessageItem.kt
@@ -42,6 +42,10 @@ abstract class AbsMessageItem : AbsBaseMessageItem
override val baseAttributes: AbsBaseMessageItem.Attributes
get() = attributes
+ override fun isCacheable(): Boolean {
+ return attributes.informationData.sendStateDecoration != SendStateDecoration.SENT
+ }
+
@EpoxyAttribute
lateinit var attributes: Attributes
diff --git a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt
index 1f12bdbd2c..46392a494f 100644
--- a/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt
+++ b/vector/src/main/java/im/vector/app/features/home/room/detail/timeline/item/CallTileTimelineItem.kt
@@ -15,6 +15,7 @@
*/
package im.vector.app.features.home.room.detail.timeline.item
+import android.content.res.Resources
import android.view.View
import android.view.ViewGroup
import android.widget.Button
@@ -31,13 +32,11 @@ import im.vector.app.R
import im.vector.app.core.epoxy.ClickListener
import im.vector.app.core.epoxy.onClick
import im.vector.app.core.extensions.setLeftDrawable
-import im.vector.app.core.extensions.setTextWithColoredPart
import im.vector.app.features.home.AvatarRenderer
import im.vector.app.features.home.room.detail.RoomDetailAction
import im.vector.app.features.home.room.detail.timeline.MessageColorProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import org.matrix.android.sdk.api.util.MatrixItem
-import timber.log.Timber
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_state)
abstract class CallTileTimelineItem : AbsBaseMessageItem() {
@@ -45,6 +44,8 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem renderInvitedStatus(holder)
+ CallStatus.IN_CALL -> renderInCallStatus(holder)
+ CallStatus.REJECTED -> renderRejectedStatus(holder)
+ CallStatus.ENDED -> renderEndedStatus(holder)
+ CallStatus.MISSED -> renderMissedStatus(holder)
}
- if (attributes.callStatus == CallStatus.INVITED && !attributes.informationData.sentByMe && attributes.isStillActive) {
- holder.acceptRejectViewGroup.isVisible = true
- holder.acceptView.onClick {
- attributes.callback?.onTimelineItemAction(RoomDetailAction.AcceptCall(callId = attributes.callId))
+ renderSendState(holder.view, null, holder.failedToSendIndicator)
+ }
+
+ private fun renderMissedStatus(holder: Holder) {
+ // Sent by me means I made the call and opponent missed it.
+ if (attributes.informationData.sentByMe) {
+ if (attributes.callKind.isVoiceCall) {
+ holder.statusView.setStatus(R.string.call_tile_no_answer, R.drawable.ic_voice_call_declined)
+ } else {
+ holder.statusView.setStatus(R.string.call_tile_no_answer, R.drawable.ic_video_call_declined)
}
- holder.rejectView.setLeftDrawable(R.drawable.ic_call_hangup, R.attr.colorOnPrimary)
- holder.rejectView.onClick {
- attributes.callback?.onTimelineItemAction(RoomDetailAction.EndCall)
+ } else {
+ if (attributes.callKind.isVoiceCall) {
+ holder.statusView.setStatus(R.string.call_tile_voice_missed, R.drawable.ic_missed_voice_call_small)
+ } else {
+ holder.statusView.setStatus(R.string.call_tile_video_missed, R.drawable.ic_missed_video_call_small)
}
- holder.statusView.isVisible = false
- when (attributes.callKind) {
- CallKind.CONFERENCE -> {
- holder.rejectView.setText(R.string.ignore)
- holder.acceptView.setText(R.string.join)
- holder.acceptView.setLeftDrawable(R.drawable.ic_call_audio_small, R.attr.colorOnPrimary)
+ }
+ holder.acceptRejectViewGroup.isVisible = true
+ holder.acceptView.setText(R.string.call_tile_call_back)
+ holder.acceptView.setLeftDrawable(attributes.callKind.icon, R.attr.colorOnPrimary)
+ holder.acceptView.onClick {
+ val callbackAction = RoomDetailAction.StartCall(attributes.callKind == CallKind.VIDEO)
+ attributes.callback?.onTimelineItemAction(callbackAction)
+ }
+ holder.rejectView.isVisible = false
+ }
+
+ private fun renderEndedStatus(holder: Holder) {
+ holder.acceptRejectViewGroup.isVisible = false
+ when (attributes.callKind) {
+ CallKind.VIDEO -> {
+ val endCallStatus = holder.resources.getString(R.string.call_tile_video_call_has_ended, attributes.formattedDuration)
+ holder.statusView.setStatus(endCallStatus)
+ }
+ CallKind.AUDIO -> {
+ val endCallStatus = holder.resources.getString(R.string.call_tile_voice_call_has_ended, attributes.formattedDuration)
+ holder.statusView.setStatus(endCallStatus)
+ }
+ CallKind.CONFERENCE -> {
+ holder.statusView.setStatus(R.string.call_tile_ended)
+ }
+ }
+ }
+
+ private fun renderRejectedStatus(holder: Holder) {
+ holder.acceptRejectViewGroup.isVisible = true
+ holder.acceptView.setText(R.string.call_tile_call_back)
+ holder.acceptView.setLeftDrawable(attributes.callKind.icon, R.attr.colorOnPrimary)
+ holder.acceptView.onClick {
+ val callbackAction = RoomDetailAction.StartCall(attributes.callKind == CallKind.VIDEO)
+ attributes.callback?.onTimelineItemAction(callbackAction)
+ }
+ holder.rejectView.isVisible = false
+ // Sent by me means I rejected the call made by opponent.
+ if (attributes.informationData.sentByMe) {
+ if (attributes.callKind.isVoiceCall) {
+ holder.statusView.setStatus(R.string.call_tile_voice_declined, R.drawable.ic_voice_call_declined)
+ } else {
+ holder.statusView.setStatus(R.string.call_tile_video_declined, R.drawable.ic_video_call_declined)
+ }
+ } else {
+ if (attributes.callKind.isVoiceCall) {
+ holder.statusView.setStatus(R.string.call_tile_no_answer, R.drawable.ic_voice_call_declined)
+ } else {
+ holder.statusView.setStatus(R.string.call_tile_no_answer, R.drawable.ic_video_call_declined)
+ }
+ }
+ }
+
+ private fun renderInCallStatus(holder: Holder) {
+ holder.acceptRejectViewGroup.isVisible = true
+ holder.acceptView.isVisible = false
+ when {
+ attributes.callKind == CallKind.CONFERENCE -> {
+ holder.rejectView.isVisible = true
+ holder.rejectView.setText(R.string.leave)
+ holder.rejectView.setLeftDrawable(R.drawable.ic_call_hangup, R.attr.colorOnPrimary)
+ holder.rejectView.onClick {
+ attributes.callback?.onTimelineItemAction(RoomDetailAction.LeaveJitsiCall)
}
- CallKind.AUDIO -> {
+ }
+ attributes.isStillActive -> {
+ holder.rejectView.isVisible = true
+ holder.rejectView.setText(R.string.call_notification_hangup)
+ holder.rejectView.setLeftDrawable(R.drawable.ic_call_hangup, R.attr.colorOnPrimary)
+ holder.rejectView.onClick {
+ attributes.callback?.onTimelineItemAction(RoomDetailAction.EndCall)
+ }
+ }
+ else -> {
+ holder.acceptRejectViewGroup.isVisible = false
+ }
+ }
+ if (attributes.callKind.isVoiceCall) {
+ holder.statusView.setStatus(R.string.call_tile_voice_active)
+ } else {
+ holder.statusView.setStatus(R.string.call_tile_video_active)
+ }
+ }
+
+ private fun renderInvitedStatus(holder: Holder) {
+ when {
+ attributes.callKind == CallKind.CONFERENCE -> {
+ holder.acceptRejectViewGroup.isVisible = true
+ holder.acceptView.onClick {
+ attributes.callback?.onTimelineItemAction(RoomDetailAction.JoinJitsiCall)
+ }
+ holder.acceptView.isVisible = true
+ holder.rejectView.isVisible = false
+ holder.acceptView.setText(R.string.join)
+ holder.acceptView.setLeftDrawable(R.drawable.ic_call_video_small, R.attr.colorOnPrimary)
+ }
+ !attributes.informationData.sentByMe && attributes.isStillActive -> {
+ holder.acceptRejectViewGroup.isVisible = true
+ holder.acceptView.isVisible = true
+ holder.rejectView.isVisible = true
+ holder.acceptView.onClick {
+ attributes.callback?.onTimelineItemAction(RoomDetailAction.AcceptCall(callId = attributes.callId))
+ }
+ holder.rejectView.setLeftDrawable(R.drawable.ic_call_hangup, R.attr.colorOnPrimary)
+ holder.rejectView.onClick {
+ attributes.callback?.onTimelineItemAction(RoomDetailAction.EndCall)
+ }
+ if (attributes.callKind == CallKind.AUDIO) {
holder.rejectView.setText(R.string.call_notification_reject)
holder.acceptView.setText(R.string.call_notification_answer)
holder.acceptView.setLeftDrawable(R.drawable.ic_call_audio_small, R.attr.colorOnPrimary)
- }
- CallKind.VIDEO -> {
+ } else if (attributes.callKind == CallKind.VIDEO) {
holder.rejectView.setText(R.string.call_notification_reject)
holder.acceptView.setText(R.string.call_notification_answer)
holder.acceptView.setLeftDrawable(R.drawable.ic_call_video_small, R.attr.colorOnPrimary)
}
- else -> {
- Timber.w("Shouldn't be in that state")
- }
}
- } else {
- holder.acceptRejectViewGroup.isVisible = false
- holder.statusView.isVisible = true
+ else -> {
+ holder.acceptRejectViewGroup.isVisible = false
+ }
+ }
+ when {
+ // Invite state for conference should show as InCallStatus
+ attributes.callKind == CallKind.CONFERENCE -> {
+ holder.statusView.setStatus(R.string.call_tile_video_active)
+ }
+ attributes.informationData.sentByMe -> {
+ holder.statusView.setStatus(R.string.call_ringing)
+ }
+ attributes.callKind.isVoiceCall -> {
+ holder.statusView.setStatus(R.string.call_tile_voice_incoming)
+ }
+ else -> {
+ holder.statusView.setStatus(R.string.call_tile_video_incoming)
+ }
}
- holder.statusView.setCallStatus(attributes)
- renderSendState(holder.view, null, holder.failedToSendIndicator)
}
- private fun TextView.setCallStatus(attributes: Attributes) {
- when (attributes.callStatus) {
- CallStatus.INVITED -> if (attributes.informationData.sentByMe) {
- setText(R.string.call_tile_you_started_call)
- } else {
- text = context.getString(R.string.call_tile_other_started_call, attributes.userOfInterest.getBestName())
- }
- CallStatus.IN_CALL -> setText(R.string.call_tile_in_call)
- CallStatus.REJECTED -> if (attributes.informationData.sentByMe) {
- setTextWithColoredPart(R.string.call_tile_you_declined, R.string.call_tile_call_back) {
- val callbackAction = RoomDetailAction.StartCall(attributes.callKind == CallKind.VIDEO)
- attributes.callback?.onTimelineItemAction(callbackAction)
- }
- } else {
- text = context.getString(R.string.call_tile_other_declined, attributes.userOfInterest.getBestName())
- }
- CallStatus.ENDED -> setText(R.string.call_tile_ended)
- }
+ private fun TextView.setStatus(@StringRes statusRes: Int, @DrawableRes drawableRes: Int? = null) {
+ val status = resources.getString(statusRes)
+ setStatus(status, drawableRes)
+ }
+
+ private fun TextView.setStatus(status: String, @DrawableRes drawableRes: Int? = null) {
+ setLeftDrawable(drawableRes ?: attributes.callKind.icon)
+ text = status
}
class Holder : AbsBaseMessageItem.Holder(STUB_ID) {
val acceptView by bind