Commit Graph
756 Commits
Author SHA1 Message Date
mckero 6fc2f560b7 fix(nav): resolve the menu layout in core:domain so page order actually applies
用户报告"把 AMSAT 从更多菜单移到主菜单没生效"。排查后发现这是两个互相
掩盖的 bug, 其中一个会让用户永久无法进入设置页。

## Bug 1 (致命): 移任意页进主菜单 -> 设置入口从所有菜单消失

对"主菜单上限 5"的定义两处不一致:
- SettingsScreen.kt:892 判断 mainItems.filter { it != "Settings" }.size >= 5,
  上限是【不含 Settings 的 5 个】, 于是认为 [Satellites,Passes,Radar,Map]
  还有空位, 直接追加 -> 共 6 项
- MainScreen.kt:165 的 .take(5) 上限是【含 Settings 的前 5 个】, 而 Settings
  排在最后 -> 被截断丢弃

结果 Settings 既不在底栏也不在更多菜单(它不在 subMenuOrder 里), 用户再也
进不去设置页, 无法自行改回, 只能清数据或重装。

## Bug 2 (用户报告): AMSAT / WavelogLog 移到主菜单被静默撤销

MainScreen.kt:161-163 给老用户补新页面的迁移逻辑【无条件执行】:

    .let { list -> if ("AMSAT" in list) list else list + "AMSAT" }

用户把 AMSAT 移出子菜单后 subMenuOrder 里没有它, 这段又加回去, 第 165 行
filter { it.screenId !in subOrder } 于是永远过滤掉 AMSAT。

副作用: 这个 bug 恰好把主菜单拉回 5 项内, 反而掩盖了 Bug 1 —— 所以用户只
看到"AMSAT 移不过去", 没触发"设置锁死"。

## Bug 3: 溢出页面凭空消失而非落入更多菜单

.take(5) 直接丢弃超出的页面, 它们既不在底栏也不在更多菜单。

## Bug 4: 设置页展示顺序与底栏实际顺序不一致 (WYSIWYG 失效)

两处各自实现排序: 设置页不做 take(5)、用 sortedWith 把 Settings 钉最后;
MainScreen 做 take(5)、Settings 位置由 screenOrder 决定。

## Bug 5: 更多菜单里 AMSAT / Roaming 当前页不高亮

MoreMenuPopup.kt:69-79 的 when (currentKey) 缺 AmSat 与 Roaming 分支,
MainScreen.kt:188-198 缺 Roaming 分支, 靠 else -> false 兜底, 新增页面时
不会有编译错误提醒。Screen 子类都是 data object, 直接用 currentKey == screen
即可, 新增页面自动生效。

## Bug 6: 设置页主菜单区不过滤 hiddenScreens

MainScreen 会过滤隐藏页, 设置页不过滤, 于是隐藏的页面仍占据设置页的位置且
"移出主菜单"按钮可点, 与底栏实际情况不符。

## 改动

新增 core/domain/navigation/MenuLayout.kt (纯 Kotlin, 为 KMP 就绪) 作为菜单
布局的唯一入口:
- resolve(): 持久化偏好 -> 底栏 + 更多菜单。先给 Settings 预留名额再截断,
  溢出页面落入更多菜单而非丢弃; 两个列表都没提到的页面按默认归位(升级不丢页)
- moveToMain() / moveToMore(): 移动语义集中一处, 拒绝把 Settings 移出

MainScreen 与 SettingsScreen 改为共用它, 删除双份实现与无条件迁移逻辑。

## 死代码清理 (每项已 grep 全仓库确认零引用)

- SettingsAction.ResetScreenOrder: 无任何派发点, 仅定义与 when 分支
- UiSettingsCard 的 onReorder 参数: 函数体内两个 DragOrderList 的 onReorder
  实参都调 onUpdateMenu, 从未调用该参数
- SettingsAction.ReorderScreens: 唯一引用是上面那个死参数
- MainScreen 的 navigateToRadar 参数: 函数体内唯一出现是注释掉的一行
- Navigation.kt 的 defaultScreenOrder / defaultSubMenuOrder: 迁移后零引用,
  默认值已在 MenuLayout 内

保留 entry<RadarDestination> 分支不动 —— RadarDestination NavKey 被
PassDetailsMatcher deeplink 使用。

## 验证

- MenuLayoutTest 13 个新测试全绿, 每个对应上面一个 bug 场景
- :core:domain:test 全量 100 个测试 0 失败 0 错误
  (DataParser 19 / Doppler 17 / Qth 8 / Transponder 7 / CwCtc 7 /
   CwDeepBuffer 13 / CwGolden 3 / CwSpectrogram 8 / MenuLayout 13 /
   WaveLogApi 5)
- :core:domain:compileKotlin + :core:presentation + :app +
  :feature:settings compileDebugKotlin => BUILD SUCCESSFUL
- 用 Python 复刻新规则重跑当初失败的全部场景: 5 个页面逐一移入主菜单,
  设置入口全部保住、页面无丢失; 隐藏页 + 移动组合无页面丢失; 幂等性通过
