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.