package com.pukka.mediaapp.ui import android.app.AlertDialog import android.content.Intent import android.os.Bundle import android.os.CountDownTimer import android.util.Log import android.view.LayoutInflater import android.view.View import android.widget.Toast import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope import androidx.lifecycle.repeatOnLifecycle import com.bumptech.glide.Glide import com.pukka.mediaapp.databinding.ActivityDetailBinding import com.pukka.mediaapp.databinding.DialogPaymentBinding import com.pukka.mediaapp.model.ContentItem import com.pukka.mediaapp.network.SignManager import com.pukka.mediaapp.sdk.JetsenxSdkUtil import com.pukka.mediaapp.ui.adapter.AppColors import com.pukka.mediaapp.utils.QRCodeUtil import com.pukka.mediaapp.utils.FileLogger import com.pukka.mediaapp.viewmodel.DetailViewModel import com.pukka.mediaapp.viewmodel.PaymentState import com.pukka.mediaapp.viewmodel.PaymentViewModel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import java.text.SimpleDateFormat import java.util.Locale /** * 详情页:海报、标题、评分、演员、简介 + 播放(含微信扫码支付流程)+ 返回。 * * 完整播放流程(改造后): * 1. 点击播放 → 采集设备标识 → 检查天卡播放权限 * 2. 有权限 → 调用SDK获取视频地址 → 跳转播放器 * 3. 无权限 → 创建订单 → 展示微信支付二维码 → 轮询支付状态 * 4. 支付成功 → 获取权益 → 初始化SDK → 获取视频地址 → 跳转播放器 * 5. 超时/关闭 → 展示重新生成按钮 */ class DetailActivity : AppCompatActivity() { private lateinit var binding: ActivityDetailBinding private val viewModel: DetailViewModel by viewModels() private val paymentViewModel: PaymentViewModel by viewModels() private var paymentDialog: AlertDialog? = null private var dialogBinding: DialogPaymentBinding? = null private var contentItem: ContentItem? = null private var sdkInitialized = false private var isFetchingVideoUrl = false // 防止重复跳转播放器 private var currentMediaId: String = "" private var countDownTimer: CountDownTimer? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityDetailBinding.inflate(layoutInflater) setContentView(binding.root) val contentcode = intent.getStringExtra(EXTRA_CONTENTCODE).orEmpty() val name = intent.getStringExtra(EXTRA_NAME).orEmpty() currentMediaId = contentcode binding.tvTitle.text = name applyTextColors() binding.btnBack.setOnClickListener { finish() } binding.btnPlay.setOnClickListener { Log.d("DetailActivity", "btnPlay clicked! contentcode=$contentcode") isFetchingVideoUrl = false // 重置标志 startPaymentFlow() } observeViewModel() observePaymentState() viewModel.loadDetail(contentcode) } /** 开始鉴权/支付流程(sign 由 SignManager 管理,无需 App 传设备标识) */ private fun startPaymentFlow() { // 取消之前的倒计时 countDownTimer?.cancel() countDownTimer = null paymentViewModel.startPaymentFlow() } private fun applyTextColors() { binding.tvTitle.setTextColor(AppColors.TEXT_PRIMARY) binding.tvActor.setTextColor(AppColors.TEXT_SECONDARY) binding.tvIntro.setTextColor(AppColors.TEXT_TERTIARY) } /** 时长格式化:秒 -> "104分钟" 或 "1小时44分钟" */ private fun formatDuration(duration: String?): String? { if (duration.isNullOrEmpty()) return null val seconds = duration.toLongOrNull() ?: return null if (seconds <= 0) return null val totalMinutes = seconds / 60 if (totalMinutes < 60) return "${totalMinutes}分钟" val hours = totalMinutes / 60 val mins = totalMinutes % 60 return if (mins > 0) "${hours}小时${mins}分钟" else "${hours}小时" } private fun observeViewModel() { lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { viewModel.detail.collect { item -> if (item == null) return@collect contentItem = item binding.tvTitle.text = item.name // 元信息行:分类 · 地区 · 年份 · 时长 val metaParts = mutableListOf() item.firstproject?.takeIf { it.isNotEmpty() }?.let { metaParts.add(it) } item.secondproject?.takeIf { it.isNotEmpty() }?.let { metaParts.add(it) } item.originalcountry?.takeIf { it.isNotEmpty() }?.let { metaParts.add(it) } item.releaseyear?.takeIf { it.isNotEmpty() }?.let { metaParts.add(it) } formatDuration(item.duration)?.let { metaParts.add(it) } binding.tvType.text = metaParts.joinToString(" · ") // 演员:| 分割取前4个 val actors = item.actordisplay?.split("|")?.map { it.trim() }?.filter { it.isNotEmpty() } binding.tvActor.text = if (!actors.isNullOrEmpty()) "主演: ${actors.take(4).joinToString(" ")}" else "" binding.tvIntro.text = item.getIntroText() // 横幅背景图 val backdrop = item.thumbimg?.takeIf { it.isNotEmpty() } ?: item.midimg?.takeIf { it.isNotEmpty() } ?: item.image?.takeIf { it.isNotEmpty() } if (!backdrop.isNullOrEmpty()) { Glide.with(this@DetailActivity).load(backdrop).into(binding.ivBackdrop) } // 海报 val poster = item.posterimg?.takeIf { it.isNotEmpty() } ?: item.getPosterUrl() Glide.with(this@DetailActivity).load(poster).into(binding.ivPoster) } } } } private fun observePaymentState() { lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { paymentViewModel.state.collect { state -> FileLogger.log("DetailActivity", "collect state=${state::class.simpleName}") when (state) { is PaymentState.Idle -> {} is PaymentState.CheckingPermission -> { // 静默鉴权:不弹窗、不提示,直接走后续播放 } is PaymentState.HasPermission -> { if (isFetchingVideoUrl) return@collect isFetchingVideoUrl = true dismissPaymentDialog() playWithThirdCardNo(state.thirdCardNo) } is PaymentState.CreatingOrder -> { showPaymentDialog("正在创建天卡订单...") } is PaymentState.WaitingForPayment -> { // 展示二维码 showQrCode( codeUrl = state.codeUrl, amount = state.amount, orderNo = state.orderNo, expireAt = state.expireAt ) } is PaymentState.PaymentSuccess -> { if (isFetchingVideoUrl) return@collect isFetchingVideoUrl = true countDownTimer?.cancel() dismissPaymentDialog() playWithThirdCardNo(state.thirdCardNo) } is PaymentState.PaymentTimeout -> { countDownTimer?.cancel() showPaymentTimeout() } is PaymentState.Error -> { countDownTimer?.cancel() isFetchingVideoUrl = false FileLogger.log("DetailActivity", "Error: msg=${state.message} dialogShowing=${paymentDialog?.isShowing}") if (paymentDialog?.isShowing != true) showPaymentDialog("") updatePaymentDialog( errorText = "失败: ${state.message}", showCloseButton = true ) } } } } } } /** * 展示微信支付二维码 */ private fun showQrCode(codeUrl: String, amount: Int, orderNo: String, expireAt: String) { val db = dialogBinding ?: showPaymentDialog("").let { dialogBinding } ?: return // 隐藏加载状态 db.progressBar.visibility = View.GONE db.pbQrcodeLoading.visibility = View.VISIBLE db.flQrcode.visibility = View.VISIBLE db.ivQrcode.visibility = View.GONE // 金额显示(分转元) db.tvPayAmount.visibility = View.VISIBLE db.tvPayAmount.text = "¥${"%.2f".format(amount / 100.0)}" // 订单号 db.tvPayOrderNo.visibility = View.VISIBLE db.tvPayOrderNo.text = "订单号: $orderNo" // 状态文本 db.tvPayStatus.text = "请使用微信扫码支付" // 按钮:显示取消(用户可随时取消支付) db.llButtons.visibility = View.GONE db.btnClose.visibility = View.VISIBLE db.btnClose.text = "取消支付" db.btnRegenerate.visibility = View.GONE // 倒计时 db.tvCountdown.visibility = View.VISIBLE startCountdown(expireAt) // 异步生成二维码 lifecycleScope.launch(Dispatchers.IO) { try { val bitmap = QRCodeUtil.generateQRCode(codeUrl, 400) withContext(Dispatchers.Main) { val currentBinding = dialogBinding ?: return@withContext currentBinding.pbQrcodeLoading.visibility = View.GONE currentBinding.ivQrcode.setImageBitmap(bitmap) currentBinding.ivQrcode.visibility = View.VISIBLE } } catch (e: Exception) { Log.e("DetailActivity", "QR code generation failed", e) withContext(Dispatchers.Main) { updatePaymentDialog( errorText = "二维码生成失败,请重试", showCloseButton = true ) } } } } /** * 启动倒计时 */ private fun startCountdown(expireAt: String) { countDownTimer?.cancel() val remainMillis = calculateRemainMillis(expireAt) if (remainMillis <= 0) { paymentViewModel.cancelPolling() showPaymentTimeout() return } countDownTimer = object : CountDownTimer(remainMillis, 1000) { override fun onTick(millisUntilFinished: Long) { val db = dialogBinding ?: return val minutes = millisUntilFinished / 60000 val seconds = (millisUntilFinished % 60000) / 1000 db.tvCountdown.text = String.format("%02d:%02d", minutes, seconds) } override fun onFinish() { val db = dialogBinding ?: return db.tvCountdown.text = "00:00" paymentViewModel.cancelPolling() showPaymentTimeout() } }.start() } /** * 解析 expireAt 计算剩余时间(毫秒) */ private fun calculateRemainMillis(expireAt: String): Long { if (expireAt.isEmpty()) { // 默认 15 分钟 return 15 * 60 * 1000L } // 尝试多种格式解析 val formats = listOf( "yyyy-MM-dd'T'HH:mm:ssXXX", "yyyy-MM-dd'T'HH:mm:ss.SSSXXX", "yyyy-MM-dd'T'HH:mm:ssZ", "yyyy-MM-dd'T'HH:mm:ss" ) for (pattern in formats) { try { val sdf = SimpleDateFormat(pattern, Locale.getDefault()) val expireTime = sdf.parse(expireAt) if (expireTime != null) { val remain = expireTime.time - System.currentTimeMillis() return if (remain > 0) remain else 0 } } catch (_: Exception) { // 尝试下一种格式 } } // 解析失败,默认 15 分钟 Log.w("DetailActivity", "Failed to parse expireAt: $expireAt, using default 15min") return 15 * 60 * 1000L } /** * 显示支付超时 */ private fun showPaymentTimeout() { val db = dialogBinding ?: return db.ivQrcode.visibility = View.GONE db.pbQrcodeLoading.visibility = View.GONE db.progressBar.visibility = View.GONE db.tvCountdown.visibility = View.GONE db.tvPayAmount.visibility = View.GONE db.llButtons.visibility = View.GONE db.tvPayStatus.text = "支付超时,请重新生成" db.btnRegenerate.visibility = View.VISIBLE db.btnClose.visibility = View.VISIBLE } /** * 用 thirdCardNo 初始化 SDK 并播放: * 1. 用 thirdCardNo 初始化SDK * 2. 调用SDK获取视频地址 * 3. 跳转播放器 */ private fun playWithThirdCardNo(thirdCardNo: String) { val item = contentItem if (item == null) { isFetchingVideoUrl = false Toast.makeText(this, "内容信息未加载", Toast.LENGTH_SHORT).show() return } if (thirdCardNo.isEmpty()) { isFetchingVideoUrl = false Toast.makeText(this, "天卡卡号缺失,请重试", Toast.LENGTH_LONG).show() return } JetsenxSdkUtil.init(this, thirdCardNo, object : JetsenxSdkUtil.InitCallback { override fun onSuccess(memberCode: String) { Log.d("DetailActivity", "SDK init success with thirdCardNo: memberCode=$memberCode") sdkInitialized = true fetchVideoUrlFromSdk(item) } override fun onFail(message: String) { Log.e("DetailActivity", "SDK init fail with thirdCardNo: $message") runOnUiThread { isFetchingVideoUrl = false SignManager.clearBenefits() Toast.makeText(this@DetailActivity, "播放SDK初始化失败: $message", Toast.LENGTH_LONG).show() } } }) } /** 调用SDK获取视频地址并跳转播放器 */ private fun fetchVideoUrlFromSdk(item: ContentItem) { val parentContentType = item.getSdkParentContentType() val programCode = item.getVideoCode() Log.d("DetailActivity", "fetchVideoUrlFromSdk: parentContentType=$parentContentType programCode=$programCode") if (programCode.isEmpty()) { runOnUiThread { isFetchingVideoUrl = false Toast.makeText(this@DetailActivity, "无法获取节目编码", Toast.LENGTH_LONG).show() } return } JetsenxSdkUtil.getAllVideoUrl(parentContentType, programCode, object : JetsenxSdkUtil.VideoUrlCallback { override fun onSuccess(videoUrl1080: String, videoUrl720: String, isFree: Boolean) { runOnUiThread { val videoUrl = if (videoUrl1080.isNotEmpty()) videoUrl1080 else videoUrl720 Log.d("DetailActivity", "获取视频地址成功: $videoUrl (isFree=$isFree)") if (videoUrl.isEmpty()) { isFetchingVideoUrl = false Toast.makeText(this@DetailActivity, "未获取到视频地址", Toast.LENGTH_LONG).show() return@runOnUiThread } dismissPaymentDialog() // 跳转播放器 val intent = Intent(this@DetailActivity, VideoPlayerActivity::class.java).apply { putExtra(VideoPlayerActivity.EXTRA_VIDEO_URL, videoUrl) putExtra(VideoPlayerActivity.EXTRA_TITLE, item.name) } startActivity(intent) } } override fun onError(code: String, message: String) { runOnUiThread { isFetchingVideoUrl = false Log.e("DetailActivity", "获取视频地址失败: code=$code msg=$message") Toast.makeText(this@DetailActivity, "获取视频地址失败: $message", Toast.LENGTH_LONG).show() } } }) } /** 显示支付对话框 */ private fun showPaymentDialog(statusText: String) { if (paymentDialog?.isShowing == true) { updatePaymentDialog(loadingText = statusText) return } dialogBinding = DialogPaymentBinding.inflate(LayoutInflater.from(this)) dialogBinding?.tvPayStatus?.text = statusText // 重新生成按钮 → 重新发起支付流程 dialogBinding?.btnRegenerate?.setOnClickListener { countDownTimer?.cancel() dialogBinding?.btnRegenerate?.visibility = View.GONE dialogBinding?.btnClose?.visibility = View.GONE isFetchingVideoUrl = false startPaymentFlow() } // 关闭按钮 dialogBinding?.btnClose?.setOnClickListener { dismissPaymentDialog() paymentViewModel.reset() } // 保留兼容:确认/取消按钮(默认隐藏) dialogBinding?.btnCancel?.setOnClickListener { dismissPaymentDialog() paymentViewModel.reset() } dialogBinding?.btnConfirm?.visibility = View.GONE dialogBinding?.flQrcode?.visibility = View.GONE paymentDialog = AlertDialog.Builder(this) .setView(dialogBinding?.root) .setCancelable(false) .create() paymentDialog?.window?.setBackgroundDrawableResource(android.R.color.transparent) paymentDialog?.show() } /** * 更新支付对话框状态 * @param loadingText 加载中文本(显示进度条) * @param errorText 错误文本(显示关闭按钮) * @param showCloseButton 是否显示关闭按钮 */ private fun updatePaymentDialog( loadingText: String? = null, errorText: String? = null, showCloseButton: Boolean = false ) { val db = dialogBinding ?: return val text = loadingText ?: errorText ?: "" db.tvPayStatus.text = text if (loadingText != null) { // 加载状态:显示进度条 + 取消按钮(始终允许用户取消) db.progressBar.visibility = View.VISIBLE db.flQrcode.visibility = View.GONE db.ivQrcode.visibility = View.GONE db.pbQrcodeLoading.visibility = View.GONE db.tvPayAmount.visibility = View.GONE db.tvCountdown.visibility = View.GONE db.tvPayOrderNo.visibility = View.GONE db.llButtons.visibility = View.GONE db.btnRegenerate.visibility = View.GONE db.btnClose.visibility = View.VISIBLE db.btnClose.text = "取消" } else if (errorText != null || showCloseButton) { // 错误/完成状态:隐藏进度条,显示关闭按钮 db.progressBar.visibility = View.GONE db.pbQrcodeLoading.visibility = View.GONE db.flQrcode.visibility = View.GONE db.ivQrcode.visibility = View.GONE db.tvCountdown.visibility = View.GONE db.llButtons.visibility = View.GONE db.btnRegenerate.visibility = View.GONE db.btnClose.visibility = View.VISIBLE db.btnClose.text = "关闭" db.tvPayAmount.visibility = View.GONE db.btnRegenerate.visibility = View.VISIBLE db.btnRegenerate.text = "重试" // 订单号 val orderNo = paymentViewModel.orderNo.value if (orderNo.isNotEmpty()) { db.tvPayOrderNo.visibility = View.VISIBLE db.tvPayOrderNo.text = "订单号: $orderNo" } } } private fun dismissPaymentDialog() { countDownTimer?.cancel() countDownTimer = null paymentViewModel.cancelPolling() paymentDialog?.dismiss() paymentDialog = null dialogBinding = null } override fun onDestroy() { super.onDestroy() countDownTimer?.cancel() countDownTimer = null paymentViewModel.cancelPolling() dismissPaymentDialog() } companion object { const val EXTRA_CONTENTCODE = "extra_contentcode" const val EXTRA_NAME = "extra_name" } }