2026-08-13 16:10:41 +00:00
mckero 5d89190348 ui: slim the More menu and drop dead controls from the CW page
用户反馈三处 UI 问题, 一并处理。

1. 更多菜单风格不符 + 遮盖感重
   - 去掉全屏 scrim 遮罩(0.35 alpha 压暗整页), 改为透明点击层, 点外部仍可关闭
   - Card 限宽 232dp 靠右下角, 从"全宽卡片"变成竖长条
   - 容器色 surfaceContainerHigh -> surfaceContainer, 与导航栏一致; 加 1dp 细边框
   - 动画从全屏 expandVertically + spring 弹跳改为右下角原点 150ms scaleIn
   - Card 加 clickable(enabled=false) 吞掉点击, 避免点卡片空白区误触关闭

2. CW 页左上角退出键点击无效 -> 删除
   根因: CwDecodeScreen(navigateUp: () -> Unit = {}) 是默认空实现, 而
   MainScreen 的 entry<Screen.CwDecode> 调用 CwDecodeScreen() 从未传入
   navigateUp, 所以点击必然无反应。按 AGENTS.md 无死代码原则删除按钮 +
   navigateUp 参数 + cw_back 字符串(五语)。

3. CW 页"正在监听"状态行在停止解码后仍显示 -> 删除
   estimatedPitch 在暂停后保留上次值, 状态行不会消失, 属误导。删除该 Text
   后 estimatedPitch / lastInferenceMs 两个 collector 成为死代码, 一并清理;
   cw_status_listening / cw_status_tone 字符串(五语)同步删除。
   signalStrength(瀑布图) 与 errorMessage(错误提示) 仍在用, 保留。

验证:
- :app:compileDebugKotlin + :feature:cw:compileDebugKotlin => BUILD SUCCESSFUL
- :core:domain:test => 31 个 CW 测试全绿 (7+13+3+8)
2026-08-13 14:48:08 +00:00
mckero 3e25e1aa2f fix(cw): keep ai.onnxruntime.** through R8 to stop native crash
根因定位(确定性): release 构建开启 R8 混淆 (convention 插件的
PluginSetupUtils 设 isMinifyEnabled=true), 但 onnxruntime-android 的 AAR
不含 proguard consumer rules (仅 aar-metadata.properties)。于是 R8 把
ai.onnxruntime.* 的类重命名/裁剪, 而 ONNX Runtime 的 Java 绑定走 JNI、
native 侧按原始类名/方法名解析 —— 类名对不上即 native 崩溃, 不产生
任何 Java 堆栈, 正好解释"闪退回桌面、无任何日志"。

此前的所有内存/线程修复都无效, 因为崩溃点根本不在 Java 层。

修复:
1. app/proguard-rules.pro 新增 (ONNX Runtime 官方文档要求):
   -keep class ai.onnxruntime.** { *; }
   -dontwarn ai.onnxruntime.**
2. app/build.gradle.kts 的 release buildType 引用该规则文件。
3. onnxruntime 1.28.0 -> 1.25.1: 1.28.0 为 2026-07-25 发布(仅三周),
   1.25.1 更成熟; 同时排除 SIGILL 类骁龙 bug (该 bug 在 1.24.4 修复,
   Android AAR 对应 1.25.0+)。

验证: 服务器完整管线 (真实20s音频->频谱->ONNX->CTC) 解码逐字符正确,
与手机端同一份代码逻辑。R8 keep 规则为官方文档明确要求项。
2026-08-13 03:46:14 +00:00
mckero 5f297f9233 fix(cw): stop the waterfall crashing and cut APK size by 87MB
三个真机实测暴露的问题:

1. 闪退 (给权限后 3-4 秒必崩, 且注入日志抓不到堆栈)
   CwWaterfallState 用 mutableIntStateOf 记录重绘版本号, 却从音频采集线程
   写入。Compose 快照状态只能在合成线程修改, 从后台线程写会在运行时崩溃 ——
   崩在 Compose 内部, 所以业务类的日志注入抓不到。
   改用 MutableStateFlow (本身线程安全), Canvas 侧 collectAsState 读取。

2. 同一状态的跨线程数据竞争
   pushSamples 在采集线程写 rows/pending, snapshot() 在绘制线程读, 而
   ArrayDeque 非线程安全 —— 并发 removeFirst()/toList() 会抛
   ConcurrentModificationException 或 IndexOutOfBoundsException。
   两侧统一加锁; FFT 计算放在锁外, 只有队列追加持锁。

3. APK 从 8.4MB 暴涨到 135MB
   onnxruntime-android 的 AAR 自带 4 个架构原生库: arm64 28M + armv7 20M +
   x86 33M + x86_64 34M = 115MB。上次提交移除 abiFilters 时把 x86 系列也
   打包了进去 (仅模拟器需要)。
   重新加上 abiFilters, 保留 arm64-v8a + armeabi-v7a 两个真机 ABI,
   预计降至约 43MB。注意这与上次"恢复 64 位"不冲突: 那次删的是只留
   armeabi-v7a 的限制, 这次是排除 x86 系列。

