Skip to content

ADFA-2436: Add Maps (offline OSM) plugin - #22

Open
fryanpan wants to merge 75 commits into
mainfrom
feature/ADFA-2436-maps-plugin
Open

fryanpan wants to merge 75 commits into
mainfrom
feature/ADFA-2436-maps-plugin

Conversation

@fryanpan

@fryanpan fryanpan commented Jun 5, 2026 •

Copy link
Copy Markdown
Contributor

ADFA-2436: Add Maps (offline OSM) plugin

Ticket

ADFA-2436

Description

Adds a Maps plugin that helps developers make a working offline mapping app in a few minutes.

Lets the developer download an OpenStreetMap (OSM) map region from an Internet in A Box (IIAB) server and build a read-only mapping app (annotation left for a future ticket)

Workflow

  1. User installs the Maps plugin in CodeOnTheGo
  2. Creates a New Project using the "Offline OSM Map" template (template provided by the plugin)
  3. In the Maps tab in the bottom sheet, they can download one or more regions and make this region activeThis downloads map data either from an IIAB server either on the internet, or local LAN
  4. Build the project
  5. Run the new app!

You need internet for steps 1 and 3, but the developer can start working offline after that. And the generated app also works offline.

Details by Commit

I asked Claude to reorganize the work into commits, to try and make a big PR easier to review. Each file is in exactly one commit. Some commits could benefit from more review than others.

In the "Files Changed" tab above, you can review by commit to get a more organized view:

Commit What it is LoC Review Notes
3825501 README + HTML tutorial + plugin registration ~500 read Intro and docs. The tutorial could probably use another edit pass at some point -- lmk if there's anything to change here.
9c2f4fd Scaffold + build config + entry point ~1,200 skim Gradle, JaCoCo + test deps, manifest (incl. the min_ide_version pin with no max), MapsPlugin. Mostly boilerplate.
d762e23 domain + util (+ tests) ~1,400 read The readable, correctness-critical pure logic — bbox/zoom math, region-id validation, atomic file writes. No Android deps, ~100% unit-tested.
4dce5dc slicer — PMTiles bbox extraction (+ tests) ~4,100 skim This is the logic to parse the 87GB single PMTiles file in IIAB's OpenStreetMap of the whole world. I let Claude implement this...but if it gets too hard to maintain/is buggy, we can bring in the pmtiles CLI that IIAB uses (will increase plugin size a bit)
bed67ac data layer (+ tests) ~2,700 read Region cache, downloader (refactored with injectable IO seams), installer, stores. Path-traversal + atomic-write guards.
1f10ddd UI — tab, region manager, bbox picker, wizard ~4,700 read UI code. Tested in E2E, not fully unit-tested.
e239546 project template + emitter (+ tests) ~2,600 read The emitter + the Kotlin and Java template sources for the generated region-map app. Includes a simple local HTTP for serving tiles (see notes below)
3fa84c1 offline-build support (vendored MapLibre repo) ~27,800 (199 files) skim briefly Vendored Maven repo (AARs + POMs). Verify licenses + the artifact set, not line-by-line. The reversible decision (alternative: require the user to be online for the first build).
d45ed3b bundled assets ~180 skim Natural Earth basemap (CC0), Noto fonts (OFL), day/night icons (CC0), THIRD_PARTY_LICENSES. Check attribution + licensing is reasonable, not code.

Testing

Automated checks below in the table, plus I've manually tested the flow + reviewed the code.

Gate Status
Build clean (assemblePlugin + test) ✅ green — 592 tests, 0 failures (3 skipped), at 7f5f63b
≥90% non-UI coverage + quality ✅ 98.1% line / 90.7% branch on non-UI code, at 7f5f63b; 8-check quality assessment
android-qa device walk ✅ test-cases.md reviewed; device-verified; StrictMode noise traced to CoGo host (ADFA-4121), not the plugin
UX review ✅ on the working e2e flow (2026-06-01); no UX changes this session
License audit ✅ 0 blockers; hilbert-curve attribution gap fixed
Code review ✅ 0 blockers; ran code review with Claude and Codex CLI
New plugin review skill ✅ GREEN-LIGHT — 7/7 rubric clauses Pass, security clean
Completeness sweep ✅ clean (naming / metadata / icons / help / deploy)
E2E Video Recording ✅ See links below in E2E section

P.S. The automated checks are a bit messy and defined in the wrapper repo. I'll make a future ticket to look into cleaning them up and adding them into the plugin repo. Let me know if there are parts of this you think are especially important (or not important!)

Unit Tests

Didn't spend too much effort trying to get to 100% coverage. Did try to get over 90% on most non-UI code....but it was a bit hard to test some of the areas that required Android integration or mocking out CodeOnTheGo integration. So I left it to the E2E tests to cover these areas.

./gradlew jacocoTestReport

Coverage by folder, measured 2026-06-04 (the totals in the Testing table are re-measured at 7f5f63b; full breakdown + the 8-check quality assessment in coverage-quality-assessment-2026-06-04.md):

Folder Line Branch
domain/ 100% 98%
slicer/ 100% 88%
util/ 96% 100%
data/ 77% 79%
templates/ 68% 56%
maps/ (MapsPlugin) 62% 29%
ui/ 0% (device-tested) —
Pure non-UI logic (excl. the 4 Android-host-bound classes) 98.8% 89.3%

On-Device Automated E2E QA Walk

Was using this method regularly during development to have Claude do end-to-end testing using mobile-mcp.

The test cases cover the whole flow from installing the plugin, using the project template, downloading and managing map regions, building the templated app offline, running the templated app, and testing the offline map in the templated map. The tests also went through both Kotlin and Java project templates. Checked at font scale 1.0 and 2.0 on a Galaxy A06.

Device recordings from running the tests:

Size and network

.cgp is 41.1 MB at 7f5f63b, arm64-v8a + armeabi-v7a.

Network: the Internet source downloads tile ranges from iiab.switnet.org and IIAB's maps-catalog.json from raw.githubusercontent.com; the LAN source talks only to the box you pick. Nothing else leaves the device.

StrictMode

Was getting a few warnings about this a few days ago (e.g. warnings/errors during project creation)...did a cleanup pass. The plugin should be StrictMode clean now -- no main-thread I/O. Sockets are traffic-stats-tagged.

But creating a project still triggers StrictMode warnings that are coming from the CodeOnTheGo side. Added a comment to ADFA-4121.

