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,