另外两处加固:
- CwDeepDecoder 的模型加载从 init{} 移入惰性 ensureLoaded(): 加载会触发
  ONNX Runtime 原生库装载, 失败时抛 UnsatisfiedLinkError; 在构造函数中抛出
  会连带崩掉创建它的 composable, try-catch 也救不回来。移到首次使用时执行,
  失败经 errorMessage 上报给 UI。
- ONNX 会话限制 intraOp 线程数为 (核数-1) 且上限 4, 给音频采集和 UI 留出
  余量, 默认行为会铺满所有核心。

验证:
./gradlew :feature:cw:compileDebugKotlin :core:data:compileDebugKotlin => 通过
./gradlew :core:domain:test => 79 个测试全绿
2026-08-12 13:55:03 +00:00
mckero 86ab1b26b7 feat(cw): vendor DeepCW ONNX model with full AGPL-3.0 compliance
背景:
DeepCW (e04/deepcw-engine) 是基于 CTC 的神经网络 CW 解码模型。本地实测
弱信号解码率显著优于传统 DSP 方案: SNR<0dB 时 CER 16.0% vs ggmorse 52.6%;
SNR>=0dB 时两者均接近 0%。模型 400-1200Hz 固定频窗自带定频, 无需频谱峰值
跟踪与噪声门。

改动:
- assets/deepcw/model.onnx      15,139,839 字节, 原样收录未作修改
- assets/deepcw/model.onnx.json 模型元数据 (采样率 3200, FFT 256, hop 48,
  65 个频段, log1p 归一化, 42 类含 CTC blank)
- licenses/DeepCW-AGPL-3.0.txt  AGPL-3.0 许可证全文
- licenses/NOTICE.md            来源仓库/作者/取用 commit/SHA-256/
  许可证兼容性说明/复现步骤
- app/build.gradle.kts: androidResources.noCompress += "onnx"

关于 noCompress:
ONNX Runtime 通过 mmap 直接读取 assets, 压缩后无法内存映射, createSession
会失败。该配置只在打包 APK 的 app 模块生效, 在 feature 库模块声明无效。

许可证:
Look4Sat 为 GPL-3.0-or-later, DeepCW 模型为 AGPL-3.0-only。GPL v3 第 13 条
明确允许两者合并分发。推理完全在本地设备进行, 不通过网络向远程用户提供
模型功能, 故 AGPL 第 13 条的网络源码提供义务不被触发。本仓库公开, 完整
对应源码提供义务已满足。

上游取用 commit: 8e264d243bbd4467bd19f3f28292219405b47e0e
模型 SHA-256: ef120799457bca042d4690944f0faf93268eb4654e7f50f28784ad63bdc1fe02
2026-08-12 11:47:02 +00:00
mckero de6c964136 build!: drop armeabi-v7a abiFilters, restore 64-bit builds
背景:
abiFilters=armeabi-v7a 的唯一成因是 Morse Expert 的 32 位
libnativedecoderjni.so, 该 .so 已在上一提交删除。此前整个应用被迫以
32 位运行: 64 位设备走兼容层、寻址受限, 且不满足应用市场的 64 位要求。
原注释称"用户设备为 32 位软件"已与实际不符 (测试机为 64 位)。

改动:
- app/build.gradle.kts: 移除 defaultConfig.ndk.abiFilters 及相关注释
- feature/cw/build.gradle.kts: 移除 abiFilters; 移除 constraintlayout 依赖
  (仅被已删除的 activity_main.xml 使用); 移除 UTF-8 JavaCompile 配置
  (模块已无 Java 源码)
- feature/cw 改为依赖 core:domain + core:data (DeepCW 前后处理落在 core)

