Skip to content

ADFA-2433 | random-xkcd: new demo plugin#6

Open
fryanpan wants to merge 12 commits into
mainfrom
feature/ADFA-2433-xkcd-plugin
Open

ADFA-2433 | random-xkcd: new demo plugin#6
fryanpan wants to merge 12 commits into
mainfrom
feature/ADFA-2433-xkcd-plugin

Conversation

@fryanpan
Copy link
Copy Markdown

@fryanpan fryanpan commented May 12, 2026

Description

Adds the random-xkcd plugin as a small plugin example that demonstrates a few concepts for building plugins.

  • Tutorial takes the user through key parts of how to make this plugin (starting from the "Plugin" template in CodeOnTheGo itself)
  • UI integration as a bottom-sheet tab
  • Fetches a random xkcd comic over HTTPS (requires network permission)
  • Single-tap fetches a new one; double-tap copies the URL to the clipboard; triple-tap copies the image via the host's FileProvider authority
  • Long-press the tab to surface a Tier-1 / Tier-2 / Tier-3 walkthrough served from assets/docs/

Can review as a whole PR, or follow the commits that go through the same steps as the tutorial doc.

Recording from Claude going through all flows using mobile-mcp (and I also tested manually)

xkcd_qa.mp4

Ticket

ADFA-2433

Observation

xkcd CC BY-NC 2.5 attribution is surfaced in the panel (always-visible credit line beneath every comic) and in the Tier-3 docs.

Had to make a corresponding minor change in CodeOnTheGo repo. See matching PR here: appdevforall/CodeOnTheGo#1297

Some open questions for the future

  • Localized Strings. Hardcoded the tab title to be "XKCD". The current Plugin API / TabItem doesn't provide a way to localize from Context (all the other plugins also hardcode strings for now).
  • Tab Tooltip Wiring. Had to make a minor change in the paired CodeOnTheGo PR to make the 3-tier tooltip work. See matching PR: ADFA-2435 | Enable per-plugin tooltips on bottom-sheet tabs CodeOnTheGo#1297

Build + Test Status

  • ./gradlew testDebugUnitTest — green (10/10 in TapCountClassifierTest)
  • ./gradlew assemblePlugin — green; random-xkcd.cgp builds at 5.0 MB (release)
  • Claude smoke-tested using mobile-mcp on Samsung Galaxy A56 (CoGo from the matching PR above, Android 16). Clean install + happy path; recording attached above.
  • Manually tested the same flow too.

🤖 Generated with Claude Code

fryanpan and others added 11 commits May 12, 2026 14:59
Adds the build, manifest skeleton, and `IPlugin` lifecycle class for
a new Code on the Go plugin. The host loads this class via
`DexClassLoader` by the fully-qualified name in `plugin.main_class`,
then drives initialize → activate → deactivate → dispose.

Wrap initialize() in try/catch so a stray exception in plugin setup
doesn't crash the host IDE — returning false here makes the IDE skip
activate() and keep running.

Subsequent commits opt this class into UIExtension (bottom-sheet tab)
and DocumentationExtension (in-IDE help). For now there are no
extensions — the plugin loads cleanly but doesn't surface any UI.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Plugins declare permissions via comma-separated values in a single
`plugin.permissions` meta-data entry — not Android's `<uses-permission>`
system.

