Compare commits

...
44 Commits
Author SHA1 Message Date
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
mckero 9a4ceddcb0 fix(settings): force custom-source toggles off when legacy example.com URL present
Old installs that once enabled the custom TLE/transceiver toggles kept
the legacy "https://example.com/tle.txt" placeholder in preferences.
Under the new "toggle replaces the online-update default source"
semantics this pointed the All/SatNOGS fetch at a dead URL and broke
satellite updates after upgrading to 4.4.8.

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

Verified: core:data tests pass, settings + app compile. Version stays
4.4.8 (覆盖 per user).
2026-08-03 17:03:48 +00:00
mckero 50233cadc1 fix(drawable): remove unsupported colorControlNormal tint from ic_reset
Pure-Compose project has no attr/colorControlNormal; aapt fails at
release resource linking. Icon composable applies its own tint.
2026-08-03 16:48:02 +00:00
mckero 8d88100b15 feat(settings): custom URL now overrides the online-update default source
The "Custom URL" dialog's two URL fields now control the default
source used by the online update, per the user's intent (one-time
setup, no extra steps per update):

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

Test updated: custom TLE URL data now lands under the "All" type
instead of "Other". Verified: core:data tests pass, settings + app
compile. Version stays 4.4.8 (覆盖 per user).
2026-08-03 16:41:14 +00:00
mckero 59d6e2ae05 fix(strings): escape apostrophe in tr whatsnew entry
"URL'leri" in the new Turkish whatsnew line broke aapt again
(Invalid unicode escape sequence). Escape the apostrophe as \'.
2026-08-03 16:22:23 +00:00
mckero e2b5c6d673 feat(settings): rename Import button to Custom URL; bump to 4.4.8
The online update always pulls every preset Celestrak/amsat URL and
the custom URL lived hidden behind the "Import" dialog's toggle. The
user wants a discoverable entry point to edit the custom source URLs.

Rename the data card button "Import" -> "Custom URL" (en: Custom URL,
zh: 自定义URL, tr: Özel URL). The existing dialog already hosts both
URL text fields (TLE + transceivers) and the file-import buttons, so
no dialog changes are needed - the import feature stays inside, as
the user requested.

Version bump per fork convention (feature change): 4.4.7 -> 4.4.8,
versionCode 447 -> 448. v4.4.7 release stays downloadable.

What's-new dialog updated in all three locales with this change.

Verified: settings + app compile, unit tests unaffected.
2026-08-03 16:15:02 +00:00
mckero da89d6426f fix(strings): escape apostrophe in tr whatsnew text
A bare apostrophe in "QTH定位器 2.0'dan" broke aapt resource
compilation (Invalid unicode escape sequence) and failed the release
build. Escape it as \' per Android string resource rules.
2026-08-03 15:52:57 +00:00
mckero c19209655b fix(roaming,settings): theme colors for night mode; unclamp cards; brand title and APK name
Three detail fixes plus branding, per user review:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

RoamingState.kt removed - state and math now live in RoamingScreen.kt.
2026-08-03 14:29:44 +00:00
mckero 88d1498eac fix(roaming): red marker tracks fix in BOTH axes via 3rd-pair lookup
The marker's vertical position was hardcoded (10dp below the label) — markerY never participated, so the dot could only move horizontally and could not reflect where the GPS fix sits inside the 4-char square.

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

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

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

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

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

The grid proportions, locator algorithm, marker mapping and boundary logic were already faithful; this change makes the visual structure faithful too.
2026-08-03 12:32:45 +00:00
mckero 97c6d609ac fix(roaming): drop auto GPS polling, show station position directly
Reverts the auto-update machinery after review — the page now simply mirrors the station position (站位) from the shared settingsRepo.stationPosition StateFlow, exactly what the Settings page shows:

- RoamingScreen: removed the LocationManager listeners, the 30s re-request loop, the provider filter and the location-disabled hint. No polling, no auto-updates; coordinates are whatever the station GPS says.
- Settings: removed the '漫游位置实时更新' toggle (stateOfRoamingLive, key, action, strings en/zh/tr) that caused the 'Other' card to overflow — the sixth unlabeled switch clipped past the card's rounded bottom edge was that row overflowing a fixed-height card. Card height back to 268.dp, five rows fit again.

Verified: core:domain tests, roaming/settings/app compile clean; zero references to RoamingLive remain.
2026-08-03 12:24:07 +00:00
mckero 44c442ddd3 feat(roaming): port live GPS tracking and location-disabled hint
Completes the port of the QTH定位器 app's location logic (read from the decompiled MainActivity):

1. Live location updates: a LocationListener registers GPS+NETWORK providers (10s / 10m, matching the reference onResume) while the Roaming page is visible and removes itself on dispose. Every fix is pushed through settingsRepo.setStationPosition, so the shared stationPosition StateFlow updates the Settings page and the map in lockstep — the page now refreshes in real time instead of only showing stale cached coordinates.

2. Provider filtering: only gps/network fixes are accepted, mirroring the reference showLocation() guard that rejects passive fixes.

3. Location-disabled hint: when GPS is off or permission is missing, the header shows a tappable '定位未开启,点击前往系统设置' row that opens ACTION_LOCATION_SOURCE_SETTINGS — the port of the reference btnLocationSettings button. The GPS status dot now has three states: fresh fix (primary), provider on but stale (error), provider off (outline).

4. Periodic recovery: a 30s re-request loop (honoring the 漫游位置实时更新 toggle) re-arms the location request after the chip falls idle.

Not ported (conflict, deliberate): the reference app's Play-store ad banner, 'New! Grid Square with map' promo and GP_IN preferences — commercial advertising does not belong in a GPL satellite tracker.
2026-08-03 11:59:31 +00:00
mckero f9250ec1f1 fix(roaming): live coords from shared flow, marker by locator pair, versioned apk
Addresses three review findings:

1. Coordinates now come straight from settingsRepo.stationPosition in the screen (collectAsStateWithLifecycle) — the exact same StateFlow the Settings page shows. Previously a separate ViewModel re-derived them, and it could lag behind the Settings page (user: '设置页更新了站位但漫游页死活不更新'). With the shared source the two pages can never disagree. RoamingViewModel removed; state derivation moved to RoamingState.fromPosition().

2. Red marker placement ported faithfully from the QTH定位器 app: it is driven by the 3rd character pair of the 8-char locator (the 'ih' in OL42ih45), mapped to a 0..1 fraction (lon a=west..x=east, lat inverted a=south..x=north), then scaled to the actual center-cell size. The grid now uses the reference proportions (columns 21.4/56.2/21.4, rows 31.5/35.9/31.7) and fills the screen, so the marker lands accurately on any device.

3. Workflow now uploads a versioned APK (look4sat-<version>.apk instead of look4sat.apk).

Also: Settings 'Other' card rows got vertical spacing (Arrangement.spacedBy) so the new roaming toggle is not glued to the night-mode row.
2026-08-03 11:49:46 +00:00
mckero bff3e1af09 chore: bump version to 4.4.7
Next release: versionCode 446 -> 447, versionName 4.4.6 -> 4.4.7. Includes the reworked Roaming page (faithful QTH定位器 port, live-update toggle, night-mode-safe theme colors).
2026-08-03 11:27:25 +00:00
mckero 6dbe30b3f6 feat(roaming): faithful QTH定位器 port, live-update toggle, night-mode safe
Rework the Roaming page after user review. It is now a faithful port of the QTH定位器 location panel, not a loose re-skin:

UI (matching the reference layout):
- GPS status dot (real: green when a fresh fix exists, outline when stale/missing) + date + time header
- Lat/Lon rows with DMS and 5-decimal display, big 8-char locator centered below
- 3x3 grid of neighboring 4-char squares with the reference proportions: center column ~2.6x wider (21.4% : 56.2% : 21.4%) and center row the tallest (31.5% : 35.9% : 31.7%); red position marker now placed at the fractional position of the fix inside the center cell (was fixed center)
- No oversized GPS button: live updating is now a Settings toggle '漫游位置实时更新' (stateOfRoamingLive, default on) that drives periodic location refresh

Style & night-mode safety:
- All colors come from MaterialTheme.colorScheme (surfaceVariant/secondaryContainer/error) — no hardcoded cyan/blue from the original app. The red night filter (ColorMatrix keeping only the R channel) blanked the old hardcoded palette; theme colors survive it.
- Chinese nav label '漫游' added to values-zh (was missing, showing English 'Roaming')

Settings:
- OtherSettings.stateOfRoamingLive persisted (default true), toggle row in Other card, height adjusted

Verified: feature:roaming, feature:settings, app compile clean.
2026-08-03 11:18:35 +00:00
mckero 8a920acead build: revert gradle heap cap, keep CI builds unconstrained
The 768m heap cap broke GitHub Actions (KSP OutOfMemoryError: Metaspace) because CI runners read the same gradle.properties. Restore -Xmx6g for CI; the 2GB local server should pass a one-off -Dorg.gradle.jvmargs instead.
2026-08-03 10:49:59 +00:00
mckero 929f652b8e chore: bump version to 4.4.6
Next release: versionCode 445 -> 446, versionName 4.4.5 -> 4.4.6. Adds the Roaming page (QTH定位器-style 3x3 Maidenhead grid) between Match and Settings, credits BG7NTA & the original author in the thanks title, and caps local Gradle heap for the 2GB build server.
2026-08-03 10:47:39 +00:00
mckero 610768501e build: add roaming module gradle file, cap local JVM heap
Include feature:roaming/build.gradle.kts (missed from the module commit) and keep the reduced Gradle heap (-Xmx768m) so local light builds don't freeze the 2GB server.
2026-08-03 10:46:32 +00:00
mckero 427e6e1862 i18n: credit BG7NTA and original author in thanks title
The settings outro title now reads 'BG7NTA & the original author would like to thank' (en) / 'BG7NTA 与原作者感谢' (zh) / 'BG7NTA ve orijinal yazar teşekkür eder' (tr), alongside the BA7OPF/BG7NTA entries added to the credits list.
2026-08-03 10:46:20 +00:00
mckero bfa5e5f474 feat(roaming): add Roaming page with 3x3 Maidenhead grid
Ports the QTH定位器 (com.us1pm.gridsquarelocator) location panel into Look4Sat as a new 'Roaming' page, restyled with the app's own look.

UI (top to bottom):
- Info header: GPS status dot, big 8-char locator, Lat/Lon rows with DMS + 5-decimal display, and a GPS 定位 button
- 3x3 grid panel: the current 4-char Maidenhead square (e.g. OL42) centered, surrounded by its 8 neighbors (OL33..OL51), with a red position marker in the center cell

Logic:
- QthConverter gains qthNeighbors(square) building the 3x3 grid with field/square carry at boundaries (AA00 wraps to RR99, IO91 crosses into J field), and qthToSquare(locator) extracting the 4-char square
- Verified against the decompiled app algorithm and the reference screenshot (OL42 grid matches exactly); 9 unit tests cover normal, boundary and field-wrap cases

Navigation:
- New bottom-nav item 'Roaming' between Match and Settings, with a crosshair icon
- New feature:roaming module (ViewModel + Compose screen) registered in the app

Build config:
- Lowered Gradle JVM heap from -Xmx6g to 768m: the 2GB build server froze on the old value; heavy release builds stay on GitHub Actions
2026-08-03 10:46:14 +00:00
mckero 213425379c merge: merge atsunatsu/main (mutual radar overlay arrows)
Merge upstream commit 2298d8ee 'feat: add mutual radar overlay arrows'. Clean auto-merge: upstream changed RadarView.kt arrow drawing, our sweep optimization stayed intact. No conflicts.

First v4.4.5 was built from c3444aed; this release rebuilds from the merged tree.
2026-08-03 10:04:09 +00:00
mckero c3444aed24 chore: bump version to 4.4.5
Next release: versionCode 444 -> 445, versionName 4.4.4 -> 4.4.5, following the upstream scheme without suffix. Includes the station panel layout fix, 5-decimal coordinates, and credits additions already merged on main.
2026-08-03 09:43:10 +00:00
mckero 368bd199e5 fix: station panel layout wraps QTH to its own line
With 5-decimal coordinates the Lat/Lon/Qth row overflowed, pushing the QTH value to a wrapped line. Split into two rows: Lat + Lon on the first line, Qth on the second.

Also append BA7OPF (pass matching feature) and BG7NTA to the credits list in en/zh/tr string resources.
2026-08-03 09:33:40 +00:00
atsunatsu 2298d8eea4 feat: add mutual radar overlay arrows 2026-08-03 17:27:48 +08:00
mckero 7bb0bbbb46 build: load release signing config from local keystore.properties
Conditionally applies the BG7NTA signingConfig when keystore.properties exists (gitignored). CI signs via apksigner with GitHub Secrets, so this only affects local builds.
2026-08-03 09:25:53 +00:00
mckero deec5581ae feat: rebrand to Look4Sat Pro, bump version to 4.4.4
App display name changed from Look4Sat to Look4Sat Pro (fork identity). Version follows upstream scheme without suffix: 4.4.3 -> 4.4.4, versionCode 443 -> 444.
2026-08-03 09:20:41 +00:00
mckero 1de5632428 feat: increase station coordinate precision to 5 decimals
Station lat/lon was stored rounded to 4 decimals (~11 m). Bump to 5 decimals (~1.1 m), slightly better than GPS hardware accuracy without showing noise.

