mirror of
https://github.com/nextcloud/talk-android
synced 2025-06-19 11:39:42 +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.OnTouchListener
|
||||
import android.view.ViewGroup
|
||||
import android.view.ViewTreeObserver.OnGlobalLayoutListener
|
||||
import android.widget.AdapterView
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.DrawableRes
|
||||
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.toColorInt
|
||||
import androidx.core.net.toUri
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import autodagger.AutoInjector
|
||||
import com.bluelinelabs.logansquare.LoganSquare
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import com.nextcloud.talk.R
|
||||
import com.nextcloud.talk.adapters.ParticipantDisplayItem
|
||||
import com.nextcloud.talk.adapters.ParticipantsAdapter
|
||||
import com.nextcloud.talk.api.NcApi
|
||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||
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.MessageSenderNoMcu
|
||||
import com.nextcloud.talk.call.MutableLocalCallParticipantModel
|
||||
import com.nextcloud.talk.call.ParticipantUiState
|
||||
import com.nextcloud.talk.call.ReactionAnimator
|
||||
import com.nextcloud.talk.call.components.ParticipantGrid
|
||||
import com.nextcloud.talk.chat.ChatActivity
|
||||
import com.nextcloud.talk.data.user.model.User
|
||||
import com.nextcloud.talk.databinding.CallActivityBinding
|
||||
@ -152,6 +154,7 @@ import io.reactivex.Observer
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
import kotlinx.coroutines.launch
|
||||
import okhttp3.Cache
|
||||
import org.apache.commons.lang3.StringEscapeUtils
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
@ -303,8 +306,10 @@ class CallActivity : CallBaseActivity() {
|
||||
private var handler: Handler? = null
|
||||
private var currentCallStatus: CallStatus? = 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 audioOutputDialog: AudioOutputDialog? = null
|
||||
private var moreCallActionsDialog: MoreCallActionsDialog? = null
|
||||
@ -399,7 +404,6 @@ class CallActivity : CallBaseActivity() {
|
||||
.setRepeatCount(PulseAnimation.INFINITE)
|
||||
.setRepeatMode(PulseAnimation.REVERSE)
|
||||
callParticipants = HashMap()
|
||||
participantDisplayItems = HashMap()
|
||||
reactionAnimator = ReactionAnimator(context, binding!!.reactionAnimationWrapper, viewThemeUtils)
|
||||
|
||||
checkInitialDevicePermissions()
|
||||
@ -734,10 +738,15 @@ class CallActivity : CallBaseActivity() {
|
||||
}
|
||||
|
||||
binding!!.switchSelfVideoButton.setOnClickListener { switchCamera() }
|
||||
binding!!.gridview.onItemClickListener =
|
||||
AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, _: Long ->
|
||||
animateCallControls(true, 0)
|
||||
}
|
||||
|
||||
// binding!!.composeParticipantGrid.setOnClickListener {
|
||||
// animateCallControls(true, 0)
|
||||
// }
|
||||
|
||||
// binding!!.composeParticipantGrid.onItemClickListener =
|
||||
// AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, _: Long ->
|
||||
// animateCallControls(true, 0)
|
||||
// }
|
||||
binding!!.lowerHandButton.setOnClickListener { l: View? -> raiseHandViewModel!!.lowerHand() }
|
||||
binding!!.pictureInPictureButton.setOnClickListener { enterPipMode() }
|
||||
}
|
||||
@ -890,20 +899,20 @@ class CallActivity : CallBaseActivity() {
|
||||
val callControlsHeight =
|
||||
applicationContext.resources.getDimension(R.dimen.call_controls_height).roundToInt()
|
||||
params.setMargins(0, 0, 0, callControlsHeight)
|
||||
binding!!.gridview.layoutParams = params
|
||||
binding!!.composeParticipantGrid.layoutParams = params
|
||||
} else {
|
||||
val params = RelativeLayout.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
params.setMargins(0, 0, 0, 0)
|
||||
binding!!.gridview.layoutParams = params
|
||||
binding!!.composeParticipantGrid.layoutParams = params
|
||||
if (cameraEnumerator!!.deviceNames.size < 2) {
|
||||
binding!!.switchSelfVideoButton.visibility = View.GONE
|
||||
}
|
||||
initSelfVideoViewForNormalMode()
|
||||
}
|
||||
binding!!.gridview.setOnTouchListener { _, me ->
|
||||
binding!!.composeParticipantGrid.setOnTouchListener { _, me ->
|
||||
val action = me.actionMasked
|
||||
if (action == MotionEvent.ACTION_DOWN) {
|
||||
animateCallControls(true, 0)
|
||||
@ -920,7 +929,7 @@ class CallActivity : CallBaseActivity() {
|
||||
false
|
||||
}
|
||||
animateCallControls(true, 0)
|
||||
initGridAdapter()
|
||||
initGrid()
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
@ -951,56 +960,34 @@ class CallActivity : CallBaseActivity() {
|
||||
localVideoTrack!!.addSink(binding!!.pipSelfVideoRenderer)
|
||||
}
|
||||
|
||||
private fun initGridAdapter() {
|
||||
Log.d(TAG, "initGridAdapter")
|
||||
val columns: Int
|
||||
val participantsInGrid = participantDisplayItems!!.size
|
||||
columns = if (resources != null &&
|
||||
resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT
|
||||
) {
|
||||
if (participantsInGrid > 2) {
|
||||
GRID_MAX_COLUMN_COUNT_PORTRAIT
|
||||
} else {
|
||||
GRID_MIN_COLUMN_COUNT_PORTRAIT
|
||||
private fun initGrid() {
|
||||
Log.d(TAG, "initGrid")
|
||||
|
||||
val participantsInGrid = participantUiStates.size
|
||||
val columns = when {
|
||||
resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT -> {
|
||||
if (participantsInGrid > 2) GRID_MAX_COLUMN_COUNT_PORTRAIT else GRID_MIN_COLUMN_COUNT_PORTRAIT
|
||||
}
|
||||
} else {
|
||||
if (participantsInGrid > 2) {
|
||||
GRID_MAX_COLUMN_COUNT_LANDSCAPE
|
||||
} else if (participantsInGrid > 1) {
|
||||
GRID_MIN_GROUP_COLUMN_COUNT_LANDSCAPE
|
||||
} else {
|
||||
GRID_MIN_COLUMN_COUNT_LANDSCAPE
|
||||
else -> {
|
||||
if (participantsInGrid > 2) {
|
||||
GRID_MAX_COLUMN_COUNT_LANDSCAPE
|
||||
} else if (participantsInGrid > 1) {
|
||||
GRID_MIN_GROUP_COLUMN_COUNT_LANDSCAPE
|
||||
} else {
|
||||
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
|
||||
.viewTreeObserver
|
||||
.addOnGlobalLayoutListener(object : OnGlobalLayoutListener {
|
||||
override fun onGlobalLayout() {
|
||||
binding!!.callInfosLinearLayout.viewTreeObserver.removeOnGlobalLayoutListener(this)
|
||||
}
|
||||
})
|
||||
if (participantsAdapter != null) {
|
||||
participantsAdapter!!.destroy()
|
||||
|
||||
binding!!.composeParticipantGrid.setContent {
|
||||
MaterialTheme {
|
||||
ParticipantGrid(
|
||||
participants = participantUiStates.toList(),
|
||||
columns = columns
|
||||
)
|
||||
}
|
||||
}
|
||||
participantsAdapter = ParticipantsAdapter(
|
||||
this,
|
||||
participantDisplayItems!!.toMap(),
|
||||
binding!!.conversationRelativeLayout,
|
||||
binding!!.callInfosLinearLayout,
|
||||
columns,
|
||||
isVoiceOnlyCall
|
||||
)
|
||||
binding!!.gridview.adapter = participantsAdapter
|
||||
|
||||
if (isInPipMode) {
|
||||
updateUiForPipMode()
|
||||
}
|
||||
@ -2116,6 +2103,7 @@ class CallActivity : CallBaseActivity() {
|
||||
videoCapturer!!.dispose()
|
||||
videoCapturer = null
|
||||
}
|
||||
binding!!.selfVideoRenderer.clearImage()
|
||||
binding!!.selfVideoRenderer.release()
|
||||
if (audioSource != null) {
|
||||
audioSource!!.dispose()
|
||||
@ -2570,19 +2558,42 @@ class CallActivity : CallBaseActivity() {
|
||||
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) {
|
||||
Log.d(TAG, "removeParticipantDisplayItem")
|
||||
val participantDisplayItem = participantDisplayItems!!.remove("$sessionId-$videoStreamType") ?: return
|
||||
participantDisplayItem.destroy()
|
||||
if (!isDestroyed) {
|
||||
initGridAdapter()
|
||||
}
|
||||
val key = "$sessionId-$videoStreamType"
|
||||
|
||||
val participant = participantItems.find { it.sessionKey == key }
|
||||
participant?.destroy()
|
||||
participantItems.removeAll { it.sessionKey == key }
|
||||
|
||||
// Also remove UI state
|
||||
participantUiStates.removeAll { it.sessionKey == key }
|
||||
|
||||
initGrid()
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
fun onMessageEvent(configurationChangeEvent: ConfigurationChangeEvent?) {
|
||||
powerManagerUtils!!.setOrientation(Objects.requireNonNull(resources).configuration.orientation)
|
||||
initGridAdapter()
|
||||
initGrid()
|
||||
}
|
||||
|
||||
private fun updateSelfVideoViewIceConnectionState(iceConnectionState: IceConnectionState) {
|
||||
@ -2677,22 +2688,37 @@ class CallActivity : CallBaseActivity() {
|
||||
}
|
||||
|
||||
private fun addParticipantDisplayItem(callParticipantModel: CallParticipantModel, videoStreamType: String) {
|
||||
if (callParticipantModel.isInternal != null && callParticipantModel.isInternal) {
|
||||
return
|
||||
}
|
||||
if (callParticipantModel.isInternal == true) return
|
||||
|
||||
val defaultGuestNick = resources.getString(R.string.nc_nick_guest)
|
||||
val participantDisplayItem = ParticipantDisplayItem(
|
||||
context,
|
||||
baseUrl,
|
||||
defaultGuestNick,
|
||||
rootEglBase,
|
||||
videoStreamType,
|
||||
roomToken,
|
||||
callParticipantModel
|
||||
context = context,
|
||||
baseUrl = baseUrl!!,
|
||||
defaultGuestNick = defaultGuestNick,
|
||||
rootEglBase = rootEglBase!!,
|
||||
streamType = videoStreamType,
|
||||
roomToken = roomToken!!,
|
||||
callParticipantModel = callParticipantModel
|
||||
)
|
||||
val sessionId = callParticipantModel.sessionId
|
||||
participantDisplayItems!!["$sessionId-$videoStreamType"] = participantDisplayItem
|
||||
initGridAdapter()
|
||||
|
||||
val sessionKey = participantDisplayItem.sessionKey
|
||||
|
||||
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) {
|
||||
@ -2725,7 +2751,7 @@ class CallActivity : CallBaseActivity() {
|
||||
binding!!.callModeTextView.text = descriptionForCallType
|
||||
binding!!.callStates.callStateTextView.setText(R.string.nc_leaving_call)
|
||||
binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE
|
||||
binding!!.gridview.visibility = View.INVISIBLE
|
||||
binding!!.composeParticipantGrid.visibility = View.INVISIBLE
|
||||
binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE
|
||||
binding!!.callStates.errorImageView.visibility = View.GONE
|
||||
}
|
||||
@ -2737,8 +2763,8 @@ class CallActivity : CallBaseActivity() {
|
||||
if (binding!!.callStates.callStateRelativeLayout.visibility != View.VISIBLE) {
|
||||
binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE
|
||||
}
|
||||
if (binding!!.gridview.visibility != View.INVISIBLE) {
|
||||
binding!!.gridview.visibility = View.INVISIBLE
|
||||
if (binding!!.composeParticipantGrid.visibility != View.INVISIBLE) {
|
||||
binding!!.composeParticipantGrid.visibility = View.INVISIBLE
|
||||
}
|
||||
if (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) {
|
||||
binding!!.callStates.callStateProgressBar.visibility = View.GONE
|
||||
}
|
||||
if (binding!!.gridview.visibility != View.VISIBLE) {
|
||||
binding!!.gridview.visibility = View.VISIBLE
|
||||
if (binding!!.composeParticipantGrid.visibility != View.VISIBLE) {
|
||||
binding!!.composeParticipantGrid.visibility = View.VISIBLE
|
||||
}
|
||||
if (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) {
|
||||
binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE
|
||||
}
|
||||
if (binding!!.gridview.visibility != View.INVISIBLE) {
|
||||
binding!!.gridview.visibility = View.INVISIBLE
|
||||
if (binding!!.composeParticipantGrid.visibility != View.INVISIBLE) {
|
||||
binding!!.composeParticipantGrid.visibility = View.INVISIBLE
|
||||
}
|
||||
if (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) {
|
||||
binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE
|
||||
}
|
||||
if (binding!!.gridview.visibility != View.INVISIBLE) {
|
||||
binding!!.gridview.visibility = View.INVISIBLE
|
||||
if (binding!!.composeParticipantGrid.visibility != View.INVISIBLE) {
|
||||
binding!!.composeParticipantGrid.visibility = View.INVISIBLE
|
||||
}
|
||||
if (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) {
|
||||
binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE
|
||||
}
|
||||
if (binding!!.gridview.visibility != View.INVISIBLE) {
|
||||
binding!!.gridview.visibility = View.INVISIBLE
|
||||
if (binding!!.composeParticipantGrid.visibility != View.INVISIBLE) {
|
||||
binding!!.composeParticipantGrid.visibility = View.INVISIBLE
|
||||
}
|
||||
if (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) {
|
||||
binding!!.callStates.callStateProgressBar.visibility = View.GONE
|
||||
}
|
||||
if (binding!!.gridview.visibility != View.INVISIBLE) {
|
||||
binding!!.gridview.visibility = View.INVISIBLE
|
||||
if (binding!!.composeParticipantGrid.visibility != View.INVISIBLE) {
|
||||
binding!!.composeParticipantGrid.visibility = View.INVISIBLE
|
||||
}
|
||||
binding!!.callStates.errorImageView.setImageResource(R.drawable.ic_av_timer_timer_24dp)
|
||||
if (binding!!.callStates.errorImageView.visibility != View.VISIBLE) {
|
||||
@ -2860,8 +2886,8 @@ class CallActivity : CallBaseActivity() {
|
||||
if (binding!!.callStates.callStateRelativeLayout.visibility != View.VISIBLE) {
|
||||
binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE
|
||||
}
|
||||
if (binding!!.gridview.visibility != View.INVISIBLE) {
|
||||
binding!!.gridview.visibility = View.INVISIBLE
|
||||
if (binding!!.composeParticipantGrid.visibility != View.INVISIBLE) {
|
||||
binding!!.composeParticipantGrid.visibility = View.INVISIBLE
|
||||
}
|
||||
if (binding!!.callStates.callStateProgressBar.visibility != View.VISIBLE) {
|
||||
binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE
|
||||
@ -3021,8 +3047,8 @@ class CallActivity : CallBaseActivity() {
|
||||
removeParticipantDisplayItem(sessionId, "screen")
|
||||
return
|
||||
}
|
||||
val hasScreenParticipantDisplayItem = participantDisplayItems!!["$sessionId-screen"] != null
|
||||
if (!hasScreenParticipantDisplayItem) {
|
||||
val screenParticipantDisplayItem = participantItems.find { it.sessionKey == "$sessionId-screen" }
|
||||
if (screenParticipantDisplayItem != null) {
|
||||
addParticipantDisplayItem(callParticipantModel, "screen")
|
||||
}
|
||||
}
|
||||
@ -3230,14 +3256,15 @@ class CallActivity : CallBaseActivity() {
|
||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||
)
|
||||
params.setMargins(0, 0, 0, 0)
|
||||
binding!!.gridview.layoutParams = params
|
||||
binding!!.composeParticipantGrid.layoutParams = params
|
||||
binding!!.callControls.visibility = View.GONE
|
||||
binding!!.callInfosLinearLayout.visibility = View.GONE
|
||||
binding!!.selfVideoViewWrapper.visibility = View.GONE
|
||||
binding!!.callStates.callStateRelativeLayout.visibility = View.GONE
|
||||
|
||||
binding!!.selfVideoRenderer.clearImage()
|
||||
binding!!.selfVideoRenderer.release()
|
||||
if (participantDisplayItems!!.size > 1) {
|
||||
if (participantItems.size > 1) {
|
||||
binding!!.pipCallConversationNameTextView.text = conversationName
|
||||
binding!!.pipSelfVideoOverlay.visibility = View.VISIBLE
|
||||
initSelfVideoViewForPipMode()
|
||||
|
@ -3,201 +3,201 @@
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2023 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* 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
|
||||
*/
|
||||
package com.nextcloud.talk.adapters;
|
||||
package com.nextcloud.talk.adapters
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.text.TextUtils;
|
||||
import android.content.Context
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
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;
|
||||
import com.nextcloud.talk.call.RaisedHand;
|
||||
import com.nextcloud.talk.models.json.participants.Participant;
|
||||
import com.nextcloud.talk.utils.ApiUtils;
|
||||
import com.nextcloud.talk.utils.DisplayUtils;
|
||||
class ParticipantDisplayItem(
|
||||
private val context: Context,
|
||||
private val baseUrl: String,
|
||||
private val defaultGuestNick: String,
|
||||
val rootEglBase: EglBase,
|
||||
private val streamType: String,
|
||||
private val roomToken: String,
|
||||
private val callParticipantModel: CallParticipantModel
|
||||
) {
|
||||
private val participantDisplayItemNotifier = ParticipantDisplayItemNotifier()
|
||||
|
||||
import org.webrtc.EglBase;
|
||||
import org.webrtc.MediaStream;
|
||||
import org.webrtc.PeerConnection;
|
||||
private val _uiStateFlow = MutableStateFlow(buildUiState())
|
||||
val uiStateFlow: StateFlow<ParticipantUiState> = _uiStateFlow.asStateFlow()
|
||||
|
||||
public class ParticipantDisplayItem {
|
||||
private val session: String = callParticipantModel.sessionId
|
||||
|
||||
/**
|
||||
* Shared handler to receive change notifications from the model on the main thread.
|
||||
*/
|
||||
private static final Handler handler = new Handler(Looper.getMainLooper());
|
||||
var actorType: ActorType? = null
|
||||
private set
|
||||
private var actorId: String? = null
|
||||
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;
|
||||
private final String defaultGuestNick;
|
||||
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();
|
||||
interface Observer {
|
||||
fun onChange()
|
||||
}
|
||||
|
||||
private final CallParticipantModel.Observer callParticipantModelObserver = new CallParticipantModel.Observer() {
|
||||
@Override
|
||||
public void onChange() {
|
||||
updateFromModel();
|
||||
private val callParticipantModelObserver: CallParticipantModel.Observer = object : CallParticipantModel.Observer {
|
||||
override fun onChange() {
|
||||
updateFromModel()
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onReaction(String reaction) {
|
||||
override fun onReaction(reaction: String) {
|
||||
}
|
||||
};
|
||||
|
||||
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() {
|
||||
this.callParticipantModel.removeObserver(callParticipantModelObserver);
|
||||
init {
|
||||
callParticipantModel.addObserver(callParticipantModelObserver, handler)
|
||||
|
||||
updateFromModel()
|
||||
}
|
||||
|
||||
private void updateFromModel() {
|
||||
actorType = callParticipantModel.getActorType();
|
||||
actorId = callParticipantModel.getActorId();
|
||||
userId = callParticipantModel.getUserId();
|
||||
nick = callParticipantModel.getNick();
|
||||
fun destroy() {
|
||||
callParticipantModel.removeObserver(callParticipantModelObserver)
|
||||
|
||||
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)) {
|
||||
iceConnectionState = callParticipantModel.getScreenIceConnectionState();
|
||||
mediaStream = callParticipantModel.getScreenMediaStream();
|
||||
isAudioEnabled = true;
|
||||
streamEnabled = true;
|
||||
private fun updateFromModel() {
|
||||
actorType = callParticipantModel.actorType
|
||||
actorId = callParticipantModel.actorId
|
||||
userId = callParticipantModel.userId
|
||||
nick = callParticipantModel.nick
|
||||
|
||||
updateUrlForAvatar()
|
||||
|
||||
if (streamType == "screen") {
|
||||
iceConnectionState = callParticipantModel.screenIceConnectionState
|
||||
mediaStream = callParticipantModel.screenMediaStream
|
||||
isAudioEnabled = true
|
||||
isStreamEnabled = true
|
||||
} else {
|
||||
iceConnectionState = callParticipantModel.getIceConnectionState();
|
||||
mediaStream = callParticipantModel.getMediaStream();
|
||||
isAudioEnabled = callParticipantModel.isAudioAvailable() != null ?
|
||||
callParticipantModel.isAudioAvailable() : false;
|
||||
streamEnabled = callParticipantModel.isVideoAvailable() != null ?
|
||||
callParticipantModel.isVideoAvailable() : false;
|
||||
iceConnectionState = callParticipantModel.iceConnectionState
|
||||
mediaStream = callParticipantModel.mediaStream
|
||||
isAudioEnabled = callParticipantModel.isAudioAvailable ?: false
|
||||
isStreamEnabled = 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)
|
||||
}
|
||||
|
||||
_uiStateFlow.value = buildUiState()
|
||||
participantDisplayItemNotifier.notifyChange()
|
||||
}
|
||||
|
||||
private void updateUrlForAvatar() {
|
||||
if (actorType == Participant.ActorType.FEDERATED) {
|
||||
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)) {
|
||||
urlForAvatar = ApiUtils.getUrlForAvatar(baseUrl, userId, true);
|
||||
urlForAvatar = getUrlForAvatar(baseUrl, userId, true)
|
||||
} else {
|
||||
urlForAvatar = ApiUtils.getUrlForGuestAvatar(baseUrl, getNick(), true);
|
||||
urlForAvatar = getUrlForGuestAvatar(baseUrl, nick, true)
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isConnected() {
|
||||
return iceConnectionState == PeerConnection.IceConnectionState.CONNECTED ||
|
||||
iceConnectionState == PeerConnection.IceConnectionState.COMPLETED ||
|
||||
// If there is no connection state that means that no connection is needed, so it is a special case that is
|
||||
// also seen as "connected".
|
||||
iceConnectionState == null;
|
||||
val isConnected: Boolean
|
||||
get() = iceConnectionState == IceConnectionState.CONNECTED ||
|
||||
iceConnectionState == IceConnectionState.COMPLETED ||
|
||||
// If there is no connection state that means that no connection is needed,
|
||||
// so it is a special case that is also seen as "connected".
|
||||
iceConnectionState == null
|
||||
|
||||
fun addObserver(observer: Observer?) {
|
||||
participantDisplayItemNotifier.addObserver(observer)
|
||||
}
|
||||
|
||||
public String getNick() {
|
||||
if (TextUtils.isEmpty(userId) && TextUtils.isEmpty(nick)) {
|
||||
return defaultGuestNick;
|
||||
}
|
||||
|
||||
return nick;
|
||||
fun removeObserver(observer: Observer?) {
|
||||
participantDisplayItemNotifier.removeObserver(observer)
|
||||
}
|
||||
|
||||
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() {
|
||||
override fun toString(): String {
|
||||
return "ParticipantSession{" +
|
||||
"userId='" + userId + '\'' +
|
||||
", actorType='" + actorType + '\'' +
|
||||
", actorId='" + actorId + '\'' +
|
||||
", session='" + session + '\'' +
|
||||
", nick='" + nick + '\'' +
|
||||
", urlForAvatar='" + urlForAvatar + '\'' +
|
||||
", mediaStream=" + mediaStream +
|
||||
", streamType='" + streamType + '\'' +
|
||||
", streamEnabled=" + streamEnabled +
|
||||
", rootEglBase=" + rootEglBase +
|
||||
", raisedHand=" + raisedHand +
|
||||
'}';
|
||||
"userId='" + userId + '\'' +
|
||||
", actorType='" + actorType + '\'' +
|
||||
", actorId='" + actorId + '\'' +
|
||||
", session='" + session + '\'' +
|
||||
", nick='" + nick + '\'' +
|
||||
", urlForAvatar='" + urlForAvatar + '\'' +
|
||||
", mediaStream=" + mediaStream +
|
||||
", streamType='" + streamType + '\'' +
|
||||
", streamEnabled=" + isStreamEnabled +
|
||||
", rootEglBase=" + rootEglBase +
|
||||
", 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"
|
||||
tools:visibility="visible">
|
||||
|
||||
<GridView
|
||||
android:id="@+id/gridview"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:gravity="center"
|
||||
android:numColumns="2"
|
||||
android:scrollbars="vertical"
|
||||
android:stretchMode="columnWidth"
|
||||
tools:listitem="@layout/call_item" />
|
||||
<!-- <GridView-->
|
||||
<!-- android:id="@+id/gridview"-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="match_parent"-->
|
||||
<!-- android:gravity="center"-->
|
||||
<!-- android:numColumns="2"-->
|
||||
<!-- android:scrollbars="vertical"-->
|
||||
<!-- android:stretchMode="columnWidth"-->
|
||||
<!-- tools:listitem="@layout/call_item" />-->
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/selfVideoViewWrapper"
|
||||
@ -368,4 +368,9 @@
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<androidx.compose.ui.platform.ComposeView
|
||||
android:id="@+id/composeParticipantGrid"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
Loading…
Reference in New Issue
Block a user