Notes & Questions for Reviewers

  • Ease of Testing + CoverageCouple of followup items would help in the future. I'll add some followup tickets:

    • Adding Kaspresso testing to the plugin repo (installs some version of CoGo and then installs + tests plugin) would helpThis would cover 80-90% of the E2E tests I ran
    • Adding mobile-mcp testing + the android QA skill from wrapper repo would also help with the tests that are a bit harder to arrange in Kaspresso (e.g. that require building an app and then starting the new app)
  • Decisions to Note

    • Offline Build Support: Decided it might be worth having in ~15 MB of vendored MapLibre libraries so a developer can build the templated map app offline. It may also be okay to drop that 15 MB download and ask the user to build once while they still have internet.
    • PMTiles SlicerClaude made a case that it could do this quickly and would save 5+ MB of CLI download of the pmtiles Go CLI that IIAB uses. So Claude was quite fast...and the code seems to work. But it also ended up being 4k lines! It might be better in the long run to rip this out and switch to the CLI to stay similar to IIAB.
    • Local HTTP Server: Ran into an issue getting MapLibre to read local-filesystem map tiles. In the MapLibre forums, running a simple local HTTP server seemed like an acceptable workaround. Maybe with more digging (or a future MapLibre version) it won't be needed.
  • Future Improvements — Two nice-to-haves tracked but not blocking v1

    • GPS location permission in CoGo's manifest so the bbox picker can show current location (needs a host change)
    • Let the user download their first region during New Project creation for a smoother one-step flow (complex — requires async work inside the Pebble wizard; current two-step create→Maps-tab→download is good enough for v1).
    • Followups: ADFA-6840, ADFA-6841, ADFA-6842, ADFA-6843, ADFA-6844

🤖 Generated with Claude Code

https://claude.ai/code/session_014Ed1X3QzdWExhALiU221EG

@fryanpan
fryanpan marked this pull request as ready for review June 5, 2026 16:13

@jatezzz jatezzz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some of these files are quite long. Please consider splitting them into smaller, more focused files to improve readability and adhere to the Single Responsibility Principle (SRP).

@fryanpan

Copy link
Copy Markdown
Contributor Author

Some of these files are quite long. Please consider splitting them into smaller, more focused files to improve readability and adhere to the Single Responsibility Principle (SRP).

@jatezzz thanks for reviewing. I already worked with Claude to do a couple of cohesion and coupling passes. Broke up many things and made them more testable.

Looks like I missed one file...BboxFragmentPicker needs help.

And I'll try to break up the RegionManagerFragment too...that popped up as a borderline judgement call last time. There's probably a better way to organize the wizard steps.

Anything else you spotted?

Also is there a guideline for how this team applies SRP? It's one of the more vague rules and I'm curious how you and the team apply it.

The more specific we can be, the more likely it is that agents can plan for and build the right thing on the first try.

@jatezzz

jatezzz commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

@fryanpan Thanks for the open response and for doing those passes with Claude!

Regarding the files: BboxFragmentPicker and RegionManagerFragment are definitely the main ones. For the wizard, splitting it into separate smaller fragments (or custom views) orchestrated by a single shared ViewModel or a state machine usually keeps things much cleaner. I didn't spot any other major offenders, but those two are a great place to start!

You make a fantastic point about SRP being vague, and I completely agree that being specific helps both humans and AI agents. Here is how we generally look at SRP pragmatically on this team:

  1. Separation of Concerns (The "UI vs. Logic" rule): Fragments/Activities should strictly handle OS lifecycle, UI bindings, and user input routing. If a Fragment is formatting data, making decisions about business rules, or directly touching file I/O/Network (which also ties into the Dispatchers.IO comment), it's violating SRP. That logic belongs in a ViewModel or a UseCase/Domain class.
  2. The "And" Heuristic: If you describe what a class does and have to use the word "AND" (e.g., "It picks a bounding box AND formats the coordinates AND saves them to disk"), it probably has too many responsibilities.
  3. Dependency Bloat: If a class or ViewModel requires injecting more than 4-5 repositories or services to do its job, it’s usually a sign that it’s orchestrating too much and should be broken down into smaller, focused UseCases.
  4. Line Count (Soft limit): While not a hard rule, if a file starts crossing the 300-400 line mark, it's usually a strong smell that it's accumulating responsibilities.

I love your point about agents planning better with specific rules. We should probably document these exact heuristics in a guide file so the AI tools can pick them up automatically as context.

Let me know how the refactoring goes with those two fragments!

fryanpan added a commit that referenced this pull request Jul 13, 2026
…(ADFA-2436)

Responds to the review on PR #22.

Dispatchers.IO (3 inline comments). Every entry point in data/ is now
`suspend` + `withContext(Dispatchers.IO)` — ActiveRegionStore, RegionCache and
FirstRegionAutoActivator (the three called out), plus RegionDownloader,
RegionInstaller, ProjectRegionCoordinator, MapFontExtractor, RegionSizeEstimator
and ReachabilityProbe. The dispatch is pushed down into the data layer rather
than wrapped at the Fragment call sites, so a caller can't do file I/O on Main.

SRP / long files. Business logic pulled out of the Fragments:
  - domain/ (pure Kotlin, JVM-unit-tested): AutoShrinkBbox, ZoomFit, ZoomLabel,
    EstimateDisplay, IiabStyleBuilder, RegionWizardStateMachine
  - data/: RegionSizeEstimator, ProjectRegionCoordinator, ReachabilityProbe
  - ui/: BboxSelectionModel, MapLocationController, BboxPickerArgs
  - slicer/: TileMath extracted from PmtilesRegionSlicer
The download wizard is now per-step Fragments (SourcePicker, DownloadProgress,
Step3Save) orchestrated by RegionWizardStateMachine, per the reviewer's
suggestion.

Line counts, code only (comments + blanks stripped):
  BboxPickerFragment    1320 -> 762 raw / 446 code
  RegionManagerFragment  647 -> 542 raw / 401 code
  PmtilesRegionSlicer    643 -> 566 raw / 349 code

Also in this pass: dark-mode correctness, and dead branding tokens stripped from
PluginTheme (plugins take the host's Material colors — they can't re-theme
host-loaded Material widgets through plugin theme attrs).
@fryanpan

fryanpan commented Jul 13, 2026 •

Copy link
Copy Markdown
Contributor Author

Re: SRP

