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 { 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) } } }