Commit Graph
145 Commits
Author SHA1 Message Date
mckero 2c26272e0b fix(ft8): repair FT8CN locale string resources for AGP9 strictness
FT8CN 0.93 shipped malformed strings that old AGP tolerated but AGP9
rejects: 148 unclosed tags (</string>>), unclosed XML comments,
trailing garbage after tags, and non-positional multi-placeholder
formats. Fixed across all 8 locale files: closed tags/comments,
removed trailing garbage, numbered placeholders in argument order
(%1$s %2$d %.3$1f style). aapt2 compile of every values-* dir now
passes 0 errors; :feature:ft8 + :app compileDebugKotlin SUCCESSFUL.
2026-08-05 05:23:57 +00:00
mckero fc1bec1915 fix(ft8): unify osmdroid version to fix duplicate classes
CI build failed at :app:checkReleaseDuplicateClasses: local
osmdroid-android-6.1.14.aar (copied from FT8CN) clashed with the
project's osmdroid 6.1.20 (feature/map remote dependency). Switched
feature/ft8 to libs.other.osmdroid (6.1.20), removed the local aar.
Verified: compileDebugJavaWithJavac + :app:compileDebugKotlin
BUILD SUCCESSFUL.
2026-08-05 05:08:51 +00:00
mckero 4d7952d293 fix(ft8): register FT8 in settings page-order list (UI settings)
User spot-check found FT8 missing from the settings screens list
(SettingsScreen.kt:703) - page order drag/hide UI could not show or
reorder it. Added "nav_ft8 to FT8" after AMSAT (name must match
Screen.screenId). Verified: :feature:settings + :app compileDebugKotlin
BUILD SUCCESSFUL.
2026-08-05 04:53:37 +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 72f507163a fix(status): shrink day cells so long satellite names stay visible
User feedback: on narrow screens long names (e.g. AO-123_[FM]) were
truncated before the mode tag. Scaled the 6 day cells down
proportionally (weight 1f -> 0.8f, height 26dp -> 24dp) and widened
the name column (weight 1.4f -> 2f) in both header and rows.
Verified: :feature:status:compileDebugKotlin BUILD SUCCESSFUL.
2026-08-04 16:41:41 +00:00
mckero 2777607ab5 fix(i18n): drop values-b+in (duplicates values-in under aapt2)
aapt2 treats in/b+in as the same locale config as id -> Duplicate
resources build failure. in and id are equivalent in Android's
resource matcher (both normalize to id), so values-in + values-id
is sufficient coverage for Indonesian devices.
2026-08-04 16:18:50 +00:00
mckero a222cbb839 fix(i18n): use BCP-47 values-b+in for Indonesian legacy locale
values-in-rID did not survive aapt2 linking (normalized away).
Switch to values-b+in (BCP-47) which compiles cleanly and keeps an
explicit "in" language config alongside values-id. Verified: aapt2
compile of values-in/values-id/values-b+in OK.
2026-08-04 16:16:40 +00:00
mckero 0ef8f05dc4 fix(i18n): keep legacy "in" locale config for old Indonesian devices
Root cause: aapt2 merges values-in into values-id (in is the legacy
alias of id), so the APK only carried the (id) config. New devices
report "id" and match; older devices report "in" and find no (in)
config -> fall back to English (friend's report: only system date
showed Indonesian).

Fixes:
- Add values-in-rID (core/presentation + feature/cw) so the APK
  keeps a real "in" language config; values-in and values-id both
  get translatable="false" on the 27 entries that English marks
  (aapt2 rejects those with multiple %-substitutions otherwise)
- Verified: aapt2 compile of values/values-in/values-id/values-in-rID
  OK; :core:presentation:mergeDebugResources + :feature:cw:mergeDebugResources
  BUILD SUCCESSFUL
