497 lines
18 KiB
Kotlin
497 lines
18 KiB
Kotlin
package com.example.defenseapplication.personal
|
||
|
||
import android.content.Intent
|
||
import android.content.pm.PackageManager
|
||
import android.graphics.Bitmap
|
||
import android.graphics.Canvas
|
||
import android.graphics.Color
|
||
import android.graphics.Paint
|
||
import android.os.Bundle
|
||
import android.os.Environment
|
||
import android.text.TextPaint
|
||
import android.view.LayoutInflater
|
||
import android.view.View
|
||
import androidx.appcompat.app.AppCompatActivity
|
||
import com.google.android.material.bottomsheet.BottomSheetDialog
|
||
import androidx.core.content.ContextCompat
|
||
import com.example.defenseapplication.R
|
||
import com.example.defenseapplication.bean.VerifyOptionPasswordRequest
|
||
import com.example.defenseapplication.databinding.DialogExpireTimeBinding
|
||
import com.example.defenseapplication.databinding.InvitationCodeActivityBinding
|
||
import com.example.defenseapplication.utils.RetrofitNetwork
|
||
import com.google.zxing.BarcodeFormat
|
||
import com.google.zxing.EncodeHintType
|
||
import com.google.zxing.qrcode.QRCodeWriter
|
||
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel
|
||
import com.gyf.immersionbar.ImmersionBar
|
||
import kotlinx.coroutines.CoroutineScope
|
||
import kotlinx.coroutines.Dispatchers
|
||
import kotlinx.coroutines.launch
|
||
import kotlinx.coroutines.withContext
|
||
import java.io.File
|
||
import java.io.FileOutputStream
|
||
import java.util.Hashtable
|
||
//邀请二维码
|
||
class InvitationCodeActivity : AppCompatActivity() {
|
||
|
||
private lateinit var binding: InvitationCodeActivityBinding
|
||
private var expireTimeInMillis: Long = EXPIRE_24H // 默认24小时
|
||
private var selectedExpireOption: Int = 0 // 0: 24h, 1: 7d,默认选中24h
|
||
private var generatedInviteCode: String = "" // 生成的邀请码
|
||
private var expireTimestamp: Long = 0 // 过期时间戳
|
||
private var qrCodeContent: String = "" // 生成的二维码内容
|
||
private var lastClickTime: Long = 0 // 上次点击时间,用于防止多次点击
|
||
private var expireTimeDialog: BottomSheetDialog? = null // 时效选择弹窗
|
||
private var isLoading = false // 是否正在加载
|
||
|
||
companion object {
|
||
const val EXPIRE_24H = 24 * 60 * 60 * 1000L
|
||
const val EXPIRE_7D = 7 * 24 * 60 * 60 * 1000L
|
||
}
|
||
|
||
override fun onCreate(savedInstanceState: Bundle?) {
|
||
super.onCreate(savedInstanceState)
|
||
binding = InvitationCodeActivityBinding.inflate(layoutInflater)
|
||
setContentView(binding.root)
|
||
|
||
ImmersionBar.with(this)
|
||
.statusBarDarkFont(true)
|
||
.titleBar(binding.topBar)
|
||
.init()
|
||
|
||
initListener()
|
||
// 默认显示时效为24h
|
||
binding.tvExpireTime.text = getString(R.string.expire_24h)
|
||
// 默认显示灰色占位区域,二维码不可见
|
||
hideQrCode()
|
||
|
||
// 页面加载时查询推荐码状态
|
||
queryRecommendationCodeStatus()
|
||
}
|
||
|
||
private fun initListener() {
|
||
binding.ivBack.setOnClickListener {
|
||
finish()
|
||
}
|
||
|
||
binding.llExpireTime.setOnClickListener {
|
||
showExpireTimeDialog()
|
||
}
|
||
|
||
binding.llWechatShare.setOnClickListener {
|
||
// 防止多次点击(间隔3秒)
|
||
val currentTime = System.currentTimeMillis()
|
||
if (currentTime - lastClickTime < 3000) {
|
||
return@setOnClickListener
|
||
}
|
||
lastClickTime = currentTime
|
||
// 微信分享功能
|
||
shareToWeChat()
|
||
}
|
||
|
||
// 撤销邀请按钮
|
||
binding.btnCancelInvite.setOnClickListener {
|
||
cancelInvitation()
|
||
}
|
||
|
||
// 重新生成按钮
|
||
binding.btnRegenerate.setOnClickListener {
|
||
regenerateInvitation()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 查询推荐码状态
|
||
*/
|
||
private fun queryRecommendationCodeStatus() {
|
||
isLoading = true
|
||
CoroutineScope(Dispatchers.IO).launch {
|
||
try {
|
||
// 注意:接口需要传入code参数,但我们可能不知道当前的推荐码
|
||
// 这里使用空字符串或默认值进行查询
|
||
val response = RetrofitNetwork.getNetworkService().selectRecommendationCode("")
|
||
withContext(Dispatchers.Main) {
|
||
isLoading = false
|
||
if (response.code == 200) {
|
||
// 有推荐码,解析返回的数据
|
||
handleRecommendationCodeResponse(response.data)
|
||
} else {
|
||
// 没有推荐码或查询失败,生成默认24小时的推荐码
|
||
generateDefaultRecommendationCode()
|
||
}
|
||
}
|
||
} catch (e: Exception) {
|
||
e.printStackTrace()
|
||
withContext(Dispatchers.Main) {
|
||
isLoading = false
|
||
// 网络异常,生成默认24小时的推荐码
|
||
generateDefaultRecommendationCode()
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 处理推荐码查询响应
|
||
*/
|
||
private fun handleRecommendationCodeResponse(data: Any?) {
|
||
// 解析返回的数据,获取推荐码和过期时间
|
||
// 这里需要根据实际接口返回格式进行解析
|
||
if (data is Map<*, *>) {
|
||
generatedInviteCode = data["code"]?.toString() ?: ""
|
||
expireTimestamp = data["expireTime"]?.toString()?.toLongOrNull() ?: 0L
|
||
|
||
// 检查推荐码是否过期
|
||
if (expireTimestamp > 0 && System.currentTimeMillis() < expireTimestamp) {
|
||
// 推荐码有效,显示二维码
|
||
qrCodeContent = "{\"type\":\"invite\",\"code\":\"$generatedInviteCode\",\"expire\":$expireTimestamp}"
|
||
showQrCode()
|
||
generateQRCode()
|
||
|
||
// 根据过期时间设置显示的时效
|
||
val remainingTime = expireTimestamp - System.currentTimeMillis()
|
||
if (remainingTime > EXPIRE_24H) {
|
||
// 超过24小时,认为是7天的
|
||
expireTimeInMillis = EXPIRE_7D
|
||
selectedExpireOption = 1
|
||
binding.tvExpireTime.text = getString(R.string.expire_7d)
|
||
} else {
|
||
expireTimeInMillis = EXPIRE_24H
|
||
selectedExpireOption = 0
|
||
binding.tvExpireTime.text = getString(R.string.expire_24h)
|
||
}
|
||
} else {
|
||
// 推荐码已过期,生成新的默认24小时推荐码
|
||
generateDefaultRecommendationCode()
|
||
}
|
||
} else {
|
||
// 数据格式不对,生成新的推荐码
|
||
generateDefaultRecommendationCode()
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 生成默认24小时的推荐码
|
||
*/
|
||
private fun generateDefaultRecommendationCode() {
|
||
expireTimeInMillis = EXPIRE_24H
|
||
selectedExpireOption = 0
|
||
binding.tvExpireTime.text = getString(R.string.expire_24h)
|
||
|
||
// 生成推荐码(本地生成,实际应该调用API)
|
||
generateQRCode()
|
||
showQrCode()
|
||
}
|
||
|
||
/**
|
||
* 处理生成推荐码成功
|
||
*/
|
||
private fun handleGenerateSuccess(data: Any?, expireDays: Int) {
|
||
// 解析返回的数据
|
||
if (data is Map<*, *>) {
|
||
generatedInviteCode = data["code"]?.toString() ?: ""
|
||
expireTimestamp = data["expireTime"]?.toString()?.toLongOrNull() ?: (System.currentTimeMillis() + expireTimeInMillis)
|
||
qrCodeContent = "{\"type\":\"invite\",\"code\":\"$generatedInviteCode\",\"expire\":$expireTimestamp}"
|
||
|
||
showQrCode()
|
||
generateQRCode()
|
||
} else {
|
||
// 如果接口没有返回推荐码,本地生成一个
|
||
generateInviteCode()
|
||
showQrCode()
|
||
generateQRCode()
|
||
}
|
||
}
|
||
|
||
private fun showExpireTimeDialog() {
|
||
val dialogBinding = DialogExpireTimeBinding.inflate(LayoutInflater.from(this))
|
||
|
||
expireTimeDialog = BottomSheetDialog(this, R.style.BottomSheetDialog)
|
||
expireTimeDialog?.setContentView(dialogBinding.root)
|
||
|
||
// 设置选中状态
|
||
when (selectedExpireOption) {
|
||
0 -> dialogBinding.rb24h.isChecked = true
|
||
1 -> dialogBinding.rb7d.isChecked = true
|
||
}
|
||
|
||
dialogBinding.radioGroup.setOnCheckedChangeListener { _, checkedId ->
|
||
when (checkedId) {
|
||
R.id.rb24h -> selectedExpireOption = 0
|
||
R.id.rb7d -> selectedExpireOption = 1
|
||
}
|
||
}
|
||
|
||
dialogBinding.tvCancel.setOnClickListener {
|
||
expireTimeDialog?.dismiss()
|
||
}
|
||
|
||
dialogBinding.tvConfirm.setOnClickListener {
|
||
// 直接设置新的时效并生成二维码
|
||
when (selectedExpireOption) {
|
||
0 -> {
|
||
expireTimeInMillis = EXPIRE_24H
|
||
binding.tvExpireTime.text = getString(R.string.expire_24h)
|
||
}
|
||
1 -> {
|
||
expireTimeInMillis = EXPIRE_7D
|
||
binding.tvExpireTime.text = getString(R.string.expire_7d)
|
||
}
|
||
}
|
||
// 显示二维码并生成
|
||
showQrCode()
|
||
generateQRCode()
|
||
expireTimeDialog?.dismiss()
|
||
}
|
||
|
||
expireTimeDialog?.show()
|
||
}
|
||
|
||
private fun hideQrCode() {
|
||
binding.ivQrCode.visibility = View.GONE
|
||
binding.ivQrLogo.visibility = View.GONE
|
||
binding.vQrPlaceholder.visibility = View.VISIBLE
|
||
}
|
||
|
||
private fun showQrCode() {
|
||
binding.vQrPlaceholder.visibility = View.GONE
|
||
binding.ivQrCode.visibility = View.VISIBLE
|
||
binding.ivQrLogo.visibility = View.VISIBLE
|
||
}
|
||
|
||
private fun generateQRCode() {
|
||
val content = if (qrCodeContent.isNotEmpty()) qrCodeContent else generateInviteCode()
|
||
val qrBitmap = createQRCodeWithLogo(content, 400, 400)
|
||
binding.ivQrCode.setImageBitmap(qrBitmap)
|
||
}
|
||
|
||
private fun generateInviteCode(): String {
|
||
val timestamp = System.currentTimeMillis()
|
||
expireTimestamp = timestamp + expireTimeInMillis
|
||
generatedInviteCode = "INV${timestamp}"
|
||
qrCodeContent = "{\"type\":\"invite\",\"code\":\"$generatedInviteCode\",\"expire\":$expireTimestamp}"
|
||
// 输出日志,方便调试验证
|
||
android.util.Log.d("InvitationCode", "生成的二维码内容: $qrCodeContent")
|
||
android.util.Log.d("InvitationCode", "推荐码: $generatedInviteCode, 过期时间戳: $expireTimestamp")
|
||
return qrCodeContent
|
||
}
|
||
|
||
private fun createQRCodeWithLogo(content: String, width: Int, height: Int): Bitmap? {
|
||
return try {
|
||
val hints = Hashtable<EncodeHintType, Any>()
|
||
hints[EncodeHintType.CHARACTER_SET] = "UTF-8"
|
||
hints[EncodeHintType.ERROR_CORRECTION] = ErrorCorrectionLevel.H
|
||
hints[EncodeHintType.MARGIN] = 1
|
||
|
||
val writer = QRCodeWriter()
|
||
val bitMatrix = writer.encode(content, BarcodeFormat.QR_CODE, width, height, hints)
|
||
|
||
val qrBitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||
val canvas = Canvas(qrBitmap)
|
||
|
||
canvas.drawColor(Color.WHITE)
|
||
|
||
val paint = Paint(Paint.ANTI_ALIAS_FLAG)
|
||
paint.color = Color.BLACK
|
||
|
||
val cellWidth = width.toFloat() / bitMatrix.width
|
||
val cellHeight = height.toFloat() / bitMatrix.height
|
||
|
||
for (x in 0 until bitMatrix.width) {
|
||
for (y in 0 until bitMatrix.height) {
|
||
if (bitMatrix.get(x, y)) {
|
||
canvas.drawRect(
|
||
x * cellWidth,
|
||
y * cellHeight,
|
||
(x + 1) * cellWidth,
|
||
(y + 1) * cellHeight,
|
||
paint
|
||
)
|
||
}
|
||
}
|
||
}
|
||
|
||
val logoDrawable = ContextCompat.getDrawable(this, R.mipmap.loge)
|
||
if (logoDrawable != null) {
|
||
val logoSize = width / 5
|
||
val logoX = (width - logoSize) / 2
|
||
val logoY = (height - logoSize) / 2
|
||
|
||
val logoBitmap = Bitmap.createBitmap(logoSize, logoSize, Bitmap.Config.ARGB_8888)
|
||
val logoCanvas = Canvas(logoBitmap)
|
||
logoDrawable.setBounds(0, 0, logoSize, logoSize)
|
||
logoDrawable.draw(logoCanvas)
|
||
|
||
canvas.drawBitmap(logoBitmap, logoX.toFloat(), logoY.toFloat(), null)
|
||
}
|
||
|
||
qrBitmap
|
||
} catch (e: Exception) {
|
||
e.printStackTrace()
|
||
null
|
||
}
|
||
}
|
||
|
||
private fun shareToWeChat() {
|
||
if (expireTimestamp == 0L || System.currentTimeMillis() > expireTimestamp) {
|
||
ToastUtils.showError(this, getString(R.string.qr_code_expired))
|
||
return
|
||
}
|
||
|
||
val shareBitmap = generateSharePoster()
|
||
if (shareBitmap == null) {
|
||
ToastUtils.showError(this, getString(R.string.share_failed))
|
||
return
|
||
}
|
||
|
||
val imagePath = saveBitmapToFile(shareBitmap)
|
||
if (imagePath.isEmpty()) {
|
||
ToastUtils.showError(this, getString(R.string.share_failed))
|
||
return
|
||
}
|
||
|
||
shareImage(imagePath)
|
||
}
|
||
|
||
private fun generateSharePoster(): Bitmap? {
|
||
return try {
|
||
val posterBackground = ContextCompat.getDrawable(this, R.drawable.share_poster_empty)
|
||
?: return null
|
||
|
||
val width = posterBackground.intrinsicWidth
|
||
val height = posterBackground.intrinsicHeight
|
||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||
val canvas = Canvas(bitmap)
|
||
|
||
posterBackground.setBounds(0, 0, width, height)
|
||
posterBackground.draw(canvas)
|
||
|
||
val qrCode = createQRCodeWithLogo(qrCodeContent, 300, 300)
|
||
?: return bitmap
|
||
|
||
val qrSize = (width * 0.38f).toInt()
|
||
val qrX = (width - qrSize) / 2
|
||
val qrY = (height * 0.60f).toInt()
|
||
|
||
val scaledQr = Bitmap.createScaledBitmap(qrCode, qrSize, qrSize, true)
|
||
canvas.drawBitmap(scaledQr, qrX.toFloat(), qrY.toFloat(), null)
|
||
|
||
val expireText = if (expireTimeInMillis == EXPIRE_24H) {
|
||
getString(R.string.expire_24h)
|
||
} else {
|
||
getString(R.string.expire_7d)
|
||
}
|
||
|
||
val tipsText = getString(R.string.share_poster_tips, expireText)
|
||
val tipsPaint = android.text.TextPaint().apply {
|
||
color = Color.parseColor("#0475F3")
|
||
textSize = 36f
|
||
isAntiAlias = true
|
||
textAlign = Paint.Align.CENTER
|
||
isFakeBoldText = false
|
||
}
|
||
|
||
val horizontalPadding = (width * 0.12f).toInt()
|
||
val textWidth = width - horizontalPadding * 2 - (width * 0.08f).toInt()
|
||
|
||
val staticLayout = android.text.StaticLayout.Builder.obtain(
|
||
tipsText, 0, tipsText.length, tipsPaint, textWidth
|
||
)
|
||
.setAlignment(android.text.Layout.Alignment.ALIGN_CENTER)
|
||
.setLineSpacing(0f, 1.4f)
|
||
.build()
|
||
|
||
val qrBottom = qrY + qrSize
|
||
val marginBelowQr = (height * 0.04f).toInt()
|
||
val textY = qrBottom + marginBelowQr
|
||
|
||
val rightOffset = (width * 0.35f).toInt()
|
||
|
||
canvas.save()
|
||
canvas.translate((horizontalPadding + rightOffset).toFloat(), textY.toFloat())
|
||
staticLayout.draw(canvas)
|
||
canvas.restore()
|
||
|
||
bitmap
|
||
} catch (e: Exception) {
|
||
e.printStackTrace()
|
||
null
|
||
}
|
||
}
|
||
|
||
private fun saveBitmapToFile(bitmap: Bitmap): String {
|
||
return try {
|
||
val storageDir = getExternalFilesDir(Environment.DIRECTORY_PICTURES)
|
||
?: return ""
|
||
|
||
val file = File(storageDir, "share_poster_${System.currentTimeMillis()}.png")
|
||
val fos = FileOutputStream(file)
|
||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos)
|
||
fos.flush()
|
||
fos.close()
|
||
file.absolutePath
|
||
} catch (e: Exception) {
|
||
e.printStackTrace()
|
||
""
|
||
}
|
||
}
|
||
|
||
private fun shareImage(imagePath: String) {
|
||
val file = File(imagePath)
|
||
if (!file.exists()) {
|
||
ToastUtils.showError(this, getString(R.string.share_failed))
|
||
return
|
||
}
|
||
|
||
val authority = "${packageName}.fileprovider"
|
||
val contentUri = androidx.core.content.FileProvider.getUriForFile(this, authority, file)
|
||
|
||
val intent = Intent(Intent.ACTION_SEND).apply {
|
||
type = "image/*"
|
||
putExtra(Intent.EXTRA_STREAM, contentUri)
|
||
putExtra(Intent.EXTRA_TEXT, getString(R.string.invite_friend))
|
||
flags = Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||
}
|
||
|
||
val resInfoList = packageManager.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY)
|
||
for (resolveInfo in resInfoList) {
|
||
val packageName = resolveInfo.activityInfo.packageName
|
||
grantUriPermission(packageName, contentUri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||
}
|
||
|
||
val chooser = Intent.createChooser(intent, getString(R.string.share_to))
|
||
if (intent.resolveActivity(packageManager) != null) {
|
||
startActivity(chooser)
|
||
} else {
|
||
ToastUtils.showError(this, getString(R.string.no_share_app))
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 撤销邀请
|
||
*/
|
||
private fun cancelInvitation() {
|
||
// 撤销邀请逻辑
|
||
hideQrCode()
|
||
generatedInviteCode = ""
|
||
qrCodeContent = ""
|
||
expireTimestamp = 0
|
||
// 显示提示
|
||
ToastUtils.showSuccess(this, getString(R.string.operation_success))
|
||
}
|
||
|
||
/**
|
||
* 重新生成邀请码
|
||
*/
|
||
private fun regenerateInvitation() {
|
||
// 根据当前选择的时效重新生成邀请码
|
||
generateQRCode()
|
||
showQrCode()
|
||
ToastUtils.showSuccess(this, getString(R.string.operation_success))
|
||
}
|
||
|
||
override fun onDestroy() {
|
||
super.onDestroy()
|
||
expireTimeDialog?.dismiss()
|
||
}
|
||
} |