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.
This commit is contained in:
mckero committed 2026-08-05 03:06:09 +00:00
1 parent 920af7bde2
commit 012ea1eeb6
9 files changed
+85 -1

No files matched your search

@@ -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()
@@ -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() }
@@ -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 包(一行) */
@@ -35,6 +35,9 @@
<string name="aprs_notif_connected">Terhubung ke APRS-IS</string>
<string name="aprs_notif_error">Kesalahan koneksi</string>
<string name="aprs_notif_stop">Berhenti</string>
<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="nav_more">Lainnya</string>
<string name="nav_log">Log</string>
<string name="nav_amsat">AMSAT</string>
@@ -35,6 +35,9 @@
<string name="aprs_notif_connected">Terhubung ke APRS-IS</string>
<string name="aprs_notif_error">Kesalahan koneksi</string>
<string name="aprs_notif_stop">Berhenti</string>
<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="nav_more">Lainnya</string>
<string name="nav_log">Log</string>
<string name="nav_amsat">AMSAT</string>
@@ -36,6 +36,9 @@
<string name="aprs_notif_connected">APRS-IS bağlı</string>
<string name="aprs_notif_error">Bağlantı hatası</string>
<string name="aprs_notif_stop">Durdur</string>
<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="nav_more">Daha Fazla</string>
<string name="nav_log">Günlük</string>
<string name="nav_amsat">AMSAT</string>
@@ -37,6 +37,9 @@
<string name="aprs_notif_connected">已连接 APRS-IS</string>
<string name="aprs_notif_error">连接错误</string>
<string name="aprs_notif_stop">停止</string>
<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="nav_more">更多</string>
<string name="nav_log">日志</string>
<string name="nav_amsat">AMSAT</string>
@@ -38,6 +38,9 @@
<string name="aprs_notif_connected">Connected to APRS-IS</string>
<string name="aprs_notif_error">Connection error</string>
<string name="aprs_notif_stop">Stop</string>
<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="nav_more">More</string>
<string name="nav_log">Log</string>
<string name="nav_amsat">AMSAT</string>
@@ -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,