diff --git a/app/src/main/java/com/rtbishop/look4sat/AprsForegroundService.kt b/app/src/main/java/com/rtbishop/look4sat/AprsForegroundService.kt index b8a29424..2438c07f 100644 --- a/app/src/main/java/com/rtbishop/look4sat/AprsForegroundService.kt +++ b/app/src/main/java/com/rtbishop/look4sat/AprsForegroundService.kt @@ -12,6 +12,7 @@ import android.widget.Toast import android.content.pm.ServiceInfo import android.os.Build import android.os.IBinder +import com.rtbishop.look4sat.MainApplication import com.rtbishop.look4sat.core.presentation.R import com.rtbishop.look4sat.core.data.aprs.AprsConfig import com.rtbishop.look4sat.core.data.aprs.AprsStore @@ -50,7 +51,13 @@ class AprsForegroundService : Service() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { when (intent?.action) { ACTION_STOP -> stopReporting() - ACTION_REPORT_NOW -> reporter?.reportNow() + ACTION_REPORT_NOW -> { + if (reporter == null) { + // 服务没在跑:先启动(未配置会 Toast 提示) + startReporting() + } + reporter?.reportNow() + } else -> startReporting() } return START_STICKY @@ -59,24 +66,29 @@ class AprsForegroundService : Service() { private fun startReporting() { val cfg = AprsStore.loadConfig(this) if (!cfg.enabled || cfg.callsign.isBlank()) { + runCatching { + Toast.makeText(this, getString(R.string.aprs_toast_not_configured), Toast.LENGTH_SHORT).show() + } stopSelf() return } startForegroundWithNotification(cfg) val rep = AprsReporter( configProvider = { AprsStore.loadConfig(this) }, - positionProvider = { lastKnownPosition() }, + positionProvider = { stationPosition() }, onState = { lastState = it }, onReport = { report -> + AprsStore.saveLastReport(this, report.ok, report.detail) updateNotification(cfg) - // 手动上报/失败时给即时反馈(前台可见) - if (report.timestamp > System.currentTimeMillis() - 60_000L) { - val msg = if (report.ok) { - getString(R.string.aprs_toast_ok) - } else { - getString(R.string.aprs_toast_fail, report.detail) - } - runCatching { Toast.makeText(this, msg, Toast.LENGTH_LONG).show() } + // 上报结果必达:成功 Toast 短显,失败 Toast 长显+原因 + val msg = if (report.ok) { + getString(R.string.aprs_toast_ok) + } else { + getString(R.string.aprs_toast_fail, report.detail) + } + runCatching { + Toast.makeText(this, msg, + if (report.ok) Toast.LENGTH_SHORT else Toast.LENGTH_LONG).show() } } ) @@ -134,8 +146,17 @@ class AprsForegroundService : Service() { nm.notify(NOTIF_ID, buildNotification(cfg)) } - /** 取最近已知位置(APRSdroid PeriodicGPS 同思路,无权限时返回 null) */ - private fun lastKnownPosition(): Pair? { + /** 上报位置:优先用设置里的站位(用户拍板);站位无效时实时 GPS 兜底 */ + private fun stationPosition(): Pair? { + // ① 站位(设置页设置的位置) + val station = runCatching { + val container = (application as MainApplication).getMainContainer() + container.settingsRepo.stationPosition.value + }.getOrNull() + if (station != null && (station.latitude != 0.0 || station.longitude != 0.0)) { + return Pair(station.latitude, station.longitude) + } + // ② 兜底:实时 GPS 最后位置 return runCatching { val lm = getSystemService(Context.LOCATION_SERVICE) as android.location.LocationManager val providers = listOf( diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsIsClient.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsIsClient.kt index eddababb..0e8d56a8 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsIsClient.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsIsClient.kt @@ -59,13 +59,33 @@ class AprsIsClient( } } - /** 发送一个 APRS 包(一行) */ - fun sendPacket(packetLine: String): Boolean { + /** + * 发送一个 APRS 包(一行)并尝试读服务器确认。 + * 返回 null=发送失败;Pair(ok, detail)=发送结果(服务器错误原文在 detail) + */ + fun sendPacket(packetLine: String): Pair? { synchronized(lock) { - val w = writer ?: return false + val w = writer ?: return null w.println(packetLine) - return !w.checkError() + if (w.checkError()) return Pair(false, "write failed") } + // 尝试读服务器响应(短超时 3s;APRS-IS 对格式错误会回错误行) + return runCatching { + val s = socket ?: return@runCatching Pair(true, "OK") + val oldTimeout = s.soTimeout + s.soTimeout = 3000 + try { + val resp = reader?.readLine() + if (resp != null && (resp.contains("Invalid", ignoreCase = true) || + resp.contains("error", ignoreCase = true))) { + Pair(false, resp.trim()) + } else { + Pair(true, if (resp.isNullOrBlank()) "OK" else resp.trim()) + } + } finally { + s.soTimeout = oldTimeout + } + }.getOrElse { Pair(true, "OK") } } /** 读一行(服务器响应,超时抛异常) */ diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsReporter.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsReporter.kt index bc64d4f5..512174ff 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsReporter.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsReporter.kt @@ -99,9 +99,10 @@ class AprsReporter( val pos = positionProvider() val packetLine = buildPositionPacket(cfg, pos?.first, pos?.second) - val ok = c.sendPacket(packetLine) - onReport(AprsReport(System.currentTimeMillis(), packetLine, ok, - if (ok) "OK" else "send failed")) + val result = c.sendPacket(packetLine) + val ok = result?.first == true + val detail = result?.second ?: "no connection" + onReport(AprsReport(System.currentTimeMillis(), packetLine, ok, detail)) if (ok) onState(AprsState.Connected) else onState(AprsState.Error) } catch (e: Exception) { runCatching { client?.disconnect() } diff --git a/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsStore.kt b/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsStore.kt index 456de041..6d10a421 100644 --- a/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsStore.kt +++ b/core/data/src/main/java/com/rtbishop/look4sat/core/data/aprs/AprsStore.kt @@ -24,6 +24,9 @@ object AprsStore { private const val KEY_STATUS = "status" private const val KEY_SYMBOL_TABLE = "symbol_table" private const val KEY_SYMBOL_CODE = "symbol_code" + private const val KEY_LAST_TIME = "last_report_time" + private const val KEY_LAST_OK = "last_report_ok" + private const val KEY_LAST_DETAIL = "last_report_detail" /** 读取配置(填入即保存,无需每次打开重填) */ fun loadConfig(context: Context): AprsConfig { @@ -42,6 +45,26 @@ object AprsStore { ) } + /** 上次上报结果(供设置卡片显示) */ + data class LastReport(val time: Long = 0L, val ok: Boolean = false, val detail: String = "") + + fun loadLastReport(context: Context): LastReport { + val p = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE) + return LastReport( + time = p.getLong(KEY_LAST_TIME, 0L), + ok = p.getBoolean(KEY_LAST_OK, false), + detail = p.getString(KEY_LAST_DETAIL, "") ?: "" + ) + } + + fun saveLastReport(context: Context, ok: Boolean, detail: String) { + context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit() + .putLong(KEY_LAST_TIME, System.currentTimeMillis()) + .putBoolean(KEY_LAST_OK, ok) + .putString(KEY_LAST_DETAIL, detail) + .apply() + } + /** 保存配置 */ fun saveConfig(context: Context, cfg: AprsConfig) { context.getSharedPreferences(PREFS, Context.MODE_PRIVATE).edit() diff --git a/core/presentation/src/main/res/values-id/strings.xml b/core/presentation/src/main/res/values-id/strings.xml index 619c3554..a8adb957 100644 --- a/core/presentation/src/main/res/values-id/strings.xml +++ b/core/presentation/src/main/res/values-id/strings.xml @@ -38,6 +38,10 @@ Hitung kode sandi APRS: laporan terkirim APRS: laporan gagal - %1$s + APRS belum dikonfigurasi - isi panggilan dulu + Laporan terakhir: %1$s %2$s + OK + gagal - %1$s Lainnya Log AMSAT diff --git a/core/presentation/src/main/res/values-in/strings.xml b/core/presentation/src/main/res/values-in/strings.xml index da770e8f..e8083071 100644 --- a/core/presentation/src/main/res/values-in/strings.xml +++ b/core/presentation/src/main/res/values-in/strings.xml @@ -38,6 +38,10 @@ Hitung kode sandi APRS: laporan terkirim APRS: laporan gagal - %1$s + APRS belum dikonfigurasi - isi panggilan dulu + Laporan terakhir: %1$s %2$s + OK + gagal - %1$s Lainnya Log AMSAT diff --git a/core/presentation/src/main/res/values-tr/strings.xml b/core/presentation/src/main/res/values-tr/strings.xml index 3f7d1956..3f672102 100644 --- a/core/presentation/src/main/res/values-tr/strings.xml +++ b/core/presentation/src/main/res/values-tr/strings.xml @@ -39,6 +39,10 @@ Parolayı hesapla APRS: rapor gönderildi APRS: rapor başarısız - %1$s + APRS yapılandırılmadı - önce çağrı girin + Son rapor: %1$s %2$s + OK + başarısız - %1$s Daha Fazla Günlük AMSAT diff --git a/core/presentation/src/main/res/values-zh/strings.xml b/core/presentation/src/main/res/values-zh/strings.xml index 2c8d368a..c7fa605c 100644 --- a/core/presentation/src/main/res/values-zh/strings.xml +++ b/core/presentation/src/main/res/values-zh/strings.xml @@ -40,6 +40,10 @@ 计算验证码 APRS: 上报成功 APRS: 上报失败 - %1$s + APRS 未配置,请先填呼号 + 上次上报: %1$s %2$s + 成功 + 失败 - %1$s 更多 日志 AMSAT diff --git a/core/presentation/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml index df8eb982..dcdc47d3 100644 --- a/core/presentation/src/main/res/values/strings.xml +++ b/core/presentation/src/main/res/values/strings.xml @@ -41,6 +41,10 @@ Compute passcode APRS: report sent OK APRS: report failed - %1$s + APRS not configured - set callsign first + Last report: %1$s %2$s + OK + failed - %1$s More Log AMSAT diff --git a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/AprsCard.kt b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/AprsCard.kt index 3ccec9fa..e5e3e3cf 100644 --- a/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/AprsCard.kt +++ b/feature/settings/src/main/java/com/rtbishop/look4sat/feature/settings/AprsCard.kt @@ -44,6 +44,8 @@ fun AprsCard() { val context = LocalContext.current var config by remember { mutableStateOf(AprsStore.loadConfig(context)) } var showDialog by remember { mutableStateOf(false) } + var lastReport by remember { mutableStateOf(AprsStore.loadLastReport(context)) } + var reportPing by remember { mutableStateOf(0) } val notifPermissionLauncher = rememberLauncherForActivityResult( ActivityResultContracts.RequestPermission() ) { } @@ -98,6 +100,23 @@ fun AprsCard() { fontSize = 13.sp, color = MaterialTheme.colorScheme.onSurfaceVariant ) + // 上次上报状态行 + val lr = lastReport + if (lr.time > 0L) { + val timeStr = java.text.SimpleDateFormat("HH:mm:ss", java.util.Locale.getDefault()) + .format(java.util.Date(lr.time)) + Text( + text = stringResource( + id = R.string.prefs_aprs_last_report, + timeStr, + if (lr.ok) stringResource(R.string.prefs_aprs_last_ok) + else stringResource(R.string.prefs_aprs_last_fail, lr.detail) + ), + fontSize = 13.sp, + color = if (lr.ok) MaterialTheme.colorScheme.primary + else MaterialTheme.colorScheme.error + ) + } // 手动上报按钮 CardButton( onClick = { @@ -105,12 +124,20 @@ fun AprsCard() { .setClassName(context.packageName, AprsStore.SERVICE_CLASS) .setAction(AprsStore.ACTION_REPORT_NOW) context.startForegroundService(intent) + reportPing++ }, text = stringResource(id = R.string.prefs_aprs_report_now), modifier = Modifier.fillMaxWidth() ) } } + // 上报后延迟刷新状态行(等结果落盘) + androidx.compose.runtime.LaunchedEffect(reportPing) { + if (reportPing > 0) { + kotlinx.coroutines.delay(6000) + lastReport = AprsStore.loadLastReport(context) + } + } if (showDialog) { AprsSettingsDialog( config = config,