- SettingsRepo.setStationPosition: round(4) -> round(5)
- QthConverter.qthToPosition: round(4) -> round(6) so 10-char locators fully roundtrip
- Update QthConverterTest expected values to 6-decimal precision
2026-08-03 09:14:13 +00:00
97 changed files with 13760 additions and 115 deletions

No files matched your search

+5 -2
View File
@@ -39,19 +39,22 @@ jobs:
run: |
echo "${{ secrets.KEY_STORE }}" | base64 -d > keystore.jks
APK=$(find app/build/outputs/apk/release -name "*.apk" | head -1)
VERSION=${TAG_NAME#v}
SIGNED_APK="app/build/outputs/apk/release/Look4Sat-Pro-${VERSION}.apk"
BUILD_TOOLS=$(ls -d ${ANDROID_HOME}/build-tools/*/ | sort -V | tail -1)
${BUILD_TOOLS}apksigner sign \
--ks keystore.jks \
--ks-pass pass:${{ secrets.KEY_STORE_PASSWORD }} \
--ks-key-alias ${{ secrets.KEY_ALIAS }} \
--key-pass pass:${{ secrets.KEY_PASSWORD }} \
--out app/build/outputs/apk/release/look4sat.apk \
--out "$SIGNED_APK" \
"$APK"
rm keystore.jks
echo "SIGNED_APK=$SIGNED_APK" >> "$GITHUB_ENV"
- name: Create Release
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release create $TAG_NAME --title=$TAG_NAME --generate-notes
gh release upload $TAG_NAME app/build/outputs/apk/release/look4sat.apk
gh release upload $TAG_NAME "$SIGNED_APK"
+1 -2
View File
@@ -39,8 +39,7 @@ captures/
.idea/
# Keystore files
# Uncomment the following line if you do not want to check your keystore files in.
#*.jks
*.jks
/*.properties
/keystore.properties
/app/keystore.jks
+33
View File
@@ -1,3 +1,36 @@
import java.util.Properties
plugins {
alias(libs.plugins.convention.applicationPlugin)
}
// Load signing config from keystore.properties (gitignored, never commit credentials)
val keystoreProperties = Properties().apply {
val propsFile = rootProject.file("keystore.properties")
if (propsFile.exists()) propsFile.inputStream().use { load(it) }
}
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") {
storeFile = rootProject.file(keystoreProperties["storeFile"] as String)
storePassword = keystoreProperties["storePassword"] as String
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
}
}
}
buildTypes {
release {
signingConfig = signingConfigs.findByName("release")
}
}
}
@@ -80,11 +80,13 @@ 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.roaming.RoamingScreen
import com.rtbishop.look4sat.feature.satellites.SatellitesDestination
import com.rtbishop.look4sat.feature.settings.SettingsDestination
@@ -130,12 +132,21 @@ 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.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 navItems = listOf(Screen.Satellites, Screen.Passes, Screen.Radar, Screen.Mutual, Screen.Roaming, Screen.CwDecode, 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 }
// Activity-scoped so the mutual query results survive navigation to Radar and back
val mutualViewModel: MutualViewModel = viewModel(
viewModelStoreOwner = context as ViewModelStoreOwner,
@@ -156,6 +167,7 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
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.Map -> screen is Screen.Map
is Screen.Settings -> screen is Screen.Settings
else -> false
@@ -224,6 +236,12 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
}
)
}
entry<Screen.Roaming> {
RoamingScreen()
}
entry<Screen.CwDecode> {
CwDecodeScreen()
}
entry<Screen.Settings> {
SettingsDestination()
}
@@ -280,4 +298,4 @@ fun MainScreen(navigateToRadar: () -> Unit = {}) {
}
}
}
}
}
@@ -31,10 +31,12 @@ 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"))
implementation(project(":feature:radar"))
implementation(project(":feature:roaming"))
implementation(project(":feature:satellites"))
implementation(project(":feature:settings"))
implementation(libs.androidx.core.splashscreen)
@@ -69,11 +69,14 @@ class DatabaseRepo(
val dataSourcesSettings = settingsRepo.dataSourcesSettings.value
val tleUrls = buildMap {
putAll(Sources.satelliteDataUrls)
if (dataSourcesSettings.useCustomTLE) put(customSourceType, dataSourcesSettings.tleUrl)
// 开关开且 URL 非空 -> All 用自定义 URL;否则用默认 URL(在线更新默认源)
put("All", if (dataSourcesSettings.useCustomTLE && dataSourcesSettings.tleUrl.isNotBlank())
dataSourcesSettings.tleUrl else Sources.defaultTleUrl)
}.filterValues { it.isNotBlank() }
val radioUrls = buildMap {
putAll(Sources.transceiversDataUrls)
if (dataSourcesSettings.useCustomTransceivers) put(customSourceType, dataSourcesSettings.transceiversUrl)
put("SatNOGS", if (dataSourcesSettings.useCustomTransceivers && dataSourcesSettings.transceiversUrl.isNotBlank())
dataSourcesSettings.transceiversUrl else Sources.defaultTransceiversUrl)
}.filterValues { it.isNotBlank() }
// launch all network requests concurrently
val tleJobs = tleUrls.values.map { url -> async { url to remoteSource.getNetworkStream(url) } }
@@ -29,6 +29,7 @@ import com.rtbishop.look4sat.core.domain.model.OtherSettings
import com.rtbishop.look4sat.core.domain.model.PassesSettings
import com.rtbishop.look4sat.core.domain.model.RCSettings
import com.rtbishop.look4sat.core.domain.model.RadioControlSettings
import com.rtbishop.look4sat.core.domain.source.Sources
import com.rtbishop.look4sat.core.domain.predict.GeoPos
import com.rtbishop.look4sat.core.domain.repository.ISettingsRepo
import com.rtbishop.look4sat.core.domain.utility.positionToQth
@@ -76,6 +77,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"
@@ -213,8 +216,8 @@ class SettingsRepo(
}
private fun setStationPosition(latitude: Double, longitude: Double, altitude: Double, locator: String) {
val newLat = latitude.round(4)
val newLon = longitude.round(4)
val newLat = latitude.round(5)
val newLon = longitude.round(5)
val newAlt = altitude.round(1)
val timestamp = System.currentTimeMillis()
println("Received new Position($newLat, $newLon, $newAlt) & Locator $locator")
@@ -356,6 +359,8 @@ 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(","))
}
new
}
@@ -371,6 +376,8 @@ 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(),
lowElevation = Double.fromBits(preferences.getLong(keyLowElevation, 15.0.toRawBits())),
highElevation = Double.fromBits(preferences.getLong(keyHighElevation, 45.0.toRawBits()))
)
@@ -390,12 +397,20 @@ class SettingsRepo(
_dataSourcesSettings.value = settings
}
private fun getDataSourcesSettings(): DataSourcesSettings = DataSourcesSettings(
useCustomTLE = preferences.getBoolean(keyUseCustomTle, false),
useCustomTransceivers = preferences.getBoolean(keyUseCustomTransceivers, false),
tleUrl = preferences.getString(keyTleUrl, "https://example.com/tle.txt") ?: "",
transceiversUrl = preferences.getString(keyTransceiversUrl, "https://example.com/radio.json") ?: ""
)
private fun getDataSourcesSettings(): DataSourcesSettings {
// 4.4.8 修复: 旧版 example.com 占位 URL 视为未配置 -> 替换为真实默认 URL 且开关强制关闭,
// 否则在线更新的 All/SatNOGS 源会指向错误地址导致更新失败
val storedTleUrl = preferences.getString(keyTleUrl, Sources.defaultTleUrl) ?: Sources.defaultTleUrl
val storedTxUrl = preferences.getString(keyTransceiversUrl, Sources.defaultTransceiversUrl) ?: Sources.defaultTransceiversUrl
val tleUrl = if (storedTleUrl == "https://example.com/tle.txt") Sources.defaultTleUrl else storedTleUrl
val txUrl = if (storedTxUrl == "https://example.com/radio.json") Sources.defaultTransceiversUrl else storedTxUrl
return DataSourcesSettings(
useCustomTLE = preferences.getBoolean(keyUseCustomTle, false) && tleUrl != Sources.defaultTleUrl,
useCustomTransceivers = preferences.getBoolean(keyUseCustomTransceivers, false) && txUrl != Sources.defaultTransceiversUrl,
tleUrl = tleUrl,
transceiversUrl = txUrl
)
}
//endregion
//region # Radio control settings
@@ -102,7 +102,8 @@ class DatabaseRepoTest {
repository.updateFromRemote()
assertTrue(localSource.insertedEntries.any { it.catnum == 25544 })
assertEquals(listOf(25544), settingsRepo.satelliteTypeIdsByType["Other"])
// 新语义: 开关开 + URL 非空 -> All 源用自定义 URL, 数据归入 All 类型
assertEquals(listOf(25544), settingsRepo.satelliteTypeIdsByType["All"])
}
private fun validCsvStream(): InputStream = """
@@ -62,7 +62,11 @@ 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()
)
data class DataSourcesSettings(
@@ -18,6 +18,10 @@
package com.rtbishop.look4sat.core.domain.source
object Sources {
// 在线更新的默认 URL(用户可通过"自定义URL"对话框修改/重置)
val defaultTleUrl = "https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=csv"
val defaultTransceiversUrl = "https://db.satnogs.org/api/transmitters/?format=json&status=active"
val satelliteDataUrls = mapOf(
"All" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=csv",
"Amateur" to "https://celestrak.org/NORAD/elements/gp.php?GROUP=amateur&FORMAT=csv",
@@ -63,7 +63,7 @@ fun qthToPosition(locator: String): GeoPos? {
latitude += 1.0 / 48.0
}
}
return GeoPos(latitude.round(4), longitude.round(4))
return GeoPos(latitude.round(6), longitude.round(6))
}
/**
@@ -100,3 +100,47 @@ private fun isValidPosition(lat: Double, lon: Double): Boolean {
private fun isValidLocator(locator: String): Boolean {
return locator.matches("[a-xA-X]{2}\\d{2}[a-xA-X]{2}(?:\\d{2}(?:[a-xA-X]{2})?)?".toRegex())
}
/**
* Returns the 4-char field+square part of a locator, e.g. "OL42ih45" -> "OL42".
* A 4-char input is returned as-is when valid.
*/
fun qthToSquare(locator: String): String {
val upper = locator.trim().uppercase()
return when {
upper.length >= 4 && isValidLocator(upper) -> upper.take(4)
upper.length == 4 && upper.matches("[a-xA-X]{2}\\d{2}".toRegex()) -> upper
else -> "----"
}
}
/**
* Builds the 3x3 grid of 4-char squares surrounding [square] (e.g. "OL42").
* Row 0 = north (lat +1), col 0 = west (lon -1). Handles field/square carry
* at boundaries (e.g. "AA00" wraps to "RR99" at the south-west corner).
* Mirrors the neighbor logic decompiled from the QTH定位器 app.
*/
fun qthNeighbors(square: String): List<String> {
if (square.length != 4) return emptyList()
val lonField = (square[0].uppercaseChar().code - 65).coerceIn(0, 17)
val latField = (square[1].uppercaseChar().code - 65).coerceIn(0, 17)
val lonSquare = square[2].digitToCharOrNull() ?: return emptyList()
val latSquare = square[3].digitToCharOrNull() ?: return emptyList()
val result = mutableListOf<String>()
for (dLat in 1 downTo -1) { // north -> south
for (dLon in -1..1) { // west -> east
var lf = lonField
var tf = latField
var ls = lonSquare + dLon
var ts = latSquare + dLat
if (ls < 0) { lf -= 1; ls = 9 } else if (ls > 9) { lf += 1; ls = 0 }
if (ts < 0) { tf -= 1; ts = 9 } else if (ts > 9) { tf += 1; ts = 0 }
lf = (lf + 18) % 18
tf = (tf + 18) % 18
result += "${('A' + lf).toChar()}${('A' + tf).toChar()}$ls$ts"
}
}
return result
}
private fun Char.digitToCharOrNull(): Int? = digitToIntOrNull()
@@ -18,7 +18,9 @@
package com.rtbishop.look4sat.core.domain
import com.rtbishop.look4sat.core.domain.utility.positionToQth
import com.rtbishop.look4sat.core.domain.utility.qthNeighbors
import com.rtbishop.look4sat.core.domain.utility.qthToPosition
import com.rtbishop.look4sat.core.domain.utility.qthToSquare
import org.junit.Test
class QthConverterTest {
@@ -26,14 +28,14 @@ class QthConverterTest {
@Test
fun `Given valid QTH returns correct POS`() {
var result = qthToPosition("io91VL39FX")
assert(result?.latitude == 51.4999 && result.longitude == -0.2231)
assert(result?.latitude == 51.499913 && result.longitude == -0.22309)
result = qthToPosition("gf15vc")
assert(result?.latitude == -34.8958 && result.longitude == -56.2083)
assert(result?.latitude == -34.895833 && result.longitude == -56.208333)
// 8-char locators: finer 30" x 15" cell center
result = qthToPosition("io91vl47")
assert(result?.latitude == 51.4896 && result.longitude == -0.2125)
assert(result?.latitude == 51.489583 && result.longitude == -0.2125)
result = qthToPosition("jn58td25")
assert(result?.latitude == 48.1479 && result.longitude == 11.6042)
assert(result?.latitude == 48.147917 && result.longitude == 11.604167)
}
@Test
@@ -82,4 +84,42 @@ class QthConverterTest {
assert(qth == qth2) { "Roundtrip failed for ($lat, $lon): $qth -> $qth2" }
}
}
@Test
fun `Given square returns correct 3x3 neighbors`() {
// Reference grid from the QTH定位器 screenshot: OL42
val neighbors = qthNeighbors("OL42")
assert(neighbors == listOf(
"OL33", "OL43", "OL53",
"OL32", "OL42", "OL52",
"OL31", "OL41", "OL51"
)) { "OL42 grid mismatch: $neighbors" }
// Center cell must be the input itself
assert(neighbors[4] == "OL42")
// 9 cells, all distinct
assert(neighbors.size == 9 && neighbors.toSet().size == 9)
}
@Test
fun `Given boundary square wraps fields correctly`() {
// South-west corner: AA00 neighbors wrap to RR99 / RA90 etc.
val sw = qthNeighbors("AA00")
assert(sw.size == 9 && sw.toSet().size == 9)
assert(sw[0] == "RA91" && sw[4] == "AA00" && sw[6] == "RR99" && sw[8] == "AR19")
// North-east corner: RR99 wraps to AA00
val ne = qthNeighbors("RR99")
assert(ne.size == 9 && ne.toSet().size == 9)
assert(ne[0] == "RA80" && ne[4] == "RR99" && ne[8] == "AR08")
// Field boundary: IO91's east neighbors cross into J field
val london = qthNeighbors("IO91")
assert(london[2] == "JO02" && london[5] == "JO01")
}
@Test
fun `Given full locator returns square part`() {
assert(qthToSquare("OL42ih45") == "OL42")
assert(qthToSquare("io91VL39FX") == "IO91")
assert(qthToSquare("JN58") == "JN58")
assert(qthToSquare("garbage!!") == "----")
}
}
@@ -21,27 +21,36 @@ 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 Settings : Screen(R.drawable.ic_settings, R.string.nav_prefs)
data object Roaming : Screen(R.drawable.ic_roaming, R.string.nav_roaming, "Roaming")
@Serializable
data object CwDecode : Screen(R.drawable.ic_cw, R.string.nav_cw, "CwDecode")
@Serializable
data object Settings : Screen(R.drawable.ic_settings, R.string.nav_prefs, "Settings")
}
// UI 设置: 默认页面顺序(与 MainScreen navItems 一致)
val defaultScreenOrder = listOf("Satellites", "Passes", "Radar", "Mutual", "Roaming", "CwDecode", "Map", "Settings")
@Serializable
data object RadarDestination : NavKey
@@ -70,4 +79,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,10 @@
<?xml version="1.0" encoding="utf-8"?>
<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="@android:color/white"
android:pathData="M12,6v3l4,-4 -4,-4v3c-4.42,0 -8,3.58 -8,8 0,1.57 0.46,3.03 1.24,4.26L6.7,14.8c-0.45,-0.83 -0.7,-1.79 -0.7,-2.8 0,-3.31 2.69,-6 6,-6zM18.76,7.74L17.3,9.2c0.44,0.84 0.7,1.79 0.7,2.8 0,3.31 -2.69,6 -6,6v-3l-4,4 4,4v-3c4.42,0 8,-3.58 8,-8 0,-1.57 -0.46,-3.03 -1.24,-4.26z" />
</vector>
@@ -0,0 +1,25 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="96"
android:viewportHeight="96">
<!-- Crosshair / grid-locator style icon: concentric rings + center dot -->
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="6"
android:strokeLineCap="round"
android:strokeLineJoin="round"
android:pathData="M 48 12 L 48 84 M 12 48 L 84 48" />
<path
android:fillColor="@android:color/transparent"
android:strokeColor="@android:color/white"
android:strokeWidth="6"
android:pathData="M 48 26 A 22 22 0 1 0 48.1 26" />
<path
android:fillColor="@android:color/white"
android:pathData="M 44 44 L 52 44 L 52 52 L 44 52 Z" />
</vector>
@@ -10,6 +10,7 @@
<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>
@@ -50,13 +51,9 @@
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">
* Switched to short-form commands in NetworkReporter, by theojalba
\n* Added CAT radio control for full-duplex SAT operation, by jcmerg
\n* Added normalized time for passes AOS/LOS, fixed refresh state bug
\n* Added multiple performance tweaks, improved passes calculation
\n* Fixed UTC pass time problem mentioned in issue #201
</string>
<string name="pass_whatsnew_message" translatable="false">
* Transponder CW kod çözücü paneli artık geliştirilmiş kod çözme motorunu canlı şelale (waterfall) görüntüsüyle kullanıyor
* CW kod çözme sayfaları aynı motoru paylaşıyor: gezinme sayfası ve transponder paneli</string>
<!-- Radar screen -->
<string name="radar_back">Geri</string>
@@ -103,7 +100,7 @@
<string name="map_visible">Görünür</string>
<!-- Settings screen -->
<string name="prefs_app_title" translatable="false">Look4Sat v%s</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">Bağış yap</string>
<string name="prefs_donate_url" translatable="false">https://ko-fi.com/rt_bishop</string>
@@ -137,7 +134,7 @@
<string name="prefs_data_entries">Uydular: %s</string>
<string name="prefs_data_radios">Transceiver\'lar: %s</string>
<string name="prefs_data_update">Güncelle</string>
<string name="prefs_data_import">İçe aktar</string>
<string name="prefs_data_import">Özel URL</string>
<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>
@@ -146,6 +143,7 @@
<string name="prefs_data_sources_tle_switch">Özel TLE URL</string>
<string name="prefs_data_sources_transceivers_switch">Özel transceiver URL</string>
<string name="prefs_data_sources_url_title" translatable="false">URL (HTTPS)</string>
<string name="prefs_data_reset_url">Varsayılan URL\'ye sıfırla</string>
<string name="prefs_data_output_title">Veri aktarımı</string>
<string name="prefs_net_output">Ağ</string>
@@ -181,6 +179,9 @@
<string name="prefs_net_perm_error">Ağ izninizi kontrol edin</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_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>
@@ -192,14 +193,16 @@
<string name="prefs_highlight_mid">Orta %1$d-%2$d°</string>
<string name="prefs_highlight_high">Yüksek &gt; %1$d°</string>
<string name="prefs_outro_title">Teşekkürler</string>
<string name="prefs_outro_title">BG7NTA ve orijinal yazar teşekkür eder</string>
<string name="prefs_outro_thanks" translatable="false">
• Look4Sat users and contributors!
\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)</string>
\n• Libre Space Foundation (SatNOGS)
\n• BA7OPF (pass matching feature)
\n• BG7NTA</string>
<string name="prefs_outro_license">Bu uygulama herhangi bir garanti sunmaz</string>
</resources>
@@ -10,6 +10,8 @@
<string name="nav_pass">过境</string>
<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>
@@ -38,6 +40,10 @@
<string name="pass_error_message">没有即将过境的卫星</string>
<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" translatable="false">
* 转发器 CW 解码面板已接入增强解码引擎,带实时瀑布图显示
* CW 解码入口共用同一引擎:导航页与转发器面板</string>
<!-- Radar screen -->
<string name="radar_back">后退</string>
@@ -110,7 +116,7 @@
<string name="prefs_data_entries">卫星:%s</string>
<string name="prefs_data_radios">收发器:%s</string>
<string name="prefs_data_update">在线更新</string>
<string name="prefs_data_import">导入文件</string>
<string name="prefs_data_import">自定义URL</string>
<string name="prefs_data_clear">清空</string>
<string name="prefs_data_clear_success">数据清空成功</string>
<string name="prefs_data_update_success">更新成功</string>
@@ -156,6 +162,9 @@
<string name="rc_enable_switch">启用 CAT 控制</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_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>
@@ -167,7 +176,7 @@
<string name="prefs_highlight_mid">中 %1$d-%2$d°</string>
<string name="prefs_highlight_high">高 &gt; %1$d°</string>
<string name="prefs_outro_title">我要感谢:</string>
<string name="prefs_outro_title">BG7NTA 与原作者感谢:</string>
<string name="prefs_outro_thanks" translatable="false">
• Look4Sat 所有用户及贡献者!
\n• David A. B. Johnson (predict4java)
@@ -175,7 +184,9 @@
\n• Alexandru Csete (Gpredict)
\n• Dr T.S. Kelso (Celestrak)
\n• Libre Space Foundation (SatNOGS)
\n• xdsopl 和 Robot36 贡献者!</string>
\n• xdsopl 和 Robot36 贡献者!
\n• BA7OPF(过境匹配功能)
\n• BG7NTA</string>
<string name="prefs_outro_license">该应用程序不提供任何保修.</string>
</resources>
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Commmon -->
<string name="app_name" translatable="false">Look4Sat</string>
<string name="app_name" translatable="false">Look4Sat Pro</string>
<string name="btn_accept">Accept</string>
<string name="btn_cancel">Cancel</string>
<string name="empty_list_message">No data to show</string>
@@ -11,6 +11,8 @@
<string name="nav_pass">Passes</string>
<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>
@@ -51,10 +53,9 @@
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">
* Added required tweaks to support Android 17 (API 37)
\n\n* Added ACCESS_LOCAL_NETWORK permission check to support network data output
</string>
<string name="pass_whatsnew_message" translatable="false">
* Transponder CW decoder panel now uses the enhanced decoding engine with a live waterfall display
* CW decoding pages share the same engine: navigation page and transponder panel</string>
<!-- Radar screen -->
<string name="radar_back">Back</string>
@@ -111,7 +112,7 @@
<string name="map_visible">Visible</string>
<!-- Settings screen -->
<string name="prefs_app_title" translatable="false">Look4Sat v%s</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">Donate</string>
<string name="prefs_donate_url" translatable="false">https://ko-fi.com/rt_bishop</string>
@@ -145,7 +146,7 @@
<string name="prefs_data_entries">Satellites: %s</string>
<string name="prefs_data_radios">Transceivers: %s</string>
<string name="prefs_data_update">Update</string>
<string name="prefs_data_import">Import</string>
<string name="prefs_data_import">Custom URL</string>
<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>
@@ -156,6 +157,7 @@
<string name="prefs_data_sources_tle_switch">Custom TLE URL</string>
<string name="prefs_data_sources_transceivers_switch">Custom transceivers URL</string>
<string name="prefs_data_sources_url_title" translatable="false">URL (HTTPS)</string>
<string name="prefs_data_reset_url">Reset to default URL</string>
<string name="prefs_data_output_title">Data output</string>
<string name="prefs_net_output">Network</string>
@@ -191,6 +193,9 @@
<string name="prefs_net_perm_error">Check your network permission</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_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>
@@ -202,7 +207,7 @@
<string name="prefs_highlight_mid">Mid %1$d–%2$d°</string>
<string name="prefs_highlight_high">High &gt; %1$d°</string>
<string name="prefs_outro_title">I would like to say thanks to</string>
<string name="prefs_outro_title">BG7NTA &amp; the original author would like to thank</string>
<string name="prefs_outro_thanks" translatable="false">
* Look4Sat users and contributors!
\n* David A. B. Johnson (predict4java)
@@ -211,6 +216,8 @@
\n* Dr T.S. Kelso (Celestrak)
\n* Libre Space Foundation (SatNOGS)
\n* xdsopl and Robot36 contributors!
\n* BA7OPF (pass matching feature)
\n* BG7NTA
</string>
<string name="prefs_outro_license">The app comes with no warranty</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 = "Microphone permission is required for CW decoding",
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("Grant permission")
}
if (permanentlyDenied) {
// 勾选了"不再询问": 引导去系统设置开启
TextButton(onClick = {
activity.startActivity(
Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.parse("package:${activity.packageName}")
)
)
}) {
Text("Open app 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,19 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="purple_500">#ff6200ee</color>
<integer name="default_font_size">18</integer>
<string name="idle">Idle</string>
<string name="waterfall_display">Waterfall display</string>
<string name="frequency_scale">Frequency scale</string>
<string name="decoded_text">Decoded text</string>
<string name="clear">Clear</string>
<string name="pause">Pause Decoding</string>
<string name="save_text">Save Text</string>
<string name="record_signals">Record Signals</string>
<string name="settings">Settings</string>
<string name="help">Online Help</string>
<string name="rate">Rate this App</string>
<string name="premium">Get Premium Version</string>
<string name="tap_back_again_to_close">Tap Back again to close the app</string>
<string name="message_type">Message Type</string>
</resources>
+4
View File
@@ -8,4 +8,8 @@ android {
dependencies {
implementation(project(":feature:mutual"))
// CW 解码面板: 复用 feature:cw 的 Morse Expert 引擎(布局 cw_panel_main + MainActivity 控制器)
implementation(project(":feature:cw"))
// 与 feature:cw 一致(控制器按 ID 递归查找 ConstraintLayout 视图)
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
}
@@ -119,9 +119,10 @@ fun RadarViewCompose(
var cachedSweepColor by remember { mutableStateOf(Color.Unspecified) }
var trackPath by remember { mutableStateOf(Path()) }
var trackEffect by remember { mutableStateOf(PathEffect.cornerPathEffect(0f)) }
// Station-B overlay track (dashed)
// Station-B overlay track (dashed, with the same directional arrow stamp as station A)
var cachedTrackBRef by remember { mutableStateOf<List<OrbitalPos>?>(null) }
var trackBPath by remember { mutableStateOf(Path()) }
var trackBEffect by remember { mutableStateOf(PathEffect.cornerPathEffect(0f)) }
// ShaderBrush is cached to avoid allocating a new GPU shader object every frame
var cachedSweepBrush by remember { mutableStateOf<ShaderBrush?>(null) }
@@ -131,7 +132,10 @@ fun RadarViewCompose(
if (radius != cachedRadius || items !== cachedItemsRef || trackB !== cachedTrackBRef) {
trackPath = createTrackPath(items, radius)
trackEffect = createTrackEffect(trackPath)
trackBPath = trackB?.let { createTrackPath(it, radius) } ?: Path()
val newTrackBPath = trackB?.let { createTrackPath(it, radius) } ?: Path()
trackBPath = newTrackBPath
trackBEffect = if (!newTrackBPath.isEmpty) createTrackEffect(newTrackBPath)
else PathEffect.cornerPathEffect(0f)
cachedSweepBrush = makeSweepBrush(center, primaryColor)
cachedRadius = radius
cachedItemsRef = items
@@ -148,13 +152,14 @@ fun RadarViewCompose(
drawElevationLabels(radius, primaryColor, measurer)
translate(center.x, center.y) {
drawTrack(trackPath, trackEffect, aimColor, primaryColor)
// Station-B overlay: full dashed track + live position dot
// (same display mode as the local station: track line + pulsing dot)
// Station-B overlay: full dashed track + directional arrows + live position dot
// (same display mode as the local station: track line/arrow + pulsing dot)
if (trackB != null && trackB.isNotEmpty() && !trackBPath.isEmpty) {
drawPath(
trackBPath, trackBColor,
style = Stroke(STROKE_WIDTH, pathEffect = PathEffect.dashPathEffect(floatArrayOf(18f, 12f)))
)
drawPath(trackBPath, trackBColor, style = Stroke(pathEffect = trackBEffect))
}
trackBPosition?.let { posB ->
if (posB.elevation > 0) {
@@ -17,6 +17,8 @@
*/
package com.rtbishop.look4sat.feature.radar
import android.app.Activity
import android.view.LayoutInflater
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
@@ -50,6 +52,7 @@ import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
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
@@ -60,6 +63,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.rotate
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.font.FontWeight
@@ -68,6 +72,8 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.constraintlayout.widget.ConstraintLayout
import com.rtbishop.look4sat.core.domain.model.SatRadio
import com.rtbishop.look4sat.core.domain.predict.OrbitalPos
import com.rtbishop.look4sat.core.domain.utility.DopplerFrequencyCalculator
@@ -75,6 +81,8 @@ import com.rtbishop.look4sat.core.presentation.CardButton
import com.rtbishop.look4sat.core.presentation.R
import com.rtbishop.look4sat.core.presentation.formatFrequency
import com.rtbishop.look4sat.core.presentation.infiniteMarquee
import com.rtbishop.look4sat.feature.cw.R as CwR
import com.ve3nea.morse_expert.MainActivity
import java.util.Locale
import kotlin.time.Duration.Companion.milliseconds
@@ -661,19 +669,56 @@ private fun CwDecoderPanel(
}
AnimatedVisibility(visible = cw.isExpanded) {
// Morse Expert 引擎(4.5.0 移植): 小布局 cw_panel_main(瀑布图 + 解码文本)
// 自带 Kotlin 贝叶斯引擎(CwDecoder)不再用于此面板(代码保留作回退)
val context = LocalContext.current
val activity = remember { context as? Activity }
val controller = remember { MainActivity() }
val rootView = remember {
LayoutInflater.from(context).inflate(CwR.layout.cw_panel_main, null) as ConstraintLayout
}
var initialized by remember { mutableStateOf(false) }
var listening by remember { mutableStateOf(false) }
// 生命周期: 展开即绑定视图(不改宿主窗口); 收起/离开自动释放
DisposableEffect(Unit) {
if (activity != null) {
controller.onCreate(activity, rootView, false)
}
onDispose {
controller.onPause()
controller.onDestroy()
}
}
// 权限已授予(首次进入/授权回调统一入口): 初始化引擎 + 启动录音
LaunchedEffect(cw.hasPermission) {
if (cw.hasPermission && !initialized) {
controller.onPermissionGranted() // = v(): 创建音频采集 + 解码核心
controller.onResume() // 立即启动录音
initialized = true
listening = true
}
}
Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
// Control buttons row
// Control buttons row(映射到控制器)
Row(
horizontalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth()
) {
if (cw.status == CwStatus.Idle) {
if (!listening) {
Button(
onClick = {
if (!cw.hasPermission) {
requestMicPermission()
} else if (!initialized) {
controller.onPermissionGranted()
controller.onResume()
initialized = true
listening = true
} else {
onAction(RadarAction.CwStartListening)
controller.onResume()
listening = true
}
},
modifier = Modifier.weight(1f)
@@ -682,58 +727,30 @@ private fun CwDecoderPanel(
}
} else {
Button(
onClick = { onAction(RadarAction.CwStopListening) },
onClick = {
controller.onPause()
listening = false
},
modifier = Modifier.weight(1f)
) {
Text(stringResource(R.string.radar_cw_stop))
}
}
OutlinedButton(
onClick = { onAction(RadarAction.CwReset) },
onClick = { controller.clearDecoded() },
modifier = Modifier.weight(1f)
) {
Text(stringResource(R.string.radar_cw_reset))
}
}
// Signal strength indicator
if (cw.status == CwStatus.Listening && cw.signalStrength > 0f) {
val strengthPct = (cw.signalStrength * 100).toInt()
Text(
text = "Signal: $strengthPct%",
fontSize = 12.sp,
color = if (cw.signalStrength > 0.5f) Color(0xFF4CAF50)
else if (cw.signalStrength > 0.2f) Color(0xFFFFC107)
else MaterialTheme.colorScheme.onSurfaceVariant
)
}
// Decoded text output
ElevatedCard(
// 瀑布图 + 解码文本(原版布局迷你版, 适配容器)
AndroidView(
factory = { rootView },
modifier = Modifier
.fillMaxWidth()
.height(120.dp)
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(8.dp)
) {
Text(
text = "Decoded:",
fontSize = 12.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
Spacer(modifier = Modifier.height(4.dp))
Text(
text = cw.decodedText.ifEmpty { "Waiting for CW signal..." },
fontSize = 16.sp,
fontWeight = FontWeight.Medium,
color = MaterialTheme.colorScheme.onSurface
)
}
}
.height(150.dp)
)
}
}
}
+11
View File
@@ -0,0 +1,11 @@
plugins {
alias(libs.plugins.convention.featurePlugin)
}
android {
namespace = "com.rtbishop.look4sat.feature.roaming"
}
dependencies {
implementation(project(":core:data"))
}
@@ -0,0 +1,791 @@
/*
* 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.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.feature.roaming
import android.Manifest
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.location.Location
import android.location.LocationListener
import android.location.LocationManager
import android.os.Bundle
import android.provider.Settings
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.shape.CircleShape
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.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.width
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.systemBars
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.text.font.FontWeight
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
/**
* 漫游页 —— 原样照搬自 QTH定位器 2.0 (com.us1pm.gridsquarelocator)。
* UI: res/layout/main.xml (结构/尺寸/颜色逐项照搬, 不做任何更改)
* 逻辑: MainActivity.showLocation() / checkEnabled() / GPS 实时监听
* 红线: UI 与逻辑均不更改, 与成品截图逐像素对齐。
*/
@Composable
fun RoamingScreen() {
val context = LocalContext.current
var state by remember { mutableStateOf(RoamingState()) }
// 照搬 onCreate: 日期固定文本 + 小时前缀(定位时拼接分钟)
val dateText = remember { SimpleDateFormat("dd MMM yyyy", Locale.getDefault()).format(Date()) }
var hourPrefix by remember { mutableStateOf("") }
// 照搬 onCreate: 初始 checkEnabled() -> 红点+设置按钮, 日期隐藏
var hasPermission by remember { mutableStateOf(false) }
val permissionLauncher = rememberLauncherForActivityResult(
ActivityResultContracts.RequestMultiplePermissions()
) { }
LaunchedEffect(Unit) {
hourPrefix = SimpleDateFormat("HH:mm", Locale.getDefault()).format(Date()).substring(0, 3)
val fine = ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_FINE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
val coarse = ContextCompat.checkSelfPermission(context, Manifest.permission.ACCESS_COARSE_LOCATION) ==
PackageManager.PERMISSION_GRANTED
hasPermission = fine || coarse
if (!hasPermission) {
permissionLauncher.launch(
arrayOf(Manifest.permission.ACCESS_FINE_LOCATION, Manifest.permission.ACCESS_COARSE_LOCATION)
)
} else {
// 照搬 onResume: 绿点+进度条+提示+日期显示, 红点/按钮隐藏
state = state.copy(gpsOn = true, gpsOff = false, showSettings = false, showProgress = true, showNotice = true, showDate = true)
}
}
// 照搬 onResume/onPause: GPS + network 监听, 10 秒 / 10 米
val locationManager = remember { context.getSystemService(Context.LOCATION_SERVICE) as LocationManager }
DisposableEffect(locationManager, hasPermission) {
if (hasPermission) {
@SuppressLint("MissingPermission")
val listener = object : LocationListener {
override fun onLocationChanged(location: Location) {
// 照搬 showLocation 开头: 只接受 gps / network provider
if (location.provider != "gps" && location.provider != "network") return
state = roamingStateFromLocation(location.latitude, location.longitude, location.time, hourPrefix)
.copy(date = dateText)
}
override fun onProviderDisabled(provider: String) {
// 照搬 checkEnabled: 红点+按钮, 日期隐藏
state = state.copy(gpsOn = false, gpsOff = true, showSettings = true, showProgress = false, showNotice = false, showDate = false)
}
override fun onProviderEnabled(provider: String) {
state = state.copy(gpsOn = true, gpsOff = false, showSettings = false, showProgress = true, showNotice = true, showDate = true)
}
@Deprecated("Deprecated in Java")
override fun onStatusChanged(provider: String?, status: Int, extras: Bundle?) = Unit
}
try {
locationManager.requestLocationUpdates("gps", 10_000L, 10f, listener)
locationManager.requestLocationUpdates("network", 10_000L, 10f, listener)
} catch (_: SecurityException) {
}
onDispose { locationManager.removeUpdates(listener) }
} else {
onDispose { }
}
}
// 照搬 res/layout/main.xml: 浅色页面, 蓝色信息区, 43sp 定位码, 3x3 九宫格, 底部版权
// 按用户要求: 上下往内缩进系统栏安全区, 避免内容被状态栏/导航栏遮挡
Column(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.background)
.windowInsetsPadding(WindowInsets.systemBars)
) {
// relativeLayout7: 顶部 GPS 状态条, 25dp 高, holo_blue_bright
Box(
modifier = Modifier
.fillMaxWidth()
.padding(2.dp)
.height(25.dp)
.background(MaterialTheme.colorScheme.surfaceVariant)
) {
Row(modifier = Modifier.fillMaxSize(), verticalAlignment = Alignment.CenterVertically) {
// tvTitleGPS: "GPS" 14sp 黑, 左 8dp
Text(
text = "GPS",
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(start = 8.dp)
)
// imGps: toRightOf tvTitleGPS + marginLeft 15dp (成品 main.xml)
Spacer(modifier = Modifier.width(15.dp))
// imGps: 绿点 (15dp, 主题色 primary -> 夜间滤镜下可见)
if (state.gpsOn) {
Box(
modifier = Modifier
.size(15.dp)
.background(MaterialTheme.colorScheme.primary, CircleShape)
)
}
// imGpsOff: 红点叠层 (15dp, 主题色 error)
if (state.gpsOff) {
Box(
modifier = Modifier
.size(15.dp)
.background(MaterialTheme.colorScheme.error, CircleShape)
)
}
Spacer(modifier = Modifier.weight(1f))
// tvDate: 14sp 黑, 居中
if (state.showDate) {
Text(text = state.date.ifEmpty { dateText }, fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant)
}
Spacer(modifier = Modifier.weight(1f))
// tvTime: 14sp 黑, 右对齐
Text(
text = state.time,
fontSize = 14.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(end = 8.dp)
)
}
// btnLocationSettings: 黄色按钮, 居中
// btnLocationSettings: 成品 #faf8f54a 半透明黄, 14sp 黑字, 居中
if (state.showSettings) {
Box(
modifier = Modifier
.align(Alignment.Center)
.background(MaterialTheme.colorScheme.primary)
.clickable { context.startActivity(Intent(Settings.ACTION_LOCATION_SOURCE_SETTINGS)) }
.padding(horizontal = 5.dp),
contentAlignment = Alignment.Center
) {
Text(text = "设置启用GPS", fontSize = 14.sp, color = MaterialTheme.colorScheme.onPrimary)
}
}
}
// linearLayout4: 经纬度区, holo_blue_bright
Column(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 2.dp)
.background(MaterialTheme.colorScheme.surfaceVariant)
) {
// 纬度行: tvLati 16sp 黑 左 8dp | tvLat 右对齐 3dp
Row(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp), verticalAlignment = Alignment.CenterVertically) {
Text(text = state.latLabel, fontSize = 16.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 8.dp))
Spacer(modifier = Modifier.weight(1f))
Text(text = state.latValue, fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(end = 3.dp))
}
// 经度行
Row(modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp), verticalAlignment = Alignment.CenterVertically) {
Text(text = state.lonLabel, fontSize = 16.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(start = 8.dp))
Spacer(modifier = Modifier.weight(1f))
Text(text = state.lonValue, fontSize = 14.sp, color = MaterialTheme.colorScheme.onSurfaceVariant, modifier = Modifier.padding(end = 3.dp))
}
}
// relativeLayout6: 定位码区, 43sp bold 黑居中
Box(
modifier = Modifier
.fillMaxWidth()
.padding(2.dp)
.background(MaterialTheme.colorScheme.surfaceVariant)
) {
Text(
text = state.loc,
fontSize = 43.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth()
)
// progressBar: 加载中, 居中
if (state.showProgress) {
CircularProgressIndicator(
modifier = Modifier.size(24.dp).align(Alignment.Center),
color = MaterialTheme.colorScheme.onSurfaceVariant,
strokeWidth = 2.dp
)
}
// tvNotice: 提示文字, 居中顶部
if (state.showNotice) {
Text(
text = "请稍等,加载可能需要几分钟的时间。",
fontSize = 16.sp,
fontStyle = androidx.compose.ui.text.font.FontStyle.Italic,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.align(Alignment.TopCenter).padding(top = 2.dp)
)
}
}
// relativeLayout: 九宫格, 占满剩余, 底部 15dp
// 用 Box 等价 RelativeLayout 布局: 左格贴左 / 中格水平居中 / 右格贴右 (成品 main.xml)
Column(modifier = Modifier.fillMaxWidth().weight(1f).padding(bottom = 15.dp)) {
// 行1: relativeLayout5 (上排: 纬度 +1)
Box(modifier = Modifier.fillMaxWidth().weight(1f)) {
GridCell(text = state.grids[0], modifier = Modifier.width(80.dp).fillMaxHeight().align(Alignment.CenterStart))
GridCell(text = state.grids[1], modifier = Modifier.width(200.dp).fillMaxHeight().align(Alignment.Center))
GridCell(text = state.grids[2], modifier = Modifier.width(80.dp).fillMaxHeight().align(Alignment.CenterEnd))
}
// 行2: relativeLayout4, 205dp 高 (中排: 中央)
Box(modifier = Modifier.fillMaxWidth().height(205.dp)) {
GridCell(text = state.grids[3], modifier = Modifier.width(80.dp).fillMaxHeight().align(Alignment.CenterStart))
// 中央列: tv22 (100x80dp centerInParent) 叠在 linearLayout3 (200x200dp) 之上
Box(modifier = Modifier.width(200.dp).fillMaxHeight().align(Alignment.Center)) {
// linearLayout3: 200x200dp, marginTop 2dp, holo_blue_bright
Box(
modifier = Modifier
.align(Alignment.TopCenter)
.offset(y = 2.dp)
.size(width = 200.dp, height = 200.dp)
.background(MaterialTheme.colorScheme.surfaceVariant)
) {
// iv: 红点 10x10dp, top|left, leftMargin/topMargin 查表绝对定位
Image(
painter = painterResource(R.drawable.roam_pnt),
contentDescription = null,
modifier = Modifier
.size(10.dp)
.offset(x = state.markerLeft.dp, y = state.markerTop.dp)
)
}
// tv22: 100x80dp centerInParent, 30sp bold, textColorHighlight(白)
Text(
text = state.grids[4],
fontSize = 30.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier
.size(width = 100.dp, height = 80.dp)
.align(Alignment.Center)
)
}
GridCell(text = state.grids[5], modifier = Modifier.width(80.dp).fillMaxHeight().align(Alignment.CenterEnd))
}
// 行3: relativeLayout3 (下排: 纬度 -1)
Box(modifier = Modifier.fillMaxWidth().weight(1f)) {
GridCell(text = state.grids[6], modifier = Modifier.width(80.dp).fillMaxHeight().align(Alignment.CenterStart))
GridCell(text = state.grids[7], modifier = Modifier.width(200.dp).fillMaxHeight().align(Alignment.Center))
GridCell(text = state.grids[8], modifier = Modifier.width(80.dp).fillMaxHeight().align(Alignment.CenterEnd))
}
}
// tv4: 底部版权, 10sp 黑 (往上拉, 底部预留空间避免被导航栏遮挡)
Text(
text = "制作:US1PM 汉化:BA7LCE",
fontSize = 10.sp,
color = MaterialTheme.colorScheme.onSurfaceVariant,
textAlign = TextAlign.Center,
modifier = Modifier.fillMaxWidth().padding(bottom = 10.dp)
)
}
}
// 周边格: 60/200/60dp, #0BACF1, bold 居中, 2dp margin (照搬 tv11/tv13/tv21/tv23/tv31/tv33)
@Composable
private fun GridCell(text: String, modifier: Modifier) {
Box(
modifier = modifier
.padding(2.dp)
.background(MaterialTheme.colorScheme.surface),
contentAlignment = Alignment.Center
) {
Text(
text = text,
fontSize = 14.sp,
fontWeight = FontWeight.Bold,
color = MaterialTheme.colorScheme.onSurface,
textAlign = TextAlign.Center
)
}
}
/**
* 照搬 MainActivity.showLocation() 的状态。
* 网格/九宫格/红点查表与成品逐行一致。
*/
data class RoamingState(
val date: String = "",
val time: String = "",
val latLabel: String = "纬度",
val latValue: String = "",
val lonLabel: String = "经度",
val lonValue: String = "",
val loc: String = "",
val grids: List<String> = List(9) { "" },
/** 红点 leftMargin(dp), 经度第 3 对字符查表 */
val markerLeft: Int = 0,
/** 红点 topMargin(dp), 纬度第 3 对字符查表(屏幕 Y 反转) */
val markerTop: Int = 0,
val gpsOn: Boolean = false,
val gpsOff: Boolean = true,
val showSettings: Boolean = true,
val showProgress: Boolean = false,
val showNotice: Boolean = false,
val showDate: Boolean = false
)
/**
* 照搬 showLocation() 的完整计算: 时间/经纬度 DMS/8 位网格/九宫格/红点查表。
*/
fun roamingStateFromLocation(lat: Double, lon: Double, fixTime: Long, hourPrefix: String): RoamingState {
// 照搬: tvTime = 缓存小时前缀 + 定位时间分钟
val minute = SimpleDateFormat("mm", Locale.getDefault()).format(Date(fixTime))
val time = hourPrefix + minute
// 方向字符
val ew = if (lon >= 0.0) "E" else "W"
val ns = if (lat >= 0.0) "N" else "S"
// 照搬: 纬度 DMS (String.valueOf 无补零)
val latAbs = if (lat < 0.0) -lat else lat
val latDeg = latAbs.toInt()
val latMinFull = (latAbs - latDeg) * 60.0
val latMin = latMinFull.toInt()
val latSec = ((latMinFull - latMin) * 60.0).toInt()
val latLabel = "纬度 $latDeg° $latMin' $latSec\" $ns"
val latValue = "$lat° "
// 照搬: 经度 DMS
val lonAbs = if (lon < 0.0) -lon else lon
val lonDeg = lonAbs.toInt()
val lonMinFull = (lonAbs - lonDeg) * 60.0
val lonMin = lonMinFull.toInt()
val lonSec = ((lonMinFull - lonMin) * 60.0).toInt()
val lonLabel = "经度 $lonDeg° $lonMin' $lonSec\" $ew"
val lonValue = "$lon° "
// 照搬: 8 位网格编码 (区间查表)
val (str, str2, str3, str4) = encodeLon(lon)
val (str5, str6, str38, str9) = encodeLat(lat)
val loc = str + str5 + str2 + str6 + str3 + str38 + str4 + str9
// 照搬: 九宫格 parseInt3 五分支
val grids = buildGrids(str, str5, str2, str6, str3, str38)
// 照搬: 红点查表 (第 3 对字符)
val markerLeft = lonMargin[str3.firstOrNull()] ?: 0
val markerTop = latMargin[str38.firstOrNull()] ?: 0
return RoamingState(
time = time,
latLabel = latLabel,
latValue = latValue,
lonLabel = lonLabel,
lonValue = lonValue,
loc = loc,
grids = grids,
markerLeft = markerLeft,
markerTop = markerTop,
gpsOn = true,
gpsOff = false,
showSettings = false,
showProgress = false,
showNotice = false,
showDate = true
)
}
// 红点 leftMargin 查表 (MainActivity 1286-1386 行): 经度第 3 对 a..x -> -2..190 dp
private val lonMargin = mapOf(
'a' to -2, 'b' to 8, 'c' to 16, 'd' to 24, 'e' to 32, 'f' to 40, 'g' to 48, 'h' to 56,
'i' to 65, 'j' to 74, 'k' to 83, 'l' to 92, 'm' to 101, 'n' to 110, 'o' to 119, 'p' to 128,
'q' to 137, 'r' to 146, 's' to 155, 't' to 163, 'u' to 171, 'v' to 179, 'w' to 184, 'x' to 190
)
// 红点 topMargin 查表 (MainActivity 1315-1363 行): 纬度第 3 对 a..x -> 190..-2 dp (屏幕 Y 反转)
private val latMargin = mapOf(
'a' to 190, 'b' to 182, 'c' to 174, 'd' to 169, 'e' to 163, 'f' to 155, 'g' to 146, 'h' to 137,
'i' to 128, 'j' to 119, 'k' to 110, 'l' to 101, 'm' to 92, 'n' to 83, 'o' to 74, 'p' to 65,
'q' to 56, 'r' to 48, 's' to 40, 't' to 32, 'u' to 24, 'v' to 16, 'w' to 8, 'x' to -2
)
// 照搬 311-611 行: 经度 -> (20°区字母, 2°数字, 2'字母, 30"数字)
private fun encodeLon(inputLon: Double): List<String> {
var longitude = inputLon
var d = 0.0
val str = when {
(longitude >= -180.0) && (longitude <= -160.0) -> { longitude += 180.0; "A" }
(longitude >= -160.0) && (longitude <= -140.0) -> { longitude += 160.0; "B" }
(longitude >= -140.0) && (longitude <= -120.0) -> { longitude += 140.0; "C" }
(longitude >= -120.0) && (longitude <= -100.0) -> { longitude += 120.0; "D" }
(longitude >= -100.0) && (longitude <= -80.0) -> { longitude += 100.0; "E" }
(longitude >= -80.0) && (longitude <= -60.0) -> { longitude += 80.0; "F" }
(longitude >= -60.0) && (longitude <= -40.0) -> { longitude += 60.0; "G" }
(longitude >= -40.0) && (longitude <= -20.0) -> { longitude += 40.0; "H" }
(longitude >= -20.0) && (longitude <= 0.0) -> { longitude += 20.0; "I" }
(longitude >= 0.0) && (longitude <= 20.0) -> "J"
(longitude >= 20.0) && (longitude <= 40.0) -> { longitude -= 20.0; "K" }
(longitude >= 40.0) && (longitude <= 60.0) -> { longitude -= 40.0; "L" }
(longitude >= 60.0) && (longitude <= 80.0) -> { longitude -= 60.0; "M" }
(longitude >= 80.0) && (longitude <= 100.0) -> { longitude -= 80.0; "N" }
(longitude >= 100.0) && (longitude <= 120.0) -> { longitude -= 100.0; "O" }
(longitude >= 120.0) && (longitude <= 140.0) -> { longitude -= 120.0; "P" }
(longitude >= 140.0) && (longitude <= 160.0) -> { longitude -= 140.0; "Q" }
(longitude >= 160.0) && (longitude <= 180.0) -> { longitude -= 160.0; "R" }
else -> { longitude = 0.0; " " }
}
val str2 = when {
((longitude >= 0.0) && (longitude <= 2.0)) || ((longitude >= -20.0) && (longitude <= -18.0)) -> { d = longitude * 60.0; "0" }
((longitude >= 2.0) && (longitude <= 4.0)) || ((longitude >= -18.0) && (longitude <= -16.0)) -> { d = (longitude - 2.0) * 60.0; "1" }
((longitude >= 4.0) && (longitude <= 6.0)) || ((longitude >= -16.0) && (longitude <= -14.0)) -> { d = (longitude - 4.0) * 60.0; "2" }
((longitude >= 6.0) && (longitude <= 8.0)) || ((longitude >= -14.0) && (longitude <= -12.0)) -> { d = (longitude - 6.0) * 60.0; "3" }
((longitude >= 8.0) && (longitude <= 10.0)) || ((longitude >= -12.0) && (longitude <= -10.0)) -> { d = (longitude - 8.0) * 60.0; "4" }
((longitude >= 10.0) && (longitude <= 12.0)) || ((longitude >= -10.0) && (longitude <= -8.0)) -> { d = (longitude - 10.0) * 60.0; "5" }
((longitude >= 12.0) && (longitude <= 14.0)) || ((longitude >= -8.0) && (longitude <= -6.0)) -> { d = (longitude - 12.0) * 60.0; "6" }
((longitude >= 14.0) && (longitude <= 16.0)) || ((longitude >= -6.0) && (longitude <= -4.0)) -> { d = (longitude - 14.0) * 60.0; "7" }
((longitude >= 16.0) && (longitude <= 18.0)) || ((longitude >= -4.0) && (longitude <= -2.0)) -> { d = (longitude - 16.0) * 60.0; "8" }
((longitude >= 18.0) && (longitude <= 20.0)) || ((longitude >= -2.0) && (longitude <= 0.0)) -> { d = (longitude - 18.0) * 60.0; "9" }
else -> { d = 0.0; " " }
}
val str3 = when {
((d >= 0.0) && (d <= 5.0)) || ((d >= -120.0) && (d <= -115.0)) -> { longitude = d * 60.0; "a" }
((d >= 5.0) && (d <= 10.0)) || ((d >= -115.0) && (d <= -110.0)) -> { longitude = (d - 5.0) * 60.0; "b" }
((d >= 10.0) && (d <= 15.0)) || ((d >= -110.0) && (d <= -105.0)) -> { longitude = (d - 10.0) * 60.0; "c" }
((d >= 15.0) && (d <= 20.0)) || ((d >= -105.0) && (d <= -100.0)) -> { longitude = (d - 15.0) * 60.0; "d" }
((d >= 20.0) && (d <= 25.0)) || ((d >= -100.0) && (d <= -95.0)) -> { longitude = (d - 20.0) * 60.0; "e" }
((d >= 25.0) && (d <= 30.0)) || ((d >= -95.0) && (d <= -90.0)) -> { longitude = (d - 25.0) * 60.0; "f" }
((d >= 30.0) && (d <= 35.0)) || ((d >= -90.0) && (d <= -85.0)) -> { longitude = (d - 30.0) * 60.0; "g" }
((d >= 35.0) && (d <= 40.0)) || ((d >= -85.0) && (d <= -80.0)) -> { longitude = (d - 35.0) * 60.0; "h" }
((d >= 40.0) && (d <= 45.0)) || ((d >= -80.0) && (d <= -75.0)) -> { longitude = (d - 40.0) * 60.0; "i" }
((d >= 45.0) && (d <= 50.0)) || ((d >= -75.0) && (d <= -70.0)) -> { longitude = (d - 45.0) * 60.0; "j" }
((d >= 50.0) && (d <= 55.0)) || ((d >= -70.0) && (d <= -65.0)) -> { longitude = (d - 50.0) * 60.0; "k" }
((d >= 55.0) && (d <= 60.0)) || ((d >= -65.0) && (d <= -60.0)) -> { longitude = (d - 55.0) * 60.0; "l" }
((d >= 60.0) && (d <= 65.0)) || ((d >= -60.0) && (d <= -55.0)) -> { longitude = (d - 60.0) * 60.0; "m" }
((d >= 65.0) && (d <= 70.0)) || ((d >= -55.0) && (d <= -50.0)) -> { longitude = (d - 65.0) * 60.0; "n" }
((d >= 70.0) && (d <= 75.0)) || ((d >= -50.0) && (d <= -45.0)) -> { longitude = (d - 70.0) * 60.0; "o" }
((d >= 75.0) && (d <= 80.0)) || ((d >= -45.0) && (d <= -40.0)) -> { longitude = (d - 75.0) * 60.0; "p" }
((d >= 80.0) && (d <= 85.0)) || ((d >= -40.0) && (d <= -35.0)) -> { longitude = (d - 80.0) * 60.0; "q" }
((d >= 85.0) && (d <= 90.0)) || ((d >= -35.0) && (d <= -30.0)) -> { longitude = (d - 85.0) * 60.0; "r" }
((d >= 90.0) && (d <= 95.0)) || ((d >= -30.0) && (d <= -25.0)) -> { longitude = (d - 90.0) * 60.0; "s" }
((d >= 95.0) && (d <= 100.0)) || ((d >= -25.0) && (d <= -20.0)) -> { longitude = (d - 95.0) * 60.0; "t" }
((d >= 100.0) && (d <= 105.0)) || ((d >= -20.0) && (d <= -15.0)) -> { longitude = (d - 100.0) * 60.0; "u" }
((d >= 105.0) && (d <= 110.0)) || ((d >= -15.0) && (d <= -10.0)) -> { longitude = (d - 105.0) * 60.0; "v" }
((d >= 110.0) && (d <= 115.0)) || ((d >= -10.0) && (d <= -5.0)) -> { longitude = (d - 110.0) * 60.0; "w" }
((d >= 115.0) && (d <= 120.0)) || ((d >= -5.0) && (d <= 0.0)) -> { longitude = (d - 115.0) * 60.0; "x" }
else -> { longitude = 0.0; " " }
}
val str4 = when {
((longitude >= 0.0) && (longitude <= 30.0)) || ((longitude >= -300.0) && (longitude <= -270.0)) -> "0"
((longitude >= 30.0) && (longitude <= 60.0)) || ((longitude >= -270.0) && (longitude <= -240.0)) -> "1"
((longitude >= 60.0) && (longitude <= 90.0)) || ((longitude >= -240.0) && (longitude <= -210.0)) -> "2"
((longitude >= 90.0) && (longitude <= 120.0)) || ((longitude >= -210.0) && (longitude <= -180.0)) -> "3"
((longitude >= 120.0) && (longitude <= 150.0)) || ((longitude >= -180.0) && (longitude <= -150.0)) -> "4"
((longitude >= 150.0) && (longitude <= 180.0)) || ((longitude >= -150.0) && (longitude <= -120.0)) -> "5"
((longitude >= 180.0) && (longitude <= 210.0)) || ((longitude >= -120.0) && (longitude <= -90.0)) -> "6"
((longitude >= 210.0) && (longitude <= 240.0)) || ((longitude >= -90.0) && (longitude <= -60.0)) -> "7"
((longitude >= 240.0) && (longitude <= 270.0)) || ((longitude >= -60.0) && (longitude <= -30.0)) -> "8"
((longitude >= 270.0) && (longitude <= 300.0)) || ((longitude >= -30.0) && (longitude <= 0.0)) -> "9"
else -> " "
}
return listOf(str, str2, str3, str4)
}
// 照搬 612-914 行: 纬度 -> (10°区字母, 1°数字, 1'字母, 15"数字)
private fun encodeLat(inputLat: Double): List<String> {
var latitude = inputLat
var d2 = 0.0
var d3 = 0.0
val str5 = when {
(latitude >= -90.0) && (latitude <= -80.0) -> { d2 = latitude + 90.0; "A" }
(latitude >= -80.0) && (latitude <= -70.0) -> { d2 = latitude + 80.0; "B" }
(latitude >= -70.0) && (latitude <= -60.0) -> { d2 = latitude + 70.0; "C" }
(latitude >= -60.0) && (latitude <= -50.0) -> { d2 = latitude + 60.0; "D" }
(latitude >= -50.0) && (latitude <= -40.0) -> { d2 = latitude + 50.0; "E" }
(latitude >= -40.0) && (latitude <= -30.0) -> { d2 = latitude + 40.0; "F" }
(latitude >= -30.0) && (latitude <= -20.0) -> { d2 = latitude + 30.0; "G" }
(latitude >= -20.0) && (latitude <= -10.0) -> { d2 = latitude + 20.0; "H" }
(latitude >= -10.0) && (latitude <= 0.0) -> { d2 = latitude + 10.0; "I" }
(latitude >= 0.0) && (latitude <= 10.0) -> { d2 = latitude; "J" }
(latitude >= 10.0) && (latitude <= 20.0) -> { d2 = latitude - 10.0; "K" }
(latitude >= 20.0) && (latitude <= 30.0) -> { d2 = latitude - 20.0; "L" }
(latitude >= 30.0) && (latitude <= 40.0) -> { d2 = latitude - 30.0; "M" }
(latitude >= 40.0) && (latitude <= 50.0) -> { d2 = latitude - 40.0; "N" }
(latitude >= 50.0) && (latitude <= 60.0) -> { d2 = latitude - 50.0; "O" }
(latitude >= 60.0) && (latitude <= 70.0) -> { d2 = latitude - 60.0; "P" }
(latitude >= 70.0) && (latitude <= 80.0) -> { d2 = latitude - 70.0; "Q" }
(latitude >= 80.0) && (latitude <= 90.0) -> { d2 = latitude - 80.0; "R" }
else -> { d2 = 0.0; " " }
}
var str6 = " "
when {
((d2 >= 0.0) && (d2 <= 1.0)) || ((d2 >= -10.0) && (d2 <= -9.0)) -> { d3 = d2 * 60.0; str6 = "0" }
((d2 >= 1.0) && (d2 <= 2.0)) || ((d2 >= -9.0) && (d2 <= -8.0)) -> { d3 = (d2 - 1.0) * 60.0; str6 = "1" }
((d2 >= 2.0) && (d2 <= 3.0)) || ((d2 >= -8.0) && (d2 <= -7.0)) -> { d3 = (d2 - 2.0) * 60.0; str6 = "2" }
((d2 >= 3.0) && (d2 <= 4.0)) || ((d2 >= -7.0) && (d2 <= -6.0)) -> { d3 = (d2 - 3.0) * 60.0; str6 = "3" }
((d2 >= 4.0) && (d2 <= 5.0)) || ((d2 >= -6.0) && (d2 <= -5.0)) -> { d3 = (d2 - 4.0) * 60.0; str6 = "4" }
((d2 >= 5.0) && (d2 <= 6.0)) || ((d2 >= -5.0) && (d2 <= -4.0)) -> { d3 = (d2 - 5.0) * 60.0; str6 = "5" }
((d2 >= 6.0) && (d2 <= 7.0)) || ((d2 >= -4.0) && (d2 <= -3.0)) -> { d3 = (d2 - 6.0) * 60.0; str6 = "6" }
((d2 >= 7.0) && (d2 <= 8.0)) || ((d2 >= -3.0) && (d2 <= -2.0)) -> { d3 = (d2 - 7.0) * 60.0; str6 = "7" }
((d2 >= 8.0) && (d2 <= 9.0)) || ((d2 >= -2.0) && (d2 <= -1.0)) -> { d3 = (d2 - 8.0) * 60.0; str6 = "8" }
((d2 >= 9.0) && (d2 <= 10.0)) || ((d2 >= -1.0) && (d2 <= 0.0)) -> { d3 = (d2 - 9.0) * 60.0; str6 = "9" }
}
val str38 = when {
((d3 >= 0.0) && (d3 <= 2.5)) || ((d3 >= -60.0) && (d3 <= -57.5)) -> { d2 = d3 * 60.0; "a" }
((d3 >= 2.5) && (d3 <= 5.0)) || ((d3 >= -57.5) && (d3 <= -55.0)) -> { d2 = (d3 - 2.5) * 60.0; "b" }
((d3 >= 5.0) && (d3 <= 7.5)) || ((d3 >= -55.0) && (d3 <= -52.5)) -> { d2 = (d3 - 5.0) * 60.0; "c" }
((d3 >= 7.5) && (d3 <= 10.0)) || ((d3 >= -52.5) && (d3 <= -50.0)) -> { d2 = (d3 - 7.5) * 60.0; "d" }
((d3 >= 10.0) && (d3 <= 12.5)) || ((d3 >= -50.0) && (d3 <= -47.5)) -> { d2 = (d3 - 10.0) * 60.0; "e" }
((d3 >= 12.5) && (d3 <= 15.0)) || ((d3 >= -47.5) && (d3 <= -45.0)) -> { d2 = (d3 - 12.5) * 60.0; "f" }
((d3 >= 15.0) && (d3 <= 17.5)) || ((d3 >= -45.0) && (d3 <= -42.5)) -> { d2 = (d3 - 15.0) * 60.0; "g" }
((d3 >= 17.5) && (d3 <= 20.0)) || ((d3 >= -42.5) && (d3 <= -40.0)) -> { d2 = (d3 - 17.5) * 60.0; "h" }
((d3 >= 20.0) && (d3 <= 22.5)) || ((d3 >= -40.0) && (d3 <= -37.5)) -> { d2 = (d3 - 20.0) * 60.0; "i" }
((d3 >= 22.5) && (d3 <= 25.0)) || ((d3 >= -37.5) && (d3 <= -35.0)) -> { d2 = (d3 - 22.5) * 60.0; "j" }
((d3 >= 25.0) && (d3 <= 27.5)) || ((d3 >= -35.0) && (d3 <= -32.5)) -> { d2 = (d3 - 25.0) * 60.0; "k" }
((d3 >= 27.5) && (d3 <= 30.0)) || ((d3 >= -32.5) && (d3 <= -30.0)) -> { d2 = (d3 - 27.5) * 60.0; "l" }
((d3 >= 30.0) && (d3 <= 32.5)) || ((d3 >= -30.0) && (d3 <= -27.5)) -> { d2 = (d3 - 30.0) * 60.0; "m" }
((d3 >= 32.5) && (d3 <= 35.0)) || ((d3 >= -27.5) && (d3 <= -25.0)) -> { d2 = (d3 - 32.5) * 60.0; "n" }
((d3 >= 35.0) && (d3 <= 37.5)) || ((d3 >= -25.0) && (d3 <= -22.5)) -> { d2 = (d3 - 35.0) * 60.0; "o" }
((d3 >= 37.5) && (d3 <= 40.0)) || ((d3 >= -22.5) && (d3 <= -20.0)) -> { d2 = (d3 - 37.5) * 60.0; "p" }
((d3 >= 40.0) && (d3 <= 42.5)) || ((d3 >= -20.0) && (d3 <= -17.5)) -> { d2 = (d3 - 40.0) * 60.0; "q" }
((d3 >= 42.5) && (d3 <= 45.0)) || ((d3 >= -17.5) && (d3 <= -15.0)) -> { d2 = (d3 - 42.5) * 60.0; "r" }
((d3 >= 45.0) && (d3 <= 47.5)) || ((d3 >= -15.0) && (d3 <= -12.5)) -> { d2 = (d3 - 45.0) * 60.0; "s" }
((d3 >= 47.5) && (d3 <= 50.0)) || ((d3 >= -12.5) && (d3 <= -10.0)) -> { d2 = (d3 - 47.5) * 60.0; "t" }
((d3 >= 50.0) && (d3 <= 52.5)) || ((d3 >= -10.0) && (d3 <= -7.5)) -> { d2 = (d3 - 50.0) * 60.0; "u" }
((d3 >= 52.5) && (d3 <= 55.0)) || ((d3 >= -7.5) && (d3 <= -5.0)) -> { d2 = (d3 - 52.5) * 60.0; "v" }
((d3 >= 55.0) && (d3 <= 57.5)) || ((d3 >= -5.0) && (d3 <= -2.5)) -> { d2 = (d3 - 55.0) * 60.0; "w" }
((d3 >= 57.5) && (d3 <= 60.0)) || ((d3 >= -2.5) && (d3 <= 0.0)) -> { d2 = (d3 - 57.5) * 60.0; "x" }
else -> { d2 = 0.0; " " }
}
val str9 = when {
((d2 >= 0.0) && (d2 <= 15.0)) || ((d2 >= -150.0) && (d2 <= -135.0)) -> "0"
((d2 >= 15.0) && (d2 <= 30.0)) || ((d2 >= -135.0) && (d2 <= -120.0)) -> "1"
((d2 >= 30.0) && (d2 <= 45.0)) || ((d2 >= -120.0) && (d2 <= -105.0)) -> "2"
((d2 >= 45.0) && (d2 <= 60.0)) || ((d2 >= -105.0) && (d2 <= -90.0)) -> "3"
((d2 >= 60.0) && (d2 <= 75.0)) || ((d2 >= -90.0) && (d2 <= -75.0)) -> "4"
((d2 >= 75.0) && (d2 <= 90.0)) || ((d2 >= -75.0) && (d2 <= -60.0)) -> "5"
((d2 >= 90.0) && (d2 <= 105.0)) || ((d2 >= -60.0) && (d2 <= -45.0)) -> "6"
((d2 >= 105.0) && (d2 <= 120.0)) || ((d2 >= -45.0) && (d2 <= -30.0)) -> "7"
((d2 >= 120.0) && (d2 <= 135.0)) || ((d2 >= -30.0) && (d2 <= -15.0)) -> "8"
((d2 >= 135.0) && (d2 <= 150.0)) || ((d2 >= -15.0) && (d2 <= 0.0)) -> "9"
else -> " "
}
return listOf(str5, str6, str38, str9)
}
/**
* 照搬 917-1283 行: 九宫格 parseInt3 五分支(内部/西界/东界/北界/南界 + 四角)。
* 返回 9 格文本: [上左, 上中, 上右, 左中, 中央, 右中, 下左, 下中, 下右]。
*/
private fun buildGrids(str: String, str5: String, str2: String, str6: String, str3: String, str38: String): List<String> {
val parseInt = str2.toInt()
val parseInt2 = str6.toInt()
val str39 = str + str5
val str40 = str2 + str6
val parseInt3 = str40.toInt()
val str41 = str39 + str40
val g = MutableList(9) { "" }
fun fmt2(n: Int) = if (n <= 9) "0$n" else n.toString()
if ((parseInt > 0) && (parseInt < 9) && (parseInt2 > 0) && (parseInt2 < 9)) {
// 内部: 数字对 ±1
val i7 = parseInt3 + 1
val i8 = i7 - 10
val valueOf9 = fmt2(i8)
val str11 = i7.toString()
val valueOf18 = (i7 + 10).toString()
val i9 = parseInt3 - 10
val valueOf10 = fmt2(i9)
val valueOf19 = (parseInt3 + 10).toString()
val i10 = parseInt3 - 1
val i11 = i10 - 10
val valueOf11 = fmt2(i11)
val str14 = i10.toString()
val str15 = (i10 + 10).toString()
g[0] = str39 + valueOf9
g[1] = str39 + str11
g[2] = str39 + valueOf18
g[3] = str39 + valueOf10
g[4] = str41
g[5] = str39 + valueOf19
g[6] = str39 + valueOf11
g[7] = str39 + str14
g[8] = str39 + str15
} else if ((parseInt == 0) && (parseInt2 > 0) && (parseInt2 < 9)) {
// 西边界: 经度 0 -> 9, 经度字母 -1
val str43 = (str[0] - 1).toString() + str5
val str44 = "9" + (parseInt2 + 1)
val i12 = parseInt3 + 1
val valueOf7 = fmt2(i12)
val str12 = (i12 + 10).toString()
val sb4 = "9" + parseInt2
val valueOf20 = (parseInt3 + 10).toString()
val str46 = "9" + (parseInt2 - 1)
val i13 = parseInt3 - 1
val valueOf8 = fmt2(i13)
val str15 = (i13 + 10).toString()
g[0] = str43 + str44
g[1] = str39 + valueOf7
g[2] = str39 + str12
g[3] = str43 + sb4
g[4] = str41
g[5] = str39 + valueOf20
g[6] = str43 + str46
g[7] = str39 + valueOf8
g[8] = str39 + str15
} else if ((parseInt == 9) && (parseInt2 > 0) && (parseInt2 < 9)) {
// 东边界: 经度 9 -> 0, 经度字母 +1
val str47 = (str[0] + 1).toString() + str5
val i14 = parseInt3 + 1
val valueOf21 = (i14 - 10).toString()
val valueOf22 = i14.toString()
val str48 = "0" + (parseInt2 + 1)
val valueOf23 = (parseInt3 - 10).toString()
val str49 = "0" + parseInt2
val i15 = parseInt3 - 1
val valueOf24 = (i15 - 10).toString()
val valueOf25 = i15.toString()
val str15 = "0" + (parseInt2 - 1)
g[0] = str39 + valueOf21
g[1] = str39 + valueOf22
g[2] = str47 + str48
g[3] = str39 + valueOf23
g[4] = str41
g[5] = str47 + str49
g[6] = str39 + valueOf24
g[7] = str39 + valueOf25
g[8] = str47 + str15
} else if ((parseInt < 9) && (parseInt > 0) && (parseInt2 == 9)) {
// 北边界: 纬度 9 -> 0, 纬度字母 +1
val str50 = str + (str5[0] + 1).toString()
val i16 = parseInt3 - 9
val i17 = i16 - 10
val valueOf4 = fmt2(i17)
val valueOf26 = i16.toString()
val valueOf27 = (i16 + 10).toString()
val i18 = parseInt3 - 10
val valueOf5 = fmt2(i18)
val valueOf28 = (parseInt3 + 10).toString()
val i19 = parseInt3 - 1
val i20 = i19 - 10
val valueOf6 = fmt2(i20)
val str14 = i19.toString()
val str15 = (i19 + 10).toString()
g[0] = str39 + valueOf4
g[1] = str50 + valueOf26
g[2] = str39 + valueOf27
g[3] = str39 + valueOf5
g[4] = str41
g[5] = str39 + valueOf28
g[6] = str39 + valueOf6
g[7] = str39 + str14
g[8] = str39 + str15
} else if ((parseInt < 9) && (parseInt > 0) && (parseInt2 == 0)) {
// 南边界: 纬度 0 -> 9, 纬度字母 -1
val str52 = str + (str5[0] - 1).toString()
val i21 = parseInt3 + 1
val i22 = i21 - 10
val valueOf = fmt2(i22)
val valueOf29 = i21.toString()
val valueOf30 = (i21 + 10).toString()
val i23 = parseInt3 - 10
val valueOf2 = fmt2(i23)
val valueOf31 = (parseInt3 + 10).toString()
val i24 = parseInt3 - 1
val valueOf3 = fmt2(i24)
val str14 = (i24 + 10).toString()
val str15 = (i24 + 20).toString()
g[0] = str39 + valueOf
g[1] = str39 + valueOf29
g[2] = str39 + valueOf30
g[3] = str39 + valueOf2
g[4] = str41
g[5] = str39 + valueOf31
g[6] = str52 + valueOf3
g[7] = str52 + str14
g[8] = str52 + str15
} else if (parseInt3 == 0) {
// 四角 00: 经纬度数字均 -1 进位
val v32 = (str[0] - 1).toString()
val v33 = (str5[0] - 1).toString()
g[0] = v32 + str5 + "91"
g[1] = str39 + "01"
g[2] = str39 + "11"
g[3] = v32 + str5 + "90"
g[4] = str41
g[5] = v32 + str5 + "10"
g[6] = v32 + v33 + "99"
g[7] = str + v33 + "09"
g[8] = str + v33 + "19"
} else if (parseInt3 == 9) {
// 四角 09: 经度 -1, 纬度 +1
val v34 = (str[0] - 1).toString()
val v35 = (str5[0] + 1).toString()
g[0] = v34 + v35 + "90"
g[1] = str + v35 + "00"
g[2] = str + v35 + "10"
g[3] = v34 + str5 + "99"
g[4] = str41
g[5] = v34 + str5 + "19"
g[6] = v34 + str5 + "98"
g[7] = v34 + str5 + "08"
g[8] = v34 + str5 + "18"
} else if (parseInt3 == 99) {
// 四角 99: 经纬度数字均 +1 进位
val v36 = (str[0] + 1).toString()
val v37 = (str5[0] + 1).toString()
g[0] = str + v37 + "80"
g[1] = str + v37 + "90"
g[2] = v36 + v37 + "00"
g[3] = str39 + "89"
g[4] = str41
g[5] = v36 + str5 + "09"
g[6] = str39 + "88"
g[7] = str39 + "98"
g[8] = v36 + str5 + "08"
} else if (parseInt3 == 90) {
// 四角 90: 经度 +1, 纬度 -1
val v38 = (str[0] + 1).toString()
val v39 = (str5[0] - 1).toString()
g[0] = str39 + "81"
g[1] = str39 + "91"
g[2] = v38 + str5 + "01"
g[3] = str39 + "80"
g[4] = str41
g[5] = v38 + str5 + "00"
g[6] = str + v39 + "89"
g[7] = str + v39 + "99"
g[8] = v38 + v39 + "09"
}
return g
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 375 B

@@ -0,0 +1,124 @@
/*
* 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.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.rtbishop.look4sat.feature.roaming
import org.junit.Assert.assertEquals
import org.junit.Test
class RoamingStateTest {
// 用户实测坐标 (QTH定位器 2.0 成品截图): 22.314066 / 108.706575
private fun stateOf(lat: Double, lon: Double) =
roamingStateFromLocation(lat, lon, System.currentTimeMillis(), "21:")
@Test
fun `user coordinates produce OL42ih45`() {
val state = stateOf(22.314066, 108.706575)
assertEquals("OL42ih45", state.loc)
}
@Test
fun `red marker from ih pair matches reference lookup`() {
val state = stateOf(22.314066, 108.706575)
// 成品查表: 经度第3对 'i' -> leftMargin 65dp, 纬度第3对 'h' -> topMargin 137dp
assertEquals(65, state.markerLeft)
assertEquals(137, state.markerTop)
}
@Test
fun `grid nine from OL42 matches reference`() {
val state = stateOf(22.314066, 108.706575)
val expected = listOf(
"OL33", "OL43", "OL53",
"OL32", "OL42", "OL52",
"OL31", "OL41", "OL51"
)
assertEquals(expected, state.grids)
}
@Test
fun `latitude dms formatting matches reference`() {
val state = stateOf(22.314066, 108.706575)
assertEquals("纬度 22° 18' 50\" N", state.latLabel)
assertEquals("22.314066° ", state.latValue)
assertEquals("经度 108° 42' 23\" E", state.lonLabel)
assertEquals("108.706575° ", state.lonValue)
}
@Test
fun `time is hour prefix plus fix minutes`() {
// fixTime = 2026-08-03 21:12:13 UTC+8 -> minutes 12
val fix = 1785820333000L // 2026-08-03 21:12:13 +0800
val state = roamingStateFromLocation(22.314066, 108.706575, fix, "21:")
assertEquals("21:12", state.time)
}
@Test
fun `west edge borrows lon letter`() {
// 经度 0.5 纬度 22.314066: 数字对 02, 西边界 parseInt==0
val state = stateOf(22.314066, 0.5)
assertEquals(8, state.loc.length)
// 左列经度 0->9, 经度字母 J-1=I: IL92
assertEquals("IL92", state.grids[3])
}
@Test
fun `north edge carries lat letter`() {
// 纬度 29.5 经度 108.706575: 数字对 49? -> 纬度数字 9 -> 北边界
val state = stateOf(29.5, 108.706575)
assertEquals(8, state.loc.length)
// 上排纬度 +1 -> 0, 纬度字母 +1 (M)
assertEquals("OM40", state.grids[1])
}
@Test
fun `east edge carries lon letter`() {
// 经度 119.5 纬度 22.314066: 经度数字 9 -> 东边界
val state = stateOf(22.314066, 119.5)
assertEquals(8, state.loc.length)
// 右上 经度 9->0, 经度字母 O+1=P, 纬度 2+1=3: PL03
assertEquals("PL03", state.grids[2])
}
@Test
fun `south edge borrows lat letter`() {
// 纬度 0.5 经度 108.706575: 纬度数字 0 -> 南边界
val state = stateOf(0.5, 108.706575)
assertEquals(8, state.loc.length)
// 下中 纬度 0->9, 纬度字母 J-1=I: OI49
assertEquals("OI49", state.grids[7])
}
@Test
fun `boundary values wrap like reference`() {
// (90, 180) -> RR99xx99 (成品区间查表行为)
val state = stateOf(90.0, 180.0)
assertEquals("RR99xx99", state.loc)
}
@Test
fun `gps state after fix matches showLocation`() {
val state = stateOf(22.314066, 108.706575)
assertEquals(true, state.gpsOn)
assertEquals(false, state.gpsOff)
assertEquals(false, state.showSettings)
assertEquals(false, state.showProgress)
assertEquals(false, state.showNotice)
assertEquals(true, state.showDate)
}
}
@@ -30,6 +30,8 @@ import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.FilterChip
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
@@ -41,11 +43,13 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.rtbishop.look4sat.core.domain.model.RCSettings
import com.rtbishop.look4sat.core.domain.source.Sources
import com.rtbishop.look4sat.core.domain.model.RadioControlSettings
import com.rtbishop.look4sat.core.presentation.CardButton
import com.rtbishop.look4sat.core.presentation.LocalSpacing
@@ -203,6 +207,17 @@ fun DataSourcesDialog(
singleLine = true,
modifier = Modifier.fillMaxWidth(),
enabled = isEnabledCustomTle.value,
trailingIcon = {
IconButton(
onClick = { urlTle.value = Sources.defaultTleUrl },
enabled = isEnabledCustomTle.value
) {
Icon(
painter = painterResource(id = R.drawable.ic_reset),
contentDescription = stringResource(id = R.string.prefs_data_reset_url)
)
}
},
)
Spacer(modifier = Modifier.height(6.dp))
Row(
@@ -232,6 +247,17 @@ fun DataSourcesDialog(
singleLine = true,
modifier = Modifier.fillMaxWidth(),
enabled = isEnabledCustomTransceivers.value,
trailingIcon = {
IconButton(
onClick = { urlTransceivers.value = Sources.defaultTransceiversUrl },
enabled = isEnabledCustomTransceivers.value
) {
Icon(
painter = painterResource(id = R.drawable.ic_reset),
contentDescription = stringResource(id = R.string.prefs_data_reset_url)
)
}
},
)
Spacer(modifier = Modifier.height(12.dp))
}
@@ -22,15 +22,26 @@ import android.os.Build
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.clickable
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.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
@@ -40,15 +51,28 @@ import androidx.compose.material3.LinearProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import kotlin.math.roundToInt
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.zIndex
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalContext
@@ -299,6 +323,15 @@ private fun SettingsScreen(uiState: SettingsState, onAction: (SettingsAction) ->
)
}
item { OtherCard(uiState.otherSettings, onAction) }
item {
UiSettingsCard(
hiddenScreens = uiState.otherSettings.hiddenScreens,
screenOrder = uiState.otherSettings.screenOrder,
onToggle = { name -> onAction(SettingsAction.ToggleScreen(name)) },
onReorder = { order -> onAction(SettingsAction.ReorderScreens(order)) },
onResetOrder = { onAction(SettingsAction.ResetScreenOrder) }
)
}
item { CardCredits() }
}
}
@@ -339,6 +372,8 @@ private fun LocationCard(
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
Text(text = "Lat: ${settings.stationPos.latitude}°")
Text(text = "Lon: ${settings.stationPos.longitude}°")
}
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
Text(text = "Qth: ${settings.stationPos.qthLocator}")
}
Spacer(modifier = Modifier.height(1.dp))
@@ -484,11 +519,12 @@ private fun OtherCardPreview() = MainTheme {
@Composable
private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Unit) {
ElevatedCard(
modifier = Modifier
.fillMaxWidth()
.height(268.dp)
modifier = Modifier.fillMaxWidth()
) {
Column(modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)) {
Column(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = stringResource(id = R.string.prefs_other_title),
color = MaterialTheme.colorScheme.primary
@@ -512,8 +548,152 @@ private fun OtherCard(settings: OtherSettings, onAction: (SettingsAction) -> Uni
}
}
/**
* UI 设置卡片: 控制底部菜单栏各页面显示/隐藏。
* 顺序固定(卫星/过境/雷达/匹配/漫游/地图/设置), 关闭后其余自动靠拢;
* "设置"页固定显示(防止失去设置入口)。
*/
@Composable
private fun SwitchRow(labelResId: Int, checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
private fun UiSettingsCard(
hiddenScreens: List<String>,
screenOrder: List<String>,
onToggle: (String) -> Unit,
onReorder: (List<String>) -> Unit,
onResetOrder: () -> Unit
) {
val screens = listOf(
R.string.nav_sat to "Satellites",
R.string.nav_pass to "Passes",
R.string.nav_radar to "Radar",
R.string.nav_mutual to "Mutual",
R.string.nav_roaming to "Roaming",
R.string.nav_cw to "CwDecode",
R.string.nav_map to "Map",
R.string.nav_prefs to "Settings"
) // name 必须与 Screen.screenId 一致 (R8 安全)
ElevatedCard(modifier = Modifier.fillMaxWidth()) {
Column(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp)
) {
Text(
text = stringResource(id = R.string.prefs_ui_title),
color = MaterialTheme.colorScheme.primary
)
screens.forEach { (labelRes, name) ->
if (name == "Settings") {
// 设置页固定显示, 开关禁用
SwitchRow(labelRes, true, null)
} else {
SwitchRow(labelRes, name !in hiddenScreens) {
onToggle(name)
}
}
}
Text(
text = stringResource(id = R.string.prefs_ui_order_title),
color = MaterialTheme.colorScheme.primary
)
DragOrderList(screens = screens, screenOrder = screenOrder, onReorder = onReorder)
TextButton(onClick = onResetOrder) {
Text(text = stringResource(id = R.string.prefs_ui_order_reset))
}
}
}
}
/** 页面顺序拖拽列表: 每行右侧抓手, 按住上下拖动实时换位, 松手持久化 */
@Composable
private fun DragOrderList(
screens: List<Pair<Int, String>>,
screenOrder: List<String>,
onReorder: (List<String>) -> Unit
) {
val itemHeight = 48.dp
val itemHeightPx = with(LocalDensity.current) { itemHeight.toPx() }
// 按 screenOrder 排序(空 = 默认顺序); remember 以 screens 参数为 key, 持久化后重组
val ordered = remember(screens, screenOrder) {
screens.sortedBy { (_, name) ->
// 未知(新页面如 CwDecode 不在旧持久化顺序里): 用默认顺序位置, 再兜底最后
screenOrder.indexOf(name).let {
if (it != -1) it else com.rtbishop.look4sat.core.presentation.defaultScreenOrder.indexOf(name).let {
if (it != -1) it else Int.MAX_VALUE
}
}
}
}
val listState = rememberLazyListState()
val items = remember(ordered) { mutableStateListOf<Pair<Int, String>>().apply { addAll(ordered) } }
var draggingIndex by remember { mutableIntStateOf(-1) }
var dragOffset by remember { mutableStateOf(0f) }
// 固定高度: 嵌套在 LazyVerticalGrid 内必须给有界高度, 否则无限高度约束崩溃
LazyColumn(
state = listState,
modifier = Modifier
.fillMaxWidth()
.height(itemHeight * items.size)
) {
itemsIndexed(items, key = { _, s -> s.second }) { index, screen ->
val isDragging = draggingIndex == index
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.height(itemHeight)
.zIndex(if (isDragging) 1f else 0f)
.graphicsLayer { translationY = if (isDragging) dragOffset else 0f }
.animateItem()
) {
Text(
text = stringResource(id = screen.first),
modifier = Modifier.weight(1f)
)
Box(
modifier = Modifier
.size(48.dp)
.padding(20.dp)
.pointerInput(screen.second) {
detectDragGestures(
onDragStart = { draggingIndex = items.indexOf(screen) },
onDrag = { change, dragAmount ->
change.consume()
dragOffset += dragAmount.y
val currentIndex = draggingIndex
val target = (currentIndex + (dragOffset / itemHeightPx).roundToInt())
.coerceIn(0, items.size - 1)
if (target != currentIndex) {
items.add(target, items.removeAt(currentIndex))
dragOffset += (currentIndex - target) * itemHeightPx
draggingIndex = target
}
},
onDragEnd = {
onReorder(items.map { it.second })
draggingIndex = -1
dragOffset = 0f
},
onDragCancel = {
draggingIndex = -1
dragOffset = 0f
}
)
}
) {
// 小圆点手柄(触控区 padding 20dp 居中 -> 8dp 圆点)
Box(
modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colorScheme.onSurfaceVariant, CircleShape)
)
}
}
}
}
}
@Composable
private fun SwitchRow(labelResId: Int, checked: Boolean, onCheckedChange: ((Boolean) -> Unit)?) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
@@ -556,15 +736,11 @@ private fun CardCreditsPreview() = MainTheme { CardCredits() }
@Composable
private fun CardCredits(modifier: Modifier = Modifier) {
ElevatedCard(
modifier = modifier
.fillMaxWidth()
.height(268.dp)
modifier = modifier.fillMaxWidth()
) {
Column(
verticalArrangement = Arrangement.SpaceBetween,
modifier = Modifier
.padding(horizontal = 8.dp, vertical = 4.dp)
.fillMaxHeight()
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp)
) {
Text(
text = stringResource(id = R.string.prefs_outro_title),
@@ -573,6 +749,8 @@ private fun CardCredits(modifier: Modifier = Modifier) {
Text(
text = stringResource(id = R.string.prefs_outro_thanks)
)
// 感谢列表与保修声明之间保留约两行间距 (用户要求)
Spacer(modifier = Modifier.height(24.dp))
Text(
text = stringResource(id = R.string.prefs_outro_license),
color = MaterialTheme.colorScheme.primary
@@ -588,7 +766,7 @@ private fun TopCard(onClick: () -> Unit, modifier: Modifier = Modifier, version:
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.height(48.dp)
.heightIn(min = 48.dp)
.clickable { onClick() }) {
Spacer(Modifier)
Icon(
@@ -603,7 +781,6 @@ private fun TopCard(onClick: () -> Unit, modifier: Modifier = Modifier, version:
modifier = Modifier
.weight(1f)
.padding(end = 6.dp)
.infiniteMarquee()
)
}
}
@@ -64,6 +64,12 @@ sealed interface SettingsAction {
data class ToggleSensor(val value: Boolean) : SettingsAction
data class ToggleLightTheme(val value: Boolean) : SettingsAction
data class ToggleNightMode(val value: Boolean) : SettingsAction
// UI 设置: 切换底部导航栏页面的隐藏状态(Screen simpleName)
data class ToggleScreen(val screenName: String) : SettingsAction
// UI 设置: 更新页面顺序
data class ReorderScreens(val order: List<String>) : SettingsAction
// UI 设置: 重置为默认顺序
data object ResetScreenOrder : SettingsAction
// Remote control
data class UpdateRC(val settings: RCSettings) : SettingsAction
@@ -121,6 +121,13 @@ class SettingsViewModel(
is SettingsAction.ToggleSensor -> settingsRepo.updateOtherSettings { it.copy(stateOfSensors = action.value) }
is SettingsAction.ToggleLightTheme -> settingsRepo.updateOtherSettings { it.copy(stateOfLightTheme = action.value) }
is SettingsAction.ToggleNightMode -> settingsRepo.updateOtherSettings { it.copy(stateOfNightMode = action.value) }
is SettingsAction.ToggleScreen -> settingsRepo.updateOtherSettings { current ->
val hidden = current.hiddenScreens.toMutableList()
if (action.screenName in hidden) hidden.remove(action.screenName) else hidden.add(action.screenName)
current.copy(hiddenScreens = hidden)
}
is SettingsAction.ReorderScreens -> settingsRepo.updateOtherSettings { it.copy(screenOrder = action.order) }
SettingsAction.ResetScreenOrder -> settingsRepo.updateOtherSettings { it.copy(screenOrder = emptyList()) }
// Remote control & data sources
is SettingsAction.UpdateRC -> settingsRepo.updateRCSettings(action.settings)
is SettingsAction.UpdateRadioControl -> settingsRepo.updateRadioControlSettings(action.settings)
+2 -2
View File
@@ -1,8 +1,8 @@
[versions]
#noinspection UnusedVersionCatalogEntry
appVersionCode = "443"
appVersionCode = "450"
#noinspection UnusedVersionCatalogEntry
appVersionName = "4.4.3"
appVersionName = "4.5.0"
#noinspection UnusedVersionCatalogEntry
compileSdk = "37"
#noinspection UnusedVersionCatalogEntry
+2
View File
@@ -21,10 +21,12 @@ include(
":core:presentation"
)
include(
":feature:cw",
":feature:map",
":feature:mutual",
":feature:passes",
":feature:radar",
":feature:roaming",
":feature:satellites",
":feature:settings"
)