From b85016ad13f93fddf0c9b93507a0fcdec92b251f Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Mon, 5 May 2025 10:16:09 +0200 Subject: [PATCH 01/28] Rename .java to .kt Signed-off-by: Marcel Hibbe --- .../adapters/{ParticipantsAdapter.java => ParticipantsAdapter.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/nextcloud/talk/adapters/{ParticipantsAdapter.java => ParticipantsAdapter.kt} (100%) diff --git a/app/src/main/java/com/nextcloud/talk/adapters/ParticipantsAdapter.java b/app/src/main/java/com/nextcloud/talk/adapters/ParticipantsAdapter.kt similarity index 100% rename from app/src/main/java/com/nextcloud/talk/adapters/ParticipantsAdapter.java rename to app/src/main/java/com/nextcloud/talk/adapters/ParticipantsAdapter.kt From 7df19b76238b612da7bb6a543937f8da91017a31 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Mon, 5 May 2025 10:16:26 +0200 Subject: [PATCH 02/28] convert ParticipantsAdapter to kt Signed-off-by: Marcel Hibbe --- .../nextcloud/talk/activities/CallActivity.kt | 4 +- .../talk/adapters/ParticipantsAdapter.kt | 267 +++++++++--------- 2 files changed, 129 insertions(+), 142 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt index b787b7afe..06a454703 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt @@ -303,7 +303,7 @@ class CallActivity : CallBaseActivity() { private var handler: Handler? = null private var currentCallStatus: CallStatus? = null private var mediaPlayer: MediaPlayer? = null - private var participantDisplayItems: MutableMap? = null + private var participantDisplayItems: MutableMap? = null private var participantsAdapter: ParticipantsAdapter? = null private var binding: CallActivityBinding? = null private var audioOutputDialog: AudioOutputDialog? = null @@ -994,7 +994,7 @@ class CallActivity : CallBaseActivity() { } participantsAdapter = ParticipantsAdapter( this, - participantDisplayItems, + participantDisplayItems!!.toMap(), binding!!.conversationRelativeLayout, binding!!.callInfosLinearLayout, columns, diff --git a/app/src/main/java/com/nextcloud/talk/adapters/ParticipantsAdapter.kt b/app/src/main/java/com/nextcloud/talk/adapters/ParticipantsAdapter.kt index c6c49773d..69dfead61 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/ParticipantsAdapter.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/ParticipantsAdapter.kt @@ -5,213 +5,200 @@ * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez * SPDX-License-Identifier: GPL-3.0-or-later */ -package com.nextcloud.talk.adapters; +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 android.content.Context +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.BaseAdapter +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.ProgressBar +import android.widget.RelativeLayout +import android.widget.TextView +import com.nextcloud.talk.R +import com.nextcloud.talk.activities.CallActivity +import com.nextcloud.talk.extensions.loadAvatarWithUrl +import com.nextcloud.talk.extensions.loadFirstLetterAvatar +import com.nextcloud.talk.models.json.participants.Participant +import org.webrtc.MediaStream +import org.webrtc.MediaStreamTrack +import org.webrtc.RendererCommon +import org.webrtc.SurfaceViewRenderer +import kotlin.math.ceil -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; +class ParticipantsAdapter( + private val mContext: Context, + participantDisplayItems: Map, + private val gridViewWrapper: RelativeLayout, + private val callInfosLinearLayout: LinearLayout, + private val columns: Int, + private val isVoiceOnlyCall: Boolean +) : BaseAdapter() { + private val participantDisplayItemObserver = ParticipantDisplayItem.Observer { this.notifyDataSetChanged() } -import org.webrtc.MediaStream; -import org.webrtc.MediaStreamTrack; -import org.webrtc.RendererCommon; -import org.webrtc.SurfaceViewRenderer; -import org.webrtc.VideoTrack; + private val participantDisplayItems = ArrayList() -import java.util.ArrayList; -import java.util.Map; + init { + this.participantDisplayItems.addAll(participantDisplayItems.values) -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 participantDisplayItems; - private final RelativeLayout gridViewWrapper; - private final LinearLayout callInfosLinearLayout; - private final int columns; - private final boolean isVoiceOnlyCall; - - public ParticipantsAdapter(Context mContext, - Map 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); + for (participantDisplayItem in this.participantDisplayItems) { + participantDisplayItem.addObserver(participantDisplayItemObserver) } } - public void destroy() { - for (ParticipantDisplayItem participantDisplayItem : participantDisplayItems) { - participantDisplayItem.removeObserver(participantDisplayItemObserver); + fun destroy() { + for (participantDisplayItem in participantDisplayItems) { + participantDisplayItem.removeObserver(participantDisplayItemObserver) } } - @Override - public int getCount() { - return participantDisplayItems.size(); + override fun getCount(): Int { + return participantDisplayItems.size } - @Override - public ParticipantDisplayItem getItem(int position) { - return participantDisplayItems.get(position); + override fun getItem(position: Int): ParticipantDisplayItem { + return participantDisplayItems[position] } - @Override - public long getItemId(int position) { - return 0; + override fun getItemId(position: Int): Long { + return 0 } - @Override - public View getView(int position, View convertView, ViewGroup parent) { - ParticipantDisplayItem participantDisplayItem = getItem(position); + @Suppress("Detekt.LongMethod", "Detekt.TooGenericExceptionCaught") + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + var convertView = convertView + val participantDisplayItem = getItem(position) - SurfaceViewRenderer surfaceViewRenderer; + val surfaceViewRenderer: SurfaceViewRenderer if (convertView == null) { - convertView = LayoutInflater.from(mContext).inflate(R.layout.call_item, parent, false); - convertView.setVisibility(View.VISIBLE); + convertView = LayoutInflater.from(mContext).inflate(R.layout.call_item, parent, false) + convertView.visibility = View.VISIBLE - surfaceViewRenderer = convertView.findViewById(R.id.surface_view); + surfaceViewRenderer = convertView.findViewById(R.id.surface_view) try { - Log.d(TAG, "hasSurface: " + participantDisplayItem.getRootEglBase().hasSurface()); + Log.d(TAG, "hasSurface: " + participantDisplayItem.rootEglBase.hasSurface()) - surfaceViewRenderer.setMirror(false); - surfaceViewRenderer.init(participantDisplayItem.getRootEglBase().getEglBaseContext(), null); - surfaceViewRenderer.setZOrderMediaOverlay(false); + surfaceViewRenderer.setMirror(false) + surfaceViewRenderer.init(participantDisplayItem.rootEglBase.eglBaseContext, null) + surfaceViewRenderer.setZOrderMediaOverlay(false) // disabled because it causes some devices to crash - surfaceViewRenderer.setEnableHardwareScaler(false); - surfaceViewRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT); - } catch (Exception e) { - Log.e(TAG, "error while initializing surfaceViewRenderer", e); + surfaceViewRenderer.setEnableHardwareScaler(false) + surfaceViewRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT) + } catch (e: Exception) { + Log.e(TAG, "error while initializing surfaceViewRenderer", e) } } else { - surfaceViewRenderer = convertView.findViewById(R.id.surface_view); + surfaceViewRenderer = convertView.findViewById(R.id.surface_view) } - ProgressBar progressBar = convertView.findViewById(R.id.participant_progress_bar); - if (!participantDisplayItem.isConnected()) { - progressBar.setVisibility(View.VISIBLE); + val progressBar = convertView!!.findViewById(R.id.participant_progress_bar) + if (!participantDisplayItem.isConnected) { + progressBar.visibility = View.VISIBLE } else { - progressBar.setVisibility(View.GONE); + progressBar.visibility = View.GONE } - ViewGroup.LayoutParams layoutParams = convertView.getLayoutParams(); - layoutParams.height = scaleGridViewItemHeight(); - convertView.setLayoutParams(layoutParams); + val layoutParams = convertView.layoutParams + layoutParams.height = scaleGridViewItemHeight() + convertView.layoutParams = layoutParams - TextView nickTextView = convertView.findViewById(R.id.peer_nick_text_view); - ImageView imageView = convertView.findViewById(R.id.avatarImageView); + val nickTextView = convertView.findViewById(R.id.peer_nick_text_view) + val imageView = convertView.findViewById(R.id.avatarImageView) - MediaStream mediaStream = participantDisplayItem.getMediaStream(); + val mediaStream = participantDisplayItem.mediaStream 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); + val videoTrack = mediaStream.videoTracks[0] + videoTrack.addSink(surfaceViewRenderer) + imageView.visibility = View.INVISIBLE + surfaceViewRenderer.visibility = View.VISIBLE + nickTextView.visibility = View.GONE } else { - imageView.setVisibility(View.VISIBLE); - surfaceViewRenderer.setVisibility(View.INVISIBLE); + imageView.visibility = View.VISIBLE + surfaceViewRenderer.visibility = View.INVISIBLE - if (((CallActivity) mContext).isInPipMode) { - nickTextView.setVisibility(View.GONE); + if ((mContext as CallActivity).isInPipMode) { + nickTextView.visibility = View.GONE } else { - nickTextView.setVisibility(View.VISIBLE); - nickTextView.setText(participantDisplayItem.getNick()); + nickTextView.visibility = View.VISIBLE + nickTextView.text = participantDisplayItem.nick } - if (participantDisplayItem.getActorType() == Participant.ActorType.GUESTS || - participantDisplayItem.getActorType() == Participant.ActorType.EMAILS) { - ImageViewExtensionsKt.loadFirstLetterAvatar( - imageView, - String.valueOf(participantDisplayItem.getNick()) - ); + if (participantDisplayItem.actorType == Participant.ActorType.GUESTS || + participantDisplayItem.actorType == Participant.ActorType.EMAILS + ) { + imageView + .loadFirstLetterAvatar( + participantDisplayItem.nick.toString() + ) } else { - ImageViewExtensionsKt.loadAvatarWithUrl(imageView,null, participantDisplayItem.getUrlForAvatar()); + imageView.loadAvatarWithUrl(null, participantDisplayItem.urlForAvatar) } } - ImageView audioOffView = convertView.findViewById(R.id.remote_audio_off); - if (!participantDisplayItem.isAudioEnabled()) { - audioOffView.setVisibility(View.VISIBLE); + val audioOffView = convertView.findViewById(R.id.remote_audio_off) + if (!participantDisplayItem.isAudioEnabled) { + audioOffView.visibility = View.VISIBLE } else { - audioOffView.setVisibility(View.GONE); + audioOffView.visibility = View.GONE } - ImageView raisedHandView = convertView.findViewById(R.id.raised_hand); - if (participantDisplayItem.getRaisedHand() != null && participantDisplayItem.getRaisedHand().getState()) { - raisedHandView.setVisibility(View.VISIBLE); + val raisedHandView = convertView.findViewById(R.id.raised_hand) + if (participantDisplayItem.raisedHand != null && participantDisplayItem.raisedHand.state) { + raisedHandView.visibility = View.VISIBLE } else { - raisedHandView.setVisibility(View.GONE); + raisedHandView.visibility = View.GONE } - return convertView; + return convertView } - private boolean hasVideoStream(ParticipantDisplayItem participantDisplayItem, MediaStream mediaStream) { - if (!participantDisplayItem.isStreamEnabled()) { - return false; + @Suppress("ReturnCount") + private fun hasVideoStream(participantDisplayItem: ParticipantDisplayItem, mediaStream: MediaStream?): Boolean { + if (!participantDisplayItem.isStreamEnabled) { + return false } - if (mediaStream == null || mediaStream.videoTracks == null) { - return false; + if (mediaStream?.videoTracks == null) { + return false } - for (VideoTrack t : mediaStream.videoTracks) { + for (t in mediaStream.videoTracks) { if (MediaStreamTrack.State.LIVE == t.state()) { - return true; + return true } } - return false; + return false } - private int scaleGridViewItemHeight() { - int headerHeight = 0; - int callControlsHeight = 0; - if (callInfosLinearLayout.getVisibility() == View.VISIBLE && isVoiceOnlyCall) { - headerHeight = callInfosLinearLayout.getHeight(); + private fun scaleGridViewItemHeight(): Int { + var headerHeight = 0 + var callControlsHeight = 0 + if (callInfosLinearLayout.visibility == View.VISIBLE && isVoiceOnlyCall) { + headerHeight = callInfosLinearLayout.height } if (isVoiceOnlyCall) { - callControlsHeight = Math.round(mContext.getResources().getDimension(R.dimen.call_controls_height)); + callControlsHeight = Math.round(mContext.resources.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)); + var itemHeight = (gridViewWrapper.height - headerHeight - callControlsHeight) / getRowsCount(count) + val itemMinHeight = Math.round(mContext.resources.getDimension(R.dimen.call_grid_item_min_height)) if (itemHeight < itemMinHeight) { - itemHeight = itemMinHeight; + itemHeight = itemMinHeight } - return itemHeight; + return itemHeight } - private int getRowsCount(int items) { - int rows = (int) Math.ceil((double) items / (double) columns); + private fun getRowsCount(items: Int): Int { + var rows = ceil(items.toDouble() / columns.toDouble()).toInt() if (rows == 0) { - rows = 1; + rows = 1 } - return rows; + return rows + } + + companion object { + private const val TAG = "ParticipantsAdapter" } } From 4b8b7630a937d785f0e6d842efb1bad5fd503108 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Mon, 5 May 2025 18:59:47 +0200 Subject: [PATCH 03/28] Rename .java to .kt Signed-off-by: Marcel Hibbe --- .../{ParticipantDisplayItem.java => ParticipantDisplayItem.kt} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/nextcloud/talk/adapters/{ParticipantDisplayItem.java => ParticipantDisplayItem.kt} (100%) diff --git a/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.java b/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt similarity index 100% rename from app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.java rename to app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt From feeec78ab442504b05e522ee4ad36f088f542b83 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Mon, 5 May 2025 18:59:48 +0200 Subject: [PATCH 04/28] WIP migrate call grid to compose Signed-off-by: Marcel Hibbe --- .../nextcloud/talk/activities/CallActivity.kt | 225 +++++++------ .../talk/adapters/ParticipantDisplayItem.kt | 318 +++++++++--------- .../talk/adapters/ParticipantsAdapter.kt | 204 ----------- .../nextcloud/talk/call/ParticipantUiState.kt | 21 ++ .../call/components/AvatarWithFallback.kt | 46 +++ .../talk/call/components/ParticipantGrid.kt | 34 ++ .../talk/call/components/ParticipantTile.kt | 180 ++++++++++ .../talk/call/components/WebRTCVideoView.kt | 23 ++ app/src/main/res/layout/call_activity.xml | 23 +- 9 files changed, 603 insertions(+), 471 deletions(-) delete mode 100644 app/src/main/java/com/nextcloud/talk/adapters/ParticipantsAdapter.kt create mode 100644 app/src/main/java/com/nextcloud/talk/call/ParticipantUiState.kt create mode 100644 app/src/main/java/com/nextcloud/talk/call/components/AvatarWithFallback.kt create mode 100644 app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt create mode 100644 app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt create mode 100644 app/src/main/java/com/nextcloud/talk/call/components/WebRTCVideoView.kt diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt index 06a454703..879217a6f 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt @@ -42,24 +42,24 @@ import android.view.OrientationEventListener import android.view.View import android.view.View.OnTouchListener import android.view.ViewGroup -import android.view.ViewTreeObserver.OnGlobalLayoutListener -import android.widget.AdapterView import android.widget.FrameLayout import android.widget.RelativeLayout import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.DrawableRes import androidx.appcompat.app.AlertDialog +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.mutableStateListOf import androidx.core.graphics.drawable.DrawableCompat import androidx.core.graphics.toColorInt import androidx.core.net.toUri import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import autodagger.AutoInjector import com.bluelinelabs.logansquare.LoganSquare import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.snackbar.Snackbar import com.nextcloud.talk.R import com.nextcloud.talk.adapters.ParticipantDisplayItem -import com.nextcloud.talk.adapters.ParticipantsAdapter import com.nextcloud.talk.api.NcApi import com.nextcloud.talk.application.NextcloudTalkApplication import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication @@ -73,7 +73,9 @@ import com.nextcloud.talk.call.MessageSender import com.nextcloud.talk.call.MessageSenderMcu import com.nextcloud.talk.call.MessageSenderNoMcu import com.nextcloud.talk.call.MutableLocalCallParticipantModel +import com.nextcloud.talk.call.ParticipantUiState import com.nextcloud.talk.call.ReactionAnimator +import com.nextcloud.talk.call.components.ParticipantGrid import com.nextcloud.talk.chat.ChatActivity import com.nextcloud.talk.data.user.model.User import com.nextcloud.talk.databinding.CallActivityBinding @@ -152,6 +154,7 @@ import io.reactivex.Observer import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers +import kotlinx.coroutines.launch import okhttp3.Cache import org.apache.commons.lang3.StringEscapeUtils import org.greenrobot.eventbus.Subscribe @@ -303,8 +306,10 @@ class CallActivity : CallBaseActivity() { private var handler: Handler? = null private var currentCallStatus: CallStatus? = null private var mediaPlayer: MediaPlayer? = null - private var participantDisplayItems: MutableMap? = null - private var participantsAdapter: ParticipantsAdapter? = null + + private val participantItems = mutableStateListOf() + private val participantUiStates = mutableStateListOf() + private var binding: CallActivityBinding? = null private var audioOutputDialog: AudioOutputDialog? = null private var moreCallActionsDialog: MoreCallActionsDialog? = null @@ -399,7 +404,6 @@ class CallActivity : CallBaseActivity() { .setRepeatCount(PulseAnimation.INFINITE) .setRepeatMode(PulseAnimation.REVERSE) callParticipants = HashMap() - participantDisplayItems = HashMap() reactionAnimator = ReactionAnimator(context, binding!!.reactionAnimationWrapper, viewThemeUtils) checkInitialDevicePermissions() @@ -734,10 +738,15 @@ class CallActivity : CallBaseActivity() { } binding!!.switchSelfVideoButton.setOnClickListener { switchCamera() } - binding!!.gridview.onItemClickListener = - AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, _: Long -> - animateCallControls(true, 0) - } + + // binding!!.composeParticipantGrid.setOnClickListener { + // animateCallControls(true, 0) + // } + + // binding!!.composeParticipantGrid.onItemClickListener = + // AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, _: Long -> + // animateCallControls(true, 0) + // } binding!!.lowerHandButton.setOnClickListener { l: View? -> raiseHandViewModel!!.lowerHand() } binding!!.pictureInPictureButton.setOnClickListener { enterPipMode() } } @@ -890,20 +899,20 @@ class CallActivity : CallBaseActivity() { val callControlsHeight = applicationContext.resources.getDimension(R.dimen.call_controls_height).roundToInt() params.setMargins(0, 0, 0, callControlsHeight) - binding!!.gridview.layoutParams = params + binding!!.composeParticipantGrid.layoutParams = params } else { val params = RelativeLayout.LayoutParams( ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT ) params.setMargins(0, 0, 0, 0) - binding!!.gridview.layoutParams = params + binding!!.composeParticipantGrid.layoutParams = params if (cameraEnumerator!!.deviceNames.size < 2) { binding!!.switchSelfVideoButton.visibility = View.GONE } initSelfVideoViewForNormalMode() } - binding!!.gridview.setOnTouchListener { _, me -> + binding!!.composeParticipantGrid.setOnTouchListener { _, me -> val action = me.actionMasked if (action == MotionEvent.ACTION_DOWN) { animateCallControls(true, 0) @@ -920,7 +929,7 @@ class CallActivity : CallBaseActivity() { false } animateCallControls(true, 0) - initGridAdapter() + initGrid() } @SuppressLint("ClickableViewAccessibility") @@ -951,56 +960,34 @@ class CallActivity : CallBaseActivity() { localVideoTrack!!.addSink(binding!!.pipSelfVideoRenderer) } - private fun initGridAdapter() { - Log.d(TAG, "initGridAdapter") - val columns: Int - val participantsInGrid = participantDisplayItems!!.size - columns = if (resources != null && - resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT - ) { - if (participantsInGrid > 2) { - GRID_MAX_COLUMN_COUNT_PORTRAIT - } else { - GRID_MIN_COLUMN_COUNT_PORTRAIT + private fun initGrid() { + Log.d(TAG, "initGrid") + + val participantsInGrid = participantUiStates.size + val columns = when { + resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT -> { + if (participantsInGrid > 2) GRID_MAX_COLUMN_COUNT_PORTRAIT else GRID_MIN_COLUMN_COUNT_PORTRAIT } - } else { - if (participantsInGrid > 2) { - GRID_MAX_COLUMN_COUNT_LANDSCAPE - } else if (participantsInGrid > 1) { - GRID_MIN_GROUP_COLUMN_COUNT_LANDSCAPE - } else { - GRID_MIN_COLUMN_COUNT_LANDSCAPE + else -> { + if (participantsInGrid > 2) { + GRID_MAX_COLUMN_COUNT_LANDSCAPE + } else if (participantsInGrid > 1) { + GRID_MIN_GROUP_COLUMN_COUNT_LANDSCAPE + } else { + GRID_MIN_COLUMN_COUNT_LANDSCAPE + } } } - binding!!.gridview.numColumns = columns - binding!!.conversationRelativeLayout - .viewTreeObserver - .addOnGlobalLayoutListener(object : OnGlobalLayoutListener { - override fun onGlobalLayout() { - binding!!.conversationRelativeLayout.viewTreeObserver.removeOnGlobalLayoutListener(this) - val height = binding!!.conversationRelativeLayout.measuredHeight - binding!!.gridview.minimumHeight = height - } - }) - binding!!.callInfosLinearLayout - .viewTreeObserver - .addOnGlobalLayoutListener(object : OnGlobalLayoutListener { - override fun onGlobalLayout() { - binding!!.callInfosLinearLayout.viewTreeObserver.removeOnGlobalLayoutListener(this) - } - }) - if (participantsAdapter != null) { - participantsAdapter!!.destroy() + + binding!!.composeParticipantGrid.setContent { + MaterialTheme { + ParticipantGrid( + participants = participantUiStates.toList(), + columns = columns + ) + } } - participantsAdapter = ParticipantsAdapter( - this, - participantDisplayItems!!.toMap(), - binding!!.conversationRelativeLayout, - binding!!.callInfosLinearLayout, - columns, - isVoiceOnlyCall - ) - binding!!.gridview.adapter = participantsAdapter + if (isInPipMode) { updateUiForPipMode() } @@ -2116,6 +2103,7 @@ class CallActivity : CallBaseActivity() { videoCapturer!!.dispose() videoCapturer = null } + binding!!.selfVideoRenderer.clearImage() binding!!.selfVideoRenderer.release() if (audioSource != null) { audioSource!!.dispose() @@ -2570,19 +2558,42 @@ class CallActivity : CallBaseActivity() { runOnUiThread { removeParticipantDisplayItem(sessionId, "video") } } + // private fun removeParticipantDisplayItem(sessionId: String?, videoStreamType: String) { + // Log.d(TAG, "removeParticipantDisplayItem") + // val participantDisplayItem = participantDisplayItems!!.remove("$sessionId-$videoStreamType") ?: return + // participantDisplayItem.destroy() + // if (!isDestroyed) { + // initGridAdapter() + // } + // } + + // private fun removeParticipantDisplayItem(sessionId: String?, videoStreamType: String) { + // val key = "$sessionId-$videoStreamType" + // + // val participant = participantItems.find { it.sessionKey == key } + // participant?.destroy() + // participantItems.remove(participant) + // + // initGrid() + // } + private fun removeParticipantDisplayItem(sessionId: String?, videoStreamType: String) { - Log.d(TAG, "removeParticipantDisplayItem") - val participantDisplayItem = participantDisplayItems!!.remove("$sessionId-$videoStreamType") ?: return - participantDisplayItem.destroy() - if (!isDestroyed) { - initGridAdapter() - } + val key = "$sessionId-$videoStreamType" + + val participant = participantItems.find { it.sessionKey == key } + participant?.destroy() + participantItems.removeAll { it.sessionKey == key } + + // Also remove UI state + participantUiStates.removeAll { it.sessionKey == key } + + initGrid() } @Subscribe(threadMode = ThreadMode.MAIN) fun onMessageEvent(configurationChangeEvent: ConfigurationChangeEvent?) { powerManagerUtils!!.setOrientation(Objects.requireNonNull(resources).configuration.orientation) - initGridAdapter() + initGrid() } private fun updateSelfVideoViewIceConnectionState(iceConnectionState: IceConnectionState) { @@ -2677,22 +2688,37 @@ class CallActivity : CallBaseActivity() { } private fun addParticipantDisplayItem(callParticipantModel: CallParticipantModel, videoStreamType: String) { - if (callParticipantModel.isInternal != null && callParticipantModel.isInternal) { - return - } + if (callParticipantModel.isInternal == true) return + val defaultGuestNick = resources.getString(R.string.nc_nick_guest) val participantDisplayItem = ParticipantDisplayItem( - context, - baseUrl, - defaultGuestNick, - rootEglBase, - videoStreamType, - roomToken, - callParticipantModel + context = context, + baseUrl = baseUrl!!, + defaultGuestNick = defaultGuestNick, + rootEglBase = rootEglBase!!, + streamType = videoStreamType, + roomToken = roomToken!!, + callParticipantModel = callParticipantModel ) - val sessionId = callParticipantModel.sessionId - participantDisplayItems!!["$sessionId-$videoStreamType"] = participantDisplayItem - initGridAdapter() + + val sessionKey = participantDisplayItem.sessionKey + + if (participantItems.none { it.sessionKey == sessionKey }) { + participantItems.add(participantDisplayItem) + + lifecycleScope.launch { + participantDisplayItem.uiStateFlow.collect { uiState -> + val index = participantUiStates.indexOfFirst { it.sessionKey == sessionKey } + if (index >= 0) { + participantUiStates[index] = uiState + } else { + participantUiStates.add(uiState) + } + } + } + } + + initGrid() } private fun setCallState(callState: CallStatus) { @@ -2725,7 +2751,7 @@ class CallActivity : CallBaseActivity() { binding!!.callModeTextView.text = descriptionForCallType binding!!.callStates.callStateTextView.setText(R.string.nc_leaving_call) binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE - binding!!.gridview.visibility = View.INVISIBLE + binding!!.composeParticipantGrid.visibility = View.INVISIBLE binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE binding!!.callStates.errorImageView.visibility = View.GONE } @@ -2737,8 +2763,8 @@ class CallActivity : CallBaseActivity() { if (binding!!.callStates.callStateRelativeLayout.visibility != View.VISIBLE) { binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE } - if (binding!!.gridview.visibility != View.INVISIBLE) { - binding!!.gridview.visibility = View.INVISIBLE + if (binding!!.composeParticipantGrid.visibility != View.INVISIBLE) { + binding!!.composeParticipantGrid.visibility = View.INVISIBLE } if (binding!!.callStates.callStateProgressBar.visibility != View.GONE) { binding!!.callStates.callStateProgressBar.visibility = View.GONE @@ -2764,8 +2790,8 @@ class CallActivity : CallBaseActivity() { if (binding!!.callStates.callStateProgressBar.visibility != View.GONE) { binding!!.callStates.callStateProgressBar.visibility = View.GONE } - if (binding!!.gridview.visibility != View.VISIBLE) { - binding!!.gridview.visibility = View.VISIBLE + if (binding!!.composeParticipantGrid.visibility != View.VISIBLE) { + binding!!.composeParticipantGrid.visibility = View.VISIBLE } if (binding!!.callStates.errorImageView.visibility != View.GONE) { binding!!.callStates.errorImageView.visibility = View.GONE @@ -2785,8 +2811,8 @@ class CallActivity : CallBaseActivity() { if (binding!!.callStates.callStateProgressBar.visibility != View.VISIBLE) { binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE } - if (binding!!.gridview.visibility != View.INVISIBLE) { - binding!!.gridview.visibility = View.INVISIBLE + if (binding!!.composeParticipantGrid.visibility != View.INVISIBLE) { + binding!!.composeParticipantGrid.visibility = View.INVISIBLE } if (binding!!.callStates.errorImageView.visibility != View.GONE) { binding!!.callStates.errorImageView.visibility = View.GONE @@ -2800,8 +2826,8 @@ class CallActivity : CallBaseActivity() { if (binding!!.callStates.callStateRelativeLayout.visibility != View.VISIBLE) { binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE } - if (binding!!.gridview.visibility != View.INVISIBLE) { - binding!!.gridview.visibility = View.INVISIBLE + if (binding!!.composeParticipantGrid.visibility != View.INVISIBLE) { + binding!!.composeParticipantGrid.visibility = View.INVISIBLE } if (binding!!.callStates.callStateProgressBar.visibility != View.VISIBLE) { binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE @@ -2818,8 +2844,8 @@ class CallActivity : CallBaseActivity() { if (binding!!.callStates.callStateRelativeLayout.visibility != View.VISIBLE) { binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE } - if (binding!!.gridview.visibility != View.INVISIBLE) { - binding!!.gridview.visibility = View.INVISIBLE + if (binding!!.composeParticipantGrid.visibility != View.INVISIBLE) { + binding!!.composeParticipantGrid.visibility = View.INVISIBLE } if (binding!!.callStates.callStateProgressBar.visibility != View.VISIBLE) { binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE @@ -2839,8 +2865,8 @@ class CallActivity : CallBaseActivity() { if (binding!!.callStates.callStateProgressBar.visibility != View.GONE) { binding!!.callStates.callStateProgressBar.visibility = View.GONE } - if (binding!!.gridview.visibility != View.INVISIBLE) { - binding!!.gridview.visibility = View.INVISIBLE + if (binding!!.composeParticipantGrid.visibility != View.INVISIBLE) { + binding!!.composeParticipantGrid.visibility = View.INVISIBLE } binding!!.callStates.errorImageView.setImageResource(R.drawable.ic_av_timer_timer_24dp) if (binding!!.callStates.errorImageView.visibility != View.VISIBLE) { @@ -2860,8 +2886,8 @@ class CallActivity : CallBaseActivity() { if (binding!!.callStates.callStateRelativeLayout.visibility != View.VISIBLE) { binding!!.callStates.callStateRelativeLayout.visibility = View.VISIBLE } - if (binding!!.gridview.visibility != View.INVISIBLE) { - binding!!.gridview.visibility = View.INVISIBLE + if (binding!!.composeParticipantGrid.visibility != View.INVISIBLE) { + binding!!.composeParticipantGrid.visibility = View.INVISIBLE } if (binding!!.callStates.callStateProgressBar.visibility != View.VISIBLE) { binding!!.callStates.callStateProgressBar.visibility = View.VISIBLE @@ -3021,8 +3047,8 @@ class CallActivity : CallBaseActivity() { removeParticipantDisplayItem(sessionId, "screen") return } - val hasScreenParticipantDisplayItem = participantDisplayItems!!["$sessionId-screen"] != null - if (!hasScreenParticipantDisplayItem) { + val screenParticipantDisplayItem = participantItems.find { it.sessionKey == "$sessionId-screen" } + if (screenParticipantDisplayItem != null) { addParticipantDisplayItem(callParticipantModel, "screen") } } @@ -3230,14 +3256,15 @@ class CallActivity : CallBaseActivity() { ViewGroup.LayoutParams.WRAP_CONTENT ) params.setMargins(0, 0, 0, 0) - binding!!.gridview.layoutParams = params + binding!!.composeParticipantGrid.layoutParams = params binding!!.callControls.visibility = View.GONE binding!!.callInfosLinearLayout.visibility = View.GONE binding!!.selfVideoViewWrapper.visibility = View.GONE binding!!.callStates.callStateRelativeLayout.visibility = View.GONE + binding!!.selfVideoRenderer.clearImage() binding!!.selfVideoRenderer.release() - if (participantDisplayItems!!.size > 1) { + if (participantItems.size > 1) { binding!!.pipCallConversationNameTextView.text = conversationName binding!!.pipSelfVideoOverlay.visibility = View.VISIBLE initSelfVideoViewForPipMode() diff --git a/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt index 122fd090a..64c1a0839 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt @@ -3,201 +3,201 @@ * * SPDX-FileCopyrightText: 2023 Andy Scherzinger * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez - * SPDX-FileCopyrightText: 2021 Marcel Hibbe + * SPDX-FileCopyrightText: 2021-2025 Marcel Hibbe * SPDX-License-Identifier: GPL-3.0-or-later */ -package com.nextcloud.talk.adapters; +package com.nextcloud.talk.adapters -import android.content.Context; -import android.os.Handler; -import android.os.Looper; -import android.text.TextUtils; +import android.content.Context +import android.os.Handler +import android.os.Looper +import android.text.TextUtils +import android.util.Log +import android.view.ViewGroup +import com.nextcloud.talk.call.CallParticipantModel +import com.nextcloud.talk.call.ParticipantUiState +import com.nextcloud.talk.call.RaisedHand +import com.nextcloud.talk.models.json.participants.Participant.ActorType +import com.nextcloud.talk.utils.ApiUtils.getUrlForAvatar +import com.nextcloud.talk.utils.ApiUtils.getUrlForFederatedAvatar +import com.nextcloud.talk.utils.ApiUtils.getUrlForGuestAvatar +import com.nextcloud.talk.utils.DisplayUtils.isDarkModeOn +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import org.webrtc.EglBase +import org.webrtc.MediaStream +import org.webrtc.PeerConnection.IceConnectionState +import org.webrtc.SurfaceViewRenderer -import com.nextcloud.talk.call.CallParticipantModel; -import com.nextcloud.talk.call.RaisedHand; -import com.nextcloud.talk.models.json.participants.Participant; -import com.nextcloud.talk.utils.ApiUtils; -import com.nextcloud.talk.utils.DisplayUtils; +class ParticipantDisplayItem( + private val context: Context, + private val baseUrl: String, + private val defaultGuestNick: String, + val rootEglBase: EglBase, + private val streamType: String, + private val roomToken: String, + private val callParticipantModel: CallParticipantModel +) { + private val participantDisplayItemNotifier = ParticipantDisplayItemNotifier() -import org.webrtc.EglBase; -import org.webrtc.MediaStream; -import org.webrtc.PeerConnection; + private val _uiStateFlow = MutableStateFlow(buildUiState()) + val uiStateFlow: StateFlow = _uiStateFlow.asStateFlow() -public class ParticipantDisplayItem { + private val session: String = callParticipantModel.sessionId - /** - * Shared handler to receive change notifications from the model on the main thread. - */ - private static final Handler handler = new Handler(Looper.getMainLooper()); + var actorType: ActorType? = null + private set + private var actorId: String? = null + private var userId: String? = null + private var iceConnectionState: IceConnectionState? = null + var nick: String? = null + get() = (if (TextUtils.isEmpty(userId) && TextUtils.isEmpty(field)) defaultGuestNick else field) - private final ParticipantDisplayItemNotifier participantDisplayItemNotifier = new ParticipantDisplayItemNotifier(); + var urlForAvatar: String? = null + private set + var mediaStream: MediaStream? = null + private set + var isStreamEnabled: Boolean = false + private set + var isAudioEnabled: Boolean = false + private set + var raisedHand: RaisedHand? = null + private set + var surfaceViewRenderer: SurfaceViewRenderer? = null - private final Context context; + val sessionKey: String + get() = "$session-$streamType" - private final String baseUrl; - private final String defaultGuestNick; - private final EglBase rootEglBase; - - private final String session; - private final String streamType; - - private final String roomToken; - - private final CallParticipantModel callParticipantModel; - - private Participant.ActorType actorType; - private String actorId; - private String userId; - private PeerConnection.IceConnectionState iceConnectionState; - private String nick; - private String urlForAvatar; - private MediaStream mediaStream; - private boolean streamEnabled; - private boolean isAudioEnabled; - private RaisedHand raisedHand; - - public interface Observer { - void onChange(); + interface Observer { + fun onChange() } - private final CallParticipantModel.Observer callParticipantModelObserver = new CallParticipantModel.Observer() { - @Override - public void onChange() { - updateFromModel(); + private val callParticipantModelObserver: CallParticipantModel.Observer = object : CallParticipantModel.Observer { + override fun onChange() { + updateFromModel() } - @Override - public void onReaction(String reaction) { + override fun onReaction(reaction: String) { } - }; - - public ParticipantDisplayItem(Context context, String baseUrl, String defaultGuestNick, EglBase rootEglBase, - String streamType, String roomToken, CallParticipantModel callParticipantModel) { - this.context = context; - - this.baseUrl = baseUrl; - this.defaultGuestNick = defaultGuestNick; - this.rootEglBase = rootEglBase; - - this.session = callParticipantModel.getSessionId(); - this.streamType = streamType; - - this.roomToken = roomToken; - - this.callParticipantModel = callParticipantModel; - this.callParticipantModel.addObserver(callParticipantModelObserver, handler); - - updateFromModel(); } - public void destroy() { - this.callParticipantModel.removeObserver(callParticipantModelObserver); + init { + callParticipantModel.addObserver(callParticipantModelObserver, handler) + + updateFromModel() } - private void updateFromModel() { - actorType = callParticipantModel.getActorType(); - actorId = callParticipantModel.getActorId(); - userId = callParticipantModel.getUserId(); - nick = callParticipantModel.getNick(); + fun destroy() { + callParticipantModel.removeObserver(callParticipantModelObserver) - this.updateUrlForAvatar(); + surfaceViewRenderer?.let { renderer -> + try { + mediaStream?.videoTracks?.firstOrNull()?.removeSink(renderer) + renderer.release() + (renderer.parent as? ViewGroup)?.removeView(renderer) + } catch (e: Exception) { + Log.w("ParticipantDisplayItem", "Error releasing renderer", e) + } + } + surfaceViewRenderer = null + } - if ("screen".equals(streamType)) { - iceConnectionState = callParticipantModel.getScreenIceConnectionState(); - mediaStream = callParticipantModel.getScreenMediaStream(); - isAudioEnabled = true; - streamEnabled = true; + private fun updateFromModel() { + actorType = callParticipantModel.actorType + actorId = callParticipantModel.actorId + userId = callParticipantModel.userId + nick = callParticipantModel.nick + + updateUrlForAvatar() + + if (streamType == "screen") { + iceConnectionState = callParticipantModel.screenIceConnectionState + mediaStream = callParticipantModel.screenMediaStream + isAudioEnabled = true + isStreamEnabled = true } else { - iceConnectionState = callParticipantModel.getIceConnectionState(); - mediaStream = callParticipantModel.getMediaStream(); - isAudioEnabled = callParticipantModel.isAudioAvailable() != null ? - callParticipantModel.isAudioAvailable() : false; - streamEnabled = callParticipantModel.isVideoAvailable() != null ? - callParticipantModel.isVideoAvailable() : false; + iceConnectionState = callParticipantModel.iceConnectionState + mediaStream = callParticipantModel.mediaStream + isAudioEnabled = callParticipantModel.isAudioAvailable ?: false + isStreamEnabled = callParticipantModel.isVideoAvailable ?: false } - raisedHand = callParticipantModel.getRaisedHand(); + raisedHand = callParticipantModel.raisedHand - participantDisplayItemNotifier.notifyChange(); + if (surfaceViewRenderer == null && mediaStream != null) { + val renderer = SurfaceViewRenderer(context).apply { + init(rootEglBase.eglBaseContext, null) + setEnableHardwareScaler(true) + setMirror(false) + } + surfaceViewRenderer = renderer + mediaStream?.videoTracks?.firstOrNull()?.addSink(renderer) + } + + _uiStateFlow.value = buildUiState() + participantDisplayItemNotifier.notifyChange() } - private void updateUrlForAvatar() { - if (actorType == Participant.ActorType.FEDERATED) { - int darkTheme = DisplayUtils.INSTANCE.isDarkModeOn(context) ? 1 : 0; - urlForAvatar = ApiUtils.getUrlForFederatedAvatar(baseUrl, roomToken, actorId, darkTheme, true); + private fun buildUiState(): ParticipantUiState { + return ParticipantUiState( + sessionKey = sessionKey, + nick = nick ?: "Guest", + isConnected = isConnected, + isAudioEnabled = isAudioEnabled, + isStreamEnabled = isStreamEnabled, + raisedHand = raisedHand?.state == true, + avatarUrl = urlForAvatar, + surfaceViewRenderer = surfaceViewRenderer + ) + } + + private fun updateUrlForAvatar() { + if (actorType == ActorType.FEDERATED) { + val darkTheme = if (isDarkModeOn(context)) 1 else 0 + urlForAvatar = getUrlForFederatedAvatar(baseUrl, roomToken, actorId!!, darkTheme, true) } else if (!TextUtils.isEmpty(userId)) { - urlForAvatar = ApiUtils.getUrlForAvatar(baseUrl, userId, true); + urlForAvatar = getUrlForAvatar(baseUrl, userId, true) } else { - urlForAvatar = ApiUtils.getUrlForGuestAvatar(baseUrl, getNick(), true); + urlForAvatar = getUrlForGuestAvatar(baseUrl, nick, true) } } - public boolean isConnected() { - return iceConnectionState == PeerConnection.IceConnectionState.CONNECTED || - iceConnectionState == PeerConnection.IceConnectionState.COMPLETED || - // If there is no connection state that means that no connection is needed, so it is a special case that is - // also seen as "connected". - iceConnectionState == null; + val isConnected: Boolean + get() = iceConnectionState == IceConnectionState.CONNECTED || + iceConnectionState == IceConnectionState.COMPLETED || + // If there is no connection state that means that no connection is needed, + // so it is a special case that is also seen as "connected". + iceConnectionState == null + + fun addObserver(observer: Observer?) { + participantDisplayItemNotifier.addObserver(observer) } - public String getNick() { - if (TextUtils.isEmpty(userId) && TextUtils.isEmpty(nick)) { - return defaultGuestNick; - } - - return nick; + fun removeObserver(observer: Observer?) { + participantDisplayItemNotifier.removeObserver(observer) } - public String getUrlForAvatar() { - return urlForAvatar; - } - - public MediaStream getMediaStream() { - return mediaStream; - } - - public boolean isStreamEnabled() { - return streamEnabled; - } - - public EglBase getRootEglBase() { - return rootEglBase; - } - - public boolean isAudioEnabled() { - return isAudioEnabled; - } - - public RaisedHand getRaisedHand() { - return raisedHand; - } - - public Participant.ActorType getActorType() { - return actorType; - } - - public void addObserver(Observer observer) { - participantDisplayItemNotifier.addObserver(observer); - } - - public void removeObserver(Observer observer) { - participantDisplayItemNotifier.removeObserver(observer); - } - - @Override - public String toString() { + override fun toString(): String { return "ParticipantSession{" + - "userId='" + userId + '\'' + - ", actorType='" + actorType + '\'' + - ", actorId='" + actorId + '\'' + - ", session='" + session + '\'' + - ", nick='" + nick + '\'' + - ", urlForAvatar='" + urlForAvatar + '\'' + - ", mediaStream=" + mediaStream + - ", streamType='" + streamType + '\'' + - ", streamEnabled=" + streamEnabled + - ", rootEglBase=" + rootEglBase + - ", raisedHand=" + raisedHand + - '}'; + "userId='" + userId + '\'' + + ", actorType='" + actorType + '\'' + + ", actorId='" + actorId + '\'' + + ", session='" + session + '\'' + + ", nick='" + nick + '\'' + + ", urlForAvatar='" + urlForAvatar + '\'' + + ", mediaStream=" + mediaStream + + ", streamType='" + streamType + '\'' + + ", streamEnabled=" + isStreamEnabled + + ", rootEglBase=" + rootEglBase + + ", raisedHand=" + raisedHand + + '}' + } + + companion object { + /** + * Shared handler to receive change notifications from the model on the main thread. + */ + private val handler = Handler(Looper.getMainLooper()) } } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/ParticipantsAdapter.kt b/app/src/main/java/com/nextcloud/talk/adapters/ParticipantsAdapter.kt deleted file mode 100644 index 69dfead61..000000000 --- a/app/src/main/java/com/nextcloud/talk/adapters/ParticipantsAdapter.kt +++ /dev/null @@ -1,204 +0,0 @@ -/* - * Nextcloud Talk - Android Client - * - * SPDX-FileCopyrightText: 2021-2023 Marcel Hibbe - * SPDX-FileCopyrightText: 2022 Daniel Calviño Sánchez - * SPDX-License-Identifier: GPL-3.0-or-later - */ -package com.nextcloud.talk.adapters - -import android.content.Context -import android.util.Log -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.BaseAdapter -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.ProgressBar -import android.widget.RelativeLayout -import android.widget.TextView -import com.nextcloud.talk.R -import com.nextcloud.talk.activities.CallActivity -import com.nextcloud.talk.extensions.loadAvatarWithUrl -import com.nextcloud.talk.extensions.loadFirstLetterAvatar -import com.nextcloud.talk.models.json.participants.Participant -import org.webrtc.MediaStream -import org.webrtc.MediaStreamTrack -import org.webrtc.RendererCommon -import org.webrtc.SurfaceViewRenderer -import kotlin.math.ceil - -class ParticipantsAdapter( - private val mContext: Context, - participantDisplayItems: Map, - private val gridViewWrapper: RelativeLayout, - private val callInfosLinearLayout: LinearLayout, - private val columns: Int, - private val isVoiceOnlyCall: Boolean -) : BaseAdapter() { - private val participantDisplayItemObserver = ParticipantDisplayItem.Observer { this.notifyDataSetChanged() } - - private val participantDisplayItems = ArrayList() - - init { - this.participantDisplayItems.addAll(participantDisplayItems.values) - - for (participantDisplayItem in this.participantDisplayItems) { - participantDisplayItem.addObserver(participantDisplayItemObserver) - } - } - - fun destroy() { - for (participantDisplayItem in participantDisplayItems) { - participantDisplayItem.removeObserver(participantDisplayItemObserver) - } - } - - override fun getCount(): Int { - return participantDisplayItems.size - } - - override fun getItem(position: Int): ParticipantDisplayItem { - return participantDisplayItems[position] - } - - override fun getItemId(position: Int): Long { - return 0 - } - - @Suppress("Detekt.LongMethod", "Detekt.TooGenericExceptionCaught") - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - var convertView = convertView - val participantDisplayItem = getItem(position) - - val surfaceViewRenderer: SurfaceViewRenderer - if (convertView == null) { - convertView = LayoutInflater.from(mContext).inflate(R.layout.call_item, parent, false) - convertView.visibility = View.VISIBLE - - surfaceViewRenderer = convertView.findViewById(R.id.surface_view) - try { - Log.d(TAG, "hasSurface: " + participantDisplayItem.rootEglBase.hasSurface()) - - surfaceViewRenderer.setMirror(false) - surfaceViewRenderer.init(participantDisplayItem.rootEglBase.eglBaseContext, null) - surfaceViewRenderer.setZOrderMediaOverlay(false) - // disabled because it causes some devices to crash - surfaceViewRenderer.setEnableHardwareScaler(false) - surfaceViewRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT) - } catch (e: Exception) { - Log.e(TAG, "error while initializing surfaceViewRenderer", e) - } - } else { - surfaceViewRenderer = convertView.findViewById(R.id.surface_view) - } - - val progressBar = convertView!!.findViewById(R.id.participant_progress_bar) - if (!participantDisplayItem.isConnected) { - progressBar.visibility = View.VISIBLE - } else { - progressBar.visibility = View.GONE - } - - val layoutParams = convertView.layoutParams - layoutParams.height = scaleGridViewItemHeight() - convertView.layoutParams = layoutParams - - val nickTextView = convertView.findViewById(R.id.peer_nick_text_view) - val imageView = convertView.findViewById(R.id.avatarImageView) - - val mediaStream = participantDisplayItem.mediaStream - if (hasVideoStream(participantDisplayItem, mediaStream)) { - val videoTrack = mediaStream.videoTracks[0] - videoTrack.addSink(surfaceViewRenderer) - imageView.visibility = View.INVISIBLE - surfaceViewRenderer.visibility = View.VISIBLE - nickTextView.visibility = View.GONE - } else { - imageView.visibility = View.VISIBLE - surfaceViewRenderer.visibility = View.INVISIBLE - - if ((mContext as CallActivity).isInPipMode) { - nickTextView.visibility = View.GONE - } else { - nickTextView.visibility = View.VISIBLE - nickTextView.text = participantDisplayItem.nick - } - if (participantDisplayItem.actorType == Participant.ActorType.GUESTS || - participantDisplayItem.actorType == Participant.ActorType.EMAILS - ) { - imageView - .loadFirstLetterAvatar( - participantDisplayItem.nick.toString() - ) - } else { - imageView.loadAvatarWithUrl(null, participantDisplayItem.urlForAvatar) - } - } - - val audioOffView = convertView.findViewById(R.id.remote_audio_off) - if (!participantDisplayItem.isAudioEnabled) { - audioOffView.visibility = View.VISIBLE - } else { - audioOffView.visibility = View.GONE - } - - val raisedHandView = convertView.findViewById(R.id.raised_hand) - if (participantDisplayItem.raisedHand != null && participantDisplayItem.raisedHand.state) { - raisedHandView.visibility = View.VISIBLE - } else { - raisedHandView.visibility = View.GONE - } - - return convertView - } - - @Suppress("ReturnCount") - private fun hasVideoStream(participantDisplayItem: ParticipantDisplayItem, mediaStream: MediaStream?): Boolean { - if (!participantDisplayItem.isStreamEnabled) { - return false - } - - if (mediaStream?.videoTracks == null) { - return false - } - - for (t in mediaStream.videoTracks) { - if (MediaStreamTrack.State.LIVE == t.state()) { - return true - } - } - - return false - } - - private fun scaleGridViewItemHeight(): Int { - var headerHeight = 0 - var callControlsHeight = 0 - if (callInfosLinearLayout.visibility == View.VISIBLE && isVoiceOnlyCall) { - headerHeight = callInfosLinearLayout.height - } - if (isVoiceOnlyCall) { - callControlsHeight = Math.round(mContext.resources.getDimension(R.dimen.call_controls_height)) - } - var itemHeight = (gridViewWrapper.height - headerHeight - callControlsHeight) / getRowsCount(count) - val itemMinHeight = Math.round(mContext.resources.getDimension(R.dimen.call_grid_item_min_height)) - if (itemHeight < itemMinHeight) { - itemHeight = itemMinHeight - } - return itemHeight - } - - private fun getRowsCount(items: Int): Int { - var rows = ceil(items.toDouble() / columns.toDouble()).toInt() - if (rows == 0) { - rows = 1 - } - return rows - } - - companion object { - private const val TAG = "ParticipantsAdapter" - } -} diff --git a/app/src/main/java/com/nextcloud/talk/call/ParticipantUiState.kt b/app/src/main/java/com/nextcloud/talk/call/ParticipantUiState.kt new file mode 100644 index 000000000..1804650d6 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/ParticipantUiState.kt @@ -0,0 +1,21 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.call + +import org.webrtc.SurfaceViewRenderer + +data class ParticipantUiState( + val sessionKey: String, + val nick: String, + val isConnected: Boolean, + val isAudioEnabled: Boolean, + val isStreamEnabled: Boolean, + val raisedHand: Boolean, + val avatarUrl: String?, + val surfaceViewRenderer: SurfaceViewRenderer? = null +) diff --git a/app/src/main/java/com/nextcloud/talk/call/components/AvatarWithFallback.kt b/app/src/main/java/com/nextcloud/talk/call/components/AvatarWithFallback.kt new file mode 100644 index 000000000..b39c279cf --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/components/AvatarWithFallback.kt @@ -0,0 +1,46 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.call.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.sp +import coil.compose.AsyncImage +import com.nextcloud.talk.call.ParticipantUiState + +@Composable +fun AvatarWithFallback(participant: ParticipantUiState) { + if (!participant.avatarUrl.isNullOrEmpty()) { + AsyncImage( + model = participant.avatarUrl, + contentDescription = "Avatar", + contentScale = ContentScale.Crop, + modifier = Modifier.fillMaxSize() + ) + } else { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Gray), + contentAlignment = Alignment.Center + ) { + Text( + text = participant.nick.firstOrNull()?.uppercase() ?: "?", + color = Color.White, + fontSize = 40.sp + ) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt new file mode 100644 index 000000000..a81216cb7 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt @@ -0,0 +1,34 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.call.components + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.grid.items +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.nextcloud.talk.call.ParticipantUiState + +@Composable +fun ParticipantGrid(participants: List, columns: Int = 2, modifier: Modifier = Modifier) { + LazyVerticalGrid( + columns = GridCells.Fixed(columns), + modifier = modifier.fillMaxSize(), + contentPadding = PaddingValues(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(participants, key = { it.sessionKey }) { participant -> + ParticipantTile(participant) + } + } +} diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt new file mode 100644 index 000000000..b1d98007e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt @@ -0,0 +1,180 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.call.components + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.unit.dp +import com.nextcloud.talk.R +import com.nextcloud.talk.call.ParticipantUiState + +@Composable +fun ParticipantTile(participant: ParticipantUiState) { + Box( + modifier = Modifier + .aspectRatio(3f / 4f) + .clip(RoundedCornerShape(12.dp)) + .background(Color.DarkGray) + ) { + if (participant.isStreamEnabled && participant.surfaceViewRenderer != null) { + WebRTCVideoView(participant.surfaceViewRenderer) + } else { + AvatarWithFallback(participant) + } + + if (participant.raisedHand) { + Icon( + painter = painterResource(id = R.drawable.ic_hand_back_left), + contentDescription = "Raised Hand", + modifier = Modifier + .align(Alignment.TopEnd) + .padding(6.dp) + .size(24.dp), + tint = Color.White + ) + } + + if (!participant.isAudioEnabled) { + Icon( + painter = painterResource(id = R.drawable.ic_mic_off_white_24px), + contentDescription = "Mic Off", + modifier = Modifier + .align(Alignment.BottomEnd) + .padding(6.dp) + .size(24.dp), + tint = Color.White + ) + } + + Text( + text = participant.nick, + color = Color.White, + modifier = Modifier + .align(Alignment.BottomStart) + .background(Color.Black.copy(alpha = 0.6f)) + .padding(6.dp), + style = MaterialTheme.typography.bodyMedium + ) + } +} + +// @Composable +// fun ParticipantItem(participant: ParticipantDisplayItem) { +// val context = LocalContext.current +// val videoTrack = participant.mediaStream?.videoTracks?.firstOrNull() +// +// Box( +// modifier = Modifier +// .aspectRatio(1f) +// .background(Color.Black) +// .padding(4.dp) +// ) { +// // Renderer +// participant.surfaceViewRenderer?.let { renderer -> +// AndroidView( +// factory = { +// // If not yet initialized +// if (renderer.parent != null) { +// (renderer.parent as? ViewGroup)?.removeView(renderer) +// } +// +// // if (!renderer.isInitialized) { // TODO +// renderer.init(participant.rootEglBase.eglBaseContext, null) +// renderer.setMirror(false) +// renderer.setZOrderMediaOverlay(false) +// renderer.setEnableHardwareScaler(false) +// renderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT) +// // } +// +// // Attach sink +// try { +// videoTrack?.removeSink(renderer) +// } catch (_: Exception) {} +// videoTrack?.addSink(renderer) +// +// renderer +// }, +// modifier = Modifier.fillMaxSize(), +// update = { view -> +// view.visibility = +// if (videoTrack != null && participant.isConnected) View.VISIBLE else View.INVISIBLE +// } +// ) +// } +// +// // Overlay: Nick or Avatar +// if (videoTrack == null || !participant.isConnected) { +// Column( +// modifier = Modifier +// .fillMaxSize() +// .background(Color.DarkGray) +// .padding(8.dp), +// verticalArrangement = Arrangement.Center, +// horizontalAlignment = Alignment.CenterHorizontally +// ) { +// Text( +// text = participant.nick!!, +// color = Color.White, +// fontSize = 16.sp, +// modifier = Modifier.padding(bottom = 8.dp) +// ) +// // Replace this with image loader like Coil if needed +// Icon( +// imageVector = Icons.Default.Person, +// contentDescription = null, +// tint = Color.White, +// modifier = Modifier.size(40.dp) +// ) +// } +// } +// +// // Status indicators (audio muted / raised hand) +// Row( +// modifier = Modifier +// .align(Alignment.TopEnd) +// .padding(4.dp) +// ) { +// if (!participant.isAudioEnabled) { +// Icon( +// painter = painterResource(id = R.drawable.account_circle_96dp), +// contentDescription = "Mic Off", +// tint = Color.Red, +// modifier = Modifier.size(20.dp) +// ) +// } +// if (participant.raisedHand?.state == true) { +// Icon( +// painter = painterResource(id = R.drawable.ic_hand_back_left), +// contentDescription = "Hand Raised", +// tint = Color.Yellow, +// modifier = Modifier.size(20.dp) +// ) +// } +// } +// +// // Loading spinner +// if (!participant.isConnected) { +// CircularProgressIndicator( +// modifier = Modifier.align(Alignment.Center) +// ) +// } +// } +// } diff --git a/app/src/main/java/com/nextcloud/talk/call/components/WebRTCVideoView.kt b/app/src/main/java/com/nextcloud/talk/call/components/WebRTCVideoView.kt new file mode 100644 index 000000000..d7bb69984 --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/call/components/WebRTCVideoView.kt @@ -0,0 +1,23 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * SPDX-License-Identifier: GPL-3.0-or-later + */ + +package com.nextcloud.talk.call.components + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.viewinterop.AndroidView +import org.webrtc.SurfaceViewRenderer + +@Composable +fun WebRTCVideoView(surfaceViewRenderer: SurfaceViewRenderer) { + AndroidView( + factory = { surfaceViewRenderer }, + update = { /* No-op, renderer is already initialized and reused */ }, + modifier = Modifier.fillMaxSize() + ) +} diff --git a/app/src/main/res/layout/call_activity.xml b/app/src/main/res/layout/call_activity.xml index df7bca34f..ebc1287d5 100644 --- a/app/src/main/res/layout/call_activity.xml +++ b/app/src/main/res/layout/call_activity.xml @@ -33,15 +33,15 @@ android:visibility="visible" tools:visibility="visible"> - + + + + + + + + + + + From 42a8afded8fed39e6bb739365091494a65e9e13b Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Mon, 5 May 2025 19:49:07 +0200 Subject: [PATCH 05/28] fix z index of videos and click listener to show controls Signed-off-by: Marcel Hibbe --- .../nextcloud/talk/activities/CallActivity.kt | 13 +- .../talk/call/components/ParticipantGrid.kt | 12 +- .../talk/call/components/ParticipantTile.kt | 125 ++++-------------- app/src/main/res/layout/call_activity.xml | 18 +-- 4 files changed, 41 insertions(+), 127 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt index 879217a6f..52037903f 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt @@ -739,14 +739,6 @@ class CallActivity : CallBaseActivity() { binding!!.switchSelfVideoButton.setOnClickListener { switchCamera() } - // binding!!.composeParticipantGrid.setOnClickListener { - // animateCallControls(true, 0) - // } - - // binding!!.composeParticipantGrid.onItemClickListener = - // AdapterView.OnItemClickListener { _: AdapterView<*>?, _: View?, _: Int, _: Long -> - // animateCallControls(true, 0) - // } binding!!.lowerHandButton.setOnClickListener { l: View? -> raiseHandViewModel!!.lowerHand() } binding!!.pictureInPictureButton.setOnClickListener { enterPipMode() } } @@ -930,6 +922,7 @@ class CallActivity : CallBaseActivity() { } animateCallControls(true, 0) initGrid() + binding!!.composeParticipantGrid.z = 0f } @SuppressLint("ClickableViewAccessibility") @@ -984,7 +977,9 @@ class CallActivity : CallBaseActivity() { ParticipantGrid( participants = participantUiStates.toList(), columns = columns - ) + ) { + animateCallControls(true, 0) + } } } diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt index a81216cb7..8e25cded0 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt @@ -7,6 +7,7 @@ package com.nextcloud.talk.call.components +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.fillMaxSize @@ -19,10 +20,17 @@ import androidx.compose.ui.unit.dp import com.nextcloud.talk.call.ParticipantUiState @Composable -fun ParticipantGrid(participants: List, columns: Int = 2, modifier: Modifier = Modifier) { +fun ParticipantGrid( + participants: List, + columns: Int = 2, + modifier: Modifier = Modifier, + onClick: () -> Unit +) { LazyVerticalGrid( columns = GridCells.Fixed(columns), - modifier = modifier.fillMaxSize(), + modifier = modifier + .fillMaxSize() + .clickable { onClick() }, contentPadding = PaddingValues(8.dp), verticalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp) diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt index b1d98007e..754d689e3 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt @@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -22,6 +23,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.nextcloud.talk.R import com.nextcloud.talk.call.ParticipantUiState @@ -73,108 +75,27 @@ fun ParticipantTile(participant: ParticipantUiState) { .padding(6.dp), style = MaterialTheme.typography.bodyMedium ) + + if (!participant.isConnected) { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center) + ) + } } } -// @Composable -// fun ParticipantItem(participant: ParticipantDisplayItem) { -// val context = LocalContext.current -// val videoTrack = participant.mediaStream?.videoTracks?.firstOrNull() -// -// Box( -// modifier = Modifier -// .aspectRatio(1f) -// .background(Color.Black) -// .padding(4.dp) -// ) { -// // Renderer -// participant.surfaceViewRenderer?.let { renderer -> -// AndroidView( -// factory = { -// // If not yet initialized -// if (renderer.parent != null) { -// (renderer.parent as? ViewGroup)?.removeView(renderer) -// } -// -// // if (!renderer.isInitialized) { // TODO -// renderer.init(participant.rootEglBase.eglBaseContext, null) -// renderer.setMirror(false) -// renderer.setZOrderMediaOverlay(false) -// renderer.setEnableHardwareScaler(false) -// renderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT) -// // } -// -// // Attach sink -// try { -// videoTrack?.removeSink(renderer) -// } catch (_: Exception) {} -// videoTrack?.addSink(renderer) -// -// renderer -// }, -// modifier = Modifier.fillMaxSize(), -// update = { view -> -// view.visibility = -// if (videoTrack != null && participant.isConnected) View.VISIBLE else View.INVISIBLE -// } -// ) -// } -// -// // Overlay: Nick or Avatar -// if (videoTrack == null || !participant.isConnected) { -// Column( -// modifier = Modifier -// .fillMaxSize() -// .background(Color.DarkGray) -// .padding(8.dp), -// verticalArrangement = Arrangement.Center, -// horizontalAlignment = Alignment.CenterHorizontally -// ) { -// Text( -// text = participant.nick!!, -// color = Color.White, -// fontSize = 16.sp, -// modifier = Modifier.padding(bottom = 8.dp) -// ) -// // Replace this with image loader like Coil if needed -// Icon( -// imageVector = Icons.Default.Person, -// contentDescription = null, -// tint = Color.White, -// modifier = Modifier.size(40.dp) -// ) -// } -// } -// -// // Status indicators (audio muted / raised hand) -// Row( -// modifier = Modifier -// .align(Alignment.TopEnd) -// .padding(4.dp) -// ) { -// if (!participant.isAudioEnabled) { -// Icon( -// painter = painterResource(id = R.drawable.account_circle_96dp), -// contentDescription = "Mic Off", -// tint = Color.Red, -// modifier = Modifier.size(20.dp) -// ) -// } -// if (participant.raisedHand?.state == true) { -// Icon( -// painter = painterResource(id = R.drawable.ic_hand_back_left), -// contentDescription = "Hand Raised", -// tint = Color.Yellow, -// modifier = Modifier.size(20.dp) -// ) -// } -// } -// -// // Loading spinner -// if (!participant.isConnected) { -// CircularProgressIndicator( -// modifier = Modifier.align(Alignment.Center) -// ) -// } -// } -// } +@Preview(showBackground = false) +@Composable +fun ParticipantTilePreview() { + val participant = ParticipantUiState( + sessionKey = "", + nick = "testuser", + isConnected = true, + isAudioEnabled = false, + isStreamEnabled = true, + raisedHand = true, + avatarUrl = "", + surfaceViewRenderer = null + ) + ParticipantTile(participant) +} diff --git a/app/src/main/res/layout/call_activity.xml b/app/src/main/res/layout/call_activity.xml index ebc1287d5..ea97114e9 100644 --- a/app/src/main/res/layout/call_activity.xml +++ b/app/src/main/res/layout/call_activity.xml @@ -33,15 +33,10 @@ android:visibility="visible" tools:visibility="visible"> - - - - - - - - - + - - From aacc0134858ed2e50d2491a958b8a757995fb865 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Tue, 6 May 2025 14:30:45 +0200 Subject: [PATCH 06/28] improve call participants layout Signed-off-by: Marcel Hibbe --- .../nextcloud/talk/activities/CallActivity.kt | 19 +-- .../talk/call/components/ParticipantGrid.kt | 159 ++++++++++++++++-- .../talk/call/components/ParticipantTile.kt | 13 +- 3 files changed, 150 insertions(+), 41 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt index 52037903f..82a83dcb2 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt @@ -956,27 +956,10 @@ class CallActivity : CallBaseActivity() { private fun initGrid() { Log.d(TAG, "initGrid") - val participantsInGrid = participantUiStates.size - val columns = when { - resources.configuration.orientation == Configuration.ORIENTATION_PORTRAIT -> { - if (participantsInGrid > 2) GRID_MAX_COLUMN_COUNT_PORTRAIT else GRID_MIN_COLUMN_COUNT_PORTRAIT - } - else -> { - if (participantsInGrid > 2) { - GRID_MAX_COLUMN_COUNT_LANDSCAPE - } else if (participantsInGrid > 1) { - GRID_MIN_GROUP_COLUMN_COUNT_LANDSCAPE - } else { - GRID_MIN_COLUMN_COUNT_LANDSCAPE - } - } - } - binding!!.composeParticipantGrid.setContent { MaterialTheme { ParticipantGrid( - participants = participantUiStates.toList(), - columns = columns + participants = participantUiStates.toList() ) { animateCallControls(true, 0) } diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt index 8e25cded0..59d153c26 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt @@ -7,36 +7,159 @@ 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.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +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.call.ParticipantUiState @Composable -fun ParticipantGrid( - participants: List, - columns: Int = 2, - modifier: Modifier = Modifier, - onClick: () -> Unit -) { - LazyVerticalGrid( - columns = GridCells.Fixed(columns), - modifier = modifier - .fillMaxSize() - .clickable { onClick() }, - contentPadding = PaddingValues(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - items(participants, key = { it.sessionKey }) { participant -> - ParticipantTile(participant) +fun ParticipantGrid(modifier: Modifier = Modifier, participants: List, onClick: () -> Unit) { + val configuration = LocalConfiguration.current + val isPortrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT + + when (participants.size) { + 0 -> {} + 1 -> { + Box( + modifier = Modifier + .fillMaxSize() + ) { + ParticipantTile( + participant = participants[0], + modifier = Modifier + .fillMaxSize() + .clickable { onClick() } + ) + } + } + + 2, 3 -> { + if (isPortrait) { + Column( + modifier = Modifier + .fillMaxSize() + .padding(vertical = 4.dp) + .clickable { onClick() }, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + participants.forEach { + ParticipantTile( + participant = it, + modifier = Modifier + .weight(1f) + .fillMaxWidth() + ) + } + } + } else { + Row( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 4.dp) + .clickable { onClick() }, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + participants.forEach { + ParticipantTile( + participant = it, + modifier = Modifier + .weight(1f) + .fillMaxHeight() + ) + } + } + } + } + + else -> { + LazyVerticalGrid( + columns = GridCells.Fixed(if (isPortrait) 2 else 3), + modifier = Modifier + .fillMaxSize() + .padding(8.dp) + .clickable { onClick() }, + verticalArrangement = Arrangement.spacedBy(8.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + items(participants) { participant -> + ParticipantTile( + participant = participant, + modifier = Modifier + .fillMaxWidth() + .aspectRatio(1.5f) + ) + } + } } } } + +const val numberOfParticipants = 4 + +@Preview( + showBackground = false +) +@Composable +fun ParticipantGridPreview() { + ParticipantGrid( + participants = getTestParticipants(numberOfParticipants) + ) {} +} + +@Preview( + showBackground = false, + heightDp = 902, + widthDp = 434 +) +@Composable +fun ParticipantGridPreviewPortrait2() { + ParticipantGrid( + participants = getTestParticipants(numberOfParticipants) + ) {} +} + +@Preview( + showBackground = false, + heightDp = 360, + widthDp = 800 +) +@Composable +fun ParticipantGridPreviewLandscape1() { + ParticipantGrid( + participants = getTestParticipants(numberOfParticipants) + ) {} +} + +fun getTestParticipants(numberOfParticipants: Int): List { + val participantList = mutableListOf() + for (i: Int in 1..numberOfParticipants) { + val participant = ParticipantUiState( + sessionKey = i.toString(), + nick = "testuser$i", + isConnected = true, + isAudioEnabled = if (i == 3) true else false, + isStreamEnabled = true, + raisedHand = true, + avatarUrl = "", + surfaceViewRenderer = null + ) + participantList.add(participant) + } + return participantList +} diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt index 754d689e3..448303b01 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt @@ -9,7 +9,7 @@ package com.nextcloud.talk.call.components import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.shape.RoundedCornerShape @@ -29,10 +29,9 @@ import com.nextcloud.talk.R import com.nextcloud.talk.call.ParticipantUiState @Composable -fun ParticipantTile(participant: ParticipantUiState) { +fun ParticipantTile(participant: ParticipantUiState, modifier: Modifier = Modifier) { Box( - modifier = Modifier - .aspectRatio(3f / 4f) + modifier = modifier .clip(RoundedCornerShape(12.dp)) .background(Color.DarkGray) ) { @@ -97,5 +96,9 @@ fun ParticipantTilePreview() { avatarUrl = "", surfaceViewRenderer = null ) - ParticipantTile(participant) + ParticipantTile( + participant = participant, + modifier = Modifier + .fillMaxWidth() + ) } From 35c777e70da3a2d7591092caad2b57cc942330ec Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Tue, 6 May 2025 15:03:28 +0200 Subject: [PATCH 07/28] move SurfaceViewRenderer into WebRTCVideoView Signed-off-by: Marcel Hibbe --- .../nextcloud/talk/activities/CallActivity.kt | 3 +- .../talk/adapters/ParticipantDisplayItem.kt | 2 +- .../nextcloud/talk/call/ParticipantUiState.kt | 4 +-- .../talk/call/components/ParticipantGrid.kt | 31 +++++++++++++------ .../talk/call/components/ParticipantTile.kt | 19 +++++++++--- .../talk/call/components/WebRTCVideoView.kt | 23 +++++++++++--- 6 files changed, 60 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt index 82a83dcb2..2eaeb68cc 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt @@ -959,7 +959,8 @@ class CallActivity : CallBaseActivity() { binding!!.composeParticipantGrid.setContent { MaterialTheme { ParticipantGrid( - participants = participantUiStates.toList() + participants = participantUiStates.toList(), + eglBase = rootEglBase!! ) { animateCallControls(true, 0) } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt index 64c1a0839..71c0fd428 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt @@ -148,7 +148,7 @@ class ParticipantDisplayItem( isStreamEnabled = isStreamEnabled, raisedHand = raisedHand?.state == true, avatarUrl = urlForAvatar, - surfaceViewRenderer = surfaceViewRenderer + mediaStream = mediaStream, ) } diff --git a/app/src/main/java/com/nextcloud/talk/call/ParticipantUiState.kt b/app/src/main/java/com/nextcloud/talk/call/ParticipantUiState.kt index 1804650d6..71b93e85f 100644 --- a/app/src/main/java/com/nextcloud/talk/call/ParticipantUiState.kt +++ b/app/src/main/java/com/nextcloud/talk/call/ParticipantUiState.kt @@ -7,7 +7,7 @@ package com.nextcloud.talk.call -import org.webrtc.SurfaceViewRenderer +import org.webrtc.MediaStream data class ParticipantUiState( val sessionKey: String, @@ -17,5 +17,5 @@ data class ParticipantUiState( val isStreamEnabled: Boolean, val raisedHand: Boolean, val avatarUrl: String?, - val surfaceViewRenderer: SurfaceViewRenderer? = null + val mediaStream: MediaStream? ) diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt index 59d153c26..f3d750b50 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt @@ -27,9 +27,15 @@ import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.nextcloud.talk.call.ParticipantUiState +import org.webrtc.EglBase @Composable -fun ParticipantGrid(modifier: Modifier = Modifier, participants: List, onClick: () -> Unit) { +fun ParticipantGrid( + modifier: Modifier = Modifier, + eglBase: EglBase?, + participants: List, + onClick: () -> Unit +) { val configuration = LocalConfiguration.current val isPortrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT @@ -44,7 +50,8 @@ fun ParticipantGrid(modifier: Modifier = Modifier, participants: List { isStreamEnabled = true, raisedHand = true, avatarUrl = "", - surfaceViewRenderer = null + mediaStream = null ) participantList.add(participant) } diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt index 448303b01..480ce530b 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt @@ -9,6 +9,7 @@ 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.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -25,18 +26,25 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import androidx.compose.ui.viewinterop.AndroidView import com.nextcloud.talk.R import com.nextcloud.talk.call.ParticipantUiState +import org.webrtc.EglBase +import org.webrtc.SurfaceViewRenderer @Composable -fun ParticipantTile(participant: ParticipantUiState, modifier: Modifier = Modifier) { +fun ParticipantTile( + participant: ParticipantUiState, + eglBase: EglBase?, + modifier: Modifier = Modifier +) { Box( modifier = modifier .clip(RoundedCornerShape(12.dp)) .background(Color.DarkGray) ) { - if (participant.isStreamEnabled && participant.surfaceViewRenderer != null) { - WebRTCVideoView(participant.surfaceViewRenderer) + if (participant.isStreamEnabled && participant.mediaStream != null) { + WebRTCVideoView(participant, eglBase) } else { AvatarWithFallback(participant) } @@ -94,11 +102,12 @@ fun ParticipantTilePreview() { isStreamEnabled = true, raisedHand = true, avatarUrl = "", - surfaceViewRenderer = null + mediaStream = null, ) ParticipantTile( participant = participant, modifier = Modifier - .fillMaxWidth() + .fillMaxWidth(), + eglBase = null ) } diff --git a/app/src/main/java/com/nextcloud/talk/call/components/WebRTCVideoView.kt b/app/src/main/java/com/nextcloud/talk/call/components/WebRTCVideoView.kt index d7bb69984..c3947a139 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/WebRTCVideoView.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/WebRTCVideoView.kt @@ -11,13 +11,28 @@ 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.call.ParticipantUiState +import org.webrtc.EglBase import org.webrtc.SurfaceViewRenderer @Composable -fun WebRTCVideoView(surfaceViewRenderer: SurfaceViewRenderer) { +fun WebRTCVideoView( + participant: ParticipantUiState, + eglBase: EglBase?, +) { AndroidView( - factory = { surfaceViewRenderer }, - update = { /* No-op, renderer is already initialized and reused */ }, - modifier = Modifier.fillMaxSize() + 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() + } ) } From 2741f5962a2fe978f5eac664f75d7b37da27383a Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Tue, 6 May 2025 18:55:50 +0200 Subject: [PATCH 08/28] sort participants by audio/video (experimental, commented out) Signed-off-by: Marcel Hibbe --- .../talk/call/components/ParticipantGrid.kt | 46 +++++++++++++------ 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt index f3d750b50..b2a465ed4 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt @@ -11,16 +11,17 @@ import android.content.res.Configuration import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items +import androidx.compose.foundation.lazy.items import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalConfiguration @@ -39,6 +40,16 @@ fun ParticipantGrid( val configuration = LocalConfiguration.current val isPortrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT + // Experimental: sort participants by audio/video enabled. Maybe only do this for many participants?? + // + // val sortedParticipants = remember(participants) { + // participants.sortedWith( + // compareByDescending { it.isAudioEnabled && it.isStreamEnabled } + // .thenByDescending { it.isAudioEnabled } + // .thenByDescending { it.isStreamEnabled } + // ) + // } + when (participants.size) { 0 -> {} 1 -> { @@ -58,37 +69,43 @@ fun ParticipantGrid( 2, 3 -> { if (isPortrait) { - Column( + LazyColumn( modifier = Modifier .fillMaxSize() .padding(vertical = 4.dp) .clickable { onClick() }, verticalArrangement = Arrangement.spacedBy(8.dp) ) { - participants.forEach { + items( + items = participants, + key = { it.sessionKey } + ) { participant -> ParticipantTile( - participant = it, + participant = participant, modifier = Modifier - .weight(1f) - .fillMaxWidth(), + .fillMaxWidth() + .aspectRatio(1.5f), eglBase = eglBase ) } } } else { - Row( + LazyRow( modifier = Modifier .fillMaxSize() .padding(horizontal = 4.dp) .clickable { onClick() }, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - participants.forEach { + items( + items = participants, + key = { it.sessionKey } + ) { participant -> ParticipantTile( - participant = it, + participant = participant, modifier = Modifier - .weight(1f) - .fillMaxHeight(), + .fillMaxHeight() + .aspectRatio(1.5f), eglBase = eglBase ) } @@ -106,7 +123,10 @@ fun ParticipantGrid( verticalArrangement = Arrangement.spacedBy(8.dp), horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - items(participants) { participant -> + items( + participants.sortedBy { it.isAudioEnabled }.asReversed(), + key = { it.sessionKey } + ) { participant -> ParticipantTile( participant = participant, modifier = Modifier From ecf6d362bfc9e70ea77f2dd772e5615435a3a2c0 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Tue, 6 May 2025 20:10:59 +0200 Subject: [PATCH 09/28] calculate item height Signed-off-by: Marcel Hibbe --- .../talk/call/components/ParticipantGrid.kt | 186 ++++++++++++++---- 1 file changed, 146 insertions(+), 40 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt index b2a465ed4..b83a37427 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt @@ -11,24 +11,26 @@ import android.content.res.Configuration import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxHeight 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.LazyColumn -import androidx.compose.foundation.lazy.LazyRow import androidx.compose.foundation.lazy.grid.GridCells import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.lazy.grid.items -import androidx.compose.foundation.lazy.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 androidx.compose.ui.unit.max import com.nextcloud.talk.call.ParticipantUiState import org.webrtc.EglBase +import kotlin.math.ceil @Composable fun ParticipantGrid( @@ -69,43 +71,37 @@ fun ParticipantGrid( 2, 3 -> { if (isPortrait) { - LazyColumn( + Column( modifier = Modifier .fillMaxSize() .padding(vertical = 4.dp) .clickable { onClick() }, verticalArrangement = Arrangement.spacedBy(8.dp) ) { - items( - items = participants, - key = { it.sessionKey } - ) { participant -> + participants.forEach { ParticipantTile( - participant = participant, + participant = it, modifier = Modifier - .fillMaxWidth() - .aspectRatio(1.5f), + .weight(1f) + .fillMaxWidth(), eglBase = eglBase ) } } } else { - LazyRow( + Row( modifier = Modifier .fillMaxSize() .padding(horizontal = 4.dp) .clickable { onClick() }, horizontalArrangement = Arrangement.spacedBy(8.dp) ) { - items( - items = participants, - key = { it.sessionKey } - ) { participant -> + participants.forEach { ParticipantTile( - participant = participant, + participant = it, modifier = Modifier - .fillMaxHeight() - .aspectRatio(1.5f), + .weight(1f) + .fillMaxHeight(), eglBase = eglBase ) } @@ -114,24 +110,30 @@ fun ParticipantGrid( } else -> { + val columns = if (isPortrait) 2 else 3 + val rows = ceil(participants.size / columns.toFloat()).toInt() + val screenHeight = LocalConfiguration.current.screenHeightDp.dp + val rawItemHeight = screenHeight / rows + val itemHeight = max(rawItemHeight, 120.dp) + LazyVerticalGrid( - columns = GridCells.Fixed(if (isPortrait) 2 else 3), + columns = GridCells.Fixed(columns), modifier = Modifier .fillMaxSize() - .padding(8.dp) + .padding(0.dp) .clickable { onClick() }, - verticalArrangement = Arrangement.spacedBy(8.dp), - horizontalArrangement = Arrangement.spacedBy(8.dp) + verticalArrangement = Arrangement.spacedBy(0.dp), + horizontalArrangement = Arrangement.spacedBy(0.dp) ) { items( - participants.sortedBy { it.isAudioEnabled }.asReversed(), + participants, key = { it.sessionKey } ) { participant -> ParticipantTile( participant = participant, modifier = Modifier - .fillMaxWidth() - .aspectRatio(1.5f), + .height(itemHeight) + .fillMaxWidth(), eglBase = eglBase ) } @@ -140,28 +142,67 @@ fun ParticipantGrid( } } -const val numberOfParticipants = 4 -@Preview( - showBackground = false -) + +@Preview @Composable fun ParticipantGridPreview() { ParticipantGrid( - participants = getTestParticipants(numberOfParticipants), + participants = getTestParticipants(1), eglBase = null ) {} } -@Preview( - showBackground = false, - heightDp = 902, - widthDp = 434 -) +@Preview @Composable -fun ParticipantGridPreviewPortrait2() { +fun TwoParticipants() { ParticipantGrid( - participants = getTestParticipants(numberOfParticipants), + participants = getTestParticipants(2), + eglBase = null + ) {} +} + +@Preview +@Composable +fun ThreeParticipants() { + ParticipantGrid( + participants = getTestParticipants(3), + eglBase = null + ) {} +} + +@Preview +@Composable +fun FourParticipants() { + ParticipantGrid( + participants = getTestParticipants(4), + eglBase = null + ) {} +} + +@Preview +@Composable +fun FiveParticipants() { + ParticipantGrid( + participants = getTestParticipants(5), + eglBase = null + ) {} +} + +@Preview +@Composable +fun SevenParticipants() { + ParticipantGrid( + participants = getTestParticipants(7), + eglBase = null + ) {} +} + +@Preview +@Composable +fun FiftyParticipants() { + ParticipantGrid( + participants = getTestParticipants(50), eglBase = null ) {} } @@ -172,9 +213,74 @@ fun ParticipantGridPreviewPortrait2() { widthDp = 800 ) @Composable -fun ParticipantGridPreviewLandscape1() { +fun OneParticipantLandscape() { ParticipantGrid( - participants = getTestParticipants(numberOfParticipants), + participants = getTestParticipants(1), + eglBase = null + ) {} +} + +@Preview( + showBackground = false, + heightDp = 360, + widthDp = 800 +) +@Composable +fun TwoParticipantsLandscape() { + ParticipantGrid( + participants = getTestParticipants(2), + eglBase = null + ) {} +} + +@Preview( + showBackground = false, + heightDp = 360, + widthDp = 800 +) +@Composable +fun ThreeParticipantsLandscape() { + ParticipantGrid( + participants = getTestParticipants(3), + eglBase = null + ) {} +} + +@Preview( + showBackground = false, + heightDp = 360, + widthDp = 800 +) +@Composable +fun FourParticipantsLandscape() { + ParticipantGrid( + participants = getTestParticipants(4), + eglBase = null + ) {} +} + +@Preview( + showBackground = false, + heightDp = 360, + widthDp = 800 +) +@Composable +fun SevenParticipantsLandscape() { + ParticipantGrid( + participants = getTestParticipants(7), + eglBase = null + ) {} +} + +@Preview( + showBackground = false, + heightDp = 360, + widthDp = 800 +) +@Composable +fun FiftyParticipantsLandscape() { + ParticipantGrid( + participants = getTestParticipants(50), eglBase = null ) {} } From bcb276d533980dbd0f3ad44980d64f07b674b97f Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Tue, 6 May 2025 20:18:54 +0200 Subject: [PATCH 10/28] add padding to cells Signed-off-by: Marcel Hibbe --- .../talk/call/components/ParticipantGrid.kt | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt index b83a37427..e13100a0e 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxHeight @@ -112,21 +113,30 @@ fun ParticipantGrid( else -> { val columns = if (isPortrait) 2 else 3 val rows = ceil(participants.size / columns.toFloat()).toInt() + val screenHeight = LocalConfiguration.current.screenHeightDp.dp - val rawItemHeight = screenHeight / rows - val itemHeight = max(rawItemHeight, 120.dp) + val itemSpacing = 8.dp + val edgePadding = 8.dp + + val totalVerticalSpacing = itemSpacing * (rows - 1) + val totalVerticalPadding = edgePadding * 2 + val availableHeight = screenHeight - totalVerticalSpacing - totalVerticalPadding + + val rawItemHeight = availableHeight / rows + val itemHeight = maxOf(rawItemHeight, 100.dp) LazyVerticalGrid( columns = GridCells.Fixed(columns), modifier = Modifier .fillMaxSize() - .padding(0.dp) + .padding(horizontal = edgePadding) // Only horizontal outer padding here .clickable { onClick() }, - verticalArrangement = Arrangement.spacedBy(0.dp), - horizontalArrangement = Arrangement.spacedBy(0.dp) + verticalArrangement = Arrangement.spacedBy(itemSpacing), + horizontalArrangement = Arrangement.spacedBy(itemSpacing), + contentPadding = PaddingValues(vertical = edgePadding) // vertical padding handled here ) { items( - participants, + participants.sortedBy { it.isAudioEnabled }.asReversed(), key = { it.sessionKey } ) { participant -> ParticipantTile( From 15d7c8371c0f973eb4883436b794e2a420aebee9 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Tue, 6 May 2025 23:06:27 +0200 Subject: [PATCH 11/28] simplify grid cell calculation Signed-off-by: Marcel Hibbe --- .../talk/call/components/ParticipantGrid.kt | 142 +++++------------- 1 file changed, 40 insertions(+), 102 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt index e13100a0e..421bbe6c1 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -28,7 +27,6 @@ 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 androidx.compose.ui.unit.max import com.nextcloud.talk.call.ParticipantUiState import org.webrtc.EglBase import kotlin.math.ceil @@ -42,118 +40,58 @@ fun ParticipantGrid( ) { val configuration = LocalConfiguration.current val isPortrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT - - // Experimental: sort participants by audio/video enabled. Maybe only do this for many participants?? - // - // val sortedParticipants = remember(participants) { - // participants.sortedWith( - // compareByDescending { it.isAudioEnabled && it.isStreamEnabled } - // .thenByDescending { it.isAudioEnabled } - // .thenByDescending { it.isStreamEnabled } - // ) - // } - - when (participants.size) { - 0 -> {} - 1 -> { - Box( - modifier = Modifier - .fillMaxSize() - ) { - ParticipantTile( - participant = participants[0], - modifier = Modifier - .fillMaxSize() - .clickable { onClick() }, - eglBase = eglBase - ) + val columns = + if (isPortrait) { + when (participants.size) { + 1, 2, 3 -> 1 + else -> 2 + } + } else { + when (participants.size) { + 1 -> 1 + 2 -> 2 + else -> 3 } } - 2, 3 -> { - if (isPortrait) { - Column( - modifier = Modifier - .fillMaxSize() - .padding(vertical = 4.dp) - .clickable { onClick() }, - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - participants.forEach { - ParticipantTile( - participant = it, - modifier = Modifier - .weight(1f) - .fillMaxWidth(), - eglBase = eglBase - ) - } - } - } else { - Row( - modifier = Modifier - .fillMaxSize() - .padding(horizontal = 4.dp) - .clickable { onClick() }, - horizontalArrangement = Arrangement.spacedBy(8.dp) - ) { - participants.forEach { - ParticipantTile( - participant = it, - modifier = Modifier - .weight(1f) - .fillMaxHeight(), - eglBase = eglBase - ) - } - } - } - } + val rows = ceil(participants.size / columns.toFloat()).toInt() - else -> { - val columns = if (isPortrait) 2 else 3 - val rows = ceil(participants.size / columns.toFloat()).toInt() + val screenHeight = LocalConfiguration.current.screenHeightDp.dp + val itemSpacing = 8.dp + val edgePadding = 8.dp - val screenHeight = LocalConfiguration.current.screenHeightDp.dp - val itemSpacing = 8.dp - val edgePadding = 8.dp + val totalVerticalSpacing = itemSpacing * (rows - 1) + val totalVerticalPadding = edgePadding * 2 + val availableHeight = screenHeight - totalVerticalSpacing - totalVerticalPadding - val totalVerticalSpacing = itemSpacing * (rows - 1) - val totalVerticalPadding = edgePadding * 2 - val availableHeight = screenHeight - totalVerticalSpacing - totalVerticalPadding + val rawItemHeight = availableHeight / rows + val itemHeight = maxOf(rawItemHeight, 100.dp) - val rawItemHeight = availableHeight / rows - val itemHeight = maxOf(rawItemHeight, 100.dp) - - LazyVerticalGrid( - columns = GridCells.Fixed(columns), + LazyVerticalGrid( + columns = GridCells.Fixed(columns), + modifier = Modifier + .fillMaxSize() + .padding(horizontal = edgePadding) // Only horizontal outer padding here + .clickable { onClick() }, + verticalArrangement = Arrangement.spacedBy(itemSpacing), + horizontalArrangement = Arrangement.spacedBy(itemSpacing), + contentPadding = PaddingValues(vertical = edgePadding) // vertical padding handled here + ) { + items( + participants.sortedBy { it.isAudioEnabled }.asReversed(), + key = { it.sessionKey } + ) { participant -> + ParticipantTile( + participant = participant, modifier = Modifier - .fillMaxSize() - .padding(horizontal = edgePadding) // Only horizontal outer padding here - .clickable { onClick() }, - verticalArrangement = Arrangement.spacedBy(itemSpacing), - horizontalArrangement = Arrangement.spacedBy(itemSpacing), - contentPadding = PaddingValues(vertical = edgePadding) // vertical padding handled here - ) { - items( - participants.sortedBy { it.isAudioEnabled }.asReversed(), - key = { it.sessionKey } - ) { participant -> - ParticipantTile( - participant = participant, - modifier = Modifier - .height(itemHeight) - .fillMaxWidth(), - eglBase = eglBase - ) - } - } + .height(itemHeight) + .fillMaxWidth(), + eglBase = eglBase + ) } } } - - @Preview @Composable fun ParticipantGridPreview() { From 91b0e97589b79fe9a00b838139fba1f755f029a3 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Tue, 6 May 2025 23:50:06 +0200 Subject: [PATCH 12/28] show rounded circle for avatar Signed-off-by: Marcel Hibbe --- .../call/components/AvatarWithFallback.kt | 57 ++++++++++++------- .../talk/call/components/ParticipantGrid.kt | 4 +- .../talk/call/components/ParticipantTile.kt | 11 +++- 3 files changed, 48 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/call/components/AvatarWithFallback.kt b/app/src/main/java/com/nextcloud/talk/call/components/AvatarWithFallback.kt index b39c279cf..0763df116 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/AvatarWithFallback.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/AvatarWithFallback.kt @@ -10,37 +10,56 @@ 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.layout.size +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.dp import androidx.compose.ui.unit.sp import coil.compose.AsyncImage import com.nextcloud.talk.call.ParticipantUiState @Composable -fun AvatarWithFallback(participant: ParticipantUiState) { - if (!participant.avatarUrl.isNullOrEmpty()) { - AsyncImage( - model = participant.avatarUrl, - contentDescription = "Avatar", - contentScale = ContentScale.Crop, - modifier = Modifier.fillMaxSize() - ) - } else { - Box( - modifier = Modifier - .fillMaxSize() - .background(Color.Gray), - contentAlignment = Alignment.Center - ) { - Text( - text = participant.nick.firstOrNull()?.uppercase() ?: "?", - color = Color.White, - fontSize = 40.sp +fun AvatarWithFallback(participant: ParticipantUiState, modifier: Modifier = Modifier) { + val initials = participant.nick + .split(" ") + .mapNotNull { it.firstOrNull()?.uppercase() } + .take(2) + .joinToString("") + + Box( + modifier = modifier + .size(150.dp) + .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 = if (initials.isNotEmpty()) initials else "?", + color = Color.Black, + fontSize = 24.sp + ) + } } } } diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt index 421bbe6c1..13ea916bb 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt @@ -49,7 +49,7 @@ fun ParticipantGrid( } else { when (participants.size) { 1 -> 1 - 2 -> 2 + 2, 4 -> 2 else -> 3 } } @@ -238,7 +238,7 @@ fun getTestParticipants(numberOfParticipants: Int): List { for (i: Int in 1..numberOfParticipants) { val participant = ParticipantUiState( sessionKey = i.toString(), - nick = "testuser$i", + nick = "testuser$i Test", isConnected = true, isAudioEnabled = if (i == 3) true else false, isStreamEnabled = true, diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt index 480ce530b..a22e24502 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt @@ -11,6 +11,7 @@ import androidx.compose.foundation.background import androidx.compose.foundation.layout.Box 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 @@ -46,7 +47,10 @@ fun ParticipantTile( if (participant.isStreamEnabled && participant.mediaStream != null) { WebRTCVideoView(participant, eglBase) } else { - AvatarWithFallback(participant) + AvatarWithFallback( + participant = participant, + modifier = Modifier.align(Alignment.Center) + ) } if (participant.raisedHand) { @@ -96,7 +100,7 @@ fun ParticipantTile( fun ParticipantTilePreview() { val participant = ParticipantUiState( sessionKey = "", - nick = "testuser", + nick = "testuser one", isConnected = true, isAudioEnabled = false, isStreamEnabled = true, @@ -107,7 +111,8 @@ fun ParticipantTilePreview() { ParticipantTile( participant = participant, modifier = Modifier - .fillMaxWidth(), + .fillMaxWidth() + .height(300.dp), eglBase = null ) } From d546046d5b4d4c1265ed4194c37fb390f4af9f50 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Tue, 6 May 2025 23:57:02 +0200 Subject: [PATCH 13/28] add shadow to nick name Signed-off-by: Marcel Hibbe --- .../talk/call/components/ParticipantGrid.kt | 5 ++++- .../talk/call/components/ParticipantTile.kt | 16 ++++++++++------ 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt index 13ea916bb..664e86c3f 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt @@ -40,6 +40,9 @@ fun ParticipantGrid( ) { val configuration = LocalConfiguration.current val isPortrait = configuration.orientation == Configuration.ORIENTATION_PORTRAIT + + val minItemHeight = 100.dp + val columns = if (isPortrait) { when (participants.size) { @@ -65,7 +68,7 @@ fun ParticipantGrid( val availableHeight = screenHeight - totalVerticalSpacing - totalVerticalPadding val rawItemHeight = availableHeight / rows - val itemHeight = maxOf(rawItemHeight, 100.dp) + val itemHeight = maxOf(rawItemHeight, minItemHeight) LazyVerticalGrid( columns = GridCells.Fixed(columns), diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt index a22e24502..0d28af8ba 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt @@ -9,7 +9,6 @@ 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.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -23,15 +22,15 @@ 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.viewinterop.AndroidView import com.nextcloud.talk.R import com.nextcloud.talk.call.ParticipantUiState import org.webrtc.EglBase -import org.webrtc.SurfaceViewRenderer @Composable fun ParticipantTile( @@ -82,9 +81,14 @@ fun ParticipantTile( color = Color.White, modifier = Modifier .align(Alignment.BottomStart) - .background(Color.Black.copy(alpha = 0.6f)) - .padding(6.dp), - style = MaterialTheme.typography.bodyMedium + .padding(10.dp), + style = MaterialTheme.typography.bodyMedium.copy( + shadow = Shadow( + color = Color.Black, + offset = Offset(6f, 6f), + blurRadius = 4f + ) + ) ) if (!participant.isConnected) { From 1136508ba24936f1a3cd6c5ab80468ad4589ced6 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Wed, 7 May 2025 10:33:58 +0200 Subject: [PATCH 14/28] fix voice only call design by workaround. For now, instead to measure height mixed from xml and compose, assume a fixed height for the xml views (callInfosLinearLayout and callControls) to limit the grid height. They is not a nice solution and should be replaced once everything is migrated to compose. Signed-off-by: Marcel Hibbe --- .../nextcloud/talk/activities/CallActivity.kt | 3 +- .../talk/adapters/ParticipantDisplayItem.kt | 2 +- .../talk/call/components/ParticipantGrid.kt | 62 ++++++++++++------- .../talk/call/components/ParticipantTile.kt | 8 +-- .../talk/call/components/WebRTCVideoView.kt | 2 +- 5 files changed, 46 insertions(+), 31 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt index 2eaeb68cc..7d8088e7c 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt @@ -960,7 +960,8 @@ class CallActivity : CallBaseActivity() { MaterialTheme { ParticipantGrid( participants = participantUiStates.toList(), - eglBase = rootEglBase!! + eglBase = rootEglBase!!, + isVoiceOnlyCall = isVoiceOnlyCall ) { animateCallControls(true, 0) } diff --git a/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt index 71c0fd428..4298f7aa8 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt @@ -148,7 +148,7 @@ class ParticipantDisplayItem( isStreamEnabled = isStreamEnabled, raisedHand = raisedHand?.state == true, avatarUrl = urlForAvatar, - mediaStream = mediaStream, + mediaStream = mediaStream ) } diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt index 664e86c3f..135f3e523 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt @@ -10,11 +10,7 @@ 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.Box -import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -36,6 +32,7 @@ fun ParticipantGrid( modifier: Modifier = Modifier, eglBase: EglBase?, participants: List, + isVoiceOnlyCall: Boolean, onClick: () -> Unit ) { val configuration = LocalConfiguration.current @@ -59,13 +56,21 @@ fun ParticipantGrid( val rows = ceil(participants.size / columns.toFloat()).toInt() - val screenHeight = LocalConfiguration.current.screenHeightDp.dp + val heightForNonGridComponents = if (isVoiceOnlyCall) { + // 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 = screenHeight - totalVerticalSpacing - totalVerticalPadding + val availableHeight = gridHeight - totalVerticalSpacing - totalVerticalPadding val rawItemHeight = availableHeight / rows val itemHeight = maxOf(rawItemHeight, minItemHeight) @@ -74,14 +79,14 @@ fun ParticipantGrid( columns = GridCells.Fixed(columns), modifier = Modifier .fillMaxSize() - .padding(horizontal = edgePadding) // Only horizontal outer padding here + .padding(horizontal = edgePadding) .clickable { onClick() }, verticalArrangement = Arrangement.spacedBy(itemSpacing), horizontalArrangement = Arrangement.spacedBy(itemSpacing), - contentPadding = PaddingValues(vertical = edgePadding) // vertical padding handled here + contentPadding = PaddingValues(vertical = edgePadding) ) { items( - participants.sortedBy { it.isAudioEnabled }.asReversed(), + participants, key = { it.sessionKey } ) { participant -> ParticipantTile( @@ -100,7 +105,8 @@ fun ParticipantGrid( fun ParticipantGridPreview() { ParticipantGrid( participants = getTestParticipants(1), - eglBase = null + eglBase = null, + isVoiceOnlyCall = false ) {} } @@ -109,7 +115,8 @@ fun ParticipantGridPreview() { fun TwoParticipants() { ParticipantGrid( participants = getTestParticipants(2), - eglBase = null + eglBase = null, + isVoiceOnlyCall = false ) {} } @@ -118,7 +125,8 @@ fun TwoParticipants() { fun ThreeParticipants() { ParticipantGrid( participants = getTestParticipants(3), - eglBase = null + eglBase = null, + isVoiceOnlyCall = false ) {} } @@ -127,7 +135,8 @@ fun ThreeParticipants() { fun FourParticipants() { ParticipantGrid( participants = getTestParticipants(4), - eglBase = null + eglBase = null, + isVoiceOnlyCall = false ) {} } @@ -136,7 +145,8 @@ fun FourParticipants() { fun FiveParticipants() { ParticipantGrid( participants = getTestParticipants(5), - eglBase = null + eglBase = null, + isVoiceOnlyCall = false ) {} } @@ -145,7 +155,8 @@ fun FiveParticipants() { fun SevenParticipants() { ParticipantGrid( participants = getTestParticipants(7), - eglBase = null + eglBase = null, + isVoiceOnlyCall = false ) {} } @@ -154,7 +165,8 @@ fun SevenParticipants() { fun FiftyParticipants() { ParticipantGrid( participants = getTestParticipants(50), - eglBase = null + eglBase = null, + isVoiceOnlyCall = false ) {} } @@ -167,7 +179,8 @@ fun FiftyParticipants() { fun OneParticipantLandscape() { ParticipantGrid( participants = getTestParticipants(1), - eglBase = null + eglBase = null, + isVoiceOnlyCall = false ) {} } @@ -180,7 +193,8 @@ fun OneParticipantLandscape() { fun TwoParticipantsLandscape() { ParticipantGrid( participants = getTestParticipants(2), - eglBase = null + eglBase = null, + isVoiceOnlyCall = false ) {} } @@ -193,7 +207,8 @@ fun TwoParticipantsLandscape() { fun ThreeParticipantsLandscape() { ParticipantGrid( participants = getTestParticipants(3), - eglBase = null + eglBase = null, + isVoiceOnlyCall = false ) {} } @@ -206,7 +221,8 @@ fun ThreeParticipantsLandscape() { fun FourParticipantsLandscape() { ParticipantGrid( participants = getTestParticipants(4), - eglBase = null + eglBase = null, + isVoiceOnlyCall = false ) {} } @@ -219,7 +235,8 @@ fun FourParticipantsLandscape() { fun SevenParticipantsLandscape() { ParticipantGrid( participants = getTestParticipants(7), - eglBase = null + eglBase = null, + isVoiceOnlyCall = false ) {} } @@ -232,7 +249,8 @@ fun SevenParticipantsLandscape() { fun FiftyParticipantsLandscape() { ParticipantGrid( participants = getTestParticipants(50), - eglBase = null + eglBase = null, + isVoiceOnlyCall = false ) {} } diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt index 0d28af8ba..35b851ea0 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt @@ -33,11 +33,7 @@ import com.nextcloud.talk.call.ParticipantUiState import org.webrtc.EglBase @Composable -fun ParticipantTile( - participant: ParticipantUiState, - eglBase: EglBase?, - modifier: Modifier = Modifier -) { +fun ParticipantTile(participant: ParticipantUiState, eglBase: EglBase?, modifier: Modifier = Modifier) { Box( modifier = modifier .clip(RoundedCornerShape(12.dp)) @@ -110,7 +106,7 @@ fun ParticipantTilePreview() { isStreamEnabled = true, raisedHand = true, avatarUrl = "", - mediaStream = null, + mediaStream = null ) ParticipantTile( participant = participant, diff --git a/app/src/main/java/com/nextcloud/talk/call/components/WebRTCVideoView.kt b/app/src/main/java/com/nextcloud/talk/call/components/WebRTCVideoView.kt index c3947a139..814013ee1 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/WebRTCVideoView.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/WebRTCVideoView.kt @@ -18,7 +18,7 @@ import org.webrtc.SurfaceViewRenderer @Composable fun WebRTCVideoView( participant: ParticipantUiState, - eglBase: EglBase?, + eglBase: EglBase? ) { AndroidView( factory = { context -> From 257dc2af543a6c4e317162e93dd2eb01db0c1356 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Wed, 7 May 2025 13:10:50 +0200 Subject: [PATCH 15/28] only show avatars for voiceonly calls improve pip handling a bit minor changes Signed-off-by: Marcel Hibbe --- .../nextcloud/talk/activities/CallActivity.kt | 52 ++++++++++++------- .../call/components/AvatarWithFallback.kt | 2 +- .../talk/call/components/ParticipantGrid.kt | 5 +- .../talk/call/components/ParticipantTile.kt | 12 +++-- app/src/main/res/layout/call_activity.xml | 5 +- 5 files changed, 49 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt index 7d8088e7c..003dc869a 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt @@ -940,17 +940,22 @@ class CallActivity : CallBaseActivity() { } private fun initSelfVideoViewForPipMode() { - 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) + if (!isVoiceOnlyCall) { + 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) + localVideoTrack?.addSink(binding?.pipSelfVideoRenderer) + } else { + binding!!.pipSelfVideoRenderer.visibility = View.GONE + } } private fun initGrid() { @@ -2187,6 +2192,7 @@ class CallActivity : CallBaseActivity() { startVideoCapture(true) } } + in ANGLE_LANDSCAPE_RIGHT_THRESHOLD_MIN..ANGLE_LANDSCAPE_RIGHT_THRESHOLD_MAX, in ANGLE_LANDSCAPE_LEFT_THRESHOLD_MIN..ANGLE_LANDSCAPE_LEFT_THRESHOLD_MAX -> { if (lastAspectRatio != RATIO_16_TO_9) { @@ -2718,6 +2724,7 @@ class CallActivity : CallBaseActivity() { handler!!.postDelayed({ setCallState(CallStatus.CALLING_TIMEOUT) }, CALLING_TIMEOUT) handler!!.post { handleCallStateJoined() } } + CallStatus.IN_CONVERSATION -> handler!!.post { handleCallStateInConversation() } CallStatus.OFFLINE -> handler!!.post { handleCallStateOffline() } CallStatus.LEAVING -> handler!!.post { handleCallStateLeaving() } @@ -3241,21 +3248,30 @@ class CallActivity : CallBaseActivity() { binding!!.callInfosLinearLayout.visibility = View.GONE binding!!.selfVideoViewWrapper.visibility = View.GONE binding!!.callStates.callStateRelativeLayout.visibility = View.GONE + binding!!.pipCallConversationNameTextView.text = conversationName - binding!!.selfVideoRenderer.clearImage() - binding!!.selfVideoRenderer.release() - if (participantItems.size > 1) { - binding!!.pipCallConversationNameTextView.text = conversationName - binding!!.pipSelfVideoOverlay.visibility = View.VISIBLE - initSelfVideoViewForPipMode() + if (isVoiceOnlyCall) { + if (participantItems.size > 1) { + binding!!.pipOverlay.visibility = View.VISIBLE + binding!!.pipSelfVideoRenderer.visibility = View.GONE + } else { + binding!!.pipOverlay.visibility = View.GONE + } } else { - binding!!.pipSelfVideoOverlay.visibility = View.GONE + binding!!.selfVideoRenderer.clearImage() + binding!!.selfVideoRenderer.release() + if (participantItems.size > 1) { + binding!!.pipOverlay.visibility = View.VISIBLE + initSelfVideoViewForPipMode() + } else { + binding!!.pipOverlay.visibility = View.GONE + } } } override fun updateUiForNormalMode() { Log.d(TAG, "updateUiForNormalMode") - binding!!.pipSelfVideoOverlay.visibility = View.GONE + binding!!.pipOverlay.visibility = View.GONE if (isVoiceOnlyCall) { binding!!.callControls.visibility = View.VISIBLE diff --git a/app/src/main/java/com/nextcloud/talk/call/components/AvatarWithFallback.kt b/app/src/main/java/com/nextcloud/talk/call/components/AvatarWithFallback.kt index 0763df116..49a09cd6c 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/AvatarWithFallback.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/AvatarWithFallback.kt @@ -55,7 +55,7 @@ fun AvatarWithFallback(participant: ParticipantUiState, modifier: Modifier = Mod contentAlignment = Alignment.Center ) { Text( - text = if (initials.isNotEmpty()) initials else "?", + text = initials.ifEmpty { "?" }, color = Color.Black, fontSize = 24.sp ) diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt index 135f3e523..1833c448a 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt @@ -94,7 +94,8 @@ fun ParticipantGrid( modifier = Modifier .height(itemHeight) .fillMaxWidth(), - eglBase = eglBase + eglBase = eglBase, + isVoiceOnlyCall = isVoiceOnlyCall ) } } @@ -261,7 +262,7 @@ fun getTestParticipants(numberOfParticipants: Int): List { sessionKey = i.toString(), nick = "testuser$i Test", isConnected = true, - isAudioEnabled = if (i == 3) true else false, + isAudioEnabled = true, isStreamEnabled = true, raisedHand = true, avatarUrl = "", diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt index 35b851ea0..385927f7e 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt @@ -33,13 +33,18 @@ import com.nextcloud.talk.call.ParticipantUiState import org.webrtc.EglBase @Composable -fun ParticipantTile(participant: ParticipantUiState, eglBase: EglBase?, modifier: Modifier = Modifier) { +fun ParticipantTile( + participant: ParticipantUiState, + eglBase: EglBase?, + modifier: Modifier = Modifier, + isVoiceOnlyCall: Boolean, +) { Box( modifier = modifier .clip(RoundedCornerShape(12.dp)) .background(Color.DarkGray) ) { - if (participant.isStreamEnabled && participant.mediaStream != null) { + if (!isVoiceOnlyCall && participant.isStreamEnabled && participant.mediaStream != null) { WebRTCVideoView(participant, eglBase) } else { AvatarWithFallback( @@ -113,6 +118,7 @@ fun ParticipantTilePreview() { modifier = Modifier .fillMaxWidth() .height(300.dp), - eglBase = null + eglBase = null, + isVoiceOnlyCall = false ) } diff --git a/app/src/main/res/layout/call_activity.xml b/app/src/main/res/layout/call_activity.xml index ea97114e9..ca67beaa2 100644 --- a/app/src/main/res/layout/call_activity.xml +++ b/app/src/main/res/layout/call_activity.xml @@ -322,7 +322,7 @@ Date: Wed, 7 May 2025 13:40:30 +0200 Subject: [PATCH 16/28] add background colors like in web to participant tile Signed-off-by: Marcel Hibbe --- .../talk/call/components/ParticipantTile.kt | 7 +- .../nextcloud/talk/utils/ColorGenerator.kt | 106 ++++++++++++++++++ 2 files changed, 111 insertions(+), 2 deletions(-) create mode 100644 app/src/main/java/com/nextcloud/talk/utils/ColorGenerator.kt diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt index 385927f7e..74f32af3f 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt @@ -30,6 +30,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.nextcloud.talk.R import com.nextcloud.talk.call.ParticipantUiState +import com.nextcloud.talk.utils.ColorGenerator import org.webrtc.EglBase @Composable @@ -39,10 +40,12 @@ fun ParticipantTile( modifier: Modifier = Modifier, isVoiceOnlyCall: Boolean, ) { + val colorInt = ColorGenerator.shared.usernameToColor(participant.nick) + Box( modifier = modifier .clip(RoundedCornerShape(12.dp)) - .background(Color.DarkGray) + .background(Color(colorInt)) ) { if (!isVoiceOnlyCall && participant.isStreamEnabled && participant.mediaStream != null) { WebRTCVideoView(participant, eglBase) @@ -86,7 +89,7 @@ fun ParticipantTile( style = MaterialTheme.typography.bodyMedium.copy( shadow = Shadow( color = Color.Black, - offset = Offset(6f, 6f), + offset = Offset(4f, 4f), blurRadius = 4f ) ) diff --git a/app/src/main/java/com/nextcloud/talk/utils/ColorGenerator.kt b/app/src/main/java/com/nextcloud/talk/utils/ColorGenerator.kt new file mode 100644 index 000000000..c8fa1e58e --- /dev/null +++ b/app/src/main/java/com/nextcloud/talk/utils/ColorGenerator.kt @@ -0,0 +1,106 @@ +/* + * Nextcloud Talk - Android Client + * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2025 Marcel Hibbe + * 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 + +class ColorGenerator private constructor() { + + private val steps = 6 + private val finalPalette: List = genColors(steps) + + companion object { + val shared = ColorGenerator() + private const val MULTIPLIER = 256.0f + + private fun stepCalc(steps: Int, colorStart: Int, colorEnd: Int): List { + 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 { + 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 { + 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] + } +} From af93877e9603836ae20493581f5c91bdf79a928d Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Wed, 7 May 2025 15:21:20 +0200 Subject: [PATCH 17/28] move self video position more to edge Signed-off-by: Marcel Hibbe --- .../main/java/com/nextcloud/talk/activities/CallActivity.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt index 003dc869a..eaed959f3 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt @@ -3376,10 +3376,10 @@ class CallActivity : CallBaseActivity() { private const val SELFVIDEO_WIDTH_16_TO_9_RATIO = 136 private const val SELFVIDEO_HEIGHT_16_TO_9_RATIO = 80 - private const val SELFVIDEO_POSITION_X_LANDSCAPE = 100F - private const val SELFVIDEO_POSITION_Y_LANDSCAPE = 100F + private const val SELFVIDEO_POSITION_X_LANDSCAPE = 50F + private const val SELFVIDEO_POSITION_Y_LANDSCAPE = 50F 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 CALLING_TIMEOUT: Long = 45000 From 51eb7edcb07f417c2c26250e14c51348a23e3fe7 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Wed, 7 May 2025 15:22:20 +0200 Subject: [PATCH 18/28] remove unused constants Signed-off-by: Marcel Hibbe --- .../com/nextcloud/talk/activities/CallActivity.kt | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt index eaed959f3..61b56fcfc 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt @@ -3391,20 +3391,8 @@ class CallActivity : CallBaseActivity() { private const val SPOTLIGHT_HEADING_SIZE: Int = 20 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 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 } } From 1703a34007aaaa109c85ce85f81c74d680891456 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Wed, 7 May 2025 15:23:39 +0200 Subject: [PATCH 19/28] remove unused code Signed-off-by: Marcel Hibbe --- .../nextcloud/talk/activities/CallActivity.kt | 23 ------------------- 1 file changed, 23 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt index 61b56fcfc..00c6c2c7b 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt @@ -2544,35 +2544,12 @@ class CallActivity : CallBaseActivity() { runOnUiThread { removeParticipantDisplayItem(sessionId, "video") } } - // private fun removeParticipantDisplayItem(sessionId: String?, videoStreamType: String) { - // Log.d(TAG, "removeParticipantDisplayItem") - // val participantDisplayItem = participantDisplayItems!!.remove("$sessionId-$videoStreamType") ?: return - // participantDisplayItem.destroy() - // if (!isDestroyed) { - // initGridAdapter() - // } - // } - - // private fun removeParticipantDisplayItem(sessionId: String?, videoStreamType: String) { - // val key = "$sessionId-$videoStreamType" - // - // val participant = participantItems.find { it.sessionKey == key } - // participant?.destroy() - // participantItems.remove(participant) - // - // initGrid() - // } - private fun removeParticipantDisplayItem(sessionId: String?, videoStreamType: String) { val key = "$sessionId-$videoStreamType" - val participant = participantItems.find { it.sessionKey == key } participant?.destroy() participantItems.removeAll { it.sessionKey == key } - - // Also remove UI state participantUiStates.removeAll { it.sessionKey == key } - initGrid() } From b6519f6ffb130f2042ccb10f94479c2c237e110b Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Wed, 7 May 2025 15:34:05 +0200 Subject: [PATCH 20/28] make sure to release pipSelfVideoRenderer after hangup Signed-off-by: Marcel Hibbe --- .../main/java/com/nextcloud/talk/activities/CallActivity.kt | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt index 00c6c2c7b..70cfcf77d 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt @@ -937,6 +937,9 @@ class CallActivity : CallBaseActivity() { binding!!.selfVideoRenderer.setEnableHardwareScaler(false) binding!!.selfVideoRenderer.setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FIT) binding!!.selfVideoRenderer.setOnTouchListener(SelfVideoTouchListener()) + + binding!!.pipSelfVideoRenderer.clearImage() + binding!!.pipSelfVideoRenderer.release() } private fun initSelfVideoViewForPipMode() { @@ -2090,6 +2093,9 @@ class CallActivity : CallBaseActivity() { } binding!!.selfVideoRenderer.clearImage() binding!!.selfVideoRenderer.release() + + binding!!.pipSelfVideoRenderer.clearImage() + binding!!.pipSelfVideoRenderer.release() if (audioSource != null) { audioSource!!.dispose() audioSource = null From 38072d40c2461f568527ab5cfb7ee737469acce2 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Wed, 7 May 2025 16:02:58 +0200 Subject: [PATCH 21/28] fix to show screenshare at least as another cell. fullscreen needs to be implemented Signed-off-by: Marcel Hibbe --- app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt index 70cfcf77d..4a85d2fc9 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt @@ -3018,7 +3018,7 @@ class CallActivity : CallBaseActivity() { return } val screenParticipantDisplayItem = participantItems.find { it.sessionKey == "$sessionId-screen" } - if (screenParticipantDisplayItem != null) { + if (screenParticipantDisplayItem == null) { addParticipantDisplayItem(callParticipantModel, "screen") } } From 99d5e5d07a6759b55cb7fa960f896f980a67f6de Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Wed, 7 May 2025 16:10:39 +0200 Subject: [PATCH 22/28] clearImage before release surfaceViewRenderer in ParticipantDisplayItem at least as another cell. fullscreen needs to be implemented Signed-off-by: Marcel Hibbe --- .../java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt index 4298f7aa8..2af4ffde3 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt @@ -94,6 +94,7 @@ class ParticipantDisplayItem( surfaceViewRenderer?.let { renderer -> try { mediaStream?.videoTracks?.firstOrNull()?.removeSink(renderer) + renderer.clearImage() renderer.release() (renderer.parent as? ViewGroup)?.removeView(renderer) } catch (e: Exception) { From 74d937c901a586d093a53590c976c3c8e28dda97 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Wed, 7 May 2025 17:28:45 +0200 Subject: [PATCH 23/28] ktlint format + fix lint warning Signed-off-by: Marcel Hibbe --- .../talk/call/components/ParticipantTile.kt | 2 +- .../talk/call/components/WebRTCVideoView.kt | 5 +- .../talk/utils/message/MessageUtils.kt | 4 +- app/src/main/res/layout/call_item.xml | 80 ------------------- app/src/main/res/values/dimens.xml | 1 - 5 files changed, 4 insertions(+), 88 deletions(-) delete mode 100644 app/src/main/res/layout/call_item.xml diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt index 74f32af3f..e17403c0c 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt @@ -38,7 +38,7 @@ fun ParticipantTile( participant: ParticipantUiState, eglBase: EglBase?, modifier: Modifier = Modifier, - isVoiceOnlyCall: Boolean, + isVoiceOnlyCall: Boolean ) { val colorInt = ColorGenerator.shared.usernameToColor(participant.nick) diff --git a/app/src/main/java/com/nextcloud/talk/call/components/WebRTCVideoView.kt b/app/src/main/java/com/nextcloud/talk/call/components/WebRTCVideoView.kt index 814013ee1..1b3c2be0f 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/WebRTCVideoView.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/WebRTCVideoView.kt @@ -16,10 +16,7 @@ import org.webrtc.EglBase import org.webrtc.SurfaceViewRenderer @Composable -fun WebRTCVideoView( - participant: ParticipantUiState, - eglBase: EglBase? -) { +fun WebRTCVideoView(participant: ParticipantUiState, eglBase: EglBase?) { AndroidView( factory = { context -> SurfaceViewRenderer(context).apply { diff --git a/app/src/main/java/com/nextcloud/talk/utils/message/MessageUtils.kt b/app/src/main/java/com/nextcloud/talk/utils/message/MessageUtils.kt index eee95b293..9ed9e524c 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/message/MessageUtils.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/message/MessageUtils.kt @@ -9,13 +9,13 @@ package com.nextcloud.talk.utils.message import android.content.Context import android.content.Intent import android.graphics.Typeface -import android.net.Uri import android.text.SpannableString import android.text.SpannableStringBuilder import android.text.Spanned import android.text.style.StyleSpan import android.util.Log import android.view.View +import androidx.core.net.toUri import com.nextcloud.talk.R import com.nextcloud.talk.chat.data.model.ChatMessage import com.nextcloud.talk.ui.theme.ViewThemeUtils @@ -145,7 +145,7 @@ class MessageUtils(val context: Context) { "file" -> { 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) } } diff --git a/app/src/main/res/layout/call_item.xml b/app/src/main/res/layout/call_item.xml deleted file mode 100644 index 9edd399a4..000000000 --- a/app/src/main/res/layout/call_item.xml +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 768c7c388..f9ccd0c99 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -54,7 +54,6 @@ 24dp 18dp - 180dp 110dp 48dp 48dp From addb022083dd3685828609eafb9d787d39fc87b1 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Mon, 12 May 2025 13:44:11 +0200 Subject: [PATCH 24/28] remove unused resources Signed-off-by: Marcel Hibbe --- app/src/main/res/values/strings.xml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index ec6a5d518..27f94ca7b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -175,7 +175,6 @@ How to translate with transifex: Notifications are granted Notifications are declined - Notifications are declined. Please allow notifications in Android settings Notification troubleshooting @@ -373,7 +372,6 @@ How to translate with transifex: Do not disturb Away Invisible - 😃 👍 👎 @@ -398,9 +396,6 @@ How to translate with transifex: There was a problem loading your chats Close Close Icon - Refresh - Please check your internet connection - Visible Enter a message … @@ -438,9 +433,7 @@ How to translate with transifex: Failed Sending Failed to send message: - Remote audio off Add attachment - Recent See %d similar message See %d similar messages @@ -461,7 +454,6 @@ How to translate with transifex: Guest access password Enter a password Error during setting/disabling the password. - Weak password Share conversation link Resend invitations Invitations were sent out again. @@ -477,11 +469,8 @@ How to translate with transifex: %s characters limit has been hit - Email Group Team - Groups - Teams Participants Add participants Start group chat From 962972dce4f8929c4e5c9877c9b6ddadd2c5bdf6 Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Mon, 12 May 2025 13:54:40 +0200 Subject: [PATCH 25/28] resolve lint/detekt warnings Signed-off-by: Marcel Hibbe --- .../talk/adapters/ParticipantDisplayItem.kt | 3 +++ .../talk/call/components/ParticipantGrid.kt | 14 ++++++++++++++ .../talk/call/components/ParticipantTile.kt | 7 +++++-- .../com/nextcloud/talk/utils/ColorGenerator.kt | 1 + 4 files changed, 23 insertions(+), 2 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt index 2af4ffde3..9b0fa75e6 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt @@ -30,6 +30,7 @@ import org.webrtc.MediaStream import org.webrtc.PeerConnection.IceConnectionState import org.webrtc.SurfaceViewRenderer +@Suppress("LongParameterList") class ParticipantDisplayItem( private val context: Context, private val baseUrl: String, @@ -79,6 +80,7 @@ class ParticipantDisplayItem( } override fun onReaction(reaction: String) { + // unused } } @@ -88,6 +90,7 @@ class ParticipantDisplayItem( updateFromModel() } + @Suppress("Detekt.TooGenericExceptionCaught") fun destroy() { callParticipantModel.removeObserver(callParticipantModelObserver) diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt index 1833c448a..b9c4e0ee6 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt @@ -27,6 +27,7 @@ import com.nextcloud.talk.call.ParticipantUiState import org.webrtc.EglBase import kotlin.math.ceil +@Suppress("MagicNumber", "TooManyFunctions") @Composable fun ParticipantGrid( modifier: Modifier = Modifier, @@ -101,6 +102,7 @@ fun ParticipantGrid( } } +@Suppress("MagicNumber") @Preview @Composable fun ParticipantGridPreview() { @@ -111,6 +113,7 @@ fun ParticipantGridPreview() { ) {} } +@Suppress("MagicNumber") @Preview @Composable fun TwoParticipants() { @@ -121,6 +124,7 @@ fun TwoParticipants() { ) {} } +@Suppress("MagicNumber") @Preview @Composable fun ThreeParticipants() { @@ -131,6 +135,7 @@ fun ThreeParticipants() { ) {} } +@Suppress("MagicNumber") @Preview @Composable fun FourParticipants() { @@ -141,6 +146,7 @@ fun FourParticipants() { ) {} } +@Suppress("MagicNumber") @Preview @Composable fun FiveParticipants() { @@ -151,6 +157,7 @@ fun FiveParticipants() { ) {} } +@Suppress("MagicNumber") @Preview @Composable fun SevenParticipants() { @@ -161,6 +168,7 @@ fun SevenParticipants() { ) {} } +@Suppress("MagicNumber") @Preview @Composable fun FiftyParticipants() { @@ -171,6 +179,7 @@ fun FiftyParticipants() { ) {} } +@Suppress("MagicNumber") @Preview( showBackground = false, heightDp = 360, @@ -185,6 +194,7 @@ fun OneParticipantLandscape() { ) {} } +@Suppress("MagicNumber") @Preview( showBackground = false, heightDp = 360, @@ -199,6 +209,7 @@ fun TwoParticipantsLandscape() { ) {} } +@Suppress("MagicNumber") @Preview( showBackground = false, heightDp = 360, @@ -213,6 +224,7 @@ fun ThreeParticipantsLandscape() { ) {} } +@Suppress("MagicNumber") @Preview( showBackground = false, heightDp = 360, @@ -227,6 +239,7 @@ fun FourParticipantsLandscape() { ) {} } +@Suppress("MagicNumber") @Preview( showBackground = false, heightDp = 360, @@ -241,6 +254,7 @@ fun SevenParticipantsLandscape() { ) {} } +@Suppress("MagicNumber") @Preview( showBackground = false, heightDp = 360, diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt index e17403c0c..dc6828adf 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt @@ -33,6 +33,9 @@ import com.nextcloud.talk.call.ParticipantUiState import com.nextcloud.talk.utils.ColorGenerator import org.webrtc.EglBase +const val NICK_OFFSET = 4f +const val NICK_BLUR_RADIUS = 4f + @Composable fun ParticipantTile( participant: ParticipantUiState, @@ -89,8 +92,8 @@ fun ParticipantTile( style = MaterialTheme.typography.bodyMedium.copy( shadow = Shadow( color = Color.Black, - offset = Offset(4f, 4f), - blurRadius = 4f + offset = Offset(NICK_OFFSET, NICK_OFFSET), + blurRadius = NICK_BLUR_RADIUS ) ) ) diff --git a/app/src/main/java/com/nextcloud/talk/utils/ColorGenerator.kt b/app/src/main/java/com/nextcloud/talk/utils/ColorGenerator.kt index c8fa1e58e..468f72374 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/ColorGenerator.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/ColorGenerator.kt @@ -15,6 +15,7 @@ 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 From eaed93087bb06a2cb45e1c2a088068c3d2b4391d Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Mon, 12 May 2025 15:01:26 +0200 Subject: [PATCH 26/28] simplify participant data structure move ParticipantUiState into ParticipantDisplayItem Signed-off-by: Marcel Hibbe --- .../nextcloud/talk/activities/CallActivity.kt | 21 +------ .../talk/adapters/ParticipantDisplayItem.kt | 12 +++- .../nextcloud/talk/call/ParticipantUiState.kt | 21 ------- .../call/components/AvatarWithFallback.kt | 2 +- .../talk/call/components/ParticipantGrid.kt | 56 ++++++++----------- .../talk/call/components/ParticipantTile.kt | 22 ++++---- .../talk/call/components/WebRTCVideoView.kt | 2 +- 7 files changed, 49 insertions(+), 87 deletions(-) delete mode 100644 app/src/main/java/com/nextcloud/talk/call/ParticipantUiState.kt diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt index 4a85d2fc9..c430ab050 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt @@ -48,12 +48,12 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.annotation.DrawableRes 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.toColorInt import androidx.core.net.toUri import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.lifecycleScope import autodagger.AutoInjector import com.bluelinelabs.logansquare.LoganSquare import com.google.android.material.dialog.MaterialAlertDialogBuilder @@ -73,7 +73,6 @@ import com.nextcloud.talk.call.MessageSender import com.nextcloud.talk.call.MessageSenderMcu import com.nextcloud.talk.call.MessageSenderNoMcu import com.nextcloud.talk.call.MutableLocalCallParticipantModel -import com.nextcloud.talk.call.ParticipantUiState import com.nextcloud.talk.call.ReactionAnimator import com.nextcloud.talk.call.components.ParticipantGrid import com.nextcloud.talk.chat.ChatActivity @@ -154,7 +153,6 @@ import io.reactivex.Observer import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.Disposable import io.reactivex.schedulers.Schedulers -import kotlinx.coroutines.launch import okhttp3.Cache import org.apache.commons.lang3.StringEscapeUtils import org.greenrobot.eventbus.Subscribe @@ -308,8 +306,6 @@ class CallActivity : CallBaseActivity() { private var mediaPlayer: MediaPlayer? = null private val participantItems = mutableStateListOf() - private val participantUiStates = mutableStateListOf() - private var binding: CallActivityBinding? = null private var audioOutputDialog: AudioOutputDialog? = null private var moreCallActionsDialog: MoreCallActionsDialog? = null @@ -966,8 +962,9 @@ class CallActivity : CallBaseActivity() { binding!!.composeParticipantGrid.setContent { MaterialTheme { + val participantUiStates = participantItems.map { it.uiStateFlow.collectAsState().value } ParticipantGrid( - participants = participantUiStates.toList(), + participantUiStates = participantUiStates, eglBase = rootEglBase!!, isVoiceOnlyCall = isVoiceOnlyCall ) { @@ -2555,7 +2552,6 @@ class CallActivity : CallBaseActivity() { val participant = participantItems.find { it.sessionKey == key } participant?.destroy() participantItems.removeAll { it.sessionKey == key } - participantUiStates.removeAll { it.sessionKey == key } initGrid() } @@ -2674,17 +2670,6 @@ class CallActivity : CallBaseActivity() { if (participantItems.none { it.sessionKey == sessionKey }) { participantItems.add(participantDisplayItem) - - lifecycleScope.launch { - participantDisplayItem.uiStateFlow.collect { uiState -> - val index = participantUiStates.indexOfFirst { it.sessionKey == sessionKey } - if (index >= 0) { - participantUiStates[index] = uiState - } else { - participantUiStates.add(uiState) - } - } - } } initGrid() diff --git a/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt b/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt index 9b0fa75e6..299505afd 100644 --- a/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt +++ b/app/src/main/java/com/nextcloud/talk/adapters/ParticipantDisplayItem.kt @@ -15,7 +15,6 @@ import android.text.TextUtils import android.util.Log import android.view.ViewGroup import com.nextcloud.talk.call.CallParticipantModel -import com.nextcloud.talk.call.ParticipantUiState import com.nextcloud.talk.call.RaisedHand import com.nextcloud.talk.models.json.participants.Participant.ActorType import com.nextcloud.talk.utils.ApiUtils.getUrlForAvatar @@ -30,6 +29,17 @@ 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, diff --git a/app/src/main/java/com/nextcloud/talk/call/ParticipantUiState.kt b/app/src/main/java/com/nextcloud/talk/call/ParticipantUiState.kt deleted file mode 100644 index 71b93e85f..000000000 --- a/app/src/main/java/com/nextcloud/talk/call/ParticipantUiState.kt +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Nextcloud Talk - Android Client - * - * SPDX-FileCopyrightText: 2025 Marcel Hibbe - * SPDX-License-Identifier: GPL-3.0-or-later - */ - -package com.nextcloud.talk.call - -import org.webrtc.MediaStream - -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? -) diff --git a/app/src/main/java/com/nextcloud/talk/call/components/AvatarWithFallback.kt b/app/src/main/java/com/nextcloud/talk/call/components/AvatarWithFallback.kt index 49a09cd6c..5cfd29a81 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/AvatarWithFallback.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/AvatarWithFallback.kt @@ -22,7 +22,7 @@ import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import coil.compose.AsyncImage -import com.nextcloud.talk.call.ParticipantUiState +import com.nextcloud.talk.adapters.ParticipantUiState @Composable fun AvatarWithFallback(participant: ParticipantUiState, modifier: Modifier = Modifier) { diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt index b9c4e0ee6..d6baa6775 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt @@ -5,6 +5,8 @@ * SPDX-License-Identifier: GPL-3.0-or-later */ +@file:Suppress("MagicNumber", "TooManyFunctions") + package com.nextcloud.talk.call.components import android.content.res.Configuration @@ -23,16 +25,15 @@ 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.call.ParticipantUiState +import com.nextcloud.talk.adapters.ParticipantUiState import org.webrtc.EglBase import kotlin.math.ceil -@Suppress("MagicNumber", "TooManyFunctions") @Composable fun ParticipantGrid( modifier: Modifier = Modifier, eglBase: EglBase?, - participants: List, + participantUiStates: List, isVoiceOnlyCall: Boolean, onClick: () -> Unit ) { @@ -43,19 +44,19 @@ fun ParticipantGrid( val columns = if (isPortrait) { - when (participants.size) { + when (participantUiStates.size) { 1, 2, 3 -> 1 else -> 2 } } else { - when (participants.size) { + when (participantUiStates.size) { 1 -> 1 2, 4 -> 2 else -> 3 } } - val rows = ceil(participants.size / columns.toFloat()).toInt() + val rows = ceil(participantUiStates.size / columns.toFloat()).toInt() val heightForNonGridComponents = if (isVoiceOnlyCall) { // this is a workaround for now. It should ~summarize the height of callInfosLinearLayout and callControls @@ -87,11 +88,11 @@ fun ParticipantGrid( contentPadding = PaddingValues(vertical = edgePadding) ) { items( - participants, + participantUiStates, key = { it.sessionKey } ) { participant -> ParticipantTile( - participant = participant, + participantUiState = participant, modifier = Modifier .height(itemHeight) .fillMaxWidth(), @@ -102,84 +103,76 @@ fun ParticipantGrid( } } -@Suppress("MagicNumber") @Preview @Composable fun ParticipantGridPreview() { ParticipantGrid( - participants = getTestParticipants(1), + participantUiStates = getTestParticipants(1), eglBase = null, isVoiceOnlyCall = false ) {} } -@Suppress("MagicNumber") @Preview @Composable fun TwoParticipants() { ParticipantGrid( - participants = getTestParticipants(2), + participantUiStates = getTestParticipants(2), eglBase = null, isVoiceOnlyCall = false ) {} } -@Suppress("MagicNumber") @Preview @Composable fun ThreeParticipants() { ParticipantGrid( - participants = getTestParticipants(3), + participantUiStates = getTestParticipants(3), eglBase = null, isVoiceOnlyCall = false ) {} } -@Suppress("MagicNumber") @Preview @Composable fun FourParticipants() { ParticipantGrid( - participants = getTestParticipants(4), + participantUiStates = getTestParticipants(4), eglBase = null, isVoiceOnlyCall = false ) {} } -@Suppress("MagicNumber") @Preview @Composable fun FiveParticipants() { ParticipantGrid( - participants = getTestParticipants(5), + participantUiStates = getTestParticipants(5), eglBase = null, isVoiceOnlyCall = false ) {} } -@Suppress("MagicNumber") @Preview @Composable fun SevenParticipants() { ParticipantGrid( - participants = getTestParticipants(7), + participantUiStates = getTestParticipants(7), eglBase = null, isVoiceOnlyCall = false ) {} } -@Suppress("MagicNumber") @Preview @Composable fun FiftyParticipants() { ParticipantGrid( - participants = getTestParticipants(50), + participantUiStates = getTestParticipants(50), eglBase = null, isVoiceOnlyCall = false ) {} } -@Suppress("MagicNumber") @Preview( showBackground = false, heightDp = 360, @@ -188,13 +181,12 @@ fun FiftyParticipants() { @Composable fun OneParticipantLandscape() { ParticipantGrid( - participants = getTestParticipants(1), + participantUiStates = getTestParticipants(1), eglBase = null, isVoiceOnlyCall = false ) {} } -@Suppress("MagicNumber") @Preview( showBackground = false, heightDp = 360, @@ -203,13 +195,12 @@ fun OneParticipantLandscape() { @Composable fun TwoParticipantsLandscape() { ParticipantGrid( - participants = getTestParticipants(2), + participantUiStates = getTestParticipants(2), eglBase = null, isVoiceOnlyCall = false ) {} } -@Suppress("MagicNumber") @Preview( showBackground = false, heightDp = 360, @@ -218,13 +209,12 @@ fun TwoParticipantsLandscape() { @Composable fun ThreeParticipantsLandscape() { ParticipantGrid( - participants = getTestParticipants(3), + participantUiStates = getTestParticipants(3), eglBase = null, isVoiceOnlyCall = false ) {} } -@Suppress("MagicNumber") @Preview( showBackground = false, heightDp = 360, @@ -233,13 +223,12 @@ fun ThreeParticipantsLandscape() { @Composable fun FourParticipantsLandscape() { ParticipantGrid( - participants = getTestParticipants(4), + participantUiStates = getTestParticipants(4), eglBase = null, isVoiceOnlyCall = false ) {} } -@Suppress("MagicNumber") @Preview( showBackground = false, heightDp = 360, @@ -248,13 +237,12 @@ fun FourParticipantsLandscape() { @Composable fun SevenParticipantsLandscape() { ParticipantGrid( - participants = getTestParticipants(7), + participantUiStates = getTestParticipants(7), eglBase = null, isVoiceOnlyCall = false ) {} } -@Suppress("MagicNumber") @Preview( showBackground = false, heightDp = 360, @@ -263,7 +251,7 @@ fun SevenParticipantsLandscape() { @Composable fun FiftyParticipantsLandscape() { ParticipantGrid( - participants = getTestParticipants(50), + participantUiStates = getTestParticipants(50), eglBase = null, isVoiceOnlyCall = false ) {} diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt index dc6828adf..4d0330c23 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt @@ -29,7 +29,7 @@ import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import com.nextcloud.talk.R -import com.nextcloud.talk.call.ParticipantUiState +import com.nextcloud.talk.adapters.ParticipantUiState import com.nextcloud.talk.utils.ColorGenerator import org.webrtc.EglBase @@ -38,28 +38,28 @@ const val NICK_BLUR_RADIUS = 4f @Composable fun ParticipantTile( - participant: ParticipantUiState, + participantUiState: ParticipantUiState, eglBase: EglBase?, modifier: Modifier = Modifier, isVoiceOnlyCall: Boolean ) { - val colorInt = ColorGenerator.shared.usernameToColor(participant.nick) + val colorInt = ColorGenerator.shared.usernameToColor(participantUiState.nick) Box( modifier = modifier .clip(RoundedCornerShape(12.dp)) .background(Color(colorInt)) ) { - if (!isVoiceOnlyCall && participant.isStreamEnabled && participant.mediaStream != null) { - WebRTCVideoView(participant, eglBase) + if (!isVoiceOnlyCall && participantUiState.isStreamEnabled && participantUiState.mediaStream != null) { + WebRTCVideoView(participantUiState, eglBase) } else { AvatarWithFallback( - participant = participant, + participant = participantUiState, modifier = Modifier.align(Alignment.Center) ) } - if (participant.raisedHand) { + if (participantUiState.raisedHand) { Icon( painter = painterResource(id = R.drawable.ic_hand_back_left), contentDescription = "Raised Hand", @@ -71,7 +71,7 @@ fun ParticipantTile( ) } - if (!participant.isAudioEnabled) { + if (!participantUiState.isAudioEnabled) { Icon( painter = painterResource(id = R.drawable.ic_mic_off_white_24px), contentDescription = "Mic Off", @@ -84,7 +84,7 @@ fun ParticipantTile( } Text( - text = participant.nick, + text = participantUiState.nick, color = Color.White, modifier = Modifier .align(Alignment.BottomStart) @@ -98,7 +98,7 @@ fun ParticipantTile( ) ) - if (!participant.isConnected) { + if (!participantUiState.isConnected) { CircularProgressIndicator( modifier = Modifier.align(Alignment.Center) ) @@ -120,7 +120,7 @@ fun ParticipantTilePreview() { mediaStream = null ) ParticipantTile( - participant = participant, + participantUiState = participant, modifier = Modifier .fillMaxWidth() .height(300.dp), diff --git a/app/src/main/java/com/nextcloud/talk/call/components/WebRTCVideoView.kt b/app/src/main/java/com/nextcloud/talk/call/components/WebRTCVideoView.kt index 1b3c2be0f..375627481 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/WebRTCVideoView.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/WebRTCVideoView.kt @@ -11,7 +11,7 @@ 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.call.ParticipantUiState +import com.nextcloud.talk.adapters.ParticipantUiState import org.webrtc.EglBase import org.webrtc.SurfaceViewRenderer From 6a048fde08b58e4a1d9085413d84d4c6a281423e Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Tue, 13 May 2025 11:27:55 +0200 Subject: [PATCH 27/28] improve/fix contents for picture in picture mode depending on amount of participants, voiceOnly call and enabled/disabled own video, the contents of PIP windows are updated. This will be further improved when speaker-view is implemented. Signed-off-by: Marcel Hibbe --- .../nextcloud/talk/activities/CallActivity.kt | 68 ++++++++----------- .../call/components/AvatarWithFallback.kt | 3 - .../talk/call/components/ParticipantGrid.kt | 44 ++++++++---- .../talk/call/components/ParticipantTile.kt | 13 +++- 4 files changed, 70 insertions(+), 58 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt index c430ab050..d0d7084fe 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.kt @@ -938,35 +938,17 @@ class CallActivity : CallBaseActivity() { binding!!.pipSelfVideoRenderer.release() } - private fun initSelfVideoViewForPipMode() { - if (!isVoiceOnlyCall) { - 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!!.pipSelfVideoRenderer.visibility = View.GONE - } - } - private fun initGrid() { Log.d(TAG, "initGrid") - + binding!!.composeParticipantGrid.visibility = View.VISIBLE binding!!.composeParticipantGrid.setContent { MaterialTheme { val participantUiStates = participantItems.map { it.uiStateFlow.collectAsState().value } ParticipantGrid( participantUiStates = participantUiStates, eglBase = rootEglBase!!, - isVoiceOnlyCall = isVoiceOnlyCall + isVoiceOnlyCall = isVoiceOnlyCall, + isInPipMode = isInPipMode ) { animateCallControls(true, 0) } @@ -3206,33 +3188,38 @@ class CallActivity : CallBaseActivity() { override fun updateUiForPipMode() { Log.d(TAG, "updateUiForPipMode") - val params = RelativeLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - params.setMargins(0, 0, 0, 0) - binding!!.composeParticipantGrid.layoutParams = params binding!!.callControls.visibility = View.GONE binding!!.callInfosLinearLayout.visibility = View.GONE binding!!.selfVideoViewWrapper.visibility = View.GONE binding!!.callStates.callStateRelativeLayout.visibility = View.GONE binding!!.pipCallConversationNameTextView.text = conversationName - if (isVoiceOnlyCall) { - if (participantItems.size > 1) { + binding!!.selfVideoRenderer.clearImage() + binding!!.selfVideoRenderer.release() + + if (participantItems.size == 1) { + binding!!.pipOverlay.visibility = View.GONE + } else { + 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 - } else { - binding!!.pipOverlay.visibility = View.GONE - } - } else { - binding!!.selfVideoRenderer.clearImage() - binding!!.selfVideoRenderer.release() - if (participantItems.size > 1) { - binding!!.pipOverlay.visibility = View.VISIBLE - initSelfVideoViewForPipMode() - } else { - binding!!.pipOverlay.visibility = View.GONE } } } @@ -3240,6 +3227,7 @@ class CallActivity : CallBaseActivity() { override fun updateUiForNormalMode() { Log.d(TAG, "updateUiForNormalMode") binding!!.pipOverlay.visibility = View.GONE + binding!!.composeParticipantGrid.visibility = View.VISIBLE if (isVoiceOnlyCall) { binding!!.callControls.visibility = View.VISIBLE diff --git a/app/src/main/java/com/nextcloud/talk/call/components/AvatarWithFallback.kt b/app/src/main/java/com/nextcloud/talk/call/components/AvatarWithFallback.kt index 5cfd29a81..fa96171f1 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/AvatarWithFallback.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/AvatarWithFallback.kt @@ -10,7 +10,6 @@ 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.layout.size import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -19,7 +18,6 @@ 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.dp import androidx.compose.ui.unit.sp import coil.compose.AsyncImage import com.nextcloud.talk.adapters.ParticipantUiState @@ -34,7 +32,6 @@ fun AvatarWithFallback(participant: ParticipantUiState, modifier: Modifier = Mod Box( modifier = modifier - .size(150.dp) .clip(CircleShape), contentAlignment = Alignment.Center ) { diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt index d6baa6775..b812b5941 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt @@ -29,12 +29,14 @@ 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, isVoiceOnlyCall: Boolean, + isInPipMode: Boolean, onClick: () -> Unit ) { val configuration = LocalConfiguration.current @@ -58,7 +60,7 @@ fun ParticipantGrid( val rows = ceil(participantUiStates.size / columns.toFloat()).toInt() - val heightForNonGridComponents = if (isVoiceOnlyCall) { + 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 @@ -97,6 +99,7 @@ fun ParticipantGrid( .height(itemHeight) .fillMaxWidth(), eglBase = eglBase, + isInPipMode = isInPipMode, isVoiceOnlyCall = isVoiceOnlyCall ) } @@ -109,7 +112,8 @@ fun ParticipantGridPreview() { ParticipantGrid( participantUiStates = getTestParticipants(1), eglBase = null, - isVoiceOnlyCall = false + isVoiceOnlyCall = false, + isInPipMode = false ) {} } @@ -119,7 +123,8 @@ fun TwoParticipants() { ParticipantGrid( participantUiStates = getTestParticipants(2), eglBase = null, - isVoiceOnlyCall = false + isVoiceOnlyCall = false, + isInPipMode = false ) {} } @@ -129,7 +134,8 @@ fun ThreeParticipants() { ParticipantGrid( participantUiStates = getTestParticipants(3), eglBase = null, - isVoiceOnlyCall = false + isVoiceOnlyCall = false, + isInPipMode = false ) {} } @@ -139,7 +145,8 @@ fun FourParticipants() { ParticipantGrid( participantUiStates = getTestParticipants(4), eglBase = null, - isVoiceOnlyCall = false + isVoiceOnlyCall = false, + isInPipMode = false ) {} } @@ -149,7 +156,8 @@ fun FiveParticipants() { ParticipantGrid( participantUiStates = getTestParticipants(5), eglBase = null, - isVoiceOnlyCall = false + isVoiceOnlyCall = false, + isInPipMode = false ) {} } @@ -159,7 +167,8 @@ fun SevenParticipants() { ParticipantGrid( participantUiStates = getTestParticipants(7), eglBase = null, - isVoiceOnlyCall = false + isVoiceOnlyCall = false, + isInPipMode = false ) {} } @@ -169,7 +178,8 @@ fun FiftyParticipants() { ParticipantGrid( participantUiStates = getTestParticipants(50), eglBase = null, - isVoiceOnlyCall = false + isVoiceOnlyCall = false, + isInPipMode = false ) {} } @@ -183,7 +193,8 @@ fun OneParticipantLandscape() { ParticipantGrid( participantUiStates = getTestParticipants(1), eglBase = null, - isVoiceOnlyCall = false + isVoiceOnlyCall = false, + isInPipMode = false ) {} } @@ -197,7 +208,8 @@ fun TwoParticipantsLandscape() { ParticipantGrid( participantUiStates = getTestParticipants(2), eglBase = null, - isVoiceOnlyCall = false + isVoiceOnlyCall = false, + isInPipMode = false ) {} } @@ -211,7 +223,8 @@ fun ThreeParticipantsLandscape() { ParticipantGrid( participantUiStates = getTestParticipants(3), eglBase = null, - isVoiceOnlyCall = false + isVoiceOnlyCall = false, + isInPipMode = false ) {} } @@ -225,7 +238,8 @@ fun FourParticipantsLandscape() { ParticipantGrid( participantUiStates = getTestParticipants(4), eglBase = null, - isVoiceOnlyCall = false + isVoiceOnlyCall = false, + isInPipMode = false ) {} } @@ -239,7 +253,8 @@ fun SevenParticipantsLandscape() { ParticipantGrid( participantUiStates = getTestParticipants(7), eglBase = null, - isVoiceOnlyCall = false + isVoiceOnlyCall = false, + isInPipMode = false ) {} } @@ -253,7 +268,8 @@ fun FiftyParticipantsLandscape() { ParticipantGrid( participantUiStates = getTestParticipants(50), eglBase = null, - isVoiceOnlyCall = false + isVoiceOnlyCall = false, + isInPipMode = false ) {} } diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt index 4d0330c23..049126025 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt @@ -36,10 +36,12 @@ import org.webrtc.EglBase const val NICK_OFFSET = 4f const val NICK_BLUR_RADIUS = 4f +@Suppress("Detekt.LongMethod") @Composable fun ParticipantTile( participantUiState: ParticipantUiState, eglBase: EglBase?, + isInPipMode: Boolean, modifier: Modifier = Modifier, isVoiceOnlyCall: Boolean ) { @@ -53,9 +55,17 @@ fun ParticipantTile( if (!isVoiceOnlyCall && participantUiState.isStreamEnabled && participantUiState.mediaStream != null) { WebRTCVideoView(participantUiState, eglBase) } else { + val avatarSize = if (isInPipMode) { + 100.dp + } else { + 150.dp + } + AvatarWithFallback( participant = participantUiState, - modifier = Modifier.align(Alignment.Center) + modifier = Modifier + .align(Alignment.Center) + .size(avatarSize) ) } @@ -125,6 +135,7 @@ fun ParticipantTilePreview() { .fillMaxWidth() .height(300.dp), eglBase = null, + isInPipMode = false, isVoiceOnlyCall = false ) } From 31433f8ed991bbb0103b17f23f9d70a881fe9d0d Mon Sep 17 00:00:00 2001 From: Marcel Hibbe Date: Tue, 13 May 2025 17:15:58 +0200 Subject: [PATCH 28/28] adapt avatar size to box size ..by using a BoxWithConstraints Signed-off-by: Marcel Hibbe --- .../talk/call/components/ParticipantGrid.kt | 5 +- .../talk/call/components/ParticipantTile.kt | 101 +++++++++--------- 2 files changed, 54 insertions(+), 52 deletions(-) diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt index b812b5941..724b0fad7 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantGrid.kt @@ -99,7 +99,6 @@ fun ParticipantGrid( .height(itemHeight) .fillMaxWidth(), eglBase = eglBase, - isInPipMode = isInPipMode, isVoiceOnlyCall = isVoiceOnlyCall ) } @@ -278,9 +277,9 @@ fun getTestParticipants(numberOfParticipants: Int): List { for (i: Int in 1..numberOfParticipants) { val participant = ParticipantUiState( sessionKey = i.toString(), - nick = "testuser$i Test", + nick = "test$i user", isConnected = true, - isAudioEnabled = true, + isAudioEnabled = false, isStreamEnabled = true, raisedHand = true, avatarUrl = "", diff --git a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt index 049126025..c15edf292 100644 --- a/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt +++ b/app/src/main/java/com/nextcloud/talk/call/components/ParticipantTile.kt @@ -9,6 +9,8 @@ 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 @@ -28,6 +30,7 @@ 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 @@ -35,83 +38,84 @@ 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?, - isInPipMode: Boolean, modifier: Modifier = Modifier, isVoiceOnlyCall: Boolean ) { val colorInt = ColorGenerator.shared.usernameToColor(participantUiState.nick) - Box( + 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 { - val avatarSize = if (isInPipMode) { - 100.dp - } else { - 150.dp - } - AvatarWithFallback( participant = participantUiState, modifier = Modifier - .align(Alignment.Center) .size(avatarSize) + .align(Alignment.Center) ) } - 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, + Box( modifier = Modifier - .align(Alignment.BottomStart) - .padding(10.dp), - style = MaterialTheme.typography.bodyMedium.copy( - shadow = Shadow( - color = Color.Black, - offset = Offset(NICK_OFFSET, NICK_OFFSET), - blurRadius = NICK_BLUR_RADIUS + .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) - ) + if (!participantUiState.isConnected) { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.Center) + ) + } } } } @@ -135,7 +139,6 @@ fun ParticipantTilePreview() { .fillMaxWidth() .height(300.dp), eglBase = null, - isInPipMode = false, isVoiceOnlyCall = false ) }