diff --git a/app/build.gradle b/app/build.gradle index c4369213..671c3a41 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -32,6 +32,33 @@ android { includeInBundle = false } + packaging { + // JNI(.so) 관련 + jniLibs { + // pickFirsts: 충돌 시 첫 파일만 채택 + pickFirsts += ["**/libaosl.so"] + } + + // 일반 리소스(META-INF 등) 관련 + resources { + // pickFirsts: 충돌 시 첫 파일만 채택 + pickFirsts += [ + "META-INF/LICENSE.txt", + "META-INF/NOTICE*" + ] + + // 자주 쓰는 제외/병합 예시 + excludes += [ + "META-INF/DEPENDENCIES", + "META-INF/AL2.0", + "META-INF/LGPL2.1" + ] + merges += [ + "META-INF/services/**" + ] + } + } + defaultConfig { applicationId "kr.co.vividnext.sodalive" minSdk 23 @@ -168,8 +195,8 @@ dependencies { implementation "io.github.bootpay:android:4.4.3" // agora - implementation "io.agora.rtc:voice-sdk:4.6.0" - implementation 'io.agora.rtm:rtm-sdk:1.5.3' + implementation "io.agora.rtc:voice-sdk:4.2.6" + implementation 'io.agora:agora-rtm:2.2.6' // Glide implementation 'com.github.bumptech.glide:glide:5.0.5' diff --git a/app/src/main/java/kr/co/vividnext/sodalive/agora/Agora.kt b/app/src/main/java/kr/co/vividnext/sodalive/agora/Agora.kt index 14b51379..8bb3f39a 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/agora/Agora.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/agora/Agora.kt @@ -6,28 +6,31 @@ import io.agora.rtc2.Constants import io.agora.rtc2.IRtcEngineEventHandler import io.agora.rtc2.RtcEngine import io.agora.rtm.ErrorInfo +import io.agora.rtm.GetOnlineUsersOptions +import io.agora.rtm.GetOnlineUsersResult +import io.agora.rtm.PublishOptions import io.agora.rtm.ResultCallback -import io.agora.rtm.RtmChannel -import io.agora.rtm.RtmChannelListener import io.agora.rtm.RtmClient -import io.agora.rtm.RtmClientListener -import io.agora.rtm.SendMessageOptions +import io.agora.rtm.RtmConfig +import io.agora.rtm.RtmConstants +import io.agora.rtm.RtmEventListener +import io.agora.rtm.SubscribeOptions import kr.co.vividnext.sodalive.BuildConfig +import kr.co.vividnext.sodalive.common.SharedPreferenceManager import kr.co.vividnext.sodalive.live.room.LiveRoomRequestType import kotlin.concurrent.thread class Agora( + private val uid: Long, private val context: Context, private val rtcEventHandler: IRtcEngineEventHandler, - private val rtmClientListener: RtmClientListener + private val rtmEventListener: RtmEventListener ) { - // RTM client instance - private var rtmClient: RtmClient? = null + // 상태 플래그: RTM 로그인 완료 여부 + private var rtmLoggedIn: Boolean = false - // RTM channel instance - private var rtmChannel: RtmChannel? = null - - private var rtcEngine: RtcEngine? = null + // 상태 플래그: RTM 로그인 진행 중 여부 + private var rtmLoginInProgress: Boolean = false init { initAgoraEngine() @@ -42,19 +45,23 @@ class Agora( } } - fun deInitAgoraEngine() { + fun deInitAgoraEngine(rtmEventListener: RtmEventListener) { deInitRtcEngine() - deInitRtmChannelAndClient() + deInitRtmClient(rtmEventListener) } // region RtcEngine + private var rtcEngine: RtcEngine? = null + @Throws(Exception::class) private fun initRtcEngine() { + Logger.e("initRtcEngine") rtcEngine = RtcEngine.create( context, BuildConfig.AGORA_APP_ID, rtcEventHandler ) + Logger.e("initRtcEngine - rtcEngine: ${rtcEngine != null}") rtcEngine!!.setChannelProfile(Constants.CHANNEL_PROFILE_LIVE_BROADCASTING) rtcEngine!!.setAudioProfile( @@ -66,6 +73,15 @@ class Agora( } fun joinRtcChannel(uid: Int, rtcToken: String, channelName: String) { + val state = rtcEngine?.connectionState + val isDisconnected = state == null || state == Constants.CONNECTION_STATE_DISCONNECTED + + if (!isDisconnected) { + Logger.e("joinRtcChannel - skip (state=$state)") + return + } + + Logger.e("joinRtcChannel - proceed (state=$state) uid=$uid channel=$channelName") rtcEngine!!.joinChannel( rtcToken, channelName, @@ -90,6 +106,10 @@ class Agora( return rtcEngine!!.connectionState } + fun isRtmLoggedIn(): Boolean { + return rtmLoggedIn + } + fun deInitRtcEngine() { if (rtcEngine != null) { rtcEngine!!.leaveChannel() @@ -103,64 +123,179 @@ class Agora( // endregion // region RtmClient + private var rtmClient: RtmClient? = null + private var roomChannelName: String? = null + @Throws(Exception::class) private fun initRtmClient() { - rtmClient = RtmClient.createInstance( - context, - BuildConfig.AGORA_APP_ID, - rtmClientListener - ) + val rtmConfig = RtmConfig.Builder(BuildConfig.AGORA_APP_ID, uid.toString()) + .eventListener(rtmEventListener) + .build() + + rtmClient = RtmClient.create(rtmConfig) } - fun createRtmChannelAndLogin( - uid: String, + fun rtmLogin( rtmToken: String, channelName: String, - rtmChannelListener: RtmChannelListener, rtmChannelJoinSuccess: () -> Unit, rtmChannelJoinFail: () -> Unit ) { - rtmChannel = rtmClient!!.createChannel(channelName, rtmChannelListener) - rtmClient!!.login( - rtmToken, - uid, - object : ResultCallback { - override fun onSuccess(p0: Void?) { - rtmChannel!!.join(object : ResultCallback { - override fun onSuccess(p0: Void?) { - Logger.e("rtmChannel join - onSuccess") - rtmChannelJoinSuccess() - } + // 이미 RTM 로그인 및 구독이 완료된 경우 재호출 방지 + if (rtmLoggedIn && roomChannelName == channelName) { + Logger.e("rtmLogin - already logged in and subscribed. skip") + return + } + // 로그인 시도 중이면 재호출 방지 + if (rtmLoginInProgress) { + Logger.e("rtmLogin - already in progress. skip") + return + } - override fun onFailure(p0: ErrorInfo?) { + roomChannelName = channelName + + fun attemptLogin(attempt: Int) { + rtmClient!!.login( + rtmToken, + object : ResultCallback { + override fun onSuccess(p0: Void?) { + Logger.e("rtmClient login - success (attempt=$attempt)") + // 로그인 성공 후 두 채널 구독 시도 + subscribeChannel(rtmChannelJoinSuccess, rtmChannelJoinFail) + } + + override fun onFailure(p0: ErrorInfo?) { + Logger.e("rtmClient login - fail (attempt=$attempt), ${p0?.errorReason}") + if (attempt < 4) { + attemptLogin(attempt + 1) + } else { + rtmLoginInProgress = false rtmChannelJoinFail() } - }) + } } + ) + } - override fun onFailure(p0: ErrorInfo?) { - } - } - ) + rtmLoginInProgress = true + attemptLogin(1) } - fun inputChat(message: String) { - val rtmMessage = rtmClient!!.createMessage() - rtmMessage.text = message + private fun subscribeChannel( + rtmChannelJoinSuccess: () -> Unit, + rtmChannelJoinFail: () -> Unit + ) { + val targetRoom = roomChannelName + if (targetRoom == null) { + Logger.e("subscribeChannel - roomChannelName is null") + rtmChannelJoinFail() + return + } - rtmChannel!!.sendMessage( - rtmMessage, - object : ResultCallback { - override fun onSuccess(p0: Void?) { - Logger.e("sendMessage - onSuccess") - } + var completed = false + var roomSubscribed = false + var inboxSubscribed = false - override fun onFailure(p0: ErrorInfo) { - Logger.e("sendMessage fail - ${p0.errorCode}") - Logger.e("sendMessage fail - ${p0.errorDescription}") - } + fun completeSuccessIfReady() { + if (!completed && roomSubscribed && inboxSubscribed) { + completed = true + rtmLoggedIn = true + rtmLoginInProgress = false + Logger.e("RTM subscribe - both channels subscribed") + rtmChannelJoinSuccess() } - ) + } + + fun failOnce(reason: String?) { + if (!completed) { + completed = true + Logger.e("RTM subscribe failed: $reason") + rtmChannelJoinFail() + } + } + + fun subscribeRoom(attempt: Int) { + val channelOptions = SubscribeOptions() + channelOptions.withMessage = true + channelOptions.withPresence = true + Logger.e("RTM subscribe(room: $targetRoom) attempt=$attempt") + rtmClient!!.subscribe( + targetRoom, + channelOptions, + object : ResultCallback { + override fun onSuccess(responseInfo: Void?) { + Logger.e("RTM subscribe(room) success at attempt=$attempt") + roomSubscribed = true + completeSuccessIfReady() + } + + override fun onFailure(errorInfo: ErrorInfo?) { + Logger.e("RTM subscribe(room) failure at attempt=$attempt reason=${errorInfo?.errorReason}") + if (attempt < 4) { + subscribeRoom(attempt + 1) + } else { + failOnce("room subscribe failed after 3 retries (4 attempts)") + } + } + } + ) + } + + fun subscribeInbox(attempt: Int) { + val inboxChannel = "inbox_$uid" + val inboxChannelOptions = SubscribeOptions() + inboxChannelOptions.withMessage = true + Logger.e("RTM subscribe(inbox: $inboxChannel) attempt=$attempt") + rtmClient!!.subscribe( + inboxChannel, + inboxChannelOptions, + object : ResultCallback { + override fun onSuccess(responseInfo: Void?) { + Logger.e("RTM subscribe(inbox) success at attempt=$attempt") + inboxSubscribed = true + completeSuccessIfReady() + } + + override fun onFailure(errorInfo: ErrorInfo?) { + Logger.e("RTM subscribe(inbox) failure at attempt=$attempt reason=${errorInfo?.errorReason}") + if (attempt < 4) { + subscribeInbox(attempt + 1) + } else { + failOnce("inbox subscribe failed after 3 retries (4 attempts)") + } + } + } + ) + } + + // 두 채널 구독을 병렬로 시도 + subscribeRoom(1) + subscribeInbox(1) + } + + fun inputChat(message: String, onFailure: () -> Unit) { + if (roomChannelName != null) { + val options = PublishOptions() + options.setChannelType(RtmConstants.RtmChannelType.MESSAGE) + rtmClient!!.publish( + roomChannelName!!, + message, + options, + object : ResultCallback { + override fun onSuccess(p0: Void?) { + Logger.e("sendMessage - onSuccess") + } + + override fun onFailure(p0: ErrorInfo) { + Logger.e("sendMessage fail - ${p0.errorCode}") + Logger.e("sendMessage fail - ${p0.errorReason}") + } + } + ) + } else { + Logger.e("inputChat - roomChannelName is null") + onFailure() + } } fun sendRawMessageToGroup( @@ -168,23 +303,30 @@ class Agora( onSuccess: (() -> Unit)? = null, onFailure: (() -> Unit)? = null ) { - val message = rtmClient!!.createMessage() - message.rawMessage = rawMessage - rtmChannel!!.sendMessage( - message, - object : ResultCallback { - override fun onSuccess(p0: Void?) { - Logger.e("sendMessage - onSuccess") - onSuccess?.invoke() - } + if (roomChannelName != null) { + val options = PublishOptions() + options.customType = "ByteArray" + rtmClient!!.publish( + roomChannelName!!, + rawMessage, + options, + object : ResultCallback { + override fun onSuccess(p0: Void?) { + Logger.e("sendMessage - onSuccess") + onSuccess?.invoke() + } - override fun onFailure(p0: ErrorInfo) { - Logger.e("sendMessage fail - ${p0.errorCode}") - Logger.e("sendMessage fail - ${p0.errorDescription}") - onFailure?.invoke() + override fun onFailure(p0: ErrorInfo) { + Logger.e("sendMessage fail - ${p0.errorCode}") + Logger.e("sendMessage fail - ${p0.errorReason}") + onFailure?.invoke() + } } - } - ) + ) + } else { + Logger.e("inputChat - roomChannelName is null") + onFailure?.invoke() + } } fun sendRawMessageToPeer( @@ -193,34 +335,71 @@ class Agora( rawMessage: ByteArray? = null, onSuccess: () -> Unit ) { - val option = SendMessageOptions() + if (roomChannelName != null) { + val message = rawMessage ?: requestType.toString().toByteArray() + val options = PublishOptions() + options.customType = "ByteArray" + rtmClient!!.publish( + "inbox_$receiverUid", + message, + options, + object : ResultCallback { + override fun onSuccess(p0: Void?) { + Logger.e("sendMessage - onSuccess") + onSuccess() + } - val message = rtmClient!!.createMessage() - message.rawMessage = rawMessage ?: requestType.toString().toByteArray() + override fun onFailure(p0: ErrorInfo) { + Logger.e("sendMessage fail - ${p0.errorCode}") + Logger.e("sendMessage fail - ${p0.errorReason}") + } + } + ) + } else { + Logger.e("inputChat - roomChannelName is null") + } + } - rtmClient!!.sendMessageToPeer( - receiverUid, - message, - option, - object : ResultCallback { - override fun onSuccess(aVoid: Void?) { - onSuccess() + fun deInitRtmClient(rtmEventListener: RtmEventListener) { + rtmClient?.removeEventListener(rtmEventListener) + rtmClient?.unsubscribe(roomChannelName, object : ResultCallback { + override fun onSuccess(responseInfo: Void?) { + Logger.e("RTM unsubscribe - $roomChannelName") + roomChannelName = null + } + + override fun onFailure(errorInfo: ErrorInfo) { + Logger.e("RTM unsubscribe fail - ${errorInfo.errorCode}") + Logger.e("RTM unsubscribe fail - ${errorInfo.errorReason}") + } + }) + rtmClient?.unsubscribe( + "inbox_${SharedPreferenceManager.userId}", + object : ResultCallback { + override fun onSuccess(responseInfo: Void?) { + Logger.e("RTM unsubscribe - inbox_${SharedPreferenceManager.userId}") } override fun onFailure(errorInfo: ErrorInfo) { + Logger.e("RTM unsubscribe fail - ${errorInfo.errorCode}") + Logger.e("RTM unsubscribe fail - ${errorInfo.errorReason}") } + }) + rtmClient?.logout(object : ResultCallback { + override fun onSuccess(responseInfo: Void?) { + Logger.e("RTM logout") + rtmClient = null } - ) - } - fun rtmChannelIsNull(): Boolean { - return rtmChannel == null - } + override fun onFailure(errorInfo: ErrorInfo) { + Logger.e("RTM logout fail - ${errorInfo.errorCode}") + Logger.e("RTM logout fail - ${errorInfo.errorReason}") + } + }) + // 상태 리셋 + rtmLoggedIn = false + rtmLoginInProgress = false - fun deInitRtmChannelAndClient() { - rtmChannel?.leave(null) - rtmChannel?.release() - rtmClient?.logout(null) } // endregion } diff --git a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt index b562ed90..a6a6b73f 100644 --- a/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt +++ b/app/src/main/java/kr/co/vividnext/sodalive/live/room/LiveRoomActivity.kt @@ -50,13 +50,12 @@ import com.google.gson.Gson import com.orhanobut.logger.Logger import io.agora.rtc2.ClientRoleOptions import io.agora.rtc2.IRtcEngineEventHandler -import io.agora.rtc2.RtcConnection -import io.agora.rtm.RtmChannelAttribute -import io.agora.rtm.RtmChannelListener -import io.agora.rtm.RtmChannelMember -import io.agora.rtm.RtmClientListener -import io.agora.rtm.RtmMessage -import io.agora.rtm.RtmMessageType +import io.agora.rtm.LinkStateEvent +import io.agora.rtm.MessageEvent +import io.agora.rtm.PresenceEvent +import io.agora.rtm.RtmClient +import io.agora.rtm.RtmConstants +import io.agora.rtm.RtmEventListener import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.core.Observable import io.reactivex.rxjava3.schedulers.Schedulers @@ -104,6 +103,7 @@ import org.koin.android.ext.android.inject import java.util.concurrent.TimeUnit import java.util.regex.Pattern import kotlin.random.Random +import io.agora.rtc2.Constants as AgoraConstants class LiveRoomActivity : BaseActivity(ActivityLiveRoomBinding::inflate) { @@ -136,51 +136,17 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB private var isSpeaker = false private var isHost = false - private var isNoChatting = false - private var remainingNoChattingTime = NO_CHATTING_TIME - private val signatureImageUrlList = mutableListOf() - private var signatureImageUrl = "" - set(value) { - field = value - - if (field.isNotBlank()) { - showSignatureImage() - } - } - - private val signatureList = mutableListOf() - private var signature: LiveRoomDonationResponse? = null - set(value) { - field = value - - if (field != null) { - showSignatureImage() - } - } - - private val heartNicknameList = mutableListOf() - private var heartNickname = "" - set(value) { - field = value - - if (field.isNotBlank()) { - showHeartMessage() - handler.postDelayed({ - if (heartNicknameList.isNotEmpty()) { - heartNickname = heartNicknameList.removeAt(0) - } else { - hideHeartMessage() - } - }, 1500) - } - } - - private var isShowSignatureImage = false private var isAvailableLikeHeart = false private var buttonPosition = IntArray(2) private var isEntryMessageEnabled = true + // joinChannel 중복 호출 방지 플래그 + private var hasInvokedJoinChannel = false + + // region 채팅 금지 + private var isNoChatting = false + private var remainingNoChattingTime = NO_CHATTING_TIME private val countDownTimer = object : CountDownTimer(remainingNoChattingTime * 1000, 1000) { override fun onTick(millisUntilFinished: Long) { remainingNoChattingTime -= 1 @@ -228,6 +194,7 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB val noChatRoomList = SharedPreferenceManager.noChatRoomList return noChatRoomList.contains(roomId) } + // endregion private val onBackPressedCallback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { @@ -235,33 +202,9 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB } } - private val rouletteConfigResult = registerForActivityResult( - ActivityResultContracts.StartActivityForResult() - ) { result -> - val resultCode = result.resultCode - val isActiveRoulette = result.data?.getBooleanExtra(Constants.EXTRA_RESULT_ROULETTE, false) - - if (resultCode == RESULT_OK && isActiveRoulette != null) { - agora.sendRawMessageToGroup( - rawMessage = Gson().toJson( - LiveRoomChatRawMessage( - type = LiveRoomChatRawMessageType.TOGGLE_ROULETTE, - message = "", - can = 0, - donationMessage = "", - isActiveRoulette = isActiveRoulette - ) - ).toByteArray() - ) - } - } - + // region lifecycle override fun onCreate(savedInstanceState: Bundle?) { - agora = Agora( - context = this, - rtcEventHandler = rtcEventHandler, - rtmClientListener = rtmClientListener - ) + initAgora() super.onCreate(savedInstanceState) onBackPressedDispatcher.addCallback(this, onBackPressedCallback) @@ -289,7 +232,7 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB if ( viewModel.isRoomInfoInitialized() && agora.getConnectionState() == - RtcConnection.CONNECTION_STATE_TYPE.CONNECTION_STATE_DISCONNECTED.ordinal + AgoraConstants.CONNECTION_STATE_DISCONNECTED ) { val userId = SharedPreferenceManager.userId agora.joinRtcChannel( @@ -300,6 +243,21 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB } } + override fun onDestroy() { + cropper.cleanup() + hideKeyboard { + viewModel.quitRoom(roomId) { + SodaLiveService.stopService(this) + agora.deInitAgoraEngine(rtmEventListener) + RtmClient.release() + } + } + countDownTimer.cancel() + super.onDestroy() + } + // endregion + + // region setupView override fun setupView() { bindData() @@ -547,133 +505,12 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB setupChatAdapter() setupSpeakerListAdapter() } - - override fun onDestroy() { - cropper.cleanup() - hideKeyboard { - viewModel.quitRoom(roomId) { - SodaLiveService.stopService(this) - agora.deInitAgoraEngine() - } - } - countDownTimer.cancel() - super.onDestroy() - } - - private fun setHeartButtonPosition() { - handler.postDelayed( - { - // 버튼의 위치 - val button = if (isHost) { - binding.flRouletteSettings - } else { - binding.flLikeHeart - } - buttonPosition = IntArray(2) - button.getLocationInWindow(buttonPosition) - }, - 500 - ) - } + // endregion private fun secondToMillis(second: Float): Long { return (second * 1000).toLong() } - private fun addHeartAnimation() { - val button = if (isHost) { - binding.flRouletteSettings - } else { - binding.flLikeHeart - } - - // 하트 이미지뷰 생성 - val heart = ImageView(this).apply { - setImageResource(R.drawable.ic_heart_pink) // 투명한 하트 이미지 - layoutParams = FrameLayout.LayoutParams( - 33.3f.dpToPx().toInt(), - 33.3f.dpToPx().toInt() - ) // 하트 크기 설정 - } - - // 하트의 초기 위치를 버튼의 위치로 설정 - heart.x = (buttonPosition[0] + button.width / 2f - 50) // X축 (가운데 정렬) - heart.y = (buttonPosition[1] - 100).toFloat() // Y축 (버튼 바로 위에 생성) - - binding.flRoot.addView(heart) - - // 하트가 위로 올라가는 애니메이션 - val animateDuration = secondToMillis(2.5f) - val moveUpAnimator = ObjectAnimator.ofFloat( - heart, - "translationY", - heart.y, - heart.y - (screenHeight * 1000 / 2337) - ).apply { - duration = animateDuration - interpolator = AccelerateDecelerateInterpolator() - } - - val isNegativeFirst = Random.nextBoolean() - - // 좌우 이동 범위를 랜덤으로 설정 - val x = 13.3f - val startX = if (isNegativeFirst) (0 - x).dpToPx() else x.dpToPx() - val endX = if (isNegativeFirst) x.dpToPx() else (0 - x).dpToPx() - - val moveAnimator = ObjectAnimator.ofFloat( - heart, - "translationX", - heart.x + startX, - heart.x + endX - ).apply { - duration = secondToMillis(1.5f) - repeatCount = ObjectAnimator.INFINITE - repeatMode = ObjectAnimator.REVERSE - interpolator = AccelerateDecelerateInterpolator() - } - - val scaleXAnimator = ObjectAnimator.ofFloat( - heart, - "scaleX", - 0.5f, - 1.0f - ).apply { - duration = animateDuration - } - - val scaleYAnimator = ObjectAnimator.ofFloat( - heart, - "scaleY", - 0.5f, - 1.0f - ).apply { - duration = animateDuration - } - - // 하트 투명도 애니메이션 - val fadeOutAnimator = ObjectAnimator.ofFloat( - heart, - "alpha", - 1f, - 0f - ).apply { - duration = animateDuration - } - - // 애니메이션 실행 - moveUpAnimator.start() - moveAnimator.start() - fadeOutAnimator.start() - scaleXAnimator.start() - scaleYAnimator.start() - - // 애니메이션이 끝나면 하트 제거 - handler.postDelayed({ - binding.flRoot.removeView(heart) - }, animateDuration) - } - private fun showOptionMenu( context: Context, userId: Long, @@ -1079,8 +916,14 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB binding.tvMenuPanDetail.text = "" } - if (agora.rtmChannelIsNull()) { + val rtcState = agora.getConnectionState() + val rtcConnected = rtcState == AgoraConstants.CONNECTION_STATE_CONNECTED + val rtmLoggedIn = agora.isRtmLoggedIn() + if (!hasInvokedJoinChannel && !(rtcConnected && rtmLoggedIn)) { + hasInvokedJoinChannel = true joinChannel(response) + } else { + Logger.e("joinChannel - skip (rtcConnected=$rtcConnected, rtmLoggedIn=$rtmLoggedIn, hasInvokedJoinChannel=$hasInvokedJoinChannel)") } } @@ -1153,112 +996,6 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB } } - private fun initLikeHeartButton() { - if (!isHost) { - binding.flLikeHeart.visibility = View.VISIBLE - binding.flLikeHeart.setOnClickListener { - if (isAvailableLikeHeart) { - binding.flLikeHeart.isEnabled = false - viewModel.likeHeart( - roomId = roomId, - onSuccess = { - val donationRawMessage = Gson().toJson( - LiveRoomChatRawMessage( - type = LiveRoomChatRawMessageType.HEART_DONATION, - message = "", - can = 1, - signature = null, - signatureImageUrl = null, - donationMessage = null - ) - ) - - agora.sendRawMessageToGroup( - rawMessage = donationRawMessage.toByteArray(), - onSuccess = { - val nickname = viewModel.getUserNickname( - SharedPreferenceManager.userId.toInt() - ) - handler.post { - addHeartMessage(nickname) - addHeartAnimation() - lifecycleScope.launch { viewModel.addHeartDonation() } - } - }, - onFailure = { - viewModel.refundDonation(roomId) - } - ) - - binding.flLikeHeart.isEnabled = true - }, - onFailure = { - binding.flLikeHeart.isEnabled = true - } - ) - } else { - SodaDialog( - activity = this@LiveRoomActivity, - layoutInflater = layoutInflater, - title = "안내", - desc = "'좋아해요'는 유료 후원입니다.\n" + - "클릭시 1캔이 소진됩니다.", - confirmButtonTitle = "확인", - confirmButtonClick = { isAvailableLikeHeart = true } - ).show(screenWidth) - } - } - } else { - binding.flLikeHeart.visibility = View.GONE - } - } - - private fun initRouletteSettingButton() { - if (isHost) { - binding.flRouletteSettings.visibility = View.VISIBLE - binding.flRouletteSettings.setOnClickListener { - hideKeyboard { - rouletteConfigResult.launch( - Intent( - applicationContext, - RouletteConfigActivity::class.java - ) - ) - } - } - } else { - binding.flRouletteSettings.visibility = View.GONE - } - } - - private fun activatingRouletteButton(isActiveRoulette: Boolean) { - handler.postDelayed( - { - if (!isHost && isActiveRoulette) { - binding.flRoulette.visibility = View.VISIBLE - binding.flRoulette.setOnClickListener { - hideKeyboard { - viewModel.showRoulette { - RoulettePreviewDialog( - activity = this, - previewList = it, - onClickSpin = { rouletteId -> - spinRoulette(rouletteId = rouletteId) - }, - layoutInflater = layoutInflater - ).show() - } - } - } - - } else { - binding.flRoulette.visibility = View.GONE - } - }, - 500 - ) - } - private fun setNoticeAndClickableUrl(textView: TextView, text: String) { textView.text = text @@ -1563,7 +1300,13 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB ) invalidateChat() - agora.inputChat(message) + agora.inputChat(message) { + Toast.makeText( + applicationContext, + "라이브 접속에 문제가 발생했습니다.\n재접속 해주세요", + Toast.LENGTH_SHORT + ).show() + } binding.etChat.setText("") } } @@ -1666,228 +1409,6 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB } } - private fun spinRoulette(rouletteId: Long) { - viewModel.spinRoulette(roomId = roomId, rouletteId = rouletteId) { can, items, randomItem -> - val rouletteRawMessage = Gson().toJson( - LiveRoomChatRawMessage( - type = LiveRoomChatRawMessageType.ROULETTE_DONATION, - message = randomItem, - can = can, - donationMessage = "", - ) - ) - - RouletteSpinDialog( - activity = this@LiveRoomActivity, - items = items, - selectedItem = randomItem, - layoutInflater = layoutInflater - ) { - agora.sendRawMessageToGroup( - rawMessage = rouletteRawMessage.toByteArray(), - onSuccess = { - handler.post { - chatAdapter.items.add( - LiveRoomRouletteDonationChat( - profileUrl = SharedPreferenceManager.profileImage, - nickname = SharedPreferenceManager.nickname, - rouletteResult = randomItem - ) - ) - invalidateChat() - lifecycleScope.launch { viewModel.addDonationCan(can) } - } - }, - onFailure = { - viewModel.refundRouletteDonation(roomId) - } - ) - }.show() - } - } - - private fun joinChannel(roomInfo: GetRoomInfoResponse) { - loadingDialog.show(width = screenWidth, message = "라이브에 입장하고 있습니다.") - - val userId = SharedPreferenceManager.userId - agora.joinRtcChannel( - uid = userId.toInt(), - rtcToken = roomInfo.rtcToken, - channelName = roomInfo.channelName - ) - - agora.createRtmChannelAndLogin( - uid = userId.toString(), - rtmToken = roomInfo.rtmToken, - channelName = roomInfo.channelName, - rtmChannelListener = object : RtmChannelListener { - override fun onMemberCountUpdated(i: Int) { - Logger.e("onMemberCountUpdated: $i") - } - - override fun onAttributesUpdated(list: List?) {} - - @SuppressLint("NotifyDataSetChanged") - override fun onMessageReceived(message: RtmMessage, fromMember: RtmChannelMember) { - Logger.e("onMessageReceived - message: ${message.text}") - Logger.e("onMessageReceived - messageType: ${message.messageType}") - - val nickname = viewModel.getUserNickname(fromMember.userId!!.toInt()) - val profileUrl = viewModel.getUserProfileUrl(fromMember.userId!!.toInt()) - val rank = viewModel.getUserRank(fromMember.userId!!.toLong()) - - if (message.messageType == RtmMessageType.RAW) { - val rawMessage = Gson().fromJson( - String(message.rawMessage), - LiveRoomChatRawMessage::class.java - ) - - when (rawMessage.type) { - LiveRoomChatRawMessageType.EDIT_ROOM_INFO, - LiveRoomChatRawMessageType.SET_MANAGER -> { - handler.post { - viewModel.getRoomInfo(roomId) - } - } - - LiveRoomChatRawMessageType.DONATION -> { - handler.post { - chatAdapter.items.add( - LiveRoomDonationChat( - memberId = fromMember.userId.toLong(), - profileUrl, - nickname, - rawMessage.message, - rawMessage.can, - rawMessage.donationMessage ?: "" - ) - ) - invalidateChat() - lifecycleScope.launch { - viewModel.addDonationCan(rawMessage.can) - } - - if (rawMessage.signature != null) { - addSignature(rawMessage.signature) - } else if (rawMessage.signatureImageUrl != null) { - addSignatureImage(rawMessage.signatureImageUrl) - } - } - } - - LiveRoomChatRawMessageType.TOGGLE_ROULETTE -> { - activatingRouletteButton( - isActiveRoulette = rawMessage.isActiveRoulette ?: false - ) - } - - LiveRoomChatRawMessageType.ROULETTE_DONATION -> { - handler.post { - chatAdapter.items.add( - LiveRoomRouletteDonationChat( - profileUrl = profileUrl, - nickname = nickname, - rouletteResult = rawMessage.message - ) - ) - invalidateChat() - lifecycleScope.launch { - viewModel.addDonationCan(rawMessage.can) - } - } - } - - LiveRoomChatRawMessageType.HEART_DONATION -> { - handler.post { - addHeartMessage(nickname) - addHeartAnimation() - lifecycleScope.launch { viewModel.addHeartDonation() } - } - } - - else -> {} - } - } else { - val memberId = fromMember.userId.toLong() - if (viewModel.isNotBlockedMember(memberId)) { - val chat = message.text - - if (chat.isNotBlank()) { - handler.post { - chatAdapter.items.add( - LiveRoomNormalChat( - userId = memberId, - profileUrl = profileUrl, - nickname = nickname, - rank = rank, - chat = chat - ) - ) - invalidateChat() - } - } - } - } - } - - @SuppressLint("NotifyDataSetChanged") - override fun onMemberJoined(member: RtmChannelMember) { - Logger.e("onMemberJoined: ${member.userId}") - viewModel.getRoomInfo(roomId, member.userId.toInt()) { - if (it.isNotBlank() && isEntryMessageEnabled) { - chatAdapter.items.add(LiveRoomJoinChat(it)) - invalidateChat() - } - } - } - - override fun onMemberLeft(member: RtmChannelMember) { - Logger.e("onMemberLeft: ${member.userId}") - if (!viewModel.isEqualToHostId(member.userId.toInt())) { - viewModel.getRoomInfo(roomId) - } - } - }, - rtmChannelJoinSuccess = { - handler.post { - loadingDialog.dismiss() - } - - if (userId == roomInfo.creatorId) { - setBroadcaster() - } else { - setAudience() - } - - val intent = Intent(this, SodaLiveService::class.java) - intent.putExtra("roomId", roomId) - intent.putExtra("content", "라이브 진행중 - ${roomInfo.title}") - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - startForegroundService(intent) - } else { - startService(intent) - } - - if (containNoChatRoom()) { - startNoChatting() - } - setHeartButtonPosition() - startPeriodicPlaybackValidation() - }, - rtmChannelJoinFail = { - agoraConnectFail() - } - ) - } - - private fun agoraConnectFail() { - handler.post { - loadingDialog.dismiss() - showToast("라이브에 접속하지 못했습니다.\n다시 시도해 주세요.") - finish() - } - } - private fun setMuteSpeakerCreator(isMute: Boolean) { binding.ivMute.visibility = if (isMute) { View.VISIBLE @@ -1896,6 +1417,7 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB } } + // region agora private val rtcEventHandler = object : IRtcEngineEventHandler() { @SuppressLint("NotifyDataSetChanged") override fun onAudioVolumeIndication( @@ -1904,13 +1426,10 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB ) { super.onAudioVolumeIndication(speakers, totalVolume) - Logger.e("onAudioVolumeIndication - $speakers") - val activeSpeakerIds = speakers .filter { it.volume > 0 } .map { it.uid } - Logger.e("onAudioVolumeIndication - $activeSpeakerIds") handler.post { if (!activeSpeakerIds.contains(0)) { speakerListAdapter.activeSpeakers.clear() @@ -1988,25 +1507,17 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB } } - private val rtmClientListener = object : RtmClientListener { - override fun onConnectionStateChanged(state: Int, reason: Int) { - val text = - "Connection state changed to $state Reason: $reason".trimIndent() - - Logger.e(text) - } - - override fun onTokenExpired() {} - override fun onTokenPrivilegeWillExpire() {} - - override fun onPeersOnlineStatusChanged(map: Map?) {} - override fun onMessageReceived(rtmMessage: RtmMessage, peerId: String) { - Logger.e("text - ${rtmMessage.text}") - Logger.e("rawMessage - ${String(rtmMessage.rawMessage)}") - Logger.e("messageType - ${rtmMessage.messageType}") - if (rtmMessage.messageType == RtmMessageType.RAW) { - val rawMessage = String(rtmMessage.rawMessage) + private val rtmEventListener = object : RtmEventListener { + override fun onMessageEvent(event: MessageEvent) { + val memberId = event.publisherId + val nickname = viewModel.getUserNickname(memberId.toInt()) + val profileUrl = viewModel.getUserProfileUrl(memberId.toInt()) + val rank = viewModel.getUserRank(memberId.toLong()) + val message = event.message + if (message.type == RtmConstants.RtmMessageType.BINARY) { + val rawMessage = String(message.data as ByteArray) + Logger.e("onMessageEvent - rawMessage - $rawMessage") if (rawMessage == LiveRoomRequestType.CHANGE_LISTENER.toString()) { handler.post { viewModel.setListener( @@ -2019,7 +1530,7 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB if (roomUserProfileDialog.isShowing()) { viewModel.getUserProfile( roomId = roomId, - userId = peerId.toLong() + userId = memberId.toLong() ) {} } } @@ -2113,32 +1624,279 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB LiveRoomChatRawMessage::class.java ) - if (message.type == LiveRoomChatRawMessageType.SECRET_DONATION) { - val nickname = viewModel.getUserNickname(peerId.toInt()) - val profileUrl = viewModel.getUserProfileUrl(peerId.toInt()) + when (message.type) { + LiveRoomChatRawMessageType.EDIT_ROOM_INFO, + LiveRoomChatRawMessageType.SET_MANAGER -> { + handler.post { + viewModel.getRoomInfo(roomId) + } + } - handler.post { - chatAdapter.items.add( - LiveRoomDonationChat( - memberId = peerId.toLong(), - profileUrl, - nickname, - message.message, - message.can, - message.donationMessage ?: "" + LiveRoomChatRawMessageType.DONATION -> { + handler.post { + chatAdapter.items.add( + LiveRoomDonationChat( + memberId = memberId.toLong(), + profileUrl, + nickname, + message.message, + message.can, + message.donationMessage ?: "" + ) ) - ) - invalidateChat() + invalidateChat() + lifecycleScope.launch { + viewModel.addDonationCan(message.can) + } - if (message.signature != null) { - addSignature(message.signature) - } else if (message.signatureImageUrl != null) { - addSignatureImage(message.signatureImageUrl) + if (message.signature != null) { + addSignature(message.signature) + } else if (message.signatureImageUrl != null) { + addSignatureImage(message.signatureImageUrl) + } + } + } + + LiveRoomChatRawMessageType.TOGGLE_ROULETTE -> { + activatingRouletteButton( + isActiveRoulette = message.isActiveRoulette ?: false + ) + } + + LiveRoomChatRawMessageType.ROULETTE_DONATION -> { + handler.post { + chatAdapter.items.add( + LiveRoomRouletteDonationChat( + profileUrl = profileUrl, + nickname = nickname, + rouletteResult = message.message + ) + ) + invalidateChat() + lifecycleScope.launch { + viewModel.addDonationCan(message.can) + } + } + } + + LiveRoomChatRawMessageType.HEART_DONATION -> { + handler.post { + addHeartMessage(nickname) + addHeartAnimation() + lifecycleScope.launch { viewModel.addHeartDonation() } + } + } + + LiveRoomChatRawMessageType.SECRET_DONATION -> { + val nickname = viewModel.getUserNickname(memberId.toInt()) + val profileUrl = viewModel.getUserProfileUrl(memberId.toInt()) + + handler.post { + chatAdapter.items.add( + LiveRoomDonationChat( + memberId = memberId.toLong(), + profileUrl, + nickname, + message.message, + message.can, + message.donationMessage ?: "" + ) + ) + invalidateChat() + + if (message.signature != null) { + addSignature(message.signature) + } else if (message.signatureImageUrl != null) { + addSignatureImage(message.signatureImageUrl) + } + } + } + } + } else if (message.type == RtmConstants.RtmMessageType.STRING) { + val chat = message.data.toString() + if (viewModel.isNotBlockedMember(memberId.toLong())) { + if (chat.isNotBlank()) { + handler.post { + chatAdapter.items.add( + LiveRoomNormalChat( + userId = memberId.toLong(), + profileUrl = profileUrl, + nickname = nickname, + rank = rank, + chat = chat + ) + ) + invalidateChat() } } } } } + + override fun onPresenceEvent(event: PresenceEvent) { + Logger.v("onPresenceEvent - $event") + val memberId = event.publisherId + val eventType = event.eventType + if (eventType == RtmConstants.RtmPresenceEventType.REMOTE_JOIN) { + Logger.e("onMemberJoined: $memberId") + speakerListAdapter.muteSpeakers.remove(memberId.toInt()) + viewModel.getRoomInfo(roomId, memberId.toInt()) { + if (it.isNotBlank() && isEntryMessageEnabled) { + chatAdapter.items.add(LiveRoomJoinChat(it)) + invalidateChat() + } + } + } else if (eventType == RtmConstants.RtmPresenceEventType.REMOTE_LEAVE) { + if (!viewModel.isEqualToHostId(memberId.toInt())) { + viewModel.getRoomInfo(roomId) + } + } + } + + override fun onLinkStateEvent(event: LinkStateEvent) { + Logger.v("onLinkStateEvent - $event") + } + } + + private fun initAgora() { + agora = Agora( + uid = SharedPreferenceManager.userId, + context = this, + rtcEventHandler = rtcEventHandler, + rtmEventListener = rtmEventListener + ) + } + + private fun joinChannel(roomInfo: GetRoomInfoResponse) { + loadingDialog.show(width = screenWidth, message = "라이브에 입장하고 있습니다.") + + val userId = SharedPreferenceManager.userId + agora.joinRtcChannel( + uid = userId.toInt(), + rtcToken = roomInfo.rtcToken, + channelName = roomInfo.channelName + ) + + agora.rtmLogin( + rtmToken = roomInfo.rtmToken, + channelName = roomInfo.channelName, + rtmChannelJoinSuccess = { + handler.post { + loadingDialog.dismiss() + } + + if (userId == roomInfo.creatorId) { + setBroadcaster() + } else { + setAudience() + } + + val intent = Intent(this, SodaLiveService::class.java) + intent.putExtra("roomId", roomId) + intent.putExtra("content", "라이브 진행중 - ${roomInfo.title}") + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + startForegroundService(intent) + } else { + startService(intent) + } + + if (containNoChatRoom()) { + startNoChatting() + } + setHeartButtonPosition() + startPeriodicPlaybackValidation() + }, + rtmChannelJoinFail = { + agoraConnectFail() + } + ) + } + + private fun agoraConnectFail() { + handler.post { + loadingDialog.dismiss() + showToast("라이브에 접속하지 못했습니다.\n다시 시도해 주세요.") + finish() + } + } + // endregion + + // region heart + private val heartNicknameList = mutableListOf() + private var heartNickname = "" + set(value) { + field = value + + if (field.isNotBlank()) { + showHeartMessage() + handler.postDelayed({ + if (heartNicknameList.isNotEmpty()) { + heartNickname = heartNicknameList.removeAt(0) + } else { + hideHeartMessage() + } + }, 1500) + } + } + + private fun initLikeHeartButton() { + if (!isHost) { + binding.flLikeHeart.visibility = View.VISIBLE + binding.flLikeHeart.setOnClickListener { + if (isAvailableLikeHeart) { + binding.flLikeHeart.isEnabled = false + viewModel.likeHeart( + roomId = roomId, + onSuccess = { + val donationRawMessage = Gson().toJson( + LiveRoomChatRawMessage( + type = LiveRoomChatRawMessageType.HEART_DONATION, + message = "", + can = 1, + signature = null, + signatureImageUrl = null, + donationMessage = null + ) + ) + + agora.sendRawMessageToGroup( + rawMessage = donationRawMessage.toByteArray(), + onSuccess = { + val nickname = viewModel.getUserNickname( + SharedPreferenceManager.userId.toInt() + ) + handler.post { + addHeartMessage(nickname) + addHeartAnimation() + lifecycleScope.launch { viewModel.addHeartDonation() } + } + }, + onFailure = { + viewModel.refundDonation(roomId) + } + ) + + binding.flLikeHeart.isEnabled = true + }, + onFailure = { + binding.flLikeHeart.isEnabled = true + } + ) + } else { + SodaDialog( + activity = this@LiveRoomActivity, + layoutInflater = layoutInflater, + title = "안내", + desc = "'좋아해요'는 유료 후원입니다.\n" + + "클릭시 1캔이 소진됩니다.", + confirmButtonTitle = "확인", + confirmButtonClick = { isAvailableLikeHeart = true } + ).show(screenWidth) + } + } + } else { + binding.flLikeHeart.visibility = View.GONE + } } private fun addHeartMessage(nickname: String) { @@ -2181,6 +1939,139 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB binding.tvHeartMessage.visibility = View.GONE } + private fun setHeartButtonPosition() { + handler.postDelayed( + { + // 버튼의 위치 + val button = if (isHost) { + binding.flRouletteSettings + } else { + binding.flLikeHeart + } + buttonPosition = IntArray(2) + button.getLocationInWindow(buttonPosition) + }, + 500 + ) + } + + private fun addHeartAnimation() { + val button = if (isHost) { + binding.flRouletteSettings + } else { + binding.flLikeHeart + } + + // 하트 이미지뷰 생성 + val heart = ImageView(this).apply { + setImageResource(R.drawable.ic_heart_pink) // 투명한 하트 이미지 + layoutParams = FrameLayout.LayoutParams( + 33.3f.dpToPx().toInt(), + 33.3f.dpToPx().toInt() + ) // 하트 크기 설정 + } + + // 하트의 초기 위치를 버튼의 위치로 설정 + heart.x = (buttonPosition[0] + button.width / 2f - 50) // X축 (가운데 정렬) + heart.y = (buttonPosition[1] - 100).toFloat() // Y축 (버튼 바로 위에 생성) + + binding.flRoot.addView(heart) + + // 하트가 위로 올라가는 애니메이션 + val animateDuration = secondToMillis(2.5f) + val moveUpAnimator = ObjectAnimator.ofFloat( + heart, + "translationY", + heart.y, + heart.y - (screenHeight * 1000 / 2337) + ).apply { + duration = animateDuration + interpolator = AccelerateDecelerateInterpolator() + } + + val isNegativeFirst = Random.nextBoolean() + + // 좌우 이동 범위를 랜덤으로 설정 + val x = 13.3f + val startX = if (isNegativeFirst) (0 - x).dpToPx() else x.dpToPx() + val endX = if (isNegativeFirst) x.dpToPx() else (0 - x).dpToPx() + + val moveAnimator = ObjectAnimator.ofFloat( + heart, + "translationX", + heart.x + startX, + heart.x + endX + ).apply { + duration = secondToMillis(1.5f) + repeatCount = ObjectAnimator.INFINITE + repeatMode = ObjectAnimator.REVERSE + interpolator = AccelerateDecelerateInterpolator() + } + + val scaleXAnimator = ObjectAnimator.ofFloat( + heart, + "scaleX", + 0.5f, + 1.0f + ).apply { + duration = animateDuration + } + + val scaleYAnimator = ObjectAnimator.ofFloat( + heart, + "scaleY", + 0.5f, + 1.0f + ).apply { + duration = animateDuration + } + + // 하트 투명도 애니메이션 + val fadeOutAnimator = ObjectAnimator.ofFloat( + heart, + "alpha", + 1f, + 0f + ).apply { + duration = animateDuration + } + + // 애니메이션 실행 + moveUpAnimator.start() + moveAnimator.start() + fadeOutAnimator.start() + scaleXAnimator.start() + scaleYAnimator.start() + + // 애니메이션이 끝나면 하트 제거 + handler.postDelayed({ + binding.flRoot.removeView(heart) + }, animateDuration) + } + // endregion + + // region signature + private var isShowSignatureImage = false + private val signatureImageUrlList = mutableListOf() + private var signatureImageUrl = "" + set(value) { + field = value + + if (field.isNotBlank()) { + showSignatureImage() + } + } + + private val signatureList = mutableListOf() + private var signature: LiveRoomDonationResponse? = null + set(value) { + field = value + + if (field != null) { + showSignatureImage() + } + } + private fun addSignatureImage(imageUrl: String) { if (imageUrl.isNotBlank()) { if (!isShowSignatureImage) { @@ -2287,7 +2178,118 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB .into(binding.ivSignature) } } + // endregion + // region roulette + private val rouletteConfigResult = registerForActivityResult( + ActivityResultContracts.StartActivityForResult() + ) { result -> + val resultCode = result.resultCode + val isActiveRoulette = result.data?.getBooleanExtra(Constants.EXTRA_RESULT_ROULETTE, false) + + if (resultCode == RESULT_OK && isActiveRoulette != null) { + agora.sendRawMessageToGroup( + rawMessage = Gson().toJson( + LiveRoomChatRawMessage( + type = LiveRoomChatRawMessageType.TOGGLE_ROULETTE, + message = "", + can = 0, + donationMessage = "", + isActiveRoulette = isActiveRoulette + ) + ).toByteArray() + ) + } + } + + private fun initRouletteSettingButton() { + if (isHost) { + binding.flRouletteSettings.visibility = View.VISIBLE + binding.flRouletteSettings.setOnClickListener { + hideKeyboard { + rouletteConfigResult.launch( + Intent( + applicationContext, + RouletteConfigActivity::class.java + ) + ) + } + } + } else { + binding.flRouletteSettings.visibility = View.GONE + } + } + + private fun activatingRouletteButton(isActiveRoulette: Boolean) { + handler.postDelayed( + { + if (!isHost && isActiveRoulette) { + binding.flRoulette.visibility = View.VISIBLE + binding.flRoulette.setOnClickListener { + hideKeyboard { + viewModel.showRoulette { + RoulettePreviewDialog( + activity = this, + previewList = it, + onClickSpin = { rouletteId -> + spinRoulette(rouletteId = rouletteId) + }, + layoutInflater = layoutInflater + ).show() + } + } + } + + } else { + binding.flRoulette.visibility = View.GONE + } + }, + 500 + ) + } + + private fun spinRoulette(rouletteId: Long) { + viewModel.spinRoulette(roomId = roomId, rouletteId = rouletteId) { can, items, randomItem -> + val rouletteRawMessage = Gson().toJson( + LiveRoomChatRawMessage( + type = LiveRoomChatRawMessageType.ROULETTE_DONATION, + message = randomItem, + can = can, + donationMessage = "", + ) + ) + + RouletteSpinDialog( + activity = this@LiveRoomActivity, + items = items, + selectedItem = randomItem, + layoutInflater = layoutInflater + ) { + agora.sendRawMessageToGroup( + rawMessage = rouletteRawMessage.toByteArray(), + onSuccess = { + handler.post { + chatAdapter.items.add( + LiveRoomRouletteDonationChat( + profileUrl = SharedPreferenceManager.profileImage, + nickname = SharedPreferenceManager.nickname, + rouletteResult = randomItem + ) + ) + invalidateChat() + lifecycleScope.launch { viewModel.addDonationCan(can) } + } + }, + onFailure = { + viewModel.refundRouletteDonation(roomId) + } + ) + }.show() + } + } + // endregion + + // region tracking private fun startPeriodicPlaybackValidation() { val period = 30L compositeDisposable.add( @@ -2306,6 +2308,7 @@ class LiveRoomActivity : BaseActivity(ActivityLiveRoomB ) ) } + // endregion companion object { private const val NO_CHATTING_TIME = 180L diff --git a/app/src/main/res/layout/dialog_live_no_chatting.xml b/app/src/main/res/layout/dialog_live_no_chatting.xml index 18e6c79b..7ed4c983 100644 --- a/app/src/main/res/layout/dialog_live_no_chatting.xml +++ b/app/src/main/res/layout/dialog_live_no_chatting.xml @@ -86,7 +86,7 @@ android:gravity="center" android:paddingVertical="16dp" android:text="취소" - android:textColor="@color/color_9970ff" + android:textColor="@color/color_3bb9f1" android:textSize="18.3sp" />