From ee2f5572fc503b64b2d5c4ae53d8c987a176e7ce Mon Sep 17 00:00:00 2001 From: klaus Date: Sat, 27 Jun 2026 01:10:27 +0900 Subject: [PATCH] =?UTF-8?q?feat(live):=20=EC=98=A8=EC=97=90=EC=96=B4=20?= =?UTF-8?q?=EB=9D=BC=EC=9D=B4=EB=B8=8C=20=ED=99=94=EB=A9=B4=EC=9D=84=20?= =?UTF-8?q?=EC=97=B0=EA=B2=B0=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 1 + .../v2/live/onair/HomeOnAirLiveActivity.kt | 311 ++++++++++++++++++ 2 files changed, 312 insertions(+) create mode 100644 app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveActivity.kt diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3596825c..a62d2966 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -113,6 +113,7 @@ + diff --git a/app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveActivity.kt b/app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveActivity.kt new file mode 100644 index 00000000..dd2e2f34 --- /dev/null +++ b/app/src/main/java/kr/co/vividnext/sodalive/v2/live/onair/HomeOnAirLiveActivity.kt @@ -0,0 +1,311 @@ +package kr.co.vividnext.sodalive.v2.live.onair + +import android.content.Context +import android.content.Intent +import android.os.Bundle +import android.view.Gravity +import android.widget.Toast +import androidx.core.view.isVisible +import androidx.media3.common.util.UnstableApi +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import com.google.gson.Gson +import kr.co.vividnext.sodalive.R +import kr.co.vividnext.sodalive.audio_content.AudioContentPlayService +import kr.co.vividnext.sodalive.audio_content.player.AudioContentPlayerService +import kr.co.vividnext.sodalive.base.BaseActivity +import kr.co.vividnext.sodalive.base.SodaDialog +import kr.co.vividnext.sodalive.common.Constants +import kr.co.vividnext.sodalive.common.LoadingDialog +import kr.co.vividnext.sodalive.common.SharedPreferenceManager +import kr.co.vividnext.sodalive.common.ToastMessage +import kr.co.vividnext.sodalive.databinding.ActivityHomeOnAirLiveBinding +import kr.co.vividnext.sodalive.live.LiveViewModel +import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailResponse +import kr.co.vividnext.sodalive.live.room.LiveRoomActivity +import kr.co.vividnext.sodalive.live.room.dialog.LivePaymentDialog +import kr.co.vividnext.sodalive.live.room.dialog.LiveRoomPasswordDialog +import kr.co.vividnext.sodalive.mypage.MyPageViewModel +import kr.co.vividnext.sodalive.mypage.auth.Auth +import kr.co.vividnext.sodalive.mypage.auth.AuthVerifyRequest +import kr.co.vividnext.sodalive.mypage.auth.BootpayResponse +import kr.co.vividnext.sodalive.settings.ContentSettingsActivity +import kr.co.vividnext.sodalive.settings.language.LanguageManager +import kr.co.vividnext.sodalive.settings.language.LocaleHelper +import kr.co.vividnext.sodalive.splash.SplashActivity +import kr.co.vividnext.sodalive.user.login.LoginActivity +import kr.co.vividnext.sodalive.v2.live.onair.model.HomeOnAirLivePageUiState +import kr.co.vividnext.sodalive.v2.live.onair.model.canEnterHomeOnAirLiveRoom +import kr.co.vividnext.sodalive.v2.live.onair.ui.HomeOnAirLiveAdapter +import org.koin.android.ext.android.inject +import org.koin.androidx.viewmodel.ext.android.viewModel +import java.text.SimpleDateFormat +import java.util.Date +import java.util.Locale +import java.util.TimeZone + +@UnstableApi +class HomeOnAirLiveActivity : BaseActivity( + ActivityHomeOnAirLiveBinding::inflate +) { + private val viewModel: HomeOnAirLiveViewModel by viewModel() + private val liveViewModel: LiveViewModel by inject() + private val myPageViewModel: MyPageViewModel by inject() + private val loadingDialog: LoadingDialog by lazy { LoadingDialog(this, layoutInflater) } + private val adapter = HomeOnAirLiveAdapter { enterLiveRoom(it.roomId) } + private var isPageLoading = false + private var isLiveEntryLoading = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + bindData() + viewModel.loadFirstPage() + } + + override fun setupView() { + binding.toolbar.tvBack.setText(R.string.live_now) + binding.toolbar.tvBack.setOnClickListener { finish() } + binding.rvHomeOnAirLive.apply { + layoutManager = LinearLayoutManager(this@HomeOnAirLiveActivity) + adapter = this@HomeOnAirLiveActivity.adapter + addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + super.onScrolled(recyclerView, dx, dy) + if (dy > 0 && !recyclerView.canScrollVertically(1)) { + viewModel.loadNextPage() + } + } + }) + } + } + + private fun bindData() { + viewModel.onAirLiveStateLiveData.observe(this) { state -> + when (state) { + HomeOnAirLivePageUiState.Loading -> Unit + HomeOnAirLivePageUiState.Empty -> showEmpty() + is HomeOnAirLivePageUiState.Error -> showEmpty() + is HomeOnAirLivePageUiState.Content -> { + binding.rvHomeOnAirLive.isVisible = true + binding.tvHomeOnAirLiveEmpty.isVisible = false + adapter.submitItems(state.content.items) + state.content.paginationErrorMessage?.let { message -> + Toast.makeText(applicationContext, message, Toast.LENGTH_LONG).show() + viewModel.consumePaginationErrorMessage() + } + } + } + } + viewModel.isLoading.observe(this) { isLoading -> + isPageLoading = isLoading + updateLoadingDialog() + } + viewModel.toastLiveData.observe(this) { toastMessage -> + toastMessage?.let(::showToast) + } + liveViewModel.isLoading.observe(this) { isLoading -> + isLiveEntryLoading = isLoading + updateLoadingDialog() + } + liveViewModel.toastLiveData.observe(this) { message -> + message?.let { Toast.makeText(applicationContext, it, Toast.LENGTH_LONG).show() } + } + } + + private fun showEmpty() { + binding.rvHomeOnAirLive.isVisible = false + binding.tvHomeOnAirLiveEmpty.isVisible = true + adapter.submitItems(emptyList()) + } + + private fun enterLiveRoom(roomId: Long) { + ensureLoginAndAdultAuth(isAdult = false) { + liveViewModel.getRoomDetail(roomId) { roomDetail -> + if (!canEnterHomeOnAirLiveRoom(roomDetail)) { + Toast.makeText(applicationContext, R.string.common_error_unknown, Toast.LENGTH_LONG).show() + return@getRoomDetail + } + + ensureLoginAndAdultAuth(isAdult = roomDetail.isAdult) { + enterLiveRoom(roomId, roomDetail) + } + } + } + } + + private fun enterLiveRoom(roomId: Long, roomDetail: GetRoomDetailResponse) { + startService( + Intent(applicationContext, AudioContentPlayService::class.java).apply { + action = AudioContentPlayService.MusicAction.STOP.name + } + ) + startService( + Intent(applicationContext, AudioContentPlayerService::class.java).apply { + action = "STOP_SERVICE" + } + ) + + val onEnterRoomSuccess = { + runOnUiThread { + startActivity( + Intent(applicationContext, LiveRoomActivity::class.java).apply { + putExtra(Constants.EXTRA_ROOM_ID, roomId) + } + ) + } + } + + if (roomDetail.manager.id == SharedPreferenceManager.userId) { + liveViewModel.enterRoom(roomId, onEnterRoomSuccess) + } else if (roomDetail.price == 0 || roomDetail.isPaid) { + if (roomDetail.isPrivateRoom) { + showPasswordDialog(roomId, can = 0, onEnterRoomSuccess = onEnterRoomSuccess) + } else { + liveViewModel.enterRoom(roomId, onEnterRoomSuccess) + } + } else { + showPaidLiveEntryDialog( + roomId = roomId, + beginDateTimeUtc = roomDetail.beginDateTimeUtc, + price = roomDetail.price, + isPrivateRoom = roomDetail.isPrivateRoom, + onEnterRoomSuccess = onEnterRoomSuccess + ) + } + } + + private fun ensureLoginAndAdultAuth(isAdult: Boolean, onAuthed: () -> Unit) { + if (SharedPreferenceManager.token.isBlank()) { + showLoginActivity() + return + } + + if (isAdult) { + val isKoreanCountry = SharedPreferenceManager.countryCode.ifBlank { "KR" } == "KR" + if (isKoreanCountry && !SharedPreferenceManager.isAuth) { + SodaDialog( + activity = this, + layoutInflater = layoutInflater, + title = getString(R.string.auth_title), + desc = getString(R.string.auth_desc_live), + confirmButtonTitle = getString(R.string.auth_go), + confirmButtonClick = { startAuthFlow() }, + cancelButtonTitle = getString(R.string.cancel), + cancelButtonClick = {}, + descGravity = Gravity.CENTER + ).show(screenWidth) + return + } + + if (!SharedPreferenceManager.isAdultContentVisible) { + startActivity( + Intent(applicationContext, ContentSettingsActivity::class.java).apply { + putExtra(Constants.EXTRA_SHOW_SENSITIVE_CONTENT_GUIDE, true) + } + ) + return + } + } + + onAuthed() + } + + private fun showLoginActivity() { + if (SharedPreferenceManager.token.isBlank()) { + startActivity( + Intent(applicationContext, LoginActivity::class.java).apply { + putExtra(Constants.EXTRA_DATA, intent.extras) + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP) + } + ) + } + } + + private fun startAuthFlow() { + Auth.auth(this, this) { json -> + val bootpayResponse = Gson().fromJson(json, BootpayResponse::class.java) + val request = AuthVerifyRequest(receiptId = bootpayResponse.data.receiptId) + runOnUiThread { + myPageViewModel.authVerify(request) { + startActivity( + Intent(applicationContext, SplashActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TASK or Intent.FLAG_ACTIVITY_NEW_TASK) + } + ) + finish() + } + } + } + } + + private fun showPasswordDialog(roomId: Long, can: Int, onEnterRoomSuccess: () -> Unit) { + LiveRoomPasswordDialog( + activity = this, + layoutInflater = layoutInflater, + can = can, + confirmButtonClick = { password -> + liveViewModel.enterRoom( + roomId = roomId, + onSuccess = onEnterRoomSuccess, + password = password + ) + } + ).show(screenWidth) + } + + private fun showPaidLiveEntryDialog( + roomId: Long, + beginDateTimeUtc: String, + price: Int, + isPrivateRoom: Boolean, + onEnterRoomSuccess: () -> Unit + ) { + if (isPrivateRoom) { + showPasswordDialog(roomId, can = price, onEnterRoomSuccess = onEnterRoomSuccess) + return + } + + val locale = Locale(LanguageManager.getEffectiveLanguage(this)) + val wrappedContext = LocaleHelper.wrap(this) + val beginDate = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.ENGLISH).apply { + timeZone = TimeZone.getTimeZone("UTC") + }.parse(beginDateTimeUtc) ?: return + val now = Date() + val dateFormat = SimpleDateFormat("yyyy-MM-dd, HH:mm", locale) + val diffTime = now.time - beginDate.time + val hours = (diffTime / (1000 * 60 * 60)).toInt() + val mins = (diffTime / (1000 * 60)).toInt() % 60 + + LivePaymentDialog( + activity = this, + layoutInflater = layoutInflater, + title = wrappedContext.getString(R.string.live_paid_title), + startDateTime = if (hours >= 1) dateFormat.format(beginDate) else null, + nowDateTime = if (hours >= 1) dateFormat.format(now) else null, + desc = wrappedContext.getString(R.string.live_paid_desc, price), + desc2 = if (hours >= 1) wrappedContext.getString(R.string.live_paid_warning, hours, mins) else null, + confirmButtonTitle = wrappedContext.getString(R.string.live_paid_confirm), + confirmButtonClick = { liveViewModel.enterRoom(roomId, onEnterRoomSuccess) }, + cancelButtonTitle = wrappedContext.getString(R.string.cancel), + cancelButtonClick = {} + ).show(screenWidth) + } + + private fun showToast(toastMessage: ToastMessage) { + toastMessage.message?.let { message -> showToast(message) } + ?: toastMessage.resId?.let { resId -> showToast(getString(resId)) } + } + + private fun updateLoadingDialog() { + if (isPageLoading || isLiveEntryLoading) { + loadingDialog.show(screenWidth) + } else { + loadingDialog.dismiss() + } + } + + companion object { + fun newIntent(context: Context): Intent = Intent(context, HomeOnAirLiveActivity::class.java) + } +}