Repository navigation
Expand file tree
/
Copy pathrandom-xkcd.html
More file actions
658 lines (622 loc) · 27.4 KB
/
Copy pathrandom-xkcd.html
File metadata and controls
658 lines (622 loc) · 27.4 KB
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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Random XKCD</title>
<style>
:root {
--bg: #fdfdfd;
--fg: #1a1a1a;
--accent: #0f766e;
--accent-light: #e6f5f3;
--border: #d0d7de;
--code-bg: #f4f6f8;
--table-stripe: #f8fafb;
--shadow: rgba(0,0,0,0.06);
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
color: var(--fg);
background: var(--bg);
line-height: 1.6;
max-width: 52rem;
margin: 0 auto;
padding: 2rem 1.5rem 4rem;
}
header { border-bottom: 3px solid var(--accent); padding-bottom: 1.2rem; margin-bottom: 2rem; }
header h1 { font-size: 2rem; color: var(--accent); }
header p.subtitle { color: #555; margin-top: 0.3rem; }
header .meta { font-size: 0.85rem; color: #777; margin-top: 0.5rem; }
nav { background: var(--accent-light); border: 1px solid var(--border); border-radius: 6px; padding: 1rem 1.5rem; margin-bottom: 2.5rem; }
nav h2 { font-size: 0.95rem; text-transform: uppercase; letter-spacing: 0.05em; color: var(--accent); margin-bottom: 0.5rem; }
nav ol { padding-left: 1.3rem; }
nav li { margin: 0.25rem 0; }
nav a { color: var(--accent); text-decoration: none; }
nav a:hover { text-decoration: underline; }
section { margin-bottom: 2.5rem; }
h2 { font-size: 1.4rem; color: var(--accent); border-bottom: 1px solid var(--border); padding-bottom: 0.3rem; margin-bottom: 1rem; }
h3 { font-size: 1.1rem; margin: 1.2rem 0 0.5rem; }
p, li { margin-bottom: 0.5rem; }
ul, ol { padding-left: 1.4rem; }
code {
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, monospace;
background: var(--code-bg);
padding: 0.15em 0.35em;
border-radius: 3px;
font-size: 0.9em;
}
pre {
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 1rem 1.2rem;
overflow-x: auto;
font-size: 0.88rem;
line-height: 1.5;
margin: 0.8rem 0 1rem;
}
pre code { background: none; padding: 0; }
table { width: 100%; border-collapse: collapse; margin: 0.8rem 0 1rem; font-size: 0.95rem; }
th, td { text-align: left; padding: 0.55rem 0.8rem; border: 1px solid var(--border); }
th { background: var(--accent-light); font-weight: 600; }
tr:nth-child(even) td { background: var(--table-stripe); }
.diagram {
background: var(--code-bg);
border: 1px solid var(--border);
border-radius: 6px;
padding: 1.2rem;
font-family: "SFMono-Regular", Consolas, monospace;
font-size: 0.85rem;
line-height: 1.55;
overflow-x: auto;
white-space: pre;
margin: 0.8rem 0 1rem;
}
.callout {
background: var(--accent-light);
border-left: 4px solid var(--accent);
padding: 0.8rem 1rem;
border-radius: 0 6px 6px 0;
margin: 1rem 0;
}
.file-tree { list-style: none; padding-left: 0; font-family: monospace; font-size: 0.9rem; }
.file-tree ul { list-style: none; padding-left: 1.5rem; }
footer { margin-top: 3rem; padding-top: 1rem; border-top: 1px solid var(--border); font-size: 0.85rem; color: #777; }
</style>
</head>
<body>
<header>
<h1>Random XKCD Plugin</h1>
<p class="subtitle">Browse xkcd comics from the Code on the Go editor bottom sheet</p>
<div class="meta">Version 1.0.0 · Author: App Dev For All · Package: <code>org.appdevforall.randomxkcd</code></div>
</header>
<nav>
<h2>Contents</h2>
<ol>
<li><a href="#overview">Executive Overview</a></li>
<li><a href="#functionality">Core Functionality</a></li>
<li><a href="#architecture">Technical Architecture</a></li>
<li><a href="#integration">Integration Points</a></li>
<li><a href="#deployment">Deployment & Usage</a></li>
<li><a href="#benefits">Key Benefits</a></li>
<li><a href="#license">Attribution & License</a></li>
</ol>
</nav>
<!-- ================================================================ -->
<section id="overview">
<h2>1. Executive Overview</h2>
<p>
Random XKCD is a Code on the Go plugin that puts an xkcd-comic browser into
the editor's bottom sheet. A new <strong>XKCD</strong> tab joins the
built-in bottom-sheet tabs (Build Output, App Logs, …); inside it,
a five-button navigation row mirrors the xkcd.com control bar —
<code>|<</code> · <code>< Prev</code> · <code>Random</code> · <code>Next ></code> · <code>>|</code>
— with the comic image rendered below. The panel opens on the latest
comic and stays there until the user navigates.
</p>
<p>
The image itself responds to two standard gestures: a single tap copies
the comic's canonical URL to the clipboard, and a double tap copies the
PNG itself (routed through the host IDE's <code>FileProvider</code>, since
plugin manifests don't register providers of their own). Long-pressing
the bottom-sheet tab opens a three-tier tooltip whose Tier 3 button
launches the full code walkthrough that ships inside the
<code>.cgp</code>.
</p>
<p>
The plugin spans three Kotlin source files and one XML layout (~600 lines
of code total), declares only the <code>network.access</code> permission,
and is deliberately written as a canonical “this is what a small
Code on the Go plugin looks like” example. Every extension point it touches
— <code>IPlugin</code>, <code>UIExtension</code>,
<code>DocumentationExtension</code> — is exercised once, in the
simplest way that still tells the truth.
</p>
</section>
<!-- ================================================================ -->
<section id="functionality">
<h2>2. Core Functionality</h2>
<h3>Bottom-sheet panel</h3>
<p>
The plugin contributes a single <strong>XKCD</strong> tab to the editor
bottom sheet, ordered at <code>200</code> among plugin tabs. The tab
body is a fragment with three regions, stacked vertically:
</p>
<ul>
<li>A five-button navigation row at the top.</li>
<li>The comic image inside a <code>FrameLayout</code> card.</li>
<li>The comic caption (number + title) and the xkcd
<em>alt</em>-text beneath it.</li>
</ul>
<p>
A progress spinner and an offline empty-state share the same region as
the image and swap in as needed.
</p>
<h3>Navigation row</h3>
<p>
The button row mirrors xkcd.com's own nav. Each button calls
<code>loadComic()</code> with a <code>Navigation</code> enum value; the
fragment debounces concurrent fetches and re-renders on the main
thread.
</p>
<table>
<thead>
<tr><th>Button</th><th>Action</th><th>Disabled when</th></tr>
</thead>
<tbody>
<tr><td><code>|<</code> First</td><td>Fetch comic <code>#1</code></td><td>never</td></tr>
<tr><td><code>< Prev</code></td><td>Fetch <code>current − 1</code></td><td>at comic <code>#1</code> or before first load</td></tr>
<tr><td><code>Random</code></td><td>Pick a random number in <code>[1, latest]</code></td><td>never</td></tr>
<tr><td><code>Next ></code></td><td>Fetch <code>current + 1</code></td><td>at the high-water mark or before first load</td></tr>
<tr><td><code>>|</code> Last</td><td>Re-probe the latest comic</td><td>never</td></tr>
</tbody>
</table>
<h3>Image gestures</h3>
<p>
A standard <code>GestureDetector.SimpleOnGestureListener</code> attached
to the <code>ImageView</code> (not the whole panel, so scrolling a tall
comic doesn't trigger spurious taps) handles two gestures:
</p>
<table>
<thead>
<tr><th>Gesture</th><th>Effect</th></tr>
</thead>
<tbody>
<tr><td>Single tap</td><td>Copy <code>https://xkcd.com/<num>/</code> to clipboard as plain text</td></tr>
<tr><td>Double tap</td><td>Copy the in-memory PNG bytes to clipboard as <code>image/png</code></td></tr>
</tbody>
</table>
<p>
On Android 13+ the system shows its own clipboard indicator, so the
plugin suppresses its toast on those versions to avoid double-up.
</p>
<h3>Network surface</h3>
<p>
All HTTP lives in <code>XkcdApiClient</code>, in one file so it reads
top-to-bottom. Two JSON endpoints, no auth, no rate-limit handling
required:
</p>
<table>
<thead>
<tr><th>Endpoint</th><th>Purpose</th></tr>
</thead>
<tbody>
<tr><td><code>GET https://xkcd.com/info.0.json</code></td><td>Latest comic metadata</td></tr>
<tr><td><code>GET https://xkcd.com/<num>/info.0.json</code></td><td>Specific comic metadata</td></tr>
<tr><td><code>GET https://imgs.xkcd.com/…</code></td><td>Comic PNG (streamed, capped at 5 MB)</td></tr>
</tbody>
</table>
<h3>Defensive parsing</h3>
<p>
The JSON parser rejects any <code>img</code> URL that doesn't start with
<code>https://imgs.xkcd.com/</code>, so a malicious or compromised
metadata response can't point the bitmap decoder at an attacker-controlled
host. JSON responses are bounded by <code>MAX_JSON_BYTES</code>
(64 KB) and image downloads by <code>MAX_IMAGE_BYTES</code>
(5 MB), both well above what xkcd actually returns. Every IO failure
converges on a single null return that routes through the offline
empty-state.
</p>
<h3>The #404 quirk</h3>
<p>
xkcd comic <code>#404</code> is a joke — its JSON endpoint returns
HTTP <code>404</code> deliberately. Both the random picker and the
Prev/Next walker skip past it: Random retries until it lands somewhere
else, and Prev/Next continue stepping in the same direction (so Prev
from #405 lands on #403, Next from #403 lands on #405). The user never
sees a “failed to load” toast for this case.
</p>
<h3>Three-tier tooltip</h3>
<p>
Long-pressing the <strong>XKCD</strong> tab opens the three-tier help
surface contributed via <code>DocumentationExtension</code>:
</p>
<table>
<thead>
<tr><th>Tier</th><th>Source</th><th>Trigger</th></tr>
</thead>
<tbody>
<tr><td>1 — one-liner</td><td><code>PluginTooltipEntry.summary</code></td><td>Long-press the tab</td></tr>
<tr><td>2 — HTML paragraph</td><td><code>PluginTooltipEntry.detail</code></td><td>“See More” on the Tier 1 popup</td></tr>
<tr><td>3 — full HTML page</td><td><code>src/main/assets/docs/index.html</code></td><td>“Code walkthrough” button inside Tier 2</td></tr>
</tbody>
</table>
<p>
The Tier 3 page is served by the host IDE at
<code>http://localhost:6174/plugin/org.appdevforall.randomxkcd/index.html</code>
and indexed at install time by the host's <code>Tier3AssetWalker</code>.
</p>
</section>
<!-- ================================================================ -->
<section id="architecture">
<h2>3. Technical Architecture</h2>
<h3>File layout</h3>
<ul class="file-tree">
<li>plugins/Random-XKCD/
<ul>
<li>build.gradle.kts</li>
<li>proguard-rules.pro</li>
<li>src/main/
<ul>
<li>AndroidManifest.xml</li>
<li>assets/
<ul>
<li>docs/ — Tier 3 walkthrough (<code>index.html</code> + <code>css/walkthrough.css</code>)</li>
<li>icon_day.png — Plugin Manager icon, light theme</li>
<li>icon_night.png — Plugin Manager icon, dark theme</li>
</ul>
</li>
<li>kotlin/org/appdevforall/randomxkcd/
<ul>
<li><strong>XkcdRandomPlugin.kt</strong> — lifecycle, tab registration, tooltip wiring</li>
<li>fragments/<strong>XkcdPanelFragment.kt</strong> — panel UI, button wiring, gestures, clipboard</li>
<li>net/<strong>XkcdApiClient.kt</strong> — HTTP, two endpoints, image stream</li>
<li>net/<strong>XkcdComic.kt</strong> — data class</li>
</ul>
</li>
<li>res/layout/fragment_xkcd_panel.xml — the panel layout</li>
<li>res/values/, res/values-night/ — colors, strings, styles</li>
</ul>
</li>
</ul>
</li>
</ul>
<h3>Class overview</h3>
<table>
<thead>
<tr><th>Class</th><th>Role</th><th>Key interfaces</th></tr>
</thead>
<tbody>
<tr>
<td><code>XkcdRandomPlugin</code></td>
<td>Plugin entry point. Lifecycle, registers the bottom-sheet tab, contributes tooltip entries and the Tier 3 docs path.</td>
<td><code>IPlugin</code>, <code>UIExtension</code>, <code>DocumentationExtension</code></td>
</tr>
<tr>
<td><code>XkcdPanelFragment</code></td>
<td>The XKCD tab body. Button wiring, image gestures, clipboard, render loop, navigation state.</td>
<td>extends <code>Fragment</code></td>
</tr>
<tr>
<td><code>XkcdApiClient</code></td>
<td>The entire HTTP surface in one file. Latest / by-number / random, plus a bounded image-stream helper.</td>
<td>—</td>
</tr>
<tr>
<td><code>XkcdComic</code></td>
<td>Plain data class: <code>num</code>, <code>title</code>, <code>alt</code>, <code>imageUrl</code>, derived <code>pageUrl</code>.</td>
<td>—</td>
</tr>
</tbody>
</table>
<h3>Data flow</h3>
<div class="diagram">
user taps a nav button (|< · < Prev · Random · Next > · >|)
|
v
XkcdPanelFragment.loadComic(Navigation)
|
| Dispatchers.IO
v
XkcdApiClient
├─ fetchLatest() ──> https://xkcd.com/info.0.json
├─ fetchByNumber(n) ──> https://xkcd.com/n/info.0.json
├─ fetchRandom() ──> loops fetchByNumber, skipping #404
└─ openImageStream() ──> https://imgs.xkcd.com/...
|
| parseComic() — rejects non-imgs.xkcd.com hosts
v
XkcdComic + raw PNG bytes
|
| main thread
v
showComic() — renders bitmap, caption, alt
|
| tap / double-tap
v
ClipboardManager (text URL OR image via host FileProvider)</div>
<h3>Lifecycle and navigation state</h3>
<p>
<code>XkcdPanelFragment</code> tracks three pieces of state across button
presses:
</p>
<table>
<thead>
<tr><th>Field</th><th>Purpose</th></tr>
</thead>
<tbody>
<tr><td><code>currentComicNum</code></td><td>Number currently displayed. Drives Prev / Next enable state.</td></tr>
<tr><td><code>latestComicNum</code></td><td>High-water mark from the most recent Latest probe (or any Random that landed higher). Caps Next.</td></tr>
<tr><td><code>lastBytes</code></td><td>Raw PNG of the displayed comic, kept so “copy image” doesn't re-download.</td></tr>
</tbody>
</table>
<p>
A single <code>loadJob</code> guards against fan-out from rapid button
presses — if a fetch is already in flight, subsequent taps are
dropped on the floor.
</p>
<h3>Cooperative cancellation</h3>
<p>
Both <code>fetchRandom</code>'s retry loop and the image-download read
loop call <code>coroutineContext.ensureActive()</code> on every iteration,
so when the host tears the fragment down (rotation, tab close) the
coroutine exits cleanly with a <code>CancellationException</code> rather
than running to completion against a dead view.
</p>
<h3>Build configuration</h3>
<table>
<tr><th>Setting</th><th>Value</th></tr>
<tr><td>Gradle plugin</td><td><code>com.itsaky.androidide.plugins.build</code></td></tr>
<tr><td>Compile / Target SDK</td><td>34</td></tr>
<tr><td>Min SDK</td><td>26</td></tr>
<tr><td>Java / Kotlin target</td><td>17</td></tr>
<tr><td>ProGuard</td><td>Disabled (<code>isMinifyEnabled = false</code>)</td></tr>
<tr><td>Plugin API</td><td><code>compileOnly</code> via <code>libs/plugin-api.jar</code></td></tr>
<tr><td>HTTP library</td><td>OkHttp 4.12.0</td></tr>
<tr><td>JSON</td><td><code>org.json</code> (ships with Android — no extra dependency)</td></tr>
<tr><td>Output format</td><td><code>.cgp</code> package</td></tr>
</table>
</section>
<!-- ================================================================ -->
<section id="integration">
<h2>4. Integration Points</h2>
<p>
Random XKCD touches the host IDE through three extension interfaces. It
does not consume any IDE services beyond the host's <code>FileProvider</code>
authority — all behavior is self-contained inside the plugin's own
sandbox.
</p>
<h3>4.1 Plugin lifecycle (<code>IPlugin</code>)</h3>
<p>
<code>XkcdRandomPlugin</code> implements <code>IPlugin</code>. The
manifest declares the entry point:
</p>
<pre><code><meta-data android:name="plugin.main_class"
android:value="org.appdevforall.randomxkcd.XkcdRandomPlugin" /></code></pre>
<p>
<code>initialize()</code> stores the <code>PluginContext</code> and is
wrapped in <code>try/catch</code> so a stray exception in setup can't
crash the host. <code>activate()</code>, <code>deactivate()</code>, and
<code>dispose()</code> log lifecycle transitions; there's no per-project
state to clean up.
</p>
<h3>4.2 Bottom-sheet tab (<code>UIExtension</code>)</h3>
<p>
<code>getEditorTabs()</code> contributes a single tab to the editor
bottom sheet:
</p>
<pre><code>TabItem(
id = TAB_ID, // "xkcd_bottom_tab"
title = "XKCD",
fragmentFactory = { XkcdPanelFragment() },
order = 200,
tooltipTag = TOOLTIP_TAG_TAB // "xkcd.tab"
)</code></pre>
<p>
The <code>fragmentFactory</code> returns a <em>new</em> fragment on every
invocation — never a singleton, because the host manages fragment
lifecycles. The <code>tooltipTag</code> wires the tab to the corresponding
<code>PluginTooltipEntry</code> below.
</p>
<h3>4.3 Three-tier docs (<code>DocumentationExtension</code>)</h3>
<p>
Two methods feed the tooltip system. <code>getTooltipEntries()</code>
returns a single <code>PluginTooltipEntry</code> with summary, detail
HTML, and a <code>Code walkthrough</code> button whose URI resolves to
<code>index.html</code> inside the Tier 3 asset directory.
<code>getTier3DocsAssetPath()</code> returns <code>"docs"</code>, telling
the host that everything under <code>src/main/assets/docs/</code> should
be indexed and served from
<code>http://localhost:6174/plugin/org.appdevforall.randomxkcd/…</code>.
</p>
<h3>4.4 Clipboard image — the FileProvider hop</h3>
<p>
Copying an image to the system clipboard requires a content URI. Plugins
can't register their own <code>FileProvider</code> at runtime (they're
loaded via <code>DexClassLoader</code>, not installed as apps), so the
double-tap path routes through the host's existing FileProvider
authority:
</p>
<ol>
<li>Write the in-memory PNG to <code>filesDir/xkcd_share/last.png</code>
on <code>Dispatchers.IO</code>.</li>
<li>Ask the host's FileProvider for a content URI:
<code>FileProvider.getUriForFile(ctx, "${ctx.packageName}.providers.fileprovider", target)</code>.</li>
<li>Wrap the URI in a <code>ClipData.newUri</code> clip — this
queries the resolver for the URI's MIME type, advertising
<code>image/*</code> to paste targets.</li>
</ol>
<p>
The host's <code>file_provider_paths.xml</code> exposes <code>filesDir</code>
and the provider declares <code>grantUriPermissions="true"</code>, so the
system can hand out a temporary read grant to whatever app calls
<code>ContentResolver.openInputStream</code> on the clip URI.
</p>
<div class="callout">
<strong>Known caveat:</strong> some OEM clipboard managers have been
observed to silently drop image clips delivered this way. The flow works
on stock Android API 24+; real-device verification is recommended for
OEM coverage.
</div>
<h3>4.5 Plugin-scoped LayoutInflater</h3>
<p>
<code>XkcdPanelFragment.onGetLayoutInflater</code> wraps the inflater via
<code>PluginFragmentHelper.getPluginInflater</code>. Without this,
<code>R.layout.fragment_xkcd_panel</code> resolves against the host
IDE's resources rather than the plugin's APK and throws
<code>Resources$NotFoundException</code> at inflate time.
</p>
<h3>4.6 Permissions</h3>
<p>
The manifest declares <strong>only</strong> <code>network.access</code>:
</p>
<pre><code><meta-data android:name="plugin.permissions"
android:value="network.access" /></code></pre>
<p>
Writes to the plugin's own <code>filesDir</code> (the FileProvider hop)
and access to the system clipboard are sandbox-allowed and need no
declaration. Code on the Go's <code>filesystem.*</code> permissions only gate the
host IDE's project-file services — not used here.
</p>
<h3>Integration summary</h3>
<div class="diagram">
Code on the Go IDE Host
+----------------------------------------------------+
| |
| bottom-sheet tab system |
| tooltip system (Tier 1 / 2 / 3 walker) |
| host FileProvider (filesDir grant authority) |
| |
+--+------------------+------------------+-----------+
| | |
v v v
XKCD tab long-press tab clipboard image
| | ^
| | |
+--+------------------+------------------+-----------+
| XkcdRandomPlugin |
| |
| IPlugin --> lifecycle |
| UIExtension --> getEditorTabs() |
| DocumentationExtension--> tooltip + Tier 3 docs |
+----------------------+-----------------------------+
|
XkcdPanelFragment
(buttons, gestures, render)
|
v
XkcdApiClient
(xkcd.com + imgs.xkcd.com)</div>
</section>
<!-- ================================================================ -->
<section id="deployment">
<h2>5. Deployment & Usage</h2>
<h3>Building</h3>
<pre><code>cd plugins/Random-XKCD
./gradlew assemblePlugin</code></pre>
<p>
Produces <code>plugins/Random-XKCD/build/plugin/random-xkcd.cgp</code> — the
bundle you sideload into Code on the Go.
</p>
<h3>Installation</h3>
<ol>
<li>Open <em>Preferences → Plugin Manager → +</em>.</li>
<li>Select the <code>random-xkcd.cgp</code> file.</li>
<li>The IDE discovers <code>XkcdRandomPlugin</code> via the manifest
metadata and activates it automatically.</li>
<li>Open any project — the <strong>XKCD</strong> tab appears in
the editor bottom sheet.</li>
</ol>
<h3>Using the panel</h3>
<ol>
<li>Tap the <strong>XKCD</strong> tab in the bottom sheet — the
latest comic loads automatically.</li>
<li>Navigate with the five-button row above the comic.</li>
<li>Single-tap the image to copy the comic's URL; double-tap to copy
the image itself.</li>
<li>Long-press the <strong>XKCD</strong> tab for the three-tier help
surface, including the full code walkthrough.</li>
</ol>
<h3>Reading the code walkthrough</h3>
<ul>
<li><strong>Inside Code on the Go</strong> (the canonical path) — long-press
the <strong>XKCD</strong> tab → tap <strong>See More</strong>
→ tap <strong>Code walkthrough</strong>. The IDE opens the page
in an in-IDE WebView.</li>
<li><strong>Outside Code on the Go</strong> — open
<code>src/main/assets/docs/index.html</code> directly in any
browser. Renders identically.</li>
</ul>
<h3>Runtime requirements</h3>
<table>
<tr><th>Requirement</th><th>Value</th></tr>
<tr><td>Min Android version</td><td>API 26 (Android 8)</td></tr>
<tr><td>Min IDE version</td><td>1.0.0</td></tr>
<tr><td>Permissions</td><td><code>network.access</code></td></tr>
<tr><td>Network access</td><td>Required — HTTPS to <code>xkcd.com</code> and <code>imgs.xkcd.com</code></td></tr>
</table>
</section>
<!-- ================================================================ -->
<section id="benefits">
<h2>6. Key Benefits</h2>
<ul>
<li><strong>A canonical small-plugin example.</strong> Under 600 lines
of Kotlin across three source files, every plugin-specific concept
called out exactly where it shows up — the in-asset
walkthrough explains each one as a numbered step.</li>
<li><strong>Real-world UI patterns.</strong> Demonstrates a bottom-sheet
tab, fragment-based UI, a plugin-scoped <code>LayoutInflater</code>,
and the standard Android <code>GestureDetector</code> idiom for
single/double-tap distinction.</li>
<li><strong>Real-world network patterns.</strong> Bounded reads, host
whitelisting on parsed URLs, cooperative cancellation under
<code>lifecycleScope</code>, and a single fetch-in-flight guard
that survives rapid button mashes.</li>
<li><strong>Cross-app image clipboard.</strong> Shows the practical
FileProvider hop a plugin needs in order to put <code>image/*</code>
data on the system clipboard — including the OEM-clipboard
caveat worth knowing before relying on it.</li>
<li><strong>Three-tier in-IDE help.</strong> Wires up
<code>DocumentationExtension</code> end-to-end — one-liner,
HTML paragraph, and a full asset-served walkthrough — serving
as a template for any plugin that wants discoverable, in-context
documentation.</li>
<li><strong>Sandbox-honest permissions.</strong> Declares only
<code>network.access</code> and documents why the clipboard and
plugin-private filesystem writes do not need declarations —
useful reference for understanding Code on the Go's permission surface.</li>
</ul>
</section>
<!-- ================================================================ -->
<section id="license">
<h2>7. Attribution & License</h2>
<p>
xkcd comics are © Randall Munroe and licensed
<strong>CC BY-NC 2.5</strong>
(<a href="https://xkcd.com/license.html">https://xkcd.com/license.html</a>).
This plugin:
</p>
<ul>
<li>Fetches comics over HTTPS from <code>xkcd.com</code> and
<code>imgs.xkcd.com</code>. No caching, no redistribution beyond
what the user explicitly copies to their own clipboard.</li>
<li>Displays an attribution line — <em>“Comics ©
Randall Munroe · xkcd.com · CC BY-NC 2.5”</em>
— beneath every comic in the panel.</li>
<li>Is itself non-commercial (open-source demo plugin for an
open-source IDE), consistent with the NC term.</li>
</ul>
<p>
The plugin's own source code is licensed per the surrounding
<code>plugin-examples</code> repository (see <code>LICENSE</code> at
the repo root). xkcd's license applies only to the comic content the
plugin displays.
</p>
</section>
<footer>
Random XKCD Plugin Documentation · Version 1.0.0 · org.appdevforall.randomxkcd
</footer>
</body>
</html>