The decompiled per-edge branches computing the surrounding nine squares had two
independent defects.
1. Field letters stepped past the alphabet
Every branch moved a field with raw character arithmetic (`str[0] - 1`,
`str5[0] + 1`) and Maidenhead fields only run A..R, so coordinates near the
edges of the world produced squares outside the alphabet:
(-89.9, -179.9) -> [@A91, AA01, AA11, @A90, AA00, @A10, @@99, A@09, A@19]
( 89.9, 179.9) -> [RS80, RS90, SS00, RR89, RR99, SR09, RR88, RR98, SR08]
2. Some moved cells kept the old field letter
The north-edge branch advanced the latitude field for the top-centre cell only,
leaving the two top corners in the previous field:
centre AA19 -> ported [AA00, AB10, AA20, ...]
correct [AB00, AB10, AB20, ...]
Cross-checked against the shared qthNeighbors helper, which is already covered
by QthConverterTest including the AA00 and RR99 wrap cases:
before: 64,800 sampled coordinates, 6,480 disagreed (all with centre square
digits 00 or x9, i.e. the north edge and the 00 corner)
after: 64,800 sampled coordinates, 0 disagree
The ring is plain Maidenhead arithmetic with no QTH-Locator-specific behaviour,
so call qthNeighbors instead of keeping a second, wrong implementation. The
now-unreferenced buildGrids branches are removed (grep confirmed the definition
was the only remaining occurrence). The existing OL42 reference grid and the
four ported edge-case tests still pass unchanged.
Reverting the fix fails both new regression tests; with it
:feature:roaming:testDebugUnitTest and :core:domain:test are green.
The QTH Locator port keeps the decompiled range tables, which close both
adjacent cells (`-20.0..0.0` then `0.0..20.0`). Kotlin's `when` takes the first
match, so any coordinate landing exactly on a field, square or subsquare
boundary was attributed to the previous cell:
(0, 0) II99xx99 should be JJ00aa00
(1, 1) JJ00lx99 should be JJ01ma00
(22, 108) OL31xx99 should be OL42aa00
(22.5, 108.5) OL42fl99 should be OL42gm00
(22.25, 108.25) OL42cf99 should be OL42dg00
At the field level the locator is wrong by a whole 20 deg x 10 deg field, and
the 3x3 neighbour grid plus the red position marker are derived from the same
characters, so the whole Roaming screen pointed at the wrong square.
Cross-checking the port against core/domain positionToQth over the grid:
before: 65,341 sampled points, 4 agreed
after: 65,341 sampled points, all agree
The independent converter was confirmed correct first: it reproduces the
user-verified reference sample OL42ih45, and hand-computing lon=-179.75
(0.25 deg into the field, x12 -> subsquare index 3 = 'd') and lat=-90
(subsquare 'a', extended digit 0) matches it rather than the port.
Rather than rewriting the faithful lookup tables, nudge the input by 1e-10 so
the closed ranges behave like the standard half-open [low, high) cells, keeping
+90/+180 inside the final R cell. Seven real-world city samples and all existing
ported-behaviour tests, including (90, 180) -> RR99xx99, are unchanged.
Regression tests added for the boundary cases and for cross-implementation
agreement. Reverting the fix fails both; with the fix
:feature:roaming:testDebugUnitTest is green.
Two races shared the same cause: pushSamples runs on the audio capture thread
while clear() runs on the Compose main thread.
1. Lost redraw notifications
Both paths did `_revision.value += 1`. That expands to get -> add -> set and is
not atomic. A controlled two-thread probe (20k increments each, five runs)
lost up to 6,402 increments / 16%; using StateFlow.update lost zero. Since
revision is the Canvas's only redraw signal, every lost update can leave the
waterfall showing stale rows. If both writes land on the same number, StateFlow
sees no value change and notifies nobody.
Use `_revision.update { it + 1 }` in both paths.
2. Clear resurrected pre-clear audio
pushSamples copies pending audio under the lock, deliberately performs FFT
outside it, then reacquires the lock to append rows. The exact interleaving:
audio thread: take old audio, start FFT
main thread: user taps Clear -> rows/pending empty
audio thread: old FFT completes -> appends old rows again
The display becomes empty then immediately redraws the audio the user cleared.
A deterministic thread probe reproduced old rows after clear. Add a generation
counter protected by the same lock: pushSamples records it before FFT and drops
the computed rows when clear incremented it meanwhile. Fixed probe remains empty.
Verification: :feature:cw:compileReleaseKotlin + full :core:domain:test BUILD
SUCCESSFUL; grep confirms no non-atomic revision increments remain.
AprsForegroundService refuses to run when callsign is blank and calls
stopSelf(), but it never writes enabled=false back to AprsStore. AprsCard did
the opposite: toggling Enable first persisted enabled=true, then started the
service. On a fresh install with no callsign this produced a permanent lie:
UI switch: ON SharedPreferences: enabled=true service: stopped
Leaving and reopening settings still showed ON even though APRS had never sent
a packet. Startup/restore code could then repeatedly try to launch a service
that immediately stops itself.
There were two entry paths with the same root cause:
1. Turning the switch on before entering a callsign.
2. Erasing an existing callsign in the dialog while APRS was already enabled.
The switch now opens the configuration dialog without persisting or starting
anything when callsign is blank. Saving the dialog also forces enabled=false
when the callsign was erased.
State-machine simulation: old state ends ON/stopped; both fixed paths end in a
consistent OFF/stopped state. :feature:settings:compileReleaseKotlin and full
:core:domain:test BUILD SUCCESSFUL.
A full-domain round-trip probe found three related boundary bugs.
1. Exact positive limits wrapped the square/subsquare terms to zero
positionToQth clamped only the A-R field index. At +90 latitude / +180
longitude the field saturated at R, but all later terms used modulo and wrapped
to square 0 / subsquare a:
(90, 180) -> RR00aa00 -> (80.002083, 160.004167)
error: -9.998 deg latitude, -19.996 deg longitude
The existing test incorrectly asserted RR00aa00 and had therefore fossilised
the defect. Clamp shifted coordinates just inside the half-open upper bound so
the limits land in the final cell RR99xx99.
2. isValidPosition allowed longitude through +360
Maidenhead covers -180..180, but 181..360 was accepted and produced plausible
locators that decoded 20-200 degrees away:
lon 181 -> decoded 161.004167 (error -19.996)
lon 270 -> decoded 170.004167 (error -99.996)
lon 360 -> decoded 160.004167 (error -199.996)
Restrict the converter contract to -180..180.
3. Locator validation allowed S-X as field letters
The first pair has 18 fields A-R, while only the later subsquare pairs use
A-X. The shared [A-X]{2} regex accepted SS00aa / XX99xx and decoded them past
the poles (up to lat 149.98, lon 299.96). Use A-R for the field pair.
The SettingsRepo caller had a separate wrapping bug that masked part of this:
it mapped longitude>180 by subtracting 180 (270 -> +90, wrong hemisphere)
instead of modulo 360 (270 -> -90). Fix that at the writer too.
Verification:
- standalone JVM sweep: 519,841 points, old code had 1,441 large-error points
with max drift 9.997917 deg lat / 19.995833 deg lon
- new Kotlin regression sweep requires every 8-char round trip <=0.01 deg
- QthConverterTest BUILD SUCCESSFUL
- full :core:domain:test + :core:data:compileReleaseKotlin BUILD SUCCESSFUL
Every ACTION_START - and every null intent delivered by START_STICKY - called
startReporting(), which always constructed a new AprsReporter and overwrote the
field without stopping the old one. AprsReporter owns an independent
SupervisorJob + periodic while(isActive) loop, so every overwritten instance
kept reporting forever and could no longer be reached by ACTION_STOP.
Simulation:
five ACTION_START events: 5 running reporters, 4 leaked -> fixed: 1 / 0
START + 3 sticky restarts: 4 running, 3 leaked -> fixed: 1 / 0
mixed real sequence: 4 running, 3 leaked -> fixed: 1 / 0
At the default 10-minute interval, four leaked reporters send 24 duplicate
position packets per hour and open 24 needless connections; this also amplifies
the connect-time socket leak fixed earlier.
startReporting now returns when the current reporter is active. Config changes
remain correct: AprsCard explicitly sends ACTION_STOP before ACTION_START, so
the old reporter is stopped and nulled before the new configuration starts.
:app:compileReleaseKotlin BUILD SUCCESSFUL.
AmSatParser deliberately uses getOrNull + mapNotNull while reading each day's
12 slots, so a shortened HTML row can legitimately produce SatDay(slots=[]).
StatusRow then selected the first non-gray slot and fell back to slots.first(),
which throws NoSuchElementException and crashes the entire AMSAT status screen.
Use firstOrNull for both lookups and render a zero-count gray placeholder when
no slot exists. Real amsat.org HTML currently has all 41 satellite rows at the
full 73 cells, but the parser's own tolerance contract means the UI must handle
what it can emit.
Verified against the live page: parser matches 41/41 rows and 477/477 reports;
:feature:status:compileReleaseKotlin BUILD SUCCESSFUL.
Both extensions are fixed-width decimal fields, but neither value was range
checked before formatting:
formatAltitude(-50.0) -> /A=-00164 ('-' eats a digit slot)
formatCourseSpeed(_, 360f) -> /360/... (course must be 000..359)
formatCourseSpeed(_, -1f) -> /-01/... (widens the field)
A negative altitude is reachable from a below-sea-level position or a poor GPS
fix, and the malformed extension corrupts everything after it in the comment
field. Altitude now clamps to 0..999999, course wraps modulo 360, and speed
clamps to three digits.
Found by the same locale probe that produced the previous commit.
:core:domain:test BUILD SUCCESSFUL.
All nine String.format calls in AprsPacket used the JVM default locale. On a
device set to Arabic, Persian or Bengali the digit shapes come out as
Eastern Arabic / Bengali numerals, so every position report was malformed:
ar_EG lat=٣٩٥٤.٢٥N lon=١١٦٢٤.٤٤E alt=/A=٠٠٠٣٢٨
fa_IR lat=۳۹۵۴.۲۵N lon=۱۱۶۲۴.۴۴E alt=/A=۰۰۰۳۲۸
bn_BD lat=৩৯৫৪.২৫N lon=১১৬২৪.৪৪E alt=/A=০০০৩২৮
APRS-IS is an ASCII line protocol, so aprsc rejects these packets outright:
APRS reporting simply never worked for those users, with no clear error.
A locale using ',' as the decimal separator would corrupt the range filter
the same way.
Affected: getDMS position encoding (all five ambiguity branches), the
DDMM.MM/DDDMM.MM assembly, formatAltitude, formatCourseSpeed and
formatRangeFilter.
TDD proof:
without Locale.ROOT: 4 of 4 AprsPacketLocaleTest cases FAILED
with Locale.ROOT: BUILD SUCCESSFUL, full :core:domain:test green
Ruled out by the same probe (no change made): getDMS degree/minute split
matches an independent DDMM.MM reference implementation over 1,800,000 sampled
latitudes with zero divergence; the passcode loop dropping the trailing NUL on
even-length callsigns is the standard algorithm's behaviour.
WaveLog v1 truncated every grid to four characters with gridsquare.take(4),
while v2 sent the same grid at full precision. QRZ backfill provides six-character
locators (e.g. OM89ab / FN31pr), so the v1 path - the one used by the user's
server in practice - degraded position precision from roughly 4.6 km to around
100 km and stored different data depending on which API version answered.
Send the complete grid through v1 as well. The ADIF length field is already
computed from the actual value, so six/eight-character locators need no special
handling.
TDD proof:
old take(4): v1_adif_preservesSixCharacterGrid FAILED
fixed: WaveLogApiPayloadTest BUILD SUCCESSFUL
full suite: :core:domain:test BUILD SUCCESSFUL
All five connect paths opened a socket, completed the TCP/RFCOMM handshake,
and only afterwards stored it in a field. Any exception in between leaked the
socket: the catch block just flipped a boolean, and disconnect() can only close
what already reached the fields.
Leak windows (statements that can throw after the handshake succeeded):
AprsIsClient.connect soTimeout / tcpNoDelay / getOutputStream / getInputStream
Ic705Controller.connect outputStream / inputStream / sendAndWaitAck
Ft817Controller.connect outputStream / inputStream
BluetoothReporter x2 outputStream
AprsIsClient is the worst case because AprsReporter retries on a timer
(intervalMin, minimum 1 minute) and nulls out the client after each failure,
so every failed attempt permanently loses one fd:
failure rate leaked fds/hour time to exhaust 1024 fds
5% 3.0 ~14.2 days
20% 12.0 ~3.6 days
50% 30.0 ~1.4 days
100% 60.0 ~17 hours
Typical trigger is a weak link where the TCP handshake succeeds but the peer
immediately RSTs (overloaded or rate-limiting APRS-IS server). Once fds run out
nothing in the process can open a socket or file any more: TLE updates, AMSAT
status and WaveLog uploads all start failing with no obvious cause.
Each path now keeps a local reference to the socket it opened and closes it in
the catch block, also clearing the stream/socket fields so a half-initialised
connection is not mistaken for a live one.
Verified: :core:data:compileReleaseKotlin BUILD SUCCESSFUL; grep confirms all
five close calls are present.
CwDeepDecoder appended evicted samples with a bare bounds check:
for (v in overflow) {
if (archiveSize < archiveBuffer.size) archiveBuffer[archiveSize++] = v
}
if (archiveSize >= ARCHIVE_THRESHOLD) { flush() }
Once archiveBuffer (64000 samples / 20 s) filled up mid-batch the remaining
samples were silently discarded, because the flush only ran after the loop.
Worst measured case: 47999 samples already accumulated (just under the 48000
flush threshold, so no flush) plus a 64000-sample overflow batch means 111999
samples pushed into a 64000 buffer -> 47999 dropped, i.e. 15 s of audio missing
from the permanently archived CW history.
Now the buffer is flushed as soon as it is full and before appending, so every
sample reaches archiveDecode. Simulation over five batch patterns: dropped
count goes 47999 -> 0 for the worst case and all 111999 samples are archived.
Bounds: single append() can evict at most capacity samples, so drainOverflow()
returns at most 64000 - archiveBuffer never needs to grow.
The previous guard (if (_isCalculating.value) return) silently dropped
concurrent calls. Every call carries filter settings the user just applied,
so a dropped one left the list showing results for the previous filter:
User clicks 'Apply' with elevation>=5
-> UI updates to show elevation>=5
-> calculatePasses(elevation>=5) called
-> but if _isCalculating=true, return immediately
-> list still shows elevation>=30 results
The guard window is wide: delay(1000) + real calculation time (hundreds
of ms to seconds), exactly when the progress indicator spins and users
naturally interact again.
Mutex serializes calls instead: the second one queues and eventually runs
with its own parameters. This also fixes the original concurrency issue
(duplicate parallel calculations) and adds finally {} so a thrown exception
cannot leave isCalculating stuck at true (frozen progress indicator).
Reverts the regression introduced in the previous attempt to add concurrency
protection.
Without key(entry.id), Compose reuses component state by position.
When the list reorders mid-countdown (new QSO inserted at index 0,
or QRZ grid backfill triggers refreshTick++), the pending deletion
transfers to a different record and removes the wrong one.
Affected screens: LogTab and WavelogLogScreen.
Android AAPT requires single quotes in string resources to be
escaped as \' to avoid being interpreted as the start of an
escape sequence. The unescaped Ayarlar'ı triggered:
'Invalid unicode escape sequence in string'
values-tr/strings.xml:108 Ayarlar'ı → Ayarlar\'ı
Increment versionCode 461 → 462 to allow reinstallation over the existing
v4.5.7 APK (required for覆盖发行版 to work on user devices).
Update whatsnew in all 4 locales (en/zh/tr/id+in) to document the 10 bug
fixes shipped in this release:
- Menu layout: Settings永久消失, AMSAT/WavelogLog forced migration
- DataParser: epoch parsing for UTC 00:00:01–00:01:26
- Radar: auto-switch to next pass, live Doppler offset
- Passes: division by zero in progress calculation
- SatelliteRepo: concurrent calculatePasses race
- WaveLog: duplicate QSO submission, grid square update race
Release notes now include both the DeepCW fp32 migration and the 10 fixes.
Satellite QSOs uploaded with BAND=SAT, which is not a legal ADIF Band
enumeration value (the legal values are concrete bands: 160M/80M/.../
2M/70CM/23CM...). Loggers that fail to parse an unknown band fall back
to a default — observed as QSOs landing in 160m. SAT is only legal as
PROP_MODE (propagation mode), which is already sent for v1.
Changes (WaveLogApi):
- bandFromHz(): map TX frequency to the real ADIF band (2M for VHF,
70CM for UHF, etc.)
- satModeFrom(): derive the ADIF SAT_MODE convention string from TX/RX
bands ("V/U" = VHF up / UHF down, "U/V", "V/S", "U/S"...; empty for
same-band links)
- v2 JSON: band=<real band>, add sat_mode when non-empty
- v1 ADIF: <band:> real band, add <sat_mode:> when non-empty;
PROP_MODE=SAT kept
Verification:
- New tests: SO-50 (145.850 up / 436.795 down) -> band 2M, sat_mode V/U;
AO-73 (435.150 up / 145.950 down) -> band 70CM, sat_mode U/V;
same-band -> empty sat_mode; satellite freqs never map to 160M.
- All wavelog payload tests + full domain suite green.
The fldigi port ran a fixed 600 Hz NCO, so any real signal not inside
600±75 Hz (the 150 Hz filter passband) decoded nothing — the decode rate
was effectively zero unless the tone happened to be on frequency. This
mirrors the behaviour of the removed channelTracker: a sliding spectral
peak detector now steers the NCO to the strongest tone.
Changes:
- Collect raw input, run a 512-pt Hann-windowed FFT every frame, find
the strongest bin in 300..1500 Hz (CW range), smooth-track it.
- First strong peak locks immediately (no RX reset, so the triggering
element survives); later large jumps (>120 Hz) retune and reset the
fldigi state machine; small drifts are eased at 20%.
- Absolute energy floor (peak < 30) so silence/noise never steers.
- estimatedPitch now reflects the tracked tone frequency.
Verification:
- New unit test: 900 Hz "CQ" with decoder initialized at 600 Hz decodes
correctly and pitch moves to ~900 Hz.
- All 9 decoder tests pass; full domain/cw/radar test suites green.
The waterfall backed its pixels with a plain FloatArray and never
signalled Compose, so the Canvas drew once (empty) and stayed frozen —
no spectrum ever appeared. Add a monotonic frame-counter State that
pushSamples bumps per FFT frame; the Canvas reads it in composition to
trigger redraws. Also switch to log-ish intensity scaling so quiet bins
stay dark while strong CW tones pop, matching the DeepCW look.
Applies to both the CW decode screen and the radar transceiver panel.
ic_baseline_delete/pause/save/share_24.xml were leftovers from the
Morse Expert View-based UI. They reference ?attr/colorControlNormal
which does not resolve in the release variant (no Material dependency
in feature:cw), breaking assembleRelease. The new Compose UI uses
icons from core:presentation, so these files are dead code.
Background:
The CW decoder previously shipped a decompiled copy of the proprietary
Morse Expert 1.15 (com/ve3nea/morse_expert + obfuscated classes,
libnativedecoderjni.so, suncompat black-magic) — a copyright liability.
This removes all of it and reimplements the decoder on the open-source
fldigi (GPL v3) CW engine as a faithful pure-Kotlin port with no JNI.
Changes:
- Delete all Morse Expert reverse-engineered code: MainActivity,
obfuscated packages (B/B0/D/E2/...), suncompat/, pas/nativedecoder,
armeabi-v7a libnativedecoderjni.so, and the original View-based layouts
(activity_main, cw_panel_main, options_menu).
- Add a full fldigi CW pipeline in core/domain/cw:
- CwFldigiDsp: NCO down-conversion, FFT filter, movavg constants
- CwFftFilt: overlap-add FFT band-pass filter (fftfilt port)
- MorseTable + SomTable: full Morse code table + SOM codebook
- CwFldigiDecoder: decode_stream AGC + hysteresis, state machine,
adaptive speed tracking (5-55 WPM), SOM winner/normalize matching
- Rewrite CwDecodeScreen as pure Compose (DeepCW-style waterfall,
live decode line, history, status cards) and CwSettingsDialog
(speed/bandwidth/SOM) with no View interop.
- Replace the Morse Expert panel in TransceiversPage with a Compose
panel driving the same decoder; mic capture at 8000 Hz.
- Drop the forced armeabi-v7a abiFilters now that no native lib exists.
Verification:
- 8 unit tests pass (CQ/HELLO at 18-20 wpm, A-J at 30 wpm with
adaptive tracking, dot/dash/Farnsworth edge cases) — all decode
correctly from synthesized CW.
- :feature:cw and :feature:radar compile; app assembleDebug succeeds.
- APK contains no ve3nea/nativedecoder/morse_expert classes.
Addresses @AlanCui4080's review feedback about the manual date calculation.
Uses SimpleDateFormat to eliminate the hand-written calendar math (regex +
days-from-epoch calculation). Avoids java.time since it requires desugaring
on minSdk 24, keeping dependencies minimal.
Before: 23 lines of manual day-from-epoch calculation
After: 7 lines using SimpleDateFormat
Ref: https://github.com/rt-bishop/Look4Sat/pull/233#discussion_r1868599947
WavelogQueue serialized and deserialized every field except
gridsquare: updateGridsquare() wrote it, but save() skipped
put("gridsquare") and all() never read it back, so the QRZ-backfilled
grid was always empty at upload time (GRIDSQUARE never made it into
the ADIF). Add both directions.
WaveLog's parse_frequency() (Logbook_model.php) treats integer input
as Hz but reads string suffixes ("145.852038M" -> 145852038 Hz).
The bare-integer freq/freq_rx values in the v2 JSON envelope could be
misread as MHz by older WaveLog versions, corrupting the band
derivation (145.852 MHz showed up as 160m). ADIF string (v1/v2) was
already correct; now the extra fields match the same semantics.
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)
- P0: fetchStatus() now runs on Dispatchers.IO - the previous
synchronous URLConnection on the main thread threw
NetworkOnMainThreadException and showed "load failed" on every open
- P1: refresh button rotates a vector icon (ic_refresh) instead of the
"↻" text glyph, whose off-center font metrics made the spinner
orbit around a shifted pivot
- P2: error state gains a Retry button (4 locales); amsat_refresh
string added (5 locales)
- versionCode 456 (bump for reinstalling over 455), versionName stays 4.5.5
- Replace HTML parsing with the official AMSAT Satellite Status API v1
(catalog.php + reports.php, JSON): AmSatApiClient (pure JVM, hand
rolled ISO-8601/epoch parsing for minSdk 24) + rewritten
AmSatRepository (satellite list from catalog, reports slotted into
6 days x 12 two-hour slots, status colors per report value)
- Fix edge-to-edge: status bar / navigation bar insets on the status
page (refresh button and update time were unreachable)
- Version 4.5.5 (455), What's new in all 5 locales
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.
User reported the app installed as "FT8CN" with the FT8CN icon and
FT8 not opening. Root causes (verified by aapt badging on the release
APK):
1. module values/strings.xml carried <string name="app_name">FT8CN
which won the resource merge -> application-label became FT8CN.
Removed it; dialogs that referenced R.string.app_name now use a
module-local ft8cn_app_name (HelpDialog/ClearCacheDataDialog).
2. module shipped its own mipmap ic_launcher* (8 files) which
overrode our launcher icon -> deleted all of them.
3. MainActivity extends AppCompatActivity but the module's
<application> lost android:theme when the conflicting attributes
were stripped -> startup crash. Re-added android:theme=Theme.Ft8CN
(MaterialComponents) on the module application element.
Verified: processReleaseManifest/Resources + ft8 javac + app kotlin
BUILD SUCCESSFUL. Plan: .hermes/plans/2026-08-05_150000-v455-fix-plan.md
feature/ft8 module manifest carried an application element from the
FT8CN standalone app (allowBackup/icon/label/usesCleartextTraffic),
clashing with the main app manifest at merge (allowBackup false vs
true, icon, label). Removed the tag - components (activities, service,
permissions) remain and merge cleanly. Verified: processRelease-
MainManifest + mergeReleaseResources BUILD SUCCESSFUL.
feature/ft8 module manifest carried an application element from the
FT8CN standalone app (allowBackup/icon/label/usesCleartextTraffic),
clashing with the main app manifest at merge (allowBackup false vs
true, icon, label). Removed the tag - components (activities, service,
permissions) remain and merge cleanly. Verified: processRelease-
MainManifest + mergeReleaseResources BUILD SUCCESSFUL.
User-prioritized WaveLog fixes (4.5.5, commit-only per instruction):
- QRZ 对方网格爬虫: QrzGridClient (domain, pure JVM) fetches
https://www.qrz.com/db/{call} with user-supplied cookies (parses
EditThisCookie JSON or raw "k=v; k=v"), extracts Grid Square from
the Detail table. Cookies NEVER built in - entered in settings.
- Settings: WaveLog card top-right gear opens QRZ cookie dialog with
test query button (detects logged-in callsign from cookie, looks up
its grid, shows result or failure in the dialog).
- LogTab: on Enter, async lookup of the other station's grid ->
queue.updateGridsquare -> uploaded with QSO (postQso gridsquare).
- LoTW satellite list: 112 names embedded (lotw.arrl.org config.tq6),
normalizeSatName maps Celestrak TLE names to LoTW names (SAUDISAT-1C
-> SO-50, FUNCUBE-1 -> AO-73, DIWATA-2B -> PO-101, ZARYA/ARISS ->
ARISS); "Update sats" button in WaveLog card downloads the live list
(LotwSatellitesRepo, SharedPreferences persisted, never in build).
- RST: rst_sent/rst_rcvd = 59/59 in both v1 ADIF and v2 JSON.
- Grid mismatch dialog kept (cloud station grid vs station QTH);
QSO gridsquare no longer uses station grid.
- New strings in 5 locales; check_strings 9 files OK.
Verified: core:domain/data + feature:settings/radar + app
compileDebugKotlin BUILD SUCCESSFUL.
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.
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.
Added to the prefs_outro_thanks list after BG7NTA in all 5 locale
files (values/zh/tr/in/id), keeping the original bullet style and the
24dp spacer before the license block (user: keep bottom spacing).
Verified: check_strings 9 files OK.
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.
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.
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.
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.
- 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.
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.