The XKCD plugin needs exactly one: `network.access`, to fetch the xkcd
JSON + image. CoGo's filesystem.* permissions gate access to the host
IDE's project files, not the plugin's own `filesDir`/`cacheDir` —
those are sandbox-allowed and need no declaration. The system clipboard
also has no permission gate (no `clipboard.*` exists; Android itself
doesn't gate clipboard either). So even with the triple-tap image-
clipboard flow that comes in a later commit, this remains the only
permission this plugin declares.

The Tier-3 walkthrough that arrives later explains the conceptual
model: permissions in CoGo gate the host's resources, not your own.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Opts the plugin class into `UIExtension` and registers one tab in the
editor bottom sheet alongside Build Output, App Logs, etc. The host's
`getEditorTabs()` call returns a `TabItem` with a fragmentFactory the
host invokes whenever the tab is shown.

The Fragment is shell-only here: a layout inflated via
`PluginFragmentHelper.getPluginInflater(pluginId, parent)` (required so
`R.layout.*` resolves against the plugin's APK and not the host IDE's),
view bindings, and an empty-state placeholder. Tap handling, network
fetch, and clipboard arrive in subsequent commits.

Includes the layout XML, string resources (with a CC BY-NC 2.5
attribution string for the xkcd credit shown beneath every comic),
colors for day + night themes, and the plugin theme style.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a small purpose-built tap-burst state machine and wires it into
the Fragment's touch listener. Android's `GestureDetector` resolves
single + double taps but not triple, and the XKCD plugin needs all
three (single = new comic, double = copy URL, triple = copy image),
so a hand-rolled classifier reads cleaner than a hybrid.

`TapCountClassifier` is intentionally pure — no clocks, no `Handler`,
no Android imports. The Fragment supplies `now` (uptime millis) and
decides when to call `resolve()`. That makes the classifier
unit-testable in plain JUnit (see `TapCountClassifierTest`).

The touch listener filters out scrolls before counting taps: an
`ACTION_DOWN`/`ACTION_UP` pair only feeds the classifier if the
finger moved less than the system touch slop. Returning `false` from
the listener avoids consuming the event, so the ScrollView keeps its
scroll behavior on tall comics.

The classifier's three outputs are routed through `handleClassification`,
which is wired to no-ops for now — subsequent commits replace the
no-ops with `loadRandomComic`, `copyUrlToClipboard`, and
`copyImageToClipboard` respectively.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a small OkHttp-based client and wires it into the Fragment's
single-tap path. The whole network surface fits in one file:

  XkcdApiClient.fetchLatest() / fetchByNumber(num) / openImageStream()

fetchRandom() picks a number in [1, latestNum] and keeps picking
until one returns a real comic. The only way it returns null is if
the initial "latest comic" probe fails (network down). xkcd #404 is
the joke "page not found" comic that returns HTTP 404 on its JSON
endpoint, so we loop past it rather than bound retries — the loop
converges in 1–2 picks on a healthy network and never gives up while
the network works.

Defensive parsing in parseComic() rejects any image URL that isn't
`https://`, so a future MITM that swaps in `http://` doesn't break
the plugin's HTTPS-only claim.

Fragment wiring:
- `loadRandomComic()` runs the fetch + decode on Dispatchers.IO and
  shows the result via lifecycleScope's Main dispatcher.
- Rapid single-taps no-op while a previous fetch is in flight.
- Read is bounded to 5 MB with cooperative cancellation so the
  coroutine respects lifecycleScope teardown mid-download.
- Plain `BitmapFactory.decodeByteArray(...)` for now; for very large
  images on low-end devices, see Android's bounded-bitmap-decoding
  pattern at
  https://developer.android.com/topic/performance/graphics/load-bitmap

Adds OkHttp 4.12.0 as the only new build dependency.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wires DOUBLE and TRIPLE tap classifications to clipboard handlers and
buffers the last comic's PNG bytes in memory so triple-tap can copy
the image without re-downloading or re-encoding the rendered Bitmap.

Text — the easy path: `ClipboardManager.setPrimaryClip(ClipData.newPlainText(...))`
needs no permission. Android doesn't gate clipboard with a
`<uses-permission>` either; foreground apps (which a plugin always is)
can write freely.

Image — the FileProvider hop:
  1. Plugin `<provider>` declarations in the manifest are dead code —
     plugins are DexClassLoader-loaded, not installed as Android apps,
     so PackageManager never registers them. The escape valve is to
     route through the host IDE's existing FileProvider authority.
  2. The host's file_provider_paths.xml exposes filesDir, so we write
     the buffered PNG to ctx.filesDir/xkcd_share/last.png and ask
     FileProvider.getUriForFile(ctx, "${ctx.packageName}.providers.fileprovider",
     target) for a content URI.
  3. `ClipData.newUri` queries the ContentResolver for the URI's MIME
     type — .png resolves to image/png, so paste targets see image/*
     and offer to paste the image, not the URL string.

Toasts on every action so the user can tell the copy happened — the
system clipboard is invisible without feedback.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Opts the plugin class into `DocumentationExtension` and registers
CoGo's per-plugin help API:

  Tier 1 (summary)  — one-liner shown when long-pressing the tab
  Tier 2 (detail)   — HTML paragraph revealed by "See More"
  Tier 3 (button)   — full HTML walkthrough page served at
                       http://localhost:6174/plugin/<pluginId>/<uri>

`getTier3DocsAssetPath() = "docs"` tells the host to walk
`src/main/assets/docs/` at install time and serve each file (CSS +
HTML) at the URL above; the asset bundle's own files reference each
other with relative paths.

The walkthrough page itself is the canonical "how to write a CoGo
plugin" example for this codebase — a 7-step tour aligned with the
commit history.

Plugin Manager icons added too: day + night variants under
src/main/assets/, declared via `plugin.icon_day` / `plugin.icon_night`
meta-data. Required for debug-built `.cgp`s; release builds skip the
check. Includes `plugin.description` for the Plugin Manager listing.

Includes xkcd attribution. xkcd comics are © Randall Munroe and
licensed CC BY-NC 2.5 (https://xkcd.com/license.html). The Tier-3
page surfaces this; the plugin's in-panel UI already shows a
visible attribution line per the previous commit's strings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A light source-tree README aimed at developers reading the plugin
in GitHub or in their editor. The primary tutorial lives in the
Tier-3 docs page (src/main/assets/docs/index.html), discoverable
inside CoGo via long-press → "See More" → "Code walkthrough" on the
XKCD tab. The README points readers there and surfaces:
  - what the plugin does in two sentences
  - build + test commands
  - the source-layout map
  - the xkcd attribution / CC BY-NC 2.5 license note

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ions

Applies the findings from the pre-push code review.

Bugfixes:
- `XkcdApiClient.parseComic` — host allowlist instead of just scheme.
  Previously accepted any `https://...` URL from the JSON's `img` field;
  now restricts to `https://imgs.xkcd.com/...`. Protects against a
  malicious / MITM response pointing the bitmap decoder at an
  attacker-controlled HTTPS server.
- `XkcdApiClient.fetchRandom` — made suspend + added
  `coroutineContext.ensureActive()` at the top of each loop iteration.
  Before, an unbounded loop with no cancellation point would run to
  completion if the Fragment was torn down mid-fetch on a flaky
  network. Now it exits with `CancellationException` cooperatively.
- `XkcdApiClient.getJson` — bound the JSON read at 64 KB via
  `Content-Length` check. Stops a pathological response from OOM-ing
  the parser; xkcd's JSON is < 1 KB in practice so the cap is generous.

Conventions parity with other plugin-examples plugins:
- `minSdk` dropped from 28 → 26 to match apk-viewer / keystore-generator
  / markdown-preview / snippets / ndk-installer-plugin. No API-28-only
  call sites in this code; the stricter target was incidental.
- Dead `R.string.tab_title` removed from `strings.xml` —
  `XkcdRandomPlugin.getEditorTabs()` hardcodes `title = "XKCD"` so the
  string was never read.

Documentation honesty:
- `XkcdApiClient` class doc + per-method docs telegraph that all public
  methods do blocking HTTP I/O and must be called from `Dispatchers.IO`.
  `fetchRandom` is suspend; the others are plain blocking funs.
- `XkcdPanelFragment.lastBytes` field doc spells out the 5 MB heap-pin
  tradeoff and lists the alternatives (re-fetch, re-compress, disk
  cache) so a copy-paster doesn't internalize the pattern as default.
- `Random.nextInt(1, latest.num + 1)` annotated with
  `// upper bound exclusive` next to the call.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Pulls in TabItem.tooltipTag from the paired CoGo PR
(appdevforall/CodeOnTheGo#1297) so random-xkcd can wire its bottom-sheet
tab to its own tooltip entry.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Set `tooltipTag = TOOLTIP_TAG_TAB` on the bottom-sheet TabItem.
  Long-pressing the XKCD tab now surfaces the plugin's own Tier-1
  tooltip ("Random xkcd comic. Tap to roll a new one.") instead of
  the generic platform placeholder. Requires the paired CoGo PR
  (appdevforall/CodeOnTheGo#1297).
- Tutorial (assets/docs/index.html):
  - Section 3 (bottom-sheet tab UI): include `tooltipTag` in the
    TabItem code example + one paragraph explaining the wire to
    Step 7's DocumentationExtension.
  - Section 7 (tooltip): back-reference noting that `tag` here is
    the same string as `TabItem.tooltipTag`.
  - Tightening pass: -16% words (2482 → 2087). Cuts: end-to-end
    recap section (duplicate of intro callout), sandbox-model
    section collapsed to a 3-bullet summary, xkcd license section
    halved, redundant phrasing across step intros, dropped the
    standalone "6c. User feedback" subsection.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@fryanpan fryanpan changed the title ADFA-2433 | random-xkcd: new demo plugin (bottom-sheet tab, tap / double-tap / triple-tap, Tier-3 walkthrough) ADFA-2433 | random-xkcd: new demo plugin May 13, 2026
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@@ -0,0 +1,353 @@
package com.codeonthego.xkcdrandom.fragments
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

for consistency with the existing plugin examples, please use org.appdevforall. as the package name


<meta-data
android:name="plugin.author"
android:value="Code on the Go Team" />
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Please use App Dev For All as plugin author

@Daniel-ADFA
Copy link
Copy Markdown
Collaborator

I wonder if having the controls just like it is on the XKCD website is better UX, what do you think @fryanpan

Screenshot 2026-05-13 at 08 24 49

@fryanpan
Copy link
Copy Markdown
Author

fryanpan commented May 14, 2026

I wonder if having the controls just like it is on the XKCD website is better UX, what do you think @fryanpan

Screenshot 2026-05-13 at 08 24 49

Yes, that would be nicer UX. I can make that edit!

Wasn't happy that we had to add a custom tap handler either...makes the demo more complicated than it needs to be. With buttons on screen for going to random, next, prev,etc. we only need to do single tap (copy url) and double tap (copy image) and I can get rid of the custom tap recognizer. That will be nicer too.

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.

2 participants