Tried to address SRP comments by splitting up UI vs. logic and break out responsibilities more sensibly:

  • BboxPickerFragment
    • Building the map style / IIAB tile source
      • ~160 lines moved to domain/IiabStyleBuilder
    • Location permissions + "center on me"
      • ~140 lines moved to ui/MapLocationController
    • Region size estimate
      • ~80 lines of logic moved to data/RegionSizeEstimator
      • ~50 lines of label formatting moved to domain/EstimateDisplay
    • Selection state
      • ~100 lines moved to ui/BboxSelectionModel
      • Includes bbox interaction logic (current box, zoom caps, estimate state and the transitions between them), previously scattered Fragment fields
    • Other misc. cleanup
      • ~45 lines of launch args to BboxPickerArgs
      • ~30 lines of MapLibre bootstrap to MapLibreBootstrap
      • ~30 lines of zoom math to ZoomLabel
    • LoC: 1320 → 762 (446 without comments)
      • Still a bit big, but smaller than before
  • RegionManagerFragment
    • Wizard orchestration
      • ~140 lines of step state and transitions moved to domain/RegionWizardStateMachine
    • The logic & IO to read/write active region for a project
      • ~50 lines moved out to data/ProjectRegionCoordinator
    • Download-result → snackbar-message mapping
      • ~20 lines moved to data/DownloadCompleteMessage, now pure and tested
    • LoC: 647 → 542

Re: Documentation

Will try to get more about SRP guidelines into a draft PR on the plugin-examples side, and also CoGo side.

@fryanpan fryanpan closed this Jul 13, 2026
@fryanpan fryanpan reopened this Jul 13, 2026
@fryanpan

fryanpan commented Jul 13, 2026 •

Copy link
Copy Markdown
Contributor Author

How this was tested

Automated

  • 440 JVM unit tests, up from 342 — the classes extracted in the refactor are now independently tested
  • ./gradlew clean assemblePlugin test green on JDK 17

On device — Samsung A56 (Android 16), plugin installed from a clean state, both template languages:

  • Kotlin — new project from the "Offline OSM Map" template
  • Java — full flow: create project → Maps tab → download + activate a region (SF Bay area, 98.6 MB) → build → install → launch → map renders with street names

Recordings

@jatezzz jatezzz left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

Overview

A new headless-plus-UI plugin that adds a Maps bottom-sheet tab and an Offline OSM Map project template. The interesting engineering is range-slicing a bbox out of IIAB's ~87 GB global PMTiles archive rather than downloading it, then bundling the slice into a generated MapLibre app that serves its own tiles over a loopback HTTP server. Region cache is shared across projects at /sdcard/CodeOnTheGo/maps/<id>/.

