From 012ea1eeb6b156e1faaaaa4526c47a16a59c89d2 Mon Sep 17 00:00:00 2001 From: QIU Date: Wed, 5 Aug 2026 03:06:09 +0000 Subject: [PATCH] fix(aprs): passcode compute button, crash log, notif permission, login verify User feedback round 2 (4.5.4): no notification shown, report not sending, no error visibility on crash. Diagnosis: APRS-IS port 14580 reachable (verified with real login test), 24580 SSL refused; login format OK. Fixes: - Settings dialog: "Compute passcode" button - fills passcode from callsign via the ported 0x73E2 algorithm (user can see the result) - Global crash handler in MainApplication: stack trace appended to files/crash_log.txt so crashes are diagnosable (user: no crash logs were available before) - POST_NOTIFICATIONS runtime permission requested when enabling APRS (Android 13+ otherwise silently hides the service notification) - AprsIsClient: read the login response (aprsc "# logresp ... unverified"/"Invalid") and surface the server message as the error - AprsForegroundService: Toast on manual report result (OK / failure with server reason) - Strings EN/ZH/TR/IN/ID +3 keys Verified: compileDebugKotlin all modules BUILD SUCCESSFUL, check_strings 9 files OK. --- .../look4sat/AprsForegroundService.kt | 14 +++++++++- .../com/rtbishop/look4sat/MainApplication.kt | 19 +++++++++++++ .../look4sat/core/data/aprs/AprsIsClient.kt | 11 ++++++++ .../src/main/res/values-id/strings.xml | 3 +++ .../src/main/res/values-in/strings.xml | 3 +++ .../src/main/res/values-tr/strings.xml | 3 +++ .../src/main/res/values-zh/strings.xml | 3 +++ .../src/main/res/values/strings.xml | 3 +++ .../look4sat/feature/settings/AprsCard.kt | 27 +++++++++++++++++++ 9 files changed, 85 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/rtbishop/look4sat/AprsForegroundService.kt b/app/src/main/java/com/rtbishop/look4sat/AprsForegroundService.kt index 087a604d..b8a29424 100644 --- a/app/src/main/java/com/rtbishop/look4sat/AprsForegroundService.kt +++ b/app/src/main/java/com/rtbishop/look4sat/AprsForegroundService.kt @@ -8,6 +8,7 @@ import android.app.Service import android.content.Context import android.content.Intent import android.content.SharedPreferences +import android.widget.Toast import android.content.pm.ServiceInfo import android.os.Build import android.os.IBinder @@ -66,7 +67,18 @@ class AprsForegroundService : Service() { configProvider = { AprsStore.loadConfig(this) }, positionProvider = { lastKnownPosition() }, onState = { lastState = it }, - onReport = { updateNotification(cfg) } + onReport = { report -> + 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() } + } + } ) reporter = rep rep.start() diff --git a/app/src/main/java/com/rtbishop/look4sat/MainApplication.kt b/app/src/main/java/com/rtbishop/look4sat/MainApplication.kt index 58fc429c..6ce1fb93 100644 --- a/app/src/main/java/com/rtbishop/look4sat/MainApplication.kt +++ b/app/src/main/java/com/rtbishop/look4sat/MainApplication.kt @@ -30,10 +30,29 @@ class MainApplication : Application(), IContainerProvider { private lateinit var container: IMainContainer + /** 全局崩溃捕获:堆栈写入 files/crash_log.txt,重启后可查看(用户要求错误报告) */ + private fun installCrashHandler() { + val defaultHandler = Thread.getDefaultUncaughtExceptionHandler() + Thread.setDefaultUncaughtExceptionHandler { thread, throwable -> + runCatching { + val log = StringBuilder() + log.append("=== Crash ${System.currentTimeMillis()} ===\n") + log.append("Thread: ").append(thread.name).append("\n") + val sw = java.io.StringWriter() + throwable.printStackTrace(java.io.PrintWriter(sw)) + log.append(sw.toString()).append("\n") + val file = java.io.File(filesDir, "crash_log.txt") + file.appendText(log.toString()) + } + defaultHandler?.uncaughtException(thread, throwable) + } + } + override fun getMainContainer(): IMainContainer = container override fun onCreate() { super.onCreate() + installCrashHandler() container = MainContainer(this) // trigger automatic update every 48 hours container.appScope.launch { checkAutoUpdate() } 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 81971c94..eddababb 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 @@ -46,6 +46,17 @@ class AprsIsClient( // 登录行 val login = AprsPacket.formatLogin(callsign, ssid, passcode, version) + filter writer?.println(login) + // 读登录响应(aprsc 返回 # logresp ... verified/unverified) + runCatching { + s.soTimeout = 8000 + val resp = reader?.readLine() + if (resp != null && (resp.contains("Invalid", ignoreCase = true) || + resp.contains("unverified", ignoreCase = true))) { + throw IllegalArgumentException(resp.trim()) + } + // 恢复超时 + s.soTimeout = timeoutSec * 1000 + } } /** 发送一个 APRS 包(一行) */ diff --git a/core/presentation/src/main/res/values-id/strings.xml b/core/presentation/src/main/res/values-id/strings.xml index fe8396d3..619c3554 100644 --- a/core/presentation/src/main/res/values-id/strings.xml +++ b/core/presentation/src/main/res/values-id/strings.xml @@ -35,6 +35,9 @@ Terhubung ke APRS-IS Kesalahan koneksi Berhenti + Hitung kode sandi + APRS: laporan terkirim + APRS: laporan 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 9ac8d4e2..da770e8f 100644 --- a/core/presentation/src/main/res/values-in/strings.xml +++ b/core/presentation/src/main/res/values-in/strings.xml @@ -35,6 +35,9 @@ Terhubung ke APRS-IS Kesalahan koneksi Berhenti + Hitung kode sandi + APRS: laporan terkirim + APRS: laporan 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 7eccd32a..3f7d1956 100644 --- a/core/presentation/src/main/res/values-tr/strings.xml +++ b/core/presentation/src/main/res/values-tr/strings.xml @@ -36,6 +36,9 @@ APRS-IS bağlı Bağlantı hatası Durdur + Parolayı hesapla + APRS: rapor gönderildi + APRS: rapor 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 f5429f13..2c8d368a 100644 --- a/core/presentation/src/main/res/values-zh/strings.xml +++ b/core/presentation/src/main/res/values-zh/strings.xml @@ -37,6 +37,9 @@ 已连接 APRS-IS 连接错误 停止 + 计算验证码 + APRS: 上报成功 + APRS: 上报失败 - %1$s 更多 日志 AMSAT diff --git a/core/presentation/src/main/res/values/strings.xml b/core/presentation/src/main/res/values/strings.xml index d063bf89..df8eb982 100644 --- a/core/presentation/src/main/res/values/strings.xml +++ b/core/presentation/src/main/res/values/strings.xml @@ -38,6 +38,9 @@ Connected to APRS-IS Connection error Stop + Compute passcode + APRS: report sent OK + APRS: report 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 ed5e91f0..3ccec9fa 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 @@ -1,6 +1,8 @@ package com.rtbishop.look4sat.feature.settings import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row @@ -20,7 +22,9 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.activity.compose.rememberLauncherForActivityResult import androidx.compose.runtime.setValue +import androidx.activity.result.contract.ActivityResultContracts import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -29,6 +33,7 @@ import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import com.rtbishop.look4sat.core.data.aprs.AprsConfig +import com.rtbishop.look4sat.core.domain.aprs.AprsPacket import com.rtbishop.look4sat.core.data.aprs.AprsStore import com.rtbishop.look4sat.core.presentation.CardButton import com.rtbishop.look4sat.core.presentation.R @@ -39,6 +44,17 @@ fun AprsCard() { val context = LocalContext.current var config by remember { mutableStateOf(AprsStore.loadConfig(context)) } var showDialog by remember { mutableStateOf(false) } + val notifPermissionLauncher = rememberLauncherForActivityResult( + ActivityResultContracts.RequestPermission() + ) { } + fun requestNotifPermissionIfNeeded() { + if (Build.VERSION.SDK_INT >= 33 && + context.checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) != + PackageManager.PERMISSION_GRANTED + ) { + notifPermissionLauncher.launch(android.Manifest.permission.POST_NOTIFICATIONS) + } + } ElevatedCard(modifier = Modifier.fillMaxWidth()) { Column( @@ -66,6 +82,7 @@ fun AprsCard() { AprsSwitchRow(R.string.prefs_aprs_enable, config.enabled) { enabled -> config = config.copy(enabled = enabled) AprsStore.saveConfig(context, config) + if (enabled) requestNotifPermissionIfNeeded() val intent = Intent() .setClassName(context.packageName, AprsStore.SERVICE_CLASS) .setAction(if (enabled) AprsStore.ACTION_START else AprsStore.ACTION_STOP) @@ -163,6 +180,16 @@ private fun AprsSettingsDialog( singleLine = true, textStyle = textStyle ) + // 计算验证码按钮(用户要求:让用户算出结果) + TextButton( + onClick = { + val call = callsign.trim().uppercase() + if (call.isNotBlank()) { + passcode = AprsPacket.passcode(call).toString() + } + }, + enabled = callsign.trim().isNotBlank() + ) { Text(stringResource(id = R.string.prefs_aprs_calc_passcode)) } Row(horizontalArrangement = Arrangement.spacedBy(8.dp)) { OutlinedTextField( value = ssid,