2026-08-04 16:04:04 +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 dd378990fe feat(wavelog): group log entries by pass session (satellite + AOS timestamp)
User request: logs should be separated by satellite/pass. Each pass
session gets an ID = satellite name + AOS timestamp (the second the
elevation hits 0, from OrbitalPass.aosTime), e.g.
"ASRTU-1-20260804-2014". Log page groups entries by session:
group title (satellite - local time) + thick divider line between
groups (md --- style); legacy entries without sessionId fall into
"Ungrouped" at the end.

- WavelogQso: +sessionId (persisted in queue JSON)
- LogTab: sessionId built from satelliteName + aosTimeMs (radar page
  passes currentPass.aosTime)
- WavelogLogScreen: grouped rendering + wavelog_ungrouped string (4 locales)
- sessionId UTC yyyyMMdd-HHmm; display converts to local time

Verified: check_strings OK (9 files); :app:compileDebugKotlin
BUILD SUCCESSFUL. Not released (batched).
2026-08-04 14:39:01 +00:00
mckero a23bf34ccf fix(radar): swipe-delete visuals restored (yellow trash + undo countdown)
Previous fix added the content-layer background OUTSIDE the offset
modifier, so the background stayed at the original position and
permanently covered the swipe-reveal area - the yellow trash icon,
75% undo button and 5s countdown were invisible while swiping.
Moved the background INSIDE the offset (background follows the
content): idle = fully covered (no bleed into Sent column), swiping
= yellow trash / undo countdown revealed as before.

Verified: :feature:radar:compileDebugKotlin BUILD SUCCESSFUL.
Not released (batched with pending fixes).
2026-08-04 14:22:27 +00:00
mckero a724c0823d fix(i18n): add values-id resource dir so Indonesian devices pick the locale
Friend's device (system language Bahasa Indonesia) fell back to
English even though values-in exists. Modern Android devices report
the Indonesian locale as "id" (ISO-639-1 current code; "in" is the
legacy alias) and resource matching is strict. Added values-id as a
copy of values-in in core/presentation and feature/cw (both language
directories ship the same translations).

Verified: check_strings OK (9 files incl. values-id);
:app:compileDebugKotlin BUILD SUCCESSFUL. Not released (batch with
pending fixes).
2026-08-04 14:18:00 +00:00
mckero cda6cf8583 fix(gps,data): official-code fixes - real GPS result callback, no fake update success (4.5.2, release pending)
Official code issues found during review (user-reported):
1. GPS "success" was shown instantly even when no fix was obtained:
   - setStationPosition() always returned true (permissions exception
     swallowed, async requestLocationUpdates without waiting)
   - now: suspend + LocationManagerCompat.getCurrentLocation (GPS
     first, network fallback), permission check upfront, 15s timeout,
     success only when onLocationChanged fires; SettingsRepo takes
     Context for the permission check; ViewModel waits for the real
     result and shows "Unable to get location - check permission and
     GPS/network signal" on failure (4 locales)
2. Data update faked success on total failure:
   - updateFromRemote now counts successful sources; 0 success throws
     IOException -> timestamp NOT refreshed, Toast "Update failed -
     check your network" (4 locales, new IShowToast resId overload)
   - OkHttp timeouts widened: connect 15s / read 20s / write 20s

Verified: check_strings OK; all modules compile.
Release intentionally NOT triggered (user: fix everything first, then
one release).
2026-08-04 14:08:38 +00:00
mckero c09e87fa4e fix(radar,settings): realtime doppler freq in Log tab, trash overlay fix, sub-menu swap (4.5.2 overwrite 5)
User-test fixes:
1. Log tab frequency now refreshes every second with the transponder
   panel (multi-Doppler): selectedRadio derived by uuid from the
   per-second transceivers list instead of a remembered stale
   reference; display and upload use radio.uplinkLow/downlinkLow
   (the same doppler-corrected values the transceiver panel shows).
2. SwipeDeleteRow: content layer now has an opaque background so the
   trash icon only appears while swiping (was bleeding through into
   the "Sent" column).