This is high-quality work. The comments explain why consistently (the native.code permission, the pmtiles://http:// scheme constraint, the ConnectivityReceiver.setConnected(true) hack, Pebble's newline trimming) — that's the kind of documentation that saves the next person a device round-trip. Day/night theming follows the reference pattern exactly (values-night/, themedPluginInflater + onGetLayoutInflater on all five fragments, zero hardcoded hex, zero hardcoded layout strings). Path-traversal guards are layered (regex validation plus canonical containment) in every path that resolves under the cache root.


Blocking

1. Tooltip category is wrong → tab tooltip will render n/a

maps/src/main/kotlin/org/appdevforall/maps/MapsPlugin.kt:189

override fun getTooltipCategory(): String = "plugin_maps"

The category must be "plugin_" + the full plugin.id, i.e. "plugin_org.appdevforall.maps". The host registers entries under the returned string but derives plugin_<pluginId> when resolving a lookup, so a short slug silently mismatches and long-press shows the literal string n/a. The build stays green and the DB row is present — only device long-press catches it.

override fun getTooltipCategory(): String = "plugin_$PLUGIN_ID"

MapsPluginCoverageTest.kt:188 asserts the wrong value too, so it needs updating with the fix. Worth noting: several existing plugins in this repo (bookshelf, random-xkcd, keystore-generator) have the same short-slug shape, while compose-preview, sketch-to-ui-plugin, and apk-viewer derive from the id. If device testing showed the Maps tooltip working, then the host is more lenient than documented — say so and I'll drop this — but the documented contract and the majority-correct plugins both point at deriving from PLUGIN_ID, which is free to do either way.

2. Template tooltip tag has no registered entry

MapTemplateBuilder.kt:63,146 attach tooltipTag = "maps.template.region_map" to the template, but getTooltipEntries() (MapsPlugin.kt:191) returns exactly one entry, tagged maps.bottom_sheet.maps_tab. The New Project card's tooltip has nothing to resolve → n/a. Add a second PluginTooltipEntry for the template tag.

3. Hard-coded upstream archive date breaks every download on rotation

RegionDownloader.kt:54,327

private const val FALLBACK_VECTOR_DATE = "2026-04-01"
...
"openstreetmap-openmaptiles.$FALLBACK_VECTOR_DATE.z00-z14.pmtiles"

The constant is named FALLBACK_ but it's the only path — there is no discovery. HttpRangeByteCache.clear()'s own comment says "IIAB rotates the same URL weekly"; when that rotation drops the 2026-04-01 file, every download 404s with no recovery and the plugin looks broken in the field.

The fix is already half-built: ReachabilityProbe.buildLanProbeUrl (ReachabilityProbe.kt:47) HEAD-probes /maps/extracts.json, "the canonical IIAB inventory". Read that inventory to resolve the current filename and keep the constant as a genuine fallback.


Should fix

4. Play Services dependency contradicts the template and can crash on GMS-free devices

build.gradle.kts:92 pulls play-services-location:21.3.0, and MapLocationController.kt:165 calls LocationServices.getFusedLocationProviderClient(ctx) — the one unguarded call in a file that runCatchings everything else. On a de-Googled or AOSP device (a meaningful slice of ADFA's low-end target audience), client construction can throw before either listener is attached, and it propagates out of fetchLastKnownLocation().

Meanwhile the generated app deliberately avoids GMS — app/build.gradle.kts.peb says "The map's current-location dot uses MapLibre's built-in default location engine, so no Play Services Location dependency is needed", and MapRegionActivity proves it works. The plugin's own enableLocationComponent already uses useDefaultLocationEngine(true). Drop the dependency and use the existing fallbackToLocationManager() walk as the primary path — smaller .cgp, consistent with the template, and no GMS assumption. At minimum, wrap the client creation in runCatching { … } ?: fallbackToLocationManager().

5. PmtilesHttpServer: partial skip serves wrong bytes under a correct-looking header

templates/region-map/app/src/main/java/PACKAGE_NAME/PmtilesHttpServer.kt.peb:131-137

while (skipped < rangeStart) {
    val sk = fis.skip(rangeStart - skipped)
    if (sk <= 0) break          // <-- then serves from the wrong offset
    skipped += sk
}

InputStream.skip returning 0 does not imply EOF. On a short skip the loop breaks and the body is streamed from wherever the cursor landed, while the already-written Content-Range header promises rangeStart-rangeEnd. MapLibre gets structurally valid but wrong PMTiles bytes — silent tile corruption that is very hard to trace. Fail the request instead (500, or 416) rather than serving mismatched data.

Two smaller items in the same file:

  • No setSoTimeout on the accepted socket, feeding an unbounded newCachedThreadPool. A hung client pins a thread indefinitely. Loopback keeps the risk low, but this is template code students will copy.
  • Suffix ranges are silently ignored: if (dash > 0) (line 95) skips bytes=-500, which then serves the whole file under a 200. MapLibre doesn't emit suffix ranges, so this is latent, not live.

Worth an explicit note in osm-tutorial.html: this server is unauthenticated and reachable by any app on the device. It's harmless here (public map tiles already world-readable inside the APK), but the pattern is teaching material and a student who points rootPrefix at something sensitive has built a local data leak. One sentence in the tutorial and a comment on the class would cover it.

6. The 206 branch lacks the size cap the 200 branch has

RangeFetcher.kt:146-154. The 200 fallback carefully refuses unknown-length or oversized bodies before buffering (lines 162-168) — good. The 206 branch calls body.bytes() unconditionally and only checks the size in a require after the whole body is in memory. A misbehaving or hostile LAN IIAB host answering 206 with a multi-GB body OOMs the process. Check body.contentLength() against length before reading, mirroring the 200 path.

Also line 149: bytes.size == length || bytes.size == (end - offset + 1).toInt() — end is defined as offset + length - 1, so the second disjunct is always identical to the first. Dead condition.

7. Rebase before merge

13 commits behind main. Both README.md (the Examples table) and .github/workflows/update-libs.yml (the MAP array) insert into regions that main has since changed, so expect conflicts in exactly those two spots. Trivial to resolve, but better done by the author than at merge time.


Polish

  • Bundled Gradle wrapper. maps/gradlew, maps/gradlew.bat, maps/gradle/wrapper/ duplicate the repo-root wrapper. Repo convention is to use the root one (cd maps && ../gradlew assemblePlugin), as flutter-template does. The ../libs/*.jar references are correct — just the wrapper to delete.
  • .cgp size. ~24 MB of assets before the plugin's own MapLibre .so. Two concrete trims: the vendored repo ships three kotlin-stdlib versions (1.7.10, 1.8.22, 2.2.10) where resolution needs one — ~3 MB; and the template pins abiFilters.add("arm64-v8a") with a clear rationale, but the plugin's own build.gradle.kts doesn't, so the .cgp carries every ABI MapLibre's AAR publishes. Applying the same filter to the plugin is consistent and free.
  • arm64-only generated app. Worth stating in the tutorial and template README — a student who reaches for an x86_64 emulator instead of a device gets a missing-.so failure, and the reason lives only in a Gradle comment.
  • Hardcoded English strings. MapLocationController.kt:126-144 — setTitle("Location is off for Code on the Go"), "Open Settings", "Not now", and the long message. The sibling maybeRequestLocation() (lines 107-117) correctly uses R.string.maps_location_perm_*. These are user-facing UI, so they belong in strings.xml alongside the rest; it's the only such offender in the plugin.
  • Bbox has no equals/hashCode/toString. It's a plain class (Bbox.kt:17). RegionDownloader.kt:123 logs bbox=$bbox, which prints Bbox@1a2b3c4 — the download log line loses its most useful field. Note the contrast with RegionInfo, where the hand-rolled equals/hashCode for the DoubleArray field is done carefully and correctly. Making Bbox a data class fixes the log and forecloses any future reference-equality surprise.
  • Delete keys off the meta-declared id. RegionManagerFragment.kt:505 calls RegionCache.delete(info.regionId), where regionId comes from meta.optString("regionId", dir.name) (RegionCache.kt:203). /sdcard is world-writable, so a planted meta.json can name a different valid region and delete the wrong directory. Not traversal — the containment check holds — but info.directory is already canonicalized and unambiguous, so passing that instead removes the class of bug.
  • HttpRangeByteCache.get hands back the internal array. RangeFetcher.kt:73 returns the cached ByteArray by reference from a process-global cache, though the KDoc calls them "instant in-memory copies". No current caller mutates it, but a .copyOf() (or a comment) would make that safe by construction rather than by convention. Separately, put's containsKey early-return (line 80) doesn't promote in an access-order map, so re-requesting a cached range doesn't refresh its LRU position.
  • compileSdk/targetSdk = 34. #54 is standardizing the toolchain across plugins — coordinate so this doesn't land needing an immediate follow-up.
  • mkdirs() precedes the containment check. ProjectMapEmitter.kt:90-95 creates the directory then verifies it doesn't escape. MAPS_ASSETS_SUBPATH is a constant so it can't, but ordering the check first makes the guard real rather than decorative.
  • Doc/code mismatch. Bbox.heightKm() (line 35) is documented as "latitude span × 111.32"; the body calls haversineKm.
  • Cleartext to LAN hosts. RegionDownloader.base() defaults to http:// for LAN (correct — IIAB boxes rarely run TLS), but the plugin declares no networkSecurityConfig and rides the host's. Worth confirming CoGo permits cleartext to arbitrary LAN hosts, or LAN downloads will fail on a host that doesn't. The template correctly scopes its own config to 127.0.0.1.

Testing

359 tests passing, 98.8% line / 89.3% branch on non-UI logic. The injectable-seam design (downloadInto's copyBasemap/sliceTiles/nowMillis, ProjectMapEmitter's Android-free purity, RegionCache's *FromRoot overloads) is the right way to get this testable without Robolectric, and it's applied consistently. UI at 0% with device E2E is a reasonable call and honestly labeled.

Gaps worth a follow-up: the PMTiles slicer is 4k lines of binary-format parsing whose failure mode is silently wrong tiles rather than a crash, and the tests exercise it against synthetic archives. A golden-file test — slice a known bbox from a checked-in small PMTiles and assert the output byte-for-byte against a pmtiles CLI extract — would be the cheapest guard against a regression here, and would also de-risk the "rip this out for the CLI later" option the PR description raises. I'd also add a regression test for #5 (a short-skip Range request) since the symptom is corruption rather than an exception.

Verdict

Approve after #1–#3. Those are all small, mechanical fixes, and #1/#2 are the class of defect that only device long-press catches — worth doing before this ships. #4–#6 are the ones I'd want addressed in this PR rather than deferred; the rest can be follow-ups.

I have not built or device-tested this — those findings come from reading the diff, so #1 in particular is worth a quick long-press check on the Maps tab to confirm.

🤖 Generated with Claude Code

fryanpan and others added 18 commits October 26, 2026 13:42
…t tests)

Hand-rolls what go-pmtiles' `pmtiles extract` does (bbox → minimal v3 archive),
over HTTP range requests against the IIAB-hosted global archive, because we can't
ship/shell-out to a per-arch Go binary on-device. The code is dense (Hilbert-curve
tile-id ranges, directory walking, run-length entries) but isolated behind a small
surface and heavily unit-tested — trust the tests. If it proves hard to maintain,
it's swappable for the `pmtiles` Go CLI.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
From the license-audit + code-review DoD pass (both 0-blocker):
- THIRD_PARTY_LICENSES.txt: add com.github.davidmoten:hilbert-curve:0.2.3 and its
  transitives (guava-mini, listenablefuture) — Apache-2.0 runtime deps bundled in
  the .cgp that were the one un-attributed entry.
- SourcePickerFragment.persistLanHost: use applicationContext (matching the off-main
  read) so the write shares the one process-wide SharedPreferencesImpl — a future
  reorder can't reintroduce a first-access disk read on Main.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…(ADFA-2436)

Responds to the review on PR #22.

Dispatchers.IO (3 inline comments). Every entry point in data/ is now
`suspend` + `withContext(Dispatchers.IO)` — ActiveRegionStore, RegionCache and
FirstRegionAutoActivator (the three called out), plus RegionDownloader,
RegionInstaller, ProjectRegionCoordinator, MapFontExtractor, RegionSizeEstimator
and ReachabilityProbe. The dispatch is pushed down into the data layer rather
than wrapped at the Fragment call sites, so a caller can't do file I/O on Main.

SRP / long files. Business logic pulled out of the Fragments:
  - domain/ (pure Kotlin, JVM-unit-tested): AutoShrinkBbox, ZoomFit, ZoomLabel,
    EstimateDisplay, IiabStyleBuilder, RegionWizardStateMachine
  - data/: RegionSizeEstimator, ProjectRegionCoordinator, ReachabilityProbe
  - ui/: BboxSelectionModel, MapLocationController, BboxPickerArgs
  - slicer/: TileMath extracted from PmtilesRegionSlicer
The download wizard is now per-step Fragments (SourcePicker, DownloadProgress,
Step3Save) orchestrated by RegionWizardStateMachine, per the reviewer's
suggestion.

Line counts, code only (comments + blanks stripped):
  BboxPickerFragment    1320 -> 762 raw / 446 code
  RegionManagerFragment  647 -> 542 raw / 401 code
  PmtilesRegionSlicer    643 -> 566 raw / 349 code

Also in this pass: dark-mode correctness, and dead branding tokens stripped from
PluginTheme (plugins take the host's Material colors — they can't re-theme
host-loaded Material widgets through plugin theme attrs).
main moved every plugin under plugins/<MixedCase>/ and gates PRs on
`addons check`. This moves maps/ there, drops the bundled Gradle wrapper
for the repo-root one (John's review, polish item 1), points the jar
references at ../../libs, renames the artifact to maps.cgp, adds
addon.json, fixes the page title and spells out Code on the Go, and adds
the README Examples row.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DEPcb7gdHpRHRHXVqNnbf
… SDK 36) (ADFA-2436)

check-toolchain.sh on main requires the toolchain Code on the Go ships
on-device, and #54's standardization landed while this PR was open
(John's polish item on compileSdk/targetSdk 34). AGP 9 compiles Kotlin
itself, so the kotlin-android plugin goes.

Two knock-on fixes:
- plugin-api on main added 13 PluginContext members. Test fakes now
  delegate to one throwing UnusedPluginContext and override what they use.
- AGP 9 writes Kotlin classes under intermediates/built_in_kotlinc, so the
  JaCoCo report was covering zero classes and still passing. It points at
  the new path and fails when it finds no classes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DEPcb7gdHpRHRHXVqNnbf
The host registers plugin tooltip entries under getTooltipCategory() but
derives "plugin_" + plugin.id when it looks one up, so "plugin_maps"
never matched and long-press on the Maps tab rendered n/a. The entry tag
now uses the shared TOOLTIP_TAG_MAPS_TAB constant.

Review: John, blocker 1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DEPcb7gdHpRHRHXVqNnbf
…-2436)

MapTemplateBuilder tags the Offline OSM Map card with
maps.template.region_map, but getTooltipEntries() only returned the tab's
entry. The tag now lives in MapsPlugin beside the tab's, and both have
entries.

The card itself still will not find it: the host's New Project screen
looks card tags up under the IDE category (TemplateListFragment ->
showIdeCategoryTooltip), so this needs a host change too. The KDoc says
so.

Review: John, blocker 2.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DEPcb7gdHpRHRHXVqNnbf
Every download used openstreetmap-openmaptiles.2026-04-01.z00-z14.pmtiles,
named by a constant called FALLBACK_ but with no discovery in front of it.
The mirror publishes a new dated archive each month (2026-10-01 is current)
and nothing guarantees the old one stays.

The internet source now reads data.vector["14"] from IIAB's
maps-catalog.json, the inventory IIAB's own region extractor uses. The
entry must be a dated z00-z14 name on iiab.switnet.org/maps/2/, the body is
capped at 256 KB, and the result is memoized per process so the size
estimate and the download read the same file. When the catalog cannot be
read the 2026-04-01 name stays as a real fallback. The archive date is
recorded as the region's source version.

LAN keeps the fallback name: extracts.json lists a box's local regions,
not the global archive, so it cannot supply this file name.

Review: John, blocker 3.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DEPcb7gdHpRHRHXVqNnbf
…(ADFA-2436)

MapLocationController built a FusedLocationProviderClient without a guard,
so on a device without Google services it could throw before either
listener was attached. The generated app already avoids GMS, and the
plugin's own blue dot uses MapLibre's default engine. The LocationManager
walk (passive, network, gps) that was the fallback is now the only path,
and the dependency and its proprietary-license entry go.

Review: John, should-fix 4.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DEPcb7gdHpRHRHXVqNnbf
…A-2436)

InputStream.skip may return 0 before the end of the stream. The generated
app's PmtilesHttpServer took that as a stop and streamed the body from
wherever the cursor was, under a Content-Range header that promised
rangeStart-rangeEnd: silently wrong PMTiles bytes. Both language variants
now skip with skipFully, which reads one byte to tell a short skip from the
end, and do it before writing any header so a failure answers 500.

The test compiles the shipped Java template against the unit-test
classpath and runs skipFully on a stream whose skip always returns 0.

Review: John, should-fix 5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018DEPcb7gdHpRHRHXVqNnbf
fryanpan and others added 26 commits October 27, 2026 10:25
…ively (ADFA-2436)

The .module guard compared only size and sha256, and as sets, so a stale
sha1 or md5 passed, and so did one file entry going stale while the other
stayed right. It now computes md5, sha1, sha256 (and sha512 once one is
recorded) from the AAR on disk and checks each against every file entry.

The doc test only failed on the exact old sentence "64-bit ARM phones
only", so dropping the 32-bit claim or rewording the restriction passed.
It now requires "64-bit and 32-bit ARM phones" and both ABI names in the
README and the tutorial, and fails on any "64-bit ARM ... only" or
"not install on a 32-bit" wording.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S8WVMAhapvazMLrZ7XozrL
…DFA-2436)

IIAB moved its maps catalog to openstreetmap-openmaptiles.2026-10-01 on
2026-10-06. The plugin reads the current name from that catalog online, but
falls back to FALLBACK_VECTOR_ARCHIVE when the catalog is unreachable and
always uses it for an IIAB LAN box, so the fallback was still sending those
downloads to the April build. The 2026-10-01 file answers 200 on the mirror.

Two tests that prove "the catalog's name wins over the fallback" used the
2026-10-01 name, which now equals the fallback and would pass even if the
catalog were ignored; they now use a 2027-04-01 name. The opt-in online
slicer tests point at the new archive too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S8WVMAhapvazMLrZ7XozrL
…FA-2436)

78ed1dd moved the fallback archive to the 2026-10-01 build, and LAN downloads always asked
the box for that name. A box installed before IIAB's 2026-10-06 update holds only the
2026-04-01 build, so its downloads would 404.

resolveTilesUrl now range-probes the box for each known name, newest first, and uses the
first it serves; a box with neither fails with both names and their status codes. The picker
probes before its first estimate, so the estimate and the download read the same file. The
cache root is resolved before the probe, so missing storage still fails before any request.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S8WVMAhapvazMLrZ7XozrL
The two http clients are process-wide, so idle connections left by earlier tests made the
"keeps its connection alive" precondition read [3, 1] instead of [1, 1] in one of three full
runs. The test now evicts both pools before it opens its own connections.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S8WVMAhapvazMLrZ7XozrL
…ile read (ADFA-2436)

A golden file records what go-pmtiles 1.31.2 `pmtiles extract` writes for one bbox of the
bundled Natural Earth archive (z1-z4, 14 tiles): tile ids and a sha256 per tile, with the
command and capture date in its header. The test slices the same bbox over HTTP, as a
download does, and from a file, and compares both to it. Tile ids matched as-is.

Tile bytes did not, from a file: FileRangeFetcher seeks and reads one RandomAccessFile while
downloadAndSlice fetches chunks from several coroutines, so two tiles came back with another
read's bytes. Production reads over HTTP, so downloads were not affected, but every file://
slicer test could flake. readRange and close are now synchronized, with a concurrency test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S8WVMAhapvazMLrZ7XozrL
…s (ADFA-2436)

Rendered the template for Kotlin and for Java with the versions Code on the Go supplies
(AGP 9.3.1, Kotlin 2.3.21) and resolved every :app configuration offline, with an empty
Gradle home, against CoGo's bundled repo plus offline-maplibre-repo in both orders. Neither
language resolves kotlin-stdlib 1.8.22, kotlin-stdlib-common 1.8.22 or kotlin-stdlib-jdk7/jdk8
1.6.21 from this repo: MapLibre 13.1.0 needs kotlin-stdlib 2.2.10, and stdlib 2.x pulls jdk7/jdk8
up to at least 1.8.0, so those versions always lose. Both projects still resolve the same
files and assembleDebug offline after removal. The .cgp drops from 44.2 MB to 42.5 MB.

The captured list of files the generated projects resolve is now a test fixture: one test
fails if any of them is missing from the repo or its manifest, another if a kotlin-stdlib jar
ships that nothing resolves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S8WVMAhapvazMLrZ7XozrL
…(ADFA-2436)

probeLanArchive accepts a plain 200 as well as a 206, because a box whose
web server ignores the Range header still holds the archive; and it reports
a failed request as "no answer" rather than a status. Neither path had a
test, so dropping 200 from the accepted codes or mislabelling an
unreachable box would have gone unnoticed. The two new cases bring non-UI
branch coverage back above 90%.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S8WVMAhapvazMLrZ7XozrL
…FA-2436)

The region-map template's offline repo still carried 26 androidx jars and aars (with their
.pom and .module files, 71 files, 1.8 MB) that neither a generated Kotlin nor a generated Java
project ever selects: older or -ktx variants that lose conflict resolution to the versions the
fixture lists, such as core 1.0.0 against core 1.13.1. Re-rendered both languages at 1c028e8
with those version dirs removed and resolved every :app configuration plus assembleDebug
offline, with an empty Gradle home, against CoGo's bundled repo and this repo in both orders.
All four runs resolve exactly the same 1161 artifacts as before the removal, and the 38 files
taken from this repo match the fixture. The .cgp drops from 42.5 MB to 41.0 MB.

The stdlib-only check is now repo-wide: any jar or aar the repo ships, in the manifest or on
disk, that is not in the resolved-files fixture fails MapTemplateAssetsTest.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S8WVMAhapvazMLrZ7XozrL
…reens (ADFA-2436)

Plugin review B1 (clause 6.7): five interactive screens had no tooltip wiring. Each now has
its own PluginTooltipEntry linking to the tutorial, and a long press on the screen's main
controls shows it through the 3-arg showTooltip under plugin_org.appdevforall.maps.

The coverage test reads the screen sources, so a screen that stops wiring a tag, or a tag
without an entry, fails the build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S8WVMAhapvazMLrZ7XozrL
… tab (ADFA-2436)

Plugin review C3 and advisory 1. The Tier 3 page moves to the name plugins/README.md asks
for, and gains a "Using the Maps tab" section: the region list's active switch, Refresh
and Delete, the source step, where the box starts, the size estimate, the zoom limit,
region naming, and the download step. Four device screenshots ship beside the page, each
under 12 KB. Each screen's tooltip links to its anchor; tests check that every linked page,
anchor and embedded image ships.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S8WVMAhapvazMLrZ7XozrL
Plugin review advisory 3. Nothing resolves from it: hilbert-curve 0.2.3, the only
com.github dependency, comes from Maven Central. With jitpack gone, buildEnvironment,
releaseRuntimeClasspath and debugUnitTestRuntimeClasspath all resolve under
--refresh-dependencies with no failures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S8WVMAhapvazMLrZ7XozrL
Plugin review advisory 4. No dependency on any resolved classpath is a com.android.support
artifact, so Jetifier has nothing to rewrite. The release build sets isMinifyEnabled = false
and assemblePlugin runs no R8 task, so the full-mode switch never applied. A clean
assemblePlugin builds without either AGP deprecation warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S8WVMAhapvazMLrZ7XozrL
…FA-2436)

IIAB serves an nginx JSON autoindex of /library/www/maps/ at /maps/data/, and its own map
page reads it. Read that listing first and take the newest exact z00-z14 name, so
*.pmtiles.old and directories never match. The file is still read from /maps/<name>: the
listing location aliases the same directory but lacks IIAB's pmtiles transfer settings.

When the listing is missing, not JSON, too large or has no match, probe by name newest
first across every published build (10-01, 09-01, 07-01, 04-01). A box with none gets a
message that it has no zoom 0-14 archive, that IIAB installs zoom 0-8 by default, and to use
the internet source, followed by everything that was tried.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S8WVMAhapvazMLrZ7XozrL
… context (ADFA-2436)

maps.html said to use the "Maps" template or any project. The template is "Offline OSM
Map" (MapTemplateBuilder.REGION_MAP_TEMPLATE_NAME), and apply refuses a project without its
map activity (ProjectMapEmitter.apply). It also said nothing outlives dispose(); MapLibre
keeps the application-scoped context MapLibreBootstrap gives it, with no teardown.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S8WVMAhapvazMLrZ7XozrL
- DownloadCompleteMessage.Spec: target @stringres at the parameter explicitly, so a future
  Kotlin applying it to the field too changes nothing.
- BboxPickerFragment.onRequestPermissionsResult: drop the super call. Fragment's
  implementation in fragment 1.8.8 is an empty method (javap: a bare return), and calling
  it was the deprecated use the compiler flagged.

compileDebugKotlin + compileReleaseKotlin with --rerun --no-build-cache: 2 warnings before,
0 after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S8WVMAhapvazMLrZ7XozrL
Both OkHttp clients are plugin-owned singletons, so dispose() already evicted their idle
sockets. A blocking execute() does not see the plugin scope's cancellation, though, and ran
on until its 20 s / 60 s timeout. Cancel each client's dispatcher before evicting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S8WVMAhapvazMLrZ7XozrL
… screens (ADFA-2436)

- Save screen: the duplicate-name warning said "Save will replace it" while Save is
  disabled for a duplicate. It now says to pick another name; behaviour unchanged.
- Region row: the active switch gets a content description ("Use this region in the
  project") so TalkBack reads more than "switch".
- Delete and Cancel + discard: red text on the host's filled button measured 1.15:1 by day
  (#BA1A1A on #555555) and 1.37:1 at night (#FFB4AB on #AAAAAA) on the A06. The host button
  is dark by day and light at night, the reverse of the plugin surfaces, so a new
  plugin_error_on_button token takes #FFC0B8 by day (4.79:1) and #7F0008 at night (4.74:1).
- Tab creation: getEditorTabs used a string resource for the title. The host calls it on
  the main thread, and the first plugin resource read there opens the plugin APK in the
  host's PluginResourceContext (StrictMode DiskReadViolation). The title is now a constant,
  which also lets a JVM test cover getEditorTabs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S8WVMAhapvazMLrZ7XozrL
…DFA-2436)

BboxPickerFragment hands map driving (style load, first camera, camera
listeners, projection, auto-shrink) to PickerMapController and the
estimate lines to EstimateLabelBinder; its constants and the location
and permission code stay where they were.

RegionManagerFragment hands the row actions to RegionListActions and the
post-download bookkeeping to DownloadCompletion. Both are suspend code
with no Fragment references, written against a new ProjectRegions
interface so the JVM tests run them over a real active.txt. Behaviour is
unchanged: same snackbars, same refresh points, same tooltip wiring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S8WVMAhapvazMLrZ7XozrL
…FA-2436)

aapt2 drops an unescaped double quote from a string resource, and
&quot; counts as unescaped. The device walk showed the duplicate-name
warning as 'A region named Lalibela already exists'; the empty-list
hint lost the quotes round "+ Download new region" the same way. Both
now use \". A JVM test reads every values*/strings.xml and fails on
any quote aapt2 would drop.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S8WVMAhapvazMLrZ7XozrL
…font scale 2.0 (ADFA-2436)

At 2.0 the save screen's "Source" key broke mid-word because the value row
claimed the width, and the download-complete snackbar cut off "Project's
active region is unchanged" at Material's 2-line default. The value now
takes the spare width and wraps; snackbars allow 5 lines.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S8WVMAhapvazMLrZ7XozrL
…ts (ADFA-2436)

The split in d3290e7 wrote seven new files with 2-space indents; the rest of the
module uses 4. Whitespace only (git diff -w is empty).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S8WVMAhapvazMLrZ7XozrL
…-2436)

An empty tile list still estimates 8 KB of header overhead, so the size line
read "0.0 MB download size", Next stayed on, and the download then failed with
"No tiles intersect bbox Bbox(...". The estimate now flags an empty result: the
size line says there is no map data in the area and Next is disabled. If a
download still hits an empty box, the failure snackbar uses a plain message.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S8WVMAhapvazMLrZ7XozrL
…s the active one (ADFA-2436)

The post-download snackbars and the "Apply" confirmation showed the region id
("lalibela-2") instead of its name. Applying a re-download of the region that was
already active said "Switched active region to", which was false. Snackbars now
use the display name, and that case says the region is already active and its
map in the project is updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S8WVMAhapvazMLrZ7XozrL
…cker (ADFA-2436)

ZoomFit used the 256-px Web Mercator constant with the map's width in physical
pixels, but MapLibre's camera zoom is based on a 512-dp world. On the Galaxy A06
(density 1.875) that zoomed in 3.75 times too far, so a saved region's edges were
off screen. It also fitted the width only, so a tall region could overflow.

Fit in dp with MapLibre's constant, on both axes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S8WVMAhapvazMLrZ7XozrL
…ded again (ADFA-2436)

FirstRegionAutoActivator returned NoOpAlreadyActive whenever active.txt named a
cached region, including the region that had just been downloaded. So a
re-download of the active region left the old tiles in the project and the
snackbar asked the user to tap Apply.

Re-apply in that case and report Updated, which the snackbar shows as "The
project's map is updated" with no Apply action.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S8WVMAhapvazMLrZ7XozrL
The template's Java/Kotlin sources live under app/src/main/java/PACKAGE_NAME/,
but the .cgt metadata did not list packageName among its required parameters.
CoGo's ZipRecipeExecutor then logs "Missing packageName, defaulted to
PACKAGE_NAME" and ends project creation with a finished-with-warnings banner.
showPackageNameOption() adds the parameter, as the NDK and Python templates do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01S8WVMAhapvazMLrZ7XozrL
@fryanpan
fryanpan force-pushed the feature/ADFA-2436-maps-plugin branch from 9687ec1 to 7f5f63b Compare October 29, 2026 19:03
@fryanpan

fryanpan commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Re: the 2026-08-03 review, at 7f5f63b.

#1 Fixed: getTooltipCategory() returns plugin_$PLUGIN_ID (MapsPlugin.kt:86, 213); reverting it fails expected:<plugin_[org.appdevforall.]maps> but was:<plugin_[]maps>. Long-press on the Maps tab checked on device.

#2: I added the missing tooltip entry (MapsPlugin.kt:305). The New Project card still shows the fallback text on device, because CoGo looks template tags up under the ide category and never checks the plugin's (TemplateListFragment.kt:150 -> ToolTipManager.kt:128-141 on CoGo stage). That part needs a CoGo fix. It belongs to ADFA-5226, which covers the same bug in the debugger overlay and asks for a sweep of the other places tooltips are looked up. I added this one there.

#3 Fixed, from a different inventory: /maps/extracts.json is a 404 on iiab.switnet.org, so the plugin reads IIAB's maps-catalog.json (what tile-extract.py.j2 uses) and keeps 2026-10-01 as a real fallback (IiabCatalog.kt:40, IiabArchiveResolver.kt:25). A reverted resolver fails expected:<...2026-[10]-01...> but was:<...2026-[04]-01...>. LAN downloads read the box's /maps/data/ listing and take the newest dated z00-z14 archive, skipping .pmtiles.old (1f1d44d, RegionDownloader.kt:401, IiabCatalog.kt:71). The listing is capped at 1 MiB. With no listing, they try 2026-10-01, 09-01, 07-01 and 04-01 by name (IiabCatalog.kt:29). Tried on a Galaxy A06 against a local server that serves an IIAB-style listing; not against a real IIAB box.

#4 Fixed: Play Services is gone; MapLocationController uses LocationManager only. NoPlayServicesTest fails if the class comes back on the classpath. Smaller .cgp too.

#5 Fixed in both templates: the stream is positioned before any header and a failed skip answers 500; suffix ranges work; 5 s read timeout and a fixed pool of 8; the class KDoc and docs/index.html:289 say the server is unauthenticated. The timeout bounds each read, not a whole request, and the KDoc says so. Moving the header write back above the skip fails expected:<HTTP/1.1 [500 ...]> but was:<HTTP/1.1 [206 Partial Content]>.

#6 Fixed: a 206 must declare exactly length, and the read stops at length + 1 for chunked bodies (RangeFetcher.kt:142,146); the dead disjunct is gone. The old code fails both new tests with a 1 MB body.

#7 Rebased onto main at 6d14aa7; the five commits since touch only other add-ons and merge cleanly. The plugin now lives at plugins/Maps under the addon layout, so the README and update-libs conflicts went away.

Wrapper: deleted; the repo-root wrapper builds it.

Size: ABI filter added, to arm64-v8a and armeabi-v7a because CoGo ships both; 48.5 MB -> 41.2 MB (44.2 MB at f841489, since the generated app now carries 32-bit MapLibre too). Stdlib pruned too (cd85de1): the four kotlin-stdlib jars no generated project resolves are gone, 44.2 MB -> 42.5 MB, and a test fails if the shipped jars and the resolved set differ. Unused androidx binaries removed too (301ae37); 41.1 MB at the final head. Fresh Kotlin and Java projects built offline on a Galaxy A06.

Both ABIs now: the generated app ships arm64-v8a and armeabi-v7a (+3.1 MB), checked as a 32-bit process on a Galaxy A06.

Strings: the location-off dialog, the delete snackbar, the download-result snackbars and the size total now come from strings.xml (maps_location_off_*, maps_regions_deleted, maps_download_*, maps_save_total_*).

Bbox: now a data class; the download log prints it.

Delete: keyed off the canonical folder now (RegionCache.kt:134), so a planted meta.json cannot point it elsewhere.

Cache: get and put copy, and a repeat put refreshes the LRU position.

SDK: on the repo's standard toolchain now (AGP 9.3.1, Kotlin 2.3.21, SDK 36).

Emitter: the containment check now runs before mkdirs().

heightKm: KDoc matches the haversine body.

Cleartext: CoGo permits it host-wide (network_security_config.xml:9, base-config cleartextTrafficPermitted="true"), and the plugin runs in that process, so no change.

Golden-file slicer test: added (fcbc910). It slices the bundled Natural Earth basemap and matches go-pmtiles 1.31.2's extract tile for tile (14 tiles). It also caught a race in local-file reads, fixed in the same commit with an 8-thread test.

Short-skip regression: added; it fails if the header goes out before the skip.

@fryanpan

fryanpan commented Oct 29, 2026 •

Copy link
Copy Markdown
Contributor Author

Some of these files are quite long. Please consider splitting them into smaller, more focused files to improve readability and adhere to the Single Responsibility Principle (SRP).

  1. Line Count (Soft limit): While not a hard rule, if a file starts crossing the 300-400 line mark, it's usually a strong smell that it's accumulating responsibilities.

I love your point about agents planning better with specific rules. We should probably document these exact heuristics in a guide file so the AI tools can pick them up automatically as context.

Done. BboxPickerFragment is now 423 lines (1320 before July, 762 before this round): map setup moved to PickerMapController and the size label to EstimateLabelBinder. RegionManagerFragment is 465 (was 545): list actions moved to RegionListActions and download results to DownloadCompletion. No behaviour change; checked on a Galaxy A06 at font scale 1.0 and 2.0.

On the guide file: I said I would draft it in July and did not. It is now ADFA-6840: a REVIEW.md for this repo modelled on CoGo's, with your four heuristics proposed for it. I will link the PR there.

@fryanpan
fryanpan requested a review from jatezzz October 29, 2026 19:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants