225 lines
8.6 KiB
Kotlin
225 lines
8.6 KiB
Kotlin
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) }
|
||
}
|
||
}
|
||
|