验证:
grep -rn 'abiFilters|jniLibs' --include=*.kts . (排除 build/) => 无匹配
APK ABI 覆盖将在 CI 产物中验证 (预期含 arm64-v8a)
2026-08-12 11:44:54 +00:00
mckero 4c62b2dfb2 Revert "feat(nav): 4.5.6 AMSAT to bottom bar, Radar to More menu, tablet rail"
This reverts commit d8e1ee3266.
2026-08-05 10:07:45 +00:00
mckero d8e1ee3266 feat(nav): 4.5.6 AMSAT to bottom bar, Radar to More menu, tablet rail
Per upstream author feedback (PR #233) and tablet UX report:
- Bottom bar keeps max 5 primary destinations: Satellites/Passes/
  AMSAT/Map/Settings; Radar moves to the More menu (still reachable
  from Passes via item click)
- Legacy migration: persisted orders are rewritten in memory
  (main menu drops Radar + appends AMSAT; More menu drops AMSAT +
  appends Radar) so existing installs get the new layout
- Wide screens / tablets (width breakpoint) now use the side
  navigation rail instead of the bottom bar - fixes the wasted
  bottom strip ("big chin") in landscape/tablet layouts
- New AMSAT tab icon: MDI satellite-variant (Apache 2.0,
  https://pictogrammers.com)
- What's new updated in 5 locales; version 4.5.6 (457)
2026-08-05 09:46:54 +00:00
mckero c9cab8457d chore(i18n): translate all code comments to English
All Chinese comments (//, /* */, KDoc) across core/app/feature/build-
logic translated to English (550 lines, 73 files after FT8 rollback).
Code logic untouched - comment text only. Verified: all modules
compileDebugKotlin BUILD SUCCESSFUL.
2026-08-05 07:59:35 +00:00
mckero 19926c58ac Revert "feat(ft8): 4.5.5 integrate FT8CN 0.93 (full port, original UI)"
This reverts commit 8a2b0a9aa3.
2026-08-05 07:47:18 +00:00
mckero 8a2b0a9aa3 feat(ft8): 4.5.5 integrate FT8CN 0.93 (full port, original UI)
Full FT8 mode integration (user chose scope C: complete port, keep
original visual style):

- feature/ft8 module: 213 Java classes from FT8CN 0.93 source
  (com.bg7yoz.ft8cn namespace preserved so code needs zero changes),
  DataBinding enabled, BuildConfig fields (APPLICATION_ID/VERSION_NAME/
  apkBuildTime) restored for AGP9
- libft8cn.so from the user's FT8CN_0.93 APK (armeabi-v7a, 1.53MB;
  differs from repo copy, APK version used)
- 237 res files + 58 assets + 4 local libs (MPAndroidChart spectrum,
  commons-net, nanohttpd, osmdroid map) copied verbatim
- Proguard keep rules for JNI classes (RegisterNatives)
- Entry: More menu "FT8" (default sub menu, after AMSAT) launches
  MainActivity via Intent (external activity, LAUNCHER filter removed
  to avoid double launcher)
- Manifest merged via module (RECORD_AUDIO/BT/location perms)
- Version 4.5.5/455 + What's new 5 languages (REPLACED per rule)
Verified: :feature:ft8:compileDebugJavaWithJavac + :app:compileDebugKotlin
BUILD SUCCESSFUL; check_strings 9 files OK.
2026-08-05 04:49:35 +00:00
mckero c9d87be289 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.
2026-08-05 03:29:36 +00:00
mckero 012ea1eeb6 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.
2026-08-05 03:06:09 +00:00
mckero 920af7bde2 fix(aprs): crash when enabling APRS with callsign set (foreground service type)
Root cause: user reported crash on enabling APRS with a callsign set
(empty callsign worked because the service stops early and never
reaches startForeground). Android 14+ requires the service to declare
foregroundServiceType when startForeground passes a type; the service
had none -> process died on toggle.

- Manifest: add android:foregroundServiceType="dataSync"
- AprsCard: use startForegroundService() for start/report actions
  (Android 8+ standard for foreground services)
- AprsForegroundService: try-catch around startForeground, fallback
  stopSelf instead of killing the process
- AprsReporter: passcode "-1" (APRSdroid "no auth" convention) also
  auto-computes from callsign
- Merged upstream 418a05e3 (zh wording fix, no conflicts)
Verified: compileDebugKotlin all modules BUILD SUCCESSFUL,
check_strings 9 files OK.
2026-08-05 02:50:40 +00:00
mckero 44ae5cef5d fix(aprs): settings crash from format arg type mismatch + service class name
- prefs_aprs_summary uses %2$d but AprsCard passed port.toString()
  (String) -> String.format threw IllegalFormatConversionException
  on every Settings screen open (crash). Pass Int now.
- Manifest service name was ".AprsForegroundService" (resolves under
  applicationId) but the class lives in com.rtbishop.look4sat.app ->
  ClassNotFoundException when toggling. Use full class name.
Verified: :feature:settings:compileDebugKotlin + :app:compileDebugKotlin
BUILD SUCCESSFUL.
2026-08-05 02:39:14 +00:00
mckero 518d9a4d22 feat(aprs): 4.5.4 APRS-IS network reporting (ported from APRSdroid 1.6.3d)
Core logic extracted from APRSdroid 1.6.3d (jadx reverse-engineered,
verified against APRS spec):
- core/domain/aprs/AprsPacket.kt: passcode algorithm (0x73E2 XOR),
  login line, uncompressed/compressed position encoding (ab0oo)
- core/data/aprs/AprsIsClient.kt: APRS-IS TCP client (login + packet
  lines, 30s reconnect strategy)
- core/data/aprs/AprsReporter.kt: periodic reporting scheduler
  (default 5 min, manual trigger support)
- core/data/aprs/AprsStore.kt: SharedPreferences persistence
  (fill once, saved)
- app/AprsForegroundService.kt: foreground service with notification
  (keeps APRS alive across pages), START_STICKY, last-known position
  provider
- feature/settings/AprsCard.kt: APRS card between DataCard and
  OutputCard, gear button opens settings dialog (server/port/
  callsign/ssid/passcode/interval/status/symbol), manual report button
- Manifest: service registration + FOREGROUND_SERVICE(_DATA_SYNC) +
  POST_NOTIFICATIONS
- Strings in EN/ZH/TR/IN/ID (23 keys x 5)
- Version 4.5.4/454 + What's new 5 languages
2026-08-05 02:07:11 +00:00
mckero f7a3dbaa2d fix(i18n): use AGP9 localeFilters for language keep-list
resourceConfigurations is rejected by AGP 9.3 ("When localeFilters
are specified, resourceConfigurations cannot include locale
qualifiers"). Switch to androidResources.localeFilters.
Verified: :app:mergeReleaseResources + :app:processReleaseResources
BUILD SUCCESSFUL.
2026-08-04 16:34:39 +00:00
mckero c525f3a11f fix(i18n): force-keep in/id locale configs via resourceConfigurations
Root cause of Indonesian not matching: release build has
isShrinkResources=true and R8 was dropping the Indonesian resource
configs entirely - aapt dump badging showed locales only
'--_--' es ru si tr uk zh (no in, no id). values-in/values-id were
merged at merge time but stripped before packaging.

Fix: explicit resourceConfigurations in app/build.gradle.kts keeping
en zh tr in id es ru si uk. Verified: :app:mergeReleaseResources
BUILD SUCCESSFUL.
2026-08-04 16:28:39 +00:00
mckero f7402a3531 feat(status): 4.5.3 AMSAT satellite status page + radar log pass divider
New feature/status module: fetches https://amsat.org/status/ and
renders a live status grid in the official site colors:
- Parser (AmSatParser): 47 satellites x 6 days x 12 two-hour slots,
  official colors (blue=Active, orange=TLM/Beacon, pink=Not Heard,
  deep-orange=Conflicting, gray=none); 598+ report details extracted
  from inline JS tooltips (callsign/date/time/grid)
- Three-level viz: color grid -> report count -> tap day cell opens
  report list dialog
- Manual refresh with spin animation + last-updated timestamp +
  legend row; loading/error states
- New "AMSAT" entry in the More menu (Screen.AmSat), integrated with
  page-order / hide-page settings (SettingsScreen screens list,
  defaultSubMenuOrder, allNavItems, migration for existing users)
- AmSatRepository via IRemoteSource.getStatusHtml() (UA header);
  shared remoteSource promoted to a lazy class property in MainContainer

Radar page Log tab: local entries now grouped by pass session with a
thick divider + satellite label between groups (matches the log page).

What's new updated in EN/ZH/TR/IN/ID. Version bumped to 4.5.3/453.
Not released (user gates all releases).
2026-08-04 15:57:41 +00:00
mckero ab1a0c0891 fix(nav,radar,wavelog): 4.5.2 third overwrite - log page entry visible, freq format 3 decimals, direct freq extraction
User-test fixes (3rd overwrite, version stays 4.5.2/452):

1. Log page was invisible everywhere outside the radar tab:
   - allNavItems in MainScreen.kt was missing Screen.WavelogLog ->
     not in bottom nav, not in More menu
   - SettingsScreen UI-order lists were missing the WavelogLog row ->
     not in page order settings either
   Now the Log page appears in the More menu (default sub menu tail,
   migration appends it) AND in UI settings page-order lists.
2. Frequency display: formatFrequency was MHz.kHz.Hz (145.900.000);
   now MHz.kHz (145.900) per request. Applies to transceiver panel
   and Log page alike (shared formatter).
3. Log tab frequency: now extracts the exact numbers shown in the
   transceiver panel (txBaseFrequencyHz for TX, uplinkLow/uplinkHigh
   range for linear) - no re-computation, no extra decimals. Upload
   uses the same tuned frequency (Hz precision kept internally).
4. Log page (More menu) table: date+time column (MM-dd HH:mm),
   row separators (table lines), uploaded checkmark kept.

Verified: check_strings OK (8 files); :app:compileDebugKotlin
BUILD SUCCESSFUL (includes upstream merge 1a552417).
2026-08-04 12:07:45 +00:00
mckero 5351afe508 fix(wavelog,radar): 4.5.2 user-test fixes (404 upload, swipe delete, transponder picker, log page)
Background: user tested 4.5.2 and reported 4 issues. Same-version
overwrite per user (4.5.2 exists solely for the logbook system).

Fixes:
1. Upload 404 — root cause: user server URL ending in /index.php was
   concatenated again (/index.php/index.php/api/v2/...) -> 404. Now:
   - normalizeUrl strips trailing /index.php
   - every request tries the index.php path first, falls back to the
     rewritten path on 404
   - 409 conflict (duplicate QSO) counts as success (moves out of queue)
   - failure messages include the actual URL + HTTP code for debugging
2. Swipe-to-delete was dead: rowWidth was never measured (0) so the
   75% threshold was 0 and the drag was clamped to 0. Now measured via
   onSizeChanged + smooth spring/tween snap-back animation.
3. Transponder picker: long card list replaced with an
   ExposedDropdownMenuBox dropdown (scrollable menu, pick one).
4. New Log page under the More menu: table view (time / frequency /
   satellite / callsign / uploaded checkmark). WavelogQso gains
   uploaded flag; uploader marks instead of removing; queue keeps
   uploaded entries (500 cap). Old persisted subMenuOrder gets
   WavelogLog appended (migration).

Verified: check_strings.py OK (8 files, 452/4.5.2);
:app:compileDebugKotlin BUILD SUCCESSFUL.
2026-08-04 11:06:47 +00:00
mckero 38cf7a3569 feat(nav,settings): bottom nav 5+N collapsible menu + Indonesian locale
Background: 8 bottom-nav items squeeze long English labels on narrow
screens. 4.5.1 introduces the 5+N pattern: 5 main tabs plus a fixed
6th "More" button that pops a second-level menu (spring bounce) with
the remaining pages.

Changes:
- MainScreen: nav split into main (<=5, screenOrder-driven) + more
  (subMenuOrder); More button with popup panel + spring animation;
  BackHandler closes the menu before navigating back
- MoreMenuPopup: bottom-end card, current page highlighted, scrim
  click to dismiss
- UI Settings: page order card now has two zones (main menu, max 5,
  Settings locked last with no drag handle / more menu); move buttons
  between zones, drag-to-reorder within zones; new subMenuOrder pref
- Defaults: main = Satellites/Passes/Radar/Map/Settings,
  more = Mutual/Roaming/CwDecode; old screenOrder migrates by
  classifying pages against the default sub menu
- Hard-coded UI strings localized (Tracking/Lat/Lon/Qth/Connect/
  Track/Stop/CW permission prompts) into EN/ZH/TR
- NEW Indonesian locale (values-in, 181 strings + cw module strings)
  - user rule: every future release must update EN/ZH/TR/IN
- Version 4.5.1 (451)

Verified: check_strings.py OK (8 files, no bare apostrophes);
:app:compileDebugKotlin BUILD SUCCESSFUL locally.
2026-08-04 08:58:05 +00:00
mckero 3c5486cc5c fix(cw): restore FFT dispatch (cond_22), CW UI settings entry, default order
Decode page showed stats but empty waterfall and no decoded text:
the ported i3/d.k() FFT dispatch was broken by a jadx structure
misplacement - the runtime path (k=1 -> a5=0 -> cond_22 single-thread
FFT) was replaced by a hallucinated `throw null` else-branch while the
real cond_22 code sat in a dead else. Verified against smali
(7030-7263): j3.c.q forward FFT + post-processing loop + tail + small-
array branch now live in the a5==0 branch.

Also:
- UiSettingsCard now lists CwDecode (toggle + drag-reorder) between
  Roaming and Map
- unknown screenIds in persisted screenOrder fall back to
  defaultScreenOrder position (CwDecode lands between Roaming and Map
  for existing users instead of trailing after Settings)

Verified: :feature:cw + :feature:settings + :app compileDebugKotlin
BUILD SUCCESSFUL.
2026-08-04 05:36:48 +00:00
mckero 544515feba fix(cw): restore app label, fix R8 code stripping, force 32-bit ABI
Three release-breaking issues found in the v4.5.0 APK (app crashed on
launch, label showed "Morse Expert", dex shrank to 282KB vs 3.5MB):
1. app_name: the ported app_values.xml shipped a "Morse Expert"
   app_name string which overrode Look4Sat Pro's label during resource
   merging - removed (no other string collisions).
2. R8 stripped nearly all code: the in-app sun.misc.Unsafe/Cleaner
   stubs clashed with android.jar library classes. Moved stubs to
   com.rtbishop.look4sat.feature.cw.suncompat and updated k3.d/s/r
   imports (k3.r keeps the reflective Class.forName("sun.misc.Unsafe")
   string, which returns null on Android hidden-API limits).
3. proguard-rules.pro added (AGP 9 variant-level
   CanProduceConsumerProguardFiles): keep pas.** (JNI RegisterNatives
   resolves by class name) plus all ported CW classes.
4. app-level ndk abiFilters forced to armeabi-v7a: the ported
   libnativedecoderjni.so is v7a-only, so a multi-ABI APK would crash
   with UnsatisfiedLinkError on arm64 devices.

Verified: :feature:cw:compileDebugKotlin BUILD SUCCESSFUL.
2026-08-04 04:56:41 +00:00
mckero 8bc1f16e2c feat(cw): wire CW decoder page into navigation (between Roaming and Map)
Compose integration of the ported CW decoder engine:
- CwDecodeScreen: AndroidView embedding the ported activity_main.xml,
  lifecycle delegated to the ported MainActivity controller (onCreate ->
  onResume, onDispose -> onPause/onDestroy), RECORD_AUDIO runtime
  permission flow (with permanent-denial -> app settings), original
  options_menu actions as a top button row (pause/clear/save/record/
  settings), double-back-to-exit preserved
- CwSettingsDialog: message_type (general_text/ham_radio_qso),
  text_font_size (7-99), and the 9 color keys (bg_color/text_color/...)
  reading/writing the same prefs keys as the original app
  (getPackageName()+"_preferences"), colors sourced from I2.b tables
- Navigation: Screen.CwDecode ("CwDecode") placed between Roaming and
  Map in the default order; defaultScreenOrder updated; ic_cw morse icon;
  nav_cw strings (en/zh/tr); app depends on :feature:cw

Verified: :feature:cw:compileDebugKotlin + :app:compileDebugKotlin
BUILD SUCCESSFUL (first pass, no errors).
2026-08-04 04:06:31 +00:00
mckero 0ea9c0ede1 feat(settings): drag-to-reorder page order in UI Settings card
Users can now change the bottom navigation order (previously fixed):

- New "Page order" section under the Settings toggle in the UI
  Settings card: vertical list of all 7 pages, each with a drag
  handle (new ic_drag drawable, Material drag_indicator glyph) on
  the right.
- Drag a handle up/down: the item follows the finger with live
  swap + animateItem() placement animation; on release the order is
  persisted (OtherSettings.screenOrder, comma-separated in prefs so
  order survives; StringSet would not).
- "Reset order" button restores the default order
  (Satellites/Passes/Radar/Mutual/Roaming/Map/Settings).
- MainScreen now sorts navItems by screenOrder (empty = default,
  stable sort keeps the canonical order); hiddenScreens filtering
  unchanged; Settings entry always visible.
- New strings prefs_ui_order_title / prefs_ui_order_reset in
  en/zh/tr; What's-new updated in all three locales.

Version stays 4.4.9 (覆盖 per user). Verified: core:data tests
pass, settings + app compile.
2026-08-04 00:28:53 +00:00
mckero c101870b71 feat(settings,ui): custom-source toggles default off; new UI Settings card; bump to 4.4.9
Two changes per user request:

1. Data source toggles default OFF with legacy-URL migration
   (continuation of the 4.4.8 fix, now also in the dialog): old
   example.com placeholder URLs are replaced by the real defaults and
   the custom toggles stay off, so the online update never points at a
   dead source after upgrading.

2. New "UI Settings" card between Other Settings and Credits:
   one switch per bottom-navigation page (卫星/过境/雷达/匹配/漫游/地图/设置
   in fixed order). Turning a page off removes it from the nav bar and
   the remaining items close up automatically; order is never
   rearranged. The Settings entry is always visible (switch disabled)
   so the user can never lose access to settings. Persisted via
   OtherSettings.hiddenScreens (StringSet of Screen.screenId; screenId
   added to the Screen sealed class - simpleName is unsafe under R8).

Version bump per fork convention: 4.4.8 -> 4.4.9, versionCode 449.
What's-new updated in en/zh/tr.

Verified: core:data + core:domain tests pass, all modules compile.
2026-08-03 18:14:53 +00:00
mckero f9250ec1f1 fix(roaming): live coords from shared flow, marker by locator pair, versioned apk
Addresses three review findings:

1. Coordinates now come straight from settingsRepo.stationPosition in the screen (collectAsStateWithLifecycle) — the exact same StateFlow the Settings page shows. Previously a separate ViewModel re-derived them, and it could lag behind the Settings page (user: '设置页更新了站位但漫游页死活不更新'). With the shared source the two pages can never disagree. RoamingViewModel removed; state derivation moved to RoamingState.fromPosition().

2. Red marker placement ported faithfully from the QTH定位器 app: it is driven by the 3rd character pair of the 8-char locator (the 'ih' in OL42ih45), mapped to a 0..1 fraction (lon a=west..x=east, lat inverted a=south..x=north), then scaled to the actual center-cell size. The grid now uses the reference proportions (columns 21.4/56.2/21.4, rows 31.5/35.9/31.7) and fills the screen, so the marker lands accurately on any device.

3. Workflow now uploads a versioned APK (look4sat-<version>.apk instead of look4sat.apk).

Also: Settings 'Other' card rows got vertical spacing (Arrangement.spacedBy) so the new roaming toggle is not glued to the night-mode row.
2026-08-03 11:49:46 +00:00
mckero bfa5e5f474 feat(roaming): add Roaming page with 3x3 Maidenhead grid
Ports the QTH定位器 (com.us1pm.gridsquarelocator) location panel into Look4Sat as a new 'Roaming' page, restyled with the app's own look.

UI (top to bottom):
- Info header: GPS status dot, big 8-char locator, Lat/Lon rows with DMS + 5-decimal display, and a GPS 定位 button
- 3x3 grid panel: the current 4-char Maidenhead square (e.g. OL42) centered, surrounded by its 8 neighbors (OL33..OL51), with a red position marker in the center cell

Logic:
- QthConverter gains qthNeighbors(square) building the 3x3 grid with field/square carry at boundaries (AA00 wraps to RR99, IO91 crosses into J field), and qthToSquare(locator) extracting the 4-char square
- Verified against the decompiled app algorithm and the reference screenshot (OL42 grid matches exactly); 9 unit tests cover normal, boundary and field-wrap cases

Navigation:
- New bottom-nav item 'Roaming' between Match and Settings, with a crosshair icon
- New feature:roaming module (ViewModel + Compose screen) registered in the app

Build config:
- Lowered Gradle JVM heap from -Xmx6g to 768m: the 2GB build server froze on the old value; heavy release builds stay on GitHub Actions
2026-08-03 10:46:14 +00:00
mckero 7bb0bbbb46 build: load release signing config from local keystore.properties
Conditionally applies the BG7NTA signingConfig when keystore.properties exists (gitignored). CI signs via apksigner with GitHub Secrets, so this only affects local builds.
2026-08-03 09:25:53 +00:00
mckero 2dfd76825b fix: fork under own applicationId com.rtbishop.look4sat.bg7nta
The official app signs com.rtbishop.look4sat with its own certificate. A fork sharing that applicationId cannot be installed over the official build (signature mismatch) and users saw overwrite/install failures.

- Add applicationId version catalog entry; namespace stays com.rtbishop.look4sat so source imports are untouched, applicationId becomes com.rtbishop.look4sat.bg7nta.
- Update PROPERTY_SATELLITE_DATA_OPTIMIZED meta-data to the fork id.
- Document the fork-applicationId requirement in the version catalog.
2026-08-03 08:38:21 +00:00
atsunatsu 1813e1bfa4 fix: refine mutual pass matching and radar overlay 2026-08-03 13:04:42 +08:00
atsunatsu 4ca33c37ad feat: keep mutual state across nav + overlay station-B track on main radar
- MutualViewModel is Activity-scoped, so returning from Radar keeps query results
- TrackSampleData gains time field for live cut-off
- Removed standalone mutual card from radar page
- RadarViewCompose draws optional dashed station-B track + current dot
- RadarScreen builds trackB from mutual data up to current time
2026-08-03 00:55:05 +08:00
atsunatsu d7e1ce11c9 fix: smooth dual-station elevation curve
- Fix cubic Bezier control points (proper Catmull-Rom to Bezier conversion)
- Refine AOS/LOS more robustly by walking from edge into the pass window
- Still 5s sampling for smooth curves
2026-08-02 23:07:28 +08:00
atsunatsu 5de370983d fix: lock screen orientation to portrait 2026-08-02 22:19:37 +08:00
atsunatsu 0bae306312 fix: default station A grid, find all mutual passes, color toArgb()
- Pre-fill station A grid from settingsRepo.stationPosition
- Find ALL mutual passes per satellite (not just first)
- Fix Color.hashCode() -> Color.toArgb() for native canvas paint
- Use 2min gap between pass searches to avoid duplicates
2026-08-02 21:44:31 +08:00
atsunatsu 1064dc739e feat: add mutual pass query with dual-station elevation curve
Port satlover.de dual-station pass matching feature:
- New feature/mutual module with mutual pass data model
- MutualViewModel: compute overlapping passes for two stations
- ElevationCurveChart: Canvas-based dual elevation curve with drag
- MutualScreen: input form + results with expandable cards
- Navigation: add Mutual tab to bottom navigation bar
- i18n: add Chinese/English strings for new feature
2026-08-02 21:22:42 +08:00
Arty Bishop f585372594 Added cleartext traffic support for custom TLE URLs #227 2026-07-30 17:15:02 +02:00
Arty Bishop 6d2b0ced49 Implemented AOS window and elevation highlight filters 2026-07-30 13:28:37 +02:00
Arty Bishop b17ea2d918 v4.4.3 - Added required tweaks to support Android 17 (API 37) 2026-06-27 14:15:05 +01:00
Arty Bishop ec1a55c93d Added initial code for possible future deeplink nav support 2026-06-07 16:01:23 +01:00
Arty Bishop 79809aaec0 Added SSTV image decoding functionality to RadarScreen 2026-06-02 20:06:22 +01:00
Arty Bishop 18c8ac1822 Merged RadarScreen and RadioControlScreen functionality 2026-05-30 14:14:40 +01:00
Arty Bishop 22607e7710 v4.3.2 - Implemented Swipe-to-Focus behavior, minor fixes 2026-05-30 13:16:14 +01:00
Arty Bishop a39b1716e7 Added consistent pass selection to Radar and Map screens 2026-05-25 18:30:49 +01:00
Arty Bishop 5af963f3c9 Added several tweaks to sunrise/sunset time calculations 2026-04-28 21:00:43 +01:00
Arty Bishop bfb9fb6ca8 Tweaked ViewModel retention to mirror the Nav2 behavior 2026-04-26 14:44:29 +01:00
Arty Bishop d3b8e951dd Added red night mode filter overlay for the whole app 2026-04-25 19:22:32 +01:00
Arty Bishop 7d5bca300d Migrated to Compose Navigation3, updated dependencies 2026-04-24 17:20:23 +01:00
jcmerg 2df0b9ba3d Add CAT radio control for full-duplex satellite operation (#207) 2026-04-10 11:21:52 +01:00