3. Settings page order: More-menu items can now move back into the
   main menu - button always visible; when the main menu is full (5),
   the last non-Settings item is automatically swapped into More.

Verified: check_strings OK (8 files); :app:compileDebugKotlin
BUILD SUCCESSFUL.
2026-08-04 13:39:22 +00:00
mckero 0bcf73e1f9 fix(wavelog,radar): sat name LoTW normalization, full grid lines, RX freq restored (4.5.2 overwrite 4) 2026-08-04 12:29:31 +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 1a55241791 merge: upstream atsunatsu CW decoder + linear calculator (keep ours, absorb Calculator tab)
Upstream added a333192d (CW decoder and linear calculator page) and
c80e3512 (linear calculator offset mapping fix). Merged with all 7
conflicts resolved by hand:

- app/build.gradle.kts: keep ours (keystore signing config + abiFilters)
- CwDecodeScreen.kt: keep ours (only difference is localized strings
  vs upstream hardcoded English)
- g3/d.java: keep ours (identical lookup table, 4-line diff)
- app_values.xml: keep ours (3 CW permission strings upstream lacks)
- feature/radar/build.gradle.kts: keep ours (same dependency line)
- TransceiversPage.kt: take upstream (includes CalculatorPage +
  our CW panel + INV title logic; conflicts were comment-only)
- RadarScreen.kt: both sides - keep our wavelog imports + Log tab,
  take upstream DopplerFrequencyCalculator import + Calculator tab;
  pages = Transceivers, Log, [Calculator if any named linear
  transponder per upstream isNamedLinearTransponder], Sstv

Result: radar page tabs are Transceivers / Log / Calculator (linear
sats only, upstream logic) / SSTV. WaveLog Log tab and all our
localization/signing preserved.

NOTE: committed WITHOUT building per user request - merge compile
verification deferred.
2026-08-04 11:59:15 +00:00
mckero 9d437a90c9 fix(wavelog,radar): 4.5.2 second overwrite - v1 API support, error dialog with copy, frequency sync
Background: user's WaveLog server has NO v2 API (all /api/v2/* return
404; /api/qso v1 works - verified with curl). First fix only tried v2
paths, so uploads still failed with 404. Also: Log tab frequencies did
not match the Doppler panel, linear transponders showed a single
frequency instead of the passband range, and errors were toast-only
(no copy).

Changes:
- WaveLogApi: full v1 support with auto fallback
  - v2 first (Bearer header + JSON fields), on 404 fall back to v1
    (key inside JSON body + ADIF string) - both with and without
    index.php prefix
  - test connection: v2 GET api/v2/token -> v1 POST
    api/get_contacts_adif (validates key + station id)
  - station gridsquare is v2-only; on v1 the uploader falls back to
    the user's QTH grid (grid check skipped/equal)
  - v1 ADIF: call/band=SAT/mode/freq+freq_rx (MHz)/qso_date/time_on
    (UTC, compact)/gridsquare(4)/sat_name/prop_mode=SAT, byte-length
    field prefixes
  - 409 duplicate counts as success in both versions
- Error dialog with copy: test/upload failures now open an AlertDialog
  with the full error (incl. actual URL + HTTP code), Copy button
  (ClipboardManager) and Cancel; uploader collects the first failure
  message
- Log tab frequency sync: LogTab receives txBaseFrequencyHz from the
  radar page (the tuned frequency shown in the transceiver panel);
  RX is computed through the same Doppler mapping as the Doppler
  panel; linear transponders show the full uplink/downlink range
  (Doppler-corrected low-high) instead of a single frequency
- Restored uploadWavelogQueue (lost in an earlier patch)

Verified: check_strings.py OK (8 files, 452/4.5.2);
:app:compileDebugKotlin BUILD SUCCESSFUL.
2026-08-04 11:47:06 +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
atsunatsu c80e351212 fix: correct linear calculator offset mapping 2026-08-04 18:41:53 +08:00
mckero 916a0d0f0a feat(wavelog,radar,settings): WaveLog logbook integration + Log tab
Background: satellite operators want to log QSOs during a pass while
watching live frequencies. 4.5.2 adds WaveLog (logbook server) API v2
integration: log from the radar page, upload to a self-hosted WaveLog
instance with grid-mismatch protection.

