VoIP: continue working on call tiles in timeline (add actions)

This commit is contained in:
ganfra 2020-12-04 16:27:41 +01:00
parent 24de6c0101
commit 139c3fdd19
11 changed files with 65 additions and 22 deletions

View File

@ -79,7 +79,7 @@ fun TextView.setLeftDrawable(@DrawableRes iconRes: Int, @ColorRes tintColor: Int
val icon = if(tintColor != null){ val icon = if(tintColor != null){
val tint = ContextCompat.getColor(context, tintColor) val tint = ContextCompat.getColor(context, tintColor)
ContextCompat.getDrawable(context, iconRes)?.also { ContextCompat.getDrawable(context, iconRes)?.also {
DrawableCompat.setTint(it, tint) DrawableCompat.setTint(it.mutate(), tint)
} }
}else { }else {
ContextCompat.getDrawable(context, iconRes) ContextCompat.getDrawable(context, iconRes)

View File

@ -339,12 +339,12 @@ class VectorCallActivity : VectorBaseActivity(), CallControlsView.InteractionLis
const val INCOMING_RINGING = "INCOMING_RINGING" const val INCOMING_RINGING = "INCOMING_RINGING"
const val INCOMING_ACCEPT = "INCOMING_ACCEPT" const val INCOMING_ACCEPT = "INCOMING_ACCEPT"
fun newIntent(context: Context, mxCall: MxCallDetail): Intent { fun newIntent(context: Context, mxCall: MxCallDetail, mode: String?): Intent {
return Intent(context, VectorCallActivity::class.java).apply { return Intent(context, VectorCallActivity::class.java).apply {
// what could be the best flags? // what could be the best flags?
flags = Intent.FLAG_ACTIVITY_NEW_TASK flags = Intent.FLAG_ACTIVITY_NEW_TASK
putExtra(MvRx.KEY_ARG, CallArgs(mxCall.roomId, mxCall.callId, mxCall.opponentUserId, !mxCall.isOutgoing, mxCall.isVideoCall)) putExtra(MvRx.KEY_ARG, CallArgs(mxCall.roomId, mxCall.callId, mxCall.opponentUserId, !mxCall.isOutgoing, mxCall.isVideoCall))
putExtra(EXTRA_MODE, OUTGOING_CREATED) putExtra(EXTRA_MODE, mode)
} }
} }

View File

@ -203,7 +203,7 @@ class WebRtcCallManager @Inject constructor(
callId = mxCall.callId) callId = mxCall.callId)
// start the activity now // start the activity now
context.startActivity(VectorCallActivity.newIntent(context, mxCall)) context.startActivity(VectorCallActivity.newIntent(context, mxCall, VectorCallActivity.OUTGOING_CREATED))
} }
override fun onCallIceCandidateReceived(mxCall: MxCall, iceCandidatesContent: CallCandidatesContent) { override fun onCallIceCandidateReceived(mxCall: MxCall, iceCandidatesContent: CallCandidatesContent) {

View File

@ -19,6 +19,8 @@ package im.vector.app.features.home.room.detail
import android.net.Uri import android.net.Uri
import android.view.View import android.view.View
import im.vector.app.core.platform.VectorViewModelAction import im.vector.app.core.platform.VectorViewModelAction
import im.vector.app.features.call.webrtc.WebRtcCall
import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineItem
import org.matrix.android.sdk.api.session.content.ContentAttachmentData import org.matrix.android.sdk.api.session.content.ContentAttachmentData
import org.matrix.android.sdk.api.session.events.model.Event import org.matrix.android.sdk.api.session.events.model.Event
import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent import org.matrix.android.sdk.api.session.room.model.message.MessageStickerContent
@ -73,6 +75,7 @@ sealed class RoomDetailAction : VectorViewModelAction {
object ResendAll : RoomDetailAction() object ResendAll : RoomDetailAction()
data class StartCall(val isVideo: Boolean) : RoomDetailAction() data class StartCall(val isVideo: Boolean) : RoomDetailAction()
data class AcceptCall(val callId: String): RoomDetailAction()
object EndCall : RoomDetailAction() object EndCall : RoomDetailAction()
data class AcceptVerificationRequest(val transactionId: String, val otherUserId: String) : RoomDetailAction() data class AcceptVerificationRequest(val transactionId: String, val otherUserId: String) : RoomDetailAction()

View File

@ -368,6 +368,7 @@ class RoomDetailFragment @Inject constructor(
is RoomDetailViewEvents.DisplayEnableIntegrationsWarning -> displayDisabledIntegrationDialog() is RoomDetailViewEvents.DisplayEnableIntegrationsWarning -> displayDisabledIntegrationDialog()
is RoomDetailViewEvents.OpenIntegrationManager -> openIntegrationManager() is RoomDetailViewEvents.OpenIntegrationManager -> openIntegrationManager()
is RoomDetailViewEvents.OpenFile -> startOpenFileIntent(it) is RoomDetailViewEvents.OpenFile -> startOpenFileIntent(it)
is RoomDetailViewEvents.DisplayAndAcceptCall -> acceptIncomingCall(it)
RoomDetailViewEvents.OpenActiveWidgetBottomSheet -> onViewWidgetsClicked() RoomDetailViewEvents.OpenActiveWidgetBottomSheet -> onViewWidgetsClicked()
is RoomDetailViewEvents.ShowInfoOkDialog -> showDialogWithMessage(it.message) is RoomDetailViewEvents.ShowInfoOkDialog -> showDialogWithMessage(it.message)
is RoomDetailViewEvents.JoinJitsiConference -> joinJitsiRoom(it.widget, it.withVideo) is RoomDetailViewEvents.JoinJitsiConference -> joinJitsiRoom(it.widget, it.withVideo)
@ -381,6 +382,7 @@ class RoomDetailFragment @Inject constructor(
is RoomDetailViewEvents.ShowRoomAvatarFullScreen -> it.matrixItem?.let { item -> is RoomDetailViewEvents.ShowRoomAvatarFullScreen -> it.matrixItem?.let { item ->
navigator.openBigImageViewer(requireActivity(), it.view, item) navigator.openBigImageViewer(requireActivity(), it.view, item)
} }
}.exhaustive }.exhaustive
} }
@ -389,6 +391,15 @@ class RoomDetailFragment @Inject constructor(
} }
} }
private fun acceptIncomingCall(event: RoomDetailViewEvents.DisplayAndAcceptCall) {
val intent = VectorCallActivity.newIntent(
context = vectorBaseActivity,
mxCall = event.call.mxCall,
mode = VectorCallActivity.INCOMING_ACCEPT
)
startActivity(intent)
}
override fun onImageReady(uri: Uri?) { override fun onImageReady(uri: Uri?) {
uri ?: return uri ?: return
roomDetailViewModel.handle( roomDetailViewModel.handle(
@ -836,6 +847,8 @@ class RoomDetailFragment @Inject constructor(
} }
} }
private fun safeStartCall2(isVideoCall: Boolean) { private fun safeStartCall2(isVideoCall: Boolean) {
val startCallAction = RoomDetailAction.StartCall(isVideoCall) val startCallAction = RoomDetailAction.StartCall(isVideoCall)
roomDetailViewModel.pendingAction = startCallAction roomDetailViewModel.pendingAction = startCallAction

View File

@ -20,6 +20,7 @@ import android.net.Uri
import android.view.View import android.view.View
import androidx.annotation.StringRes import androidx.annotation.StringRes
import im.vector.app.core.platform.VectorViewEvents import im.vector.app.core.platform.VectorViewEvents
import im.vector.app.features.call.webrtc.WebRtcCall
import im.vector.app.features.command.Command import im.vector.app.features.command.Command
import org.matrix.android.sdk.api.session.widgets.model.Widget import org.matrix.android.sdk.api.session.widgets.model.Widget
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
@ -73,6 +74,8 @@ sealed class RoomDetailViewEvents : VectorViewEvents {
abstract class SendMessageResult : RoomDetailViewEvents() abstract class SendMessageResult : RoomDetailViewEvents()
data class DisplayAndAcceptCall(val call: WebRtcCall): RoomDetailViewEvents()
object DisplayPromptForIntegrationManager : RoomDetailViewEvents() object DisplayPromptForIntegrationManager : RoomDetailViewEvents()
object DisplayEnableIntegrationsWarning : RoomDetailViewEvents() object DisplayEnableIntegrationsWarning : RoomDetailViewEvents()

View File

@ -269,6 +269,7 @@ class RoomDetailViewModel @AssistedInject constructor(
is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment() is RoomDetailAction.SelectStickerAttachment -> handleSelectStickerAttachment()
is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager() is RoomDetailAction.OpenIntegrationManager -> handleOpenIntegrationManager()
is RoomDetailAction.StartCall -> handleStartCall(action) is RoomDetailAction.StartCall -> handleStartCall(action)
is RoomDetailAction.AcceptCall -> handleAcceptCall(action)
is RoomDetailAction.EndCall -> handleEndCall() is RoomDetailAction.EndCall -> handleEndCall()
is RoomDetailAction.ManageIntegrations -> handleManageIntegrations() is RoomDetailAction.ManageIntegrations -> handleManageIntegrations()
is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action) is RoomDetailAction.AddJitsiWidget -> handleAddJitsiConference(action)
@ -289,6 +290,12 @@ class RoomDetailViewModel @AssistedInject constructor(
}.exhaustive }.exhaustive
} }
private fun handleAcceptCall(action: RoomDetailAction.AcceptCall) {
callManager.getCallById(action.callId)?.also {
_viewEvents.post(RoomDetailViewEvents.DisplayAndAcceptCall(it))
}
}
private fun handleSetNewAvatar(action: RoomDetailAction.SetAvatarAction) { private fun handleSetNewAvatar(action: RoomDetailAction.SetAvatarAction) {
viewModelScope.launch(Dispatchers.IO) { viewModelScope.launch(Dispatchers.IO) {
try { try {

View File

@ -30,10 +30,12 @@ import im.vector.app.core.date.VectorDateFormatter
import im.vector.app.core.epoxy.LoadingItem_ import im.vector.app.core.epoxy.LoadingItem_
import im.vector.app.core.extensions.localDateTime import im.vector.app.core.extensions.localDateTime
import im.vector.app.core.extensions.nextOrNull import im.vector.app.core.extensions.nextOrNull
import im.vector.app.features.call.webrtc.WebRtcCallManager
import im.vector.app.features.home.room.detail.RoomDetailAction 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.RoomDetailViewState
import im.vector.app.features.home.room.detail.UnreadState import im.vector.app.features.home.room.detail.UnreadState
import im.vector.app.features.home.room.detail.timeline.factory.MergedHeaderItemFactory import im.vector.app.features.home.room.detail.timeline.factory.MergedHeaderItemFactory
import im.vector.app.features.home.room.detail.timeline.factory.NoticeItemFactory
import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactory import im.vector.app.features.home.room.detail.timeline.factory.TimelineItemFactory
import im.vector.app.features.home.room.detail.timeline.helper.ContentDownloadStateTrackerBinder 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.ContentUploadStateTrackerBinder
@ -47,6 +49,7 @@ import im.vector.app.features.home.room.detail.timeline.item.CallTileTimelineIte
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.DaySeparatorItem_ import im.vector.app.features.home.room.detail.timeline.item.DaySeparatorItem_
import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData import im.vector.app.features.home.room.detail.timeline.item.MessageInformationData
import im.vector.app.features.home.room.detail.timeline.item.NoticeItemBuilder
import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData import im.vector.app.features.home.room.detail.timeline.item.ReadReceiptData
import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_ import im.vector.app.features.home.room.detail.timeline.item.TimelineReadMarkerItem_
import im.vector.app.features.media.ImageContentRenderer import im.vector.app.features.media.ImageContentRenderer
@ -73,6 +76,8 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
private val timelineMediaSizeProvider: TimelineMediaSizeProvider, private val timelineMediaSizeProvider: TimelineMediaSizeProvider,
private val mergedHeaderItemFactory: MergedHeaderItemFactory, private val mergedHeaderItemFactory: MergedHeaderItemFactory,
private val session: Session, private val session: Session,
private val callManager: WebRtcCallManager,
private val noticeItemFactory: NoticeItemFactory,
@TimelineEventControllerHandler @TimelineEventControllerHandler
private val backgroundHandler: Handler private val backgroundHandler: Handler
) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor { ) : EpoxyController(backgroundHandler, backgroundHandler), Timeline.Listener, EpoxyController.Interceptor {
@ -187,12 +192,17 @@ class TimelineEventController @Inject constructor(private val dateFormatter: Vec
adapterPositionMapping.clear() adapterPositionMapping.clear()
val callIds = mutableSetOf<String>() val callIds = mutableSetOf<String>()
val modelsIterator = models.listIterator() val modelsIterator = models.listIterator()
val showHiddenEvents = vectorPreferences.shouldShowHiddenEvents()
modelsIterator.withIndex().forEach { modelsIterator.withIndex().forEach {
val index = it.index val index = it.index
val epoxyModel = it.value val epoxyModel = it.value
if (epoxyModel is CallTileTimelineItem) { if (epoxyModel is CallTileTimelineItem) {
val callId = epoxyModel.attributes.callId val callId = epoxyModel.attributes.callId
if (callIds.contains(callId)) { val call = callManager.getCallById(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) || (call == null && epoxyModel.attributes.callStatus.isActive())
if (shouldRemoveCallItem && !showHiddenEvents) {
modelsIterator.remove() modelsIterator.remove()
return@forEach return@forEach
} }

View File

@ -63,25 +63,25 @@ class CallItemFactory @Inject constructor(
} }
return when (event.root.getClearType()) { return when (event.root.getClearType()) {
EventType.CALL_ANSWER -> { EventType.CALL_ANSWER -> {
if (call == null) return null
createCallTileTimelineItem( createCallTileTimelineItem(
callId = callId, callId = callId,
callStatus = CallTileTimelineItem.CallStatus.IN_CALL, callStatus = CallTileTimelineItem.CallStatus.IN_CALL,
callKind = callKind, callKind = callKind,
callback = callback, callback = callback,
highlight = highlight, highlight = highlight,
informationData = informationData informationData = informationData,
isStillActive = call != null
) )
} }
EventType.CALL_INVITE -> { EventType.CALL_INVITE -> {
if (call == null) return null
createCallTileTimelineItem( createCallTileTimelineItem(
callId = callId, callId = callId,
callStatus = CallTileTimelineItem.CallStatus.INVITED, callStatus = CallTileTimelineItem.CallStatus.INVITED,
callKind = callKind, callKind = callKind,
callback = callback, callback = callback,
highlight = highlight, highlight = highlight,
informationData = informationData informationData = informationData,
isStillActive = call != null
) )
} }
EventType.CALL_REJECT -> { EventType.CALL_REJECT -> {
@ -91,7 +91,8 @@ class CallItemFactory @Inject constructor(
callKind = callKind, callKind = callKind,
callback = callback, callback = callback,
highlight = highlight, highlight = highlight,
informationData = informationData informationData = informationData,
isStillActive = false
) )
} }
EventType.CALL_HANGUP -> { EventType.CALL_HANGUP -> {
@ -101,7 +102,8 @@ class CallItemFactory @Inject constructor(
callKind = callKind, callKind = callKind,
callback = callback, callback = callback,
highlight = highlight, highlight = highlight,
informationData = informationData informationData = informationData,
isStillActive = false
) )
} }
else -> null else -> null
@ -124,6 +126,7 @@ class CallItemFactory @Inject constructor(
callStatus: CallTileTimelineItem.CallStatus, callStatus: CallTileTimelineItem.CallStatus,
informationData: MessageInformationData, informationData: MessageInformationData,
highlight: Boolean, highlight: Boolean,
isStillActive: Boolean,
callback: TimelineEventController.Callback? callback: TimelineEventController.Callback?
): CallTileTimelineItem? { ): CallTileTimelineItem? {
@ -140,7 +143,9 @@ class CallItemFactory @Inject constructor(
itemLongClickListener = it.itemLongClickListener, itemLongClickListener = it.itemLongClickListener,
reactionPillCallback = it.reactionPillCallback, reactionPillCallback = it.reactionPillCallback,
readReceiptsCallback = it.readReceiptsCallback, readReceiptsCallback = it.readReceiptsCallback,
userOfInterest = userOfInterest userOfInterest = userOfInterest,
callback = callback,
isStillActive = isStillActive
) )
} }
return CallTileTimelineItem_() return CallTileTimelineItem_()

View File

@ -15,31 +15,26 @@
*/ */
package im.vector.app.features.home.room.detail.timeline.item package im.vector.app.features.home.room.detail.timeline.item
import android.content.Context
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.Button import android.widget.Button
import android.widget.ImageView import android.widget.ImageView
import android.widget.RelativeLayout import android.widget.RelativeLayout
import android.widget.TextView import android.widget.TextView
import androidx.annotation.ColorRes
import androidx.annotation.DrawableRes import androidx.annotation.DrawableRes
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.core.content.ContextCompat
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import com.airbnb.epoxy.EpoxyAttribute import com.airbnb.epoxy.EpoxyAttribute
import com.airbnb.epoxy.EpoxyModelClass import com.airbnb.epoxy.EpoxyModelClass
import im.vector.app.R import im.vector.app.R
import im.vector.app.core.extensions.setLeftDrawable import im.vector.app.core.extensions.setLeftDrawable
import im.vector.app.core.extensions.setTextOrHide
import im.vector.app.core.extensions.setTextWithColoredPart import im.vector.app.core.extensions.setTextWithColoredPart
import im.vector.app.features.home.AvatarRenderer 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.MessageColorProvider
import im.vector.app.features.home.room.detail.timeline.TimelineEventController import im.vector.app.features.home.room.detail.timeline.TimelineEventController
import org.matrix.android.sdk.api.util.MatrixItem import org.matrix.android.sdk.api.util.MatrixItem
import timber.log.Timber
@EpoxyModelClass(layout = R.layout.item_timeline_event_base_state) @EpoxyModelClass(layout = R.layout.item_timeline_event_base_state)
abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Holder>() { abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Holder>() {
@ -62,14 +57,14 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
attributes.avatarRenderer.render(attributes.userOfInterest, holder.creatorAvatarView) attributes.avatarRenderer.render(attributes.userOfInterest, holder.creatorAvatarView)
holder.callKindView.setText(attributes.callKind.title) holder.callKindView.setText(attributes.callKind.title)
holder.callKindView.setLeftDrawable(attributes.callKind.icon) holder.callKindView.setLeftDrawable(attributes.callKind.icon)
if (attributes.callStatus == CallStatus.INVITED && !attributes.informationData.sentByMe) { if (attributes.callStatus == CallStatus.INVITED && !attributes.informationData.sentByMe && attributes.isStillActive) {
holder.acceptRejectViewGroup.isVisible = true holder.acceptRejectViewGroup.isVisible = true
holder.acceptView.setOnClickListener { holder.acceptView.setOnClickListener {
Timber.v("On accept call: $attributes.callId ") attributes.callback?.onTimelineItemAction(RoomDetailAction.AcceptCall(callId = attributes.callId))
} }
holder.rejectView.setLeftDrawable(R.drawable.ic_call_hangup, R.color.riotx_notice) holder.rejectView.setLeftDrawable(R.drawable.ic_call_hangup, R.color.riotx_notice)
holder.rejectView.setOnClickListener { holder.rejectView.setOnClickListener {
Timber.v("On reject call: $attributes.callId") attributes.callback?.onTimelineItemAction(RoomDetailAction.EndCall)
} }
holder.statusView.isVisible = false holder.statusView.isVisible = false
when (attributes.callKind) { when (attributes.callKind) {
@ -101,6 +96,8 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
when (attributes.callStatus) { when (attributes.callStatus) {
CallStatus.INVITED -> if (attributes.informationData.sentByMe) { CallStatus.INVITED -> if (attributes.informationData.sentByMe) {
setText(R.string.call_tile_you_started_call) 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.IN_CALL -> setText(R.string.call_tile_in_call)
CallStatus.REJECTED -> if (attributes.informationData.sentByMe) { CallStatus.REJECTED -> if (attributes.informationData.sentByMe) {
@ -133,6 +130,8 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
val callKind: CallKind, val callKind: CallKind,
val callStatus: CallStatus, val callStatus: CallStatus,
val userOfInterest: MatrixItem, val userOfInterest: MatrixItem,
val isStillActive: Boolean,
val callback: TimelineEventController.Callback? = null,
override val informationData: MessageInformationData, override val informationData: MessageInformationData,
override val avatarRenderer: AvatarRenderer, override val avatarRenderer: AvatarRenderer,
override val messageColorProvider: MessageColorProvider, override val messageColorProvider: MessageColorProvider,
@ -152,6 +151,8 @@ abstract class CallTileTimelineItem : AbsBaseMessageItem<CallTileTimelineItem.Ho
INVITED, INVITED,
IN_CALL, IN_CALL,
REJECTED, REJECTED,
ENDED ENDED;
fun isActive() = this == INVITED || this == IN_CALL
} }
} }

View File

@ -2749,6 +2749,7 @@
<string name="matrix_to_card_title">Matrix Link</string> <string name="matrix_to_card_title">Matrix Link</string>
<string name="call_tile_you_started_call">You started a call</string> <string name="call_tile_you_started_call">You started a call</string>
<string name="call_tile_other_started_call">%1$s started a call</string>
<string name="call_tile_in_call">You\'re currently in this call</string> <string name="call_tile_in_call">You\'re currently in this call</string>
<string name="call_tile_you_declined">You declined this call %1$s</string> <string name="call_tile_you_declined">You declined this call %1$s</string>
<string name="call_tile_other_declined">%1$s declined this call</string> <string name="call_tile_other_declined">%1$s declined this call</string>