fix(aprs): upload feedback, manual report reliability, station position
User testing round 3 (4.5.4): no success feedback on upload, aprs.fi shows nothing, passcode calculator OK, notification present. - Manual report now works even when service not running: ACTION_REPORT_NOW starts the service first (Toast "not configured" if missing callsign) - Upload result feedback guaranteed: Toast always shows (short OK / long fail+reason), last result persisted (time/ok/detail) and shown in the settings card "Last report: HH:mm:ss OK/failed - detail" - Position source: station position from settingsRepo (user decision) with live GPS last-known as fallback - sendPacket reads the server confirmation line (short 3s timeout); server error text (Invalid/error) surfaces in Toast + card - Strings EN/ZH/TR/IN/ID +4 keys Verified: compileDebugKotlin all modules BUILD SUCCESSFUL, check_strings 9 files OK.
This commit is contained in:
1 parent
012ea1eeb6
commit
c9d87be289
10 files changed
+131
-19
No files matched your search
@@ -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<Double, Double>? {
|
||||
/** 上报位置:优先用设置里的站位(用户拍板);站位无效时实时 GPS 兜底 */
|
||||
private fun stationPosition(): Pair<Double, Double>? {
|
||||
// ① 站位(设置页设置的位置)
|
||||
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(
|
||||
|
||||
@@ -59,13 +59,33 @@ class AprsIsClient(
|
||||
}
|
||||
}
|
||||
|
||||
/** 发送一个 APRS 包(一行) */
|
||||
fun sendPacket(packetLine: String): Boolean {
|
||||
/**
|
||||
* 发送一个 APRS 包(一行)并尝试读服务器确认。
|
||||
* 返回 null=发送失败;Pair(ok, detail)=发送结果(服务器错误原文在 detail)
|
||||
*/
|
||||
fun sendPacket(packetLine: String): Pair<Boolean, String>? {
|
||||
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") }
|
||||
}
|
||||
|
||||
/** 读一行(服务器响应,超时抛异常) */
|
||||
|
||||
@@ -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() }
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<string name="prefs_aprs_calc_passcode">Hitung kode sandi</string>
|
||||
<string name="aprs_toast_ok">APRS: laporan terkirim</string>
|
||||
<string name="aprs_toast_fail">APRS: laporan gagal - %1$s</string>
|
||||
<string name="aprs_toast_not_configured">APRS belum dikonfigurasi - isi panggilan dulu</string>
|
||||
<string name="prefs_aprs_last_report">Laporan terakhir: %1$s %2$s</string>
|
||||
<string name="prefs_aprs_last_ok">OK</string>
|
||||
<string name="prefs_aprs_last_fail">gagal - %1$s</string>
|
||||
<string name="nav_more">Lainnya</string>
|
||||
<string name="nav_log">Log</string>
|
||||
<string name="nav_amsat">AMSAT</string>
|
||||
|
||||
@@ -38,6 +38,10 @@
|
||||
<string name="prefs_aprs_calc_passcode">Hitung kode sandi</string>
|
||||
<string name="aprs_toast_ok">APRS: laporan terkirim</string>
|
||||
<string name="aprs_toast_fail">APRS: laporan gagal - %1$s</string>
|
||||
<string name="aprs_toast_not_configured">APRS belum dikonfigurasi - isi panggilan dulu</string>
|
||||
<string name="prefs_aprs_last_report">Laporan terakhir: %1$s %2$s</string>
|
||||
<string name="prefs_aprs_last_ok">OK</string>
|
||||
<string name="prefs_aprs_last_fail">gagal - %1$s</string>
|
||||
<string name="nav_more">Lainnya</string>
|
||||
<string name="nav_log">Log</string>
|
||||
<string name="nav_amsat">AMSAT</string>
|
||||
|
||||
@@ -39,6 +39,10 @@
|
||||
<string name="prefs_aprs_calc_passcode">Parolayı hesapla</string>
|
||||
<string name="aprs_toast_ok">APRS: rapor gönderildi</string>
|
||||
<string name="aprs_toast_fail">APRS: rapor başarısız - %1$s</string>
|
||||
<string name="aprs_toast_not_configured">APRS yapılandırılmadı - önce çağrı girin</string>
|
||||
<string name="prefs_aprs_last_report">Son rapor: %1$s %2$s</string>
|
||||
<string name="prefs_aprs_last_ok">OK</string>
|
||||
<string name="prefs_aprs_last_fail">başarısız - %1$s</string>
|
||||
<string name="nav_more">Daha Fazla</string>
|
||||
<string name="nav_log">Günlük</string>
|
||||
<string name="nav_amsat">AMSAT</string>
|
||||
|
||||
@@ -40,6 +40,10 @@
|
||||
<string name="prefs_aprs_calc_passcode">计算验证码</string>
|
||||
<string name="aprs_toast_ok">APRS: 上报成功</string>
|
||||
<string name="aprs_toast_fail">APRS: 上报失败 - %1$s</string>
|
||||
<string name="aprs_toast_not_configured">APRS 未配置,请先填呼号</string>
|
||||
<string name="prefs_aprs_last_report">上次上报: %1$s %2$s</string>
|
||||
<string name="prefs_aprs_last_ok">成功</string>
|
||||
<string name="prefs_aprs_last_fail">失败 - %1$s</string>
|
||||
<string name="nav_more">更多</string>
|
||||
<string name="nav_log">日志</string>
|
||||
<string name="nav_amsat">AMSAT</string>
|
||||
|
||||
@@ -41,6 +41,10 @@
|
||||
<string name="prefs_aprs_calc_passcode">Compute passcode</string>
|
||||
<string name="aprs_toast_ok">APRS: report sent OK</string>
|
||||
<string name="aprs_toast_fail">APRS: report failed - %1$s</string>
|
||||
<string name="aprs_toast_not_configured">APRS not configured - set callsign first</string>
|
||||
<string name="prefs_aprs_last_report">Last report: %1$s %2$s</string>
|
||||
<string name="prefs_aprs_last_ok">OK</string>
|
||||
<string name="prefs_aprs_last_fail">failed - %1$s</string>
|
||||
<string name="nav_more">More</string>
|
||||
<string name="nav_log">Log</string>
|
||||
<string name="nav_amsat">AMSAT</string>
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user