Compare commits

..
31 Commits
Author SHA1 Message Date
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 9a54da0808 build(cw): bump to 4.5.0 with what's-new for CW decoder release
- 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).
2026-08-04 04:21:19 +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 15ab82f1c8 fix(strings): escape apostrophe in tr whatsnew entry
"UI Ayarları'nda" broke aapt again (Invalid unicode escape
sequence). Escape the apostrophe as \'.
2026-08-04 00:36:07 +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 c496d90d5a fix(version): actually bump to 4.4.9 (449)
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.
2026-08-03 18:21:52 +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
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
119 changed files with 16548 additions and 307 deletions

No files matched your search

+7
View File
@@ -11,6 +11,13 @@ val keystoreProperties = Properties().apply {
}
android {
// CW 解码 native 库仅 armeabi-v7a(照搬 Morse Expert 1.15): 全 ABI 打包会在
// arm64 设备 loadLibrary 失败, 强制 32 位兼容(用户设备为 32 位软件)
defaultConfig {
ndk {
abiFilters += listOf("armeabi-v7a")
}
}
signingConfigs {
if (keystoreProperties["storeFile"] != null) {
create("release") {
@@ -17,16 +17,23 @@
*/
package com.rtbishop.look4sat
import androidx.activity.compose.BackHandler
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateContentSize
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.RepeatMode
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.infiniteRepeatable
import androidx.compose.animation.core.rememberInfiniteTransition
import androidx.compose.animation.core.spring
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.slideInHorizontally
import androidx.compose.animation.slideOutHorizontally
import androidx.compose.animation.togetherWith
@@ -53,6 +60,9 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
@@ -80,11 +90,14 @@ import com.rtbishop.look4sat.core.presentation.RadarDestination
import com.rtbishop.look4sat.core.presentation.Screen
import com.rtbishop.look4sat.core.presentation.hasEnoughHeight
import com.rtbishop.look4sat.core.presentation.hasEnoughWidth
import com.rtbishop.look4sat.feature.cw.CwDecodeScreen
import com.rtbishop.look4sat.feature.map.MapDestination
import com.rtbishop.look4sat.feature.mutual.MutualScreen
import com.rtbishop.look4sat.feature.mutual.MutualViewModel
import com.rtbishop.look4sat.feature.passes.PassesDestination
import com.rtbishop.look4sat.feature.radar.RadarDestination
import com.rtbishop.look4sat.feature.radar.WavelogLogScreen
import com.rtbishop.look4sat.feature.status.SatStatusScreen
import com.rtbishop.look4sat.feature.roaming.RoamingScreen
import com.rtbishop.look4sat.feature.satellites.SatellitesDestination
import com.rtbishop.look4sat.feature.settings.SettingsDestination
@@ -131,12 +144,35 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
val currentKey = backStack.lastOrNull()
val navigateBack: () -> Unit = { backStack.removeLastOrNull() }
val fadeTransition = fadeIn(animationSpec = tween(350)) togetherWith fadeOut(animationSpec = tween(350))
val navItems = listOf(Screen.Satellites, Screen.Passes, Screen.Radar, Screen.Mutual, Screen.Roaming, Screen.Map, Screen.Settings)
val context = LocalContext.current
val container = (context.applicationContext as IContainerProvider).getMainContainer()
val trackingState by container.radioTrackingService.state.collectAsStateWithLifecycle()
val otherSettings by container.settingsRepo.otherSettings.collectAsStateWithLifecycle()
// UI 设置: 按 screenOrder 排序(空 = 默认顺序), 再按 hiddenScreens 过滤(设置页固定保留)
val allNavItems = listOf(Screen.Satellites, Screen.Passes, Screen.Radar, Screen.Mutual, Screen.Roaming, Screen.CwDecode, Screen.WavelogLog, Screen.AmSat, Screen.Map, Screen.Settings)
.sortedBy { screen ->
// 未知(新页面如 CwDecode 不在旧持久化顺序里): 用默认顺序位置(漫游↔地图), 再兜底最后
val idx = otherSettings.screenOrder.indexOf(screen.screenId)
if (idx != -1) idx
else com.rtbishop.look4sat.core.presentation.defaultScreenOrder.indexOf(screen.screenId).let {
if (it != -1) it else Int.MAX_VALUE
}
}
.filter { it.screenId !in otherSettings.hiddenScreens || it is Screen.Settings }
// 4.5.1 折叠菜单: 主菜单(底部栏 5 槽) + 更多菜单(溢出页面)
// 老用户迁移: 已持久化的 subMenuOrder 不含新页面 WavelogLog → 追加到子菜单尾部
val subOrder = (otherSettings.subMenuOrder.ifEmpty { com.rtbishop.look4sat.core.presentation.defaultSubMenuOrder })
.let { list -> if ("WavelogLog" in list) list else list + "WavelogLog" }
.let { list -> if ("AMSAT" in list) list else list + "AMSAT" }
val mainNavItems = remember(allNavItems, subOrder) {
allNavItems.filter { it.screenId !in subOrder }.take(5)
}
val moreNavItems = remember(allNavItems, subOrder) {
subOrder.mapNotNull { id -> allNavItems.find { it.screenId == id } }
}
var moreExpanded by remember { mutableStateOf(false) }
// 更多菜单打开时拦截返回: 先关菜单
BackHandler(enabled = moreExpanded) { moreExpanded = false }
// Activity-scoped so the mutual query results survive navigation to Radar and back
val mutualViewModel: MutualViewModel = viewModel(
viewModelStoreOwner = context as ViewModelStoreOwner,
@@ -151,12 +187,15 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
) {
NavigationSuiteScaffold(
navigationSuiteItems = {
navItems.forEach { screen ->
mainNavItems.forEach { screen ->
val isSelected = when (currentKey) {
is Screen.Satellites -> screen is Screen.Satellites
is Screen.Passes -> screen is Screen.Passes
is Screen.Radar -> screen is Screen.Radar
is Screen.Mutual -> screen is Screen.Mutual
is Screen.CwDecode -> screen is Screen.CwDecode
is Screen.WavelogLog -> screen is Screen.WavelogLog
is Screen.AmSat -> screen is Screen.AmSat
is Screen.Map -> screen is Screen.Map
is Screen.Settings -> screen is Screen.Settings
else -> false
@@ -167,11 +206,26 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
selected = isSelected,
onClick = {
if (isSelected) return@item
moreExpanded = false
while (backStack.size > 1) backStack.removeAt(backStack.size - 1)
if (screen !is Screen.Passes) backStack.add(screen)
}
)
}
// 更多菜单按钮(固定第 6 槽, 子菜单非空才显示)
if (moreNavItems.isNotEmpty()) {
item(
icon = {
Icon(
painterResource(com.rtbishop.look4sat.R.drawable.ic_more),
stringResource(com.rtbishop.look4sat.core.presentation.R.string.nav_more)
)
},
label = { Text(stringResource(com.rtbishop.look4sat.core.presentation.R.string.nav_more)) },
selected = moreExpanded,
onClick = { moreExpanded = !moreExpanded }
)
}
},
navigationSuiteColors = NavigationSuiteDefaults.colors(
navigationRailContainerColor = MaterialTheme.colorScheme.surfaceContainer
@@ -182,106 +236,137 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
else -> NavigationSuiteType.ShortNavigationBarMedium
}
) {
Column {
NavDisplay(
backStack = backStack,
modifier = Modifier.weight(1f),
onBack = navigateBack,
transitionSpec = { fadeTransition },
popTransitionSpec = { fadeTransition },
predictivePopTransitionSpec = { fadeTransition },
entryDecorators = listOf(
// Required for saving Compose state per entry
rememberSaveableStateHolderNavEntryDecorator(),
// Required for ViewModel scoping per entry
rememberViewModelStoreNavEntryDecorator()
),
entryProvider = entryProvider {
entry<Screen.Satellites> {
SatellitesDestination(navigateUp = navigateBack)
}
entry<Screen.Passes> {
PassesDestination { catNum, aosTime ->
container.setMutualPassData(MutualPassData())
container.satelliteRepo.selectPass(catNum, aosTime)
backStack.add(Screen.Radar)
// navigateToRadar()
Box {
Column(modifier = Modifier.fillMaxSize()) {
NavDisplay(
backStack = backStack,
modifier = Modifier.weight(1f).fillMaxWidth(),
onBack = navigateBack,
transitionSpec = { fadeTransition },
popTransitionSpec = { fadeTransition },
predictivePopTransitionSpec = { fadeTransition },
entryDecorators = listOf(
// Required for saving Compose state per entry
rememberSaveableStateHolderNavEntryDecorator(),
// Required for ViewModel scoping per entry
rememberViewModelStoreNavEntryDecorator()
),
entryProvider = entryProvider {
entry<Screen.Satellites> {
SatellitesDestination(navigateUp = navigateBack)
}
}
entry<Screen.Radar> {
RadarDestination(navigateUp = navigateBack)
}
entry<Screen.Map> {
MapDestination()
}
entry<Screen.Mutual> {
MutualScreen(
viewModel = mutualViewModel,
navigateUp = navigateBack,
navigateToRadar = { catNum, aosTime, pass ->
container.setMutualPassData(pass ?: MutualPassData())
entry<Screen.Passes> {
PassesDestination { catNum, aosTime ->
container.setMutualPassData(MutualPassData())
container.satelliteRepo.selectPass(catNum, aosTime)
backStack.add(Screen.Radar)
}
)
}
entry<Screen.Roaming> {
RoamingScreen()
}
entry<Screen.Settings> {
SettingsDestination()
}
}
)
// Radio tracking status banner
if (trackingState.isActive) {
val infiniteTransition = rememberInfiniteTransition(label = "trackingPulse")
val alpha by infiniteTransition.animateFloat(
initialValue = 1f, targetValue = 0.4f,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
), label = "pulseAlpha"
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.background(MaterialTheme.colorScheme.primaryContainer)
.clickable {
val pass = trackingState.currentPass
if (pass != null) {
container.setMutualPassData(MutualPassData())
container.satelliteRepo.selectPass(pass.catNum, pass.aosTime)
backStack.add(Screen.Radar)
// navigateToRadar()
}
}
.padding(horizontal = 12.dp, vertical = 6.dp)
) {
Box(
entry<Screen.Radar> {
RadarDestination(navigateUp = navigateBack)
}
entry<Screen.Map> {
MapDestination()
}
entry<Screen.Mutual> {
MutualScreen(
viewModel = mutualViewModel,
navigateUp = navigateBack,
navigateToRadar = { catNum, aosTime, pass ->
container.setMutualPassData(pass ?: MutualPassData())
container.satelliteRepo.selectPass(catNum, aosTime)
backStack.add(Screen.Radar)
}
)
}
entry<Screen.Roaming> {
RoamingScreen()
}
entry<Screen.CwDecode> {
CwDecodeScreen()
}
entry<Screen.AmSat> {
SatStatusScreen(container = container)
}
entry<Screen.WavelogLog> {
WavelogLogScreen(queue = container.wavelogQueue)
}
entry<Screen.Settings> {
SettingsDestination()
}
}
)
// Radio tracking status banner
if (trackingState.isActive) {
val infiniteTransition = rememberInfiniteTransition(label = "trackingPulse")
val alpha by infiniteTransition.animateFloat(
initialValue = 1f, targetValue = 0.4f,
animationSpec = infiniteRepeatable(
animation = tween(1000, easing = LinearEasing),
repeatMode = RepeatMode.Reverse
), label = "pulseAlpha"
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.size(8.dp)
.clip(CircleShape)
.background(Color(0xFF4CAF50).copy(alpha = alpha))
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = "Tracking: ${trackingState.currentPass?.name ?: ""}",
fontSize = 13.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.weight(1f)
)
val txOk = if (trackingState.txConnected) "TX" else ""
val rxOk = if (trackingState.rxConnected) "RX" else ""
Text(
text = listOf(txOk, rxOk).filter { it.isNotBlank() }.joinToString("/"),
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
.fillMaxWidth()
.background(MaterialTheme.colorScheme.primaryContainer)
.clickable {
val pass = trackingState.currentPass
if (pass != null) {
container.setMutualPassData(MutualPassData())
container.satelliteRepo.selectPass(pass.catNum, pass.aosTime)
backStack.add(Screen.Radar)
}
}
.padding(horizontal = 12.dp, vertical = 6.dp)
) {
Box(
modifier = Modifier
.size(8.dp)
.clip(CircleShape)
.background(Color(0xFF4CAF50).copy(alpha = alpha))
)
Spacer(modifier = Modifier.width(8.dp))
Text(
text = stringResource(com.rtbishop.look4sat.core.presentation.R.string.tracking_status, trackingState.currentPass?.name ?: ""),
fontSize = 13.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onPrimaryContainer,
modifier = Modifier.weight(1f)
)
val txOk = if (trackingState.txConnected) "TX" else ""
val rxOk = if (trackingState.rxConnected) "RX" else ""
Text(
text = listOf(txOk, rxOk).filter { it.isNotBlank() }.joinToString("/"),
fontSize = 12.sp,
color = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
}
// 更多菜单弹出面板(覆盖在内容上, 底部栏上方; spring 弹跳)
AnimatedVisibility(
visible = moreExpanded,
modifier = Modifier.fillMaxSize(),
enter = expandVertically(
animationSpec = spring(dampingRatio = Spring.DampingRatioMediumBouncy)
) + fadeIn(),
exit = shrinkVertically() + fadeOut()
) {
MoreMenuPopup(
items = moreNavItems,
currentKey = currentKey,
onDismiss = { moreExpanded = false },
onSelect = { screen ->
moreExpanded = false
while (backStack.size > 1) backStack.removeAt(backStack.size - 1)
if (screen !is Screen.Passes) backStack.add(screen)
}
)
}
}
}
}
}
}
@@ -0,0 +1,106 @@
/*
* MoreMenuPopup.kt — 底部导航「更多」二级菜单弹出面板(4.5.1)。
*
* 覆盖在内容区上(底部栏上方, 右对齐), 竖排菜单项(图标+文本+箭头),
* 当前页高亮; 点击遮罩关闭, 点击项跳转。弹出/收起动画由调用处
* (MainScreen 的 AnimatedVisibility + spring)驱动。
*/
package com.rtbishop.look4sat
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.navigation3.runtime.NavKey
import com.rtbishop.look4sat.core.presentation.Screen
@Composable
fun MoreMenuPopup(
items: List<Screen>,
currentKey: NavKey?,
onDismiss: () -> Unit,
onSelect: (Screen) -> Unit
) {
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.scrim.copy(alpha = 0.35f))
.clickable(onClick = onDismiss)
) {
Card(
modifier = Modifier
.align(Alignment.BottomEnd)
.padding(12.dp),
shape = RoundedCornerShape(12.dp),
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.surfaceContainerHigh
)
) {
Column(modifier = Modifier.padding(vertical = 4.dp)) {
items.forEach { screen ->
val isSelected = when (currentKey) {
is Screen.Satellites -> screen is Screen.Satellites
is Screen.Passes -> screen is Screen.Passes
is Screen.Radar -> screen is Screen.Radar
is Screen.Mutual -> screen is Screen.Mutual
is Screen.CwDecode -> screen is Screen.CwDecode
is Screen.WavelogLog -> screen is Screen.WavelogLog
is Screen.Map -> screen is Screen.Map
is Screen.Settings -> screen is Screen.Settings
else -> false
}
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable { onSelect(screen) }
.padding(horizontal = 16.dp, vertical = 12.dp)
) {
Icon(
painter = painterResource(screen.iconResId),
contentDescription = stringResource(screen.titleResId),
tint = if (isSelected) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.size(20.dp)
)
Spacer(modifier = Modifier.width(12.dp))
Text(
text = stringResource(screen.titleResId),
fontSize = 14.sp,
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal,
color = if (isSelected) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f)
)
Text(
text = "›",
fontSize = 16.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}
}
}
+9
View File
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M12,8c1.1,0 2,-0.9 2,-2s-0.9,-2 -2,-2 -2,0.9 -2,2 0.9,2 2,2zM12,10c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2zM12,16c-1.1,0 -2,0.9 -2,2s0.9,2 2,2 2,-0.9 2,-2 -0.9,-2 -2,-2z" />
</vector>
@@ -31,6 +31,7 @@ internal class ApplicationPlugin : Plugin<Project> {
implementation(project(":core:data"))
implementation(project(":core:domain"))
implementation(project(":core:presentation"))
implementation(project(":feature:cw"))
implementation(project(":feature:map"))
implementation(project(":feature:mutual"))
implementation(project(":feature:passes"))
@@ -38,6 +39,7 @@ internal class ApplicationPlugin : Plugin<Project> {
implementation(project(":feature:roaming"))
implementation(project(":feature:satellites"))
implementation(project(":feature:settings"))
implementation(project(":feature:status"))
implementation(libs.androidx.core.splashscreen)
implementation(libs.compose.material3.adaptive)
implementation(libs.compose.navigation3)
@@ -29,6 +29,7 @@ import com.rtbishop.look4sat.core.data.framework.Ft817Controller
import com.rtbishop.look4sat.core.data.framework.Ic705Controller
import com.rtbishop.look4sat.core.data.framework.NetworkReporter
import com.rtbishop.look4sat.core.data.framework.RadioTrackingService
import com.rtbishop.look4sat.core.data.repository.AmSatRepository
import com.rtbishop.look4sat.core.data.repository.DatabaseRepo
import com.rtbishop.look4sat.core.data.repository.SatelliteRepo
import com.rtbishop.look4sat.core.data.repository.SelectionRepo
@@ -40,6 +41,9 @@ import com.rtbishop.look4sat.core.data.usecase.AddToCalendar
import com.rtbishop.look4sat.core.data.usecase.AudioCapture
import com.rtbishop.look4sat.core.data.usecase.SaveImage
import com.rtbishop.look4sat.core.data.usecase.ShowToast
import com.rtbishop.look4sat.core.domain.wavelog.IWavelogQueueStore
import com.rtbishop.look4sat.core.domain.wavelog.WavelogQueue
import com.rtbishop.look4sat.core.domain.wavelog.WavelogUploader
import com.rtbishop.look4sat.core.domain.model.RadioControlSettings
import com.rtbishop.look4sat.core.domain.repository.IDatabaseRepo
import com.rtbishop.look4sat.core.domain.repository.IMainContainer
@@ -70,12 +74,14 @@ import okhttp3.OkHttpClient
class MainContainer(private val context: Context) : IMainContainer {
private val localSource = provideLocalSource()
private val remoteSource by lazy { provideRemoteSource() }
private val mainHandler = CoroutineExceptionHandler { _, error -> println("MainHandler: $error") }
override val appScope = CoroutineScope(SupervisorJob() + Dispatchers.Default + mainHandler)
override val settingsRepo = provideSettingsRepo()
override val selectionRepo = provideSelectionRepo()
override val satelliteRepo = provideSatelliteRepo()
override val databaseRepo = provideDatabaseRepo()
override val amSatRepo = AmSatRepository(remoteSource)
override val radioTrackingService: IRadioTrackingService by lazy {
val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
RadioTrackingService(appScope, manager, satelliteRepo, settingsRepo)
@@ -96,6 +102,16 @@ class MainContainer(private val context: Context) : IMainContainer {
override fun provideSaveImage(): ISaveImage = SaveImage(context)
// WaveLog 日志(4.5.2): 本地队列 + 上传器(共享实例)
override val wavelogQueue: WavelogQueue by lazy {
val prefs = context.getSharedPreferences("wavelog", Context.MODE_PRIVATE)
WavelogQueue(object : IWavelogQueueStore {
override fun load(): String = prefs.getString("wavelog_queue", "[]") ?: "[]"
override fun save(json: String) = prefs.edit().putString("wavelog_queue", json).apply()
})
}
override fun provideWavelogUploader(): WavelogUploader = WavelogUploader(settingsRepo, wavelogQueue)
override fun provideBluetoothReporter(): IReporter {
val manager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
val rc = settingsRepo.rcSettings.value
@@ -149,7 +165,6 @@ class MainContainer(private val context: Context) : IMainContainer {
private fun provideDatabaseRepo(): IDatabaseRepo {
val dbDispatcher = Dispatchers.Default
val dataParser = DataParser(dbDispatcher)
val remoteSource = provideRemoteSource()
return DatabaseRepo(dbDispatcher, dataParser, localSource, remoteSource, settingsRepo)
}
@@ -160,7 +175,14 @@ class MainContainer(private val context: Context) : IMainContainer {
}
private fun provideRemoteSource(): IRemoteSource {
return RemoteSource(Dispatchers.IO, context.contentResolver, OkHttpClient.Builder().build())
return RemoteSource(
Dispatchers.IO, context.contentResolver,
OkHttpClient.Builder()
.connectTimeout(15, java.util.concurrent.TimeUnit.SECONDS)
.readTimeout(20, java.util.concurrent.TimeUnit.SECONDS)
.writeTimeout(20, java.util.concurrent.TimeUnit.SECONDS)
.build()
)
}
private fun provideSatelliteRepo(): ISatelliteRepo {
@@ -176,6 +198,6 @@ class MainContainer(private val context: Context) : IMainContainer {
val appPrefsFileName = "${context.packageName}_preferences"
val appPreferences = context.getSharedPreferences(appPrefsFileName, Context.MODE_PRIVATE)
val appVersionName = context.packageManager.getPackageInfo(context.packageName, 0).versionName ?: "4.0.4"
return SettingsRepo(manager, appPreferences, appVersionName)
return SettingsRepo(context, manager, appPreferences, appVersionName)
}
}
@@ -0,0 +1,21 @@
package com.rtbishop.look4sat.core.data.repository
import com.rtbishop.look4sat.core.data.source.AmSatParser
import com.rtbishop.look4sat.core.domain.model.SatStatusPage
import com.rtbishop.look4sat.core.domain.repository.IAmSatRepository
import com.rtbishop.look4sat.core.domain.source.IRemoteSource
/** AMSAT 状态仓库: 抓 HTML → 解析 → SatStatusPage */
class AmSatRepository(private val remoteSource: IRemoteSource) : IAmSatRepository {
override suspend fun fetchStatus(): SatStatusPage? {
val html = remoteSource.getStatusHtml() ?: return null
if (html.isBlank()) return null
return try {
AmSatParser.parse(html, System.currentTimeMillis())
} catch (exception: Exception) {
println("AmSatRepository parse exception: $exception")
null
}
}
}
@@ -81,14 +81,21 @@ class DatabaseRepo(
// launch all network requests concurrently
val tleJobs = tleUrls.values.map { url -> async { url to remoteSource.getNetworkStream(url) } }
val radioJobs = radioUrls.values.map { url -> async { url to remoteSource.getNetworkStream(url) } }
// 统计成功源数: 0 成功视为更新失败(不刷时间戳, 抛异常让 UI 提示)
val tleResults = tleJobs.awaitAll()
val radioResults = radioJobs.awaitAll()
val successCount = tleResults.count { it.second != null } + radioResults.count { it.second != null }
if (successCount == 0) {
throw java.io.IOException("All data sources failed to download")
}
// parse fetched data concurrently and associate with types
val importedEntries = tleJobs.awaitAll().flatMap { (url, stream) ->
val importedEntries = tleResults.flatMap { (url, stream) ->
val type = tleUrls.entries.find { it.value == url }?.key ?: customSourceType
stream?.let { parseSatelliteStream(url, unwrapIfZipped(url, it)) }.orEmpty().also { entries ->
settingsRepo.setSatelliteTypeIds(type, entries.map { it.catnum })
}
}
val importedRadios = radioJobs.awaitAll().flatMap { (url, stream) ->
val importedRadios = radioResults.flatMap { (url, stream) ->
stream?.let { dataParser.parseJSONStream(unwrapIfZipped(url, it)) }.orEmpty()
}
// insert parsed data into the database
@@ -40,6 +40,7 @@ import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.update
class SettingsRepo(
private val context: android.content.Context,
private val locationManager: LocationManager,
private val preferences: SharedPreferences,
override val appVersionName: String
@@ -77,6 +78,8 @@ class SettingsRepo(
private val keyStateOfUtc = "stateOfUtc"
private val keyStateOfLightTheme = "stateOfLightTheme"
private val keyStateOfNightMode = "stateOfNightMode"
private val keyHiddenScreens = "hiddenScreens"
private val keyScreenOrder = "screenOrder"
private val keyStationAltitude = "stationAltitude"
private val keyStationLatitude = "stationLatitude"
private val keyStationLongitude = "stationLongitude"
@@ -92,6 +95,11 @@ class SettingsRepo(
private val keyUseCustomTransceivers = "useCustomTransceivers"
private val keyTleUrl = "tleUrl"
private val keyTransceiversUrl = "transceiversUrl"
private val keySubMenuOrder = "subMenuOrder"
private val keyWavelogUrl = "wavelogUrl"
private val keyWavelogApiKey = "wavelogApiKey"
private val keyWavelogStationId = "wavelogStationId"
private val keyWavelogAutoUpload = "wavelogAutoUpload"
private val separatorComma = ","
//region # Satellites selection settings
@@ -179,23 +187,41 @@ class SettingsRepo(
return true
}
override fun setStationPosition(): Boolean {
if (!LocationManagerCompat.isLocationEnabled(locationManager)) return false
try {
val hasGps = LocationManagerCompat.hasProvider(locationManager, providerGps)
val hasNet = LocationManagerCompat.hasProvider(locationManager, providerNet)
val provider = if (hasGps) providerGps else if (hasNet) providerNet else providerDef
val location = locationManager.getLastKnownLocation(providerDef)
if (location == null || System.currentTimeMillis() - location.time > 600_000L) {
println("Requesting location for $provider provider")
locationManager.requestLocationUpdates(provider, 0L, 0f, this)
} else {
setStationPosition(location.latitude, location.longitude, location.altitude)
}
} catch (exception: SecurityException) {
println("No permissions were given - $exception")
/** GPS 定位: 一次性 getCurrentLocation(GPS 优先, 15 秒超时), 拿到位置才返回 true */
override suspend fun setStationPosition(): Boolean {
// 权限前置: 无定位权限直接失败(不吞异常)
if (androidx.core.content.ContextCompat.checkSelfPermission(
context, android.Manifest.permission.ACCESS_FINE_LOCATION
) != android.content.pm.PackageManager.PERMISSION_GRANTED
) {
println("GPS: no fine location permission")
return false
}
if (!LocationManagerCompat.isLocationEnabled(locationManager)) return false
return kotlinx.coroutines.suspendCancellableCoroutine { cont ->
val signal = android.os.CancellationSignal()
val handler = android.os.Handler(android.os.Looper.getMainLooper())
val executor = java.util.concurrent.Executor { handler.post(it) }
// 15 秒超时
val timeout = handler.postDelayed({
signal.cancel()
if (cont.isActive) cont.resume(false) { }
}, 15_000L)
val listener = androidx.core.util.Consumer<Location> { location ->
handler.removeCallbacksAndMessages(null)
setStationPosition(location.latitude, location.longitude, location.altitude)
if (cont.isActive) cont.resume(true) { }
}
val hasGps = LocationManagerCompat.hasProvider(locationManager, providerGps)
val provider = if (hasGps) providerGps else providerNet
try {
LocationManagerCompat.getCurrentLocation(locationManager, provider, signal, executor, listener)
} catch (exception: SecurityException) {
handler.removeCallbacksAndMessages(null)
if (cont.isActive) cont.resume(false) { }
}
cont.invokeOnCancellation { signal.cancel(); handler.removeCallbacksAndMessages(null) }
}
return true
}
override fun setStationPosition(locator: String): Boolean {
@@ -357,6 +383,13 @@ class SettingsRepo(
putString(keySstvMode, new.sstvMode)
putLong(keyLowElevation, new.lowElevation.toRawBits())
putLong(keyHighElevation, new.highElevation.toRawBits())
putStringSet(keyHiddenScreens, new.hiddenScreens.toSet())
putString(keyScreenOrder, new.screenOrder.joinToString(","))
putString(keySubMenuOrder, new.subMenuOrder.joinToString(","))
putString(keyWavelogUrl, new.wavelogUrl)
putString(keyWavelogApiKey, new.wavelogApiKey)
putString(keyWavelogStationId, new.wavelogStationId)
putBoolean(keyWavelogAutoUpload, new.wavelogAutoUpload)
}
new
}
@@ -372,8 +405,15 @@ class SettingsRepo(
shouldSeeWarning = preferences.getBoolean(keyShouldSeeWarning, true),
shouldSeeWhatsNew = preferences.getBoolean(keyShouldSeeWhatsNew, true),
sstvMode = preferences.getString(keySstvMode, null) ?: "Auto",
hiddenScreens = preferences.getStringSet(keyHiddenScreens, emptySet())?.toList() ?: emptyList(),
screenOrder = preferences.getString(keyScreenOrder, null)?.split(",")?.filter { it.isNotBlank() } ?: emptyList(),
subMenuOrder = preferences.getString(keySubMenuOrder, null)?.split(",")?.filter { it.isNotBlank() } ?: emptyList(),
lowElevation = Double.fromBits(preferences.getLong(keyLowElevation, 15.0.toRawBits())),
highElevation = Double.fromBits(preferences.getLong(keyHighElevation, 45.0.toRawBits()))
highElevation = Double.fromBits(preferences.getLong(keyHighElevation, 45.0.toRawBits())),
wavelogUrl = preferences.getString(keyWavelogUrl, null) ?: "",
wavelogApiKey = preferences.getString(keyWavelogApiKey, null) ?: "",
wavelogStationId = preferences.getString(keyWavelogStationId, null) ?: "",
wavelogAutoUpload = preferences.getBoolean(keyWavelogAutoUpload, false)
)
//endregion
@@ -0,0 +1,136 @@
package com.rtbishop.look4sat.core.data.source
import com.rtbishop.look4sat.core.domain.model.SatDay
import com.rtbishop.look4sat.core.domain.model.SatReport
import com.rtbishop.look4sat.core.domain.model.SatSlot
import com.rtbishop.look4sat.core.domain.model.SatStatus
import com.rtbishop.look4sat.core.domain.model.SatStatusPage
import java.util.regex.Pattern
/**
* AMSAT 卫星状态页解析器(https://amsat.org/status/)
*
* 页面结构(静态 HTML, 2026-08 实测):
* - 状态表: <table> 内 48 行, 表头 = Name + 6 天(每个 colspan=12)
* 数据行 73 格: [0]=卫星名, [1..72] = 6 天 × 12 个 2 小时槽(新→旧)
* 每格 <td width=9 bgcolor="颜色">数字</td>, 有报告时数字带
* docTips.show('id') 链接
* - 报告详情在页面内嵌 JS:
* tips.a885153 = new Array(5,5,120,'状态<br>呼号<br>网格<br>日期<br>时间段 UTC')
*
* 状态色: #648fff=活跃, #ffb000=仅遥测, #dc267f=未听到, #fe6100=矛盾
*/
object AmSatParser {
private val STATUS_COLORS = mapOf(
"#648fff" to 0xFF648FFF.toLong(),
"#ffb000" to 0xFFFFB000.toLong(),
"#dc267f" to 0xFFDC267F.toLong(),
"#fe6100" to 0xFFFE6100.toLong()
)
private val GRAY = 0xFFC0C0C0.toLong()
private val rowRe = Pattern.compile("<tr>(.*?)</tr>", Pattern.DOTALL)
private val cellRe = Pattern.compile("(<t[dh][^>]*>.*?</t[dh]>)", Pattern.DOTALL)
private val bgRe = Pattern.compile("bgcolor=\"?(#[0-9a-fA-F]{6}|C0C0C0)\"?")
private val linkRe = Pattern.compile("docTips\\.show\\('(a\\d+)'\\)")
private val tipRe = Pattern.compile(
"tips\\.(a\\d+)\\s*=\\s*new\\s+Array\\(\\s*\\d+,\\s*\\d+,\\s*\\d+,\\s*'([^']*)'"
)
/** 解析完整页面 */
fun parse(html: String, fetchedAtUtcMs: Long): SatStatusPage {
val reports = parseReports(html)
val statuses = parseStatusTable(html, reports)
return SatStatusPage(fetchedAtUtcMs, statuses, reports)
}
/** 提取全部报告(tips.* JS 数组) */
fun parseReports(html: String): Map<String, SatReport> {
val map = mutableMapOf<String, SatReport>()
val m = tipRe.matcher(html)
while (m.find()) {
val id = m.group(1)
val parts = m.group(2).split("<br>")
map[id] = SatReport(
id = id,
statusText = parts.getOrElse(0) { "" }.trim(),
call = parts.getOrElse(1) { "" }.trim(),
grid = parts.getOrElse(2) { "" }.trim(),
dateUtc = parts.getOrElse(3) { "" }.trim(),
timeUtc = parts.getOrElse(4) { "" }.trim()
)
}
return map
}
/** 解析状态表(48 行卫星) */
fun parseStatusTable(html: String, reports: Map<String, SatReport>): List<SatStatus> {
val result = mutableListOf<SatStatus>()
val tables = extractTables(html)
for (table in tables) {
val rows = rowRe.matcher(table)
val parsed = mutableListOf<SatStatus>()
var rowIndex = 0
var dayHeaders: List<String> = emptyList()
while (rows.find()) {
val rowHtml = rows.group(1)
val cells = cellRe.matcher(rowHtml)
val cellList = mutableListOf<String>()
while (cells.find()) cellList.add(cells.group(1))
if (rowIndex == 0) {
// 表头: Name + 6 天(每个 colspan=12)
dayHeaders = cellList.drop(1).take(6).map { stripHtml(it) }
rowIndex++
continue
}
if (cellList.size < 7) { rowIndex++; continue }
val name = stripHtml(cellList[0])
if (name.isBlank()) { rowIndex++; continue }
val days = mutableListOf<SatDay>()
for (d in 0 until 6) {
val start = 1 + d * 12
val end = start + 12
val slots = (start until end).mapNotNull { i ->
cellList.getOrNull(i)?.let { cell ->
val bg = bgRe.matcher(cell)
val color = if (bg.find()) {
STATUS_COLORS[bg.group(1).lowercase()] ?: GRAY
} else GRAY
val link = linkRe.matcher(cell)
val ids = mutableListOf<String>()
while (link.find()) ids.add(link.group(1))
val hasReport = ids.isNotEmpty()
val count = if (hasReport) {
stripHtml(cell).trim().toIntOrNull() ?: ids.size
} else 0
SatSlot(
statusColor = if (hasReport) color else GRAY,
count = count,
reportIds = ids
)
}
}
days.add(SatDay(dayHeaders.getOrElse(d) { "" }, slots))
}
parsed.add(SatStatus(name, days))
rowIndex++
}
if (parsed.isNotEmpty()) {
result.addAll(parsed)
break
}
}
return result
}
private fun extractTables(html: String): List<String> {
val result = mutableListOf<String>()
val m = Pattern.compile("<table.*?</table>", Pattern.DOTALL).matcher(html)
while (m.find()) result.add(m.group())
return result
}
private fun stripHtml(s: String): String =
s.replace(Regex("<[^>]+>"), "").trim()
}
@@ -43,6 +43,22 @@ class RemoteSource(
}
}
override suspend fun getStatusHtml(): String? = withContext(dispatcher) {
try {
val request = Request.Builder()
.url("https://amsat.org/status/")
.header("User-Agent", "Mozilla/5.0 (Linux; Android 13) Look4Sat/4.5")
.build()
httpClient.newCall(request).execute().use { response ->
if (!response.isSuccessful) return@use null
response.body?.string()
}
} catch (exception: Exception) {
println("RemoteSource amsat status exception: $exception")
null
}
}
override suspend fun getNetworkStream(url: String): InputStream? = withContext(dispatcher) {
try {
val networkRequest = Request.Builder().url(url).build()
@@ -25,4 +25,8 @@ class ShowToast(private val context: Context) : IShowToast {
override fun invoke(message: String) {
Toast.makeText(context, message, Toast.LENGTH_SHORT).show()
}
override fun invoke(resId: Int) {
invoke(context.getString(resId))
}
}
+5
View File
@@ -1,3 +1,8 @@
plugins {
alias(libs.plugins.convention.coreDomainPlugin)
}
dependencies {
// 编译期使用 org.json(构造/解析 WaveLog API 请求体); 运行时用 Android 系统自带的 org.json
compileOnly("org.json:json:20240303")
}
@@ -0,0 +1,37 @@
package com.rtbishop.look4sat.core.domain.model
/** 单条卫星状态报告(AMSAT 网站 tooltip 数据) */
data class SatReport(
val id: String, // 报告 ID(a885153)
val statusText: String, // Heard / Telemetry Only / Not Heard ...
val call: String, // 呼号
val grid: String, // 网格坐标(可为空)
val dateUtc: String, // 2026-08-04
val timeUtc: String // 2:46-:59 UTC
)
/** 单个 2 小时槽的状态 */
data class SatSlot(
val statusColor: Long, // ARGB 状态色(-1 = 无报告)
val count: Int, // 报告数量(0 = 无)
val reportIds: List<String> = emptyList() // 该槽报告 ID 列表
)
/** 卫星一天的状态(12 个 2 小时槽) */
data class SatDay(
val dateLabel: String, // "Aug 4"
val slots: List<SatSlot> // 12 槽(00-02 ... 22-24)
)
/** 单个卫星 6 天状态 */
data class SatStatus(
val name: String, // "AO-123_[FM]"
val days: List<SatDay> // 6 天(新→旧)
)
/** 页面整体解析结果 */
data class SatStatusPage(
val fetchedAtUtcMs: Long,
val statuses: List<SatStatus>,
val reports: Map<String, SatReport> // id → 报告
)
@@ -62,7 +62,18 @@ data class OtherSettings(
val shouldSeeWhatsNew: Boolean,
val sstvMode: String = "Auto",
val lowElevation: Double = 15.0,
val highElevation: Double = 45.0
val highElevation: Double = 45.0,
// UI 设置: 底部导航栏隐藏的页面(Screen simpleName 列表, 默认空 = 全部显示)
val hiddenScreens: List<String> = emptyList(),
// UI 设置: 页面顺序(空 = 默认顺序: 卫星/过境/雷达/地图/设置)
val screenOrder: List<String> = emptyList(),
// UI 设置: 更多菜单顺序(空 = 默认: 匹配/漫游/CW解码)
val subMenuOrder: List<String> = emptyList(),
// WaveLog 日志系统(4.5.2): 服务器配置
val wavelogUrl: String = "",
val wavelogApiKey: String = "",
val wavelogStationId: String = "",
val wavelogAutoUpload: Boolean = false
)
data class DataSourcesSettings(
@@ -0,0 +1,9 @@
package com.rtbishop.look4sat.core.domain.repository
import com.rtbishop.look4sat.core.domain.model.SatStatusPage
/** AMSAT 卫星状态数据源 */
interface IAmSatRepository {
/** 抓取并解析 AMSAT 状态页; 失败返回 null */
suspend fun fetchStatus(): SatStatusPage?
}
@@ -30,6 +30,7 @@ interface IMainContainer {
val selectionRepo: ISelectionRepo
val satelliteRepo: ISatelliteRepo
val databaseRepo: IDatabaseRepo
val amSatRepo: IAmSatRepository
val radioTrackingService: IRadioTrackingService
val mutualPassData: StateFlow<MutualPassData>
fun setMutualPassData(data: MutualPassData)
@@ -42,6 +43,9 @@ interface IMainContainer {
fun provideRxRadioController(): IRadioController
fun provideAudioCapture(): IAudioCapture
fun provideSaveImage(): ISaveImage
// WaveLog 日志(4.5.2)
val wavelogQueue: com.rtbishop.look4sat.core.domain.wavelog.WavelogQueue
fun provideWavelogUploader(): com.rtbishop.look4sat.core.domain.wavelog.WavelogUploader
}
data class MutualPassData(
@@ -45,7 +45,8 @@ interface ISettingsRepo {
//region # Station position settings
val stationPosition: StateFlow<GeoPos>
fun setStationPosition(latitude: Double, longitude: Double, altitude: Double): Boolean
fun setStationPosition(): Boolean
/** GPS 定位(挂起): 拿到位置才返回 true; 权限缺失/超时/无信号返回 false */
suspend fun setStationPosition(): Boolean
fun setStationPosition(locator: String): Boolean
//endregion
@@ -22,4 +22,7 @@ import java.io.InputStream
interface IRemoteSource {
suspend fun getFileStream(uri: String): InputStream?
suspend fun getNetworkStream(url: String): InputStream?
/** 抓取 AMSAT 状态页 HTML(带 UA, 返回 null = 失败) */
suspend fun getStatusHtml(): String?
}
@@ -19,4 +19,7 @@ package com.rtbishop.look4sat.core.domain.usecase
interface IShowToast {
operator fun invoke(message: String)
/** 按资源 ID 显示(四语文案) */
operator fun invoke(resId: Int)
}
@@ -1,4 +1,4 @@
/*
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
@@ -11,6 +11,7 @@ package com.rtbishop.look4sat.core.domain.utility
import com.rtbishop.look4sat.core.domain.model.SatRadio
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
import java.util.Locale
/**
* Computes Doppler-corrected reciprocal frequencies for linear transponders.
@@ -44,6 +45,9 @@ object DopplerFrequencyCalculator {
* Given a downlink frequency, compute the Doppler-corrected uplink frequency
* with an offset applied to the downlink (in Hz).
* Returns null if the transponder is not a linear passband type.
*
* The user-entered downlink frequency already includes the offset, so subtract
* it before mapping the downlink passband position back to the uplink.
*/
fun computeUplinkFromDownlinkWithOffset(
downlinkHz: Long,
@@ -52,7 +56,7 @@ object DopplerFrequencyCalculator {
offsetHz: Long
): Long? {
if (!isLinearTransponder(transponder)) return null
val baseUplink = TransponderMapper.mapDownlinkToUplink(downlinkHz + offsetHz, transponder) ?: return null
val baseUplink = TransponderMapper.mapDownlinkToUplink(downlinkHz - offsetHz, transponder) ?: return null
return orbitalPos.getUplinkFreq(baseUplink)
}
@@ -95,4 +99,26 @@ object DopplerFrequencyCalculator {
return upLow != null && upHigh != null && downLow != null && downHigh != null
&& upLow != upHigh && downLow != downHigh
}
/**
* True for the radio entry that should drive the standalone Calculator page.
*
* A frequency range alone is not enough: some non-user-facing or drifting data entries
* can also have low/high frequencies. The calculator is meant for the named linear
* transponder entry, e.g. "Linear Transponder", "Linear Transp.", "SSB Transponder".
*/
fun isNamedLinearTransponder(transponder: SatRadio): Boolean {
if (!isLinearTransponder(transponder)) return false
val info = transponder.info.lowercase(Locale.ENGLISH)
val modes = listOfNotNull(transponder.downlinkMode, transponder.uplinkMode)
.joinToString(separator = " ")
.lowercase(Locale.ENGLISH)
val hasLinearName = info.contains("linear")
val hasTransponderName = info.contains("transponder") || info.contains("transp") ||
info.contains("xponder") || info.contains("xpdr")
val hasLinearMode = listOf("ssb", "usb", "lsb", "cw").any { modes.contains(it) }
return (hasLinearName && hasTransponderName) || (hasTransponderName && hasLinearMode)
}
}
@@ -0,0 +1,266 @@
/*
* WaveLogApi.kt — WaveLog 日志服务器 API 客户端(4.5.2 覆盖修复 2)。
*
* 同时支持 v1 与 v2(用户服务器实测只有 v1, v2 返回 404):
* v2: POST {base}/api/v2/qso (Authorization: Bearer + JSON 字段)
* v1: POST {base}/index.php/api/qso (key 在 JSON body + ADIF 字符串)
* 策略: 优先 v2, 404 自动降级 v1。
* 测试连接: v2 GET api/v2/token → 404 时 v1 POST api/get_contacts_adif。
* 站点网格: 仅 v2 有 GET api/v2/station/{id}; v1 无此端点 → 降级用用户 QTH。
*/
package com.rtbishop.look4sat.core.domain.wavelog
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.json.JSONObject
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.OutputStreamWriter
import java.net.HttpURLConnection
import java.net.URL
import java.util.Locale
/** 站点信息(GET /api/v2/station/{id} 结果) */
data class WavelogStation(
val id: Int,
val name: String,
val callsign: String,
val gridsquare: String
)
sealed class WavelogResult {
data class Success(val message: String) : WavelogResult()
data class Failure(val message: String) : WavelogResult()
}
object WaveLogApi {
private const val TIMEOUT_MS = 15000
/** 规范化服务器地址: 去尾斜杠/去尾部 index.php; 无协议前缀时补 https:// */
fun normalizeUrl(raw: String): String {
var u = raw.trim().trimEnd('/')
if (u.isBlank()) return ""
if (!u.startsWith("http://") && !u.startsWith("https://")) u = "https://$u"
if (u.endsWith("/index.php")) u = u.removeSuffix("/index.php")
return u
}
/** 测试连接: v2 GET api/v2/token → 404 时 v1 POST api/get_contacts_adif */
suspend fun testToken(url: String, apiKey: String, stationId: String = ""): WavelogResult = withContext(Dispatchers.IO) {
val base = normalizeUrl(url)
if (base.isBlank()) return@withContext WavelogResult.Failure("服务器地址为空")
// v2: GET /index.php/api/v2/token
val v2 = httpRequest("$base/index.php/api/v2/token", "GET", apiKey, null)
if (v2.first in 200..299) return@withContext WavelogResult.Success("连接成功 (API v2)")
// v1: POST /index.php/api/get_contacts_adif(key 在 body)
if (stationId.isNotBlank()) {
val body = JSONObject().apply {
put("key", apiKey)
put("station_id", stationId)
put("fetchfromid", 0)
}.toString()
val v1 = httpRequest("$base/index.php/api/get_contacts_adif", "POST", apiKey, body)
if (v1.first in 200..299) return@withContext WavelogResult.Success("连接成功 (API v1)")
if (v1.first == 401) return@withContext WavelogResult.Failure("API 密钥无效 (v1: 401)")
}
// 无 index.php 的 v1 尝试
val body = JSONObject().apply {
put("key", apiKey)
put("station_id", stationId)
put("fetchfromid", 0)
}.toString()
val v1b = httpRequest("$base/api/get_contacts_adif", "POST", apiKey, body)
if (v1b.first in 200..299) return@withContext WavelogResult.Success("连接成功 (API v1)")
if (v1b.first == 401) return@withContext WavelogResult.Failure("API 密钥无效 (v1: 401)")
WavelogResult.Failure("连接失败: v2 HTTP ${v2.first}, v1 HTTP ${v1b.first} — 请确认服务器地址/密钥正确")
}
/** 站点信息: 仅 v2; v1 无此端点返回 null 标记(网格检测降级用用户 QTH) */
suspend fun getStation(url: String, apiKey: String, stationId: String): WavelogResult = withContext(Dispatchers.IO) {
val base = normalizeUrl(url)
if (base.isBlank()) return@withContext WavelogResult.Failure("服务器地址为空")
val (code, resp) = httpRequest("$base/index.php/api/v2/station/$stationId", "GET", apiKey, null)
if (code in 200..299) {
return@withContext try {
val obj = JSONObject(resp)
val data = obj.optJSONObject("data") ?: obj
val station = WavelogStation(
id = data.optInt("id"),
name = data.optString("name"),
callsign = data.optString("callsign"),
gridsquare = data.optString("gridsquare")
)
WavelogResult.Success(JSONObject().apply {
put("id", station.id); put("name", station.name)
put("callsign", station.callsign); put("gridsquare", station.gridsquare)
}.toString())
} catch (e: Exception) {
WavelogResult.Failure("解析失败: ${e.message}")
}
}
// v1 无 station 端点 → 返回 Success 空(调用方降级用用户 QTH)
WavelogResult.Success("")
}
/** LoTW 认可的卫星名: 取括号前主名 + 大写(ISS 特判) */
fun normalizeSatName(raw: String): String {
val main = raw.substringBefore('(').trim()
.ifBlank { raw.trim() }
.uppercase(Locale.ENGLISH)
// ISS 特判: 主名是 ISS/ZARYA/ARISS 变体 → ISS(LoTW 认可名)
return when {
main == "ZARYA" || main.startsWith("ISS") -> "ISS"
main == "ARISS" -> "ISS"
else -> main
}
}
/** 创建 QSO: 优先 v2, 404 降级 v1(ADIF) */
suspend fun postQso(
url: String,
apiKey: String,
stationProfileId: String,
qso: WavelogQso,
gridsquare: String
): WavelogResult = withContext(Dispatchers.IO) {
val base = normalizeUrl(url)
if (base.isBlank()) return@withContext WavelogResult.Failure("服务器地址为空")
val satName = normalizeSatName(qso.satName)
// v2: POST /index.php/api/v2/qso(JSON 字段)
val v2Body = JSONObject().apply {
put("station_profile_id", stationProfileId.toIntOrNull() ?: 0)
put("call", qso.call)
put("band", "SAT")
put("mode", qso.mode)
put("qso_date", utcDate(qso.timeUtcMs))
put("time_on", utcTime(qso.timeUtcMs))
put("freq", qso.freqTxHz)
put("freq_rx", qso.freqRxHz)
put("gridsquare", gridsquare)
put("sat_name", satName)
}
val (code, resp) = httpRequest("$base/index.php/api/v2/qso", "POST", apiKey, v2Body.toString())
if (code in 200..299) return@withContext WavelogResult.Success("已上传 (v2)")
if (code == 409) return@withContext WavelogResult.Success("重复(已存在)")
// v1: POST /index.php/api/qso(key 在 body + ADIF)
val v1Body = JSONObject().apply {
put("key", apiKey)
put("station_profile_id", stationProfileId)
put("type", "adif")
put("string", toAdif(qso, gridsquare, satName))
}
val (code1, resp1) = httpRequest("$base/index.php/api/qso", "POST", apiKey, v1Body.toString())
if (code1 in 200..299) return@withContext WavelogResult.Success("已上传 (v1)")
// 无 index.php 的 v1
val (code1b, resp1b) = httpRequest("$base/api/qso", "POST", apiKey, v1Body.toString())
if (code1b in 200..299) return@withContext WavelogResult.Success("已上传 (v1)")
WavelogResult.Failure("上传失败: v2 HTTP $code, v1 HTTP $code1 — ${shortError(resp1.ifBlank { resp1b })}")
}
/** v1 ADIF 字符串(频率 MHz, 长度=UTF-8 字节数, sat_name 已规范化) */
private fun toAdif(qso: WavelogQso, gridsquare: String, satName: String): String {
fun field(name: String, value: String): String {
val bytes = value.toByteArray(Charsets.UTF_8).size
return "<$name:$bytes>$value"
}
return buildString {
append(field("call", qso.call))
append(field("band", "SAT"))
append(field("mode", qso.mode))
append(field("freq", String.format(Locale.ENGLISH, "%.6f", qso.freqTxHz / 1_000_000.0)))
if (qso.freqRxHz > 0) {
append(field("freq_rx", String.format(Locale.ENGLISH, "%.6f", qso.freqRxHz / 1_000_000.0)))
}
append(field("qso_date", utcDateCompact(qso.timeUtcMs)))
append(field("time_on", utcTimeCompact(qso.timeUtcMs)))
if (gridsquare.isNotBlank()) append(field("gridsquare", gridsquare.take(4)))
if (satName.isNotBlank()) {
append(field("sat_name", satName))
append(field("prop_mode", "SAT"))
}
append("<eor>")
}
}
/** 通用 HTTP 请求(返回 code + body) */
private fun httpRequest(url: String, method: String, apiKey: String, jsonBody: String?): Pair<Int, String> {
return try {
val conn = URL(url).openConnection() as HttpURLConnection
conn.requestMethod = method
conn.connectTimeout = TIMEOUT_MS
conn.readTimeout = TIMEOUT_MS
if (apiKey.isNotBlank()) conn.setRequestProperty("Authorization", "Bearer $apiKey")
if (jsonBody != null) {
conn.doOutput = true
conn.setRequestProperty("Content-Type", "application/json")
conn.setRequestProperty("Accept", "application/json")
OutputStreamWriter(conn.outputStream, Charsets.UTF_8).use { it.write(jsonBody) }
}
val code = conn.responseCode
val stream = if (code in 200..299) conn.inputStream else conn.errorStream
val body = if (stream != null) {
BufferedReader(InputStreamReader(stream, Charsets.UTF_8)).use { it.readText() }
} else ""
code to body
} catch (e: Exception) {
-1 to (e.message ?: e.javaClass.simpleName)
}
}
private fun shortError(body: String): String {
if (body.startsWith("<")) return body.take(80) // HTML 错误页
return try {
val obj = JSONObject(body)
val err = obj.optJSONObject("error")
err?.optString("message")?.ifBlank { body.take(120) }
?: obj.optString("reason").ifBlank { obj.optString("message").ifBlank { body.take(120) } }
} catch (_: Exception) {
body.take(120)
}
}
private fun utcDate(ms: Long): String {
val cal = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC"))
cal.timeInMillis = ms
return "%04d-%02d-%02d".format(
cal.get(java.util.Calendar.YEAR), cal.get(java.util.Calendar.MONTH) + 1,
cal.get(java.util.Calendar.DAY_OF_MONTH)
)
}
private fun utcTime(ms: Long): String {
val cal = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC"))
cal.timeInMillis = ms
return "%02d:%02d:%02d".format(
cal.get(java.util.Calendar.HOUR_OF_DAY), cal.get(java.util.Calendar.MINUTE),
cal.get(java.util.Calendar.SECOND)
)
}
private fun utcDateCompact(ms: Long): String {
val cal = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC"))
cal.timeInMillis = ms
return "%04d%02d%02d".format(
cal.get(java.util.Calendar.YEAR), cal.get(java.util.Calendar.MONTH) + 1,
cal.get(java.util.Calendar.DAY_OF_MONTH)
)
}
private fun utcTimeCompact(ms: Long): String {
val cal = java.util.Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC"))
cal.timeInMillis = ms
return "%02d%02d%02d".format(
cal.get(java.util.Calendar.HOUR_OF_DAY), cal.get(java.util.Calendar.MINUTE),
cal.get(java.util.Calendar.SECOND)
)
}
}
@@ -0,0 +1,100 @@
/*
* WavelogQueue.kt — WaveLog 本地日志队列(4.5.2)。
*
* 纯 Kotlin(不依赖 Android): 存储走 IWavelogQueueStore 接口,
* 由 core/data 用 SharedPreferences 实现。
* 队列上限 500 条(超出丢最旧)。
*/
package com.rtbishop.look4sat.core.domain.wavelog
import org.json.JSONArray
import org.json.JSONObject
/** 存储抽象(SharedPreferences 实现见 core/data) */
interface IWavelogQueueStore {
fun load(): String
fun save(json: String)
}
/** 待上传的 QSO 条目(本地队列元素,与 POST /api/v2/qso 字段对应) */
data class WavelogQso(
val id: String, // 本地唯一 id(UUID)
val timeUtcMs: Long, // 回车时刻 UTC 毫秒(本地显示 + 组装 qso_date/time_on)
val call: String,
val mode: String,
val freqTxHz: Long, // 上行(回车那一秒多普勒修正)
val freqRxHz: Long, // 下行
val satName: String,
val sessionId: String = "", // 场次 ID: 卫星名-AOS 时间戳(过境仰角 0 秒), 空=未分组(旧数据)
val uploaded: Boolean = false // 是否已成功上传(4.5.2 修复: 成功后保留标记, 表格打勾)
)
class WavelogQueue(private val store: IWavelogQueueStore) {
private val key = "wavelog_queue"
fun all(): List<WavelogQso> {
val raw = store.load()
return try {
val arr = JSONArray(raw)
(0 until arr.length()).map { i ->
val o = arr.getJSONObject(i)
WavelogQso(
id = o.getString("id"),
timeUtcMs = o.getLong("timeUtcMs"),
call = o.optString("call"),
mode = o.optString("mode"),
freqTxHz = o.optLong("freqTxHz"),
freqRxHz = o.optLong("freqRxHz"),
satName = o.optString("satName"),
sessionId = o.optString("sessionId"),
uploaded = o.optBoolean("uploaded", false)
)
}
} catch (_: Exception) { emptyList() }
}
@Synchronized
fun add(qso: WavelogQso) {
val list = all().toMutableList()
list.add(0, qso) // 最新在前
if (list.size > 500) list.removeAt(list.size - 1)
save(list)
}
@Synchronized
fun remove(id: String) {
save(all().filter { it.id != id })
}
@Synchronized
fun removeAll(ids: Set<String>) {
save(all().filter { it.id !in ids })
}
/** 标记为已上传(保留在队列, 表格打勾) */
@Synchronized
fun markUploaded(id: String) {
save(all().map { if (it.id == id) it.copy(uploaded = true) else it })
}
/** 移除所有已上传条目(可选项, 保持队列精简) */
@Synchronized
fun removeUploaded() {
save(all().filter { !it.uploaded })
}
private fun save(list: List<WavelogQso>) {
val arr = JSONArray()
list.forEach { q ->
arr.put(JSONObject().apply {
put("id", q.id); put("timeUtcMs", q.timeUtcMs); put("call", q.call)
put("mode", q.mode); put("freqTxHz", q.freqTxHz)
put("freqRxHz", q.freqRxHz); put("satName", q.satName)
put("sessionId", q.sessionId)
put("uploaded", q.uploaded)
})
}
store.save(arr.toString())
}
}
@@ -0,0 +1,91 @@
/*
* WavelogUploader.kt — WaveLog 队列上传调度(4.5.2)。
*
* 手动/周期上传共用: 逐条网格检测(用户 QTH 前 4 位 vs 台站网格前 4 位)
* → POST /api/v2/qso → 成功移出队列。
* 网格不一致: 返回 NeedConfirm(由 UI 弹窗「忽略并上传/取消」),
* 确认后带 force=true 重试本批。
*/
package com.rtbishop.look4sat.core.domain.wavelog
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
import org.json.JSONObject
sealed class UploadOutcome {
data class NeedConfirm(val stationGrid: String, val userGrid: String) : UploadOutcome()
data class Done(
val successCount: Int,
val failedCount: Int,
val message: String,
val firstError: String = ""
) : UploadOutcome()
}
class WavelogUploader(
private val settingsRepo: ISettingsRepo,
private val queue: WavelogQueue
) {
// 台站网格缓存(每次上传前刷新; 失败用旧值)
private var cachedStationGrid: String? = null
/** 上传整个队列。force=true 跳过网格确认(用户已选「忽略并上传」) */
suspend fun uploadQueue(force: Boolean = false): UploadOutcome {
val settings = settingsRepo.otherSettings.value
val url = settings.wavelogUrl
val apiKey = settings.wavelogApiKey
val stationId = settings.wavelogStationId
if (url.isBlank() || apiKey.isBlank() || stationId.isBlank()) {
return UploadOutcome.Done(0, queue.all().size, "未配置 WaveLog 服务器")
}
// 1. 拉站点信息(拿台站网格); v1 无此端点时降级用用户 QTH
val stationGrid = getStationGrid(url, apiKey, stationId) ?: userQthGrid()
if (stationGrid.isNullOrBlank()) {
return UploadOutcome.Done(0, queue.all().size, "无法获取站点信息(检查站点 ID/密钥权限)")
}
// 2. 网格检测: 用户当前 QTH 前 4 位 vs 台站网格前 4 位(v1 降级时网格相同, 跳过)
if (!force) {
val userGrid = userQthGrid()
if (userGrid != null && stationGrid.take(4).lowercase() != userGrid.take(4).lowercase()) {
return UploadOutcome.NeedConfirm(stationGrid, userGrid)
}
}
// 3. 逐条上传(成功后标记 uploaded, 保留在本地供日志页打勾)
val entries = queue.all()
var ok = 0
var fail = 0
var firstError = ""
for (qso in entries) {
if (qso.uploaded) { ok++; continue }
val result = WaveLogApi.postQso(url, apiKey, stationId, qso, stationGrid)
if (result is WavelogResult.Success) {
ok++
queue.markUploaded(qso.id)
} else {
fail++
if (firstError.isBlank()) firstError = (result as? WavelogResult.Failure)?.message ?: ""
}
}
val message = if (fail == 0) "成功上传 $ok 条" else "成功 $ok 条, 失败 $fail 条(保留待重试)"
return UploadOutcome.Done(ok, fail, message, firstError)
}
private suspend fun getStationGrid(url: String, apiKey: String, stationId: String): String? {
val result = WaveLogApi.getStation(url, apiKey, stationId)
if (result is WavelogResult.Success) {
return try {
JSONObject(result.message).optString("gridsquare").takeIf { it.isNotBlank() }
?: cachedStationGrid
} catch (_: Exception) { cachedStationGrid }
}
return cachedStationGrid
}
/** 用户当前 QTH 网格(前 4 位; 无 QTH 返回 null = 跳过检测) */
private fun userQthGrid(): String? {
return settingsRepo.stationPosition.value.qthLocator?.takeIf { it.length >= 4 }
}
}
@@ -13,12 +13,15 @@ class DopplerFrequencyCalculatorTest {
upHigh: Long = 145_500_000L,
downLow: Long = 435_000_000L,
downHigh: Long? = 435_500_000L,
inverted: Boolean = false
inverted: Boolean = false,
info: String = "Linear Transponder",
downlinkMode: String? = "USB",
uplinkMode: String? = "LSB"
) = SatRadio(
uuid = "linear", info = "Linear Transponder", isAlive = true,
uuid = "linear", info = info, isAlive = true,
downlinkLow = downLow, downlinkHigh = downHigh,
downlinkMode = "USB", uplinkLow = upLow, uplinkHigh = upHigh,
uplinkMode = "LSB", isInverted = inverted, catnum = 12345
downlinkMode = downlinkMode, uplinkLow = upLow, uplinkHigh = upHigh,
uplinkMode = uplinkMode, isInverted = inverted, catnum = 12345
)
private fun fmTransponder() = SatRadio(
@@ -48,6 +51,28 @@ class DopplerFrequencyCalculatorTest {
assertFalse(DopplerFrequencyCalculator.isLinearTransponder(xpdr))
}
@Test
fun isNamedLinearTransponder_returnsTrueForLinearTransponderName() {
assertTrue(DopplerFrequencyCalculator.isNamedLinearTransponder(linearTransponder()))
}
@Test
fun isNamedLinearTransponder_returnsTrueForSsbTransponderName() {
val xpdr = linearTransponder(info = "Mode V/U SSB Transponder", downlinkMode = "USB", uplinkMode = "LSB")
assertTrue(DopplerFrequencyCalculator.isNamedLinearTransponder(xpdr))
}
@Test
fun isNamedLinearTransponder_returnsFalseForRangeEntryWithoutTransponderName() {
val driftingRangeEntry = linearTransponder(info = "Upper side band (drifting)")
assertFalse(DopplerFrequencyCalculator.isNamedLinearTransponder(driftingRangeEntry))
}
@Test
fun isNamedLinearTransponder_returnsFalseForFmRepeater() {
assertFalse(DopplerFrequencyCalculator.isNamedLinearTransponder(fmTransponder()))
}
@Test
fun computeUplinkFromDownlink_linear_noDoppler() {
val xpdr = linearTransponder()
@@ -93,6 +118,53 @@ class DopplerFrequencyCalculatorTest {
assertNull(result)
}
@Test
fun computeDownlinkFromUplink_withPositiveOffset_addsOffsetToDownlink() {
val xpdr = linearTransponder()
val orbitalPos = pos(0.0)
val downlink = DopplerFrequencyCalculator.computeDownlinkFromUplinkWithOffset(
uplinkHz = 145_200_000L,
transponder = xpdr,
orbitalPos = orbitalPos,
offsetHz = 2_500L
)
assertEquals(435_202_500L, downlink)
}
@Test
fun computeUplinkFromDownlink_withPositiveOffset_subtractsOffsetBeforeMapping() {
val xpdr = linearTransponder()
val orbitalPos = pos(0.0)
val uplink = DopplerFrequencyCalculator.computeUplinkFromDownlinkWithOffset(
downlinkHz = 435_202_500L,
transponder = xpdr,
orbitalPos = orbitalPos,
offsetHz = 2_500L
)
assertEquals(145_200_000L, uplink)
}
@Test
fun computeOffsetRoundTrip_handlesNegativeOffset() {
val xpdr = linearTransponder()
val orbitalPos = pos(0.0)
val downlink = DopplerFrequencyCalculator.computeDownlinkFromUplinkWithOffset(
uplinkHz = 145_200_000L,
transponder = xpdr,
orbitalPos = orbitalPos,
offsetHz = -2_500L
)
assertEquals(435_197_500L, downlink)
val uplink = DopplerFrequencyCalculator.computeUplinkFromDownlinkWithOffset(
downlinkHz = downlink!!,
transponder = xpdr,
orbitalPos = orbitalPos,
offsetHz = -2_500L
)
assertEquals(145_200_000L, uplink)
}
@Test
fun computeUplinkFromDownlink_invertedTransponder() {
val xpdr = linearTransponder(inverted = true, downHigh = 435_500_000L)
@@ -518,6 +518,5 @@ fun formatFrequency(frequencyHz: Long): String {
if (frequencyHz <= 0) return "---"
val mhz = frequencyHz / 1_000_000
val khz = (frequencyHz % 1_000_000) / 1_000
val hz = frequencyHz % 1_000
return String.format(Locale.ENGLISH, "%d.%03d.%03d", mhz, khz, hz)
return String.format(Locale.ENGLISH, "%d.%03d", mhz, khz)
}
@@ -21,30 +21,45 @@ import androidx.navigation3.runtime.NavKey
import kotlinx.serialization.Serializable
@Serializable
sealed class Screen(val iconResId: Int, val titleResId: Int) : NavKey {
sealed class Screen(val iconResId: Int, val titleResId: Int, val screenId: String) : NavKey {
@Serializable
data object Satellites : Screen(R.drawable.ic_satellites, R.string.nav_sat)
data object Satellites : Screen(R.drawable.ic_satellites, R.string.nav_sat, "Satellites")
@Serializable
data object Passes : Screen(R.drawable.ic_passes, R.string.nav_pass)
data object Passes : Screen(R.drawable.ic_passes, R.string.nav_pass, "Passes")
@Serializable
data object Radar : Screen(R.drawable.ic_radar, R.string.nav_radar)
data object Radar : Screen(R.drawable.ic_radar, R.string.nav_radar, "Radar")
@Serializable
data object Map : Screen(R.drawable.ic_map, R.string.nav_map)
data object Map : Screen(R.drawable.ic_map, R.string.nav_map, "Map")
@Serializable
data object Mutual : Screen(R.drawable.ic_match, R.string.nav_mutual)
data object Mutual : Screen(R.drawable.ic_match, R.string.nav_mutual, "Mutual")
@Serializable
data object Roaming : Screen(R.drawable.ic_roaming, R.string.nav_roaming)
data object Roaming : Screen(R.drawable.ic_roaming, R.string.nav_roaming, "Roaming")
@Serializable
data object Settings : Screen(R.drawable.ic_settings, R.string.nav_prefs)
data object CwDecode : Screen(R.drawable.ic_cw, R.string.nav_cw, "CwDecode")
@Serializable
data object WavelogLog : Screen(R.drawable.ic_log, R.string.nav_log, "WavelogLog")
@Serializable
data object AmSat : Screen(R.drawable.ic_satellites, R.string.nav_amsat, "AMSAT")
@Serializable
data object Settings : Screen(R.drawable.ic_settings, R.string.nav_prefs, "Settings")
}
// UI 设置: 默认主菜单页面顺序(底部栏 5 槽)
val defaultScreenOrder = listOf("Satellites", "Passes", "Radar", "Map", "Settings")
// UI 设置: 默认更多菜单顺序(进「更多」的页面)
val defaultSubMenuOrder = listOf("Mutual", "Roaming", "CwDecode", "WavelogLog", "AMSAT")
@Serializable
data object RadarDestination : NavKey
@@ -73,4 +88,4 @@ class DeeplinkResolver(private val fallbackDestination: NavKey = Screen.Passes)
matchers.forEach { it.match(deeplink)?.let { match -> return match } }
return fallbackDestination
}
}
}
@@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<!-- Morse style icon: a dot above a dash -->
<path
android:fillColor="#FFFFFFFF"
android:pathData="M 8 4 L 16 4 L 16 8 L 8 8 Z" />
<path
android:fillColor="#FFFFFFFF"
android:pathData="M 3 16 L 21 16 L 21 20 L 3 20 Z" />
</vector>
@@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#FF000000"
android:pathData="M6,2h9l5,5v13c0,1.1 -0.9,2 -2,2H6c-1.1,0 -2,-0.9 -2,-2V4c0,-1.1 0.9,-2 2,-2zM14,3.5V8h4.5L14,3.5zM8,12h8v2H8v-2zM8,16h8v2H8v-2z" />
</vector>
@@ -0,0 +1,252 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">Look4Sat Pro</string>
<string name="btn_accept">Terima</string>
<string name="btn_cancel">Batal</string>
<string name="empty_list_message">Tidak ada data untuk ditampilkan</string>
<string name="nav_sat">Satelit</string>
<string name="nav_pass">Lintasan</string>
<string name="nav_radar">Radar</string>
<string name="nav_mutual">Pasangan</string>
<string name="nav_roaming">Jelajah</string>
<string name="nav_cw">Dekoder CW</string>
<string name="nav_map">Peta</string>
<string name="nav_prefs">Pengaturan</string>
<string name="nav_more">Lainnya</string>
<string name="nav_log">Log</string>
<string name="nav_amsat">AMSAT</string>
<string name="amsat_title">Status Satelit AMSAT</string>
<string name="amsat_updated">Diperbarui:</string>
<string name="amsat_active">Aktif</string>
<string name="amsat_tlm">TLM/beacon</string>
<string name="amsat_not_heard">Tidak terdengar</string>
<string name="amsat_conflict">Konflik</string>
<string name="amsat_name">Name</string>
<string name="amsat_load_failed">Gagal memuat - periksa jaringan dan coba lagi</string>
<string name="amsat_no_reports">Tidak ada laporan untuk periode ini</string>
<string name="amsat_close">Tutup</string>
<string name="sat_type_hint">Jenis: %s</string>
<string name="sat_type_title">Pilih jenis satelit</string>
<string name="sat_search_hint">Id - Nama</string>
<string name="sat_search_clear">Bersihkan</string>
<string name="sat_clear_all">Bersihkan semua</string>
<string name="sat_select_all">Pilih semua</string>
<string name="sat_empty_list_message">Pastikan kata kunci pencarian Anda benar dan DB sudah diperbarui</string>
<string name="sat_warning_title">Peringatan\!</string>
<string name="sat_warning_message">
Ada lebih dari 9000 satelit yang terdaftar di aplikasi ini.
Tidak masuk akal untuk melacak semuanya sekaligus.
\n\nSelalu coba persempit daftar hanya ke satelit yang
Anda minati melalui pencarian dan pemilih jenis.</string>
<string name="pass_filter_title">Filter lintasan</string>
<string name="pass_filter_elev">Elevasi minimal</string>
<string name="pass_filter_hours">Waktu ke depan</string>
<string name="pass_filter_aos_time">Jendela AOS</string>
<string name="pass_filter_invert_time">Balik jendela AOS</string>
<string name="pass_filter_deep_space">DeepSpace (periode >225 mnt)</string>
<string name="pass_time_placeholder" translatable="false"> -- : -- : -- </string>
<string name="pass_modes_title">Pilih jenis modulasi</string>
<string name="pass_satId" translatable="false">%05d</string>
<string name="pass_elevation">Elevasi: %.1f°</string>
<string name="pass_deep_space" translatable="false">DeepSpace</string>
<string name="pass_altitude">Ketinggian: %d km</string>
<string name="pass_aosLos" translatable="false">%03d° - %03d°</string>
<string name="pass_empty_list_message">
Pastikan filter Anda benar dan Anda sudah memilih satelit</string>
<string name="pass_error_message">Tidak ada lintasan satelit mendatang untuk ditampilkan</string>
<string name="pass_welcome_title">Selamat datang di Look4Sat\!</string>
<string name="pass_welcome_message">
Pastikan Anda mengatur posisi melalui GPS, Lat/Lon atau QTH di Pengaturan.
\n\nPerbarui database setidaknya seminggu sekali untuk prediksi yang akurat.</string>
<string name="pass_whatsnew_title" translatable="false">Apa yang baru di Look4Sat</string>
<string name="pass_whatsnew_message" translatable="false">
* BARU: Halaman status satelit AMSAT - tabel status langsung (warna resmi), jumlah laporan, laporan detail per satelit/hari
* Log dikelompokkan per lintasan satelit (sesi = satelit + waktu AOS), garis pemisah antar sesi
* GPS kini hanya menampilkan sukses saat posisi benar-benar didapat (timeout 15 dtk, cek izin)
* Pesan kegagalan jelas saat database satelit gagal diunduh
* Frekuensi log mengikuti panel Doppler secara real-time; visual hapus geser dipulihkan
* Perbaikan bahasa Indonesia (values-id)</string>
<string name="radar_back">Kembali</string>
<string name="radar_notify">Beri tahu</string>
<string name="radar_az_text">Azimuth</string>
<string name="radar_az_value" translatable="false">%.1f°</string>
<string name="radar_el_text">Elevasi</string>
<string name="radar_el_value" translatable="false">%.1f°</string>
<string name="radar_alt_text">Ketinggian</string>
<string name="radar_alt_value" translatable="false">%.0f km</string>
<string name="radar_dist_text">Jarak</string>
<string name="radar_dist_value" translatable="false">%.0f km</string>
<string name="radar_eclipsed">Gerhana</string>
<string name="radar_downlink">Downlink</string>
<string name="radar_uplink">Uplink</string>
<string name="radar_link_low" translatable="false">%.4f</string>
<string name="radar_link_lowHigh" translatable="false">%.4f - %.4f</string>
<string name="radar_no_link" translatable="false">- - . - -</string>
<string name="radar_no_data">Satelit ini tidak memiliki transceiver</string>
<string name="radar_add">Tambah</string>
<string name="radar_data">Transceiver satelit:</string>
<string name="radar_inverted">Terbalik: %s</string>
<string name="radar_mode">Mode: %s</string>
<string name="radar_string_no">Tidak</string>
<string name="radar_string_yes">Ya</string>
<string name="radar_visible">Terlihat</string>
<string name="radar_doppler_calc">Kalkulator Frekuensi Doppler</string>
<string name="radar_doppler_tx_hint">Masukkan frekuensi TX (MHz)</string>
<string name="radar_doppler_rx_hint">Masukkan frekuensi RX (MHz)</string>
<string name="radar_doppler_offset_hint">Offset (kHz)</string>
<string name="radar_doppler_info">Untuk transponder linier, ketik satu frekuensi untuk melihat frekuensi lainnya</string>
<string name="radar_cw_decoder">Dekoder CW</string>
<string name="radar_cw_start">Mulai</string>
<string name="radar_cw_stop">Berhenti</string>
<string name="radar_cw_reset">Bersihkan</string>
<string name="map_prev">Sebelumnya</string>
<string name="map_next">Berikutnya</string>
<string name="map_copyright" translatable="false">© Kontributor OpenStreetMap</string>
<string name="map_azimuth">Azimuth: %.1f°</string>
<string name="map_elevation">Elevasi: %.1f°</string>
<string name="map_altitude">Ketinggian: %.0f km</string>
<string name="map_distance">Jarak: %.0f km</string>
<string name="map_latitude">Lintang: %.1f°</string>
<string name="map_longitude">Bujur: %.1f°</string>
<string name="map_qth" translatable="false">QTH: %s</string>
<string name="map_phase">Fase: %.1f°</string>
<string name="map_eclipsed">Gerhana</string>
<string name="map_period">Periode: %.0f mnt</string>
<string name="map_velocity">Kecepatan: %.2f km/d</string>
<string name="map_visibility">Visibilitas: %s</string>
<string name="map_visible">Terlihat</string>
<string name="prefs_app_title" translatable="false">Look4Sat Pro v%s</string>
<string name="prefs_app_url" translatable="false">https://play.google.com/store/apps/details?id=com.rtbishop.look4sat</string>
<string name="prefs_donate_title">Donasi</string>
<string name="prefs_donate_url" translatable="false">https://ko-fi.com/rt_bishop</string>
<string name="prefs_fdroid_title" translatable="false">F-Droid</string>
<string name="prefs_fdroid_url" translatable="false">https://f-droid.org/en/packages/com.rtbishop.look4sat/</string>
<string name="prefs_github_title" translatable="false">GitHub</string>
<string name="prefs_github_url" translatable="false">https://github.com/rt-bishop/Look4Sat/</string>
<string name="prefs_license_title" translatable="false">GPLv3</string>
<string name="prefs_license_url" translatable="false">https://www.gnu.org/licenses/gpl-3.0.html</string>
<string name="prefs_privacy_title">Kebijakan Privasi</string>
<string name="prefs_privacy_url" translatable="false">https://sites.google.com/view/look4sat-privacy-policy/home</string>
<string name="prefs_updated_title">Diperbarui: %s</string>
<string name="prefs_updated_time">d MMM yyyy - HH:mm:ss</string>
<string name="prefs_loc_title">Posisi stasiun</string>
<string name="prefs_loc_gps_title">GPS</string>
<string name="prefs_loc_gps_error">Tidak dapat mengambil lokasi - periksa izin dan sinyal GPS/jaringan</string>
<string name="prefs_loc_input_title">Input</string>
<string name="prefs_loc_input_error">Lokasi yang dimasukkan tidak valid</string>
<string name="prefs_loc_qth_title">QTH</string>
<string name="prefs_loc_qth_error">Lokator QTH tidak valid</string>
<string name="prefs_loc_success">Posisi berhasil diperbarui</string>
<string name="prefs_station_title">Pengaturan posisi stasiun</string>
<string name="prefs_station_lat_text">Atur garis lintang stasiun Anda</string>
<string name="prefs_station_lon_text">Atur garis bujur stasiun Anda</string>
<string name="prefs_locator_title">Pengaturan lokator QTH</string>
<string name="prefs_locator_text">Atur posisi stasiun menggunakan lokator</string>
<string name="prefs_data_title">Data satelit</string>
<string name="prefs_data_entries">Satelit: %s</string>
<string name="prefs_data_radios">Transceiver: %s</string>
<string name="prefs_data_update">Perbarui</string>
<string name="prefs_data_import">URL Kustom</string>
<string name="prefs_data_clear">Bersihkan</string>
<string name="prefs_data_clear_success">Data berhasil dibersihkan</string>
<string name="prefs_data_update_success">Pembaruan berhasil diselesaikan</string>
<string name="prefs_data_update_failed">Pembaruan gagal - periksa jaringan Anda</string>
<string name="prefs_data_import_satellites_error">Tidak ada satelit yang diimpor. Pilih file TLE/3LE (.txt) atau OMM (.csv) yang valid.</string>
<string name="prefs_data_import_transceivers_error">Tidak ada transceiver yang diimpor. Pilih file SatNOGS (.json) yang valid.</string>
<string name="prefs_data_sources_title">Sumber data kustom</string>
<string name="prefs_data_sources_tle_switch">URL TLE Kustom</string>
<string name="prefs_data_sources_transceivers_switch">URL transceiver kustom</string>
<string name="prefs_data_sources_url_title" translatable="false">URL (HTTPS)</string>
<string name="prefs_data_reset_url">Setel ulang ke URL bawaan</string>
<string name="prefs_data_output_title">Keluaran data</string>
<string name="prefs_net_output">Jaringan</string>
<string name="prefs_bt_output">Bluetooth</string>
<string name="prefs_cat_output">CAT</string>
<string name="nav_radiocontrol">Kontrol Radio</string>
<string name="rc_settings_title">Kontrol Radio CAT</string>
<string name="rc_radio_model">Model Radio</string>
<string name="rc_tx_device_hint">Alamat BT Radio TX</string>
<string name="rc_rx_device_hint">Alamat BT Radio RX</string>
<string name="rc_tx_name_hint">Nama Radio TX</string>
<string name="rc_rx_name_hint">Nama Radio RX</string>
<string name="rc_enable_switch">Aktifkan Kontrol CAT</string>
<string name="prefs_net_title">Keluaran data jaringan</string>
<string name="prefs_net_rotator_switch">Aktifkan keluaran rotator</string>
<string name="prefs_net_rotator_address_hint">IP:Port</string>
<string name="prefs_net_rotator_format_hint">Format</string>
<string name="prefs_net_frequency_switch">Aktifkan keluaran frekuensi</string>
<string name="prefs_net_frequency_address_hint">IP:Port</string>
<string name="prefs_net_frequency_format_hint">Format</string>
<string name="prefs_bt_title">Keluaran data Bluetooth</string>
<string name="prefs_bt_rotator_switch">Aktifkan keluaran rotator</string>
<string name="prefs_bt_rotator_device_hint">Id perangkat</string>
<string name="prefs_bt_rotator_output_hint">Format data</string>
<string name="prefs_bt_frequency_switch">Aktifkan keluaran frekuensi</string>
<string name="prefs_bt_frequency_device_hint">Id perangkat</string>
<string name="prefs_bt_frequency_output_hint">Format data</string>
<string name="prefs_bt_perm_error">Periksa izin bluetooth Anda</string>
<string name="prefs_net_perm_error">Periksa izin jaringan Anda</string>
<string name="tracking_status">Melacak: %s</string>
<string name="loc_lat_text">Lintang: %s°</string>
<string name="loc_lon_text">Bujur: %s°</string>
<string name="loc_qth_text">QTH: %s</string>
<string name="connect_radio">Hubungkan</string>
<string name="disconnect_radio">Putuskan</string>
<string name="track_radio">Lacak</string>
<string name="stop_radio">Berhenti</string>
<string name="prefs_wavelog_title">Server log WaveLog</string>
<string name="prefs_wavelog_url_hint">Alamat server</string>
<string name="prefs_wavelog_key_hint">Kunci API</string>
<string name="prefs_wavelog_station_hint">ID stasiun</string>
<string name="prefs_wavelog_auto">Unggah otomatis</string>
<string name="prefs_wavelog_test">Uji koneksi</string>
<string name="prefs_wavelog_upload">Unggah sekarang</string>
<string name="wavelog_title">Log</string>
<string name="wavelog_select_transponder">Pilih transponder</string>
<string name="wavelog_call_hint">Tanda panggil</string>
<string name="wavelog_mode_hint">Mode</string>
<string name="wavelog_saved">Tersimpan ke log lokal</string>
<string name="wavelog_need_config">Konfigurasikan WaveLog di Pengaturan terlebih dahulu</string>
<string name="wavelog_empty">Belum ada entri log</string>
<string name="wavelog_grid_mismatch">Grid QTH (%1$s) tidak cocok dengan grid stasiun (%2$s). Tetap unggah?</string>
<string name="wavelog_ignore">Abaikan dan unggah</string>
<string name="wavelog_cancel">Batal</string>
<string name="wavelog_copy">Salin</string>
<string name="wavelog_delete_undo">Urungkan</string>
<string name="wavelog_col_time">Waktu</string>
<string name="wavelog_col_freq">Frek.</string>
<string name="wavelog_col_sat">Satelit</string>
<string name="wavelog_col_call">Panggilan</string>
<string name="wavelog_col_uploaded">Terkirim</string>
<string name="prefs_other_title">Pengaturan lainnya</string>
<string name="prefs_ui_title">Pengaturan UI</string>
<string name="prefs_ui_order_title">Urutan halaman</string>
<string name="prefs_ui_main_title">Menu utama (bilah bawah)</string>
<string name="prefs_ui_sub_title">Menu lainnya</string>
<string name="prefs_ui_move_out">Pindah ke menu lainnya</string>
<string name="prefs_ui_move_back">Kembalikan ke menu utama</string>
<string name="prefs_ui_order_locked">Tetap (tidak dapat dipindahkan)</string>
<string name="prefs_ui_order_reset">Setel ulang urutan</string>
<string name="prefs_other_switch_utc">Tampilkan waktu lintasan dalam UTC</string>
<string name="prefs_other_switch_update">Aktifkan pembaruan data otomatis</string>
<string name="prefs_other_switch_sweep">Aktifkan animasi sapuan radar</string>
<string name="prefs_other_switch_sensors">Gunakan sensor untuk memutar tampilan radar</string>
<string name="prefs_other_switch_night_mode">Aktifkan filter mode malam merah</string>
<string name="prefs_highlight_title">Sorotan elevasi</string>
<string name="prefs_highlight_low">Rendah &lt; %1$d°</string>
<string name="prefs_highlight_mid">Sedang %1$d–%2$d°</string>
<string name="prefs_highlight_high">Tinggi &gt; %1$d°</string>
<string name="prefs_outro_title">BG7NTA &amp; penulis asli mengucapkan terima kasih</string>
<string name="prefs_outro_thanks" translatable="false">
* Pengguna dan kontributor Look4Sat!
\n* David A. B. Johnson (predict4java)
\n* Dave Moten (predict4java)
\n* Alexandru Csete (Gpredict)
\n* Dr T.S. Kelso (Celestrak)
\n* Libre Space Foundation (SatNOGS)
\n* xdsopl dan kontributor Robot36!
\n* BA7OPF (fitur pencocokan lintasan)
\n* BG7NTA</string>
<string name="prefs_outro_license">Aplikasi ini hadir tanpa jaminan</string>
</resources>
@@ -0,0 +1,252 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">Look4Sat Pro</string>
<string name="btn_accept">Terima</string>
<string name="btn_cancel">Batal</string>
<string name="empty_list_message">Tidak ada data untuk ditampilkan</string>
<string name="nav_sat">Satelit</string>
<string name="nav_pass">Lintasan</string>
<string name="nav_radar">Radar</string>
<string name="nav_mutual">Pasangan</string>
<string name="nav_roaming">Jelajah</string>
<string name="nav_cw">Dekoder CW</string>
<string name="nav_map">Peta</string>
<string name="nav_prefs">Pengaturan</string>
<string name="nav_more">Lainnya</string>
<string name="nav_log">Log</string>
<string name="nav_amsat">AMSAT</string>
<string name="amsat_title">Status Satelit AMSAT</string>
<string name="amsat_updated">Diperbarui:</string>
<string name="amsat_active">Aktif</string>
<string name="amsat_tlm">TLM/beacon</string>
<string name="amsat_not_heard">Tidak terdengar</string>
<string name="amsat_conflict">Konflik</string>
<string name="amsat_name">Name</string>
<string name="amsat_load_failed">Gagal memuat - periksa jaringan dan coba lagi</string>
<string name="amsat_no_reports">Tidak ada laporan untuk periode ini</string>
<string name="amsat_close">Tutup</string>
<string name="sat_type_hint">Jenis: %s</string>
<string name="sat_type_title">Pilih jenis satelit</string>
<string name="sat_search_hint">Id - Nama</string>
<string name="sat_search_clear">Bersihkan</string>
<string name="sat_clear_all">Bersihkan semua</string>
<string name="sat_select_all">Pilih semua</string>
<string name="sat_empty_list_message">Pastikan kata kunci pencarian Anda benar dan DB sudah diperbarui</string>
<string name="sat_warning_title">Peringatan\!</string>
<string name="sat_warning_message">
Ada lebih dari 9000 satelit yang terdaftar di aplikasi ini.
Tidak masuk akal untuk melacak semuanya sekaligus.
\n\nSelalu coba persempit daftar hanya ke satelit yang
Anda minati melalui pencarian dan pemilih jenis.</string>
<string name="pass_filter_title">Filter lintasan</string>
<string name="pass_filter_elev">Elevasi minimal</string>
<string name="pass_filter_hours">Waktu ke depan</string>
<string name="pass_filter_aos_time">Jendela AOS</string>
<string name="pass_filter_invert_time">Balik jendela AOS</string>
<string name="pass_filter_deep_space">DeepSpace (periode >225 mnt)</string>
<string name="pass_time_placeholder" translatable="false"> -- : -- : -- </string>
<string name="pass_modes_title">Pilih jenis modulasi</string>
<string name="pass_satId" translatable="false">%05d</string>
<string name="pass_elevation">Elevasi: %.1f°</string>
<string name="pass_deep_space" translatable="false">DeepSpace</string>
<string name="pass_altitude">Ketinggian: %d km</string>
<string name="pass_aosLos" translatable="false">%03d° - %03d°</string>
<string name="pass_empty_list_message">
Pastikan filter Anda benar dan Anda sudah memilih satelit</string>
<string name="pass_error_message">Tidak ada lintasan satelit mendatang untuk ditampilkan</string>
<string name="pass_welcome_title">Selamat datang di Look4Sat\!</string>
<string name="pass_welcome_message">
Pastikan Anda mengatur posisi melalui GPS, Lat/Lon atau QTH di Pengaturan.
\n\nPerbarui database setidaknya seminggu sekali untuk prediksi yang akurat.</string>
<string name="pass_whatsnew_title" translatable="false">Apa yang baru di Look4Sat</string>
<string name="pass_whatsnew_message" translatable="false">
* BARU: Halaman status satelit AMSAT - tabel status langsung (warna resmi), jumlah laporan, laporan detail per satelit/hari
* Log dikelompokkan per lintasan satelit (sesi = satelit + waktu AOS), garis pemisah antar sesi
* GPS kini hanya menampilkan sukses saat posisi benar-benar didapat (timeout 15 dtk, cek izin)
* Pesan kegagalan jelas saat database satelit gagal diunduh
* Frekuensi log mengikuti panel Doppler secara real-time; visual hapus geser dipulihkan
* Perbaikan bahasa Indonesia (values-id)</string>
<string name="radar_back">Kembali</string>
<string name="radar_notify">Beri tahu</string>
<string name="radar_az_text">Azimuth</string>
<string name="radar_az_value" translatable="false">%.1f°</string>
<string name="radar_el_text">Elevasi</string>
<string name="radar_el_value" translatable="false">%.1f°</string>
<string name="radar_alt_text">Ketinggian</string>
<string name="radar_alt_value" translatable="false">%.0f km</string>
<string name="radar_dist_text">Jarak</string>
<string name="radar_dist_value" translatable="false">%.0f km</string>
<string name="radar_eclipsed">Gerhana</string>
<string name="radar_downlink">Downlink</string>
<string name="radar_uplink">Uplink</string>
<string name="radar_link_low" translatable="false">%.4f</string>
<string name="radar_link_lowHigh" translatable="false">%.4f - %.4f</string>
<string name="radar_no_link" translatable="false">- - . - -</string>
<string name="radar_no_data">Satelit ini tidak memiliki transceiver</string>
<string name="radar_add">Tambah</string>
<string name="radar_data">Transceiver satelit:</string>
<string name="radar_inverted">Terbalik: %s</string>
<string name="radar_mode">Mode: %s</string>
<string name="radar_string_no">Tidak</string>
<string name="radar_string_yes">Ya</string>
<string name="radar_visible">Terlihat</string>
<string name="radar_doppler_calc">Kalkulator Frekuensi Doppler</string>
<string name="radar_doppler_tx_hint">Masukkan frekuensi TX (MHz)</string>
<string name="radar_doppler_rx_hint">Masukkan frekuensi RX (MHz)</string>
<string name="radar_doppler_offset_hint">Offset (kHz)</string>
<string name="radar_doppler_info">Untuk transponder linier, ketik satu frekuensi untuk melihat frekuensi lainnya</string>
<string name="radar_cw_decoder">Dekoder CW</string>
<string name="radar_cw_start">Mulai</string>
<string name="radar_cw_stop">Berhenti</string>
<string name="radar_cw_reset">Bersihkan</string>
<string name="map_prev">Sebelumnya</string>
<string name="map_next">Berikutnya</string>
<string name="map_copyright" translatable="false">© Kontributor OpenStreetMap</string>
<string name="map_azimuth">Azimuth: %.1f°</string>
<string name="map_elevation">Elevasi: %.1f°</string>
<string name="map_altitude">Ketinggian: %.0f km</string>
<string name="map_distance">Jarak: %.0f km</string>
<string name="map_latitude">Lintang: %.1f°</string>
<string name="map_longitude">Bujur: %.1f°</string>
<string name="map_qth" translatable="false">QTH: %s</string>
<string name="map_phase">Fase: %.1f°</string>
<string name="map_eclipsed">Gerhana</string>
<string name="map_period">Periode: %.0f mnt</string>
<string name="map_velocity">Kecepatan: %.2f km/d</string>
<string name="map_visibility">Visibilitas: %s</string>
<string name="map_visible">Terlihat</string>
<string name="prefs_app_title" translatable="false">Look4Sat Pro v%s</string>
<string name="prefs_app_url" translatable="false">https://play.google.com/store/apps/details?id=com.rtbishop.look4sat</string>
<string name="prefs_donate_title">Donasi</string>
<string name="prefs_donate_url" translatable="false">https://ko-fi.com/rt_bishop</string>
<string name="prefs_fdroid_title" translatable="false">F-Droid</string>
<string name="prefs_fdroid_url" translatable="false">https://f-droid.org/en/packages/com.rtbishop.look4sat/</string>
<string name="prefs_github_title" translatable="false">GitHub</string>
<string name="prefs_github_url" translatable="false">https://github.com/rt-bishop/Look4Sat/</string>
<string name="prefs_license_title" translatable="false">GPLv3</string>
<string name="prefs_license_url" translatable="false">https://www.gnu.org/licenses/gpl-3.0.html</string>
<string name="prefs_privacy_title">Kebijakan Privasi</string>
<string name="prefs_privacy_url" translatable="false">https://sites.google.com/view/look4sat-privacy-policy/home</string>
<string name="prefs_updated_title">Diperbarui: %s</string>
<string name="prefs_updated_time">d MMM yyyy - HH:mm:ss</string>
<string name="prefs_loc_title">Posisi stasiun</string>
<string name="prefs_loc_gps_title">GPS</string>
<string name="prefs_loc_gps_error">Tidak dapat mengambil lokasi - periksa izin dan sinyal GPS/jaringan</string>
<string name="prefs_loc_input_title">Input</string>
<string name="prefs_loc_input_error">Lokasi yang dimasukkan tidak valid</string>
<string name="prefs_loc_qth_title">QTH</string>
<string name="prefs_loc_qth_error">Lokator QTH tidak valid</string>
<string name="prefs_loc_success">Posisi berhasil diperbarui</string>
<string name="prefs_station_title">Pengaturan posisi stasiun</string>
<string name="prefs_station_lat_text">Atur garis lintang stasiun Anda</string>
<string name="prefs_station_lon_text">Atur garis bujur stasiun Anda</string>
<string name="prefs_locator_title">Pengaturan lokator QTH</string>
<string name="prefs_locator_text">Atur posisi stasiun menggunakan lokator</string>
<string name="prefs_data_title">Data satelit</string>
<string name="prefs_data_entries">Satelit: %s</string>
<string name="prefs_data_radios">Transceiver: %s</string>
<string name="prefs_data_update">Perbarui</string>
<string name="prefs_data_import">URL Kustom</string>
<string name="prefs_data_clear">Bersihkan</string>
<string name="prefs_data_clear_success">Data berhasil dibersihkan</string>
<string name="prefs_data_update_success">Pembaruan berhasil diselesaikan</string>
<string name="prefs_data_update_failed">Pembaruan gagal - periksa jaringan Anda</string>
<string name="prefs_data_import_satellites_error">Tidak ada satelit yang diimpor. Pilih file TLE/3LE (.txt) atau OMM (.csv) yang valid.</string>
<string name="prefs_data_import_transceivers_error">Tidak ada transceiver yang diimpor. Pilih file SatNOGS (.json) yang valid.</string>
<string name="prefs_data_sources_title">Sumber data kustom</string>
<string name="prefs_data_sources_tle_switch">URL TLE Kustom</string>
<string name="prefs_data_sources_transceivers_switch">URL transceiver kustom</string>
<string name="prefs_data_sources_url_title" translatable="false">URL (HTTPS)</string>
<string name="prefs_data_reset_url">Setel ulang ke URL bawaan</string>
<string name="prefs_data_output_title">Keluaran data</string>
<string name="prefs_net_output">Jaringan</string>
<string name="prefs_bt_output">Bluetooth</string>
<string name="prefs_cat_output">CAT</string>
<string name="nav_radiocontrol">Kontrol Radio</string>
<string name="rc_settings_title">Kontrol Radio CAT</string>
<string name="rc_radio_model">Model Radio</string>
<string name="rc_tx_device_hint">Alamat BT Radio TX</string>
<string name="rc_rx_device_hint">Alamat BT Radio RX</string>
<string name="rc_tx_name_hint">Nama Radio TX</string>
<string name="rc_rx_name_hint">Nama Radio RX</string>
<string name="rc_enable_switch">Aktifkan Kontrol CAT</string>
<string name="prefs_net_title">Keluaran data jaringan</string>
<string name="prefs_net_rotator_switch">Aktifkan keluaran rotator</string>
<string name="prefs_net_rotator_address_hint">IP:Port</string>
<string name="prefs_net_rotator_format_hint">Format</string>
<string name="prefs_net_frequency_switch">Aktifkan keluaran frekuensi</string>
<string name="prefs_net_frequency_address_hint">IP:Port</string>
<string name="prefs_net_frequency_format_hint">Format</string>
<string name="prefs_bt_title">Keluaran data Bluetooth</string>
<string name="prefs_bt_rotator_switch">Aktifkan keluaran rotator</string>
<string name="prefs_bt_rotator_device_hint">Id perangkat</string>
<string name="prefs_bt_rotator_output_hint">Format data</string>
<string name="prefs_bt_frequency_switch">Aktifkan keluaran frekuensi</string>
<string name="prefs_bt_frequency_device_hint">Id perangkat</string>
<string name="prefs_bt_frequency_output_hint">Format data</string>
<string name="prefs_bt_perm_error">Periksa izin bluetooth Anda</string>
<string name="prefs_net_perm_error">Periksa izin jaringan Anda</string>
<string name="tracking_status">Melacak: %s</string>
<string name="loc_lat_text">Lintang: %s°</string>
<string name="loc_lon_text">Bujur: %s°</string>
<string name="loc_qth_text">QTH: %s</string>
<string name="connect_radio">Hubungkan</string>
<string name="disconnect_radio">Putuskan</string>
<string name="track_radio">Lacak</string>
<string name="stop_radio">Berhenti</string>
<string name="prefs_wavelog_title">Server log WaveLog</string>
<string name="prefs_wavelog_url_hint">Alamat server</string>
<string name="prefs_wavelog_key_hint">Kunci API</string>
<string name="prefs_wavelog_station_hint">ID stasiun</string>
<string name="prefs_wavelog_auto">Unggah otomatis</string>
<string name="prefs_wavelog_test">Uji koneksi</string>
<string name="prefs_wavelog_upload">Unggah sekarang</string>
<string name="wavelog_title">Log</string>
<string name="wavelog_select_transponder">Pilih transponder</string>
<string name="wavelog_call_hint">Tanda panggil</string>
<string name="wavelog_mode_hint">Mode</string>
<string name="wavelog_saved">Tersimpan ke log lokal</string>
<string name="wavelog_need_config">Konfigurasikan WaveLog di Pengaturan terlebih dahulu</string>
<string name="wavelog_empty">Belum ada entri log</string>
<string name="wavelog_grid_mismatch">Grid QTH (%1$s) tidak cocok dengan grid stasiun (%2$s). Tetap unggah?</string>
<string name="wavelog_ignore">Abaikan dan unggah</string>
<string name="wavelog_cancel">Batal</string>
<string name="wavelog_copy">Salin</string>
<string name="wavelog_delete_undo">Urungkan</string>
<string name="wavelog_col_time">Waktu</string>
<string name="wavelog_col_freq">Frek.</string>
<string name="wavelog_col_sat">Satelit</string>
<string name="wavelog_col_call">Panggilan</string>
<string name="wavelog_col_uploaded">Terkirim</string>
<string name="wavelog_ungrouped">Tidak dikelompokkan</string>
<string name="prefs_other_title">Pengaturan lainnya</string>
<string name="prefs_ui_title">Pengaturan UI</string>
<string name="prefs_ui_order_title">Urutan halaman</string>
<string name="prefs_ui_main_title">Menu utama (bilah bawah)</string>
<string name="prefs_ui_sub_title">Menu lainnya</string>
<string name="prefs_ui_move_out">Pindah ke menu lainnya</string>
<string name="prefs_ui_move_back">Kembalikan ke menu utama</string>
<string name="prefs_ui_order_locked">Tetap (tidak dapat dipindahkan)</string>
<string name="prefs_ui_order_reset">Setel ulang urutan</string>
<string name="prefs_other_switch_utc">Tampilkan waktu lintasan dalam UTC</string>
<string name="prefs_other_switch_update">Aktifkan pembaruan data otomatis</string>
<string name="prefs_other_switch_sweep">Aktifkan animasi sapuan radar</string>
<string name="prefs_other_switch_sensors">Gunakan sensor untuk memutar tampilan radar</string>
<string name="prefs_other_switch_night_mode">Aktifkan filter mode malam merah</string>
<string name="prefs_highlight_title">Sorotan elevasi</string>
<string name="prefs_highlight_low">Rendah &lt; %1$d°</string>
<string name="prefs_highlight_mid">Sedang %1$d–%2$d°</string>
<string name="prefs_highlight_high">Tinggi &gt; %1$d°</string>
<string name="prefs_outro_title">BG7NTA &amp; penulis asli mengucapkan terima kasih</string>
<string name="prefs_outro_thanks" translatable="false">
* Pengguna dan kontributor Look4Sat!
\n* David A. B. Johnson (predict4java)
\n* Dave Moten (predict4java)
\n* Alexandru Csete (Gpredict)
\n* Dr T.S. Kelso (Celestrak)
\n* Libre Space Foundation (SatNOGS)
\n* xdsopl dan kontributor Robot36!
\n* BA7OPF (fitur pencocokan lintasan)
\n* BG7NTA</string>
<string name="prefs_outro_license">Aplikasi ini hadir tanpa jaminan</string>
</resources>
@@ -0,0 +1,252 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name" translatable="false">Look4Sat Pro</string>
<string name="btn_accept">Terima</string>
<string name="btn_cancel">Batal</string>
<string name="empty_list_message">Tidak ada data untuk ditampilkan</string>
<string name="nav_sat">Satelit</string>
<string name="nav_pass">Lintasan</string>
<string name="nav_radar">Radar</string>
<string name="nav_mutual">Pasangan</string>
<string name="nav_roaming">Jelajah</string>
<string name="nav_cw">Dekoder CW</string>
<string name="nav_map">Peta</string>
<string name="nav_prefs">Pengaturan</string>
<string name="nav_more">Lainnya</string>
<string name="nav_log">Log</string>
<string name="nav_amsat">AMSAT</string>
<string name="amsat_title">Status Satelit AMSAT</string>
<string name="amsat_updated">Diperbarui:</string>
<string name="amsat_active">Aktif</string>
<string name="amsat_tlm">TLM/beacon</string>
<string name="amsat_not_heard">Tidak terdengar</string>
<string name="amsat_conflict">Konflik</string>
<string name="amsat_name">Name</string>
<string name="amsat_load_failed">Gagal memuat - periksa jaringan dan coba lagi</string>
<string name="amsat_no_reports">Tidak ada laporan untuk periode ini</string>
<string name="amsat_close">Tutup</string>
<string name="sat_type_hint">Jenis: %s</string>
<string name="sat_type_title">Pilih jenis satelit</string>
<string name="sat_search_hint">Id - Nama</string>
<string name="sat_search_clear">Bersihkan</string>
<string name="sat_clear_all">Bersihkan semua</string>
<string name="sat_select_all">Pilih semua</string>
<string name="sat_empty_list_message">Pastikan kata kunci pencarian Anda benar dan DB sudah diperbarui</string>
<string name="sat_warning_title">Peringatan\!</string>
<string name="sat_warning_message">
Ada lebih dari 9000 satelit yang terdaftar di aplikasi ini.
Tidak masuk akal untuk melacak semuanya sekaligus.
\n\nSelalu coba persempit daftar hanya ke satelit yang
Anda minati melalui pencarian dan pemilih jenis.</string>
<string name="pass_filter_title">Filter lintasan</string>
<string name="pass_filter_elev">Elevasi minimal</string>
<string name="pass_filter_hours">Waktu ke depan</string>
<string name="pass_filter_aos_time">Jendela AOS</string>
<string name="pass_filter_invert_time">Balik jendela AOS</string>
<string name="pass_filter_deep_space">DeepSpace (periode >225 mnt)</string>
<string name="pass_time_placeholder" translatable="false"> -- : -- : -- </string>
<string name="pass_modes_title">Pilih jenis modulasi</string>
<string name="pass_satId" translatable="false">%05d</string>
<string name="pass_elevation">Elevasi: %.1f°</string>
<string name="pass_deep_space" translatable="false">DeepSpace</string>
<string name="pass_altitude">Ketinggian: %d km</string>
<string name="pass_aosLos" translatable="false">%03d° - %03d°</string>
<string name="pass_empty_list_message">
Pastikan filter Anda benar dan Anda sudah memilih satelit</string>
<string name="pass_error_message">Tidak ada lintasan satelit mendatang untuk ditampilkan</string>
<string name="pass_welcome_title">Selamat datang di Look4Sat\!</string>
<string name="pass_welcome_message">
Pastikan Anda mengatur posisi melalui GPS, Lat/Lon atau QTH di Pengaturan.
\n\nPerbarui database setidaknya seminggu sekali untuk prediksi yang akurat.</string>
<string name="pass_whatsnew_title" translatable="false">Apa yang baru di Look4Sat</string>
<string name="pass_whatsnew_message" translatable="false">
* BARU: Halaman status satelit AMSAT - tabel status langsung (warna resmi), jumlah laporan, laporan detail per satelit/hari
* Log dikelompokkan per lintasan satelit (sesi = satelit + waktu AOS), garis pemisah antar sesi
* GPS kini hanya menampilkan sukses saat posisi benar-benar didapat (timeout 15 dtk, cek izin)
* Pesan kegagalan jelas saat database satelit gagal diunduh
* Frekuensi log mengikuti panel Doppler secara real-time; visual hapus geser dipulihkan
* Perbaikan bahasa Indonesia (values-id)</string>
<string name="radar_back">Kembali</string>
<string name="radar_notify">Beri tahu</string>
<string name="radar_az_text">Azimuth</string>
<string name="radar_az_value" translatable="false">%.1f°</string>
<string name="radar_el_text">Elevasi</string>
<string name="radar_el_value" translatable="false">%.1f°</string>
<string name="radar_alt_text">Ketinggian</string>
<string name="radar_alt_value" translatable="false">%.0f km</string>
<string name="radar_dist_text">Jarak</string>
<string name="radar_dist_value" translatable="false">%.0f km</string>
<string name="radar_eclipsed">Gerhana</string>
<string name="radar_downlink">Downlink</string>
<string name="radar_uplink">Uplink</string>
<string name="radar_link_low" translatable="false">%.4f</string>
<string name="radar_link_lowHigh" translatable="false">%.4f - %.4f</string>
<string name="radar_no_link" translatable="false">- - . - -</string>
<string name="radar_no_data">Satelit ini tidak memiliki transceiver</string>
<string name="radar_add">Tambah</string>
<string name="radar_data">Transceiver satelit:</string>
<string name="radar_inverted">Terbalik: %s</string>
<string name="radar_mode">Mode: %s</string>
<string name="radar_string_no">Tidak</string>
<string name="radar_string_yes">Ya</string>
<string name="radar_visible">Terlihat</string>
<string name="radar_doppler_calc">Kalkulator Frekuensi Doppler</string>
<string name="radar_doppler_tx_hint">Masukkan frekuensi TX (MHz)</string>
<string name="radar_doppler_rx_hint">Masukkan frekuensi RX (MHz)</string>
<string name="radar_doppler_offset_hint">Offset (kHz)</string>
<string name="radar_doppler_info">Untuk transponder linier, ketik satu frekuensi untuk melihat frekuensi lainnya</string>
<string name="radar_cw_decoder">Dekoder CW</string>
<string name="radar_cw_start">Mulai</string>
<string name="radar_cw_stop">Berhenti</string>
<string name="radar_cw_reset">Bersihkan</string>
<string name="map_prev">Sebelumnya</string>
<string name="map_next">Berikutnya</string>
<string name="map_copyright" translatable="false">© Kontributor OpenStreetMap</string>
<string name="map_azimuth">Azimuth: %.1f°</string>
<string name="map_elevation">Elevasi: %.1f°</string>
<string name="map_altitude">Ketinggian: %.0f km</string>
<string name="map_distance">Jarak: %.0f km</string>
<string name="map_latitude">Lintang: %.1f°</string>
<string name="map_longitude">Bujur: %.1f°</string>
<string name="map_qth" translatable="false">QTH: %s</string>
<string name="map_phase">Fase: %.1f°</string>
<string name="map_eclipsed">Gerhana</string>
<string name="map_period">Periode: %.0f mnt</string>
<string name="map_velocity">Kecepatan: %.2f km/d</string>
<string name="map_visibility">Visibilitas: %s</string>
<string name="map_visible">Terlihat</string>
<string name="prefs_app_title" translatable="false">Look4Sat Pro v%s</string>
<string name="prefs_app_url" translatable="false">https://play.google.com/store/apps/details?id=com.rtbishop.look4sat</string>
<string name="prefs_donate_title">Donasi</string>
<string name="prefs_donate_url" translatable="false">https://ko-fi.com/rt_bishop</string>
<string name="prefs_fdroid_title" translatable="false">F-Droid</string>
<string name="prefs_fdroid_url" translatable="false">https://f-droid.org/en/packages/com.rtbishop.look4sat/</string>
<string name="prefs_github_title" translatable="false">GitHub</string>
<string name="prefs_github_url" translatable="false">https://github.com/rt-bishop/Look4Sat/</string>
<string name="prefs_license_title" translatable="false">GPLv3</string>
<string name="prefs_license_url" translatable="false">https://www.gnu.org/licenses/gpl-3.0.html</string>
<string name="prefs_privacy_title">Kebijakan Privasi</string>
<string name="prefs_privacy_url" translatable="false">https://sites.google.com/view/look4sat-privacy-policy/home</string>
<string name="prefs_updated_title">Diperbarui: %s</string>
<string name="prefs_updated_time">d MMM yyyy - HH:mm:ss</string>
<string name="prefs_loc_title">Posisi stasiun</string>
<string name="prefs_loc_gps_title">GPS</string>
<string name="prefs_loc_gps_error">Tidak dapat mengambil lokasi - periksa izin dan sinyal GPS/jaringan</string>
<string name="prefs_loc_input_title">Input</string>
<string name="prefs_loc_input_error">Lokasi yang dimasukkan tidak valid</string>
<string name="prefs_loc_qth_title">QTH</string>
<string name="prefs_loc_qth_error">Lokator QTH tidak valid</string>
<string name="prefs_loc_success">Posisi berhasil diperbarui</string>
<string name="prefs_station_title">Pengaturan posisi stasiun</string>
<string name="prefs_station_lat_text">Atur garis lintang stasiun Anda</string>
<string name="prefs_station_lon_text">Atur garis bujur stasiun Anda</string>
<string name="prefs_locator_title">Pengaturan lokator QTH</string>
<string name="prefs_locator_text">Atur posisi stasiun menggunakan lokator</string>
<string name="prefs_data_title">Data satelit</string>
<string name="prefs_data_entries">Satelit: %s</string>
<string name="prefs_data_radios">Transceiver: %s</string>
<string name="prefs_data_update">Perbarui</string>
<string name="prefs_data_import">URL Kustom</string>
<string name="prefs_data_clear">Bersihkan</string>
<string name="prefs_data_clear_success">Data berhasil dibersihkan</string>
<string name="prefs_data_update_success">Pembaruan berhasil diselesaikan</string>
<string name="prefs_data_update_failed">Pembaruan gagal - periksa jaringan Anda</string>
<string name="prefs_data_import_satellites_error">Tidak ada satelit yang diimpor. Pilih file TLE/3LE (.txt) atau OMM (.csv) yang valid.</string>
<string name="prefs_data_import_transceivers_error">Tidak ada transceiver yang diimpor. Pilih file SatNOGS (.json) yang valid.</string>
<string name="prefs_data_sources_title">Sumber data kustom</string>
<string name="prefs_data_sources_tle_switch">URL TLE Kustom</string>
<string name="prefs_data_sources_transceivers_switch">URL transceiver kustom</string>
<string name="prefs_data_sources_url_title" translatable="false">URL (HTTPS)</string>
<string name="prefs_data_reset_url">Setel ulang ke URL bawaan</string>
<string name="prefs_data_output_title">Keluaran data</string>
<string name="prefs_net_output">Jaringan</string>
<string name="prefs_bt_output">Bluetooth</string>
<string name="prefs_cat_output">CAT</string>
<string name="nav_radiocontrol">Kontrol Radio</string>
<string name="rc_settings_title">Kontrol Radio CAT</string>
<string name="rc_radio_model">Model Radio</string>
<string name="rc_tx_device_hint">Alamat BT Radio TX</string>
<string name="rc_rx_device_hint">Alamat BT Radio RX</string>
<string name="rc_tx_name_hint">Nama Radio TX</string>
<string name="rc_rx_name_hint">Nama Radio RX</string>
<string name="rc_enable_switch">Aktifkan Kontrol CAT</string>
<string name="prefs_net_title">Keluaran data jaringan</string>
<string name="prefs_net_rotator_switch">Aktifkan keluaran rotator</string>
<string name="prefs_net_rotator_address_hint">IP:Port</string>
<string name="prefs_net_rotator_format_hint">Format</string>
<string name="prefs_net_frequency_switch">Aktifkan keluaran frekuensi</string>
<string name="prefs_net_frequency_address_hint">IP:Port</string>
<string name="prefs_net_frequency_format_hint">Format</string>
<string name="prefs_bt_title">Keluaran data Bluetooth</string>
<string name="prefs_bt_rotator_switch">Aktifkan keluaran rotator</string>
<string name="prefs_bt_rotator_device_hint">Id perangkat</string>
<string name="prefs_bt_rotator_output_hint">Format data</string>
<string name="prefs_bt_frequency_switch">Aktifkan keluaran frekuensi</string>
<string name="prefs_bt_frequency_device_hint">Id perangkat</string>
<string name="prefs_bt_frequency_output_hint">Format data</string>
<string name="prefs_bt_perm_error">Periksa izin bluetooth Anda</string>
<string name="prefs_net_perm_error">Periksa izin jaringan Anda</string>
<string name="tracking_status">Melacak: %s</string>
<string name="loc_lat_text">Lintang: %s°</string>
<string name="loc_lon_text">Bujur: %s°</string>
<string name="loc_qth_text">QTH: %s</string>
<string name="connect_radio">Hubungkan</string>
<string name="disconnect_radio">Putuskan</string>
<string name="track_radio">Lacak</string>
<string name="stop_radio">Berhenti</string>
<string name="prefs_wavelog_title">Server log WaveLog</string>
<string name="prefs_wavelog_url_hint">Alamat server</string>
<string name="prefs_wavelog_key_hint">Kunci API</string>
<string name="prefs_wavelog_station_hint">ID stasiun</string>
<string name="prefs_wavelog_auto">Unggah otomatis</string>
<string name="prefs_wavelog_test">Uji koneksi</string>
<string name="prefs_wavelog_upload">Unggah sekarang</string>
<string name="wavelog_title">Log</string>
<string name="wavelog_select_transponder">Pilih transponder</string>
<string name="wavelog_call_hint">Tanda panggil</string>
<string name="wavelog_mode_hint">Mode</string>
<string name="wavelog_saved">Tersimpan ke log lokal</string>
<string name="wavelog_need_config">Konfigurasikan WaveLog di Pengaturan terlebih dahulu</string>
<string name="wavelog_empty">Belum ada entri log</string>
<string name="wavelog_grid_mismatch">Grid QTH (%1$s) tidak cocok dengan grid stasiun (%2$s). Tetap unggah?</string>
<string name="wavelog_ignore">Abaikan dan unggah</string>
<string name="wavelog_cancel">Batal</string>
<string name="wavelog_copy">Salin</string>
<string name="wavelog_delete_undo">Urungkan</string>
<string name="wavelog_col_time">Waktu</string>
<string name="wavelog_col_freq">Frek.</string>
<string name="wavelog_col_sat">Satelit</string>
<string name="wavelog_col_call">Panggilan</string>
<string name="wavelog_col_uploaded">Terkirim</string>
<string name="wavelog_ungrouped">Tidak dikelompokkan</string>
<string name="prefs_other_title">Pengaturan lainnya</string>
<string name="prefs_ui_title">Pengaturan UI</string>
<string name="prefs_ui_order_title">Urutan halaman</string>
<string name="prefs_ui_main_title">Menu utama (bilah bawah)</string>
<string name="prefs_ui_sub_title">Menu lainnya</string>
<string name="prefs_ui_move_out">Pindah ke menu lainnya</string>
<string name="prefs_ui_move_back">Kembalikan ke menu utama</string>
<string name="prefs_ui_order_locked">Tetap (tidak dapat dipindahkan)</string>
<string name="prefs_ui_order_reset">Setel ulang urutan</string>
<string name="prefs_other_switch_utc">Tampilkan waktu lintasan dalam UTC</string>
<string name="prefs_other_switch_update">Aktifkan pembaruan data otomatis</string>
<string name="prefs_other_switch_sweep">Aktifkan animasi sapuan radar</string>
<string name="prefs_other_switch_sensors">Gunakan sensor untuk memutar tampilan radar</string>
<string name="prefs_other_switch_night_mode">Aktifkan filter mode malam merah</string>
<string name="prefs_highlight_title">Sorotan elevasi</string>
<string name="prefs_highlight_low">Rendah &lt; %1$d°</string>
<string name="prefs_highlight_mid">Sedang %1$d–%2$d°</string>
<string name="prefs_highlight_high">Tinggi &gt; %1$d°</string>
<string name="prefs_outro_title">BG7NTA &amp; penulis asli mengucapkan terima kasih</string>
<string name="prefs_outro_thanks" translatable="false">
* Pengguna dan kontributor Look4Sat!
\n* David A. B. Johnson (predict4java)
\n* Dave Moten (predict4java)
\n* Alexandru Csete (Gpredict)
\n* Dr T.S. Kelso (Celestrak)
\n* Libre Space Foundation (SatNOGS)
\n* xdsopl dan kontributor Robot36!
\n* BA7OPF (fitur pencocokan lintasan)
\n* BG7NTA</string>
<string name="prefs_outro_license">Aplikasi ini hadir tanpa jaminan</string>
</resources>
@@ -10,8 +10,22 @@
<string name="nav_sat">Uydular</string>
<string name="nav_pass">Geçişler</string>
<string name="nav_radar">Radar</string>
<string name="nav_cw">CW Dekoder</string>
<string name="nav_map">Harita</string>
<string name="nav_prefs">Ayarlar</string>
<string name="nav_more">Daha Fazla</string>
<string name="nav_log">Günlük</string>
<string name="nav_amsat">AMSAT</string>
<string name="amsat_title">AMSAT Uydu Durumu</string>
<string name="amsat_updated">Güncellendi:</string>
<string name="amsat_active">Aktif</string>
<string name="amsat_tlm">TLM/işaret</string>
<string name="amsat_not_heard">Duyulmadı</string>
<string name="amsat_conflict">Çelişkili</string>
<string name="amsat_name">Name</string>
<string name="amsat_load_failed">Yükleme başarısız - ağı kontrol edip tekrar deneyin</string>
<string name="amsat_no_reports">Bu dönem için rapor yok</string>
<string name="amsat_close">Kapat</string>
<!-- Satellites screen -->
<string name="sat_type_hint">Uydu türü: %s</string>
@@ -50,14 +64,13 @@
Ayarlar\'dan GPS, Enlem/Boylam veya QTH kullanarak konumunuzu belirlediğinizden emin olun.
\n\nDoğru tahminler alabilmek için veritabanını en az haftada bir güncelleyin.</string>
<string name="pass_whatsnew_title" translatable="false">What\'s new in Look4Sat</string>
<string name="pass_whatsnew_message" translatable="false">
* Roaming sayfası QTH定位器 2.0\'dan birebir taşındı: 8 haneli konum, 3x3 ızgara, kırmızı işaret, canlı GPS
\n\n* İçeriğin sistem durum/gezinme çubuklarıyla çakışması düzeltildi; ızgara sütunları referans düzenle eşleşiyor
\n\n* Roaming sayfası renkleri uygulama temasını takip eder - kırmızı gece modunda tamamen görünür
\n\n* Ayarlar: sıkışan anahtar satırları ve teşekkür aralığı düzeltildi
\n\n* Uygulama adı artık Look4Sat Pro
\n\n* Ayarlar: "İçe aktar" düğmesi "Özel URL" olarak yeniden adlandırıldı - TLE/alıcı-verici kaynak URL\'leri doğrudan düzenlenebilir
</string>
<string name="pass_whatsnew_message" translatable="false">
* YENİ: AMSAT uydu durum sayfası - canlı durum tablosu (resmi renkler), rapor sayıları, uydu/gün bazında detaylı raporlar
* Günlükler uydu geçişine göre gruplanır (oturum = uydu + AOS zamanı), oturumlar arasında ayırıcı çizgi
* GPS artık yalnızca gerçek konum alındığında başarı gösterir (15 sn zaman aşımı, izin kontrolü)
* Uydu veritabanı indirilemediğinde net hata mesajı gösterilir
* Günlük frekansları Doppler panelini gerçek zamanlı takip eder; kaydırarak silme görselleri düzeltildi
* Endonezya dili düzeltmesi (values-id)</string>
<!-- Radar screen -->
<string name="radar_back">Geri</string>
@@ -122,7 +135,7 @@
<string name="prefs_loc_title">İstasyon konumu</string>
<string name="prefs_loc_gps_title">GPS</string>
<string name="prefs_loc_gps_error">Konum izinlerinizi kontrol edin</string>
<string name="prefs_loc_gps_error">Konum alınamıyor - izinleri ve GPS/ağ sinyalini kontrol edin</string>
<string name="prefs_loc_input_title">Manuel giriş</string>
<string name="prefs_loc_input_error">Geçersiz konum girildi</string>
<string name="prefs_loc_qth_title">QTH</string>
@@ -142,6 +155,7 @@
<string name="prefs_data_clear">Temizle</string>
<string name="prefs_data_clear_success">Veriler başarıyla temizlendi</string>
<string name="prefs_data_update_success">Güncelleme başarıyla tamamlandı</string>
<string name="prefs_data_update_failed">Güncelleme başarısız - ağınızı kontrol edin</string>
<string name="prefs_data_sources_title">Özel veri kaynakları</string>
<string name="prefs_data_sources_tle_switch">Özel TLE URL</string>
@@ -182,7 +196,48 @@
<string name="prefs_bt_perm_error">Bluetooth izninizi kontrol edin</string>
<string name="prefs_net_perm_error">Ağ izninizi kontrol edin</string>
<string name="tracking_status">İzleniyor: %s</string>
<string name="loc_lat_text">Enlem: %s°</string>
<string name="loc_lon_text">Boylam: %s°</string>
<string name="loc_qth_text">QTH: %s</string>
<string name="connect_radio">Bağlan</string>
<string name="disconnect_radio">Bağlantıyı Kes</string>
<string name="track_radio">Takip</string>
<string name="stop_radio">Dur</string>
<string name="prefs_wavelog_title">WaveLog günlük sunucusu</string>
<string name="prefs_wavelog_url_hint">Sunucu adresi</string>
<string name="prefs_wavelog_key_hint">API anahtarı</string>
<string name="prefs_wavelog_station_hint">İstasyon kimliği</string>
<string name="prefs_wavelog_auto">Otomatik yükleme</string>
<string name="prefs_wavelog_test">Bağlantıyı test et</string>
<string name="prefs_wavelog_upload">Şimdi yükle</string>
<string name="wavelog_title">Günlük</string>
<string name="wavelog_select_transponder">Transponder seç</string>
<string name="wavelog_call_hint">Çağrı işareti</string>
<string name="wavelog_mode_hint">Mod</string>
<string name="wavelog_saved">Yerel günlüğe kaydedildi</string>
<string name="wavelog_need_config">Önce Ayarlar\'da WaveLog\'u yapılandırın</string>
<string name="wavelog_empty">Henüz günlük kaydı yok</string>
<string name="wavelog_grid_mismatch">QTH ızgarası (%1$s) istasyon ızgarasıyla (%2$s) eşleşmiyor. Yine de yüklensin mi?</string>
<string name="wavelog_ignore">Yoksay ve yükle</string>
<string name="wavelog_cancel">İptal</string>
<string name="wavelog_copy">Kopyala</string>
<string name="wavelog_delete_undo">Geri al</string>
<string name="wavelog_col_time">Zaman</string>
<string name="wavelog_col_freq">Frekans</string>
<string name="wavelog_col_sat">Uydu</string>
<string name="wavelog_col_call">Çağrı</string>
<string name="wavelog_col_uploaded">Gönderildi</string>
<string name="wavelog_ungrouped">Gruplanmamış</string>
<string name="prefs_other_title">Diğer ayarlar</string>
<string name="prefs_ui_title">UI Ayarları</string>
<string name="prefs_ui_order_title">Sayfa sırası</string>
<string name="prefs_ui_main_title">Ana menü (alt çubuk)</string>
<string name="prefs_ui_sub_title">Daha fazla menü</string>
<string name="prefs_ui_move_out">Daha fazla menüye taşı</string>
<string name="prefs_ui_move_back">Ana menüye geri taşı</string>
<string name="prefs_ui_order_locked">Sabit (taşınamaz)</string>
<string name="prefs_ui_order_reset">Sıralamayı sıfırla</string>
<string name="prefs_other_switch_utc">Geçiş saatini UTC olarak göster</string>
<string name="prefs_other_switch_update">Otomatik veri güncellemeyi etkinleştir</string>
<string name="prefs_other_switch_sweep">Radar taramasını etkinleştir</string>
@@ -11,8 +11,22 @@
<string name="nav_radar">雷达</string>
<string name="nav_mutual">匹配</string>
<string name="nav_roaming">漫游</string>
<string name="nav_cw">CW解码</string>
<string name="nav_map">地图</string>
<string name="nav_prefs">设置</string>
<string name="nav_more">更多</string>
<string name="nav_log">日志</string>
<string name="nav_amsat">AMSAT</string>
<string name="amsat_title">AMSAT 卫星状态</string>
<string name="amsat_updated">更新:</string>
<string name="amsat_active">活跃</string>
<string name="amsat_tlm">仅遥测</string>
<string name="amsat_not_heard">未听到</string>
<string name="amsat_conflict">矛盾</string>
<string name="amsat_name">Name</string>
<string name="amsat_load_failed">加载失败,请检查网络后重试</string>
<string name="amsat_no_reports">该时段无报告</string>
<string name="amsat_close">关闭</string>
<!-- Satellites screen -->
<string name="sat_type_hint">类型:%s</string>
@@ -40,14 +54,13 @@
<string name="pass_welcome_title">欢迎来到 Look4Sat\!</string>
<string name="pass_welcome_message">请务必在设置中通过GPS、经纬度或QTH定位您的位置\n建议至少每周更新一次数据库,以确保预测结果的准确性</string>
<string name="pass_whatsnew_title">Look4Sat Pro 更新内容</string>
<string name="pass_whatsnew_message">
* 漫游页完整移植 QTH定位器 2.0:8 位网格、3x3 九宫格、红点查表定位、实时 GPS
\n\n* 修复内容被系统状态栏/导航栏遮挡;九宫格列宽与原版一致
\n\n* 漫游页配色跟随应用主题,红色夜间模式下清晰可见
\n\n* 设置:修复开关行挤压与感谢列表间距
\n\n* 应用名现为 Look4Sat Pro
\n\n* 设置:导入文件按钮更名为自定义URL,可直接编辑 TLE/收发机数据源地址
</string>
<string name="pass_whatsnew_message" translatable="false">
* 新增: AMSAT 卫星状态页 - 实时状态表格(官网配色)、报告数量、逐卫星/逐日详细报告
* 日志按场次分组(场次 = 卫星 + AOS 时间), 组间横线分隔
* GPS 真正定位成功才会提示成功(15 秒超时, 权限检查)
* 卫星数据库下载失败时有明确失败提示
* 日志频率实时跟随多普勒面板; 左滑删除视觉恢复
* 印尼语识别修复(values-id)</string>
<!-- Radar screen -->
<string name="radar_back">后退</string>
@@ -104,7 +117,7 @@
<string name="prefs_loc_title">站位</string>
<string name="prefs_loc_gps_title">GPS 定位</string>
<string name="prefs_loc_gps_error">检查您的位置权限</string>
<string name="prefs_loc_gps_error">无法获取位置,请检查定位权限和GPS/网络信号</string>
<string name="prefs_loc_input_title">输入位置</string>
<string name="prefs_loc_input_error">输入的位置无效</string>
<string name="prefs_loc_qth_title">QTH 网格</string>
@@ -124,6 +137,7 @@
<string name="prefs_data_clear">清空</string>
<string name="prefs_data_clear_success">数据清空成功</string>
<string name="prefs_data_update_success">更新成功</string>
<string name="prefs_data_update_failed">更新失败,请检查网络</string>
<string name="prefs_data_import_satellites_error">未导入卫星。请选择有效的 TLE/3LE (.txt) 或 OMM (.csv) 文件。</string>
<string name="prefs_data_import_transceivers_error">未导入收发器。请选择有效的 SatNOGS (.json) 文件。</string>
@@ -165,7 +179,48 @@
<string name="rc_rx_name_hint">接收电台名称</string>
<string name="rc_enable_switch">启用 CAT 控制</string>
<string name="tracking_status">跟踪: %s</string>
<string name="loc_lat_text">纬度: %s°</string>
<string name="loc_lon_text">经度: %s°</string>
<string name="loc_qth_text">QTH: %s</string>
<string name="connect_radio">连接</string>
<string name="disconnect_radio">断开</string>
<string name="track_radio">跟踪</string>
<string name="stop_radio">停止</string>
<string name="prefs_wavelog_title">WaveLog 日志服务器</string>
<string name="prefs_wavelog_url_hint">服务器地址</string>
<string name="prefs_wavelog_key_hint">API 密钥</string>
<string name="prefs_wavelog_station_hint">站点 ID</string>
<string name="prefs_wavelog_auto">自动上传</string>
<string name="prefs_wavelog_test">测试连接</string>
<string name="prefs_wavelog_upload">手动上传</string>
<string name="wavelog_title">日志</string>
<string name="wavelog_select_transponder">选择转发器</string>
<string name="wavelog_call_hint">对方呼号</string>
<string name="wavelog_mode_hint">模式</string>
<string name="wavelog_saved">已存入本地日志</string>
<string name="wavelog_need_config">请先在设置中配置 WaveLog</string>
<string name="wavelog_empty">暂无日志记录</string>
<string name="wavelog_grid_mismatch">当前 QTH 网格(%1$s)与站点网格(%2$s)不一致,仍要上传?</string>
<string name="wavelog_ignore">忽略并上传</string>
<string name="wavelog_cancel">取消</string>
<string name="wavelog_copy">复制</string>
<string name="wavelog_delete_undo">撤销</string>
<string name="wavelog_col_time">时间</string>
<string name="wavelog_col_freq">频率</string>
<string name="wavelog_col_sat">卫星</string>
<string name="wavelog_col_call">呼号</string>
<string name="wavelog_col_uploaded">已传</string>
<string name="wavelog_ungrouped">未分组</string>
<string name="prefs_other_title">其他设置</string>
<string name="prefs_ui_title">UI设置</string>
<string name="prefs_ui_order_title">页面顺序</string>
<string name="prefs_ui_main_title">主菜单(底部栏)</string>
<string name="prefs_ui_sub_title">更多菜单</string>
<string name="prefs_ui_move_out">移入更多菜单</string>
<string name="prefs_ui_move_back">移回主菜单</string>
<string name="prefs_ui_order_locked">固定(不可移动)</string>
<string name="prefs_ui_order_reset">重置顺序</string>
<string name="prefs_other_switch_utc">以 UTC 时间显示</string>
<string name="prefs_other_switch_update">启用卫星数据自动更新</string>
<string name="prefs_other_switch_sweep">启用雷达扫描动画</string>
@@ -12,8 +12,22 @@
<string name="nav_radar">Radar</string>
<string name="nav_mutual">Match</string>
<string name="nav_roaming">Roaming</string>
<string name="nav_cw">CW Decoder</string>
<string name="nav_map">Map</string>
<string name="nav_prefs">Settings</string>
<string name="nav_more">More</string>
<string name="nav_log">Log</string>
<string name="nav_amsat">AMSAT</string>
<string name="amsat_title">AMSAT Satellite Status</string>
<string name="amsat_updated">Updated:</string>
<string name="amsat_active">Active</string>
<string name="amsat_tlm">TLM/Beacon</string>
<string name="amsat_not_heard">Not Heard</string>
<string name="amsat_conflict">Conflicting</string>
<string name="amsat_name">Name</string>
<string name="amsat_load_failed">Load failed - check network and retry</string>
<string name="amsat_no_reports">No reports for this period</string>
<string name="amsat_close">Close</string>
<!-- Satellites screen -->
<string name="sat_type_hint">Type: %s</string>
@@ -52,14 +66,13 @@
Make sure to set your position via GPS, Lat/Lon or QTH in Settings.
\n\nPlease update the database at least weekly to get accurate predictions.</string>
<string name="pass_whatsnew_title" translatable="false">What\'s new in Look4Sat</string>
<string name="pass_whatsnew_message" translatable="false">
* Roaming page ported verbatim from QTH定位器 2.0: 8-char locator, 3x3 grid, red marker lookup, live GPS
\n\n* Fixed content overlapping the system status/navigation bars; grid columns now match the reference layout
\n\n* Roaming page colors follow the app theme - fully visible in red night mode
\n\n* Settings: fixed squeezed toggle rows and credits spacing
\n\n* App name is now Look4Sat Pro
\n\n* Settings: "Import" button renamed to "Custom URL" - edit TLE/transceiver source URLs directly
</string>
<string name="pass_whatsnew_message" translatable="false">
* NEW: AMSAT satellite status page - live status grid (official colors), report counts, detailed reports per satellite/day
* Log entries grouped by satellite pass (session ID = satellite + AOS time), divider lines between sessions
* GPS now only reports success when a real fix is acquired (15s timeout, permission check)
* Data update shows a clear failure message when the satellite database cannot be downloaded
* Log frequencies follow the Doppler panel in real time; swipe-to-delete visuals restored
* Indonesian locale fix (values-id)</string>
<!-- Radar screen -->
<string name="radar_back">Back</string>
@@ -134,7 +147,7 @@
<string name="prefs_loc_title">Station position</string>
<string name="prefs_loc_gps_title">GPS</string>
<string name="prefs_loc_gps_error">Check your location permissions</string>
<string name="prefs_loc_gps_error">Unable to get location - check permission and GPS/network signal</string>
<string name="prefs_loc_input_title">Input</string>
<string name="prefs_loc_input_error">Invalid location entered</string>
<string name="prefs_loc_qth_title">QTH</string>
@@ -154,6 +167,7 @@
<string name="prefs_data_clear">Clear</string>
<string name="prefs_data_clear_success">Data was cleared successfully</string>
<string name="prefs_data_update_success">Update completed successfully</string>
<string name="prefs_data_update_failed">Update failed - check your network</string>
<string name="prefs_data_import_satellites_error">No satellites imported. Select a valid TLE/3LE (.txt) or OMM (.csv) file.</string>
<string name="prefs_data_import_transceivers_error">No transceivers imported. Select a valid SatNOGS (.json) file.</string>
@@ -196,7 +210,48 @@
<string name="prefs_bt_perm_error">Check your bluetooth permission</string>
<string name="prefs_net_perm_error">Check your network permission</string>
<string name="tracking_status">Tracking: %s</string>
<string name="loc_lat_text">Lat: %s°</string>
<string name="loc_lon_text">Lon: %s°</string>
<string name="loc_qth_text">Qth: %s</string>
<string name="connect_radio">Connect</string>
<string name="disconnect_radio">Disconnect</string>
<string name="track_radio">Track</string>
<string name="stop_radio">Stop</string>
<string name="prefs_wavelog_title">WaveLog log server</string>
<string name="prefs_wavelog_url_hint">Server address</string>
<string name="prefs_wavelog_key_hint">API key</string>
<string name="prefs_wavelog_station_hint">Station ID</string>
<string name="prefs_wavelog_auto">Auto upload</string>
<string name="prefs_wavelog_test">Test connection</string>
<string name="prefs_wavelog_upload">Upload now</string>
<string name="wavelog_title">Log</string>
<string name="wavelog_select_transponder">Select transponder</string>
<string name="wavelog_call_hint">Callsign</string>
<string name="wavelog_mode_hint">Mode</string>
<string name="wavelog_saved">Saved to local log</string>
<string name="wavelog_need_config">Configure WaveLog in Settings first</string>
<string name="wavelog_empty">No log entries yet</string>
<string name="wavelog_grid_mismatch">QTH grid (%1$s) does not match station grid (%2$s). Upload anyway?</string>
<string name="wavelog_ignore">Ignore and upload</string>
<string name="wavelog_cancel">Cancel</string>
<string name="wavelog_copy">Copy</string>
<string name="wavelog_delete_undo">Undo</string>
<string name="wavelog_col_time">Time</string>
<string name="wavelog_col_freq">Freq</string>
<string name="wavelog_col_sat">Satellite</string>
<string name="wavelog_col_call">Call</string>
<string name="wavelog_col_uploaded">Sent</string>
<string name="wavelog_ungrouped">Ungrouped</string>
<string name="prefs_other_title">Other settings</string>
<string name="prefs_ui_title">UI Settings</string>
<string name="prefs_ui_order_title">Page order</string>
<string name="prefs_ui_main_title">Main menu (bottom bar)</string>
<string name="prefs_ui_sub_title">More menu</string>
<string name="prefs_ui_move_out">Move to more menu</string>
<string name="prefs_ui_move_back">Move back to main menu</string>
<string name="prefs_ui_order_locked">Fixed (cannot be moved)</string>
<string name="prefs_ui_order_reset">Reset order</string>
<string name="prefs_other_switch_utc">Show pass time in UTC</string>
<string name="prefs_other_switch_update">Enable automatic data update</string>
<string name="prefs_other_switch_sweep">Enable radar sweep animation</string>
+38
View File
@@ -0,0 +1,38 @@
import com.android.build.api.dsl.LibraryExtension
import com.android.build.api.variant.CanProduceConsumerProguardFiles
plugins {
alias(libs.plugins.convention.featurePlugin)
}
android {
namespace = "com.rtbishop.look4sat.feature.cw"
compileOptions {
encoding = "UTF-8"
}
// 照搬 Morse Expert 1.15: native 解码器仅 armeabi-v7a
defaultConfig {
ndk {
abiFilters += listOf("armeabi-v7a")
}
}
}
// CW 类保持规则(JNI 按类名注册 + 照搬混淆类保逻辑), 由 app 的 R8 消费
// AGP 9: consumer 规则走 variant 级 CanProduceConsumerProguardFiles
androidComponents {
onVariants(selector().all()) { variant ->
(variant as? CanProduceConsumerProguardFiles)?.consumerProguardFiles?.add(
project.layout.projectDirectory.file("proguard-rules.pro")
)
}
}
dependencies {
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
}
// 照搬的 Java 源码含中文注释, 强制 UTF-8 编译(compileOptions 在部分 AGP 版本不生效)
tasks.withType<JavaCompile>().configureEach {
options.encoding = "UTF-8"
}
+31
View File
@@ -0,0 +1,31 @@
# Look4Sat Pro — CW 解码模块(照搬 Morse Expert 1.15)混淆规则
# 所有 CW 类保持原名: pas.* 通过 RegisterNatives 按类名注册 JNI(混淆即崩);
# 其余为照搬的混淆名类(命名已无混淆意义, 保持以保逻辑完整)。
# JNI 引擎(RegisterNatives 按类名/方法名查表)
-keep class pas.** { *; }
# 照搬的解码/UI 类(全部保持)
-keep class com.ve3nea.morse_expert.** { *; }
-keep class H2.** { *; }
-keep class I2.b { *; }
-keep class J2.** { *; }
-keep class k3.** { *; }
-keep class i3.** { *; }
-keep class g3.** { *; }
-keep class j3.** { *; }
-keep class s.** { *; }
-keep class d1.AbstractC1518b { *; }
-keep class B0.b { *; }
-keep class B.RunnableC0001b { *; }
-keep class D.n { *; }
-keep class E2.g { *; }
-keep class F2.a { *; }
-keep class I0.d { *; }
-keep class K1.a { *; }
-keep class j1.C1646n { *; }
# sun.misc stub(Android 无此类, 仅编译期; 防止 R8 异常处理)
-dontwarn sun.misc.**
-keep class sun.misc.Unsafe { *; }
-keep class sun.misc.Cleaner { *; }
@@ -0,0 +1,63 @@
package B;
import android.text.SpannableStringBuilder;
import com.ve3nea.morse_expert.DecodedTextView;
import com.ve3nea.morse_expert.MainActivity;
import H2.b;
/**
* 照搬自 Morse Expert 1.15 B.RunnableC0001b, 类名保持混淆原名。
* R8 合并 Runnable 手术: 构造按 smali 真实签名 5 参 (Object,Object,int,int,boolean),
* 字段 = i(Object)/h(Object)/g(int)/f(int); run() 仅保留 case 4(f==2 对应 pswitch_4:
* 解码文本 UI 更新, H2.b 调用), 其余 case(权限/Intent/通知等库代码)已裁剪。
* 注: jadx 4 参构造为错误还原, 已按 smali 修正。
*/
public final class RunnableC0001b implements Runnable {
public final /* synthetic */ int f;
/* renamed from: g, reason: collision with root package name */
public final int g;
/* renamed from: h, reason: collision with root package name */
public final Object h;
/* renamed from: i, reason: collision with root package name */
public final Object i;
public /* synthetic */ RunnableC0001b(Object obj, Object obj2, int i4, int i5, boolean z) {
this.i = obj;
this.h = obj2;
this.g = i4;
this.f = i5;
}
@Override // java.lang.Runnable
public final void run() {
if (this.f == 2) {
MainActivity mainActivity = ((b) this.i).f620p;
if (mainActivity != null) {
SpannableStringBuilder spannableStringBuilder = (SpannableStringBuilder) this.h;
int i5 = this.g;
DecodedTextView decodedTextView = (DecodedTextView) mainActivity.f11035D.f11888g;
if (!decodedTextView.f11032n && !decodedTextView.f11033o.get()) {
decodedTextView.beginBatchEdit();
try {
int length2 = decodedTextView.length();
int min = Math.min(length2, i5);
if (min > 0) {
decodedTextView.getEditableText().delete(length2 - min, length2);
}
decodedTextView.q(spannableStringBuilder);
decodedTextView.endBatchEdit();
return;
} catch (Throwable th) {
decodedTextView.endBatchEdit();
throw th;
}
}
return;
}
return;
}
}
}
+61
View File
@@ -0,0 +1,61 @@
package B0;
import android.widget.TextView;
import com.ve3nea.morse_expert.MainActivity;
import com.ve3nea.morse_expert.ScaleView;
import java.util.Locale;
import d1.AbstractC1518b;
/**
* 照搬自 Morse Expert 1.15 B0.b, 类名保持混淆原名。
* R8 合并 Runnable 手术: 构造按 smali 真实签名 (H2/b, String, [I) -> f=4;
* 只保留 case 4(状态栏更新: dBFS/Hz/dB/WPM + ScaleView 频率标记, H2.b 调用),
* 其余 case(WorkManager/动画等库代码)已裁剪。
* r3 = scaleView.getHeight()(smali 证实, jadx 寄存器未展开)。
*/
public final class b implements Runnable {
public final /* synthetic */ int f;
/* renamed from: g, reason: collision with root package name */
public final Object g;
/* renamed from: h, reason: collision with root package name */
public final Object h;
/* renamed from: i, reason: collision with root package name */
public final Object i;
public b(H2.b bVar, String str, int[] iArr) {
this.f = 4;
this.g = bVar;
this.h = iArr;
this.i = str;
}
@Override // java.lang.Runnable
public final void run() {
if (this.f == 4) {
MainActivity mainActivity = ((H2.b) this.g).f621q;
if (mainActivity != null) {
String str2 = (String) this.i;
int[] iArr = (int[]) this.h;
String concat;
String format = String.format(Locale.US, " %3d dBFS", Integer.valueOf(iArr[0]));
if (iArr[5] == 0) {
concat = format.concat(" Idle ");
} else {
concat = format.concat(String.format(Locale.US, " %4d Hz %2d dB %2d WPM ", Integer.valueOf(iArr[1]), Integer.valueOf(iArr[2]), Integer.valueOf(iArr[3])));
}
((TextView) mainActivity.f11035D.f11890i).setText(AbstractC1518b.e(concat, str2));
ScaleView scaleView = (ScaleView) mainActivity.f11035D.f11889h;
int i4 = iArr[1];
scaleView.f11041g = iArr[5];
// r3 = scaleView.getHeight()(smali 证实)
scaleView.f = (scaleView.getHeight() - 1) - Math.round(((Math.round(i4 * 0.128f) - H2.b.f606z) * scaleView.getHeight()) / H2.b.f603B);
scaleView.invalidate();
return;
}
return;
}
}
}
+69
View File
@@ -0,0 +1,69 @@
package D;
import android.graphics.Bitmap;
import android.opengl.GLES20;
import android.opengl.GLUtils;
import com.ve3nea.morse_expert.WaterfallSurfaceView;
/**
* 照搬自 Morse Expert 1.15, 类名保持混淆原名。
* R8 合并 Runnable 手术: 只保留 case 1(瀑布图频谱->OpenGL 纹理, H2.b 调用),
* 其余 case(库/广告)已裁剪。
*/
public final /* synthetic */ class n implements Runnable {
public final /* synthetic */ int f;
/* renamed from: g, reason: collision with root package name */
public final /* synthetic */ Object f224g;
/* renamed from: h, reason: collision with root package name */
public final /* synthetic */ Object f225h;
public /* synthetic */ n(Object obj, int i4, Object obj2) {
this.f = i4;
this.f224g = obj;
this.f225h = obj2;
}
@Override // java.lang.Runnable
public final void run() {
int i4 = this.f;
Object obj = this.f225h;
Object obj2 = this.f224g;
if (i4 == 1) {
float[] fArr = (float[]) obj;
J2.b bVar = ((WaterfallSurfaceView) obj2).f;
Bitmap bitmap = bVar.f733h;
int width = bVar.f730d.getWidth();
double d4 = width;
double b4 = (bVar.b() + 4.0d) % d4;
double d5 = bVar.f737l - b4;
if (d5 > d4 / 2.0d) {
d5 -= d4;
} else if (d5 < (-width) / 2.0d) {
d5 += d4;
}
if (Math.abs(d5) > 8.0d) {
bVar.f737l = (int) Math.floor(b4);
} else {
bVar.f732g = (d5 * 1.0E-5d) + bVar.f732g;
}
int height = bitmap.getHeight() - 1;
for (int i5 = 0; i5 <= height; i5++) {
J2.a aVar = bVar.f734i;
int round = Math.round((fArr[i5] * 4.0f) + 30.0f);
aVar.getClass();
bitmap.setPixel(0, height - i5, aVar.f727a[Math.max(0, Math.min(255, round))]);
}
int i6 = bVar.f737l;
GLES20.glBindTexture(3553, bVar.f728a[0]);
J2.b.a();
GLUtils.texSubImage2D(3553, 0, i6, 0, bitmap);
J2.b.a();
int i7 = bVar.f737l + 1;
bVar.f737l = i7;
bVar.f737l = i7 % width;
return;
}
}
}
+44
View File
@@ -0,0 +1,44 @@
package E2;
import android.util.Log;
import android.view.MotionEvent;
import android.view.View;
import com.ve3nea.morse_expert.MainActivity;
import com.ve3nea.morse_expert.ScaleView;
/**
* 照搬自 Morse Expert 1.15 (com.ve3nea.morse_expert), 类名保持混淆原名。
* R8 合并类手术: 只保留 case 1(瀑布图触摸选频率=应用逻辑),
* 其余 case(广告 P4 / appcompat 焦点 / 弹窗)为库代码, 已裁剪。
*/
public final class g implements View.OnTouchListener {
public final /* synthetic */ int f;
/* renamed from: g, reason: collision with root package name */
public final /* synthetic */ Object f411g;
public /* synthetic */ g(Object obj, int i4) {
this.f = i4;
this.f411g = obj;
}
@Override // android.view.View.OnTouchListener
public final boolean onTouch(View view, MotionEvent motionEvent) {
int i4 = this.f;
Object obj = this.f411g;
if (i4 == 1) {
MainActivity mainActivity = (MainActivity) obj;
if (motionEvent.getActionMasked() == 0 && mainActivity.f11037F != null) {
// smali 证实: p1 先被重赋值为 f11035D.h(ScaleView)再 getHeight;
// 公式 = round((f606z + ((scaleH-1-y)*f603B/scaleH)) / 0.128f)
ScaleView scaleView = (ScaleView) mainActivity.f11035D.f11889h;
float y3 = motionEvent.getY();
int round = Math.round((H2.b.f606z + ((((scaleView.getHeight() - 1) - y3) * H2.b.f603B) / scaleView.getHeight())) / 0.128f);
mainActivity.f11037F.f622r.set(round);
Log.i("Waterfall", String.format("Touch at %d Hz`", Integer.valueOf(round)));
}
return false;
}
return false;
}
}
+53
View File
@@ -0,0 +1,53 @@
package F2;
import com.rtbishop.look4sat.feature.cw.R;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuItem;
import com.ve3nea.morse_expert.DecodedTextView;
/* loaded from: classes.dex */
public final class a implements ActionMode.Callback {
/* renamed from: a, reason: collision with root package name */
public final /* synthetic */ DecodedTextView f564a;
public a(DecodedTextView decodedTextView) {
this.f564a = decodedTextView;
}
@Override // android.view.ActionMode.Callback
public final boolean onActionItemClicked(ActionMode actionMode, MenuItem menuItem) {
int itemId = menuItem.getItemId();
DecodedTextView decodedTextView = this.f564a;
if (itemId != 73) {
if (itemId != 74) {
return false;
}
decodedTextView.s();
return true;
}
decodedTextView.r();
actionMode.finish();
return true;
}
@Override // android.view.ActionMode.Callback
public final boolean onCreateActionMode(ActionMode actionMode, Menu menu) {
this.f564a.f11032n = true;
// 原: if (menu.findItem(R.id.shareText) == null) 检查(shareText 为原 APK 资源, 简化后始终添加)
menu.add(0, 74, 0, "Share").setIcon(R.drawable.ic_baseline_share_24);
menu.add(0, 73, 0, "Save").setIcon(R.drawable.ic_baseline_save_24);
return true;
}
@Override // android.view.ActionMode.Callback
public final void onDestroyActionMode(ActionMode actionMode) {
this.f564a.f11032n = false;
}
@Override // android.view.ActionMode.Callback
public final boolean onPrepareActionMode(ActionMode actionMode, Menu menu) {
return false;
}
}
+82
View File
@@ -0,0 +1,82 @@
package H2;
import android.media.AudioRecord;
import android.util.Log;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
/* loaded from: classes.dex */
public final class a implements Runnable {
public final int f;
/* renamed from: g, reason: collision with root package name */
public final AudioRecord f596g;
/* renamed from: h, reason: collision with root package name */
public final ExecutorService f597h;
/* renamed from: i, reason: collision with root package name */
public final AtomicBoolean f598i;
/* renamed from: j, reason: collision with root package name */
public final short[] f599j;
/* renamed from: k, reason: collision with root package name */
public final float[] f600k;
/* renamed from: l, reason: collision with root package name */
public b f601l;
public a() {
int minBufferSize = AudioRecord.getMinBufferSize(8000, 16, 2);
int i4 = b.f604C;
this.f = i4;
ExecutorService newSingleThreadExecutor = Executors.newSingleThreadExecutor();
this.f597h = newSingleThreadExecutor;
AtomicBoolean atomicBoolean = new AtomicBoolean(false);
this.f598i = atomicBoolean;
this.f599j = new short[i4];
this.f600k = new float[i4];
AudioRecord audioRecord = new AudioRecord(1, 8000, 16, 2, minBufferSize * 4);
this.f596g = audioRecord;
if (audioRecord.getState() == 1) {
audioRecord.startRecording();
if (audioRecord.getState() != 1) {
Log.e("AudioRecord", "cannot start");
}
atomicBoolean.set(true);
newSingleThreadExecutor.execute(this);
return;
}
Log.e("AudioInput", "Failed to initialize");
}
@Override // java.lang.Runnable
public final void run() {
while (true) {
try {
if (this.f598i.get()) {
float[] fArr = this.f600k;
AudioRecord audioRecord = this.f596g;
short[] sArr = this.f599j;
int i4 = this.f;
if (audioRecord.read(sArr, 0, i4) == i4) {
for (int i5 = 0; i5 < i4; i5++) {
fArr[i5] = sArr[i5] / 32768.0f;
}
b bVar = this.f601l;
if (bVar != null) {
bVar.a(fArr);
}
}
} else {
TimeUnit.MILLISECONDS.sleep(100L);
}
} catch (Exception e4) {
Log.e("Audio thread", "Exception: ", e4);
}
}
}
}
+582
View File
@@ -0,0 +1,582 @@
package H2;
import B.RunnableC0001b;
import D.n;
import android.content.Context;
import android.media.MediaScannerConnection;
import android.os.Handler;
import android.os.Looper;
import android.text.SpannableStringBuilder;
import android.text.format.DateFormat;
import android.text.style.ForegroundColorSpan;
import android.util.Log;
import com.ve3nea.morse_expert.MainActivity;
import com.ve3nea.morse_expert.WaterfallSurfaceView;
import i3.d;
import java.io.BufferedOutputStream;
import java.io.DataOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.Arrays;
import java.util.Date;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
import k3.s;
import pas.decoder;
import pas.nativedecoder;
/* loaded from: classes.dex */
public final class b {
/* renamed from: A, reason: collision with root package name */
public static final int f602A;
/* renamed from: B, reason: collision with root package name */
public static final int f603B;
/* renamed from: C, reason: collision with root package name */
public static final int f604C;
/* renamed from: D, reason: collision with root package name */
public static final d f605D;
/* renamed from: z, reason: collision with root package name */
public static final int f606z;
/* renamed from: a, reason: collision with root package name */
public final float[] f607a;
/* renamed from: b, reason: collision with root package name */
public final float[] f608b;
public final float[] c;
/* renamed from: d, reason: collision with root package name */
public final float[] f609d;
/* renamed from: e, reason: collision with root package name */
public final nativedecoder.TNativeDecoder f610e;
public int f;
/* renamed from: g, reason: collision with root package name */
public int f611g;
/* renamed from: h, reason: collision with root package name */
public int f612h;
/* renamed from: i, reason: collision with root package name */
public decoder.TDecoderState f613i;
/* renamed from: j, reason: collision with root package name */
public String f614j;
/* renamed from: k, reason: collision with root package name */
public final c f615k;
/* renamed from: l, reason: collision with root package name */
public int f616l;
/* renamed from: m, reason: collision with root package name */
public final I2.b f617m;
/* renamed from: n, reason: collision with root package name */
public final Handler f618n;
/* renamed from: o, reason: collision with root package name */
public WaterfallSurfaceView f619o;
/* renamed from: p, reason: collision with root package name */
public MainActivity f620p;
/* renamed from: q, reason: collision with root package name */
public MainActivity f621q;
/* renamed from: r, reason: collision with root package name */
public final AtomicInteger f622r;
/* renamed from: s, reason: collision with root package name */
public final AtomicBoolean f623s;
/* renamed from: t, reason: collision with root package name */
public final Context f624t;
/* renamed from: u, reason: collision with root package name */
public int f625u;
/* renamed from: v, reason: collision with root package name */
public final AtomicBoolean f626v;
/* renamed from: w, reason: collision with root package name */
public DataOutputStream f627w;
/* renamed from: x, reason: collision with root package name */
public int f628x;
/* renamed from: y, reason: collision with root package name */
public String f629y;
/* JADX WARN: Removed duplicated region for block: B:14:0x012d */
/* JADX WARN: Type inference failed for: r0v4, types: [i3.d, java.lang.Object] */
static {
boolean z3;
float f;
float f4;
float f5;
int round = Math.round(25.6f);
f606z = round;
int round2 = Math.round(153.6f);
f602A = round2;
f603B = round2 - round;
f604C = Math.round(85.333336f);
i3.d obj = new i3.d();
if (2048 > 1073741824) {
z3 = true;
} else {
z3 = false;
}
obj.f11709l = z3;
int i4 = (int) 1024;
obj.f11700a = i4;
obj.f11701b = 1024L;
if (!z3) {
obj.f11708k = 1;
int[] iArr = new int[((int) g3.c.a((1 << (((int) (g3.c.d(((float) 1024) + 0.5f) / g3.c.d(2.0d))) / 2)) + 2)) + 2];
obj.c = iArr;
float[] fArr = new float[i4];
obj.f11703e = fArr;
int i5 = (i4 * 2) >> 2;
obj.f11704g = i5;
iArr[0] = i5;
iArr[1] = 1;
if (i5 > 2) {
int i6 = i5 >> 1;
float f6 = 0.7853982f / i6;
float f7 = 2.0f * f6;
float b4 = (float) g3.c.b(i6 * f6); // r9 = i6 (smali)
fArr[0] = 1.0f;
fArr[1] = b4;
if (i6 == 4) {
double d4 = f7;
f4 = 0.5f;
fArr[2] = (float) g3.c.b(d4);
fArr[3] = (float) g3.c.f(d4);
} else {
f4 = 0.5f;
if (i6 > 4) {
iArr[2] = 0;
iArr[3] = 16;
int i7 = i5;
int i8 = 2;
f5 = 1.0f;
while (i7 > 32) {
int i9 = i8 << 1;
int i10 = i8 << 4;
for (int i11 = i8; i11 < i9; i11++) {
int i12 = iArr[i11] << 2;
iArr[i8 + i11] = i12;
iArr[i9 + i11] = i12 + i10;
}
i7 >>= 2;
i8 = i9;
}
fArr[2] = 0.5f / ((float) g3.c.b(f7));
fArr[3] = 0.5f / ((float) g3.c.b(6.0f * f6));
for (int i13 = 4; i13 < i6; i13 += 4) {
float f8 = i13 * f6;
double d5 = f8;
fArr[i13] = (float) g3.c.b(d5);
fArr[i13 + 1] = (float) g3.c.f(d5);
double d6 = 3.0f * f8;
fArr[i13 + 2] = (float) g3.c.b(d6);
fArr[i13 + 3] = -((float) g3.c.f(d6));
}
int i14 = 0;
while (i6 > 2) {
int i15 = i14 + i6;
i6 >>= 1;
fArr[i15] = f5;
fArr[i15 + 1] = b4;
if (i6 == 4) {
float f9 = fArr[i14 + 4];
float f10 = fArr[i14 + 5];
fArr[i15 + 2] = f9;
fArr[i15 + 3] = f10;
} else if (i6 > 4) {
float f11 = fArr[i14 + 4];
float f12 = fArr[i14 + 6];
fArr[i15 + 2] = f4 / f11;
fArr[i15 + 3] = f4 / f12;
for (int i16 = 4; i16 < i6; i16 += 4) {
int i17 = (i16 * 2) + i14;
int i18 = i15 + i16;
float f13 = fArr[i17];
float f14 = fArr[i17 + 1];
float f15 = fArr[i17 + 2];
float f16 = fArr[i17 + 3];
fArr[i18] = f13;
fArr[i18 + 1] = f14;
fArr[i18 + 2] = f15;
fArr[i18 + 3] = f16;
}
}
i14 = i15;
}
}
}
f5 = 1.0f;
int i142 = 0;
while (i6 > 2) {
}
}
int i19 = i4 >> 2;
obj.f11706i = i19;
iArr[1] = i19;
if (i19 > 1) {
int i20 = i19 >> 1;
float f17 = 0.7853982f / i20;
float b5 = (float) g3.c.b(i20 * f17); // r6 = i20 (smali)
fArr[i5] = b5;
fArr[i5 + i20] = b5 * 0.5f;
for (int i21 = 1; i21 < i20; i21++) {
double d7 = i21 * f17;
fArr[i5 + i21] = ((float) g3.c.b(d7)) * 0.5f;
fArr[(i5 + i19) - i21] = ((float) g3.c.f(d7)) * 0.5f;
}
}
} else {
obj.f11708k = 1;
s sVar = new s(((long) g3.c.a((1 << ((int) (((long) (g3.c.d(((float) 1024) + 0.5f) / g3.c.d(2.0d))) / 2))) + 2)) + 2, true);
obj.f11702d = sVar;
k3.d dVar = new k3.d(1024L, true);
obj.f = dVar;
obj.f11705h = 512L;
sVar.c(0L, 512L);
sVar.c(1L, 1L);
if (512 > 2) {
long j4 = 512 >> 1;
float f18 = 0.7853982f / ((float) j4);
float f19 = f18 * 2.0f;
float b6 = (float) g3.c.b(j4 * f18); // r14 = j4 (smali 同模式)
dVar.c(0L, 1.0f);
dVar.c(1L, b6);
if (j4 == 4) {
f = 0.5f;
double d8 = f19;
dVar.c(2L, (float) g3.c.b(d8));
dVar.c(3L, (float) g3.c.f(d8));
} else {
f = 0.5f;
if (j4 > 4) {
sVar.c(2L, 0L);
sVar.c(3L, 16L);
long j5 = 2;
long j6 = 512;
while (j6 > 32) {
long j7 = j5 << 1;
long j8 = j5 << 4;
long j9 = j5;
while (j9 < j7) {
long b7 = sVar.b(j9) << 2;
sVar.c(j5 + j9, b7);
sVar.c(j7 + j9, b7 + j8);
j9++;
f19 = f19;
}
j6 >>= 2;
j5 = j7;
}
dVar.c(2L, 0.5f / ((float) g3.c.b(f19)));
dVar.c(3L, 0.5f / ((float) g3.c.b(6.0f * f18)));
long j10 = 4;
while (j10 < j4) {
float f20 = ((float) j10) * f18;
double d9 = f20;
dVar.c(j10, (float) g3.c.b(d9));
dVar.c(j10 + 1, (float) g3.c.f(d9));
double d10 = 3.0f * f20;
long j11 = j10;
dVar.c(j10 + 2, (float) g3.c.b(d10));
dVar.c(j11 + 3, -((float) g3.c.f(d10)));
j10 = j11 + 4;
}
}
}
long j12 = 0;
long j13 = 2;
while (j4 > j13) {
long j14 = j12 + j4;
j4 >>= 1;
dVar.c(j14, 1.0f);
dVar.c(j14 + 1, b6);
if (j4 == 4) {
float b8 = dVar.b(j12 + 4);
float b9 = dVar.b(j12 + 5);
j13 = 2;
dVar.c(j14 + 2, b8);
dVar.c(j14 + 3, b9);
} else {
j13 = 2;
if (j4 > 4) {
float b10 = dVar.b(j12 + 4);
float b11 = dVar.b(6 + j12);
dVar.c(j14 + 2, f / b10);
dVar.c(j14 + 3, f / b11);
long j15 = 4;
while (j15 < j4) {
long j16 = (j15 * 2) + j12;
float f21 = b6;
long j17 = j14 + j15;
long j18 = j12;
float b12 = dVar.b(j16);
long j19 = j14;
float b13 = dVar.b(j16 + 1);
float b14 = dVar.b(j16 + 2);
float b15 = dVar.b(j16 + 3);
dVar.c(j17, b12);
dVar.c(j17 + 1, b13);
dVar.c(j17 + 2, b14);
dVar.c(j17 + 3, b15);
j15 += 4;
b6 = f21;
j12 = j18;
j14 = j19;
}
}
}
b6 = b6;
j12 = j14;
}
}
long j20 = 256;
obj.f11707j = 256L;
long j21 = 1;
sVar.c(1L, 256L);
if (256 > 1) {
long j22 = 256 >> 1;
float f22 = 0.7853982f / ((float) j22);
long j23 = 512;
dVar.c(512L, (float) g3.c.b(j22 * f22)); // r1 = j22 (smali)
dVar.c(512 + j22, dVar.b(512L) * 0.5f);
long j24 = 1;
while (j24 < j22) {
long j25 = j20;
long j26 = j21;
double d11 = ((float) j24) * f22;
long j27 = j23;
dVar.c(j23 + j24, ((float) g3.c.b(d11)) * 0.5f);
dVar.c((j27 + j25) - j24, ((float) g3.c.f(d11)) * 0.5f);
j24 += j26;
j20 = j25;
j21 = j26;
j23 = j27;
}
}
}
f605D = obj;
}
public b(Context context) {
float[] fArr = new float[1024];
fArr[512] = 1.0f;
for (int i4 = 1; i4 < 512; i4++) {
double d4 = (i4 * 6.283185307179586d) / 1024;
float cos = (float) ((Math.cos(d4 * 2.0d) * 0.08d) + (0.42d - (Math.cos(d4) * 0.5d)));
fArr[i4] = cos;
double d5 = 0.003125f * 6.283185307179586d * (i4 - 512);
float sin = (float) ((Math.sin(d5) / d5) * cos);
fArr[i4] = sin;
fArr[1024 - i4] = sin;
}
this.f607a = fArr;
this.f608b = new float[1024];
this.c = new float[1024];
this.f609d = new float[f603B];
this.f610e = nativedecoder.TNativeDecoder.Create();
this.f614j = "";
this.f615k = new c(4000, 0);
I2.b bVar = new I2.b();
this.f617m = bVar;
this.f618n = new Handler(Looper.getMainLooper());
this.f622r = new AtomicInteger();
this.f623s = new AtomicBoolean(false);
this.f625u = 0;
this.f626v = new AtomicBoolean(false);
this.f628x = 0;
this.f624t = context;
bVar.b(context);
}
/* JADX WARN: Removed duplicated region for block: B:57:0x01e2 */
/* JADX WARN: Removed duplicated region for block: B:63:0x020a A[LOOP:6: B:62:0x0208->B:63:0x020a, LOOP_END] */
/* JADX WARN: Removed duplicated region for block: B:66:0x0216 A[LOOP:7: B:65:0x0214->B:66:0x0216, LOOP_END] */
/* JADX WARN: Removed duplicated region for block: B:70:0x0232 */
/* JADX WARN: Removed duplicated region for block: B:73:? A[RETURN, SYNTHETIC] */
/*
Code decompiled incorrectly, please refer to instructions dump.
*/
public final void a(float[] fArr) {
c cVar;
float[] fArr2;
float[] fArr3;
b bVar;
Exception exc;
int i4;
float f;
int i5;
WaterfallSurfaceView waterfallSurfaceView;
int length = fArr.length;
int i6 = 0;
while (true) {
cVar = this.f615k;
if (i6 >= length) {
break;
}
cVar.c(Math.abs(fArr[i6]));
i6++;
}
this.f616l = Math.round(((float) Math.log10(Math.max(-0.5d, cVar.f633e))) * 20.0f);
int i7 = f604C;
int i8 = 1024 - i7;
float[] fArr4 = this.f608b;
System.arraycopy(fArr4, i7, fArr4, 0, i8);
System.arraycopy(fArr, 0, fArr4, i8, i7);
int i9 = 0;
while (true) {
fArr2 = this.c;
if (i9 >= 1024) {
break;
}
fArr2[i9] = fArr4[i9] * this.f607a[i9];
i9++;
}
f605D.k(fArr2, 0);
int i10 = f606z;
int i11 = i10;
while (true) {
int i12 = f602A;
float f4 = 0.0f;
fArr3 = this.f609d;
if (i11 < i12) {
int i13 = i11 * 2;
float f5 = fArr2[i13];
float f6 = fArr2[i13 + 1];
float f7 = ((f6 * f6) + (f5 * f5)) * 0.001f;
if (!Float.isNaN(f7)) {
f4 = f7;
}
fArr3[i11 - i10] = f4;
i11++;
} else {
// smali: 循环内无 try; 录音写入 try 在循环后(见下)
break;
}
}
DataOutputStream dataOutputStream = this.f627w;
Context context = this.f624t;
AtomicBoolean atomicBoolean = this.f626v;
try { // smali try_start_0: 录音写入整块(原 jadx 误把 try 放 break 处)
if (dataOutputStream == null && atomicBoolean.get()) {
File file = new File(context.getExternalFilesDir(null), ((String) DateFormat.format("yyyy-MM-dd_HH_mm_ss", new Date())) + "_spectra.bin");
this.f629y = file.getAbsoluteFile().toString();
this.f627w = new DataOutputStream(new BufferedOutputStream(new FileOutputStream(file, false)));
this.f628x = 0;
this.f614j = " Recording...";
}
if (this.f627w != null) {
for (float f8 : fArr3) {
this.f627w.writeFloat(f8);
}
}
if (atomicBoolean.get()) {
int i14 = this.f628x + 1;
this.f628x = i14;
if (i14 >= 2048) {
atomicBoolean.set(false);
this.f627w.close();
this.f627w = null;
this.f614j = "";
MediaScannerConnection.scanFile(context, new String[]{this.f629y}, new String[]{"application/x-binary"}, null);
}
}
} catch (IOException e4) {
Log.e("File write", "Exception: ", e4);
}
nativedecoder.TNativeDecoder tNativeDecoder = this.f610e;
try {
this.f613i = tNativeDecoder.ProcessSpectrum(fArr3);
int i15 = this.f625u + 1;
this.f625u = i15;
Handler handler = this.f618n;
if (i15 == 15) {
try {
this.f625u = 0;
this.f = tNativeDecoder.getPitch();
this.f611g = tNativeDecoder.getSnr();
this.f612h = tNativeDecoder.getWpm();
handler.post(new B0.b(this, this.f614j, new int[]{this.f616l, this.f, this.f611g, this.f612h, tNativeDecoder.getSignalCount(), this.f613i.Value}));
AtomicInteger atomicInteger = this.f622r;
int i16 = atomicInteger.get();
if (i16 != 0) {
tNativeDecoder.LockToPitch(i16);
atomicInteger.set(0);
}
tNativeDecoder.setHamMode(this.f623s.get());
} catch (Exception e5) {
// jadx 幻觉区(空循环/空 if 为 catch 错位还原)已裁剪; 原 smali: 记录异常后跳回正常流程尾部
exc = e5;
bVar = this;
Log.e("Audio Processor", "Exception", exc);
}
}
int charCount = tNativeDecoder.getCharCount();
if (charCount > 0) {
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder();
int i18 = 0;
while (i18 < charCount) {
decoder.TCharInfo GetChar = tNativeDecoder.GetChar(i18);
spannableStringBuilder.append(GetChar.getChar());
ForegroundColorSpan foregroundColorSpan = new ForegroundColorSpan(this.f617m.a(GetChar));
int i19 = i18 + 1;
spannableStringBuilder.setSpan(foregroundColorSpan, i18, i19, 33);
i18 = i19;
}
bVar = this;
try {
handler.post(new RunnableC0001b(bVar, spannableStringBuilder, tNativeDecoder.getDeleteCount(), 2, false));
} catch (Exception e6) {
// jadx 幻觉区已裁剪(同 catch e5); 原 smali: 记录异常后跳回正常流程尾部
exc = e6;
Log.e("Audio Processor", "Exception", exc);
}
} else {
bVar = this;
}
} catch (Exception e7) {
// jadx 幻觉(e 变量)已裁剪
bVar = this;
}
i4 = f603B;
int i1722 = i4 / 2;
float[] copyOfRange22 = Arrays.copyOfRange(fArr3, i1722 - 20, i1722 + 20);
Arrays.sort(copyOfRange22);
f = copyOfRange22[20];
if (f > 0.0f) {
for (int i20 = 0; i20 < i4; i20++) {
fArr3[i20] = fArr3[i20] / f;
}
}
float[] fArr522 = (float[]) fArr3.clone();
new c(12, 1).a(fArr522);
new c(12, 0).a(fArr522);
for (i5 = 0; i5 < i4; i5++) {
fArr3[i5] = fArr3[i5] - fArr522[i5];
}
for (int i21 = 0; i21 < i4; i21++) {
fArr3[i21] = ((float) Math.log10(Math.max(-0.5d, fArr3[i21]) + 1.0d)) * 10.0f;
}
waterfallSurfaceView = bVar.f619o;
if (waterfallSurfaceView != null) {
waterfallSurfaceView.queueEvent(new n(waterfallSurfaceView, 1, (float[]) fArr3.clone()));
}
}
}
+89
View File
@@ -0,0 +1,89 @@
package H2;
/* loaded from: classes.dex */
public final class c {
/* renamed from: a, reason: collision with root package name */
public final int f630a;
/* renamed from: b, reason: collision with root package name */
public float[] f631b;
public int c;
/* renamed from: d, reason: collision with root package name */
public float f632d;
/* renamed from: e, reason: collision with root package name */
public float f633e;
public final /* synthetic */ int f;
public c(int i4, int i5) {
this.f = i5;
this.f630a = i4 - 1;
}
public final void a(float[] fArr) {
int i4 = this.f630a / 2;
int length = fArr.length;
if (length > i4) {
this.f631b = null;
for (int i5 = 0; i5 < i4; i5++) {
c(fArr[i5]);
}
for (int i6 = i4; i6 < length; i6++) {
fArr[i6 - i4] = c(fArr[i6]);
}
for (int i7 = length; i7 < length + i4; i7++) {
fArr[i7 - i4] = c(fArr[length - 1]);
}
return;
}
throw new IllegalArgumentException("Sliding Filter error: input too short");
}
public final float b(float f, float f4) {
switch (this.f) {
case 0:
return Math.max(f, f4);
default:
return Math.min(f, f4);
}
}
public final float c(float f) {
float[] fArr = this.f631b;
int i4 = this.f630a;
if (fArr == null) {
this.f631b = new float[i4];
for (int i5 = 0; i5 < i4; i5++) {
this.f631b[i5] = f;
}
this.f632d = f;
return f;
}
float b4 = b(this.f632d, f);
this.f632d = b4;
this.f633e = b(this.f631b[this.c], b4);
float[] fArr2 = this.f631b;
int i6 = this.c;
int i7 = i6 + 1;
this.c = i7;
fArr2[i6] = f;
if (i7 == i4) {
int length = fArr2.length;
while (true) {
this.c = length - 1;
int i8 = this.c;
if (i8 <= 0) {
break;
}
float[] fArr3 = this.f631b;
fArr3[i8 - 1] = b(fArr3[i8 - 1], fArr3[i8]);
length = this.c;
}
this.c = 0;
this.f632d = f;
}
return this.f633e;
}
}
+42
View File
@@ -0,0 +1,42 @@
package I0;
/**
* 照搬自 Morse Expert 1.15 I0.d, 类名保持混淆原名。
* R8 合并类手术: 只保留 (i3.d, int, int, float[]) FFT 任务构造;
* run() 按 smali 原样还原: R8 已将任务体优化为死代码(aget 后 throw null,
* 永不执行), 保留原语义(行为与原 APK 完全一致)。
* SystemForegroundService(work 库)case 已裁剪。
*/
public final class d implements Runnable {
public final /* synthetic */ int f = 0;
/* renamed from: g, reason: collision with root package name */
public final /* synthetic */ int f653g;
/* renamed from: h, reason: collision with root package name */
public final /* synthetic */ int f654h;
/* renamed from: i, reason: collision with root package name */
public final /* synthetic */ Object f655i;
/* renamed from: j, reason: collision with root package name */
public final /* synthetic */ Object f656j;
public d(i3.d dVar, int i4, int i5, float[] fArr) {
this.f656j = dVar;
this.f653g = i4;
this.f654h = i5;
this.f655i = fArr;
}
@Override // java.lang.Runnable
public final void run() {
int i6 = this.f654h;
int i7 = this.f653g;
if (i7 >= i6) {
return;
}
float f = ((float[]) this.f655i)[i7 * 2];
// R8 原代码: aget 后 throw null(死代码, 永不执行) —— 照搬语义, 不执行任何计算
}
}
+73
View File
@@ -0,0 +1,73 @@
package I2;
import android.content.Context;
import android.content.SharedPreferences;
import pas.decoder;
/* loaded from: classes.dex */
public final class b {
/* renamed from: b, reason: collision with root package name */
public static final String[] f663b = {"bg_color", "text_color", "text_color_weak", "call_color", "call_color_weak", "cq_color", "cq_color_weak", "rst_color", "rst_color_weak"};
public static final String[] c = {"Background color", "Text color", "Text color, weak", "Callsign color", "Callsign color, weak", "CQ color", "CQ color, weak", "RST color", "RST color, weak"};
/* renamed from: d, reason: collision with root package name */
public static final int[] f664d = {-3084048, -16777216, -5592406, -65536, -30584, -16776961, -7829249, -65281, -30465};
/* renamed from: a, reason: collision with root package name */
public final int[] f665a = (int[]) f664d.clone();
public final int a(decoder.TCharInfo tCharInfo) {
boolean lowSnr = tCharInfo.getLowSnr();
int[] iArr = this.f665a;
if (lowSnr) {
int i4 = tCharInfo.getWordType().Value;
if (i4 != 0) {
if (i4 != 1) {
if (i4 != 2) {
if (i4 != 3) {
if (i4 != 4) {
if (i4 == 5) {
return iArr[4];
}
return -16777216;
}
return iArr[2];
}
return iArr[6];
}
return iArr[8];
}
return iArr[2];
}
return iArr[2];
}
int i5 = tCharInfo.getWordType().Value;
if (i5 != 0) {
if (i5 != 1) {
if (i5 != 2) {
if (i5 != 3) {
if (i5 != 4) {
if (i5 != 5) {
return -16777216;
}
return iArr[3];
}
return iArr[1];
}
return iArr[5];
}
return iArr[7];
}
return iArr[1];
}
return iArr[1];
}
public final void b(Context context) {
SharedPreferences sharedPreferences = context.getSharedPreferences(context.getPackageName() + "_preferences", 0); // 原 c0.x.a(context)
for (int i4 = 0; i4 < 9; i4++) {
this.f665a[i4] = sharedPreferences.getInt(f663b[i4], f664d[i4]);
}
}
}
+45
View File
@@ -0,0 +1,45 @@
package J2;
/* loaded from: classes.dex */
public final class a {
/* renamed from: b, reason: collision with root package name */
public static final int[] f726b = {16, 17749, 65535, 16776960, 16711680};
/* renamed from: a, reason: collision with root package name */
public final int[] f727a;
public a() {
int[] iArr = new int[256];
this.f727a = iArr;
int i4 = 4;
float f = 255.0f / 4;
int[] iArr2 = f726b;
int i5 = 0;
int i6 = -16777216;
iArr[0] = iArr2[0] | (-16777216);
int i7 = 1;
while (i7 <= i4) {
int round = Math.round(i7 * f);
iArr[round] = iArr2[i7] | i6;
int i8 = round - i5;
int i9 = iArr[i5];
int i10 = (i9 >> 16) & 255;
int i11 = (i9 >> 8) & 255;
int i12 = i9 & 255;
float f4 = i8;
// r10 = iArr2[i7] (下一个锚点色, smali: aget v10, v4, v8)
float f5 = (((iArr2[i7] >> 16) & 255) - i10) / f4;
float f6 = (((iArr2[i7] >> 8) & 255) - i11) / f4;
float f7 = ((iArr2[i7] & 255) - i12) / f4;
for (int i13 = 1; i13 < i8; i13++) {
float f8 = i13;
iArr[i5 + i13] = ((Math.round(f5 * f8) + i10) << 16) | (-16777216) | ((Math.round(f6 * f8) + i11) << 8) | (Math.round(f8 * f7) + i12);
}
i7++;
i5 = round;
i4 = 4;
i6 = -16777216;
}
}
}
+183
View File
@@ -0,0 +1,183 @@
package J2;
import android.content.Context;
import android.graphics.Bitmap;
import android.opengl.GLES20;
import android.opengl.GLSurfaceView;
import android.opengl.GLUtils;
import android.os.SystemClock;
import android.util.Log;
import java.nio.Buffer;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.FloatBuffer;
import javax.microedition.khronos.egl.EGLConfig;
import javax.microedition.khronos.opengles.GL10;
/* loaded from: classes.dex */
public final class b implements GLSurfaceView.Renderer {
/* renamed from: b, reason: collision with root package name */
public FloatBuffer f729b;
public int c;
/* renamed from: d, reason: collision with root package name */
public final Bitmap f730d;
public final Context f;
/* renamed from: g, reason: collision with root package name */
public double f732g;
/* renamed from: h, reason: collision with root package name */
public final Bitmap f733h;
/* renamed from: i, reason: collision with root package name */
public final a f734i;
/* renamed from: j, reason: collision with root package name */
public long f735j;
/* renamed from: k, reason: collision with root package name */
public double f736k;
/* renamed from: l, reason: collision with root package name */
public int f737l;
/* renamed from: a, reason: collision with root package name */
public final int[] f728a = new int[1];
/* renamed from: e, reason: collision with root package name */
public final float[] f731e = {-1.0f, -1.0f, 0.0f, 1.0f, -1.0f, 1.0f, 0.0f, 0.0f, 1.0f, -1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 1.0f, 0.0f};
public b(Context context, int i4) {
Bitmap.Config config = Bitmap.Config.ARGB_8888;
Bitmap createBitmap = Bitmap.createBitmap(2048, i4, config);
this.f730d = createBitmap;
createBitmap.eraseColor(16);
this.f732g = 0.09375d;
this.f734i = new a();
this.f735j = 0L;
this.f736k = 0.0d;
this.f737l = -1;
this.f = context;
this.f733h = Bitmap.createBitmap(1, i4, config);
}
public static void a() {
int glGetError = GLES20.glGetError();
if (glGetError != 0) {
StackTraceElement stackTraceElement = Thread.currentThread().getStackTrace()[3];
Log.e(stackTraceElement.getFileName() + ":" + stackTraceElement.getLineNumber(), ", GLES20 error: " + glGetError);
}
}
public final double b() {
long uptimeMillis = SystemClock.uptimeMillis();
double width = (((uptimeMillis - this.f735j) * this.f732g) + this.f736k) % this.f730d.getWidth();
this.f735j = uptimeMillis;
this.f736k = width;
return width;
}
public final void c(GL10 gl10) {
GLES20.glUseProgram(this.c);
a();
GLES20.glActiveTexture(33984);
a();
GLES20.glBindTexture(3553, this.f728a[0]);
a();
GLES20.glUniform1i(GLES20.glGetUniformLocation(this.c, "uTexture"), 0);
a();
int glGetAttribLocation = GLES20.glGetAttribLocation(this.c, "aPosition");
a();
int glGetAttribLocation2 = GLES20.glGetAttribLocation(this.c, "aTexPos");
a();
this.f729b.position(0);
a();
GLES20.glVertexAttribPointer(glGetAttribLocation, 2, 5126, false, 16, (Buffer) this.f729b);
a();
GLES20.glEnableVertexAttribArray(glGetAttribLocation);
a();
this.f729b.position(2);
a();
GLES20.glVertexAttribPointer(glGetAttribLocation2, 2, 5126, false, 16, (Buffer) this.f729b);
a();
GLES20.glEnableVertexAttribArray(glGetAttribLocation2);
a();
GLES20.glDrawArrays(5, 0, 4);
a();
}
public final void d(GL10 gl10, int i4, int i5) {
GLES20.glViewport(0, 0, i4, i5);
a();
}
@Override // android.opengl.GLSurfaceView.Renderer
public final void onDrawFrame(GL10 gl10) {
float b4 = (float) (b() / this.f730d.getWidth());
float f = 0.5f + b4;
float[] fArr = this.f731e;
fArr[6] = f;
fArr[2] = f;
float f4 = b4 + 1.0f;
fArr[14] = f4;
fArr[10] = f4;
this.f729b.position(0);
this.f729b.put(this.f731e);
c(gl10);
}
@Override // android.opengl.GLSurfaceView.Renderer
public final void onSurfaceChanged(GL10 gl10, int i4, int i5) {
d(gl10, i4, i5);
float width = 1.0f - (((this.f730d.getWidth() * 0.7f) / (i4 / this.f.getResources().getDisplayMetrics().density)) * 2.0f);
float[] fArr = this.f731e;
fArr[4] = width;
fArr[0] = width;
this.f729b.position(0);
this.f729b.put(this.f731e);
}
@Override // android.opengl.GLSurfaceView.Renderer
public final void onSurfaceCreated(GL10 gl10, EGLConfig eGLConfig) {
int glCreateShader = GLES20.glCreateShader(35633);
a();
GLES20.glShaderSource(glCreateShader, "attribute vec2 aPosition;\nattribute vec2 aTexPos;\nvarying vec2 vTexPos;\nvoid main() {\n vTexPos = aTexPos;\n gl_Position = vec4(aPosition.xy, 0.0, 1.0);\n}");
a();
GLES20.glCompileShader(glCreateShader);
a();
int glCreateShader2 = GLES20.glCreateShader(35632);
a();
GLES20.glShaderSource(glCreateShader2, "precision mediump float;\nuniform sampler2D uTexture;\nvarying vec2 vTexPos;\nvoid main(void)\n{\n gl_FragColor = texture2D(uTexture, vTexPos);\n}");
a();
GLES20.glCompileShader(glCreateShader2);
a();
this.c = GLES20.glCreateProgram();
a();
GLES20.glAttachShader(this.c, glCreateShader);
a();
GLES20.glAttachShader(this.c, glCreateShader2);
a();
GLES20.glLinkProgram(this.c);
a();
int[] iArr = this.f728a;
GLES20.glGenTextures(1, iArr, 0);
a();
GLES20.glBindTexture(3553, iArr[0]);
a();
GLES20.glTexParameteri(3553, 10240, 9729);
GLES20.glTexParameteri(3553, 10242, 10497);
GLES20.glTexParameteri(3553, 10241, 9729);
GLES20.glTexParameteri(3553, 10243, 10497);
a();
GLUtils.texImage2D(3553, 0, this.f730d, 0);
a();
float[] fArr = this.f731e;
ByteBuffer allocateDirect = ByteBuffer.allocateDirect(fArr.length * 4);
allocateDirect.order(ByteOrder.nativeOrder());
FloatBuffer asFloatBuffer = allocateDirect.asFloatBuffer();
this.f729b = asFloatBuffer;
asFloatBuffer.put(fArr);
}
}
+10
View File
@@ -0,0 +1,10 @@
package K1;
/**
* 照搬自 Morse Expert 1.15 K1.a 精简版: 仅保留 k3 FFT 库引用的内存统计静态字段
* (K1.a.c 长整型记账)。原 K1.a(828 行)的其余方法为状态栏/库代码, 已裁剪
* (状态栏逻辑已在 MainActivity 内联)。
*/
public class a {
public static long c = 0;
}
@@ -0,0 +1,213 @@
/*
* CwDecodeScreen.kt — Look4Sat 的 CW 解码页面(Compose 外壳)。
*
* 照搬自 Morse Expert 1.15 的 com.ve3nea.morse_expert.MainActivity(已改为普通控制器类):
* - AndroidView 嵌入原版 activity_main.xml(ConstraintLayout 根, 含 decodedTextView/scaleView/
* statusTextView/verticalLayout/waterfallView);
* - 生命周期 onCreate/onResume/onPause/onDestroy 全部委托给控制器, 顺序与原 Activity 一致
* (onCreate 必须在 onResume 前, onDispose 先 onPause 再 onDestroy);
* - 麦克风权限(RECORD_AUDIO)由 Compose 侧管理, 授予后调用 onPermissionGranted()(= v()),
* 用 initialized 标记保证只调一次(v() 会 new a() 覆盖 f11036E);
* - 原 options_menu 的 清除/暂停/保存/录音/设置 改为顶部一行按钮(图标用模块内原版 drawable)。
*
* 注意: 控制器 onCreate 内的 setImmersive(window, false) 会把宿主窗口恢复为
* decorFitsSystemWindows(true)(原版行为照搬, 会对整个宿主 Activity 生效)。
*/
package com.rtbishop.look4sat.feature.cw
import android.Manifest
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.net.Uri
import android.provider.Settings
import android.view.LayoutInflater
import androidx.activity.compose.BackHandler
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.material3.Button
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.constraintlayout.widget.ConstraintLayout
import com.ve3nea.morse_expert.MainActivity
@Composable
fun CwDecodeScreen(navigateUp: () -> Unit = {}) {
val context = LocalContext.current
val activity = remember(context) {
context as? Activity ?: error("CwDecodeScreen must be hosted in an Activity")
}
// 照搬的控制器(普通类, 非 Activity); 每次进入页面新建实例
val controller = remember { MainActivity() }
// 提前 inflate 原版布局, 供 AndroidView 与控制器 onCreate 共用同一根视图
val rootView = remember(context) {
LayoutInflater.from(context).inflate(R.layout.activity_main, null) as ConstraintLayout
}
var permissionGranted by remember {
mutableStateOf(
context.checkSelfPermission(Manifest.permission.RECORD_AUDIO) ==
PackageManager.PERMISSION_GRANTED
)
}
var permanentlyDenied by remember { mutableStateOf(false) }
// v() 会 new a() 覆盖 f11036E, 必须保证只初始化一次
var initialized by remember { mutableStateOf(false) }
var showSettings by remember { mutableStateOf(false) }
// 生命周期: onCreate 必须在 onResume 前; onDispose 先 onPause 再 onDestroy
DisposableEffect(Unit) {
controller.onCreate(activity, rootView)
controller.onResume()
onDispose {
controller.onPause()
controller.onDestroy()
}
}
// 权限授予后启动解码核心; "首次进入已授权" 与 "弹窗回调授权" 两条路径统一走这里
LaunchedEffect(permissionGranted) {
if (permissionGranted && !initialized) {
controller.onPermissionGranted() // = v(): 创建音频采集 + 解码核心
// v() 只创建核心; 页面此刻已处于 resumed, 需再走一次 onResume 立即启动录音
// (AudioRecord 是新建的, startRecording 不会重复; GLSurfaceView.onResume 幂等)
controller.onResume()
initialized = true
}
}
val permissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestPermission()
) { granted ->
permissionGranted = granted
permanentlyDenied = !granted &&
!activity.shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)
}
// 原版 tap_back_again_to_close: 单击返回只提示, 2 秒内再按才退出
BackHandler {
if (!controller.handleBackPress()) navigateUp()
}
if (showSettings) {
CwSettingsDialog(controller = controller, onDismiss = { showSettings = false })
}
Column(
modifier = Modifier
.fillMaxSize()
.systemBarsPadding()
) {
// 原 options_menu: 暂停/清除/保存(原版图标) + 录音/设置(文字按钮)
Row(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.SpaceEvenly
) {
IconButton(onClick = { controller.togglePause() }) {
Icon(
painter = painterResource(R.drawable.ic_baseline_pause_24),
contentDescription = stringResource(R.string.pause)
)
}
IconButton(onClick = { controller.clearDecoded() }) {
Icon(
painter = painterResource(R.drawable.ic_baseline_delete_24),
contentDescription = stringResource(R.string.clear)
)
}
IconButton(onClick = { controller.saveText() }) {
Icon(
painter = painterResource(R.drawable.ic_baseline_save_24),
contentDescription = stringResource(R.string.save_text)
)
}
// recordSignals 需要解码核心(f11037F), 未初始化时禁用
TextButton(
onClick = { if (initialized) controller.recordSignals() },
enabled = initialized
) {
Text(stringResource(R.string.record_signals))
}
TextButton(onClick = { showSettings = true }) {
Text(stringResource(R.string.settings))
}
}
Box(modifier = Modifier.weight(1f).fillMaxWidth()) {
AndroidView(
factory = { rootView },
modifier = Modifier.fillMaxSize()
)
if (!permissionGranted) {
// 未授予麦克风权限: 遮罩 + 提示文字 + 请求按钮
Box(
modifier = Modifier
.fillMaxSize()
.background(Color(0x99000000)),
contentAlignment = Alignment.Center
) {
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(
text = stringResource(R.string.cw_mic_permission),
color = Color.White,
textAlign = TextAlign.Center,
modifier = Modifier.padding(horizontal = 24.dp)
)
Spacer(modifier = Modifier.height(12.dp))
Button(onClick = {
permissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
}) {
Text(stringResource(R.string.cw_grant_permission))
}
if (permanentlyDenied) {
// 勾选了"不再询问": 引导去系统设置开启
TextButton(onClick = {
activity.startActivity(
Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.parse("package:${activity.packageName}")
)
)
}) {
Text(stringResource(R.string.cw_open_settings))
}
}
}
}
}
}
}
}
@@ -0,0 +1,218 @@
/*
* Look4Sat. Amateur radio satellite tracker and pass predictor.
* Copyright (C) 2019-2026 Arty Bishop and contributors.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*/
package com.rtbishop.look4sat.feature.cw
import android.content.Context
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Slider
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.ve3nea.morse_expert.MainActivity
import kotlin.math.roundToInt
/** 键名/默认值照搬原 Morse Expert(root_preferences.xml + SettingsActivity 逻辑)。 */
private const val KEY_MESSAGE_TYPE = "message_type"
private const val KEY_TEXT_FONT_SIZE = "text_font_size"
private const val DEFAULT_MESSAGE_TYPE = "general_text"
private const val DEFAULT_TEXT_FONT_SIZE = 18
private const val MIN_FONT_SIZE = 7
private const val MAX_FONT_SIZE = 99
/** 预置色板(9 色, 3x3 网格);原 ColorPreferenceCompat 用第三方 colorpicker, 不引入。 */
private val PaletteColors = listOf(
0xFFD0F0F0.toInt(), 0xFF000000.toInt(), 0xFFFFFFFF.toInt(),
0xFFAAAAAA.toInt(), 0xFFFF0000.toInt(), 0xFF00FF00.toInt(),
0xFF0000FF.toInt(), 0xFFFFFF00.toInt(), 0xFFFF00FF.toInt(),
)
/**
* CW 设置弹窗(替代原 Morse Expert 的 SettingsActivity)。
* 设置项与键名照搬原 APK: message_type / text_font_size / 9 个颜色
* (I2.b.f663b 键名, I2.b.f664d 默认值, I2.b.c 英文标题)。
* 存储: SharedPreferences(getPackageName() + "_preferences")。
* 点击 OK 保存并调用 controller.onResume() 立即生效; Cancel/点外部仅关闭不保存。
*/
@Composable
fun CwSettingsDialog(controller: MainActivity, onDismiss: () -> Unit) {
val activity = controller.mActivity ?: return
val prefs = remember(activity) {
activity.getSharedPreferences(activity.packageName + "_preferences", Context.MODE_PRIVATE)
}
var messageType by remember {
mutableStateOf(prefs.getString(KEY_MESSAGE_TYPE, DEFAULT_MESSAGE_TYPE) ?: DEFAULT_MESSAGE_TYPE)
}
var fontSize by remember {
mutableStateOf(
prefs.getInt(KEY_TEXT_FONT_SIZE, DEFAULT_TEXT_FONT_SIZE)
.toFloat().coerceIn(MIN_FONT_SIZE.toFloat(), MAX_FONT_SIZE.toFloat())
)
}
var colorValues by remember {
mutableStateOf(IntArray(9) { i -> prefs.getInt(I2.b.f663b[i], I2.b.f664d[i]) })
}
AlertDialog(
onDismissRequest = onDismiss,
title = { Text("CW Settings") },
text = {
Column(
modifier = Modifier
.verticalScroll(rememberScrollState())
.padding(top = 4.dp)
) {
Text("Message type", style = MaterialTheme.typography.titleSmall)
MessageTypeRow("general_text", "General Text", messageType) { messageType = it }
MessageTypeRow("ham_radio_qso", "Ham Radio QSO", messageType) { messageType = it }
HorizontalDivider(Modifier.padding(vertical = 8.dp))
Text("Text font size", style = MaterialTheme.typography.titleSmall)
Row(verticalAlignment = Alignment.CenterVertically) {
Slider(
value = fontSize,
onValueChange = { fontSize = it },
valueRange = MIN_FONT_SIZE.toFloat()..MAX_FONT_SIZE.toFloat(),
steps = MAX_FONT_SIZE - MIN_FONT_SIZE - 1,
modifier = Modifier.weight(1f),
)
Text(
text = "${fontSize.roundToInt()}",
style = MaterialTheme.typography.titleMedium,
textAlign = TextAlign.End,
modifier = Modifier.width(44.dp),
)
}
HorizontalDivider(Modifier.padding(vertical = 8.dp))
Text("Colors", style = MaterialTheme.typography.titleSmall)
for (i in 0 until 9) {
ColorSettingRow(
title = I2.b.c[i],
value = colorValues[i],
onSelect = { selected ->
colorValues = colorValues.copyOf().also { it[i] = selected }
},
)
}
}
},
confirmButton = {
TextButton(onClick = {
val editor = prefs.edit()
editor.putString(KEY_MESSAGE_TYPE, messageType)
editor.putInt(KEY_TEXT_FONT_SIZE, fontSize.roundToInt())
for (i in 0 until 9) {
editor.putInt(I2.b.f663b[i], colorValues[i])
}
editor.apply()
// 原 SettingsActivity 返回时由 MainActivity.onResume() 重新读取全部设置; 照搬逻辑已就位
controller.onResume()
onDismiss()
}) { Text("OK") }
},
dismissButton = {
TextButton(onClick = onDismiss) { Text("Cancel") }
},
)
}
@Composable
private fun MessageTypeRow(option: String, label: String, selected: String, onSelect: (String) -> Unit) {
Row(
modifier = Modifier
.fillMaxWidth()
.clip(RoundedCornerShape(6.dp))
.clickable { onSelect(option) }
.padding(vertical = 2.dp),
verticalAlignment = Alignment.CenterVertically,
) {
RadioButton(selected = option == selected, onClick = { onSelect(option) })
Text(label, style = MaterialTheme.typography.bodyMedium)
}
}
@Composable
private fun ColorSettingRow(title: String, value: Int, onSelect: (Int) -> Unit) {
Column(Modifier.fillMaxWidth().padding(vertical = 6.dp)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(title, style = MaterialTheme.typography.bodyMedium, modifier = Modifier.weight(1f))
Box(
Modifier
.size(20.dp)
.clip(RoundedCornerShape(4.dp))
.background(Color(value)),
)
Spacer(Modifier.width(6.dp))
Text(
text = String.format("#%06X", value and 0xFFFFFF),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
Spacer(Modifier.height(6.dp))
PaletteGrid(selected = value, onSelect = onSelect)
}
}
@Composable
private fun PaletteGrid(selected: Int, onSelect: (Int) -> Unit) {
Column(verticalArrangement = Arrangement.spacedBy(6.dp)) {
PaletteColors.chunked(3).forEach { rowColors ->
Row(horizontalArrangement = Arrangement.spacedBy(6.dp)) {
rowColors.forEach { color ->
val isSelected = color == selected
Box(
modifier = Modifier
.size(36.dp)
.border(
width = if (isSelected) 3.dp else 1.dp,
color = if (isSelected) MaterialTheme.colorScheme.primary
else MaterialTheme.colorScheme.outlineVariant,
shape = RoundedCornerShape(6.dp),
)
.clip(RoundedCornerShape(6.dp))
.background(Color(color))
.clickable { onSelect(color) },
)
}
}
}
}
}
@@ -0,0 +1,14 @@
package com.rtbishop.look4sat.feature.cw.suncompat;
/**
* 编译期 stub:Android 无 sun.misc.Cleaner(照搬 Morse Expert 1.15 k3 FFT 库的清理回调,
* 实际 FFT 1024 点不触发 native 路径)。
*/
public class Cleaner {
public static Cleaner create(Object referent, Runnable thunk) {
return null;
}
public void clean() {
}
}
@@ -0,0 +1,83 @@
package com.rtbishop.look4sat.feature.cw.suncompat;
/**
* 编译期 stub:Android 无 sun.misc.Unsafe(运行时通过反射获取,失败时 k3.r.f12109a 为 null)。
* 照搬自 Morse Expert 1.15(k3 FFT 库的大数组 native 路径 >1GB 不触发,实际 FFT 走 float[] 分支)。
*/
public class Unsafe {
public long allocateMemory(long bytes) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void freeMemory(long address) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public byte getByte(long address) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void putByte(long address, byte value) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public short getShort(long address) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void putShort(long address, short value) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public int getInt(long address) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void putInt(long address, int value) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public long getLong(long address) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void putLong(long address, long value) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public float getFloat(long address) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void putFloat(long address, float value) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public double getDouble(long address) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void putDouble(long address, double value) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void setMemory(long address, long bytes, byte value) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void copyMemory(long src, long dst, long len) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public Object getObject(Object o, long offset) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public void putObject(Object o, long offset, Object value) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
public long objectFieldOffset(java.lang.reflect.Field field) {
throw new UnsupportedOperationException("sun.misc.Unsafe not available on Android");
}
}
@@ -0,0 +1,138 @@
package com.ve3nea.morse_expert;
import F2.a;
import android.content.Context;
import android.content.Intent;
import android.media.MediaScannerConnection;
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.SpannableStringBuilder;
import android.text.TextUtils;
import android.text.format.DateFormat;
import android.util.AttributeSet;
import android.util.Log;
import android.view.ActionMode;
import android.widget.TextView;
import android.widget.Toast;
import java.io.File;
import java.io.PrintWriter;
import java.util.Date;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* 照搬自 Morse Expert 1.15 DecodedTextView, 类名保持混淆原名。
* 适配: 原 extends l.C1702e0(appcompat 混淆类) -> 直接 extends TextView
* (C1702e0 仅提供 TextView 兼容功能, 本类未使用其特有方法)。
*/
public class DecodedTextView extends TextView {
/* renamed from: m, reason: collision with root package name */
public final Context f11031m;
/* renamed from: n, reason: collision with root package name */
public boolean f11032n;
/* renamed from: o, reason: collision with root package name */
public final AtomicBoolean f11033o;
public DecodedTextView(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
this.f11032n = false;
this.f11033o = new AtomicBoolean(false);
this.f11031m = context;
setCustomSelectionActionModeCallback(getActionModeCallback());
}
private ActionMode.Callback getActionModeCallback() {
return new a(this);
}
private String getDecodedText() {
int selectionStart = getSelectionStart();
int selectionEnd = getSelectionEnd();
int max = Math.max(0, Math.min(selectionStart, selectionEnd));
int max2 = Math.max(0, Math.max(selectionStart, selectionEnd));
if (max2 > max) {
return getText().subSequence(max, max2).toString();
}
return getText().toString();
}
@Override // android.widget.TextView
public final void onSelectionChanged(int i4, int i5) {
super.onSelectionChanged(i4, i5);
if (hasSelection()) {
this.f11032n = true;
}
}
public final void q(SpannableStringBuilder spannableStringBuilder) {
boolean z3;
try {
CharSequence text = getText();
Layout layout = getLayout();
int lineCount = getLineCount();
int length = text.length();
int lineHeight = getLineHeight();
int scrollY = getScrollY();
if (layout.getLineBottom(lineCount - 1) == getHeight() + scrollY) {
z3 = true;
} else {
z3 = false;
}
getSelectionStart();
getSelectionEnd();
hasSelection();
if (z3) {
Selection.setSelection((Spannable) text, length, length);
}
append(spannableStringBuilder);
int i4 = lineCount - 50;
if (i4 > 0) {
int i5 = lineHeight * i4;
getEditableText().delete(0, layout.getLineEnd(lineCount - 51));
if (scrollY >= i5) {
scrollBy(0, -i5);
}
}
} catch (Exception e4) {
Log.e("printChars()", "Exception: ", e4);
}
}
public final void r() {
String decodedText = getDecodedText();
boolean isEmpty = TextUtils.isEmpty(decodedText);
Context context = this.f11031m;
if (isEmpty) {
Toast.makeText(context, "No text to save", 0).show();
return;
}
try {
File file = new File(getContext().getExternalFilesDir(null), ((String) DateFormat.format("yyyy-MM-dd_HH_mm_ss", new Date())) + ".txt");
String absolutePath = file.getAbsolutePath();
PrintWriter printWriter = new PrintWriter(file);
try {
printWriter.println(decodedText);
printWriter.close();
MediaScannerConnection.scanFile(context, new String[]{absolutePath}, new String[]{"text/plain"}, null);
Toast.makeText(context, "Text saved to " + absolutePath, 1).show();
} catch (Throwable th) {
printWriter.close();
throw th;
}
} catch (Exception e4) {
Log.e("File Save", "Failed", e4);
Toast.makeText(context, "Unable to save: " + e4.getMessage(), 1).show();
}
}
public final void s() {
Intent intent = new Intent("android.intent.action.SEND");
intent.setFlags(268435456);
intent.putExtra("android.intent.extra.TEXT", getDecodedText());
intent.setType("text/plain");
this.f11031m.startActivity(Intent.createChooser(intent, "Share Decoded Text"));
}
}
@@ -0,0 +1,232 @@
package com.ve3nea.morse_expert;
import android.content.Context;
import android.os.Build;
import android.view.View;
import android.view.ViewGroup;
import android.view.Window;
import android.widget.LinearLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.rtbishop.look4sat.feature.cw.R;
import E2.g;
import H2.a;
import H2.b;
import j1.C1646n;
/**
* Ported verbatim from Morse Expert 1.15 MainActivity (com.ve3nea.morse_expert); obfuscated
* class/field names kept as-is. Surgery notes: originally extends e.AbstractActivityC1557l
* (obfuscated AppCompat base) -> now a plain controller class, Activity injected via
* onCreate(Activity, ConstraintLayout) for Compose AndroidView integration. Ads (d1.*),
* billing (I2.a), premium/rate menu removed per user. h3.b.r (recursive findViewById),
* K1.a.M (immersive status bar), B.i (permissions) replaced with equivalent standard APIs.
* Decoder logic (H2.a audio, H2.b core, pas engine) untouched.
*/
public class MainActivity {
public static final /* synthetic */ int f11034K = 0;
/* renamed from: D, reason: collision with root package name */
public C1646n f11035D;
/* renamed from: E, reason: collision with root package name */
public a f11036E;
/* renamed from: F, reason: collision with root package name */
public b f11037F;
/* renamed from: G, reason: collision with root package name */
public long f11038G = 0;
/* renamed from: J, reason: collision with root package name */
public final g f11040J = new g(this, 1);
/** Injected Activity context (replaces the original this Activity capabilities) */
public android.app.Activity mActivity;
public void onCreate(android.app.Activity activity, ConstraintLayout constraintLayout) {
onCreate(activity, constraintLayout, true);
}
/**
* applyImmersive=false 用于嵌入小容器(如转发器 CW 面板):
* 不触碰宿主窗口的系统栏标志(避免影响整个 Activity)。
*/
public void onCreate(android.app.Activity activity, ConstraintLayout constraintLayout, boolean applyImmersive) {
this.mActivity = activity;
if (applyImmersive) {
// K1.a.M(getWindow(), false): immersive status bar (nav/fullscreen flags, ported as-is)
setImmersive(activity.getWindow(), false);
}
View inflate = constraintLayout;
int i4 = R.id.decodedTextView;
DecodedTextView decodedTextView = (DecodedTextView) findViewRecursive(inflate, R.id.decodedTextView);
if (decodedTextView != null) {
i4 = R.id.scaleView;
ScaleView scaleView = (ScaleView) findViewRecursive(inflate, R.id.scaleView);
if (scaleView != null) {
i4 = R.id.statusTextView;
TextView textView = (TextView) findViewRecursive(inflate, R.id.statusTextView);
if (textView != null) {
i4 = R.id.verticalLayout;
LinearLayout linearLayout = (LinearLayout) findViewRecursive(inflate, R.id.verticalLayout);
if (linearLayout != null) {
i4 = R.id.waterfallView;
WaterfallSurfaceView waterfallSurfaceView = (WaterfallSurfaceView) findViewRecursive(inflate, R.id.waterfallView);
if (waterfallSurfaceView != null) {
this.f11035D = new C1646n(constraintLayout, decodedTextView, scaleView, textView, linearLayout, waterfallSurfaceView);
((WaterfallSurfaceView) this.f11035D.f11892k).setOnTouchListener(this.f11040J);
// Originally: permission check then v(); permission flow handled by the Compose page
// Originally: ad banner mounting (removed)
return;
}
}
}
}
}
throw new NullPointerException("Missing required view with ID: " + inflate.getResources().getResourceName(i4));
}
/** Original h3.b.r: recursive findViewById (ported behavior) */
private static View findViewRecursive(View view, int id) {
if (view instanceof ViewGroup) {
ViewGroup viewGroup = (ViewGroup) view;
int childCount = viewGroup.getChildCount();
for (int i = 0; i < childCount; i++) {
View found = viewGroup.getChildAt(i).findViewById(id);
if (found != null) {
return found;
}
}
return null;
}
return null;
}
/** Original K1.a.M(window, false): immersive (hide nav/fullscreen flags, ported as-is) */
private static void setImmersive(Window window, boolean immersive) {
if (Build.VERSION.SDK_INT >= 30) {
window.setDecorFitsSystemWindows(!immersive);
if (immersive) {
window.getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_FULLSCREEN);
} else {
window.getDecorView().setSystemUiVisibility(
View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
}
} else {
View decorView = window.getDecorView();
int systemUiVisibility = decorView.getSystemUiVisibility();
decorView.setSystemUiVisibility(immersive ? (systemUiVisibility & (-1793)) : (systemUiVisibility | 1792));
}
}
/** Original v(): audio capture + decoder core init (logic unchanged) */
public void v() {
this.f11036E = new a();
b bVar = new b(mActivity.getApplicationContext());
this.f11037F = bVar;
this.f11036E.f601l = bVar;
bVar.f619o = (WaterfallSurfaceView) this.f11035D.f11892k;
bVar.f620p = this;
bVar.f621q = this;
}
/** Original onResume: restore decoding (message type/font/background/record start/waterfall) */
public void onResume() {
android.content.SharedPreferences prefs = mActivity.getSharedPreferences(
mActivity.getPackageName() + "_preferences", Context.MODE_PRIVATE);
String messageType = prefs.getString("message_type", "general_text");
b bVar = this.f11037F;
if (bVar != null) {
bVar.f623s.set(messageType.equals("ham_radio_qso"));
}
((DecodedTextView) this.f11035D.f11888g).setTextSize(Math.max(7, prefs.getInt("text_font_size", 18)));
b bVar2 = this.f11037F;
if (bVar2 != null) {
bVar2.f617m.b(mActivity);
((DecodedTextView) this.f11035D.f11888g).setBackgroundColor(this.f11037F.f617m.f665a[0]);
}
a aVar = this.f11036E;
if (aVar != null) {
android.media.AudioRecord audioRecord = aVar.f596g;
audioRecord.startRecording();
if (audioRecord.getState() != 1) {
android.util.Log.e("AudioRecord", "cannot start");
}
aVar.f598i.set(true);
}
((WaterfallSurfaceView) this.f11035D.f11892k).onResume();
// Originally: ad lifecycle (removed)
}
/** Original onPause: stop recording + waterfall pause */
public void onPause() {
a aVar = this.f11036E;
if (aVar != null) {
aVar.f598i.set(false);
android.media.AudioRecord audioRecord = aVar.f596g;
audioRecord.stop();
if (audioRecord.getState() != 1) {
android.util.Log.e("AudioRecord", "cannot stop");
}
}
((WaterfallSurfaceView) this.f11035D.f11892k).onPause();
}
/** Original onDestroy: release audio/decoder (billing cleanup removed) */
public void onDestroy() {
a aVar = this.f11036E;
if (aVar != null) {
aVar.f598i.set(false);
try {
aVar.f596g.stop();
aVar.f596g.release();
} catch (Exception ignored) {
}
aVar.f597h.shutdownNow();
}
this.f11036E = null;
this.f11037F = null;
}
/** Menu: clear decoded text (original clear_mnu) */
public void clearDecoded() {
((DecodedTextView) this.f11035D.f11888g).setText("");
}
/** Menu: pause/resume decoding (original pause_mnu, red/white icon toggle logic) */
public boolean togglePause() {
java.util.concurrent.atomic.AtomicBoolean atomicBoolean = ((DecodedTextView) this.f11035D.f11888g).f11033o;
atomicBoolean.set(!atomicBoolean.get());
return atomicBoolean.get();
}
/** Menu: save text (original save_mnu -> DecodedTextView.r()) */
public void saveText() {
((DecodedTextView) this.f11035D.f11888g).r();
}
/** Menu: record signals (original record_signals_mnu) */
public void recordSignals() {
this.f11037F.f626v.set(true);
}
/** Original tap_back_again_to_close hint */
public boolean handleBackPress() {
if (System.currentTimeMillis() < this.f11038G + 2000) {
return true; // double-back: exit
}
this.f11038G = System.currentTimeMillis();
Toast.makeText(mActivity, R.string.tap_back_again_to_close, 0).show();
return false;
}
/** Start decoding after permission granted (original onRequestPermissionsResult success) */
public void onPermissionGranted() {
v();
}
}
@@ -0,0 +1,76 @@
package com.ve3nea.morse_expert;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.util.AttributeSet;
import android.view.View;
/* loaded from: classes.dex */
public class ScaleView extends View {
public int f;
/* renamed from: g, reason: collision with root package name */
public int f11041g;
/* renamed from: h, reason: collision with root package name */
public final Paint f11042h;
/* renamed from: i, reason: collision with root package name */
public final Paint f11043i;
/* renamed from: j, reason: collision with root package name */
public final Paint f11044j;
/* renamed from: k, reason: collision with root package name */
public final Paint f11045k;
public ScaleView(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
Paint paint = new Paint();
this.f11042h = paint;
paint.setColor(-16711936);
Paint paint2 = new Paint();
this.f11043i = paint2;
paint2.setColor(-16744448);
Paint paint3 = new Paint();
this.f11044j = paint3;
paint3.setColor(-256);
Paint paint4 = new Paint();
this.f11045k = paint4;
paint4.setColor(-8355840);
}
@Override // android.view.View
public final void onDraw(Canvas canvas) {
Paint paint;
super.onDraw(canvas);
int i4 = this.f11041g;
if (i4 != 1) {
if (i4 != 2) {
if (i4 != 3) {
if (i4 != 4) {
return;
} else {
paint = this.f11044j;
}
} else {
paint = this.f11045k;
}
} else {
paint = this.f11042h;
}
} else {
paint = this.f11043i;
}
int round = (int) Math.round(getWidth() * 0.7d);
Path path = new Path();
path.moveTo(0.0f, this.f);
float f = round;
path.lineTo(f, this.f - round);
path.lineTo(f, this.f + round);
path.close();
canvas.drawPath(path, paint);
}
}
@@ -0,0 +1,19 @@
package com.ve3nea.morse_expert;
import J2.b;
import android.content.Context;
import android.opengl.GLSurfaceView;
import android.util.AttributeSet;
/* loaded from: classes.dex */
public class WaterfallSurfaceView extends GLSurfaceView {
public final b f;
public WaterfallSurfaceView(Context context, AttributeSet attributeSet) {
super(context, attributeSet);
setEGLContextClientVersion(2);
b bVar = new b(context, H2.b.f603B);
this.f = bVar;
setRenderer(bVar);
}
}
@@ -0,0 +1,82 @@
package d1;
/* renamed from: d1.b, reason: case insensitive filesystem */
/* 照搬自 Morse Expert 1.15, 混淆名保持。b(Parcel)/c(y2.d) 为库工具已裁剪(L1/y2 库依赖)。 */
public abstract /* synthetic */ class AbstractC1518b {
public static float a(float f, float f4, k3.d dVar, long j4, float f5, float f6) {
dVar.c(j4, f - f4);
return f5 + f6;
}
public static String d(String str, int i4) {
return str + i4;
}
public static String e(String str, String str2) {
return str + str2;
}
public static String f(String str, String str2, String str3) {
return str + str2 + str3;
}
public static String g(StringBuilder sb, int i4, String str) {
sb.append(i4);
sb.append(str);
return sb.toString();
}
public static String h(StringBuilder sb, String str, String str2) {
sb.append(str);
sb.append(str2);
return sb.toString();
}
public static /* synthetic */ boolean i(Object obj) {
return obj != null;
}
public static float j(float f, float f4, k3.d dVar, long j4, float f5, float f6) {
dVar.c(j4, f + f4);
return f5 + f6;
}
public static String k(String str, String str2) {
return str + str2;
}
public static float l(float f, float f4, k3.d dVar, long j4, float f5, float f6) {
dVar.c(j4, f - f4);
return f5 - f6;
}
public static float m(float f, float f4, k3.d dVar, long j4, float f5, float f6) {
dVar.c(j4, f + f4);
return f5 - f6;
}
public static /* synthetic */ String n(int i4) {
switch (i4) {
case 1:
return "NONE";
case 2:
return "LEFT";
case 3:
return "TOP";
case 4:
return "RIGHT";
case 5:
return "BOTTOM";
case 6:
return "BASELINE";
case 7:
return "CENTER";
case 8:
return "CENTER_X";
case 9:
return "CENTER_Y";
default:
throw null;
}
}
}
+34
View File
@@ -0,0 +1,34 @@
package g3;
/* loaded from: classes.dex */
public final class a {
/* renamed from: a, reason: collision with root package name */
public final int f11582a;
/* renamed from: b, reason: collision with root package name */
public final double f11583b;
public final double c;
public a(double d4) {
int i4 = (int) (0.6366197723675814d * d4);
while (true) {
double d5 = -i4;
double d6 = 1.570796251296997d * d5;
double d7 = d4 + d6;
double d8 = 7.549789948768648E-8d * d5;
double d9 = d8 + d7;
double d10 = (-((d7 - d4) - d6)) + (-((d9 - d7) - d8));
double d11 = d5 * 6.123233995736766E-17d;
double d12 = d11 + d9;
double d13 = d10 + (-((d12 - d9) - d11));
if (d12 > 0.0d) {
this.f11582a = i4;
this.f11583b = d12;
this.c = d13;
return;
}
i4--;
}
}
}
+8
View File
@@ -0,0 +1,8 @@
package g3;
/* loaded from: classes.dex */
public abstract class b {
/* renamed from: a, reason: collision with root package name */
public static final double[][] f11584a = (double[][]) d.f11591a.clone();
}
+507
View File
@@ -0,0 +1,507 @@
package g3;
/* loaded from: classes.dex */
public abstract class c {
/* renamed from: a, reason: collision with root package name */
public static final double[][] f11585a;
/* renamed from: b, reason: collision with root package name */
public static final double[] f11586b;
public static final double[] c;
/* renamed from: d, reason: collision with root package name */
public static final double[] f11587d;
/* renamed from: e, reason: collision with root package name */
public static final double[] f11588e;
public static final long[] f;
/* renamed from: g, reason: collision with root package name */
public static final long[] f11589g;
/* renamed from: h, reason: collision with root package name */
public static final double[] f11590h;
static {
StrictMath.log(Double.MAX_VALUE);
f11585a = new double[][]{new double[]{1.0d, 5.669184079525E-24d}, new double[]{-0.25d, -0.25d}, new double[]{0.3333333134651184d, 1.986821492305628E-8d}, new double[]{-0.25d, -6.663542893624021E-14d}, new double[]{0.19999998807907104d, 1.1921056801463227E-8d}, new double[]{-0.1666666567325592d, -7.800414592973399E-9d}, new double[]{0.1428571343421936d, 5.650007086920087E-9d}, new double[]{-0.12502530217170715d, -7.44321345601866E-11d}, new double[]{0.11113807559013367d, 9.219544613762692E-9d}};
f11586b = new double[]{0.0d, 0.1246747374534607d, 0.24740394949913025d, 0.366272509098053d, 0.4794255495071411d, 0.5850973129272461d, 0.6816387176513672d, 0.7675435543060303d, 0.8414709568023682d, 0.902267575263977d, 0.9489846229553223d, 0.9808930158615112d, 0.9974949359893799d, 0.9985313415527344d};
c = new double[]{0.0d, -4.068233003401932E-9d, 9.755392680573412E-9d, 1.9987994582857286E-8d, -1.0902938113007961E-8d, -3.9986783938944604E-8d, 4.23719669792332E-8d, -5.207000323380292E-8d, 2.800552834259E-8d, 1.883511811213715E-8d, -3.5997360512765566E-9d, 4.116164446561962E-8d, 5.0614674548127384E-8d, -1.0129027912496858E-9d};
f11587d = new double[]{1.0d, 0.9921976327896118d, 0.9689123630523682d, 0.9305076599121094d, 0.8775825500488281d, 0.8109631538391113d, 0.7316888570785522d, 0.6409968137741089d, 0.5403022766113281d, 0.4311765432357788d, 0.3153223395347595d, 0.19454771280288696d, 0.07073719799518585d, -0.05417713522911072d};
f11588e = new double[]{0.0d, 3.4439717236742845E-8d, 5.865827662008209E-8d, -3.7999795083850525E-8d, 1.184154459111628E-8d, -3.43338934259355E-8d, 1.1795268640216787E-8d, 4.438921624363781E-8d, 2.925681159240093E-8d, -2.6437112632041807E-8d, 2.2860509143963117E-8d, -4.813899778443457E-9d, 3.6725170580355583E-9d, 2.0217439756338078E-10d};
f = new long[]{2935890503282001226L, 9154082963658192752L, 3952090531849364496L, 9193070505571053912L, 7910884519577875640L, 113236205062349959L, 4577762542105553359L, -5034868814120038111L, 4208363204685324176L, 5648769086999809661L, 2819561105158720014L, -4035746434778044925L, -302932621132653753L, -2644281811660520851L, -3183605296591799669L, 6722166367014452318L, -3512299194304650054L, -7278142539171889152L};
f11589g = new long[]{-3958705157555305932L, -4267615245585081135L};
f11590h = new double[]{0.0d, 0.125d, 0.25d, 0.375d, 0.5d, 0.625d, 0.75d, 0.875d, 1.0d, 1.125d, 1.25d, 1.375d, 1.5d, 1.625d};
}
public static double a(double d4) {
double d5;
if (d4 != d4) {
return d4;
}
if (d4 == d4 && d4 < 4.503599627370496E15d && d4 > -4.503599627370496E15d) {
long j4 = (long) d4;
if (d4 < 0.0d && j4 != d4) {
j4--;
}
if (j4 == 0) {
d5 = j4 * d4;
} else {
d5 = j4;
}
} else {
d5 = d4;
}
if (d5 == d4) {
return d5;
}
double d6 = d5 + 1.0d;
if (d6 == 0.0d) {
return d4 * d6;
}
return d6;
}
public static double b(double d4) {
double g4;
double d5 = 0.0d;
if (d4 < 0.0d) {
d4 = -d4;
}
if (d4 != d4 || d4 == Double.POSITIVE_INFINITY) {
return Double.NaN;
}
int i4 = 0;
if (d4 > 3294198.0d) {
double[] dArr = new double[3];
e(d4, dArr);
i4 = ((int) dArr[0]) & 3;
d4 = dArr[1];
d5 = dArr[2];
} else if (d4 > 1.5707963267948966d) {
a aVar = new a(d4);
i4 = aVar.f11582a & 3;
d4 = aVar.f11583b;
d5 = aVar.c;
}
if (i4 != 0) {
if (i4 != 1) {
if (i4 != 2) {
if (i4 != 3) {
return Double.NaN;
}
return g(d4, d5);
}
g4 = c(d4, d5);
} else {
g4 = g(d4, d5);
}
return -g4;
}
return c(d4, d5);
}
public static double c(double d4, double d5) {
double d6 = 1.5707963267948966d - d4;
return g(d6, (6.123233995736766E-17d - d5) + (-((d6 - 1.5707963267948966d) + d4)));
}
public static double d(double d4) {
if (d4 != 0.0d) {
long doubleToRawLongBits = Double.doubleToRawLongBits(d4);
if (((Long.MIN_VALUE & doubleToRawLongBits) != 0 || d4 != d4) && d4 != 0.0d) {
return Double.NaN;
}
if (d4 == Double.POSITIVE_INFINITY) {
return Double.POSITIVE_INFINITY;
}
int i4 = ((int) (doubleToRawLongBits >> 52)) - 1023;
if ((9218868437227405312L & doubleToRawLongBits) == 0) {
if (d4 == 0.0d) {
return Double.NEGATIVE_INFINITY;
}
doubleToRawLongBits <<= 1;
while ((4503599627370496L & doubleToRawLongBits) == 0) {
i4--;
doubleToRawLongBits <<= 1;
}
}
if ((i4 == -1 || i4 == 0) && d4 < 1.01d && d4 > 0.99d) {
double d5 = d4 - 1.0d;
double d6 = d5 * 1.073741824E9d;
double d7 = (d5 + d6) - d6;
double d8 = d5 - d7;
double[][] dArr = f11585a;
double[] dArr2 = dArr[dArr.length - 1];
double d9 = dArr2[0];
double d10 = dArr2[1];
for (int length = dArr.length - 2; length >= 0; length--) {
double d11 = d9 * d7;
double d12 = (d10 * d8) + (d10 * d7) + (d9 * d8);
double d13 = d11 * 1.073741824E9d;
double d14 = (d11 + d13) - d13;
double d15 = (d11 - d14) + d12;
double[] dArr3 = dArr[length];
double d16 = d14 + dArr3[0];
double d17 = d15 + dArr3[1];
double d18 = d16 * 1.073741824E9d;
d9 = (d16 + d18) - d18;
d10 = (d16 - d9) + d17;
}
double d19 = d9 * d7;
double d20 = (d10 * d8) + (d7 * d10) + (d9 * d8);
double d21 = 1.073741824E9d * d19;
double d22 = (d19 + d21) - d21;
return (d19 - d22) + d20 + d22;
}
long j4 = 4499201580859392L & doubleToRawLongBits;
double[] dArr4 = b.f11584a[(int) (j4 >> 42)];
double d23 = (doubleToRawLongBits & 4398046511103L) / (j4 + 4.503599627370496E15d);
double d24 = (((((((((((-0.16624882440418567d) * d23) + 0.19999954120254515d) * d23) - 0.2499999997677497d) * d23) + 0.3333333333332802d) * d23) - 0.5d) * d23) + 1.0d) * d23;
double d25 = i4;
double d26 = 0.6931470632553101d * d25;
double d27 = dArr4[0];
double d28 = d26 + d27;
double d29 = (-((d28 - d26) - d27)) + 0.0d;
double d30 = d28 + d24;
double d31 = d29 + (-((d30 - d28) - d24));
double d32 = d25 * 1.1730463525082348E-7d;
double d33 = d30 + d32;
double d34 = d31 + (-((d33 - d30) - d32));
double d35 = dArr4[1];
double d36 = d33 + d35;
double d37 = d36 + 0.0d;
return d34 + (-((d36 - d33) - d35)) + (-((d37 - d36) - 0.0d)) + d37;
}
return Double.NEGATIVE_INFINITY;
}
public static void e(double d4, double[] dArr) {
long j4;
long j5;
long j6;
boolean z3;
boolean z4;
boolean z5;
boolean z6;
boolean z7;
boolean z8;
boolean z9;
boolean z10;
boolean z11;
boolean z12;
boolean z13;
boolean z14;
boolean z15;
boolean z16;
boolean z17;
boolean z18;
boolean z19;
boolean z20;
boolean z21;
boolean z22;
boolean z23;
long j7;
long doubleToRawLongBits = Double.doubleToRawLongBits(d4);
int i4 = ((int) ((doubleToRawLongBits >> 52) & 2047)) - 1022;
long j8 = ((doubleToRawLongBits & 4503599627370495L) | 4503599627370496L) << 11;
int i5 = i4 >> 6;
int i6 = i4 - (i5 << 6);
long[] jArr = f;
if (i6 != 0) {
if (i5 == 0) {
j7 = 0;
} else {
j7 = jArr[i5 - 1] << i6;
}
long j9 = jArr[i5];
int i7 = 64 - i6;
j4 = j7 | (j9 >>> i7);
long j10 = jArr[i5 + 1];
j5 = (j9 << i6) | (j10 >>> i7);
j6 = (jArr[i5 + 2] >>> i7) | (j10 << i6);
} else {
if (i5 == 0) {
j4 = 0;
} else {
j4 = jArr[i5 - 1];
}
j5 = jArr[i5];
j6 = jArr[i5 + 1];
}
long j11 = j8 >>> 32;
long j12 = j8 & 4294967295L;
long j13 = j5 >>> 32;
long j14 = j5 & 4294967295L;
long j15 = j11 * j13;
long j16 = j12 * j14;
long j17 = j13 * j12;
long j18 = j14 * j11;
long j19 = j16 + (j18 << 32);
long j20 = j15 + (j18 >>> 32);
if ((j16 & Long.MIN_VALUE) != 0) {
z3 = true;
} else {
z3 = false;
}
if ((j18 & 2147483648L) != 0) {
z4 = true;
} else {
z4 = false;
}
long j21 = j19 & Long.MIN_VALUE;
if (j21 != 0) {
z5 = true;
} else {
z5 = false;
}
if ((z3 && z4) || ((z3 || z4) && !z5)) {
j20++;
}
if (j21 != 0) {
z6 = true;
} else {
z6 = false;
}
if ((j17 & 2147483648L) != 0) {
z7 = true;
} else {
z7 = false;
}
long j22 = j19 + (j17 << 32);
long j23 = j20 + (j17 >>> 32);
long j24 = j22 & Long.MIN_VALUE;
if (j24 != 0) {
z8 = true;
} else {
z8 = false;
}
if ((z6 && z7) || ((z6 || z7) && !z8)) {
j23++;
}
long j25 = j6 >>> 32;
long j26 = (j11 * j25) + ((((j6 & 4294967295L) * j11) + (j25 * j12)) >>> 32);
if (j24 != 0) {
z9 = true;
} else {
z9 = false;
}
if ((j26 & Long.MIN_VALUE) != 0) {
z10 = true;
} else {
z10 = false;
}
long j27 = j22 + j26;
if ((j27 & Long.MIN_VALUE) != 0) {
z11 = true;
} else {
z11 = false;
}
if ((z9 && z10) || ((z9 || z10) && !z11)) {
j23++;
}
long j28 = j4 >>> 32;
long j29 = j4 & 4294967295L;
long j30 = (j12 * j29) + (((j11 * j29) + (j12 * j28)) << 32) + j23;
int i8 = (int) (j30 >>> 62);
long j31 = (j27 >>> 62) | (j30 << 2);
long j32 = j27 << 2;
long j33 = j31 >>> 32;
long j34 = j31 & 4294967295L;
long[] jArr2 = f11589g;
long j35 = jArr2[0];
long j36 = j35 >>> 32;
long j37 = j35 & 4294967295L;
long j38 = j34 * j37;
long j39 = j34 * j36;
long j40 = j33 * j37;
long j41 = j38 + (j40 << 32);
long j42 = (j33 * j36) + (j40 >>> 32);
if ((j38 & Long.MIN_VALUE) != 0) {
z12 = true;
} else {
z12 = false;
}
if ((j40 & 2147483648L) != 0) {
z13 = true;
} else {
z13 = false;
}
long j43 = j41 & Long.MIN_VALUE;
if (j43 != 0) {
z14 = true;
} else {
z14 = false;
}
if ((z12 && z13) || ((z12 || z13) && !z14)) {
j42++;
}
if (j43 != 0) {
z15 = true;
} else {
z15 = false;
}
if ((j39 & 2147483648L) != 0) {
z16 = true;
} else {
z16 = false;
}
long j44 = j41 + (j39 << 32);
long j45 = j42 + (j39 >>> 32);
long j46 = j44 & Long.MIN_VALUE;
if (j46 != 0) {
z17 = true;
} else {
z17 = false;
}
if ((z15 && z16) || ((z15 || z16) && !z17)) {
j45++;
}
long j47 = jArr2[1];
long j48 = j47 >>> 32;
long j49 = (j33 * j48) + (((j33 * (j47 & 4294967295L)) + (j34 * j48)) >>> 32);
if (j46 != 0) {
z18 = true;
} else {
z18 = false;
}
if ((j49 & Long.MIN_VALUE) != 0) {
z19 = true;
} else {
z19 = false;
}
long j50 = j44 + j49;
long j51 = j50 & Long.MIN_VALUE;
if (j51 != 0) {
z20 = true;
} else {
z20 = false;
}
if ((z18 && z19) || ((z18 || z19) && !z20)) {
j45++;
}
long j52 = j32 >>> 32;
long j53 = (j52 * j36) + (((j52 * j37) + ((j32 & 4294967295L) * j36)) >>> 32);
if (j51 != 0) {
z21 = true;
} else {
z21 = false;
}
if ((j53 & Long.MIN_VALUE) != 0) {
z22 = true;
} else {
z22 = false;
}
if (((j50 + j53) & Long.MIN_VALUE) != 0) {
z23 = true;
} else {
z23 = false;
}
if ((z21 && z22) || ((z21 || z22) && !z23)) {
j45++;
}
double d5 = (j45 >>> 12) / 4.503599627370496E15d;
double d6 = ((((j45 & 4095) << 40) + (f11589g[0] >>> 24)) / 4.503599627370496E15d) / 4.503599627370496E15d; // r34 = f11589g[0](smali: aget-wide v5, v2)
double d7 = d5 + d6;
dArr[0] = i8;
dArr[1] = d7 * 2.0d;
dArr[2] = (-((d7 - d5) - d6)) * 2.0d;
}
/* JADX WARN: Removed duplicated region for block: B:20:0x005f */
/* JADX WARN: Removed duplicated region for block: B:22:0x0063 */
/* JADX WARN: Removed duplicated region for block: B:32:0x007a */
/*
Code decompiled incorrectly, please refer to instructions dump.
*/
public static double f(double d4) {
boolean z3;
double d5;
double d6;
double g4;
int i4 = 0;
double d7 = 0.0d;
if (d4 < 0.0d) {
d5 = -d4;
z3 = true;
} else {
z3 = false;
d5 = d4;
}
if (d5 == 0.0d) {
if (Double.doubleToRawLongBits(d4) >= 0) {
return 0.0d;
}
return -0.0d;
}
if (d5 != d5 || d5 == Double.POSITIVE_INFINITY) {
return Double.NaN;
}
if (d5 > 3294198.0d) {
double[] dArr = new double[3];
e(d5, dArr);
i4 = ((int) dArr[0]) & 3;
d5 = dArr[1];
d6 = dArr[2];
} else {
if (d5 > 1.5707963267948966d) {
a aVar = new a(d5);
int i5 = aVar.f11582a & 3;
d5 = aVar.f11583b;
d6 = aVar.c;
i4 = i5;
}
if (z3) {
i4 ^= 2;
}
// smali 证实: f() = sin(象限 switch!):
// case 0 -> g(d5,0)(sin(0-π/2))
// case 1 -> c(d5,0)(sin(π/2-π) = cos(余角))
// case 2 -> -g(d5,0)(sin(π-3π/2) = -sin(余角))
// case 3 -> -c(d5,0)(sin(3π/2-2π) = -cos(余角))
// jadx 畸形嵌套 if 把 case 0 还原成 return NaN(小角度全 NaN!),
// 且 case 1/2/3 全走 g() —— 旋转因子表 329 个 NaN + 频谱错乱的根源!
if (i4 == 0) {
return g(d5, d7);
}
if (i4 == 1) {
return c(d5, d7);
}
if (i4 == 2) {
return -g(d5, d7);
}
return -c(d5, d7);
}
d7 = d6;
// jadx 还原不完整(smali 650 行, jadx 只还原 63 行); f() 未被任何代码调用(死代码),
// 按 smali 尾部语义补 return(cond_0: v22 = v22 + v14; return v22)
return d7;
}
public static double g(double d4, double d5) {
int i4 = (int) ((8.0d * d4) + 0.5d);
double d6 = d4 - f11590h[i4];
double d7 = f11586b[i4];
double d8 = c[i4];
double d9 = f11587d[i4];
double d10 = f11588e[i4];
double d11 = d6 * d6;
double d12 = ((((((2.7553817452272217E-6d * d11) - 1.9841269659586505E-4d) * d11) + 0.008333333333329196d) * d11) - 0.16666666666666666d) * d11 * d6;
double d13 = ((((((2.479773539153719E-5d * d11) - 0.0013888888689039883d) * d11) + 0.041666666666621166d) * d11) - 0.49999999999999994d) * d11;
double d14 = 1.073741824E9d * d6;
double d15 = (d6 + d14) - d14;
double d16 = (d6 - d15) + d12;
double d17 = d7 + 0.0d;
double d18 = d9 * d15;
double d19 = d17 + d18;
double d20 = (d10 * d16) + (d8 * d13) + (d10 * d15) + (d9 * d16) + (d7 * d13) + (-((d17 - 0.0d) - d7)) + 0.0d + (-((d19 - d17) - d18)) + d8;
if (d5 != 0.0d) {
double d21 = (((d13 + 1.0d) * (d9 + d10)) - ((d15 + d16) * (d7 + d8))) * d5;
double d22 = d19 + d21;
d20 += -((d22 - d19) - d21);
d19 = d22;
}
return d19 + d20;
}
}
+9
View File
@@ -0,0 +1,9 @@
package g3;
/* loaded from: classes.dex */
public abstract class d {
/* renamed from: a, reason: collision with root package name */
public static final double[][] f11591a = {new double[]{0.0d, 0.0d}, new double[]{9.760860120877624E-4d, -3.903230345984362E-11d}, new double[]{0.0019512202125042677d, -8.124251825289188E-11d}, new double[]{0.0029254043474793434d, -1.8374207360194882E-11d}, new double[]{0.0038986406289041042d, -2.1324678121885073E-10d}, new double[]{0.004870930686593056d, -4.5199654318611534E-10d}, new double[]{0.00584227591753006d, -2.933016992001806E-10d}, new double[]{0.006812678650021553d, -2.325147219074669E-10d}, new double[]{0.007782140746712685d, -3.046577356838847E-10d}, new double[]{0.008750664070248604d, -5.500631513861575E-10d}, new double[]{0.00971824862062931d, 8.48292035519895E-10d}, new double[]{0.010684899985790253d, 1.1422610134013436E-10d}, new double[]{0.01165061630308628d, 9.168889933128375E-10d}, new double[]{0.012615403160452843d, -5.303786078838E-10d}, new double[]{0.013579258695244789d, -5.688639355498786E-10d}, new double[]{0.01454218477010727d, 7.296670293275653E-10d}, new double[]{0.015504186972975731d, -4.370104767451421E-10d}, new double[]{0.016465261578559875d, 1.43695591408832E-9d}, new double[]{0.01742541790008545d, -1.1862263158849434E-9d}, new double[]{0.018384650349617004d, -9.482976524690715E-10d}, new double[]{0.01934296265244484d, 1.9068609515836638E-10d}, new double[]{0.020300358533859253d, 2.655990315697216E-10d}, new double[]{0.021256837993860245d, 1.0315548713040775E-9d}, new double[]{0.022212404757738113d, 5.13345647019085E-10d}, new double[]{0.02316705882549286d, 4.5604151934208014E-10d}, new double[]{0.02412080392241478d, -1.1255706987475148E-9d}, new double[]{0.025073636323213577d, 1.2289023836765196E-9d}, new double[]{0.02602556347846985d, 1.7990281828096504E-9d}, new double[]{0.026976589113473892d, -1.4152718164638451E-9d}, new double[]{0.02792670577764511d, 7.568772963781632E-10d}, new double[]{0.0288759246468544d, -1.1449998592111558E-9d}, new double[]{0.029824241995811462d, -1.6850976862319495E-9d}, new double[]{0.030771657824516296d, 8.422373919843096E-10d}, new double[]{0.0317181795835495d, 6.872350402175489E-10d}, new double[]{0.03266380727291107d, -4.541194749189272E-10d}, new double[]{0.03360854089260101d, -8.9064764856495E-10d}, new double[]{0.034552380442619324d, 1.0640404096769032E-9d}, new double[]{0.0354953333735466d, -3.5901655945224663E-10d}, new double[]{0.03643739968538284d, -3.4829517943661266E-9d}, new double[]{0.037378571927547455d, 8.149473794244232E-10d}, new double[]{0.03831886500120163d, -6.990650304449166E-10d}, new double[]{0.03925827145576477d, 1.0883076226453258E-9d}, new double[]{0.040196798741817474d, 3.845192807999274E-10d}, new double[]{0.04113444685935974d, -1.1570594692045927E-9d}, new double[]{0.04207121580839157d, -1.8877045166697178E-9d}, new double[]{0.043007105588912964d, -1.6332083257987747E-10d}, new double[]{0.04394212365150452d, -1.7950057534514933E-9d}, new double[]{0.04487626254558563d, 2.302710041648838E-9d}, new double[]{0.045809537172317505d, -1.1410233017161343E-9d}, new double[]{0.04674194008111954d, -3.0498741599744685E-9d}, new double[]{0.04767347127199173d, -1.8026348269183678E-9d}, new double[]{0.04860413819551468d, -3.233204600453039E-9d}, new double[]{0.04953393340110779d, 1.7211688427961583E-9d}, new double[]{0.05046287178993225d, -2.329967807055457E-10d}, new double[]{0.05139094591140747d, -4.191810118556531E-11d}, new double[]{0.052318163216114044d, -3.5574324788328143E-9d}, new double[]{0.053244516253471375d, -1.7346590916458485E-9d}, new double[]{0.05417001247406006d, -4.343048751383674E-10d}, new double[]{0.055094651877880096d, 1.92909364037955E-9d}, new double[]{0.056018441915512085d, -5.139745677199588E-10d}, new double[]{0.05694137513637543d, 1.2637629975129189E-9d}, new double[]{0.05786345899105072d, 1.3840561112481119E-9d}, new double[]{0.058784693479537964d, 1.414889689612056E-9d}, new double[]{0.05970507860183716d, 2.9199191907666474E-9d}, new double[]{0.0606246218085289d, 7.90594243412116E-12d}, new double[]{0.06154331564903259d, 1.6844747839686189E-9d}, new double[]{0.06246116757392883d, 2.0498074572151747E-9d}, new double[]{0.06337818503379822d, -4.800180493433863E-9d}, new double[]{0.06429435312747955d, -2.4220822960064277E-9d}, new double[]{0.06520968675613403d, -4.179048566709334E-9d}, new double[]{0.06612417101860046d, 6.363872957010456E-9d}, new double[]{0.06703783571720123d, 9.339468680056365E-10d}, new double[]{0.06795066595077515d, -4.04226739708981E-9d}, new double[]{0.0688626617193222d, -7.043545052852817E-9d}, new double[]{0.06977382302284241d, -6.552819560439773E-9d}, new double[]{0.07068414986133575d, -1.0571674860370546E-9d}, new double[]{0.07159365713596344d, -3.948954622015801E-9d}, new double[]{0.07250232994556427d, 1.1776625988228244E-9d}, new double[]{0.07341018319129944d, 9.221072639606492E-10d}, new double[]{0.07431721687316895d, -3.219119568928366E-9d}, new double[]{0.0752234160900116d, 5.147575929018918E-9d}, new double[]{0.07612881064414978d, -2.291749683541979E-9d}, new doubleLine truncated
new double[]{0.6813591718673706d, 5.294053246347931E-8d}, new double[]{0.681853175163269d, -1.2220826223585654E-9d}, new double[]{0.6823468208312988d, 5.8377876767612725E-8d}, new double[]{0.6828403472900391d, -6.437492120743254E-9d}, new double[]{0.6833335161209106d, 4.2990710043633113E-8d}, new double[]{0.6838265657424927d, -3.1516131027023284E-8d}, new double[]{0.684319257736206d, 8.70017386744679E-9d}, new double[]{0.6848117113113403d, 4.466959125843237E-8d}, new double[]{0.6853040456771851d, -4.25782656420497E-8d}, new double[]{0.6857960224151611d, -1.4386267593671393E-8d}, new double[]{0.6862877607345581d, 1.0274494061148778E-8d}, new double[]{0.686779260635376d, 3.164186629229597E-8d}, new double[]{0.6872705221176147d, 4.995334552140326E-8d}, new double[]{0.687761664390564d, -5.3763211240398744E-8d}, new double[]{0.6882524490356445d, -4.0852427502515625E-8d}, new double[]{0.688742995262146d, -3.0287143914420064E-8d}, new double[]{0.6892333030700684d, -2.183125937905008E-8d}, new double[]{0.6897233724594116d, -1.524901992178814E-8d}, new double[]{0.6902132034301758d, -1.0305018010328949E-8d}, new double[]{0.6907027959823608d, -6.764191876212205E-9d}, new double[]{0.6911921501159668d, -4.391824838015402E-9d}, new double[]{0.6916812658309937d, -2.9535446262017846E-9d}, new double[]{0.6921701431274414d, -2.2153227096187463E-9d}, new double[]{0.6926587820053101d, -1.943473623641502E-9d}};
}
+41
View File
@@ -0,0 +1,41 @@
package i3;
/* loaded from: classes.dex */
public final class a implements Runnable {
public final /* synthetic */ int f;
/* renamed from: g, reason: collision with root package name */
public final /* synthetic */ int f11686g;
/* renamed from: h, reason: collision with root package name */
public final /* synthetic */ int f11687h;
/* renamed from: i, reason: collision with root package name */
public final /* synthetic */ float[] f11688i;
/* renamed from: j, reason: collision with root package name */
public final /* synthetic */ float[] f11689j;
/* renamed from: k, reason: collision with root package name */
public final /* synthetic */ d f11690k;
public a(d dVar, int i4, int i5, int i6, float[] fArr, float[] fArr2) {
this.f11690k = dVar;
this.f = i4;
this.f11686g = i5;
this.f11687h = i6;
this.f11688i = fArr;
this.f11689j = fArr2;
}
@Override // java.lang.Runnable
public final void run() {
int i4 = this.f11686g;
int i5 = this.f;
if (i5 >= i4) {
return;
}
float f = this.f11689j[this.f11687h + i5];
throw null;
}
}
+41
View File
@@ -0,0 +1,41 @@
package i3;
/* loaded from: classes.dex */
public final class b implements Runnable {
public final /* synthetic */ long f;
/* renamed from: g, reason: collision with root package name */
public final /* synthetic */ long f11691g;
/* renamed from: h, reason: collision with root package name */
public final /* synthetic */ long f11692h;
/* renamed from: i, reason: collision with root package name */
public final /* synthetic */ k3.d f11693i;
/* renamed from: j, reason: collision with root package name */
public final /* synthetic */ k3.d f11694j;
/* renamed from: k, reason: collision with root package name */
public final /* synthetic */ d f11695k;
public b(d dVar, long j4, long j5, long j6, k3.d dVar2, k3.d dVar3) {
this.f11695k = dVar;
this.f = j4;
this.f11691g = j5;
this.f11692h = j6;
this.f11693i = dVar2;
this.f11694j = dVar3;
}
@Override // java.lang.Runnable
public final void run() {
long j4 = this.f11691g;
long j5 = this.f;
if (j5 >= j4) {
return;
}
this.f11694j.b(this.f11692h + j5);
throw null;
}
}
+55
View File
@@ -0,0 +1,55 @@
package i3;
import k3.g;
import k3.s;
/* loaded from: classes.dex */
public final class c implements Runnable {
public final /* synthetic */ int f = 1;
/* renamed from: g, reason: collision with root package name */
public final /* synthetic */ long f11696g;
/* renamed from: h, reason: collision with root package name */
public final /* synthetic */ long f11697h;
/* renamed from: i, reason: collision with root package name */
public final /* synthetic */ g f11698i;
/* renamed from: j, reason: collision with root package name */
public final /* synthetic */ Object f11699j;
public c(long j4, long j5, s sVar, s sVar2) {
this.f11696g = j4;
this.f11697h = j5;
this.f11698i = sVar;
this.f11699j = sVar2;
}
@Override // java.lang.Runnable
public final void run() {
switch (this.f) {
case 0:
k3.d dVar = (k3.d) this.f11698i;
long j4 = this.f11697h;
long j5 = this.f11696g;
if (j5 >= j4) {
return;
}
dVar.b(j5 * 2);
throw null;
default:
for (long j6 = this.f11696g; j6 < this.f11697h; j6++) {
((s) this.f11698i).c(j6, ((s) this.f11699j).b(j6));
}
return;
}
}
public c(d dVar, long j4, long j5, k3.d dVar2) {
this.f11699j = dVar;
this.f11696g = j4;
this.f11697h = j5;
this.f11698i = dVar2;
}
}
File diff suppressed because it is too large. Load diff
+42
View File
@@ -0,0 +1,42 @@
package j1;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.constraintlayout.widget.ConstraintLayout;
import com.ve3nea.morse_expert.DecodedTextView;
import com.ve3nea.morse_expert.ScaleView;
import com.ve3nea.morse_expert.WaterfallSurfaceView;
/**
* 照搬自 Morse Expert 1.15 j1.C1646n, 类名/字段名保持混淆原名。
* R8 合并类手术: 只保留视图持有器(6 参构造 + 5 个 View 字段);
* 其余 158-1278 行(广告/计费合并方法)已整段裁剪(用户要求广告不迁移)。
*/
public final class C1646n {
public final /* synthetic */ int f;
/* renamed from: g, reason: collision with root package name */
public Object f11888g;
/* renamed from: h, reason: collision with root package name */
public Object f11889h;
/* renamed from: i, reason: collision with root package name */
public Object f11890i;
/* renamed from: j, reason: collision with root package name */
public Object f11891j;
/* renamed from: k, reason: collision with root package name */
public Object f11892k;
public C1646n(ConstraintLayout constraintLayout, DecodedTextView decodedTextView, ScaleView scaleView, TextView textView, LinearLayout linearLayout, WaterfallSurfaceView waterfallSurfaceView) {
this.f = 1;
this.f11888g = decodedTextView;
this.f11889h = scaleView;
this.f11890i = textView;
this.f11891j = linearLayout;
this.f11892k = waterfallSurfaceView;
}
}
+100
View File
@@ -0,0 +1,100 @@
package j3;
/* loaded from: classes.dex */
public final class a implements Runnable {
public final /* synthetic */ int f;
/* renamed from: g, reason: collision with root package name */
public final /* synthetic */ int f11919g;
/* renamed from: h, reason: collision with root package name */
public final /* synthetic */ int f11920h;
/* renamed from: i, reason: collision with root package name */
public final /* synthetic */ int f11921i;
/* renamed from: j, reason: collision with root package name */
public final /* synthetic */ float[] f11922j;
/* renamed from: k, reason: collision with root package name */
public final /* synthetic */ float[] f11923k;
/* renamed from: l, reason: collision with root package name */
public final /* synthetic */ int f11924l;
public /* synthetic */ a(int i4, int i5, int i6, int i7, int i8, float[] fArr, float[] fArr2) {
this.f = i8;
this.f11919g = i4;
this.f11920h = i5;
this.f11921i = i6;
this.f11922j = fArr;
this.f11923k = fArr2;
this.f11924l = i7;
}
@Override // java.lang.Runnable
public final void run() {
switch (this.f) {
case 0:
int i4 = this.f11919g;
int i5 = this.f11920h;
int i6 = i4 + i5;
int i7 = this.f11921i;
while (true) {
int i8 = i7;
if (i8 > 512) {
i7 = i8 >> 2;
c.w(i7, i6 - i7, this.f11924l - (i8 >> 3), this.f11922j, this.f11923k);
} else {
int i9 = this.f11924l;
float[] fArr = this.f11923k;
c.u(i8, 1, this.f11922j, i6 - i8, i9, fArr);
int i10 = i4 - i8;
int i11 = 0;
int i12 = i5 - i8;
while (i12 > 0) {
int i13 = i11 + 1;
int i14 = this.f11924l;
float[] fArr2 = this.f11923k;
int i15 = i12;
i11 = i13;
int i16 = this.f11924l;
float[] fArr3 = this.f11923k;
c.u(i8, c.E(i8, i12, i13, this.f11919g, i14, this.f11922j, fArr2), this.f11922j, i10 + i15, i16, fArr3);
i12 = i15 - i8;
}
return;
}
}
default:
int i17 = this.f11919g;
int i18 = this.f11920h;
int i19 = i17 + i18;
int i20 = this.f11921i;
int i21 = 1;
while (i20 > 512) {
i20 >>= 2;
i21 <<= 2;
c.y(i20, i19 - i20, this.f11924l - i20, this.f11922j, this.f11923k);
}
int i22 = this.f11924l;
float[] fArr4 = this.f11923k;
c.u(i20, 0, this.f11922j, i19 - i20, i22, fArr4);
int i23 = i21 >> 1;
int i24 = i17 - i20;
int i25 = i18 - i20;
while (i25 > 0) {
int i26 = i23 + 1;
int i27 = this.f11924l;
float[] fArr5 = this.f11923k;
int i28 = i25;
i23 = i26;
int i29 = this.f11924l;
float[] fArr6 = this.f11923k;
c.u(i20, c.E(i20, i25, i26, this.f11919g, i27, this.f11922j, fArr5), this.f11922j, i24 + i28, i29, fArr6);
i25 = i28 - i20;
}
return;
}
}
}
+104
View File
@@ -0,0 +1,104 @@
package j3;
import k3.d;
/* loaded from: classes.dex */
public final class b implements Runnable {
public final /* synthetic */ int f;
/* renamed from: g, reason: collision with root package name */
public final /* synthetic */ long f11925g;
/* renamed from: h, reason: collision with root package name */
public final /* synthetic */ long f11926h;
/* renamed from: i, reason: collision with root package name */
public final /* synthetic */ long f11927i;
/* renamed from: j, reason: collision with root package name */
public final /* synthetic */ d f11928j;
/* renamed from: k, reason: collision with root package name */
public final /* synthetic */ d f11929k;
/* renamed from: l, reason: collision with root package name */
public final /* synthetic */ long f11930l;
public /* synthetic */ b(long j4, long j5, long j6, d dVar, d dVar2, long j7, int i4) {
this.f = i4;
this.f11925g = j4;
this.f11926h = j5;
this.f11927i = j6;
this.f11928j = dVar;
this.f11929k = dVar2;
this.f11930l = j7;
}
@Override // java.lang.Runnable
public final void run() {
switch (this.f) {
case 0:
long j4 = this.f11925g;
long j5 = this.f11926h;
long j6 = j4 + j5;
long j7 = this.f11927i;
while (j7 > 512) {
long j8 = j7 >> 2;
c.x(j8, j6 - j8, this.f11930l - (j7 >> 3), this.f11928j, this.f11929k);
j7 = j8;
}
c.v(j7, 1L, j6 - j7, this.f11930l, this.f11928j, this.f11929k);
long j9 = j4 - j7;
long j10 = j5 - j7;
long j11 = 0;
for (long j12 = 0; j10 > j12; j12 = 0) {
long j13 = j11 + 1;
long j14 = j10;
c.v(j7, c.F(j7, j10, j13, this.f11925g, this.f11930l, this.f11928j, this.f11929k), j9 + j14, this.f11930l, this.f11928j, this.f11929k);
j10 = j14 - j7;
j11 = j13;
j9 = j9;
}
return;
case 1:
long j15 = this.f11925g;
long j16 = this.f11926h;
long j17 = j15 + j16;
long j18 = this.f11927i;
long j19 = 1;
while (j18 > 512) {
long j20 = j18 >> 2;
j19 <<= 2;
c.z(j20, j17 - j20, this.f11930l - j20, this.f11928j, this.f11929k);
j18 = j20;
}
c.v(j18, 0L, j17 - j18, this.f11930l, this.f11928j, this.f11929k);
long j21 = j19 >> 1;
long j22 = j15 - j18;
long j23 = j16 - j18;
while (j23 > 0) {
long j24 = j21 + 1;
long j25 = j23;
j21 = j24;
c.v(j18, c.F(j18, j23, j24, this.f11925g, this.f11930l, this.f11928j, this.f11929k), j22 + j25, this.f11930l, this.f11928j, this.f11929k);
j23 = j25 - j18;
}
return;
default:
for (long j26 = this.f11925g; j26 < this.f11926h; j26++) {
this.f11928j.c(this.f11927i + j26, this.f11929k.b(this.f11930l + j26));
}
return;
}
}
public b(long j4, long j5, long j6, long j7, d dVar, d dVar2) {
this.f = 2;
this.f11925g = j4;
this.f11926h = j5;
this.f11928j = dVar;
this.f11927i = j6;
this.f11929k = dVar2;
this.f11930l = j7;
}
}
File diff suppressed because it is too large. Load diff
+11
View File
@@ -0,0 +1,11 @@
package k3;
import java.lang.Thread;
/* loaded from: classes.dex */
public final class a implements Thread.UncaughtExceptionHandler {
@Override // java.lang.Thread.UncaughtExceptionHandler
public final void uncaughtException(Thread thread, Throwable th) {
th.printStackTrace();
}
}
+25
View File
@@ -0,0 +1,25 @@
package k3;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
/* loaded from: classes.dex */
public final class b implements ThreadFactory {
/* renamed from: b, reason: collision with root package name */
public static final ThreadFactory f12091b = Executors.defaultThreadFactory();
/* renamed from: a, reason: collision with root package name */
public final a f12092a;
public b(a aVar) {
this.f12092a = aVar;
}
@Override // java.util.concurrent.ThreadFactory
public final Thread newThread(Runnable runnable) {
Thread newThread = f12091b.newThread(runnable);
newThread.setUncaughtExceptionHandler(this.f12092a);
return newThread;
}
}
+42
View File
@@ -0,0 +1,42 @@
package k3;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
/* loaded from: classes.dex */
public abstract class c {
/* renamed from: a, reason: collision with root package name */
public static final ExecutorService f12093a;
/* renamed from: b, reason: collision with root package name */
public static ExecutorService f12094b;
public static final int c;
/* renamed from: d, reason: collision with root package name */
public static final long f12095d;
/* JADX WARN: Type inference failed for: r1v0, types: [java.lang.Object, k3.a] */
static {
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool(new b(new a()));
f12093a = newCachedThreadPool;
f12094b = newCachedThreadPool;
c = Runtime.getRuntime().availableProcessors();
f12095d = 100000L;
}
public static Future a(Runnable runnable) {
if (f12094b.isShutdown() || f12094b.isTerminated()) {
f12094b = f12093a;
}
return f12094b.submit(runnable);
}
public static void b(Future[] futureArr) throws InterruptedException, java.util.concurrent.ExecutionException {
// smali: 无 catch 直接向上抛 -> Java 声明 throws(忠实表达)
for (Future future : futureArr) {
future.get();
}
}
}
+100
View File
@@ -0,0 +1,100 @@
package k3;
import com.rtbishop.look4sat.feature.cw.suncompat.Cleaner;
/* loaded from: classes.dex */
public final class d extends g {
/* renamed from: k, reason: collision with root package name */
public float[] f12096k;
public d() {
}
public d(long j4, boolean z3) {
this.f = q.f12106g;
this.f12103h = 4L;
if (j4 > 0) {
this.f12102g = j4;
if (j4 > 1073741824) {
this.f12105j = r.f12109a.allocateMemory(4 * j4);
if (z3) {
a(j4);
}
Cleaner.create(this, new f(this.f12105j, this.f12102g, this.f12103h));
K1.a.c += this.f12102g * this.f12103h;
return;
}
this.f12096k = new float[(int) j4];
return;
}
throw new IllegalArgumentException(j4 + " is not a positive long value");
}
public final float b(long j4) {
float[] fArr = this.f12096k;
long j5 = this.f12105j;
if (j5 != 0) {
return r.f12109a.getFloat((this.f12103h * j4) + j5);
}
if (this.f12104i) {
return fArr[0];
}
return fArr[(int) j4];
}
public final void c(long j4, float f) {
long j5 = this.f12105j;
if (j5 != 0) {
r.f12109a.putFloat((this.f12103h * j4) + j5, f);
} else {
if (!this.f12104i) {
this.f12096k[(int) j4] = f;
return;
}
throw new IllegalAccessError("Constant arrays cannot be modified.");
}
}
/* JADX WARN: Type inference failed for: r0v1, types: [k3.g, java.lang.Object, k3.d] */
public final Object clone() {
if (this.f12104i) {
long j4 = this.f12102g;
float b4 = b(0L);
d gVar = new d();
gVar.f = q.f12106g;
gVar.f12103h = 4L;
if (j4 > 0) {
gVar.f12102g = j4;
gVar.f12104i = true;
gVar.f12096k = new float[]{b4};
return gVar;
}
throw new IllegalArgumentException(j4 + " is not a positive long value");
}
d dVar = new d(this.f12102g, false);
r.a(0L, 0L, this.f12102g, this, dVar);
return dVar;
}
@Override // k3.g
public final boolean equals(Object obj) {
if (!super.equals(obj) || this.f12096k != ((d) obj).f12096k) {
return false;
}
return true;
}
@Override // k3.g
public final int hashCode() {
int i4;
int hashCode = super.hashCode() * 29;
float[] fArr = this.f12096k;
if (fArr != null) {
i4 = fArr.hashCode();
} else {
i4 = 0;
}
return hashCode + i4;
}
}
+79
View File
@@ -0,0 +1,79 @@
package k3;
/* loaded from: classes.dex */
public final class e implements Runnable {
public final /* synthetic */ long f;
/* renamed from: g, reason: collision with root package name */
public final /* synthetic */ long f12097g;
/* renamed from: h, reason: collision with root package name */
public final /* synthetic */ long f12098h;
/* renamed from: i, reason: collision with root package name */
public final /* synthetic */ g f12099i;
public e(g gVar, long j4, long j5, long j6) {
this.f12099i = gVar;
this.f = j4;
this.f12097g = j5;
this.f12098h = j6;
}
/* JADX WARN: Can't fix incorrect switch cases order, some code will duplicate */
/* JADX WARN: Failed to find 'out' block for switch in B:2:0x0011. Please report as an issue. */
@Override // java.lang.Runnable
public final void run() {
g gVar = this.f12099i;
int ordinal = gVar.f.ordinal();
long j4 = this.f12098h;
long j5 = this.f12097g;
long j6 = this.f;
switch (ordinal) {
case 0:
case 1:
case 2:
case 10:
case 11:
while (j6 < j5) {
r.f12109a.putByte((gVar.f12103h * j6) + j4, (byte) 0);
j6++;
}
return;
case 3:
while (j6 < j5) {
r.f12109a.putShort((gVar.f12103h * j6) + j4, (short) 0);
j6++;
}
return;
case 4:
while (j6 < j5) {
r.f12109a.putInt((gVar.f12103h * j6) + j4, 0);
j6++;
}
return;
case 5:
while (j6 < j5) {
r.f12109a.putLong((gVar.f12103h * j6) + j4, 0L);
j6++;
}
return;
case 6:
while (j6 < j5) {
r.f12109a.putFloat((gVar.f12103h * j6) + j4, 0.0f);
j6++;
}
return;
case 7:
while (j6 < j5) {
r.f12109a.putDouble((gVar.f12103h * j6) + j4, 0.0d);
j6++;
}
return;
case 8:
case 9:
default:
throw new IllegalArgumentException("Invalid array type.");
}
}
}
+32
View File
@@ -0,0 +1,32 @@
package k3;
/* loaded from: classes.dex */
public final class f implements Runnable {
public long f;
/* renamed from: g, reason: collision with root package name */
public final long f12100g;
/* renamed from: h, reason: collision with root package name */
public final long f12101h;
public f(long j4, long j5, long j6) {
this.f = j4;
this.f12100g = j5;
this.f12101h = j6;
}
@Override // java.lang.Runnable
public final void run() {
long j4 = this.f;
if (j4 != 0) {
r.f12109a.freeMemory(j4);
this.f = 0L;
long j5 = K1.a.c - (this.f12100g * this.f12101h);
K1.a.c = j5;
if (j5 < 0) {
K1.a.c = 0L;
}
}
}
}
+92
View File
@@ -0,0 +1,92 @@
package k3;
import java.io.Serializable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
/* loaded from: classes.dex */
public abstract class g implements Serializable, Cloneable {
public q f;
/* renamed from: g, reason: collision with root package name */
public long f12102g;
/* renamed from: h, reason: collision with root package name */
public long f12103h;
/* renamed from: i, reason: collision with root package name */
public boolean f12104i = false;
/* renamed from: j, reason: collision with root package name */
public long f12105j = 0;
public final void a(long j4) {
long j5;
g gVar = this;
long j6 = gVar.f12105j;
if (j6 != 0) {
long j7 = c.c;
double[][] dArr = g3.c.f11585a;
if (j4 <= j7) {
j7 = j4;
}
int i4 = (int) j7;
if (i4 > 2 && j4 >= c.f12095d) {
long j8 = j4 / i4;
Future[] futureArr = new Future[i4];
int i5 = 0;
while (i5 < i4) {
long j9 = i5 * j8;
if (i5 == i4 - 1) {
j5 = j4;
} else {
j5 = j9 + j8;
}
futureArr[i5] = c.a(new e(gVar, j9, j5, j6));
i5++;
gVar = gVar;
j6 = j6;
}
g gVar2 = gVar;
try {
c.b(futureArr);
return;
} catch (InterruptedException unused) {
r.f12109a.setMemory(gVar2.f12105j, gVar2.f12103h * j4, (byte) 0);
return;
} catch (ExecutionException unused2) {
r.f12109a.setMemory(gVar2.f12105j, j4 * gVar2.f12103h, (byte) 0);
return;
}
}
r.f12109a.setMemory(j6, gVar.f12103h * j4, (byte) 0);
}
}
public boolean equals(Object obj) {
if (obj != null && (obj instanceof g)) {
g gVar = (g) obj;
if (this.f == gVar.f && this.f12102g == gVar.f12102g && this.f12103h == gVar.f12103h && this.f12104i == gVar.f12104i && this.f12105j == gVar.f12105j) {
return true;
}
return false;
}
return false;
}
public int hashCode() {
int i4;
q qVar = this.f;
if (qVar != null) {
i4 = qVar.hashCode();
} else {
i4 = 0;
}
long j4 = this.f12102g;
int i5 = (((203 + i4) * 29) + ((int) (j4 ^ (j4 >>> 32)))) * 29;
long j5 = this.f12103h;
int i6 = (((i5 + ((int) (j5 ^ (j5 >>> 32)))) * 29) + (this.f12104i ? 1 : 0)) * 841;
long j6 = this.f12105j;
return i6 + ((int) (j6 ^ (j6 >>> 32)));
}
}
+8
View File
@@ -0,0 +1,8 @@
package k3;
/* loaded from: classes.dex */
public final class l extends q {
public l() {
super("LONG", 5);
}
}
+8
View File
@@ -0,0 +1,8 @@
package k3;
/* loaded from: classes.dex */
public final class m extends q {
public m() {
super("FLOAT", 6);
}
}
+8
View File
@@ -0,0 +1,8 @@
package k3;
/* loaded from: classes.dex */
public final class n extends q {
public n() {
super("DOUBLE", 7);
}
}
+68
View File
@@ -0,0 +1,68 @@
package k3;
/**
* R8 生成的伪 enum(k3 FFT 库的数组类型枚举)。
* smali 中 q 的直接子类:l(LONG/5)、m(FLOAT/6)、n(DOUBLE/7),
* 其余常量(BYTE/UNSIGNED_BYTE/SHORT/INT/COMPLEX_FLOAT/COMPLEX_DOUBLE/STRING/OBJECT)
* 直接以 q 实例表达。
*/
public class q {
public static final l f;
/* renamed from: g, reason: collision with root package name */
public static final m f12106g;
/* renamed from: h, reason: collision with root package name */
public static final n f12107h;
/* renamed from: i, reason: collision with root package name */
public static final /* synthetic */ q[] f12108i;
private final String name;
private final int ordinal;
protected q(String name, int ordinal) {
this.name = name;
this.ordinal = ordinal;
}
public final String name() {
return name;
}
public final int ordinal() {
return ordinal;
}
public static q valueOf(String str) {
for (q qVar : f12108i) {
if (qVar.name.equals(str)) {
return qVar;
}
}
throw new IllegalArgumentException("No enum constant k3.q." + str);
}
public static q[] values() {
return (q[]) f12108i.clone();
}
static {
q logic = new q("LOGIC", 0);
q byteType = new q("BYTE", 1);
q unsignedByte = new q("UNSIGNED_BYTE", 2);
q shortType = new q("SHORT", 3);
q intType = new q("INT", 4);
l longType = new l();
f = longType;
m floatType = new m();
f12106g = floatType;
n doubleType = new n();
f12107h = doubleType;
q complexFloat = new q("COMPLEX_FLOAT", 8);
q complexDouble = new q("COMPLEX_DOUBLE", 9);
q string = new q("STRING", 10);
q object = new q("OBJECT", 11);
f12108i = new q[]{logic, byteType, unsignedByte, shortType, intType, longType, floatType, doubleType, complexFloat, complexDouble, string, object};
}
}
+97
View File
@@ -0,0 +1,97 @@
package k3;
import java.lang.reflect.Field;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import com.rtbishop.look4sat.feature.cw.suncompat.Unsafe;
/* loaded from: classes.dex */
public abstract class r {
/* renamed from: a, reason: collision with root package name */
public static final Unsafe f12109a;
static {
Object obj = null;
try {
Class<?> cls = Class.forName("sun.misc.Unsafe");
Field declaredField = cls.getDeclaredField("theUnsafe");
declaredField.setAccessible(true);
obj = declaredField.get(cls);
} catch (ClassNotFoundException | IllegalAccessException | IllegalArgumentException | NoSuchFieldException | SecurityException ignored) {
// Android 适配: sun.misc.Unsafe 可能不可用(隐藏 API), 静默降级为 null;
// 大数组 native 路径(j4 > 1GB)不会在实际解码中触发
}
f12109a = (Unsafe) obj;
}
public static void a(long j4, long j5, long j6, d dVar, d dVar2) {
long j7;
if (j4 >= 0 && j4 < dVar.f12102g) {
if (j5 >= 0 && j5 < dVar2.f12102g) {
if (j6 >= 0) {
if (!dVar2.f12104i) {
long j8 = c.c;
double[][] dArr = g3.c.f11585a;
if (j6 <= j8) {
j8 = j6;
}
int i4 = (int) j8;
if (i4 >= 2 && j6 >= c.f12095d) {
long j9 = j6 / i4;
Future[] futureArr = new Future[i4];
int i5 = 0;
while (i5 < i4) {
long j10 = i5 * j9;
if (i5 == i4 - 1) {
j7 = j6;
} else {
j7 = j10 + j9;
}
Future[] futureArr2 = futureArr;
int i6 = i5;
futureArr2[i6] = c.a(new j3.b(j10, j7, j5, j4, dVar2, dVar));
i5 = i6 + 1;
futureArr = futureArr2;
}
try {
c.b(futureArr);
return;
} catch (InterruptedException unused) {
long j11 = j4;
long j12 = j5;
while (j11 < j4 + j6) {
dVar2.c(j12, dVar.b(j11));
j11++;
j12++;
}
return;
} catch (ExecutionException unused2) {
long j13 = j4;
long j14 = j5;
while (j13 < j4 + j6) {
dVar2.c(j14, dVar.b(j13));
j13++;
j14++;
}
return;
}
}
long j15 = j4;
long j16 = j5;
while (j15 < j4 + j6) {
dVar2.c(j16, dVar.b(j15));
j15++;
j16++;
}
return;
}
throw new IllegalArgumentException("Constant arrays cannot be modified.");
}
throw new IllegalArgumentException("length < 0");
}
throw new ArrayIndexOutOfBoundsException("destPos < 0 || destPos >= dest.length()");
}
throw new ArrayIndexOutOfBoundsException("srcPos < 0 || srcPos >= src.length()");
}
}
+167
View File
@@ -0,0 +1,167 @@
package k3;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import com.rtbishop.look4sat.feature.cw.suncompat.Cleaner;
import com.rtbishop.look4sat.feature.cw.suncompat.Unsafe;
/* loaded from: classes.dex */
public final class s extends g {
/* renamed from: k, reason: collision with root package name */
public long[] f12110k;
public s() {
}
public s(long j4, boolean z3) {
this.f = q.f;
this.f12103h = 8L;
if (j4 > 0) {
this.f12102g = j4;
if (j4 > 1073741824) {
this.f12105j = r.f12109a.allocateMemory(8 * j4);
if (z3) {
a(j4);
}
Cleaner.create(this, new f(this.f12105j, this.f12102g, this.f12103h));
K1.a.c += this.f12102g * this.f12103h;
return;
}
this.f12110k = new long[(int) j4];
return;
}
throw new IllegalArgumentException(j4 + " is not a positive long value");
}
public final long b(long j4) {
long[] jArr = this.f12110k;
long j5 = this.f12105j;
if (j5 != 0) {
return r.f12109a.getLong((this.f12103h * j4) + j5);
}
if (this.f12104i) {
return jArr[0];
}
return jArr[(int) j4];
}
public final void c(long j4, long j5) {
long j6 = this.f12105j;
if (j6 != 0) {
r.f12109a.putLong((this.f12103h * j4) + j6, j5);
} else {
if (!this.f12104i) {
this.f12110k[(int) j4] = j5;
return;
}
throw new IllegalAccessError("Constant arrays cannot be modified.");
}
}
/* JADX WARN: Type inference failed for: r0v29, types: [k3.g, java.lang.Object, k3.s] */
public final Object clone() {
long j4;
int i4 = 0;
long j5 = 0;
if (this.f12104i) {
long j6 = this.f12102g;
long b4 = b(0L);
s gVar = new s();
gVar.f = q.f12107h;
gVar.f12103h = 8L;
if (j6 > 0) {
gVar.f12102g = j6;
gVar.f12104i = true;
gVar.f12110k = new long[]{b4};
return gVar;
}
throw new IllegalArgumentException(j6 + " is not a positive long value");
}
s sVar = new s(this.f12102g, false);
long j7 = this.f12102g;
Unsafe unsafe = r.f12109a;
if (0 < j7) {
if (0 < sVar.f12102g) {
if (j7 >= 0) {
if (!sVar.f12104i) {
long j8 = c.c;
double[][] dArr = g3.c.f11585a;
if (j7 <= j8) {
j8 = j7;
}
int i5 = (int) j8;
if (i5 >= 2 && j7 >= c.f12095d) {
long j9 = j7 / i5;
Future[] futureArr = new Future[i5];
while (i4 < i5) {
long j10 = i4 * j9;
if (i4 == i5 - 1) {
j4 = j7;
} else {
j4 = j10 + j9;
}
Future[] futureArr2 = futureArr;
int i6 = i4;
futureArr2[i6] = c.a(new i3.c(j10, j4, sVar, this));
i4 = i6 + 1;
futureArr = futureArr2;
}
try {
c.b(futureArr);
return sVar;
} catch (InterruptedException unused) {
long j11 = 0;
while (j5 < j7) {
sVar.c(j11, b(j5));
j5++;
j11++;
}
} catch (ExecutionException unused2) {
long j12 = 0;
while (j5 < j7) {
sVar.c(j12, b(j5));
j5++;
j12++;
}
}
} else {
long j13 = 0;
while (j5 < j7) {
sVar.c(j13, b(j5));
j5++;
j13++;
}
}
return sVar;
}
throw new IllegalArgumentException("Constant arrays cannot be modified.");
}
throw new IllegalArgumentException("length < 0");
}
throw new ArrayIndexOutOfBoundsException("destPos < 0 || destPos >= dest.length()");
}
throw new ArrayIndexOutOfBoundsException("srcPos < 0 || srcPos >= src.length()");
}
@Override // k3.g
public final boolean equals(Object obj) {
if (!super.equals(obj) || this.f12110k != ((s) obj).f12110k) {
return false;
}
return true;
}
@Override // k3.g
public final int hashCode() {
int i4;
int hashCode = super.hashCode() * 29;
long[] jArr = this.f12110k;
if (jArr != null) {
i4 = jArr.hashCode();
} else {
i4 = 0;
}
return hashCode + i4;
}
}
+144
View File
@@ -0,0 +1,144 @@
package pas;
import pas.system;
/* loaded from: classes.dex */
public class cwstru {
/* loaded from: classes.dex */
public static class TMorseCharInfo extends system.Record {
public TMorseCharInfo(long j4, boolean z3) {
__Init(j4, z3);
}
private native void __Destroy(long j4);
@Override // pas.system.PascalObjectEx
public void __Release() {
__Destroy(this._pasobj);
this._pasobj = 0L;
}
@Override // pas.system.Record
public int __Size() {
return __Size(0);
}
public native char getCh();
public native int getIdxE();
public native boolean getLowSnr();
public native TWordEndType getWordEnd();
public native float getWordSpaceProb();
public native TMorseWordType getWordType();
public native void setCh(char c);
public native void setIdxE(int i4);
public native void setLowSnr(boolean z3);
public native void setWordEnd(TWordEndType tWordEndType);
public native void setWordSpaceProb(float f);
public native void setWordType(TMorseWordType tMorseWordType);
public TMorseCharInfo() {
__Init(0L, true);
}
public TMorseCharInfo(system.PascalObject pascalObject) {
super(pascalObject);
}
public TMorseCharInfo(long j4) {
super(j4);
}
}
/* loaded from: classes.dex */
public static class TMorseWordType extends system.Enum {
public static final int wtCall = 5;
public static final int wtCq = 3;
public static final int wtGenWord = 0;
public static final int wtKeyword = 1;
public static final int wtQrl = 4;
public static final int wtRst = 2;
public TMorseWordType(int i4) {
this.Value = i4;
}
public static TMorseWordType wtCall() {
return new TMorseWordType(5);
}
public static TMorseWordType wtCq() {
return new TMorseWordType(3);
}
public static TMorseWordType wtGenWord() {
return new TMorseWordType(0);
}
public static TMorseWordType wtKeyword() {
return new TMorseWordType(1);
}
public static TMorseWordType wtQrl() {
return new TMorseWordType(4);
}
public static TMorseWordType wtRst() {
return new TMorseWordType(2);
}
@Override // pas.system.Enum
public boolean equals(Object obj) {
if (((obj instanceof TMorseWordType) && this.Value == ((TMorseWordType) obj).Value) || super.equals(obj)) {
return true;
}
return false;
}
}
/* loaded from: classes.dex */
public static class TWordEndType extends system.Enum {
public static final int weLong = 2;
public static final int weNone = 0;
public static final int weShort = 1;
public TWordEndType(int i4) {
this.Value = i4;
}
public static TWordEndType weLong() {
return new TWordEndType(2);
}
public static TWordEndType weNone() {
return new TWordEndType(0);
}
public static TWordEndType weShort() {
return new TWordEndType(1);
}
@Override // pas.system.Enum
public boolean equals(Object obj) {
if (((obj instanceof TWordEndType) && this.Value == ((TWordEndType) obj).Value) || super.equals(obj)) {
return true;
}
return false;
}
}
static {
system.InitJni();
}
}
+171
View File
@@ -0,0 +1,171 @@
package pas;
import pas.cwstru;
import pas.system;
/* loaded from: classes.dex */
public class decoder {
/* loaded from: classes.dex */
public static class TCharInfo extends system.TObject {
public TCharInfo(system.PascalObject pascalObject) {
super(pascalObject);
}
public static TCharInfo Class() {
return new TCharInfo(system.GetClassRef(1));
}
public static native TCharInfo Create();
public static native TCharInfo Create(char c);
public static native TCharInfo Create(cwstru.TMorseCharInfo tMorseCharInfo);
public static system.TClass TClass() {
return system.GetTClass(1);
}
public native char getChar();
public native boolean getLowSnr();
public native TMorseWordType getWordType();
public TCharInfo(long j4) {
super(j4);
}
}
/* loaded from: classes.dex */
public static class TDecoder extends system.TObject {
public TDecoder(system.PascalObject pascalObject) {
super(pascalObject);
}
public static TDecoder Class() {
return new TDecoder(system.GetClassRef(2));
}
public static native TDecoder Create();
public static system.TClass TClass() {
return system.GetTClass(2);
}
public native TCharInfo GetChar(int i4);
public native void LockToPitch(int i4);
public native int getCharCount();
public native int getDeleteCount();
public native boolean getHamMode();
public native int getPitch();
public native int getSignalCount();
public native int getSnr();
public native int getWpm();
public native void setHamMode(boolean z3);
public TDecoder(long j4) {
super(j4);
}
}
/* loaded from: classes.dex */
public static class TDecoderState extends system.Enum {
public static final int dsDecoding = 2;
public static final int dsLockedDecoding = 4;
public static final int dsLockedPause = 3;
public static final int dsNone = 0;
public static final int dsPause = 1;
public TDecoderState(int i4) {
this.Value = i4;
}
public static TDecoderState dsDecoding() {
return new TDecoderState(2);
}
public static TDecoderState dsLockedDecoding() {
return new TDecoderState(4);
}
public static TDecoderState dsLockedPause() {
return new TDecoderState(3);
}
public static TDecoderState dsNone() {
return new TDecoderState(0);
}
public static TDecoderState dsPause() {
return new TDecoderState(1);
}
@Override // pas.system.Enum
public boolean equals(Object obj) {
if (((obj instanceof TDecoderState) && this.Value == ((TDecoderState) obj).Value) || super.equals(obj)) {
return true;
}
return false;
}
}
/* loaded from: classes.dex */
public static class TMorseWordType extends system.Enum {
public static final int wtCall = 5;
public static final int wtCq = 3;
public static final int wtGenWord = 0;
public static final int wtKeyword = 1;
public static final int wtQrl = 4;
public static final int wtRst = 2;
public TMorseWordType(int i4) {
this.Value = i4;
}
public static TMorseWordType wtCall() {
return new TMorseWordType(5);
}
public static TMorseWordType wtCq() {
return new TMorseWordType(3);
}
public static TMorseWordType wtGenWord() {
return new TMorseWordType(0);
}
public static TMorseWordType wtKeyword() {
return new TMorseWordType(1);
}
public static TMorseWordType wtQrl() {
return new TMorseWordType(4);
}
public static TMorseWordType wtRst() {
return new TMorseWordType(2);
}
@Override // pas.system.Enum
public boolean equals(Object obj) {
if (((obj instanceof TMorseWordType) && this.Value == ((TMorseWordType) obj).Value) || super.equals(obj)) {
return true;
}
return false;
}
}
static {
system.InitJni();
}
}
@@ -0,0 +1,35 @@
package pas;
import pas.decoder;
import pas.system;
/* loaded from: classes.dex */
public class nativedecoder {
/* loaded from: classes.dex */
public static class TNativeDecoder extends decoder.TDecoder {
public TNativeDecoder(system.PascalObject pascalObject) {
super(pascalObject);
}
public static TNativeDecoder Class() {
return new TNativeDecoder(system.GetClassRef(3));
}
public static native TNativeDecoder Create();
public static system.TClass TClass() {
return system.GetTClass(3);
}
public native decoder.TDecoderState ProcessSpectrum(Object obj);
public TNativeDecoder(long j4) {
super(j4);
}
}
static {
system.InitJni();
}
}
+385
View File
@@ -0,0 +1,385 @@
package pas;
import java.util.Date;
import java.util.TimeZone;
/* loaded from: classes.dex */
public class system {
private static boolean _JniLibLoaded = false;
/* loaded from: classes.dex */
public static abstract class BaseSet {
protected int Value = 0;
public abstract int Base();
public abstract int ElMax();
public abstract byte Size();
}
/* loaded from: classes.dex */
public static class Enum {
public int Value;
public int Ord() {
return this.Value;
}
public boolean equals(Object obj) {
return (obj instanceof Integer) && this.Value == ((Integer) obj).intValue();
}
public int hashCode() {
return this.Value;
}
public boolean equals(int i4) {
return this.Value == i4;
}
}
/* loaded from: classes.dex */
public static class MethodPtr extends PascalObjectEx {
protected String mName;
protected Object mObject;
protected String mSignature;
public MethodPtr() {
this._cleanup = true;
this._pasobj = -1L;
}
private native void __Destroy();
private native void __Init(Object obj, String str, String str2, boolean z3);
public void Init() {
boolean z3;
Object obj = this.mObject;
String str = this.mName;
String str2 = this.mSignature;
if (this != obj) {
z3 = true;
} else {
z3 = false;
}
__Init(obj, str, str2, z3);
}
@Override // pas.system.PascalObjectEx
public void __Release() {
if (this._pasobj > 0) {
__Destroy();
}
}
}
/* loaded from: classes.dex */
public static abstract class PascalInterface extends PascalObjectEx {
public PascalInterface(long j4, boolean z3) {
this._pasobj = j4;
__Init();
}
public abstract void __Init();
public void __TypeCast(PascalObject pascalObject, String str) {
if (pascalObject != null) {
if (pascalObject instanceof PascalInterface) {
this._pasobj = pascalObject._pasobj;
__Init();
} else {
this._pasobj = system.InterfaceCast(pascalObject._pasobj, str);
}
}
}
}
/* loaded from: classes.dex */
public static class Record extends PascalObjectEx {
protected PascalObject _objref;
public Record(PascalObject pascalObject) {
super(pascalObject);
this._objref = pascalObject;
}
public void __Init(long j4, boolean z3) {
this._pasobj = j4;
this._cleanup = z3;
if (j4 == 0 && __Size() != 0) {
this._pasobj = system.AllocMemory(__Size());
}
}
public int __Size() {
return 0;
}
@Override // pas.system.PascalObjectEx, pas.system.PascalObject
public void finalize() {
long j4 = this._pasobj;
if (j4 < 0) {
this._pasobj = -j4;
this._cleanup = true;
}
super.finalize();
}
public Record(long j4) {
super(j4);
}
public final int __Size(int i4) {
return system.GetRecordSize(i4);
}
public Record() {
}
}
/* loaded from: classes.dex */
public static abstract class Set<TS extends BaseSet, TE extends Enum> extends BaseSet {
public Set() {
}
public void Assign(TS ts) {
this.Value = ts.Value;
}
public void Exclude(TE... teArr) {
for (TE te : teArr) {
this.Value = (~GetMask(te)) & this.Value;
}
}
public int GetMask(TE te) {
return 1 << (te.Ord() - Base());
}
public boolean Has(TE te) {
if ((GetMask(te) & this.Value) != 0) {
return true;
}
return false;
}
public void Include(TE... teArr) {
for (TE te : teArr) {
this.Value = GetMask(te) | this.Value;
}
}
public void Intersect(TS ts) {
this.Value = ts.Value & this.Value;
}
public boolean IsEmpty() {
if (this.Value == 0) {
return true;
}
return false;
}
public boolean equals(TS ts) {
return this.Value == ts.Value;
}
public Set(TE... teArr) {
Include(teArr);
}
public void Exclude(TS ts) {
this.Value = (~ts.Value) & this.Value;
}
public void Include(TS ts) {
this.Value = ts.Value | this.Value;
}
public boolean equals(TE te) {
return this.Value == te.Ord();
}
public Set(TS... tsArr) {
for (TS ts : tsArr) {
Include(ts); // smali: Set(TS...) 循环调 Include(BaseSet)
}
}
public boolean equals(int i4) {
return this.Value == i4;
}
}
/* loaded from: classes.dex */
public static class TClass extends TObject {
public TClass(PascalObject pascalObject) {
super(pascalObject);
}
}
/* loaded from: classes.dex */
public static class TDateTime {
public static double get(Date date) {
return ((date.getTime() + TimeZone.getDefault().getOffset(date.getTime())) / 8.64E7d) + 25569.0d;
}
public static double getUTC(Date date) {
return (date.getTime() / 8.64E7d) + 25569.0d;
}
public static Date toDate(double d4) {
return new Date(Math.round((d4 - 25569.0d) * 8.64E7d) - TimeZone.getDefault().getOffset(Math.round((d4 - 25569.0d) * 8.64E7d))); // r3 = 前值(smali)
}
public static Date toDateUTC(double d4) {
return new Date(Math.round((d4 - 25569.0d) * 8.64E7d));
}
}
/* loaded from: classes.dex */
public static class TObject extends PascalObject {
public TObject(PascalObject pascalObject) {
super(pascalObject);
}
public static TObject Class() {
return new TObject(system.GetClassRef(0));
}
public static native TObject Create();
public static TClass TClass() {
return system.GetTClass(0);
}
public native String ClassName();
public native boolean ClassNameIs(String str);
public native TClass ClassParent();
public native TClass ClassType();
public native void Destroy();
public native void Free();
public native boolean InheritsFrom(TClass tClass);
public native int InstanceSize();
public TObject(long j4) {
super(j4);
}
}
static {
InitJni();
}
public static native long AllocMemory(int i4);
public static native long GetClassRef(int i4);
public static native byte[] GetMemoryAsArray(long j4, int i4);
/* JADX INFO: Access modifiers changed from: private */
public static native int GetRecordSize(int i4);
public static TClass GetTClass(int i4) {
TClass tClass = new TClass(null);
tClass._pasobj = GetClassRef(i4);
return tClass;
}
public static void InitJni() {
if (!_JniLibLoaded) {
_JniLibLoaded = true;
System.loadLibrary("nativedecoderjni");
}
}
/* JADX INFO: Access modifiers changed from: private */
public static native long InterfaceCast(long j4, String str);
public static long Pointer(PascalObject pascalObject) {
if (pascalObject == null) {
return 0L;
}
return pascalObject._pasobj;
}
public static native void SetMemoryFromArray(long j4, byte[] bArr);
/* loaded from: classes.dex */
public static class PascalObject {
protected long _pasobj;
static {
system.InitJni();
}
public PascalObject() {
this._pasobj = 0L;
}
public boolean equals(Object obj) {
if ((obj instanceof PascalObject) && this._pasobj == ((PascalObject) obj)._pasobj) {
return true;
}
return false;
}
public void finalize() {
}
public int hashCode() {
return (int) this._pasobj;
}
public PascalObject(PascalObject pascalObject) {
this._pasobj = 0L;
if (pascalObject != null) {
this._pasobj = pascalObject._pasobj;
}
}
public PascalObject(long j4) {
this._pasobj = j4;
}
}
/* loaded from: classes.dex */
public static class PascalObjectEx extends PascalObject {
protected boolean _cleanup;
public PascalObjectEx() {
this._cleanup = false;
}
public void __Release() {
this._pasobj = 0L;
}
@Override // pas.system.PascalObject
public void finalize() {
if (this._cleanup) {
__Release();
}
super.finalize();
}
public PascalObjectEx(PascalObject pascalObject) {
super(pascalObject);
this._cleanup = false;
}
public PascalObjectEx(long j4) {
super(j4);
this._cleanup = false;
}
}
}
@@ -0,0 +1,16 @@
package s;
/* renamed from: s.e, reason: case insensitive filesystem */
/* loaded from: classes.dex */
public abstract /* synthetic */ class AbstractC1880e {
/* renamed from: a, reason: collision with root package name */
public static final /* synthetic */ int[] f13163a = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28};
public static /* synthetic */ int a(int i4) {
if (i4 != 0) {
return i4 - 1;
}
throw null;
}
}
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="?attr/colorControlNormal"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M6,19c0,1.1 0.9,2 2,2h8c1.1,0 2,-0.9 2,-2V7H6v12zM19,4h-3.5l-1,-1h-5l-1,1H5v2h14V4z"/>
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="?attr/colorControlNormal"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M6,19h4L10,5L6,5v14zM14,5v14h4L18,5h-4z"/>
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="?attr/colorControlNormal"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M17,3L5,3c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2L21,7l-4,-4zM12,19c-1.66,0 -3,-1.34 -3,-3s1.34,-3 3,-3 3,1.34 3,3 -1.34,3 -3,3zM15,9L5,9L5,5h10v4z"/>
</vector>
@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:tint="?attr/colorControlNormal"
android:height="24dp"
android:width="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M18,16.08c-0.76,0 -1.44,0.3 -1.96,0.77L8.91,12.7c0.05,-0.23 0.09,-0.46 0.09,-0.7s-0.04,-0.47 -0.09,-0.7l7.05,-4.11c0.54,0.5 1.25,0.81 2.04,0.81 1.66,0 3,-1.34 3,-3s-1.34,-3 -3,-3 -3,1.34 -3,3c0,0.24 0.04,0.47 0.09,0.7L8.04,9.81C7.5,9.31 6.79,9 6,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3c0.79,0 1.5,-0.31 2.04,-0.81l7.12,4.16c-0.05,0.21 -0.08,0.43 -0.08,0.65 0,1.61 1.31,2.92 2.92,2.92 1.61,0 2.92,-1.31 2.92,-2.92s-1.31,-2.92 -2.92,-2.92z"/>
</vector>
@@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
android:fitsSystemWindows="true"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:id="@+id/verticalLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true">
<TextView
android:textColor="#ffffff"
android:id="@+id/statusTextView"
android:background="@color/purple_500"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/idle"/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="100dp">
<com.ve3nea.morse_expert.WaterfallSurfaceView
android:id="@+id/waterfallView"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:contentDescription="@string/waterfall_display"/>
<com.ve3nea.morse_expert.ScaleView
android:id="@+id/scaleView"
android:background="#24545a"
android:layout_width="12dp"
android:layout_height="match_parent"
android:layout_weight="0"
android:contentDescription="@string/frequency_scale"/>
</LinearLayout>
<com.ve3nea.morse_expert.DecodedTextView
android:textSize="18sp"
android:typeface="monospace"
android:textColor="#444444"
android:textColorHint="#888888"
android:gravity="start|top"
android:id="@+id/decodedTextView"
android:background="#d1f0f4"
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="0dp"
android:hint="@string/decoded_text"
android:freezesText="false"
android:layout_weight="1"
android:inputType="none"
android:fadeScrollbars="false"
android:textIsSelectable="true"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,59 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:orientation="vertical"
android:id="@+id/verticalLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:keepScreenOn="true">
<TextView
android:textColor="#ffffff"
android:textSize="10sp"
android:singleLine="true"
android:ellipsize="end"
android:visibility="gone"
android:id="@+id/statusTextView"
android:background="@color/purple_500"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/idle"/>
<LinearLayout
android:orientation="horizontal"
android:layout_width="match_parent"
android:layout_height="80dp">
<com.ve3nea.morse_expert.WaterfallSurfaceView
android:id="@+id/waterfallView"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:contentDescription="@string/waterfall_display"/>
<com.ve3nea.morse_expert.ScaleView
android:id="@+id/scaleView"
android:background="#24545a"
android:layout_width="8dp"
android:layout_height="match_parent"
android:layout_weight="0"
android:contentDescription="@string/frequency_scale"/>
</LinearLayout>
<com.ve3nea.morse_expert.DecodedTextView
android:textSize="12sp"
android:typeface="monospace"
android:textColor="#444444"
android:textColorHint="#888888"
android:gravity="start|top"
android:id="@+id/decodedTextView"
android:background="#d1f0f4"
android:scrollbars="vertical"
android:layout_width="match_parent"
android:layout_height="0dp"
android:hint="@string/decoded_text"
android:freezesText="false"
android:layout_weight="1"
android:minHeight="48dp"
android:inputType="none"
android:fadeScrollbars="false"
android:textIsSelectable="true"/>
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
@@ -0,0 +1,33 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:icon="@drawable/ic_baseline_pause_24"
android:id="@+id/pause_mnu"
android:title="@string/pause"
app:showAsAction="ifRoom"/>
<item
android:icon="@drawable/ic_baseline_delete_24"
android:id="@+id/clear_mnu"
android:title="@string/clear"
app:showAsAction="ifRoom"/>
<item
android:icon="@drawable/ic_baseline_save_24"
android:id="@+id/save_mnu"
android:title="@string/save_text"
app:showAsAction="ifRoom"/>
<item
android:id="@+id/record_signals_mnu"
android:title="@string/record_signals"/>
<item
android:id="@+id/settings_mnu"
android:title="@string/settings"/>
<item
android:id="@+id/help_mnu"
android:title="@string/help"/>
<item
android:id="@+id/rate_mnu"
android:title="@string/rate"/>
<item
android:id="@+id/premium_mnu"
android:title="@string/premium"/>
</menu>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_500">#ff6200ee</color>
<integer name="default_font_size">18</integer>
<string name="idle">Diam</string>
<string name="waterfall_display">Tampilan waterfall</string>
<string name="frequency_scale">Skala frekuensi</string>
<string name="decoded_text">Teks terdekode</string>
<string name="clear">Bersihkan</string>
<string name="pause">Jeda Dekode</string>
<string name="save_text">Simpan Teks</string>
<string name="record_signals">Rekam Sinyal</string>
<string name="settings">Pengaturan</string>
<string name="help">Bantuan Daring</string>
<string name="rate">Nilai Aplikasi Ini</string>
<string name="premium">Dapatkan Versi Premium</string>
<string name="tap_back_again_to_close">Ketuk Kembali lagi untuk menutup aplikasi</string>
<string name="message_type">Tipe Pesan</string>
<string name="cw_mic_permission">Izin mikrofon diperlukan untuk dekode CW</string>
<string name="cw_grant_permission">Beri izin</string>
<string name="cw_open_settings">Buka pengaturan aplikasi</string>
</resources>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_500">#ff6200ee</color>
<integer name="default_font_size">18</integer>
<string name="idle">Diam</string>
<string name="waterfall_display">Tampilan waterfall</string>
<string name="frequency_scale">Skala frekuensi</string>
<string name="decoded_text">Teks terdekode</string>
<string name="clear">Bersihkan</string>
<string name="pause">Jeda Dekode</string>
<string name="save_text">Simpan Teks</string>
<string name="record_signals">Rekam Sinyal</string>
<string name="settings">Pengaturan</string>
<string name="help">Bantuan Daring</string>
<string name="rate">Nilai Aplikasi Ini</string>
<string name="premium">Dapatkan Versi Premium</string>
<string name="tap_back_again_to_close">Ketuk Kembali lagi untuk menutup aplikasi</string>
<string name="message_type">Tipe Pesan</string>
<string name="cw_mic_permission">Izin mikrofon diperlukan untuk dekode CW</string>
<string name="cw_grant_permission">Beri izin</string>
<string name="cw_open_settings">Buka pengaturan aplikasi</string>
</resources>
@@ -0,0 +1,22 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_500">#ff6200ee</color>
<integer name="default_font_size">18</integer>
<string name="idle">Diam</string>
<string name="waterfall_display">Tampilan waterfall</string>
<string name="frequency_scale">Skala frekuensi</string>
<string name="decoded_text">Teks terdekode</string>
<string name="clear">Bersihkan</string>
<string name="pause">Jeda Dekode</string>
<string name="save_text">Simpan Teks</string>
<string name="record_signals">Rekam Sinyal</string>
<string name="settings">Pengaturan</string>
<string name="help">Bantuan Daring</string>
<string name="rate">Nilai Aplikasi Ini</string>
<string name="premium">Dapatkan Versi Premium</string>
<string name="tap_back_again_to_close">Ketuk Kembali lagi untuk menutup aplikasi</string>
<string name="message_type">Tipe Pesan</string>
<string name="cw_mic_permission">Izin mikrofon diperlukan untuk dekode CW</string>
<string name="cw_grant_permission">Beri izin</string>
<string name="cw_open_settings">Buka pengaturan aplikasi</string>
</resources>
Loaded 100 of 119 files, more files were not shown because too many files have changed in this diff. Show more