Changes:
- Radar page: new third tab "Log" between Transceivers and SSTV
  - pick a transponder, watch live TX/RX Doppler-corrected frequencies
  - enter callsign, Enter stores locally (UTC time + that second's
    frequencies sampled together)
  - local entry list shows time/frequency/callsign only (no upload
    status, per user: proves the entry was saved)
  - swipe-to-delete: yellow trash while swiping, turns into Undo at
    75%, 5s countdown before auto-delete (custom gesture, no
    SwipeToDismissBox)
- Settings: new WaveLog card (server URL / API key / station ID /
  auto-upload switch / test connection / upload now buttons) matching
  the user's reference screenshot layout
- Upload pipeline: POST /index.php/api/v2/qso with required fields
  (station_profile_id, call, band=SAT, mode, qso_date, time_on UTC)
  plus freq/freq_rx (Hz), gridsquare (from station profile via
  GET /api/v2/station/{id}), sat_name; RST omitted per user
- Grid check: station gridsquare (first 4) vs user QTH (first 4);
  mismatch shows a confirm dialog (ignore & upload / cancel)
- Auto upload: 10-minute in-app retry loop (only when switch on);
  manual upload button; local queue capped at 500, all entries stored
  locally regardless of switch
- Fixed: subMenuOrder was never persisted (4.5.1 regression)
- core/domain: compileOnly org.json (runtime uses Android's)
- Version 4.5.2 (452)

Verified: check_strings.py OK (8 files, 452/4.5.2);
:app:compileDebugKotlin BUILD SUCCESSFUL locally.
2026-08-04 10:29:40 +00:00
atsunatsu a333192df5 feat: add CW decoder and linear calculator page 2026-08-04 17:49:28 +08: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 760a26b007 Merge remote-tracking branch 'atsunatsu/main' 2026-08-04 08:10:17 +00:00
mckero 47f3e424ff feat(radar,cw): transponder CW panel switches to the Morse Expert engine
Background: the transponder panel CW decoder (added by the upstream
fork author) used a lightweight Kotlin Bayesian engine (core/domain/cw,
kept untouched as a fallback). This change makes the panel use the
Morse Expert engine ported in 4.5.0, so both CW entry points share the
same decoder with a live waterfall.

Changes:
- New mini layout cw_panel_main.xml (waterfall 80dp + decoded text,
  status line hidden but ID kept for controller lookup)
- MainActivity.onCreate overload with applyImmersive flag; panel binds
  with false so the host window system bars are not touched
- CwDecoderPanel now embeds the mini layout via AndroidView and drives
  the MainActivity controller: start/stop/reset map to the engine,
  lifecycle follows panel expand (start) / collapse (release mic)
- radar module now depends on feature:cw (+ constraintlayout 2.2.1,
  same as cw) for layout + controller reuse
- What's new rewritten in EN/TR/ZH for this release only

Verified: :feature:radar:compileDebugKotlin and :app:compileDebugKotlin
BUILD SUCCESSFUL locally; check_strings.py OK (7 files, no bare
apostrophes).
2026-08-04 07:56:11 +00:00
mckero f731775346 fix(cw): repair FFT pipeline (NaN twiddle table + dead cond_22 path)
The waterfall showed mirrored/upside-down garbage because the FFT
never produced a valid spectrum:
1. g3.c.f() (high-precision sin) had its quadrant-0 case mangled by
   jadx into a nested-if that returned NaN for small angles - the
   twiddle factor table ended up with 329/1024 NaN entries.
   Restored the smali switch: case0->g(), case1->c(), case2->-g(),
   case3->-c().
2. i3.d.k() routed the runtime FFT (a5==0, single-thread path) into
   the else of if(a5!=1) instead of if(a5!=0), so the FFT never ran
   and the output stayed in the time domain (peak at bin 357 for a
   669Hz tone instead of bin 86).

Verified with a JVM harness (static-block tables + pure-tone inputs):
200Hz->bin26, 400Hz->bin51, 669Hz->bin86, 1000Hz->bin128, all exact.
Twiddle table NaN count: 329 -> 0.
2026-08-04 06:55:03 +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 5dc7ce6a22 feat(cw): port Morse Expert 1.15 CW decoder engine (feature:cw compiles)
Full port of the CW/Morse decoder from Morse Expert 1.15
(com.ve3nea.morse_expert), preserving obfuscated class names and logic.
Ads (gms.ads), billing (BillingClient), and library code were removed per
user; SettingsActivity deferred to Compose integration.

Ported modules:
- pas.*: decoder engine interface + JNI (libnativedecoderjni.so, armeabi-v7a)
- k3.*: FFT library (pseudo-enum q rewritten from smali; sun.misc stubs
  for Unsafe/Cleaner since Android lacks them; FFT path uses float[] branch)
- i3.*, g3.*, j3.*, s.*, d1.AbstractC1518b: DSP/sin-cos tables (long[] tables
  f/f11589g restored from smali)
- H2.a/b: audio capture + decode control core (jadx catch-block illusions
  cleaned per smali; recording try/catch restored around write block)
- J2.a/b: waterfall OpenGL renderer + palette (r10 = anchor color per smali)
- E2.g: waterfall touch frequency picker (height = ScaleView per smali)
- B0.b/B.RunnableC0001b: status-bar + decoded-text UI runnables (real
  constructors recovered from smali)
- MainActivity → controller class (Activity injected; immersive status bar
  inlined); C1646n trimmed to view holder; D.n keeps waterfall texture case
- F2.a: text selection menu (Save/Share)

Verified: :feature:cw:compileDebugJavaWithJavac BUILD SUCCESSFUL.
2026-08-04 03:57:25 +00:00
mckero ff408747cd fix(settings): drag-reorder tracks live position; grip becomes a single dot
Two drag issues from user testing:
1. Only adjacent swaps worked - the drag gesture could not move an
   item past multiple positions. Root cause: the pointerInput closure
   kept the index captured at composition; after the live swap
   (items.add(target, removeAt(index))) the closure's index was
   stale, so subsequent targets were computed from the wrong origin
   and the saved order got corrupted (also made previously moved
   items snap back).
   Fix: track the dragged item's live position via draggingIndex -
   onDragStart resolves it with items.indexOf(screen), onDrag computes
   the target from draggingIndex and updates it after each swap.
2. The six-dot grip icon was ugly; replaced with a single 8dp themed
   dot (Box + CircleShape inside the 48dp touch area, onSurfaceVariant
   color). ic_drag.xml removed.

What's-new rewritten in en/zh/tr with ONLY this release's changes
(user rule: replace, never append history). Version stays 4.4.9
(覆盖 per user). Verified: settings + app compile, check_strings.py
clean.
2026-08-04 01:10:13 +00:00
mckero 6636dd8e98 fix(settings): crash when scrolling Settings (nested LazyColumn unbounded height)
v4.4.9 page-order list crashed the Settings screen on scroll: the
drag-reorder LazyColumn sits inside the Settings LazyVerticalGrid
item, and a vertically scrollable child measured with infinite max
height throws IllegalStateException, killing the app before the UI
Settings card even renders.

Fix: give the LazyColumn a bounded height (itemHeight * items.size
= 7 rows × 48dp = 336dp). Drag logic, animation and persistence
unchanged.

What's-new rewritten in en/zh/tr with ONLY this fix (user rule:
replace, never append history). Version stays 4.4.9 (覆盖 per user).
Verified: settings + app compile; check_strings.py clean.
2026-08-04 00:52:43 +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 9a4ceddcb0 fix(settings): force custom-source toggles off when legacy example.com URL present
Old installs that once enabled the custom TLE/transceiver toggles kept
the legacy "https://example.com/tle.txt" placeholder in preferences.
Under the new "toggle replaces the online-update default source"
semantics this pointed the All/SatNOGS fetch at a dead URL and broke
satellite updates after upgrading to 4.4.8.

Migration in getDataSourcesSettings(): example.com placeholders are
replaced by the real default URLs (Celestrak All / SatNOGS) and the
toggles are forced OFF unless a non-default, real user URL is set.
Also disable the reset (loop) icon buttons while their toggle is off.

Verified: core:data tests pass, settings + app compile. Version stays
4.4.8 (覆盖 per user).
2026-08-03 17:03:48 +00:00
mckero 8d88100b15 feat(settings): custom URL now overrides the online-update default source
The "Custom URL" dialog's two URL fields now control the default
source used by the online update, per the user's intent (one-time
setup, no extra steps per update):

- Defaults are real URLs now: TLE = Celestrak "All" (active group
  CSV), transceivers = SatNOGS API. The example.com placeholders are
  gone.
- updateFromRemote(): the "All" TLE entry and the "SatNOGS" entry are
  replaced by the user's URLs when the corresponding custom toggle is
  ON and the URL is non-blank; otherwise the real defaults are used.
  The old "Other" appended-source logic is removed - the toggle now
  means "use my URL for this source" instead of "download an extra
  source". Each source (TLE / transceivers) is independent.
- Each URL field gained a reset (loop) icon button on the right that
  restores the default URL text (new ic_reset drawable, Autorenew
  vector). Strings added in en/zh/tr.

Test updated: custom TLE URL data now lands under the "All" type
instead of "Other". Verified: core:data tests pass, settings + app
compile. Version stays 4.4.8 (覆盖 per user).
2026-08-03 16:41:14 +00:00
mckero c19209655b fix(roaming,settings): theme colors for night mode; unclamp cards; brand title and APK name
Three detail fixes plus branding, per user review:

1. Night mode visibility (roaming page): hardcoded reference blues
   (#01DDFF/#0BACF1) collapse to black under the red ColorMatrix
   filter (R channel only), making the page unreadable. Replace ALL
   colors with MaterialTheme.colorScheme (background/surface/
   surfaceVariant/onSurface/onSurfaceVariant/primary/error). GPS
   status dots become themed circles (primary/error) so they survive
   the red filter; red marker keeps the original pnt drawable.

2. Settings cards unclamped:
   - OtherCard: fixed height(268.dp) squeezed the last toggle row
     (spacing 42/42/42/15px on device); drop the fixed height.
   - CardCredits: same 268dp + SpaceBetween overflowed, gluing the
     last thanks entry to the warranty line; drop fixed height, use
     spacedBy(8.dp) and insert 24dp before the warranty text.

3. Branding:
   - Settings top title: "Look4Sat v%s" -> "Look4Sat Pro v%s" (en/tr);
     title Text no longer marquees and wraps instead (heightIn(min=48)).
   - APK asset name: look4sat-<ver>.apk -> Look4Sat-Pro-<ver>.apk.
   - What's-new dialog (pass_whatsnew_message) rewritten in all three
     locales (en/zh/tr) with this release's changes; zh gets its own
     localized title "Look4Sat Pro 更新内容".

Verified: roaming/settings/passes/app compile, 11 unit tests pass.
2026-08-03 15:46:21 +00:00
mckero d456bdf4b5 fix(roaming): stretch side cells to 80dp like the reference RelativeLayout
The user spotted two white "beams" (20dp gaps) between the center
cell and the side cells. Root cause: the reference app's RelativeLayout
ignores the fixed 60dp width when a child has BOTH a left rule
(alignParentLeft) and a right rule (toLeftOf=center cell) - the width
is stretched to right-rule minus left-rule, i.e. 80dp on a 360dp
screen. The three cells therefore sit flush with only 2dp margins
between them, no gaps.

Port that behavior: side cells 60dp -> 80dp (6 places), center cell
stays 200dp centered. Verified against the reference screenshot pixel
measurements (side cells ~76.5dp incl. margins, center ~198dp).

Verified: feature + app compile, 11 unit tests pass. Marker lookup,
grid math and GPS logic untouched.
2026-08-03 15:22:08 +00:00
mckero f06b17a194 fix(roaming): align grid columns like reference and lift footer
Two remaining proportion issues on the user's device:

1. Right-side gap: the three grid rows used a continuous Row
   (60+200+60dp), leaving ~84px of blank space on the right of the
   screen. The reference app uses a RelativeLayout where the right
   column is pinned to the screen edge and the 20dp gaps sit on both
   sides of the centered middle column. Convert each row to a Box:
   left cell align(CenterStart), middle cell align(Center), right
   cell align(CenterEnd) - pixel-identical to the reference.

2. Footer legibility: the credit line ("制作:US1PM  汉化:BA7LCE")
   had a fixed 15dp height with no bottom margin, so it sat directly
   against the navigation bar and part of the text was hard to read.
   Drop the fixed height and add 10dp bottom padding so the text
   renders fully with breathing room above the nav bar.

Verified: feature + app compile, 11 unit tests pass. Marker lookup,
grid math and GPS logic untouched.
2026-08-03 15:09:45 +00:00
mckero 675194075e fix(roaming): inset page content from system bars
The ported page rendered edge-to-edge: the GPS bar started right at
the top of the screen and the footer sat against the navigation bar,
so system UI overlapped the content (user: "顶头"). The reference app
is not edge-to-edge and keeps its content inside the safe area.

Add windowInsetsPadding(WindowInsets.systemBars) on the page root
column, shrinking the top and bottom by the status/navigation bar
height exactly as the user requested ("上方和下方往里面缩一点点").
Grid columns (60/200/60dp), rows, marker lookup and all logic are
untouched - verified pixel-identical column ratios vs the reference
screenshot (147:395:147 vs 146:395:148).

Verified: feature + app compile, 11 unit tests pass.
2026-08-03 14:55:46 +00:00
mckero 0ecccc4746 feat(roaming): port QTH定位器 2.0 page and logic verbatim
The roaming page was repeatedly rebuilt by hand and the red marker
still rendered at the wrong spot on the user's device. Per the user's
explicit instruction the whole page is now a faithful, line-by-line
port of the reference app (QTH定位器 2.0, com.us1pm.gridsquarelocator)
with zero UI or logic changes.

UI (res/layout/main.xml, byte-verified via aapt2 dump):
- 25dp holo-blue GPS bar: "GPS" 14sp, green/red status dot 15dp
  (original mipmaps copied as drawables), centered date, right time,
  translucent-yellow "设置启用GPS" button that opens location settings
- Latitude/longitude rows: 16sp black labels, right-aligned decimal
  values, DMS label format "纬度  22° 18' 50" N" exactly as reference
- 43sp bold black locator, centered, with progress spinner + notice
- 3x3 continuous grid: 60/200/60dp columns, middle row fixed 205dp,
  edge cells #0BACF1, center cell 200x200dp holo-blue (#01DDFF as
  shown on the user's device), center label 100x80dp 30sp bold white
  (textColorHighlight) centered, bottom "制作:US1PM  汉化:BA7LCE"
- Red marker: original pnt.png (red square with white outline),
  10x10dp, absolutely positioned by the reference lookup tables
  (lon 3rd pair a..x -> leftMargin -2..190dp, lat 3rd pair a..x ->
  topMargin 190..-2dp, screen-Y inverted)

Logic (MainActivity.java showLocation/checkEnabled/onResume):
- 8-char locator via the reference range-lookup tables
- 3x3 neighbor grid via parseInt3 five-branch logic incl. all four
  corner-carry tables (00/09/99/90)
- Live GPS + network updates 10s/10m while the page is shown,
  provider filtered to gps/network, checkEnabled three-state
  (green dot / red dot + settings button) exactly like the reference
- Time = cached hour prefix + fix minutes; date "dd MMM yyyy"

Dropped only the Play-store ad banner (conflicts with GPL project).

Verified: 11 unit tests pass (locator, marker lookup, grid, DMS,
time, all edge branches); feature + app modules compile.

RoamingState.kt removed - state and math now live in RoamingScreen.kt.
2026-08-03 14:29:44 +00:00
atsunatsu a7b41c95c7 fix: improve match header and daily sun times
Match page UI:
- Always show a meaningful status line in the top bar instead of leaving the second row blank on first entry.
- Add a compact status chip for waiting, calculating, result, no-match, and error states.
- Remove the duplicate intro card so the first screen starts directly with station inputs.

Pass list sun times:
- Compute sunrise/sunset from each visible date group's 00:00 in the selected timezone.
- Avoid using an arbitrary pass AOS as the rise/set search start, which could jump later-day headers to the following day's events.
2026-08-03 21:38:57 +08:00
mckero 88d1498eac fix(roaming): red marker tracks fix in BOTH axes via 3rd-pair lookup
The marker's vertical position was hardcoded (10dp below the label) — markerY never participated, so the dot could only move horizontally and could not reflect where the GPS fix sits inside the 4-char square.

Now the marker is positioned in both axes from the cell's top-left corner:
- x = markerX * cellWidth, y = markerY * cellHeight (screen Y)
- markerX/markerY come from the 3rd character pair via the reference lookup tables (lon a=-2..x=190 as leftMargin; lat a=190..x=-2 as topMargin, i.e. latitude inverted on screen)
- Verified numerically against the decompiled tables: a=0/1.0, i=0.333/0.667, x=0.958/0.042 — matching within 3% (the reference table is slightly non-uniform)

Label stays at upper-middle (28sp bold); marker is only drawn when a valid locator exists.
2026-08-03 12:55:37 +00:00
mckero 31ae04329d fix(roaming): clock ticker moves the header time; GPS mirrors station pos
Two bugs from the last release:

1. The header clock was frozen: Date() was only evaluated during recomposition, and with no state changes the time never moved. Added a 1s LaunchedEffect ticker that updates a now-state, so the date/time text re-renders and actually advances — matching the reference app, which refreshes the clock on every location callback.

2. GPS indicator: replaced the 10-minute freshness check with a direct 'has a real fix' check (timestamp > 0 and coords non-zero). The page mirrors the station position (站位) from the shared StateFlow, so the GPS dot is green whenever the station has a fix — GPS shows exactly what the station GPS says, nothing more.
2026-08-03 12:48:17 +00:00
mckero eae6304124 feat(roaming): faithful continuous-table grid layout, not card-based
Reworks the Roaming grid to structurally match the reference app instead of a card-style panel:

- Removed the outer ElevatedCard, rounded cells, cell gaps and inner padding: the 3x3 grid is now one continuous table that fills the panel, cells connected edge-to-edge.
- Cells separated by 2dp divider lines that run the full width/height of each row/column, crossing at right angles like a real coordinate grid (the reference app's continuous separator lines).
- Cells are square-cornered (no rounded corners), background fills each cell fully.
- Column widths 21.4% : 56.2% : 21.4%, row heights 31.5% : 35.9% : 31.7% retained.
- Center cell: OL42 is larger (28sp bold) and placed at upper-middle; the red marker sits BELOW the text with a 10dp gap, horizontally offset by the 3rd-pair fraction — never overlapping the label, matching the reference 'text above, marker below' layout.
- Surrounding labels bumped to 16sp Medium (larger/stronger than before).
- Info header also switched from an ElevatedCard to a flat continuous block so the page reads as one continuous surface, like the reference.

The grid proportions, locator algorithm, marker mapping and boundary logic were already faithful; this change makes the visual structure faithful too.
2026-08-03 12:32:45 +00:00