- 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.
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.
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.
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.
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.
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
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).
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).
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).
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).
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).
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.
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).
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.
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.
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.
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.
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).
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.
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.
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.
- versionCode 449 -> 450, versionName 4.4.9 -> 4.5.0
- pass_whatsnew_message rewritten (en/zh/tr) to describe ONLY this
release: new CW Decoder page (live Morse decoding + waterfall) and
its settings (message type, font size, 9 color themes)
Verified: check_strings.py OK (no bare apostrophes).
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).
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.
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.
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.
The previous commit message claimed the bump but the edit never
landed (script aborted on a syntax error before touching the toml);
versionName stayed 4.4.8/448. Fix the version now - the v4.4.9 tag
must carry versionName 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.
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).
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).
The online update always pulls every preset Celestrak/amsat URL and
the custom URL lived hidden behind the "Import" dialog's toggle. The
user wants a discoverable entry point to edit the custom source URLs.
Rename the data card button "Import" -> "Custom URL" (en: Custom URL,
zh: 自定义URL, tr: Özel URL). The existing dialog already hosts both
URL text fields (TLE + transceivers) and the file-import buttons, so
no dialog changes are needed - the import feature stays inside, as
the user requested.
Version bump per fork convention (feature change): 4.4.7 -> 4.4.8,
versionCode 447 -> 448. v4.4.7 release stays downloadable.
What's-new dialog updated in all three locales with this change.
Verified: settings + app compile, unit tests unaffected.
A bare apostrophe in "QTH定位器 2.0'dan" broke aapt resource
compilation (Invalid unicode escape sequence) and failed the release
build. Escape it as \' per Android string resource rules.
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.