mirror of
https://github.com/nextcloud/talk-android
synced 2025-06-19 11:39:42 +01:00
Merge pull request #4947 from nextcloud/improveCallGrid
Improve call grid & Picture-inPicture view
This commit is contained in:
commit
f8bfa0485c
@ -42,13 +42,14 @@ 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.collectAsState
|
||||||
|
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
|
||||||
@ -59,7 +60,6 @@ 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
|
||||||
@ -74,6 +74,7 @@ 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.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
|
||||||
@ -303,8 +304,8 @@ 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 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 +400,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 +734,7 @@ class CallActivity : CallBaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding!!.switchSelfVideoButton.setOnClickListener { switchCamera() }
|
binding!!.switchSelfVideoButton.setOnClickListener { switchCamera() }
|
||||||
binding!!.gridview.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 +887,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 +917,8 @@ class CallActivity : CallBaseActivity() {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
animateCallControls(true, 0)
|
animateCallControls(true, 0)
|
||||||
initGridAdapter()
|
initGrid()
|
||||||
|
binding!!.composeParticipantGrid.z = 0f
|
||||||
}
|
}
|
||||||
|
|
||||||
@SuppressLint("ClickableViewAccessibility")
|
@SuppressLint("ClickableViewAccessibility")
|
||||||
@ -935,72 +933,28 @@ class CallActivity : CallBaseActivity() {
|
|||||||
binding!!.selfVideoRenderer.setEnableHardwareScaler(false)
|
binding!!.selfVideoRenderer.setEnableHardwareScaler(false)
|
||||||
binding!!.selfVideoRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
|
binding!!.selfVideoRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
|
||||||
binding!!.selfVideoRenderer.setOnTouchListener(SelfVideoTouchListener())
|
binding!!.selfVideoRenderer.setOnTouchListener(SelfVideoTouchListener())
|
||||||
|
|
||||||
|
binding!!.pipSelfVideoRenderer.clearImage()
|
||||||
|
binding!!.pipSelfVideoRenderer.release()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initSelfVideoViewForPipMode() {
|
private fun initGrid() {
|
||||||
try {
|
Log.d(TAG, "initGrid")
|
||||||
binding!!.pipSelfVideoRenderer.init(rootEglBase!!.eglBaseContext, null)
|
binding!!.composeParticipantGrid.visibility = View.VISIBLE
|
||||||
} catch (e: IllegalStateException) {
|
binding!!.composeParticipantGrid.setContent {
|
||||||
Log.d(TAG, "pipGroupVideoRenderer already initialized", e)
|
MaterialTheme {
|
||||||
}
|
val participantUiStates = participantItems.map { it.uiStateFlow.collectAsState().value }
|
||||||
binding!!.pipSelfVideoRenderer.setZOrderMediaOverlay(true)
|
ParticipantGrid(
|
||||||
// disabled because it causes some devices to crash
|
participantUiStates = participantUiStates,
|
||||||
binding!!.pipSelfVideoRenderer.setEnableHardwareScaler(false)
|
eglBase = rootEglBase!!,
|
||||||
binding!!.pipSelfVideoRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
|
isVoiceOnlyCall = isVoiceOnlyCall,
|
||||||
|
isInPipMode = isInPipMode
|
||||||
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) {
|
animateCallControls(true, 0)
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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()
|
|
||||||
}
|
|
||||||
participantsAdapter = ParticipantsAdapter(
|
|
||||||
this,
|
|
||||||
participantDisplayItems,
|
|
||||||
binding!!.conversationRelativeLayout,
|
|
||||||
binding!!.callInfosLinearLayout,
|
|
||||||
columns,
|
|
||||||
isVoiceOnlyCall
|
|
||||||
)
|
|
||||||
binding!!.gridview.adapter = participantsAdapter
|
|
||||||
if (isInPipMode) {
|
if (isInPipMode) {
|
||||||
updateUiForPipMode()
|
updateUiForPipMode()
|
||||||
}
|
}
|
||||||
@ -2116,7 +2070,11 @@ class CallActivity : CallBaseActivity() {
|
|||||||
videoCapturer!!.dispose()
|
videoCapturer!!.dispose()
|
||||||
videoCapturer = null
|
videoCapturer = null
|
||||||
}
|
}
|
||||||
|
binding!!.selfVideoRenderer.clearImage()
|
||||||
binding!!.selfVideoRenderer.release()
|
binding!!.selfVideoRenderer.release()
|
||||||
|
|
||||||
|
binding!!.pipSelfVideoRenderer.clearImage()
|
||||||
|
binding!!.pipSelfVideoRenderer.release()
|
||||||
if (audioSource != null) {
|
if (audioSource != null) {
|
||||||
audioSource!!.dispose()
|
audioSource!!.dispose()
|
||||||
audioSource = null
|
audioSource = null
|
||||||
@ -2219,6 +2177,7 @@ class CallActivity : CallBaseActivity() {
|
|||||||
startVideoCapture(true)
|
startVideoCapture(true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
in ANGLE_LANDSCAPE_RIGHT_THRESHOLD_MIN..ANGLE_LANDSCAPE_RIGHT_THRESHOLD_MAX,
|
in ANGLE_LANDSCAPE_RIGHT_THRESHOLD_MIN..ANGLE_LANDSCAPE_RIGHT_THRESHOLD_MAX,
|
||||||
in ANGLE_LANDSCAPE_LEFT_THRESHOLD_MIN..ANGLE_LANDSCAPE_LEFT_THRESHOLD_MAX -> {
|
in ANGLE_LANDSCAPE_LEFT_THRESHOLD_MIN..ANGLE_LANDSCAPE_LEFT_THRESHOLD_MAX -> {
|
||||||
if (lastAspectRatio != RATIO_16_TO_9) {
|
if (lastAspectRatio != RATIO_16_TO_9) {
|
||||||
@ -2571,18 +2530,17 @@ class CallActivity : CallBaseActivity() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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
|
val participant = participantItems.find { it.sessionKey == key }
|
||||||
participantDisplayItem.destroy()
|
participant?.destroy()
|
||||||
if (!isDestroyed) {
|
participantItems.removeAll { it.sessionKey == key }
|
||||||
initGridAdapter()
|
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 +2635,26 @@ 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
initGrid()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun setCallState(callState: CallStatus) {
|
private fun setCallState(callState: CallStatus) {
|
||||||
@ -2712,6 +2674,7 @@ class CallActivity : CallBaseActivity() {
|
|||||||
handler!!.postDelayed({ setCallState(CallStatus.CALLING_TIMEOUT) }, CALLING_TIMEOUT)
|
handler!!.postDelayed({ setCallState(CallStatus.CALLING_TIMEOUT) }, CALLING_TIMEOUT)
|
||||||
handler!!.post { handleCallStateJoined() }
|
handler!!.post { handleCallStateJoined() }
|
||||||
}
|
}
|
||||||
|
|
||||||
CallStatus.IN_CONVERSATION -> handler!!.post { handleCallStateInConversation() }
|
CallStatus.IN_CONVERSATION -> handler!!.post { handleCallStateInConversation() }
|
||||||
CallStatus.OFFLINE -> handler!!.post { handleCallStateOffline() }
|
CallStatus.OFFLINE -> handler!!.post { handleCallStateOffline() }
|
||||||
CallStatus.LEAVING -> handler!!.post { handleCallStateLeaving() }
|
CallStatus.LEAVING -> handler!!.post { handleCallStateLeaving() }
|
||||||
@ -2725,7 +2688,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 +2700,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 +2727,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 +2748,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 +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.VISIBLE) {
|
if (binding!!.callStates.callStateProgressBar.visibility != View.VISIBLE) {
|
||||||
binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE
|
binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE
|
||||||
@ -2818,8 +2781,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 +2802,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 +2823,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 +2984,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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3225,30 +3188,46 @@ class CallActivity : CallBaseActivity() {
|
|||||||
|
|
||||||
override fun updateUiForPipMode() {
|
override fun updateUiForPipMode() {
|
||||||
Log.d(TAG, "updateUiForPipMode")
|
Log.d(TAG, "updateUiForPipMode")
|
||||||
val params = RelativeLayout.LayoutParams(
|
|
||||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
|
||||||
ViewGroup.LayoutParams.WRAP_CONTENT
|
|
||||||
)
|
|
||||||
params.setMargins(0, 0, 0, 0)
|
|
||||||
binding!!.gridview.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.release()
|
|
||||||
if (participantDisplayItems!!.size > 1) {
|
|
||||||
binding!!.pipCallConversationNameTextView.text = conversationName
|
binding!!.pipCallConversationNameTextView.text = conversationName
|
||||||
binding!!.pipSelfVideoOverlay.visibility = View.VISIBLE
|
|
||||||
initSelfVideoViewForPipMode()
|
binding!!.selfVideoRenderer.clearImage()
|
||||||
|
binding!!.selfVideoRenderer.release()
|
||||||
|
|
||||||
|
if (participantItems.size == 1) {
|
||||||
|
binding!!.pipOverlay.visibility = View.GONE
|
||||||
} else {
|
} else {
|
||||||
binding!!.pipSelfVideoOverlay.visibility = View.GONE
|
binding!!.composeParticipantGrid.visibility = View.GONE
|
||||||
|
|
||||||
|
if (localVideoTrack?.enabled() == true) {
|
||||||
|
binding!!.pipOverlay.visibility = View.VISIBLE
|
||||||
|
binding!!.pipSelfVideoRenderer.visibility = View.VISIBLE
|
||||||
|
|
||||||
|
try {
|
||||||
|
binding!!.pipSelfVideoRenderer.init(rootEglBase!!.eglBaseContext, null)
|
||||||
|
} catch (e: IllegalStateException) {
|
||||||
|
Log.d(TAG, "pipGroupVideoRenderer already initialized", e)
|
||||||
|
}
|
||||||
|
binding!!.pipSelfVideoRenderer.setZOrderMediaOverlay(true)
|
||||||
|
// disabled because it causes some devices to crash
|
||||||
|
binding!!.pipSelfVideoRenderer.setEnableHardwareScaler(false)
|
||||||
|
binding!!.pipSelfVideoRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT)
|
||||||
|
|
||||||
|
localVideoTrack?.addSink(binding?.pipSelfVideoRenderer)
|
||||||
|
} else {
|
||||||
|
binding!!.pipOverlay.visibility = View.VISIBLE
|
||||||
|
binding!!.pipSelfVideoRenderer.visibility = View.GONE
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun updateUiForNormalMode() {
|
override fun updateUiForNormalMode() {
|
||||||
Log.d(TAG, "updateUiForNormalMode")
|
Log.d(TAG, "updateUiForNormalMode")
|
||||||
binding!!.pipSelfVideoOverlay.visibility = View.GONE
|
binding!!.pipOverlay.visibility = View.GONE
|
||||||
|
binding!!.composeParticipantGrid.visibility = View.VISIBLE
|
||||||
|
|
||||||
if (isVoiceOnlyCall) {
|
if (isVoiceOnlyCall) {
|
||||||
binding!!.callControls.visibility = View.VISIBLE
|
binding!!.callControls.visibility = View.VISIBLE
|
||||||
@ -3353,10 +3332,10 @@ class CallActivity : CallBaseActivity() {
|
|||||||
private const val SELFVIDEO_WIDTH_16_TO_9_RATIO = 136
|
private const val SELFVIDEO_WIDTH_16_TO_9_RATIO = 136
|
||||||
private const val SELFVIDEO_HEIGHT_16_TO_9_RATIO = 80
|
private const val SELFVIDEO_HEIGHT_16_TO_9_RATIO = 80
|
||||||
|
|
||||||
private const val SELFVIDEO_POSITION_X_LANDSCAPE = 100F
|
private const val SELFVIDEO_POSITION_X_LANDSCAPE = 50F
|
||||||
private const val SELFVIDEO_POSITION_Y_LANDSCAPE = 100F
|
private const val SELFVIDEO_POSITION_Y_LANDSCAPE = 50F
|
||||||
private const val SELFVIDEO_POSITION_X_PORTRAIT = 300F
|
private const val SELFVIDEO_POSITION_X_PORTRAIT = 300F
|
||||||
private const val SELFVIDEO_POSITION_Y_PORTRAIT = 100F
|
private const val SELFVIDEO_POSITION_Y_PORTRAIT = 50F
|
||||||
|
|
||||||
private const val FIVE_SECONDS: Long = 5000
|
private const val FIVE_SECONDS: Long = 5000
|
||||||
private const val CALLING_TIMEOUT: Long = 45000
|
private const val CALLING_TIMEOUT: Long = 45000
|
||||||
@ -3368,20 +3347,8 @@ class CallActivity : CallBaseActivity() {
|
|||||||
private const val SPOTLIGHT_HEADING_SIZE: Int = 20
|
private const val SPOTLIGHT_HEADING_SIZE: Int = 20
|
||||||
private const val SPOTLIGHT_SUBHEADING_SIZE: Int = 16
|
private const val SPOTLIGHT_SUBHEADING_SIZE: Int = 16
|
||||||
|
|
||||||
private const val GRID_MAX_COLUMN_COUNT_PORTRAIT: Int = 2
|
|
||||||
private const val GRID_MIN_COLUMN_COUNT_PORTRAIT: Int = 1
|
|
||||||
private const val GRID_MAX_COLUMN_COUNT_LANDSCAPE: Int = 3
|
|
||||||
private const val GRID_MIN_GROUP_COLUMN_COUNT_LANDSCAPE: Int = 2
|
|
||||||
private const val GRID_MIN_COLUMN_COUNT_LANDSCAPE: Int = 1
|
|
||||||
|
|
||||||
private const val DELAY_ON_ERROR_STOP_THRESHOLD: Int = 16
|
private const val DELAY_ON_ERROR_STOP_THRESHOLD: Int = 16
|
||||||
|
|
||||||
private const val BY_50_PERCENT = 0.5
|
|
||||||
private const val BY_80_PERCENT = 0.8
|
|
||||||
|
|
||||||
private const val Y_POS_CALL_INFO: Float = 250f
|
|
||||||
private const val Y_POS_NO_CALL_INFO: Float = 20f
|
|
||||||
|
|
||||||
private const val SESSION_ID_PREFFIX_END: Int = 4
|
private const val SESSION_ID_PREFFIX_END: Int = 4
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,203 +0,0 @@
|
|||||||
/*
|
|
||||||
* Nextcloud Talk - Android Client
|
|
||||||
*
|
|
||||||
* 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-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
package com.nextcloud.talk.adapters;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.Looper;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
import org.webrtc.EglBase;
|
|
||||||
import org.webrtc.MediaStream;
|
|
||||||
import org.webrtc.PeerConnection;
|
|
||||||
|
|
||||||
public class ParticipantDisplayItem {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Shared handler to receive change notifications from the model on the main thread.
|
|
||||||
*/
|
|
||||||
private static final Handler handler = new Handler(Looper.getMainLooper());
|
|
||||||
|
|
||||||
private final ParticipantDisplayItemNotifier participantDisplayItemNotifier = new ParticipantDisplayItemNotifier();
|
|
||||||
|
|
||||||
private final Context context;
|
|
||||||
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
private final CallParticipantModel.Observer callParticipantModelObserver = new CallParticipantModel.Observer() {
|
|
||||||
@Override
|
|
||||||
public void onChange() {
|
|
||||||
updateFromModel();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
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() {
|
|
||||||
this.callParticipantModel.removeObserver(callParticipantModelObserver);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateFromModel() {
|
|
||||||
actorType = callParticipantModel.getActorType();
|
|
||||||
actorId = callParticipantModel.getActorId();
|
|
||||||
userId = callParticipantModel.getUserId();
|
|
||||||
nick = callParticipantModel.getNick();
|
|
||||||
|
|
||||||
this.updateUrlForAvatar();
|
|
||||||
|
|
||||||
if ("screen".equals(streamType)) {
|
|
||||||
iceConnectionState = callParticipantModel.getScreenIceConnectionState();
|
|
||||||
mediaStream = callParticipantModel.getScreenMediaStream();
|
|
||||||
isAudioEnabled = true;
|
|
||||||
streamEnabled = true;
|
|
||||||
} else {
|
|
||||||
iceConnectionState = callParticipantModel.getIceConnectionState();
|
|
||||||
mediaStream = callParticipantModel.getMediaStream();
|
|
||||||
isAudioEnabled = callParticipantModel.isAudioAvailable() != null ?
|
|
||||||
callParticipantModel.isAudioAvailable() : false;
|
|
||||||
streamEnabled = callParticipantModel.isVideoAvailable() != null ?
|
|
||||||
callParticipantModel.isVideoAvailable() : false;
|
|
||||||
}
|
|
||||||
|
|
||||||
raisedHand = callParticipantModel.getRaisedHand();
|
|
||||||
|
|
||||||
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);
|
|
||||||
} else if (!TextUtils.isEmpty(userId)) {
|
|
||||||
urlForAvatar = ApiUtils.getUrlForAvatar(baseUrl, userId, true);
|
|
||||||
} else {
|
|
||||||
urlForAvatar = ApiUtils.getUrlForGuestAvatar(baseUrl, getNick(), 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public String getNick() {
|
|
||||||
if (TextUtils.isEmpty(userId) && TextUtils.isEmpty(nick)) {
|
|
||||||
return defaultGuestNick;
|
|
||||||
}
|
|
||||||
|
|
||||||
return nick;
|
|
||||||
}
|
|
||||||
|
|
||||||
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{" +
|
|
||||||
"userId='" + userId + '\'' +
|
|
||||||
", actorType='" + actorType + '\'' +
|
|
||||||
", actorId='" + actorId + '\'' +
|
|
||||||
", session='" + session + '\'' +
|
|
||||||
", nick='" + nick + '\'' +
|
|
||||||
", urlForAvatar='" + urlForAvatar + '\'' +
|
|
||||||
", mediaStream=" + mediaStream +
|
|
||||||
", streamType='" + streamType + '\'' +
|
|
||||||
", streamEnabled=" + streamEnabled +
|
|
||||||
", rootEglBase=" + rootEglBase +
|
|
||||||
", raisedHand=" + raisedHand +
|
|
||||||
'}';
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,217 @@
|
|||||||
|
/*
|
||||||
|
* Nextcloud Talk - Android Client
|
||||||
|
*
|
||||||
|
* SPDX-FileCopyrightText: 2023 Andy Scherzinger <info@andy-scherzinger.de>
|
||||||
|
* SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez <danxuliu@gmail.com>
|
||||||
|
* SPDX-FileCopyrightText: 2021-2025 Marcel Hibbe <dev@mhibbe.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
package com.nextcloud.talk.adapters
|
||||||
|
|
||||||
|
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.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
|
||||||
|
|
||||||
|
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 mediaStream: MediaStream?
|
||||||
|
)
|
||||||
|
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
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()
|
||||||
|
|
||||||
|
private val _uiStateFlow = MutableStateFlow(buildUiState())
|
||||||
|
val uiStateFlow: StateFlow<ParticipantUiState> = _uiStateFlow.asStateFlow()
|
||||||
|
|
||||||
|
private val session: String = callParticipantModel.sessionId
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
val sessionKey: String
|
||||||
|
get() = "$session-$streamType"
|
||||||
|
|
||||||
|
interface Observer {
|
||||||
|
fun onChange()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val callParticipantModelObserver: CallParticipantModel.Observer = object : CallParticipantModel.Observer {
|
||||||
|
override fun onChange() {
|
||||||
|
updateFromModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onReaction(reaction: String) {
|
||||||
|
// unused
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
callParticipantModel.addObserver(callParticipantModelObserver, handler)
|
||||||
|
|
||||||
|
updateFromModel()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("Detekt.TooGenericExceptionCaught")
|
||||||
|
fun destroy() {
|
||||||
|
callParticipantModel.removeObserver(callParticipantModelObserver)
|
||||||
|
|
||||||
|
surfaceViewRenderer?.let { renderer ->
|
||||||
|
try {
|
||||||
|
mediaStream?.videoTracks?.firstOrNull()?.removeSink(renderer)
|
||||||
|
renderer.clearImage()
|
||||||
|
renderer.release()
|
||||||
|
(renderer.parent as? ViewGroup)?.removeView(renderer)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.w("ParticipantDisplayItem", "Error releasing renderer", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
surfaceViewRenderer = null
|
||||||
|
}
|
||||||
|
|
||||||
|
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.iceConnectionState
|
||||||
|
mediaStream = callParticipantModel.mediaStream
|
||||||
|
isAudioEnabled = callParticipantModel.isAudioAvailable ?: false
|
||||||
|
isStreamEnabled = callParticipantModel.isVideoAvailable ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
raisedHand = callParticipantModel.raisedHand
|
||||||
|
|
||||||
|
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 fun buildUiState(): ParticipantUiState {
|
||||||
|
return ParticipantUiState(
|
||||||
|
sessionKey = sessionKey,
|
||||||
|
nick = nick ?: "Guest",
|
||||||
|
isConnected = isConnected,
|
||||||
|
isAudioEnabled = isAudioEnabled,
|
||||||
|
isStreamEnabled = isStreamEnabled,
|
||||||
|
raisedHand = raisedHand?.state == true,
|
||||||
|
avatarUrl = urlForAvatar,
|
||||||
|
mediaStream = mediaStream
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 = getUrlForAvatar(baseUrl, userId, true)
|
||||||
|
} else {
|
||||||
|
urlForAvatar = getUrlForGuestAvatar(baseUrl, nick, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun removeObserver(observer: Observer?) {
|
||||||
|
participantDisplayItemNotifier.removeObserver(observer)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun toString(): String {
|
||||||
|
return "ParticipantSession{" +
|
||||||
|
"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,217 +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.ImageViewExtensionsKt;
|
|
||||||
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 org.webrtc.VideoTrack;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
public class ParticipantsAdapter extends BaseAdapter {
|
|
||||||
|
|
||||||
private static final String TAG = "ParticipantsAdapter";
|
|
||||||
|
|
||||||
private final ParticipantDisplayItem.Observer participantDisplayItemObserver = this::notifyDataSetChanged;
|
|
||||||
|
|
||||||
private final Context mContext;
|
|
||||||
private final ArrayList<ParticipantDisplayItem> participantDisplayItems;
|
|
||||||
private final RelativeLayout gridViewWrapper;
|
|
||||||
private final LinearLayout callInfosLinearLayout;
|
|
||||||
private final int columns;
|
|
||||||
private final boolean isVoiceOnlyCall;
|
|
||||||
|
|
||||||
public ParticipantsAdapter(Context mContext,
|
|
||||||
Map<String, ParticipantDisplayItem> participantDisplayItems,
|
|
||||||
RelativeLayout gridViewWrapper,
|
|
||||||
LinearLayout callInfosLinearLayout,
|
|
||||||
int columns,
|
|
||||||
boolean isVoiceOnlyCall) {
|
|
||||||
this.mContext = mContext;
|
|
||||||
this.gridViewWrapper = gridViewWrapper;
|
|
||||||
this.callInfosLinearLayout = callInfosLinearLayout;
|
|
||||||
this.columns = columns;
|
|
||||||
this.isVoiceOnlyCall = isVoiceOnlyCall;
|
|
||||||
|
|
||||||
this.participantDisplayItems = new ArrayList<>();
|
|
||||||
this.participantDisplayItems.addAll(participantDisplayItems.values());
|
|
||||||
|
|
||||||
for (ParticipantDisplayItem participantDisplayItem : this.participantDisplayItems) {
|
|
||||||
participantDisplayItem.addObserver(participantDisplayItemObserver);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void destroy() {
|
|
||||||
for (ParticipantDisplayItem participantDisplayItem : participantDisplayItems) {
|
|
||||||
participantDisplayItem.removeObserver(participantDisplayItemObserver);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getCount() {
|
|
||||||
return participantDisplayItems.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public ParticipantDisplayItem getItem(int position) {
|
|
||||||
return participantDisplayItems.get(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getItemId(int position) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View getView(int position, View convertView, ViewGroup parent) {
|
|
||||||
ParticipantDisplayItem participantDisplayItem = getItem(position);
|
|
||||||
|
|
||||||
SurfaceViewRenderer surfaceViewRenderer;
|
|
||||||
if (convertView == null) {
|
|
||||||
convertView = LayoutInflater.from(mContext).inflate(R.layout.call_item, parent, false);
|
|
||||||
convertView.setVisibility(View.VISIBLE);
|
|
||||||
|
|
||||||
surfaceViewRenderer = convertView.findViewById(R.id.surface_view);
|
|
||||||
try {
|
|
||||||
Log.d(TAG, "hasSurface: " + participantDisplayItem.getRootEglBase().hasSurface());
|
|
||||||
|
|
||||||
surfaceViewRenderer.setMirror(false);
|
|
||||||
surfaceViewRenderer.init(participantDisplayItem.getRootEglBase().getEglBaseContext(), null);
|
|
||||||
surfaceViewRenderer.setZOrderMediaOverlay(false);
|
|
||||||
// disabled because it causes some devices to crash
|
|
||||||
surfaceViewRenderer.setEnableHardwareScaler(false);
|
|
||||||
surfaceViewRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT);
|
|
||||||
} catch (Exception e) {
|
|
||||||
Log.e(TAG, "error while initializing surfaceViewRenderer", e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
surfaceViewRenderer = convertView.findViewById(R.id.surface_view);
|
|
||||||
}
|
|
||||||
|
|
||||||
ProgressBar progressBar = convertView.findViewById(R.id.participant_progress_bar);
|
|
||||||
if (!participantDisplayItem.isConnected()) {
|
|
||||||
progressBar.setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
|
||||||
progressBar.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
ViewGroup.LayoutParams layoutParams = convertView.getLayoutParams();
|
|
||||||
layoutParams.height = scaleGridViewItemHeight();
|
|
||||||
convertView.setLayoutParams(layoutParams);
|
|
||||||
|
|
||||||
TextView nickTextView = convertView.findViewById(R.id.peer_nick_text_view);
|
|
||||||
ImageView imageView = convertView.findViewById(R.id.avatarImageView);
|
|
||||||
|
|
||||||
MediaStream mediaStream = participantDisplayItem.getMediaStream();
|
|
||||||
if (hasVideoStream(participantDisplayItem, mediaStream)) {
|
|
||||||
VideoTrack videoTrack = mediaStream.videoTracks.get(0);
|
|
||||||
videoTrack.addSink(surfaceViewRenderer);
|
|
||||||
imageView.setVisibility(View.INVISIBLE);
|
|
||||||
surfaceViewRenderer.setVisibility(View.VISIBLE);
|
|
||||||
nickTextView.setVisibility(View.GONE);
|
|
||||||
} else {
|
|
||||||
imageView.setVisibility(View.VISIBLE);
|
|
||||||
surfaceViewRenderer.setVisibility(View.INVISIBLE);
|
|
||||||
|
|
||||||
if (((CallActivity) mContext).isInPipMode) {
|
|
||||||
nickTextView.setVisibility(View.GONE);
|
|
||||||
} else {
|
|
||||||
nickTextView.setVisibility(View.VISIBLE);
|
|
||||||
nickTextView.setText(participantDisplayItem.getNick());
|
|
||||||
}
|
|
||||||
if (participantDisplayItem.getActorType() == Participant.ActorType.GUESTS ||
|
|
||||||
participantDisplayItem.getActorType() == Participant.ActorType.EMAILS) {
|
|
||||||
ImageViewExtensionsKt.loadFirstLetterAvatar(
|
|
||||||
imageView,
|
|
||||||
String.valueOf(participantDisplayItem.getNick())
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ImageViewExtensionsKt.loadAvatarWithUrl(imageView,null, participantDisplayItem.getUrlForAvatar());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageView audioOffView = convertView.findViewById(R.id.remote_audio_off);
|
|
||||||
if (!participantDisplayItem.isAudioEnabled()) {
|
|
||||||
audioOffView.setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
|
||||||
audioOffView.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageView raisedHandView = convertView.findViewById(R.id.raised_hand);
|
|
||||||
if (participantDisplayItem.getRaisedHand() != null && participantDisplayItem.getRaisedHand().getState()) {
|
|
||||||
raisedHandView.setVisibility(View.VISIBLE);
|
|
||||||
} else {
|
|
||||||
raisedHandView.setVisibility(View.GONE);
|
|
||||||
}
|
|
||||||
|
|
||||||
return convertView;
|
|
||||||
}
|
|
||||||
|
|
||||||
private boolean hasVideoStream(ParticipantDisplayItem participantDisplayItem, MediaStream mediaStream) {
|
|
||||||
if (!participantDisplayItem.isStreamEnabled()) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mediaStream == null || mediaStream.videoTracks == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (VideoTrack t : mediaStream.videoTracks) {
|
|
||||||
if (MediaStreamTrack.State.LIVE == t.state()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int scaleGridViewItemHeight() {
|
|
||||||
int headerHeight = 0;
|
|
||||||
int callControlsHeight = 0;
|
|
||||||
if (callInfosLinearLayout.getVisibility() == View.VISIBLE && isVoiceOnlyCall) {
|
|
||||||
headerHeight = callInfosLinearLayout.getHeight();
|
|
||||||
}
|
|
||||||
if (isVoiceOnlyCall) {
|
|
||||||
callControlsHeight = Math.round(mContext.getResources().getDimension(R.dimen.call_controls_height));
|
|
||||||
}
|
|
||||||
int itemHeight = (gridViewWrapper.getHeight() - headerHeight - callControlsHeight) / getRowsCount(getCount());
|
|
||||||
int itemMinHeight = Math.round(mContext.getResources().getDimension(R.dimen.call_grid_item_min_height));
|
|
||||||
if (itemHeight < itemMinHeight) {
|
|
||||||
itemHeight = itemMinHeight;
|
|
||||||
}
|
|
||||||
return itemHeight;
|
|
||||||
}
|
|
||||||
|
|
||||||
private int getRowsCount(int items) {
|
|
||||||
int rows = (int) Math.ceil((double) items / (double) columns);
|
|
||||||
if (rows == 0) {
|
|
||||||
rows = 1;
|
|
||||||
}
|
|
||||||
return rows;
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,62 @@
|
|||||||
|
/*
|
||||||
|
* 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.foundation.shape.CircleShape
|
||||||
|
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.layout.ContentScale
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import com.nextcloud.talk.adapters.ParticipantUiState
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun AvatarWithFallback(participant: ParticipantUiState, modifier: Modifier = Modifier) {
|
||||||
|
val initials = participant.nick
|
||||||
|
.split(" ")
|
||||||
|
.mapNotNull { it.firstOrNull()?.uppercase() }
|
||||||
|
.take(2)
|
||||||
|
.joinToString("")
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.clip(CircleShape),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
if (!participant.avatarUrl.isNullOrEmpty()) {
|
||||||
|
AsyncImage(
|
||||||
|
model = participant.avatarUrl,
|
||||||
|
contentDescription = "Avatar",
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clip(CircleShape)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.background(Color.White, CircleShape),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = initials.ifEmpty { "?" },
|
||||||
|
color = Color.Black,
|
||||||
|
fontSize = 24.sp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,291 @@
|
|||||||
|
/*
|
||||||
|
* Nextcloud Talk - Android Client
|
||||||
|
*
|
||||||
|
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
@file:Suppress("MagicNumber", "TooManyFunctions")
|
||||||
|
|
||||||
|
package com.nextcloud.talk.call.components
|
||||||
|
|
||||||
|
import android.content.res.Configuration
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
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.platform.LocalConfiguration
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.nextcloud.talk.adapters.ParticipantUiState
|
||||||
|
import org.webrtc.EglBase
|
||||||
|
import kotlin.math.ceil
|
||||||
|
|
||||||
|
@Suppress("LongParameterList")
|
||||||
|
@Composable
|
||||||
|
fun ParticipantGrid(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
eglBase: EglBase?,
|
||||||
|
participantUiStates: List<ParticipantUiState>,
|
||||||
|
isVoiceOnlyCall: Boolean,
|
||||||
|
isInPipMode: Boolean,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
val isPortrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT
|
||||||
|
|
||||||
|
val minItemHeight = 100.dp
|
||||||
|
|
||||||
|
val columns =
|
||||||
|
if (isPortrait) {
|
||||||
|
when (participantUiStates.size) {
|
||||||
|
1, 2, 3 -> 1
|
||||||
|
else -> 2
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
when (participantUiStates.size) {
|
||||||
|
1 -> 1
|
||||||
|
2, 4 -> 2
|
||||||
|
else -> 3
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val rows = ceil(participantUiStates.size / columns.toFloat()).toInt()
|
||||||
|
|
||||||
|
val heightForNonGridComponents = if (isVoiceOnlyCall && !isInPipMode) {
|
||||||
|
// this is a workaround for now. It should ~summarize the height of callInfosLinearLayout and callControls
|
||||||
|
// Once everything is migrated to jetpack, this workaround should be obsolete or solved in a better way
|
||||||
|
240.dp
|
||||||
|
} else {
|
||||||
|
0.dp
|
||||||
|
}
|
||||||
|
|
||||||
|
val gridHeight = LocalConfiguration.current.screenHeightDp.dp - heightForNonGridComponents
|
||||||
|
val itemSpacing = 8.dp
|
||||||
|
val edgePadding = 8.dp
|
||||||
|
|
||||||
|
val totalVerticalSpacing = itemSpacing * (rows - 1)
|
||||||
|
val totalVerticalPadding = edgePadding * 2
|
||||||
|
val availableHeight = gridHeight - totalVerticalSpacing - totalVerticalPadding
|
||||||
|
|
||||||
|
val rawItemHeight = availableHeight / rows
|
||||||
|
val itemHeight = maxOf(rawItemHeight, minItemHeight)
|
||||||
|
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(columns),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(horizontal = edgePadding)
|
||||||
|
.clickable { onClick() },
|
||||||
|
verticalArrangement = Arrangement.spacedBy(itemSpacing),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(itemSpacing),
|
||||||
|
contentPadding = PaddingValues(vertical = edgePadding)
|
||||||
|
) {
|
||||||
|
items(
|
||||||
|
participantUiStates,
|
||||||
|
key = { it.sessionKey }
|
||||||
|
) { participant ->
|
||||||
|
ParticipantTile(
|
||||||
|
participantUiState = participant,
|
||||||
|
modifier = Modifier
|
||||||
|
.height(itemHeight)
|
||||||
|
.fillMaxWidth(),
|
||||||
|
eglBase = eglBase,
|
||||||
|
isVoiceOnlyCall = isVoiceOnlyCall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun ParticipantGridPreview() {
|
||||||
|
ParticipantGrid(
|
||||||
|
participantUiStates = getTestParticipants(1),
|
||||||
|
eglBase = null,
|
||||||
|
isVoiceOnlyCall = false,
|
||||||
|
isInPipMode = false
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun TwoParticipants() {
|
||||||
|
ParticipantGrid(
|
||||||
|
participantUiStates = getTestParticipants(2),
|
||||||
|
eglBase = null,
|
||||||
|
isVoiceOnlyCall = false,
|
||||||
|
isInPipMode = false
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun ThreeParticipants() {
|
||||||
|
ParticipantGrid(
|
||||||
|
participantUiStates = getTestParticipants(3),
|
||||||
|
eglBase = null,
|
||||||
|
isVoiceOnlyCall = false,
|
||||||
|
isInPipMode = false
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun FourParticipants() {
|
||||||
|
ParticipantGrid(
|
||||||
|
participantUiStates = getTestParticipants(4),
|
||||||
|
eglBase = null,
|
||||||
|
isVoiceOnlyCall = false,
|
||||||
|
isInPipMode = false
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun FiveParticipants() {
|
||||||
|
ParticipantGrid(
|
||||||
|
participantUiStates = getTestParticipants(5),
|
||||||
|
eglBase = null,
|
||||||
|
isVoiceOnlyCall = false,
|
||||||
|
isInPipMode = false
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun SevenParticipants() {
|
||||||
|
ParticipantGrid(
|
||||||
|
participantUiStates = getTestParticipants(7),
|
||||||
|
eglBase = null,
|
||||||
|
isVoiceOnlyCall = false,
|
||||||
|
isInPipMode = false
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun FiftyParticipants() {
|
||||||
|
ParticipantGrid(
|
||||||
|
participantUiStates = getTestParticipants(50),
|
||||||
|
eglBase = null,
|
||||||
|
isVoiceOnlyCall = false,
|
||||||
|
isInPipMode = false
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(
|
||||||
|
showBackground = false,
|
||||||
|
heightDp = 360,
|
||||||
|
widthDp = 800
|
||||||
|
)
|
||||||
|
@Composable
|
||||||
|
fun OneParticipantLandscape() {
|
||||||
|
ParticipantGrid(
|
||||||
|
participantUiStates = getTestParticipants(1),
|
||||||
|
eglBase = null,
|
||||||
|
isVoiceOnlyCall = false,
|
||||||
|
isInPipMode = false
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(
|
||||||
|
showBackground = false,
|
||||||
|
heightDp = 360,
|
||||||
|
widthDp = 800
|
||||||
|
)
|
||||||
|
@Composable
|
||||||
|
fun TwoParticipantsLandscape() {
|
||||||
|
ParticipantGrid(
|
||||||
|
participantUiStates = getTestParticipants(2),
|
||||||
|
eglBase = null,
|
||||||
|
isVoiceOnlyCall = false,
|
||||||
|
isInPipMode = false
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(
|
||||||
|
showBackground = false,
|
||||||
|
heightDp = 360,
|
||||||
|
widthDp = 800
|
||||||
|
)
|
||||||
|
@Composable
|
||||||
|
fun ThreeParticipantsLandscape() {
|
||||||
|
ParticipantGrid(
|
||||||
|
participantUiStates = getTestParticipants(3),
|
||||||
|
eglBase = null,
|
||||||
|
isVoiceOnlyCall = false,
|
||||||
|
isInPipMode = false
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(
|
||||||
|
showBackground = false,
|
||||||
|
heightDp = 360,
|
||||||
|
widthDp = 800
|
||||||
|
)
|
||||||
|
@Composable
|
||||||
|
fun FourParticipantsLandscape() {
|
||||||
|
ParticipantGrid(
|
||||||
|
participantUiStates = getTestParticipants(4),
|
||||||
|
eglBase = null,
|
||||||
|
isVoiceOnlyCall = false,
|
||||||
|
isInPipMode = false
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(
|
||||||
|
showBackground = false,
|
||||||
|
heightDp = 360,
|
||||||
|
widthDp = 800
|
||||||
|
)
|
||||||
|
@Composable
|
||||||
|
fun SevenParticipantsLandscape() {
|
||||||
|
ParticipantGrid(
|
||||||
|
participantUiStates = getTestParticipants(7),
|
||||||
|
eglBase = null,
|
||||||
|
isVoiceOnlyCall = false,
|
||||||
|
isInPipMode = false
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(
|
||||||
|
showBackground = false,
|
||||||
|
heightDp = 360,
|
||||||
|
widthDp = 800
|
||||||
|
)
|
||||||
|
@Composable
|
||||||
|
fun FiftyParticipantsLandscape() {
|
||||||
|
ParticipantGrid(
|
||||||
|
participantUiStates = getTestParticipants(50),
|
||||||
|
eglBase = null,
|
||||||
|
isVoiceOnlyCall = false,
|
||||||
|
isInPipMode = false
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getTestParticipants(numberOfParticipants: Int): List<ParticipantUiState> {
|
||||||
|
val participantList = mutableListOf<ParticipantUiState>()
|
||||||
|
for (i: Int in 1..numberOfParticipants) {
|
||||||
|
val participant = ParticipantUiState(
|
||||||
|
sessionKey = i.toString(),
|
||||||
|
nick = "test$i user",
|
||||||
|
isConnected = true,
|
||||||
|
isAudioEnabled = false,
|
||||||
|
isStreamEnabled = true,
|
||||||
|
raisedHand = true,
|
||||||
|
avatarUrl = "",
|
||||||
|
mediaStream = null
|
||||||
|
)
|
||||||
|
participantList.add(participant)
|
||||||
|
}
|
||||||
|
return participantList
|
||||||
|
}
|
@ -0,0 +1,144 @@
|
|||||||
|
/*
|
||||||
|
* 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.BoxWithConstraints
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
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.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.Shadow
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.min
|
||||||
|
import com.nextcloud.talk.R
|
||||||
|
import com.nextcloud.talk.adapters.ParticipantUiState
|
||||||
|
import com.nextcloud.talk.utils.ColorGenerator
|
||||||
|
import org.webrtc.EglBase
|
||||||
|
|
||||||
|
const val NICK_OFFSET = 4f
|
||||||
|
const val NICK_BLUR_RADIUS = 4f
|
||||||
|
const val AVATAR_SIZE_FACTOR = 0.6f
|
||||||
|
|
||||||
|
@Suppress("Detekt.LongMethod")
|
||||||
|
@Composable
|
||||||
|
fun ParticipantTile(
|
||||||
|
participantUiState: ParticipantUiState,
|
||||||
|
eglBase: EglBase?,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
isVoiceOnlyCall: Boolean
|
||||||
|
) {
|
||||||
|
val colorInt = ColorGenerator.shared.usernameToColor(participantUiState.nick)
|
||||||
|
|
||||||
|
BoxWithConstraints(
|
||||||
|
modifier = modifier
|
||||||
|
.clip(RoundedCornerShape(12.dp))
|
||||||
|
.background(Color(colorInt))
|
||||||
|
) {
|
||||||
|
val avatarSize = min(maxWidth, maxHeight) * AVATAR_SIZE_FACTOR
|
||||||
|
|
||||||
|
if (!isVoiceOnlyCall && participantUiState.isStreamEnabled && participantUiState.mediaStream != null) {
|
||||||
|
WebRTCVideoView(participantUiState, eglBase)
|
||||||
|
} else {
|
||||||
|
AvatarWithFallback(
|
||||||
|
participant = participantUiState,
|
||||||
|
modifier = Modifier
|
||||||
|
.size(avatarSize)
|
||||||
|
.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.padding(8.dp)
|
||||||
|
) {
|
||||||
|
if (participantUiState.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 (!participantUiState.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 = participantUiState.nick,
|
||||||
|
color = Color.White,
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomStart),
|
||||||
|
style = MaterialTheme.typography.bodyMedium.copy(
|
||||||
|
shadow = Shadow(
|
||||||
|
color = Color.Black,
|
||||||
|
offset = Offset(NICK_OFFSET, NICK_OFFSET),
|
||||||
|
blurRadius = NICK_BLUR_RADIUS
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!participantUiState.isConnected) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.align(Alignment.Center)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview(showBackground = false)
|
||||||
|
@Composable
|
||||||
|
fun ParticipantTilePreview() {
|
||||||
|
val participant = ParticipantUiState(
|
||||||
|
sessionKey = "",
|
||||||
|
nick = "testuser one",
|
||||||
|
isConnected = true,
|
||||||
|
isAudioEnabled = false,
|
||||||
|
isStreamEnabled = true,
|
||||||
|
raisedHand = true,
|
||||||
|
avatarUrl = "",
|
||||||
|
mediaStream = null
|
||||||
|
)
|
||||||
|
ParticipantTile(
|
||||||
|
participantUiState = participant,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(300.dp),
|
||||||
|
eglBase = null,
|
||||||
|
isVoiceOnlyCall = false
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* 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 com.nextcloud.talk.adapters.ParticipantUiState
|
||||||
|
import org.webrtc.EglBase
|
||||||
|
import org.webrtc.SurfaceViewRenderer
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun WebRTCVideoView(participant: ParticipantUiState, eglBase: EglBase?) {
|
||||||
|
AndroidView(
|
||||||
|
factory = { context ->
|
||||||
|
SurfaceViewRenderer(context).apply {
|
||||||
|
init(eglBase?.eglBaseContext, null)
|
||||||
|
setEnableHardwareScaler(true)
|
||||||
|
setMirror(false)
|
||||||
|
participant.mediaStream?.videoTracks?.firstOrNull()?.addSink(this)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
onRelease = {
|
||||||
|
participant.mediaStream?.videoTracks?.firstOrNull()?.removeSink(it)
|
||||||
|
it.release()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
107
app/src/main/java/com/nextcloud/talk/utils/ColorGenerator.kt
Normal file
107
app/src/main/java/com/nextcloud/talk/utils/ColorGenerator.kt
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
/*
|
||||||
|
* Nextcloud Talk - Android Client
|
||||||
|
*
|
||||||
|
* SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
|
||||||
|
* SPDX-FileCopyrightText: 2025 Marcel Hibbe <dev@mhibbe.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.nextcloud.talk.utils
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import java.security.MessageDigest
|
||||||
|
import kotlin.math.abs
|
||||||
|
|
||||||
|
// See https://github.com/nextcloud/nextcloud-vue/blob/56b79afae93f4701a0cb933bfeb7b4a2fbd590fb/src/functions/usernameToColor/usernameToColor.js
|
||||||
|
// and https://github.com/nextcloud/nextcloud-vue/blob/56b79afae93f4701a0cb933bfeb7b4a2fbd590fb/src/utils/GenColors.js
|
||||||
|
|
||||||
|
@Suppress("MagicNumber")
|
||||||
|
class ColorGenerator private constructor() {
|
||||||
|
|
||||||
|
private val steps = 6
|
||||||
|
private val finalPalette: List<Int> = genColors(steps)
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val shared = ColorGenerator()
|
||||||
|
private const val MULTIPLIER = 256.0f
|
||||||
|
|
||||||
|
private fun stepCalc(steps: Int, colorStart: Int, colorEnd: Int): List<Float> {
|
||||||
|
val r0 = Color.red(colorStart) / MULTIPLIER
|
||||||
|
val g0 = Color.green(colorStart) / MULTIPLIER
|
||||||
|
val b0 = Color.blue(colorStart) / MULTIPLIER
|
||||||
|
|
||||||
|
val r1 = Color.red(colorEnd) / MULTIPLIER
|
||||||
|
val g1 = Color.green(colorEnd) / MULTIPLIER
|
||||||
|
val b1 = Color.blue(colorEnd) / MULTIPLIER
|
||||||
|
|
||||||
|
return listOf(
|
||||||
|
(r1 - r0) / steps,
|
||||||
|
(g1 - g0) / steps,
|
||||||
|
(b1 - b0) / steps
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun mixPalette(steps: Int, color1: Int, color2: Int): List<Int> {
|
||||||
|
val palette = mutableListOf(color1)
|
||||||
|
val step = stepCalc(steps, color1, color2)
|
||||||
|
|
||||||
|
val rStart = Color.red(color1) / MULTIPLIER
|
||||||
|
val gStart = Color.green(color1) / MULTIPLIER
|
||||||
|
val bStart = Color.blue(color1) / MULTIPLIER
|
||||||
|
|
||||||
|
for (i in 1 until steps) {
|
||||||
|
val r = (abs(rStart + step[0] * i) * MULTIPLIER).toInt().coerceIn(0, 255)
|
||||||
|
val g = (abs(gStart + step[1] * i) * MULTIPLIER).toInt().coerceIn(0, 255)
|
||||||
|
val b = (abs(bStart + step[2] * i) * MULTIPLIER).toInt().coerceIn(0, 255)
|
||||||
|
|
||||||
|
palette.add(Color.rgb(r, g, b))
|
||||||
|
}
|
||||||
|
|
||||||
|
return palette
|
||||||
|
}
|
||||||
|
|
||||||
|
fun genColors(steps: Int): List<Int> {
|
||||||
|
val red = Color.rgb(
|
||||||
|
(182 / MULTIPLIER * 255).toInt(),
|
||||||
|
(70 / MULTIPLIER * 255).toInt(),
|
||||||
|
(157 / MULTIPLIER * 255).toInt()
|
||||||
|
)
|
||||||
|
val yellow = Color.rgb(
|
||||||
|
(221 / MULTIPLIER * 255).toInt(),
|
||||||
|
(203 / MULTIPLIER * 255).toInt(),
|
||||||
|
(85 / MULTIPLIER * 255).toInt()
|
||||||
|
)
|
||||||
|
val blue = Color.rgb(
|
||||||
|
0,
|
||||||
|
(130 / MULTIPLIER * 255).toInt(),
|
||||||
|
(201 / MULTIPLIER * 255).toInt()
|
||||||
|
)
|
||||||
|
|
||||||
|
val palette1 = mixPalette(steps, red, yellow).toMutableList()
|
||||||
|
val palette2 = mixPalette(steps, yellow, blue)
|
||||||
|
val palette3 = mixPalette(steps, blue, red)
|
||||||
|
|
||||||
|
palette1.addAll(palette2)
|
||||||
|
palette1.addAll(palette3)
|
||||||
|
|
||||||
|
return palette1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun usernameToColor(username: String): Int {
|
||||||
|
val hash = username.lowercase()
|
||||||
|
var hashInt: Int
|
||||||
|
|
||||||
|
val bytes = hash.toByteArray(Charsets.UTF_8)
|
||||||
|
val md = MessageDigest.getInstance("MD5")
|
||||||
|
val digest = md.digest(bytes)
|
||||||
|
val hex = digest.joinToString("") { "%02x".format(it) }
|
||||||
|
|
||||||
|
hashInt = hex.map { it.digitToInt(16) % 16 }.sum()
|
||||||
|
|
||||||
|
val maximum = steps * 3
|
||||||
|
hashInt %= maximum
|
||||||
|
|
||||||
|
return finalPalette[hashInt]
|
||||||
|
}
|
||||||
|
}
|
@ -9,13 +9,13 @@ package com.nextcloud.talk.utils.message
|
|||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.Typeface
|
import android.graphics.Typeface
|
||||||
import android.net.Uri
|
|
||||||
import android.text.SpannableString
|
import android.text.SpannableString
|
||||||
import android.text.SpannableStringBuilder
|
import android.text.SpannableStringBuilder
|
||||||
import android.text.Spanned
|
import android.text.Spanned
|
||||||
import android.text.style.StyleSpan
|
import android.text.style.StyleSpan
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.core.net.toUri
|
||||||
import com.nextcloud.talk.R
|
import com.nextcloud.talk.R
|
||||||
import com.nextcloud.talk.chat.data.model.ChatMessage
|
import com.nextcloud.talk.chat.data.model.ChatMessage
|
||||||
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
import com.nextcloud.talk.ui.theme.ViewThemeUtils
|
||||||
@ -145,7 +145,7 @@ class MessageUtils(val context: Context) {
|
|||||||
|
|
||||||
"file" -> {
|
"file" -> {
|
||||||
itemView?.setOnClickListener { v ->
|
itemView?.setOnClickListener { v ->
|
||||||
val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(individualHashMap["link"]))
|
val browserIntent = Intent(Intent.ACTION_VIEW, individualHashMap["link"]?.toUri())
|
||||||
context.startActivity(browserIntent)
|
context.startActivity(browserIntent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,15 +33,10 @@
|
|||||||
android:visibility="visible"
|
android:visibility="visible"
|
||||||
tools:visibility="visible">
|
tools:visibility="visible">
|
||||||
|
|
||||||
<GridView
|
<androidx.compose.ui.platform.ComposeView
|
||||||
android:id="@+id/gridview"
|
android:id="@+id/composeParticipantGrid"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="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
|
<FrameLayout
|
||||||
android:id="@+id/selfVideoViewWrapper"
|
android:id="@+id/selfVideoViewWrapper"
|
||||||
@ -327,7 +322,7 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
android:id="@+id/pipSelfVideoOverlay"
|
android:id="@+id/pipOverlay"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="match_parent"
|
android:layout_height="match_parent"
|
||||||
android:background="@color/black"
|
android:background="@color/black"
|
||||||
@ -341,9 +336,8 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_centerHorizontal="true"
|
android:layout_centerHorizontal="true"
|
||||||
android:layout_marginStart="5dp"
|
android:layout_marginStart="5dp"
|
||||||
android:layout_marginTop="40dp"
|
android:layout_marginTop="0dp"
|
||||||
android:layout_marginEnd="5dp"
|
android:layout_marginEnd="5dp"
|
||||||
android:layout_marginBottom="-30dp"
|
|
||||||
android:ellipsize="end"
|
android:ellipsize="end"
|
||||||
android:maxLines="3"
|
android:maxLines="3"
|
||||||
android:textAlignment="center"
|
android:textAlignment="center"
|
||||||
|
@ -1,80 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<!--
|
|
||||||
~ Nextcloud Talk - Android Client
|
|
||||||
~
|
|
||||||
~ SPDX-FileCopyrightText: 2021 Andy Scherzinger <info@andy-scherzinger.de>
|
|
||||||
~ SPDX-FileCopyrightText: 2021 Marcel Hibbe <dev@mhibbe.de>
|
|
||||||
~ SPDX-FileCopyrightText: 2017 Mario Danic <mario@lovelyhq.com>
|
|
||||||
~ SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
-->
|
|
||||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:id="@+id/relative_layout"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:gravity="center"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/avatarImageView"
|
|
||||||
android:layout_width="80dp"
|
|
||||||
android:layout_height="80dp"
|
|
||||||
android:layout_centerInParent="true"
|
|
||||||
android:contentDescription="@string/avatar"/>
|
|
||||||
|
|
||||||
<org.webrtc.SurfaceViewRenderer
|
|
||||||
android:id="@+id/surface_view"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:visibility="invisible" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="horizontal"
|
|
||||||
android:layout_alignParentBottom="true">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/peer_nick_text_view"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_marginStart="10dp"
|
|
||||||
android:layout_marginBottom="6dp"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:maxEms="8"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:textAlignment="viewStart"
|
|
||||||
android:textColor="@color/white"
|
|
||||||
tools:text="Bill Murray 12345678901234567890" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/remote_audio_off"
|
|
||||||
android:layout_width="16dp"
|
|
||||||
android:layout_height="16dp"
|
|
||||||
android:layout_marginStart="10dp"
|
|
||||||
android:layout_marginBottom="6dp"
|
|
||||||
android:contentDescription="@string/nc_remote_audio_off"
|
|
||||||
android:src="@drawable/ic_mic_off_white_24px"
|
|
||||||
android:visibility="invisible"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/raised_hand"
|
|
||||||
android:layout_width="16dp"
|
|
||||||
android:layout_height="16dp"
|
|
||||||
android:layout_marginStart="10dp"
|
|
||||||
android:layout_marginBottom="6dp"
|
|
||||||
android:contentDescription="@string/raise_hand"
|
|
||||||
android:src="@drawable/ic_hand_back_left"
|
|
||||||
android:visibility="invisible"
|
|
||||||
tools:visibility="visible" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<ProgressBar
|
|
||||||
android:id="@+id/participant_progress_bar"
|
|
||||||
style="?android:attr/progressBarStyle"
|
|
||||||
android:layout_width="@dimen/call_participant_progress_bar_size"
|
|
||||||
android:layout_height="@dimen/call_participant_progress_bar_size"
|
|
||||||
android:layout_centerInParent="@bool/value_true" />
|
|
||||||
|
|
||||||
</RelativeLayout>
|
|
@ -54,7 +54,6 @@
|
|||||||
<dimen name="dialog_padding">24dp</dimen>
|
<dimen name="dialog_padding">24dp</dimen>
|
||||||
<dimen name="dialog_padding_top_bottom">18dp</dimen>
|
<dimen name="dialog_padding_top_bottom">18dp</dimen>
|
||||||
|
|
||||||
<dimen name="call_grid_item_min_height">180dp</dimen>
|
|
||||||
<dimen name="call_controls_height">110dp</dimen>
|
<dimen name="call_controls_height">110dp</dimen>
|
||||||
<dimen name="call_participant_progress_bar_size">48dp</dimen>
|
<dimen name="call_participant_progress_bar_size">48dp</dimen>
|
||||||
<dimen name="call_self_participant_progress_bar_size">48dp</dimen>
|
<dimen name="call_self_participant_progress_bar_size">48dp</dimen>
|
||||||
|
@ -175,7 +175,6 @@ How to translate with transifex:
|
|||||||
|
|
||||||
<string name="nc_settings_notifications_granted">Notifications are granted</string>
|
<string name="nc_settings_notifications_granted">Notifications are granted</string>
|
||||||
<string name="nc_settings_notifications_declined">Notifications are declined</string>
|
<string name="nc_settings_notifications_declined">Notifications are declined</string>
|
||||||
<string name="nc_settings_notifications_declined_hint">Notifications are declined. Please allow notifications in Android settings</string>
|
|
||||||
|
|
||||||
<!-- Diagnose -->
|
<!-- Diagnose -->
|
||||||
<string name="nc_notifications_troubleshooting_dialog_title">Notification troubleshooting</string>
|
<string name="nc_notifications_troubleshooting_dialog_title">Notification troubleshooting</string>
|
||||||
@ -373,7 +372,6 @@ How to translate with transifex:
|
|||||||
<string name="dnd">Do not disturb</string>
|
<string name="dnd">Do not disturb</string>
|
||||||
<string name="away">Away</string>
|
<string name="away">Away</string>
|
||||||
<string name="invisible">Invisible</string>
|
<string name="invisible">Invisible</string>
|
||||||
<string name="divider" translatable="false">—</string>
|
|
||||||
<string name="default_emoji" translatable="false">😃</string>
|
<string name="default_emoji" translatable="false">😃</string>
|
||||||
<string name="emoji_thumbsUp" translatable="false">👍</string>
|
<string name="emoji_thumbsUp" translatable="false">👍</string>
|
||||||
<string name="emoji_thumbsDown" translatable="false">👎</string>
|
<string name="emoji_thumbsDown" translatable="false">👎</string>
|
||||||
@ -398,9 +396,6 @@ How to translate with transifex:
|
|||||||
<string name="error_loading_chats">There was a problem loading your chats</string>
|
<string name="error_loading_chats">There was a problem loading your chats</string>
|
||||||
<string name="close">Close</string>
|
<string name="close">Close</string>
|
||||||
<string name="close_icon">Close Icon</string>
|
<string name="close_icon">Close Icon</string>
|
||||||
<string name="nc_refresh">Refresh</string>
|
|
||||||
<string name="nc_check_your_internet">Please check your internet connection</string>
|
|
||||||
<string name="nc_visible">Visible</string>
|
|
||||||
|
|
||||||
<!-- Chat -->
|
<!-- Chat -->
|
||||||
<string name="nc_hint_enter_a_message">Enter a message …</string>
|
<string name="nc_hint_enter_a_message">Enter a message …</string>
|
||||||
@ -438,9 +433,7 @@ How to translate with transifex:
|
|||||||
<string name="nc_message_failed">Failed</string>
|
<string name="nc_message_failed">Failed</string>
|
||||||
<string name="nc_message_sending">Sending</string>
|
<string name="nc_message_sending">Sending</string>
|
||||||
<string name="nc_message_failed_to_send">Failed to send message:</string>
|
<string name="nc_message_failed_to_send">Failed to send message:</string>
|
||||||
<string name="nc_remote_audio_off">Remote audio off</string>
|
|
||||||
<string name="nc_add_attachment">Add attachment</string>
|
<string name="nc_add_attachment">Add attachment</string>
|
||||||
<string name="emoji_category_recent">Recent</string>
|
|
||||||
<plurals name="see_similar_system_messages">
|
<plurals name="see_similar_system_messages">
|
||||||
<item quantity="one">See %d similar message</item>
|
<item quantity="one">See %d similar message</item>
|
||||||
<item quantity="other">See %d similar messages</item>
|
<item quantity="other">See %d similar messages</item>
|
||||||
@ -461,7 +454,6 @@ How to translate with transifex:
|
|||||||
<string name="nc_guest_access_password_dialog_title">Guest access password</string>
|
<string name="nc_guest_access_password_dialog_title">Guest access password</string>
|
||||||
<string name="nc_guest_access_password_dialog_hint">Enter a password</string>
|
<string name="nc_guest_access_password_dialog_hint">Enter a password</string>
|
||||||
<string name="nc_guest_access_password_failed">Error during setting/disabling the password.</string>
|
<string name="nc_guest_access_password_failed">Error during setting/disabling the password.</string>
|
||||||
<string name="nc_guest_access_password_weak_alert_title">Weak password</string>
|
|
||||||
<string name="nc_guest_access_share_link">Share conversation link</string>
|
<string name="nc_guest_access_share_link">Share conversation link</string>
|
||||||
<string name="nc_guest_access_resend_invitations">Resend invitations</string>
|
<string name="nc_guest_access_resend_invitations">Resend invitations</string>
|
||||||
<string name="nc_guest_access_resend_invitations_successful">Invitations were sent out again.</string>
|
<string name="nc_guest_access_resend_invitations_successful">Invitations were sent out again.</string>
|
||||||
@ -477,11 +469,8 @@ How to translate with transifex:
|
|||||||
|
|
||||||
<!-- Other -->
|
<!-- Other -->
|
||||||
<string name="nc_limit_hit">%s characters limit has been hit</string>
|
<string name="nc_limit_hit">%s characters limit has been hit</string>
|
||||||
<string name="nc_email">Email</string>
|
|
||||||
<string name="nc_group">Group</string>
|
<string name="nc_group">Group</string>
|
||||||
<string name="nc_team">Team</string>
|
<string name="nc_team">Team</string>
|
||||||
<string name="nc_groups">Groups</string>
|
|
||||||
<string name="nc_teams">Teams</string>
|
|
||||||
<string name="nc_participants">Participants</string>
|
<string name="nc_participants">Participants</string>
|
||||||
<string name="nc_participants_add">Add participants</string>
|
<string name="nc_participants_add">Add participants</string>
|
||||||
<string name="nc_start_group_chat">Start group chat</string>
|
<string name="nc_start_group_chat">Start group chat</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user