mirror of
https://github.com/nextcloud/talk-android
synced 2025-06-20 03:59:35 +01:00
WIP migrate call grid to compose
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
parent
4b8b7630a9
commit
feeec78ab4
@ -42,24 +42,24 @@ import android.view.OrientationEventListener
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.View.OnTouchListener
|
import android.view.View.OnTouchListener
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import android.view.ViewTreeObserver.OnGlobalLayoutListener
|
|
||||||
import android.widget.AdapterView
|
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import android.widget.RelativeLayout
|
import android.widget.RelativeLayout
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.annotation.DrawableRes
|
import androidx.annotation.DrawableRes
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.mutableStateListOf
|
||||||
import androidx.core.graphics.drawable.DrawableCompat
|
import androidx.core.graphics.drawable.DrawableCompat
|
||||||
import androidx.core.graphics.toColorInt
|
import androidx.core.graphics.toColorInt
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import androidx.lifecycle.ViewModelProvider
|
import androidx.lifecycle.ViewModelProvider
|
||||||
|
import androidx.lifecycle.lifecycleScope
|
||||||
import autodagger.AutoInjector
|
import autodagger.AutoInjector
|
||||||
import com.bluelinelabs.logansquare.LoganSquare
|
import com.bluelinelabs.logansquare.LoganSquare
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import com.google.android.material.snackbar.Snackbar
|
import com.google.android.material.snackbar.Snackbar
|
||||||
import com.nextcloud.talk.R
|
import com.nextcloud.talk.R
|
||||||
import com.nextcloud.talk.adapters.ParticipantDisplayItem
|
import com.nextcloud.talk.adapters.ParticipantDisplayItem
|
||||||
import com.nextcloud.talk.adapters.ParticipantsAdapter
|
|
||||||
import com.nextcloud.talk.api.NcApi
|
import com.nextcloud.talk.api.NcApi
|
||||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||||
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||||
@ -73,7 +73,9 @@ import com.nextcloud.talk.call.MessageSender
|
|||||||
import com.nextcloud.talk.call.MessageSenderMcu
|
import com.nextcloud.talk.call.MessageSenderMcu
|
||||||
import com.nextcloud.talk.call.MessageSenderNoMcu
|
import com.nextcloud.talk.call.MessageSenderNoMcu
|
||||||
import com.nextcloud.talk.call.MutableLocalCallParticipantModel
|
import com.nextcloud.talk.call.MutableLocalCallParticipantModel
|
||||||
|
import com.nextcloud.talk.call.ParticipantUiState
|
||||||
import com.nextcloud.talk.call.ReactionAnimator
|
import com.nextcloud.talk.call.ReactionAnimator
|
||||||
|
import com.nextcloud.talk.call.components.ParticipantGrid
|
||||||
import com.nextcloud.talk.chat.ChatActivity
|
import com.nextcloud.talk.chat.ChatActivity
|
||||||
import com.nextcloud.talk.data.user.model.User
|
import com.nextcloud.talk.data.user.model.User
|
||||||
import com.nextcloud.talk.databinding.CallActivityBinding
|
import com.nextcloud.talk.databinding.CallActivityBinding
|
||||||
@ -152,6 +154,7 @@ import io.reactivex.Observer
|
|||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.disposables.Disposable
|
import io.reactivex.disposables.Disposable
|
||||||
import io.reactivex.schedulers.Schedulers
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import okhttp3.Cache
|
import okhttp3.Cache
|
||||||
import org.apache.commons.lang3.StringEscapeUtils
|
import org.apache.commons.lang3.StringEscapeUtils
|
||||||
import org.greenrobot.eventbus.Subscribe
|
import org.greenrobot.eventbus.Subscribe
|
||||||
@ -303,8 +306,10 @@ class CallActivity : CallBaseActivity() {
|
|||||||
private var handler: Handler? = null
|
private var handler: Handler? = null
|
||||||
private var currentCallStatus: CallStatus? = null
|
private var currentCallStatus: CallStatus? = null
|
||||||
private var mediaPlayer: MediaPlayer? = null
|
private var mediaPlayer: MediaPlayer? = null
|
||||||
private var participantDisplayItems: MutableMap<String, ParticipantDisplayItem>? = null
|
|
||||||
private var participantsAdapter: ParticipantsAdapter? = null
|
private val participantItems = mutableStateListOf<ParticipantDisplayItem>()
|
||||||
|
private val participantUiStates = mutableStateListOf<ParticipantUiState>()
|
||||||
|
|
||||||
private var binding: CallActivityBinding? = null
|
private var binding: CallActivityBinding? = null
|
||||||
private var audioOutputDialog: AudioOutputDialog? = null
|
private var audioOutputDialog: AudioOutputDialog? = null
|
||||||
private var moreCallActionsDialog: MoreCallActionsDialog? = null
|
private var moreCallActionsDialog: MoreCallActionsDialog? = null
|
||||||
@ -399,7 +404,6 @@ class CallActivity : CallBaseActivity() {
|
|||||||
.setRepeatCount(PulseAnimation.INFINITE)
|
.setRepeatCount(PulseAnimation.INFINITE)
|
||||||
.setRepeatMode(PulseAnimation.REVERSE)
|
.setRepeatMode(PulseAnimation.REVERSE)
|
||||||
callParticipants = HashMap()
|
callParticipants = HashMap()
|
||||||
participantDisplayItems = HashMap()
|
|
||||||
reactionAnimator = ReactionAnimator(context, binding!!.reactionAnimationWrapper, viewThemeUtils)
|
reactionAnimator = ReactionAnimator(context, binding!!.reactionAnimationWrapper, viewThemeUtils)
|
||||||
|
|
||||||
checkInitialDevicePermissions()
|
checkInitialDevicePermissions()
|
||||||
@ -734,10 +738,15 @@ class CallActivity : CallBaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding!!.switchSelfVideoButton.setOnClickListener { switchCamera() }
|
binding!!.switchSelfVideoButton.setOnClickListener { switchCamera() }
|
||||||
binding!!.gridview.onItemClickListener =
|
|
||||||
AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, _: Long ->
|
// binding!!.composeParticipantGrid.setOnClickListener {
|
||||||
animateCallControls(true, 0)
|
// animateCallControls(true, 0)
|
||||||
}
|
// }
|
||||||
|
|
||||||
|
// binding!!.composeParticipantGrid.onItemClickListener =
|
||||||
|
// AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, _: Long ->
|
||||||
|
// animateCallControls(true, 0)
|
||||||
|
// }
|
||||||
binding!!.lowerHandButton.setOnClickListener { l: View? -> raiseHandViewModel!!.lowerHand() }
|
binding!!.lowerHandButton.setOnClickListener { l: View? -> raiseHandViewModel!!.lowerHand() }
|
||||||
binding!!.pictureInPictureButton.setOnClickListener { enterPipMode() }
|
binding!!.pictureInPictureButton.setOnClickListener { enterPipMode() }
|
||||||
}
|
}
|
||||||
@ -890,20 +899,20 @@ class CallActivity : CallBaseActivity() {
|
|||||||
val callControlsHeight =
|
val callControlsHeight =
|
||||||
applicationContext.resources.getDimension(R.dimen.call_controls_height).roundToInt()
|
applicationContext.resources.getDimension(R.dimen.call_controls_height).roundToInt()
|
||||||
params.setMargins(0, 0, 0, callControlsHeight)
|
params.setMargins(0, 0, 0, callControlsHeight)
|
||||||
binding!!.gridview.layoutParams = params
|
binding!!.composeParticipantGrid.layoutParams = params
|
||||||
} else {
|
} else {
|
||||||
val params = RelativeLayout.LayoutParams(
|
val params = RelativeLayout.LayoutParams(
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
)
|
)
|
||||||
params.setMargins(0, 0, 0, 0)
|
params.setMargins(0, 0, 0, 0)
|
||||||
binding!!.gridview.layoutParams = params
|
binding!!.composeParticipantGrid.layoutParams = params
|
||||||
if (cameraEnumerator!!.deviceNames.size < 2) {
|
if (cameraEnumerator!!.deviceNames.size < 2) {
|
||||||
binding!!.switchSelfVideoButton.visibility = View.GONE
|
binding!!.switchSelfVideoButton.visibility = View.GONE
|
||||||
}
|
}
|
||||||
initSelfVideoViewForNormalMode()
|
initSelfVideoViewForNormalMode()
|
||||||
}
|
}
|
||||||
binding!!.gridview.setOnTouchListener { _, me ->
|
binding!!.composeParticipantGrid.setOnTouchListener { _, me ->
|
||||||
val action = me.actionMasked
|
val action = me.actionMasked
|
||||||
if (action == MotionEvent.ACTION_DOWN) {
|
if (action == MotionEvent.ACTION_DOWN) {
|
||||||
animateCallControls(true, 0)
|
animateCallControls(true, 0)
|
||||||
@ -920,7 +929,7 @@ class CallActivity : CallBaseActivity() {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
animateCallControls(true, 0)
|
animateCallControls(true, 0)
|
||||||
initGridAdapter()
|
initGrid()
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
@ -951,19 +960,15 @@ class CallActivity : CallBaseActivity() {
|
|||||||
localVideoTrack!!.addSink(binding!!.pipSelfVideoRenderer)
|
localVideoTrack!!.addSink(binding!!.pipSelfVideoRenderer)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initGridAdapter() {
|
private fun initGrid() {
|
||||||
Log.d(TAG, "initGridAdapter")
|
Log.d(TAG, "initGrid")
|
||||||
val columns: Int
|
|
||||||
val participantsInGrid = participantDisplayItems!!.size
|
val participantsInGrid = participantUiStates.size
|
||||||
columns = if (resources != null &&
|
val columns = when {
|
||||||
resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
|
resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT -> {
|
||||||
) {
|
if (participantsInGrid > 2) GRID_MAX_COLUMN_COUNT_PORTRAIT else GRID_MIN_COLUMN_COUNT_PORTRAIT
|
||||||
if (participantsInGrid > 2) {
|
|
||||||
GRID_MAX_COLUMN_COUNT_PORTRAIT
|
|
||||||
} else {
|
|
||||||
GRID_MIN_COLUMN_COUNT_PORTRAIT
|
|
||||||
}
|
}
|
||||||
} else {
|
else -> {
|
||||||
if (participantsInGrid > 2) {
|
if (participantsInGrid > 2) {
|
||||||
GRID_MAX_COLUMN_COUNT_LANDSCAPE
|
GRID_MAX_COLUMN_COUNT_LANDSCAPE
|
||||||
} else if (participantsInGrid > 1) {
|
} else if (participantsInGrid > 1) {
|
||||||
@ -972,35 +977,17 @@ class CallActivity : CallBaseActivity() {
|
|||||||
GRID_MIN_COLUMN_COUNT_LANDSCAPE
|
GRID_MIN_COLUMN_COUNT_LANDSCAPE
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
binding!!.gridview.numColumns = columns
|
|
||||||
binding!!.conversationRelativeLayout
|
|
||||||
.viewTreeObserver
|
|
||||||
.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
|
|
||||||
override fun onGlobalLayout() {
|
|
||||||
binding!!.conversationRelativeLayout.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
|
||||||
val height = binding!!.conversationRelativeLayout.measuredHeight
|
|
||||||
binding!!.gridview.minimumHeight = height
|
|
||||||
}
|
}
|
||||||
})
|
|
||||||
binding!!.callInfosLinearLayout
|
binding!!.composeParticipantGrid.setContent {
|
||||||
.viewTreeObserver
|
MaterialTheme {
|
||||||
.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
|
ParticipantGrid(
|
||||||
override fun onGlobalLayout() {
|
participants = participantUiStates.toList(),
|
||||||
binding!!.callInfosLinearLayout.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
columns = columns
|
||||||
}
|
|
||||||
})
|
|
||||||
if (participantsAdapter != null) {
|
|
||||||
participantsAdapter!!.destroy()
|
|
||||||
}
|
|
||||||
participantsAdapter = ParticipantsAdapter(
|
|
||||||
this,
|
|
||||||
participantDisplayItems!!.toMap(),
|
|
||||||
binding!!.conversationRelativeLayout,
|
|
||||||
binding!!.callInfosLinearLayout,
|
|
||||||
columns,
|
|
||||||
isVoiceOnlyCall
|
|
||||||
)
|
)
|
||||||
binding!!.gridview.adapter = participantsAdapter
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isInPipMode) {
|
if (isInPipMode) {
|
||||||
updateUiForPipMode()
|
updateUiForPipMode()
|
||||||
}
|
}
|
||||||
@ -2116,6 +2103,7 @@ class CallActivity : CallBaseActivity() {
|
|||||||
videoCapturer!!.dispose()
|
videoCapturer!!.dispose()
|
||||||
videoCapturer = null
|
videoCapturer = null
|
||||||
}
|
}
|
||||||
|
binding!!.selfVideoRenderer.clearImage()
|
||||||
binding!!.selfVideoRenderer.release()
|
binding!!.selfVideoRenderer.release()
|
||||||
if (audioSource != null) {
|
if (audioSource != null) {
|
||||||
audioSource!!.dispose()
|
audioSource!!.dispose()
|
||||||
@ -2570,19 +2558,42 @@ class CallActivity : CallBaseActivity() {
|
|||||||
runOnUiThread { removeParticipantDisplayItem(sessionId, "video") }
|
runOnUiThread { removeParticipantDisplayItem(sessionId, "video") }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// private fun removeParticipantDisplayItem(sessionId: String?, videoStreamType: String) {
|
||||||
|
// Log.d(TAG, "removeParticipantDisplayItem")
|
||||||
|
// val participantDisplayItem = participantDisplayItems!!.remove("$sessionId-$videoStreamType") ?: return
|
||||||
|
// participantDisplayItem.destroy()
|
||||||
|
// if (!isDestroyed) {
|
||||||
|
// initGridAdapter()
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// private fun removeParticipantDisplayItem(sessionId: String?, videoStreamType: String) {
|
||||||
|
// val key = "$sessionId-$videoStreamType"
|
||||||
|
//
|
||||||
|
// val participant = participantItems.find { it.sessionKey == key }
|
||||||
|
// participant?.destroy()
|
||||||
|
// participantItems.remove(participant)
|
||||||
|
//
|
||||||
|
// initGrid()
|
||||||
|
// }
|
||||||
|
|
||||||
private fun removeParticipantDisplayItem(sessionId: String?, videoStreamType: String) {
|
private fun removeParticipantDisplayItem(sessionId: String?, videoStreamType: String) {
|
||||||
Log.d(TAG, "removeParticipantDisplayItem")
|
val key = "$sessionId-$videoStreamType"
|
||||||
val participantDisplayItem = participantDisplayItems!!.remove("$sessionId-$videoStreamType") ?: return
|
|
||||||
participantDisplayItem.destroy()
|
val participant = participantItems.find { it.sessionKey == key }
|
||||||
if (!isDestroyed) {
|
participant?.destroy()
|
||||||
initGridAdapter()
|
participantItems.removeAll { it.sessionKey == key }
|
||||||
}
|
|
||||||
|
// Also remove UI state
|
||||||
|
participantUiStates.removeAll { it.sessionKey == key }
|
||||||
|
|
||||||
|
initGrid()
|
||||||
}
|
}
|
||||||
|
|
||||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||||
fun onMessageEvent(configurationChangeEvent: ConfigurationChangeEvent?) {
|
fun onMessageEvent(configurationChangeEvent: ConfigurationChangeEvent?) {
|
||||||
powerManagerUtils!!.setOrientation(Objects.requireNonNull(resources).configuration.orientation)
|
powerManagerUtils!!.setOrientation(Objects.requireNonNull(resources).configuration.orientation)
|
||||||
initGridAdapter()
|
initGrid()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateSelfVideoViewIceConnectionState(iceConnectionState: IceConnectionState) {
|
private fun updateSelfVideoViewIceConnectionState(iceConnectionState: IceConnectionState) {
|
||||||
@ -2677,22 +2688,37 @@ class CallActivity : CallBaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun addParticipantDisplayItem(callParticipantModel: CallParticipantModel, videoStreamType: String) {
|
private fun addParticipantDisplayItem(callParticipantModel: CallParticipantModel, videoStreamType: String) {
|
||||||
if (callParticipantModel.isInternal != null && callParticipantModel.isInternal) {
|
if (callParticipantModel.isInternal == true) return
|
||||||
return
|
|
||||||
}
|
|
||||||
val defaultGuestNick = resources.getString(R.string.nc_nick_guest)
|
val defaultGuestNick = resources.getString(R.string.nc_nick_guest)
|
||||||
val participantDisplayItem = ParticipantDisplayItem(
|
val participantDisplayItem = ParticipantDisplayItem(
|
||||||
context,
|
context = context,
|
||||||
baseUrl,
|
baseUrl = baseUrl!!,
|
||||||
defaultGuestNick,
|
defaultGuestNick = defaultGuestNick,
|
||||||
rootEglBase,
|
rootEglBase = rootEglBase!!,
|
||||||
videoStreamType,
|
streamType = videoStreamType,
|
||||||
roomToken,
|
roomToken = roomToken!!,
|
||||||
callParticipantModel
|
callParticipantModel = callParticipantModel
|
||||||
)
|
)
|
||||||
val sessionId = callParticipantModel.sessionId
|
|
||||||
participantDisplayItems!!["$sessionId-$videoStreamType"] = participantDisplayItem
|
val sessionKey = participantDisplayItem.sessionKey
|
||||||
initGridAdapter()
|
|
||||||
|
if (participantItems.none { it.sessionKey == sessionKey }) {
|
||||||
|
participantItems.add(participantDisplayItem)
|
||||||
|
|
||||||
|
lifecycleScope.launch {
|
||||||
|
participantDisplayItem.uiStateFlow.collect { uiState ->
|
||||||
|
val index = participantUiStates.indexOfFirst { it.sessionKey == sessionKey }
|
||||||
|
if (index >= 0) {
|
||||||
|
participantUiStates[index] = uiState
|
||||||
|
} else {
|
||||||
|
participantUiStates.add(uiState)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initGrid()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setCallState(callState: CallStatus) {
|
private fun setCallState(callState: CallStatus) {
|
||||||
@ -2725,7 +2751,7 @@ class CallActivity : CallBaseActivity() {
|
|||||||
binding!!.callModeTextView.text = descriptionForCallType
|
binding!!.callModeTextView.text = descriptionForCallType
|
||||||
binding!!.callStates.callStateTextView.setText(R.string.nc_leaving_call)
|
binding!!.callStates.callStateTextView.setText(R.string.nc_leaving_call)
|
||||||
binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE
|
binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE
|
||||||
binding!!.gridview.visibility = View.INVISIBLE
|
binding!!.composeParticipantGrid.visibility = View.INVISIBLE
|
||||||
binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE
|
binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE
|
||||||
binding!!.callStates.errorImageView.visibility = View.GONE
|
binding!!.callStates.errorImageView.visibility = View.GONE
|
||||||
}
|
}
|
||||||
@ -2737,8 +2763,8 @@ class CallActivity : CallBaseActivity() {
|
|||||||
if (binding!!.callStates.callStateRelativeLayout.visibility != View.VISIBLE) {
|
if (binding!!.callStates.callStateRelativeLayout.visibility != View.VISIBLE) {
|
||||||
binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE
|
binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
if (binding!!.gridview.visibility != View.INVISIBLE) {
|
if (binding!!.composeParticipantGrid.visibility != View.INVISIBLE) {
|
||||||
binding!!.gridview.visibility = View.INVISIBLE
|
binding!!.composeParticipantGrid.visibility = View.INVISIBLE
|
||||||
}
|
}
|
||||||
if (binding!!.callStates.callStateProgressBar.visibility != View.GONE) {
|
if (binding!!.callStates.callStateProgressBar.visibility != View.GONE) {
|
||||||
binding!!.callStates.callStateProgressBar.visibility = View.GONE
|
binding!!.callStates.callStateProgressBar.visibility = View.GONE
|
||||||
@ -2764,8 +2790,8 @@ class CallActivity : CallBaseActivity() {
|
|||||||
if (binding!!.callStates.callStateProgressBar.visibility != View.GONE) {
|
if (binding!!.callStates.callStateProgressBar.visibility != View.GONE) {
|
||||||
binding!!.callStates.callStateProgressBar.visibility = View.GONE
|
binding!!.callStates.callStateProgressBar.visibility = View.GONE
|
||||||
}
|
}
|
||||||
if (binding!!.gridview.visibility != View.VISIBLE) {
|
if (binding!!.composeParticipantGrid.visibility != View.VISIBLE) {
|
||||||
binding!!.gridview.visibility = View.VISIBLE
|
binding!!.composeParticipantGrid.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
if (binding!!.callStates.errorImageView.visibility != View.GONE) {
|
if (binding!!.callStates.errorImageView.visibility != View.GONE) {
|
||||||
binding!!.callStates.errorImageView.visibility = View.GONE
|
binding!!.callStates.errorImageView.visibility = View.GONE
|
||||||
@ -2785,8 +2811,8 @@ class CallActivity : CallBaseActivity() {
|
|||||||
if (binding!!.callStates.callStateProgressBar.visibility != View.VISIBLE) {
|
if (binding!!.callStates.callStateProgressBar.visibility != View.VISIBLE) {
|
||||||
binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE
|
binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
if (binding!!.gridview.visibility != View.INVISIBLE) {
|
if (binding!!.composeParticipantGrid.visibility != View.INVISIBLE) {
|
||||||
binding!!.gridview.visibility = View.INVISIBLE
|
binding!!.composeParticipantGrid.visibility = View.INVISIBLE
|
||||||
}
|
}
|
||||||
if (binding!!.callStates.errorImageView.visibility != View.GONE) {
|
if (binding!!.callStates.errorImageView.visibility != View.GONE) {
|
||||||
binding!!.callStates.errorImageView.visibility = View.GONE
|
binding!!.callStates.errorImageView.visibility = View.GONE
|
||||||
@ -2800,8 +2826,8 @@ class CallActivity : CallBaseActivity() {
|
|||||||
if (binding!!.callStates.callStateRelativeLayout.visibility != View.VISIBLE) {
|
if (binding!!.callStates.callStateRelativeLayout.visibility != View.VISIBLE) {
|
||||||
binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE
|
binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
if (binding!!.gridview.visibility != View.INVISIBLE) {
|
if (binding!!.composeParticipantGrid.visibility != View.INVISIBLE) {
|
||||||
binding!!.gridview.visibility = View.INVISIBLE
|
binding!!.composeParticipantGrid.visibility = View.INVISIBLE
|
||||||
}
|
}
|
||||||
if (binding!!.callStates.callStateProgressBar.visibility != View.VISIBLE) {
|
if (binding!!.callStates.callStateProgressBar.visibility != View.VISIBLE) {
|
||||||
binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE
|
binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE
|
||||||
@ -2818,8 +2844,8 @@ class CallActivity : CallBaseActivity() {
|
|||||||
if (binding!!.callStates.callStateRelativeLayout.visibility != View.VISIBLE) {
|
if (binding!!.callStates.callStateRelativeLayout.visibility != View.VISIBLE) {
|
||||||
binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE
|
binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
if (binding!!.gridview.visibility != View.INVISIBLE) {
|
if (binding!!.composeParticipantGrid.visibility != View.INVISIBLE) {
|
||||||
binding!!.gridview.visibility = View.INVISIBLE
|
binding!!.composeParticipantGrid.visibility = View.INVISIBLE
|
||||||
}
|
}
|
||||||
if (binding!!.callStates.callStateProgressBar.visibility != View.VISIBLE) {
|
if (binding!!.callStates.callStateProgressBar.visibility != View.VISIBLE) {
|
||||||
binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE
|
binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE
|
||||||
@ -2839,8 +2865,8 @@ class CallActivity : CallBaseActivity() {
|
|||||||
if (binding!!.callStates.callStateProgressBar.visibility != View.GONE) {
|
if (binding!!.callStates.callStateProgressBar.visibility != View.GONE) {
|
||||||
binding!!.callStates.callStateProgressBar.visibility = View.GONE
|
binding!!.callStates.callStateProgressBar.visibility = View.GONE
|
||||||
}
|
}
|
||||||
if (binding!!.gridview.visibility != View.INVISIBLE) {
|
if (binding!!.composeParticipantGrid.visibility != View.INVISIBLE) {
|
||||||
binding!!.gridview.visibility = View.INVISIBLE
|
binding!!.composeParticipantGrid.visibility = View.INVISIBLE
|
||||||
}
|
}
|
||||||
binding!!.callStates.errorImageView.setImageResource(R.drawable.ic_av_timer_timer_24dp)
|
binding!!.callStates.errorImageView.setImageResource(R.drawable.ic_av_timer_timer_24dp)
|
||||||
if (binding!!.callStates.errorImageView.visibility != View.VISIBLE) {
|
if (binding!!.callStates.errorImageView.visibility != View.VISIBLE) {
|
||||||
@ -2860,8 +2886,8 @@ class CallActivity : CallBaseActivity() {
|
|||||||
if (binding!!.callStates.callStateRelativeLayout.visibility != View.VISIBLE) {
|
if (binding!!.callStates.callStateRelativeLayout.visibility != View.VISIBLE) {
|
||||||
binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE
|
binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE
|
||||||
}
|
}
|
||||||
if (binding!!.gridview.visibility != View.INVISIBLE) {
|
if (binding!!.composeParticipantGrid.visibility != View.INVISIBLE) {
|
||||||
binding!!.gridview.visibility = View.INVISIBLE
|
binding!!.composeParticipantGrid.visibility = View.INVISIBLE
|
||||||
}
|
}
|
||||||
if (binding!!.callStates.callStateProgressBar.visibility != View.VISIBLE) {
|
if (binding!!.callStates.callStateProgressBar.visibility != View.VISIBLE) {
|
||||||
binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE
|
binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE
|
||||||
@ -3021,8 +3047,8 @@ class CallActivity : CallBaseActivity() {
|
|||||||
removeParticipantDisplayItem(sessionId, "screen")
|
removeParticipantDisplayItem(sessionId, "screen")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
val hasScreenParticipantDisplayItem = participantDisplayItems!!["$sessionId-screen"] != null
|
val screenParticipantDisplayItem = participantItems.find { it.sessionKey == "$sessionId-screen" }
|
||||||
if (!hasScreenParticipantDisplayItem) {
|
if (screenParticipantDisplayItem != null) {
|
||||||
addParticipantDisplayItem(callParticipantModel, "screen")
|
addParticipantDisplayItem(callParticipantModel, "screen")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3230,14 +3256,15 @@ class CallActivity : CallBaseActivity() {
|
|||||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
)
|
)
|
||||||
params.setMargins(0, 0, 0, 0)
|
params.setMargins(0, 0, 0, 0)
|
||||||
binding!!.gridview.layoutParams = params
|
binding!!.composeParticipantGrid.layoutParams = params
|
||||||
binding!!.callControls.visibility = View.GONE
|
binding!!.callControls.visibility = View.GONE
|
||||||
binding!!.callInfosLinearLayout.visibility = View.GONE
|
binding!!.callInfosLinearLayout.visibility = View.GONE
|
||||||
binding!!.selfVideoViewWrapper.visibility = View.GONE
|
binding!!.selfVideoViewWrapper.visibility = View.GONE
|
||||||
binding!!.callStates.callStateRelativeLayout.visibility = View.GONE
|
binding!!.callStates.callStateRelativeLayout.visibility = View.GONE
|
||||||
|
|
||||||
|
binding!!.selfVideoRenderer.clearImage()
|
||||||
binding!!.selfVideoRenderer.release()
|
binding!!.selfVideoRenderer.release()
|
||||||
if (participantDisplayItems!!.size > 1) {
|
if (participantItems.size > 1) {
|
||||||
binding!!.pipCallConversationNameTextView.text = conversationName
|
binding!!.pipCallConversationNameTextView.text = conversationName
|
||||||
binding!!.pipSelfVideoOverlay.visibility = View.VISIBLE
|
binding!!.pipSelfVideoOverlay.visibility = View.VISIBLE
|
||||||
initSelfVideoViewForPipMode()
|
initSelfVideoViewForPipMode()
|
||||||
|
@ -3,189 +3,182 @@
|
|||||||
*
|
*
|
||||||
* SPDX-FileCopyrightText: 2023 Andy Scherzinger <info@andy-scherzinger.de>
|
* SPDX-FileCopyrightText: 2023 Andy Scherzinger <info@andy-scherzinger.de>
|
||||||
* SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez <danxuliu@gmail.com>
|
* SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez <danxuliu@gmail.com>
|
||||||
* SPDX-FileCopyrightText: 2021 Marcel Hibbe <dev@mhibbe.de>
|
* SPDX-FileCopyrightText: 2021-2025 Marcel Hibbe <dev@mhibbe.de>
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
*/
|
*/
|
||||||
package com.nextcloud.talk.adapters;
|
package com.nextcloud.talk.adapters
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context
|
||||||
import android.os.Handler;
|
import android.os.Handler
|
||||||
import android.os.Looper;
|
import android.os.Looper
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import com.nextcloud.talk.call.CallParticipantModel
|
||||||
|
import com.nextcloud.talk.call.ParticipantUiState
|
||||||
|
import com.nextcloud.talk.call.RaisedHand
|
||||||
|
import com.nextcloud.talk.models.json.participants.Participant.ActorType
|
||||||
|
import com.nextcloud.talk.utils.ApiUtils.getUrlForAvatar
|
||||||
|
import com.nextcloud.talk.utils.ApiUtils.getUrlForFederatedAvatar
|
||||||
|
import com.nextcloud.talk.utils.ApiUtils.getUrlForGuestAvatar
|
||||||
|
import com.nextcloud.talk.utils.DisplayUtils.isDarkModeOn
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import org.webrtc.EglBase
|
||||||
|
import org.webrtc.MediaStream
|
||||||
|
import org.webrtc.PeerConnection.IceConnectionState
|
||||||
|
import org.webrtc.SurfaceViewRenderer
|
||||||
|
|
||||||
import com.nextcloud.talk.call.CallParticipantModel;
|
class ParticipantDisplayItem(
|
||||||
import com.nextcloud.talk.call.RaisedHand;
|
private val context: Context,
|
||||||
import com.nextcloud.talk.models.json.participants.Participant;
|
private val baseUrl: String,
|
||||||
import com.nextcloud.talk.utils.ApiUtils;
|
private val defaultGuestNick: String,
|
||||||
import com.nextcloud.talk.utils.DisplayUtils;
|
val rootEglBase: EglBase,
|
||||||
|
private val streamType: String,
|
||||||
|
private val roomToken: String,
|
||||||
|
private val callParticipantModel: CallParticipantModel
|
||||||
|
) {
|
||||||
|
private val participantDisplayItemNotifier = ParticipantDisplayItemNotifier()
|
||||||
|
|
||||||
import org.webrtc.EglBase;
|
private val _uiStateFlow = MutableStateFlow(buildUiState())
|
||||||
import org.webrtc.MediaStream;
|
val uiStateFlow: StateFlow<ParticipantUiState> = _uiStateFlow.asStateFlow()
|
||||||
import org.webrtc.PeerConnection;
|
|
||||||
|
|
||||||
public class ParticipantDisplayItem {
|
private val session: String = callParticipantModel.sessionId
|
||||||
|
|
||||||
/**
|
var actorType: ActorType? = null
|
||||||
* Shared handler to receive change notifications from the model on the main thread.
|
private set
|
||||||
*/
|
private var actorId: String? = null
|
||||||
private static final Handler handler = new Handler(Looper.getMainLooper());
|
private var userId: String? = null
|
||||||
|
private var iceConnectionState: IceConnectionState? = null
|
||||||
|
var nick: String? = null
|
||||||
|
get() = (if (TextUtils.isEmpty(userId) && TextUtils.isEmpty(field)) defaultGuestNick else field)
|
||||||
|
|
||||||
private final ParticipantDisplayItemNotifier participantDisplayItemNotifier = new ParticipantDisplayItemNotifier();
|
var urlForAvatar: String? = null
|
||||||
|
private set
|
||||||
|
var mediaStream: MediaStream? = null
|
||||||
|
private set
|
||||||
|
var isStreamEnabled: Boolean = false
|
||||||
|
private set
|
||||||
|
var isAudioEnabled: Boolean = false
|
||||||
|
private set
|
||||||
|
var raisedHand: RaisedHand? = null
|
||||||
|
private set
|
||||||
|
var surfaceViewRenderer: SurfaceViewRenderer? = null
|
||||||
|
|
||||||
private final Context context;
|
val sessionKey: String
|
||||||
|
get() = "$session-$streamType"
|
||||||
|
|
||||||
private final String baseUrl;
|
interface Observer {
|
||||||
private final String defaultGuestNick;
|
fun onChange()
|
||||||
private final EglBase rootEglBase;
|
|
||||||
|
|
||||||
private final String session;
|
|
||||||
private final String streamType;
|
|
||||||
|
|
||||||
private final String roomToken;
|
|
||||||
|
|
||||||
private final CallParticipantModel callParticipantModel;
|
|
||||||
|
|
||||||
private Participant.ActorType actorType;
|
|
||||||
private String actorId;
|
|
||||||
private String userId;
|
|
||||||
private PeerConnection.IceConnectionState iceConnectionState;
|
|
||||||
private String nick;
|
|
||||||
private String urlForAvatar;
|
|
||||||
private MediaStream mediaStream;
|
|
||||||
private boolean streamEnabled;
|
|
||||||
private boolean isAudioEnabled;
|
|
||||||
private RaisedHand raisedHand;
|
|
||||||
|
|
||||||
public interface Observer {
|
|
||||||
void onChange();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private final CallParticipantModel.Observer callParticipantModelObserver = new CallParticipantModel.Observer() {
|
private val callParticipantModelObserver: CallParticipantModel.Observer = object : CallParticipantModel.Observer {
|
||||||
@Override
|
override fun onChange() {
|
||||||
public void onChange() {
|
updateFromModel()
|
||||||
updateFromModel();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
override fun onReaction(reaction: String) {
|
||||||
public void onReaction(String reaction) {
|
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
public ParticipantDisplayItem(Context context, String baseUrl, String defaultGuestNick, EglBase rootEglBase,
|
|
||||||
String streamType, String roomToken, CallParticipantModel callParticipantModel) {
|
|
||||||
this.context = context;
|
|
||||||
|
|
||||||
this.baseUrl = baseUrl;
|
|
||||||
this.defaultGuestNick = defaultGuestNick;
|
|
||||||
this.rootEglBase = rootEglBase;
|
|
||||||
|
|
||||||
this.session = callParticipantModel.getSessionId();
|
|
||||||
this.streamType = streamType;
|
|
||||||
|
|
||||||
this.roomToken = roomToken;
|
|
||||||
|
|
||||||
this.callParticipantModel = callParticipantModel;
|
|
||||||
this.callParticipantModel.addObserver(callParticipantModelObserver, handler);
|
|
||||||
|
|
||||||
updateFromModel();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void destroy() {
|
init {
|
||||||
this.callParticipantModel.removeObserver(callParticipantModelObserver);
|
callParticipantModel.addObserver(callParticipantModelObserver, handler)
|
||||||
|
|
||||||
|
updateFromModel()
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateFromModel() {
|
fun destroy() {
|
||||||
actorType = callParticipantModel.getActorType();
|
callParticipantModel.removeObserver(callParticipantModelObserver)
|
||||||
actorId = callParticipantModel.getActorId();
|
|
||||||
userId = callParticipantModel.getUserId();
|
|
||||||
nick = callParticipantModel.getNick();
|
|
||||||
|
|
||||||
this.updateUrlForAvatar();
|
surfaceViewRenderer?.let { renderer ->
|
||||||
|
try {
|
||||||
|
mediaStream?.videoTracks?.firstOrNull()?.removeSink(renderer)
|
||||||
|
renderer.release()
|
||||||
|
(renderer.parent as? ViewGroup)?.removeView(renderer)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w("ParticipantDisplayItem", "Error releasing renderer", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
surfaceViewRenderer = null
|
||||||
|
}
|
||||||
|
|
||||||
if ("screen".equals(streamType)) {
|
private fun updateFromModel() {
|
||||||
iceConnectionState = callParticipantModel.getScreenIceConnectionState();
|
actorType = callParticipantModel.actorType
|
||||||
mediaStream = callParticipantModel.getScreenMediaStream();
|
actorId = callParticipantModel.actorId
|
||||||
isAudioEnabled = true;
|
userId = callParticipantModel.userId
|
||||||
streamEnabled = true;
|
nick = callParticipantModel.nick
|
||||||
|
|
||||||
|
updateUrlForAvatar()
|
||||||
|
|
||||||
|
if (streamType == "screen") {
|
||||||
|
iceConnectionState = callParticipantModel.screenIceConnectionState
|
||||||
|
mediaStream = callParticipantModel.screenMediaStream
|
||||||
|
isAudioEnabled = true
|
||||||
|
isStreamEnabled = true
|
||||||
} else {
|
} else {
|
||||||
iceConnectionState = callParticipantModel.getIceConnectionState();
|
iceConnectionState = callParticipantModel.iceConnectionState
|
||||||
mediaStream = callParticipantModel.getMediaStream();
|
mediaStream = callParticipantModel.mediaStream
|
||||||
isAudioEnabled = callParticipantModel.isAudioAvailable() != null ?
|
isAudioEnabled = callParticipantModel.isAudioAvailable ?: false
|
||||||
callParticipantModel.isAudioAvailable() : false;
|
isStreamEnabled = callParticipantModel.isVideoAvailable ?: false
|
||||||
streamEnabled = callParticipantModel.isVideoAvailable() != null ?
|
|
||||||
callParticipantModel.isVideoAvailable() : false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
raisedHand = callParticipantModel.getRaisedHand();
|
raisedHand = callParticipantModel.raisedHand
|
||||||
|
|
||||||
participantDisplayItemNotifier.notifyChange();
|
if (surfaceViewRenderer == null && mediaStream != null) {
|
||||||
|
val renderer = SurfaceViewRenderer(context).apply {
|
||||||
|
init(rootEglBase.eglBaseContext, null)
|
||||||
|
setEnableHardwareScaler(true)
|
||||||
|
setMirror(false)
|
||||||
|
}
|
||||||
|
surfaceViewRenderer = renderer
|
||||||
|
mediaStream?.videoTracks?.firstOrNull()?.addSink(renderer)
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateUrlForAvatar() {
|
_uiStateFlow.value = buildUiState()
|
||||||
if (actorType == Participant.ActorType.FEDERATED) {
|
participantDisplayItemNotifier.notifyChange()
|
||||||
int darkTheme = DisplayUtils.INSTANCE.isDarkModeOn(context) ? 1 : 0;
|
}
|
||||||
urlForAvatar = ApiUtils.getUrlForFederatedAvatar(baseUrl, roomToken, actorId, darkTheme, true);
|
|
||||||
|
private fun buildUiState(): ParticipantUiState {
|
||||||
|
return ParticipantUiState(
|
||||||
|
sessionKey = sessionKey,
|
||||||
|
nick = nick ?: "Guest",
|
||||||
|
isConnected = isConnected,
|
||||||
|
isAudioEnabled = isAudioEnabled,
|
||||||
|
isStreamEnabled = isStreamEnabled,
|
||||||
|
raisedHand = raisedHand?.state == true,
|
||||||
|
avatarUrl = urlForAvatar,
|
||||||
|
surfaceViewRenderer = surfaceViewRenderer
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateUrlForAvatar() {
|
||||||
|
if (actorType == ActorType.FEDERATED) {
|
||||||
|
val darkTheme = if (isDarkModeOn(context)) 1 else 0
|
||||||
|
urlForAvatar = getUrlForFederatedAvatar(baseUrl, roomToken, actorId!!, darkTheme, true)
|
||||||
} else if (!TextUtils.isEmpty(userId)) {
|
} else if (!TextUtils.isEmpty(userId)) {
|
||||||
urlForAvatar = ApiUtils.getUrlForAvatar(baseUrl, userId, true);
|
urlForAvatar = getUrlForAvatar(baseUrl, userId, true)
|
||||||
} else {
|
} else {
|
||||||
urlForAvatar = ApiUtils.getUrlForGuestAvatar(baseUrl, getNick(), true);
|
urlForAvatar = getUrlForGuestAvatar(baseUrl, nick, true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isConnected() {
|
val isConnected: Boolean
|
||||||
return iceConnectionState == PeerConnection.IceConnectionState.CONNECTED ||
|
get() = iceConnectionState == IceConnectionState.CONNECTED ||
|
||||||
iceConnectionState == PeerConnection.IceConnectionState.COMPLETED ||
|
iceConnectionState == IceConnectionState.COMPLETED ||
|
||||||
// If there is no connection state that means that no connection is needed, so it is a special case that is
|
// If there is no connection state that means that no connection is needed,
|
||||||
// also seen as "connected".
|
// so it is a special case that is also seen as "connected".
|
||||||
iceConnectionState == null;
|
iceConnectionState == null
|
||||||
|
|
||||||
|
fun addObserver(observer: Observer?) {
|
||||||
|
participantDisplayItemNotifier.addObserver(observer)
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getNick() {
|
fun removeObserver(observer: Observer?) {
|
||||||
if (TextUtils.isEmpty(userId) && TextUtils.isEmpty(nick)) {
|
participantDisplayItemNotifier.removeObserver(observer)
|
||||||
return defaultGuestNick;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nick;
|
override fun toString(): String {
|
||||||
}
|
|
||||||
|
|
||||||
public String getUrlForAvatar() {
|
|
||||||
return urlForAvatar;
|
|
||||||
}
|
|
||||||
|
|
||||||
public MediaStream getMediaStream() {
|
|
||||||
return mediaStream;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isStreamEnabled() {
|
|
||||||
return streamEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public EglBase getRootEglBase() {
|
|
||||||
return rootEglBase;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isAudioEnabled() {
|
|
||||||
return isAudioEnabled;
|
|
||||||
}
|
|
||||||
|
|
||||||
public RaisedHand getRaisedHand() {
|
|
||||||
return raisedHand;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Participant.ActorType getActorType() {
|
|
||||||
return actorType;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void addObserver(Observer observer) {
|
|
||||||
participantDisplayItemNotifier.addObserver(observer);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void removeObserver(Observer observer) {
|
|
||||||
participantDisplayItemNotifier.removeObserver(observer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public String toString() {
|
|
||||||
return "ParticipantSession{" +
|
return "ParticipantSession{" +
|
||||||
"userId='" + userId + '\'' +
|
"userId='" + userId + '\'' +
|
||||||
", actorType='" + actorType + '\'' +
|
", actorType='" + actorType + '\'' +
|
||||||
@ -195,9 +188,16 @@ public class ParticipantDisplayItem {
|
|||||||
", urlForAvatar='" + urlForAvatar + '\'' +
|
", urlForAvatar='" + urlForAvatar + '\'' +
|
||||||
", mediaStream=" + mediaStream +
|
", mediaStream=" + mediaStream +
|
||||||
", streamType='" + streamType + '\'' +
|
", streamType='" + streamType + '\'' +
|
||||||
", streamEnabled=" + streamEnabled +
|
", streamEnabled=" + isStreamEnabled +
|
||||||
", rootEglBase=" + rootEglBase +
|
", rootEglBase=" + rootEglBase +
|
||||||
", raisedHand=" + raisedHand +
|
", raisedHand=" + raisedHand +
|
||||||
'}';
|
'}'
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Shared handler to receive change notifications from the model on the main thread.
|
||||||
|
*/
|
||||||
|
private val handler = Handler(Looper.getMainLooper())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,204 +0,0 @@
|
|||||||
/*
|
|
||||||
* Nextcloud Talk - Android Client
|
|
||||||
*
|
|
||||||
* SPDX-FileCopyrightText: 2021-2023 Marcel Hibbe <dev@mhibbe.de>
|
|
||||||
* SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez <danxuliu@gmail.com>
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
package com.nextcloud.talk.adapters
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.Log
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.BaseAdapter
|
|
||||||
import android.widget.ImageView
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import android.widget.ProgressBar
|
|
||||||
import android.widget.RelativeLayout
|
|
||||||
import android.widget.TextView
|
|
||||||
import com.nextcloud.talk.R
|
|
||||||
import com.nextcloud.talk.activities.CallActivity
|
|
||||||
import com.nextcloud.talk.extensions.loadAvatarWithUrl
|
|
||||||
import com.nextcloud.talk.extensions.loadFirstLetterAvatar
|
|
||||||
import com.nextcloud.talk.models.json.participants.Participant
|
|
||||||
import org.webrtc.MediaStream
|
|
||||||
import org.webrtc.MediaStreamTrack
|
|
||||||
import org.webrtc.RendererCommon
|
|
||||||
import org.webrtc.SurfaceViewRenderer
|
|
||||||
import kotlin.math.ceil
|
|
||||||
|
|
||||||
class ParticipantsAdapter(
|
|
||||||
private val mContext: Context,
|
|
||||||
participantDisplayItems: Map<String?, ParticipantDisplayItem>,
|
|
||||||
private val gridViewWrapper: RelativeLayout,
|
|
||||||
private val callInfosLinearLayout: LinearLayout,
|
|
||||||
private val columns: Int,
|
|
||||||
private val isVoiceOnlyCall: Boolean
|
|
||||||
) : BaseAdapter() {
|
|
||||||
private val participantDisplayItemObserver = ParticipantDisplayItem.Observer { this.notifyDataSetChanged() }
|
|
||||||
|
|
||||||
private val participantDisplayItems = ArrayList<ParticipantDisplayItem>()
|
|
||||||
|
|
||||||
init {
|
|
||||||
this.participantDisplayItems.addAll(participantDisplayItems.values)
|
|
||||||
|
|
||||||
for (participantDisplayItem in this.participantDisplayItems) {
|
|
||||||
participantDisplayItem.addObserver(participantDisplayItemObserver)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun destroy() {
|
|
||||||
for (participantDisplayItem in participantDisplayItems) {
|
|
||||||
participantDisplayItem.removeObserver(participantDisplayItemObserver)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getCount(): Int {
|
|
||||||
return participantDisplayItems.size
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItem(position: Int): ParticipantDisplayItem {
|
|
||||||
return participantDisplayItems[position]
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemId(position: Int): Long {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("Detekt.LongMethod", "Detekt.TooGenericExceptionCaught")
|
|
||||||
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View {
|
|
||||||
var convertView = convertView
|
|
||||||
val participantDisplayItem = getItem(position)
|
|
||||||
|
|
||||||
val surfaceViewRenderer: SurfaceViewRenderer
|
|
||||||
if (convertView == null) {
|
|
||||||
convertView = LayoutInflater.from(mContext).inflate(R.layout.call_item, parent, false)
|
|
||||||
convertView.visibility = View.VISIBLE
|
|
||||||
|
|
||||||
surfaceViewRenderer = convertView.findViewById(R.id.surface_view)
|
|
||||||
try {
|
|
||||||
Log.d(TAG, "hasSurface: " + participantDisplayItem.rootEglBase.hasSurface())
|
|
||||||
|
|
||||||
surfaceViewRenderer.setMirror(false)
|
|
||||||
surfaceViewRenderer.init(participantDisplayItem.rootEglBase.eglBaseContext, null)
|
|
||||||
surfaceViewRenderer.setZOrderMediaOverlay(false)
|
|
||||||
// disabled because it causes some devices to crash
|
|
||||||
surfaceViewRenderer.setEnableHardwareScaler(false)
|
|
||||||
surfaceViewRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "error while initializing surfaceViewRenderer", e)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
surfaceViewRenderer = convertView.findViewById(R.id.surface_view)
|
|
||||||
}
|
|
||||||
|
|
||||||
val progressBar = convertView!!.findViewById<ProgressBar>(R.id.participant_progress_bar)
|
|
||||||
if (!participantDisplayItem.isConnected) {
|
|
||||||
progressBar.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
progressBar.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
val layoutParams = convertView.layoutParams
|
|
||||||
layoutParams.height = scaleGridViewItemHeight()
|
|
||||||
convertView.layoutParams = layoutParams
|
|
||||||
|
|
||||||
val nickTextView = convertView.findViewById<TextView>(R.id.peer_nick_text_view)
|
|
||||||
val imageView = convertView.findViewById<ImageView>(R.id.avatarImageView)
|
|
||||||
|
|
||||||
val mediaStream = participantDisplayItem.mediaStream
|
|
||||||
if (hasVideoStream(participantDisplayItem, mediaStream)) {
|
|
||||||
val videoTrack = mediaStream.videoTracks[0]
|
|
||||||
videoTrack.addSink(surfaceViewRenderer)
|
|
||||||
imageView.visibility = View.INVISIBLE
|
|
||||||
surfaceViewRenderer.visibility = View.VISIBLE
|
|
||||||
nickTextView.visibility = View.GONE
|
|
||||||
} else {
|
|
||||||
imageView.visibility = View.VISIBLE
|
|
||||||
surfaceViewRenderer.visibility = View.INVISIBLE
|
|
||||||
|
|
||||||
if ((mContext as CallActivity).isInPipMode) {
|
|
||||||
nickTextView.visibility = View.GONE
|
|
||||||
} else {
|
|
||||||
nickTextView.visibility = View.VISIBLE
|
|
||||||
nickTextView.text = participantDisplayItem.nick
|
|
||||||
}
|
|
||||||
if (participantDisplayItem.actorType == Participant.ActorType.GUESTS ||
|
|
||||||
participantDisplayItem.actorType == Participant.ActorType.EMAILS
|
|
||||||
) {
|
|
||||||
imageView
|
|
||||||
.loadFirstLetterAvatar(
|
|
||||||
participantDisplayItem.nick.toString()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
imageView.loadAvatarWithUrl(null, participantDisplayItem.urlForAvatar)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val audioOffView = convertView.findViewById<ImageView>(R.id.remote_audio_off)
|
|
||||||
if (!participantDisplayItem.isAudioEnabled) {
|
|
||||||
audioOffView.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
audioOffView.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
val raisedHandView = convertView.findViewById<ImageView>(R.id.raised_hand)
|
|
||||||
if (participantDisplayItem.raisedHand != null && participantDisplayItem.raisedHand.state) {
|
|
||||||
raisedHandView.visibility = View.VISIBLE
|
|
||||||
} else {
|
|
||||||
raisedHandView.visibility = View.GONE
|
|
||||||
}
|
|
||||||
|
|
||||||
return convertView
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("ReturnCount")
|
|
||||||
private fun hasVideoStream(participantDisplayItem: ParticipantDisplayItem, mediaStream: MediaStream?): Boolean {
|
|
||||||
if (!participantDisplayItem.isStreamEnabled) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mediaStream?.videoTracks == null) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
for (t in mediaStream.videoTracks) {
|
|
||||||
if (MediaStreamTrack.State.LIVE == t.state()) {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun scaleGridViewItemHeight(): Int {
|
|
||||||
var headerHeight = 0
|
|
||||||
var callControlsHeight = 0
|
|
||||||
if (callInfosLinearLayout.visibility == View.VISIBLE && isVoiceOnlyCall) {
|
|
||||||
headerHeight = callInfosLinearLayout.height
|
|
||||||
}
|
|
||||||
if (isVoiceOnlyCall) {
|
|
||||||
callControlsHeight = Math.round(mContext.resources.getDimension(R.dimen.call_controls_height))
|
|
||||||
}
|
|
||||||
var itemHeight = (gridViewWrapper.height - headerHeight - callControlsHeight) / getRowsCount(count)
|
|
||||||
val itemMinHeight = Math.round(mContext.resources.getDimension(R.dimen.call_grid_item_min_height))
|
|
||||||
if (itemHeight < itemMinHeight) {
|
|
||||||
itemHeight = itemMinHeight
|
|
||||||
}
|
|
||||||
return itemHeight
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getRowsCount(items: Int): Int {
|
|
||||||
var rows = ceil(items.toDouble() / columns.toDouble()).toInt()
|
|
||||||
if (rows == 0) {
|
|
||||||
rows = 1
|
|
||||||
}
|
|
||||||
return rows
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "ParticipantsAdapter"
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,21 @@
|
|||||||
|
/*
|
||||||
|
* Nextcloud Talk - Android Client
|
||||||
|
*
|
||||||
|
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.nextcloud.talk.call
|
||||||
|
|
||||||
|
import org.webrtc.SurfaceViewRenderer
|
||||||
|
|
||||||
|
data class ParticipantUiState(
|
||||||
|
val sessionKey: String,
|
||||||
|
val nick: String,
|
||||||
|
val isConnected: Boolean,
|
||||||
|
val isAudioEnabled: Boolean,
|
||||||
|
val isStreamEnabled: Boolean,
|
||||||
|
val raisedHand: Boolean,
|
||||||
|
val avatarUrl: String?,
|
||||||
|
val surfaceViewRenderer: SurfaceViewRenderer? = null
|
||||||
|
)
|
@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* Nextcloud Talk - Android Client
|
||||||
|
*
|
||||||
|
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.nextcloud.talk.call.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import com.nextcloud.talk.call.ParticipantUiState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AvatarWithFallback(participant: ParticipantUiState) {
|
||||||
|
if (!participant.avatarUrl.isNullOrEmpty()) {
|
||||||
|
AsyncImage(
|
||||||
|
model = participant.avatarUrl,
|
||||||
|
contentDescription = "Avatar",
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.Gray),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = participant.nick.firstOrNull()?.uppercase() ?: "?",
|
||||||
|
color = Color.White,
|
||||||
|
fontSize = 40.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Nextcloud Talk - Android Client
|
||||||
|
*
|
||||||
|
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.nextcloud.talk.call.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.nextcloud.talk.call.ParticipantUiState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ParticipantGrid(participants: List<ParticipantUiState>, columns: Int = 2, modifier: Modifier = Modifier) {
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(columns),
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
contentPadding = PaddingValues(8.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
items(participants, key = { it.sessionKey }) { participant ->
|
||||||
|
ParticipantTile(participant)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,180 @@
|
|||||||
|
/*
|
||||||
|
* Nextcloud Talk - Android Client
|
||||||
|
*
|
||||||
|
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.nextcloud.talk.call.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.nextcloud.talk.R
|
||||||
|
import com.nextcloud.talk.call.ParticipantUiState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ParticipantTile(participant: ParticipantUiState) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.aspectRatio(3f / 4f)
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(Color.DarkGray)
|
||||||
|
) {
|
||||||
|
if (participant.isStreamEnabled && participant.surfaceViewRenderer != null) {
|
||||||
|
WebRTCVideoView(participant.surfaceViewRenderer)
|
||||||
|
} else {
|
||||||
|
AvatarWithFallback(participant)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (participant.raisedHand) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.ic_hand_back_left),
|
||||||
|
contentDescription = "Raised Hand",
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(6.dp)
|
||||||
|
.size(24.dp),
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!participant.isAudioEnabled) {
|
||||||
|
Icon(
|
||||||
|
painter = painterResource(id = R.drawable.ic_mic_off_white_24px),
|
||||||
|
contentDescription = "Mic Off",
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomEnd)
|
||||||
|
.padding(6.dp)
|
||||||
|
.size(24.dp),
|
||||||
|
tint = Color.White
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(
|
||||||
|
text = participant.nick,
|
||||||
|
color = Color.White,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomStart)
|
||||||
|
.background(Color.Black.copy(alpha = 0.6f))
|
||||||
|
.padding(6.dp),
|
||||||
|
style = MaterialTheme.typography.bodyMedium
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Composable
|
||||||
|
// fun ParticipantItem(participant: ParticipantDisplayItem) {
|
||||||
|
// val context = LocalContext.current
|
||||||
|
// val videoTrack = participant.mediaStream?.videoTracks?.firstOrNull()
|
||||||
|
//
|
||||||
|
// Box(
|
||||||
|
// modifier = Modifier
|
||||||
|
// .aspectRatio(1f)
|
||||||
|
// .background(Color.Black)
|
||||||
|
// .padding(4.dp)
|
||||||
|
// ) {
|
||||||
|
// // Renderer
|
||||||
|
// participant.surfaceViewRenderer?.let { renderer ->
|
||||||
|
// AndroidView(
|
||||||
|
// factory = {
|
||||||
|
// // If not yet initialized
|
||||||
|
// if (renderer.parent != null) {
|
||||||
|
// (renderer.parent as? ViewGroup)?.removeView(renderer)
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // if (!renderer.isInitialized) { // TODO
|
||||||
|
// renderer.init(participant.rootEglBase.eglBaseContext, null)
|
||||||
|
// renderer.setMirror(false)
|
||||||
|
// renderer.setZOrderMediaOverlay(false)
|
||||||
|
// renderer.setEnableHardwareScaler(false)
|
||||||
|
// renderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
|
||||||
|
// // }
|
||||||
|
//
|
||||||
|
// // Attach sink
|
||||||
|
// try {
|
||||||
|
// videoTrack?.removeSink(renderer)
|
||||||
|
// } catch (_: Exception) {}
|
||||||
|
// videoTrack?.addSink(renderer)
|
||||||
|
//
|
||||||
|
// renderer
|
||||||
|
// },
|
||||||
|
// modifier = Modifier.fillMaxSize(),
|
||||||
|
// update = { view ->
|
||||||
|
// view.visibility =
|
||||||
|
// if (videoTrack != null && participant.isConnected) View.VISIBLE else View.INVISIBLE
|
||||||
|
// }
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Overlay: Nick or Avatar
|
||||||
|
// if (videoTrack == null || !participant.isConnected) {
|
||||||
|
// Column(
|
||||||
|
// modifier = Modifier
|
||||||
|
// .fillMaxSize()
|
||||||
|
// .background(Color.DarkGray)
|
||||||
|
// .padding(8.dp),
|
||||||
|
// verticalArrangement = Arrangement.Center,
|
||||||
|
// horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
// ) {
|
||||||
|
// Text(
|
||||||
|
// text = participant.nick!!,
|
||||||
|
// color = Color.White,
|
||||||
|
// fontSize = 16.sp,
|
||||||
|
// modifier = Modifier.padding(bottom = 8.dp)
|
||||||
|
// )
|
||||||
|
// // Replace this with image loader like Coil if needed
|
||||||
|
// Icon(
|
||||||
|
// imageVector = Icons.Default.Person,
|
||||||
|
// contentDescription = null,
|
||||||
|
// tint = Color.White,
|
||||||
|
// modifier = Modifier.size(40.dp)
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Status indicators (audio muted / raised hand)
|
||||||
|
// Row(
|
||||||
|
// modifier = Modifier
|
||||||
|
// .align(Alignment.TopEnd)
|
||||||
|
// .padding(4.dp)
|
||||||
|
// ) {
|
||||||
|
// if (!participant.isAudioEnabled) {
|
||||||
|
// Icon(
|
||||||
|
// painter = painterResource(id = R.drawable.account_circle_96dp),
|
||||||
|
// contentDescription = "Mic Off",
|
||||||
|
// tint = Color.Red,
|
||||||
|
// modifier = Modifier.size(20.dp)
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// if (participant.raisedHand?.state == true) {
|
||||||
|
// Icon(
|
||||||
|
// painter = painterResource(id = R.drawable.ic_hand_back_left),
|
||||||
|
// contentDescription = "Hand Raised",
|
||||||
|
// tint = Color.Yellow,
|
||||||
|
// modifier = Modifier.size(20.dp)
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // Loading spinner
|
||||||
|
// if (!participant.isConnected) {
|
||||||
|
// CircularProgressIndicator(
|
||||||
|
// modifier = Modifier.align(Alignment.Center)
|
||||||
|
// )
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
@ -0,0 +1,23 @@
|
|||||||
|
/*
|
||||||
|
* Nextcloud Talk - Android Client
|
||||||
|
*
|
||||||
|
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.nextcloud.talk.call.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import org.webrtc.SurfaceViewRenderer
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WebRTCVideoView(surfaceViewRenderer: SurfaceViewRenderer) {
|
||||||
|
AndroidView(
|
||||||
|
factory = { surfaceViewRenderer },
|
||||||
|
update = { /* No-op, renderer is already initialized and reused */ },
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
@ -33,15 +33,15 @@
|
|||||||
android:visibility="visible"
|
android:visibility="visible"
|
||||||
tools:visibility="visible">
|
tools:visibility="visible">
|
||||||
|
|
||||||
<GridView
|
<!-- <GridView-->
|
||||||
android:id="@+id/gridview"
|
<!-- android:id="@+id/gridview"-->
|
||||||
android:layout_width="match_parent"
|
<!-- android:layout_width="match_parent"-->
|
||||||
android:layout_height="match_parent"
|
<!-- android:layout_height="match_parent"-->
|
||||||
android:gravity="center"
|
<!-- android:gravity="center"-->
|
||||||
android:numColumns="2"
|
<!-- android:numColumns="2"-->
|
||||||
android:scrollbars="vertical"
|
<!-- android:scrollbars="vertical"-->
|
||||||
android:stretchMode="columnWidth"
|
<!-- android:stretchMode="columnWidth"-->
|
||||||
tools:listitem="@layout/call_item" />
|
<!-- tools:listitem="@layout/call_item" />-->
|
||||||
|
|
||||||
<FrameLayout
|
<FrameLayout
|
||||||
android:id="@+id/selfVideoViewWrapper"
|
android:id="@+id/selfVideoViewWrapper"
|
||||||
@ -368,4 +368,9 @@
|
|||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
|
<androidx.compose.ui.platform.ComposeView
|
||||||
|
android:id="@+id/composeParticipantGrid"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent" />
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
Loading…
Reference in New Issue
Block a user