yunzesms/SMS/app/src/main/java/com/yunzer/sms/MainActivity.kt
2026-03-25 16:56:45 +08:00

225 lines
8.6 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.yunzer.sms
import android.Manifest
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.ScrollView
import android.widget.TextView
import android.widget.Toast
import androidx.activity.ComponentActivity
import androidx.activity.result.contract.ActivityResultContracts
import androidx.core.content.ContextCompat
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
class MainActivity : ComponentActivity() {
private lateinit var etBackendUrl: EditText
private lateinit var etApiKey: EditText
private lateinit var btnSave: Button
private lateinit var etTestPhone: EditText
private lateinit var btnSmsTest: Button
private lateinit var btnClearLog: Button
private lateinit var tvStatus: TextView
private lateinit var tvLog: TextView
private lateinit var svLog: ScrollView
private val logReceiver = object : android.content.BroadcastReceiver() {
override fun onReceive(context: android.content.Context?, intent: Intent?) {
if (intent == null) return
val msg = intent.getStringExtra(SmsGatewayLogger.EXTRA_MESSAGE) ?: return
val level = intent.getStringExtra(SmsGatewayLogger.EXTRA_LEVEL) ?: "INFO"
val line = buildLogLine(level, msg)
runOnUiThread { appendLogLine(line) }
}
}
private val configStore by lazy { ConfigStore(this) }
private val permissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) {
// 权限申请后可能有两类后续动作:
// 1启动网关
// 2如果用户在申请前点过短信测试则继续发送测试
if (pendingSmsTestPhone != null) {
val p = pendingSmsTestPhone!!
pendingSmsTestPhone = null
sendTestSms(p)
return@registerForActivityResult
}
maybeStartGateway()
}
private var pendingSmsTestPhone: String? = null
private val testSmsContent =
"【云泽网】您好,这是一条测试短信,用于验证短信收发是否正常,请勿回复,谢谢!"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
etBackendUrl = findViewById(R.id.etBackendUrl)
etApiKey = findViewById(R.id.etApiKey)
btnSave = findViewById(R.id.btnSave)
etTestPhone = findViewById(R.id.etTestPhone)
btnSmsTest = findViewById(R.id.btnSmsTest)
btnClearLog = findViewById(R.id.btnClearLog)
tvStatus = findViewById(R.id.tvStatus)
tvLog = findViewById(R.id.tvLog)
svLog = findViewById(R.id.svLog)
configStore.load()?.let { cfg ->
etBackendUrl.setText(cfg.backendUrl)
etApiKey.setText(cfg.apiKey)
tvStatus.text = getString(R.string.config_saved)
tvLog.text = buildLogLine("INFO", "应用已配置,等待启动网关")
} ?: run {
tvStatus.text = getString(R.string.status_missing)
tvLog.text = buildLogLine("INFO", "尚未配置:请填写后端地址/ApiKey")
}
btnSave.setOnClickListener {
val backendUrl = etBackendUrl.text.toString().trim()
val apiKey = etApiKey.text.toString().trim()
if (!backendUrl.startsWith("http")) {
Toast.makeText(this, getString(R.string.config_invalid), Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
if (apiKey.isEmpty()) {
Toast.makeText(this, getString(R.string.config_invalid), Toast.LENGTH_SHORT).show()
return@setOnClickListener
}
configStore.save(GatewayConfig(backendUrl = backendUrl, apiKey = apiKey))
tvStatus.text = getString(R.string.config_saved)
SmsGatewayLogger.post(this, "INFO", "配置已保存backend=$backendUrl")
maybeStartGateway()
}
btnSmsTest.setOnClickListener {
val phoneRaw = etTestPhone.text.toString().trim()
val phone = normalizePhone(phoneRaw)
if (phone.isEmpty() || !phone.startsWith("+")) {
Toast.makeText(this, getString(R.string.sms_test_invalid_phone), Toast.LENGTH_SHORT)
.show()
SmsGatewayLogger.post(this, "WARN", "短信测试参数不合法 phone='$phoneRaw'")
return@setOnClickListener
}
// 若未授权,则申请权限后在回调里继续发送
val need = missingPermissions().filter { it == Manifest.permission.SEND_SMS }
if (need.isNotEmpty()) {
pendingSmsTestPhone = phone
Toast.makeText(this, "需要短信发送权限,正在申请…", Toast.LENGTH_SHORT).show()
SmsGatewayLogger.post(this, "INFO", "短信测试前缺少 SEND_SMS 权限,准备申请")
permissionLauncher.launch(need.toTypedArray())
return@setOnClickListener
}
sendTestSms(phone)
}
btnClearLog.setOnClickListener {
tvLog.text = ""
SmsGatewayLogger.post(this, "INFO", "日志已清空")
}
ensurePermissionsAndMaybeStart()
}
override fun onStart() {
super.onStart()
registerReceiver(logReceiver, IntentFilter(SmsGatewayLogger.ACTION_LOG))
}
override fun onStop() {
super.onStop()
unregisterReceiver(logReceiver)
}
private fun ensurePermissionsAndMaybeStart() {
val missing = missingPermissions()
if (missing.isNotEmpty()) {
Toast.makeText(this, getString(R.string.permission_tip), Toast.LENGTH_SHORT).show()
SmsGatewayLogger.post(this, "WARN", "缺少权限:$missing")
permissionLauncher.launch(missing.toTypedArray())
} else {
maybeStartGateway()
}
}
private fun maybeStartGateway() {
if (!hasAllRequiredPermissions()) return
val cfg = configStore.load() ?: return
SmsGatewayLogger.post(this, "INFO", "准备启动网关(仅使用 apiKey 鉴权)")
tvStatus.text = getString(R.string.config_saved)
val intent = Intent(this, TaskSyncService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
ContextCompat.startForegroundService(this, intent)
} else {
startService(intent)
}
}
private fun missingPermissions(): List<String> {
val need = mutableListOf(
Manifest.permission.RECEIVE_SMS,
Manifest.permission.SEND_SMS
)
if (Build.VERSION.SDK_INT >= 33) {
need.add(Manifest.permission.POST_NOTIFICATIONS)
}
return need.filter { perm ->
ContextCompat.checkSelfPermission(this, perm) != PackageManager.PERMISSION_GRANTED
}
}
private fun hasAllRequiredPermissions(): Boolean {
return missingPermissions().isEmpty()
}
private fun normalizePhone(phone: String): String {
// 简单归一化:去空格/破折号
return phone.replace(" ", "").replace("-", "")
}
private fun sendTestSms(phone: String) {
SmsGatewayLogger.post(this, "INFO", "开始发送短信测试 phone=$phone")
val result = SmsSender.sendSms(this, phone, testSmsContent)
if (result.isSuccess) {
SmsGatewayLogger.post(this, "INFO", getString(R.string.sms_test_success))
tvStatus.text = "短信测试:成功"
} else {
val err = result.exceptionOrNull()?.message ?: "unknown_error"
SmsGatewayLogger.post(this, "ERROR", "${getString(R.string.sms_test_failed)} err=$err")
tvStatus.text = "短信测试:失败"
}
}
private fun buildLogLine(level: String, msg: String): String {
val ts = SimpleDateFormat("HH:mm:ss.SSS", Locale.getDefault()).format(Date())
return "$ts [$level] $msg\n"
}
private fun appendLogLine(line: String) {
val existing = tvLog.text?.toString().orEmpty()
val combined = existing + line
val lines = combined.split("\n")
tvLog.text = if (lines.size > 220) {
val tail = lines.takeLast(220).joinToString("\n").trimEnd()
tail + "\n"
} else {
combined
}
svLog.post { svLog.fullScroll(View.FOCUS_DOWN) }
}
}