Build, debug, and extend Supernote e-ink device plugins using the sn-plugin-lib SDK (React Native + Android). Trigger this skill whenever the user mentions Supernote, sn-plugin-lib, PluginManager, PluginCommAPI, PluginFileAPI, PluginNoteAPI, PluginDocAPI, .snplg files, e-ink plugin development, or wants to create/modify a plugin for Supernote NOTE or DOC apps. Also trigger when the user discusses EMR coordinates, lasso operations on e-ink devices, or any React Native plugin targeting the Supernote PluginHost runtime. Even if the user just says 'plugin for my notebook' or 'extend my note-taking app' in the context of Supernote hardware, use this skill.
Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.
Quelldateien prüfen
Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
Ein direkter Befehl überspringt den Prüf-Prompt. Prüfen Sie die Quelle, bevor Sie ihn ausführen.
Build, debug, and extend Supernote e-ink device plugins using the sn-plugin-lib SDK (React Native + Android). Trigger this skill whenever the user mentions Supernote, sn-plugin-lib, PluginManager, PluginCommAPI, PluginFileAPI, PluginNoteAPI, PluginDocAPI, .snplg files, e-ink plugin development, or wants to create/modify a plugin for Supernote NOTE or DOC apps. Also trigger when the user discusses EMR coordinates, lasso operations on e-ink devices, or any React Native plugin targeting the Supernote PluginHost runtime. Even if the user just says 'plugin for my notebook' or 'extend my note-taking app' in the context of Supernote hardware, use this skill.
⚠️ INK2TASK LOCAL CORRECTIONS — read before following this file
This skill was written for a different plugin. Some of it is wrong for this
codebase, and the rest is what we learned the hard way. Everything here was
verified on our own hardware — items 1-5 on an A5X, items 6-18 on a Manta
(added 2026-08-23), which is where the device-specific ones matter.
Gotcha #9 says call saveCurrentNote() BEFORE insertElements /
replaceElements. DO NOT DO THIS on our redraw path. Committing the
editor's buffer first makes the page wipe a no-op and leaves the user's
handwriting behind — the exact bug that took ~10 build cycles to find and
fix. Our order is: capture reads → replaceElements → thensaveCurrentNote + reloadFile. See the comment in plugin/src/utils/capture.ts.
Gotcha #4 (recycle elements) is correct and we follow it — via
recycleElement(uuid), NOT element.recycle(). Nothing we hold is an
Element class instance: transformElements/createElement mutate the raw
bridge objects in place, so .recycle is undefined on them. Use
recycleElements() from plugin/src/utils/sdk.ts.
Gotcha #33 / setup-and-build §5b overstate the reactPackages problem for
us, and understate a different one. Our buildPlugin.sh DOES pick up a
hand-written package — but only because it parses MainApplication.kt for
add(...Package()) calls. Adding the source file alone is not enough
(find_project_react_packages, which scans android/, is dead code and
never runs). Also: the script derives the registered name from the first
class X regex hit including comments, which once produced
com.helloworld.should. Always verify the generated
build/generated/PluginConfig.json.
Pattern 6 / floating-window.md works on this device — confirmed v1.0.6.
Our sync-progress bubble is built from it. Two deviations: ours is
FLAG_NOT_TOUCHABLE | FLAG_NOT_FOCUSABLE (purely informational, so pen input
passes straight through — never make an overlay tappable on a writing
device), and we needed no permission grant because the HOST already holds
SYSTEM_ALERT_WINDOW. The doc's tap handling, drag→EMR mapping, and
foreground detection are all unused here.
Gotcha #23 understates getCurrentFilePath()/getCurrentPageNum(): polling
does NOT fix staleness, because the value itself is wrong, not just late.
Confirmed on-device 2026-08-13/14 across three independent fix attempts (a
second delayed re-read, a getCurrentPageNum() cross-check, and a live
incident trace via adb logcat) and cross-validated against an unrelated
third-party plugin's own bug tracker
(vincentaravantinos/supernote-collapse-expand), which hit the same gap.
These calls report the last note/page THIS PLUGIN itself touched, not
what's genuinely on screen — there is no "what's currently displayed" signal
anywhere in the SDK surface. Two concrete traps this causes: (a) a
background listener (motion/button) can fire a write against your own note
even when the user is truly elsewhere, if your plugin touched that note
recently enough; (b) if your plugin makes ANY native call that reads a
different note in between two of your own getCurrentFilePath() calls
(e.g. reading a lasso selection's source note for OCR), a later call in the
same handler can report that OTHER note instead of your own — so two
resolves seconds apart, in the same function, can disagree. The fix not
the read: resolve the target ONCE per operation and thread that single
result through every step (capture, write, redraw) instead of re-resolving
independently at each one — see syncThenFetch's target option and its
doc comment in plugin/src/actions.ts for the pattern that fixed exactly
this in our lasso-capture-then-redraw flow.
PARTLY SUPERSEDED — see correction #16. The claim that no screen-scoped
signal exists was true of the SDK as it stood in August 2026. The plugin
preview firmware added PluginCommAPI.canHandwrite, which does report on the
current view, and it fixes the menu-overlay case. Everything above about
getCurrentFilePath/getCurrentPageNum still stands.
The whole AndroidManifest.xml in plugin/android/ is INERT. This APK
is never installed as an app — app.npk is unpacked inside
com.ratta.supernote.pluginhost and runs under the HOST's manifest. So a
permission or a usesCleartextTraffic flag added to our manifest changes
nothing at runtime. Whatever the host declares is what you get. This is easy
to lose days to, because the manifest edit looks like it should work.
Plain http:// is BLOCKED in the plugin host on newer firmware, and the
failure is disguised.NetworkSecurityPolicy.isCleartextTrafficPermitted()
returns false (the host targets SDK 35 and declares neither
usesCleartextTraffic nor a networkSecurityConfig), so every fetch to a
LAN address fails instantly with React Native's generic
Network request failed. Device-confirmed on a Manta 2026-08-23: 1016
discovery probes completed in 2.8 seconds without a packet leaving the
device, including one aimed at a server that answered a raw socket from that
same tablet. It reads exactly like "no server on the network". An A5X on
Android 8.1 never hit this, and other Mantas connect fine, so it tracks the
plugin-host version rather than the model.
Workaround: the policy governs the platform HTTP stacks, not
java.net.Socket. Ink2TaskNetModule.httpRequest speaks HTTP/1.0 with
Connection: close (body ends at EOF, so there is no chunked parsing) on its
own thread; plugin/src/utils/lanHttp.ts wraps it as lanFetch with a
fetch-shaped result and falls back to real fetch for https, when cleartext
IS permitted, and when the native module is missing. Check the policy before
blaming the network — and note that once probes are real, a dead address
costs a full timeout, so any concurrency figure tuned against instant
failures is fiction.
/proc/net/route is unreadable from Android 11 on, so there is no
JS-only way to learn the tablet's own IP. Needed for any subnet sweep.
Ink2TaskNetModule.getLocalIpv4() tries three sources in order:
NetworkInterface (no permission needed AND it carries the prefix length —
preferred), ConnectivityManager LinkProperties, then the deprecated
WifiManager.getConnectionInfo().getIpAddress() (IPv4 only, no mask, assumes
/24, and note the value is little-endian). Permissions come from the host
(correction #6), which already holds ACCESS_WIFI_STATE and
ACCESS_NETWORK_STATE.
Manta device detection: trust PluginManager.getDeviceType(), not the
model string, and never trust Dimensions for pixels. A Manta reports
ro.product.model = "Supernote Nomad" while carrying a 1920x2560 panel;
getDeviceType() correctly returns 5 (A5X = 3, Nomad = 4). Separately,
React Native's Dimensions returns DP, not pixels — a Manta reads
1024x1365.33 at a PixelRatio.get() of 1.875 — so any size comparison
against a panel constant must multiply by the pixel ratio first. Ours did
not, which made a whole fallback branch dead code. See
plugin/src/utils/deviceSize.ts.
recognizeElements has a positional dead zone on the last row of a
page. It failed with code 117 on the bottom capture box while an
identically-built box in the middle of the page succeeded — same stroke
count, correct rect, ink verifiably inside it, correct pageSize. Padding
the rect, retrying, and splitting the strokes all failed to fix it. What
works is retrying against a larger supported canvas
(alternatePageSize). Only ever go larger: dropping to a smaller canvas
puts the ink outside it and guarantees failure, which is what our first
version of this did on the Manta. See recognizeResilient in
plugin/src/utils/capture.ts.
Drawing facts worth knowing, from the community's .note format
reverse-engineering (plugin/assets/vector-format-spec.md, cross-checked
against Supernote's own PDF exports):
penWidth / thickness is hundredths of a page pixel. penWidth: 100
is a 1px line. penType: 10 (needle) renders at 0.94–1.04x its nominal
width, so it is the pen to use when the width has to be exact.
penColor: 254 is white, and white always wins — it covers darker ink
regardless of draw order. That makes thick white strokes a plausible
cover-up for ink you could not erase.
Do NOT reach for a filled rectangle to cover something. A filled rect is a
2-point stroke record whose pen/color/thickness fields are
meaningless; the real fill colour lives in TITLE_ footer metadata that
the SDK cannot write. Thick strokes are the route.
insertNotePage / getPageSize / page-count traps on multi-page notes.insertNotePage requires a non-empty template argument. getPageSize fails
with 1207 on a page that does not exist, which aborts a sync — so clamp
any page index you kept in your own state against the note's real page
count, since the user can delete pages between your calculation and your
draw. And do not infer "this is the template page" from
getNoteTotalPageNum() === 1: that heuristic inverts on a 3-page note and
draws a second SYNC button over the baked-in one.
replaceElements wipes the entire page, so ALL reads must precede ALL
writes when you touch more than one page. Our harvestPages reads every
active page, then captures and completes each, before anything is redrawn.
Related: pages must be processed sequentially, not in parallel — each page
is its own whole-page mutation built from a shared native element cache, and
overlapping them risks interleaving two pages' batches.
The .snplg filename must equal pluginKey EXACTLY, or the device
refuses to install it. Supernote's installer verifies the filename against
the package and rejects any renamed copy with "Installation Failed. The
plugin might have been modified." — even when the bytes are md5-identical
(a version-stamped Ink2Task-0.2.50.snplg failed; the byte-identical
Ink2Task.snplg installed fine). So ship the canonically-named file and put
the version in the release notes, not the filename. Also: a reinstall needs
a remove first, because the host caches an unpacked copy.
showRattaDialog's boolean is the RIGHT-hand button. The signature is
showRattaDialog(tip, leftBtnTxt, rightBtnTxt, isSuccess): Promise<boolean>
and the SDK never says which button true means. Device-confirmed on a
Manta 2026-08-24: left="Keep" right="Delete" -> true, with the delete
actually happening. So put the CONFIRMING action on the right. For anything
irreversible, still write the call so that only an explicit true acts and
every other outcome (the other button, a dismissed dialog, a throw) is the
safe one -- then a firmware change to the button order shows up as "nothing
happens" instead of as destroyed user data.
canHandwrite() IS the screen-scoped signal correction #5 says does not
exist. Correction #5 concluded, correctly for its time, that nothing in
the SDK reports what is genuinely on screen. PluginCommAPI.canHandwrite
(sn-plugin-lib 0.1.65, plugin preview firmware) does: it reports whether the
current view is accepting handwriting. Device-verified on a Manta
2026-08-25 — six taps landing on the top menu overlay all returned
{success:true, result:false}, and normal page taps all returned true.
That fixes the long-standing "the menu overlay fires the on-page button
underneath it" bug, because the overlay does NOT stop taps reaching the
page.
Guard it as if (canWrite === false) return; and nothing stronger. An
error, a missing method, or an unreadable envelope must all let the tap
through, or the same build breaks on firmware without the method.
What does NOT work, checked on device: waiting ~70ms to see whether the
toolbar fires first (a suggestion from r/Supernote_dev). The toolbar press
never reaches the plugin as a button event at all — every tap logged
"no button press has ever happened".
The new page-element APIs are not usable yet, and one of them wedges the
plugin. On the preview firmware, deletePageElements (present in the
library, ABSENT from the docs) genuinely removes exactly the element named
and leaves the rest alone. But batchUpdatePageElements returned
success: true while changing nothing at all when handed the same element
as both the delete target and the insert payload, and calling
PluginFileAPI.getElements straight after it stops the JS thread — a
Promise.race timeout never fired, and three syncs in a row hung. A read
right after deletePageElements came back stale instead.
So: these calls cannot verify their own work by reading the page back, and
a success flag from them is not evidence of anything. Element indices are
1-based here (numInPage ran 1..39 on a 39-element page) while the older
path-based calls are 0-based. Probe from a button, never from inside the
sync path.
Jest cannot transform sn-plugin-lib or react-native-fs (ESM), so
anything you want unit tested must import NOTHING. Our tested modules
(taskText.ts, pagination.ts, deviceSize.ts, listMatch.ts,
serverFeatures.ts) are deliberately import-free and hold the logic, while
the SDK-touching wrappers around them stay untested. Two suites that do
import the SDK have never run.
Our own hard-won findings live in this project's Claude memory
(ink2task-sdk-gotchas). Where that and this skill conflict, the memory
wins — it was measured on this device against this code.
You are an expert Supernote plugin developer. Supernote plugins extend the NOTE (handwriting notebook) and DOC (document reader) apps on Supernote e-ink devices. Plugins run inside a PluginHost process that provides a React Native runtime, and communicate with NOTE/DOC via AIDL + SDK interfaces.
Before You Start
Always read the appropriate reference file(s) before writing code:
EMR coordinates: Hardware pen sampling coords, higher precision. Used for stroke points, Element.maxX/maxY.
Pixel coordinates: Screen pixels (left-top origin). Used for Rect params, lasso, geometry insertion, UI layout.
Conversion: PointUtils.androidPoint2Emr(point, pageSize) / emrPoint2Android(…). Get pageSize from PluginFileAPI.getPageSize(path, page). See api-quick-ref.md §6 for supported sizes.
Which APIs use which? Pixel: insertGeometry, insertFiveStar, insertText(textRect), lassoElements, getLassoRect, resizeLassoRect, Title/TextBox/Picture/Geometry fields. EMR: Stroke.points, FiveStar.points (stored), Element.maxX/maxY.
Layer Restrictions
Main layer (layer=0): Supports ALL element types.
Custom layers (layer 1-3): Only strokes, pictures, text boxes, and geometry. NO titles, links, or five-stars.
DOC files: Only have one layer (main). Cannot insert text boxes, titles, or links.
Lasso Context
Many APIs (getLassoElements, getLassoRect, modifyLassoText, setLassoTitle, etc.) require an active lasso context — the user must have lasso-selected something first.
modifyLassoText and modifyLassoLink only work when exactly one element of that type is selected.
setLassoBoxState(2) = permanently removes the lasso. Use only when the operation is done. setLassoBoxState(3) (0.1.43+) = hides all lasso UI but preserves the lasso state internally.
Element & ElementDataAccessor
Element is the universal data structure for all visible items (strokes, titles, links, text boxes, geometry, pictures, five-stars).
Large data (angles, contours, stroke points) uses ElementDataAccessor — a lazy accessor, NOT a full array. Call size(), get(index), getRange(start, end) to fetch data on demand.
Always call element.recycle() when done to free native-side memory.
Always call PluginCommAPI.createElement(type) before inserting new elements — this creates the native-side cache and accessor references.
What do you need to do?
│
├─ Manage plugin lifecycle, buttons, events, device info, touch events
│ → PluginManager (references/api-quick-ref.md §1) — includes registerMotionListener (0.1.43+)
│
├─ Work with current page context (lasso, stickers, geometry, reload)
│ → PluginCommAPI (references/api-quick-ref.md §2)
│
├─ Operate on file data (pages, elements, layers, templates, keywords)
│ → PluginFileAPI (references/api-quick-ref.md §3)
│
├─ NOTE-specific features (text, titles, links, images, save)
│ → PluginNoteAPI (references/api-quick-ref.md §4)
│
├─ DOC-specific features (selected text, page text)
│ → PluginDocAPI (references/api-quick-ref.md §5)
│
├─ Route lasso/toolbar buttons to different screens without showing main panel
│ → Pending Button ID pattern (references/patterns.md Pattern 5)
│
├─ Show a persistent overlay that survives closePluginView()
│ → Native Floating Window (references/patterns.md Pattern 6)
│
├─ Disable the EMR pen during a plugin-driven gesture (e.g. pen lasso on overlay)
│ so strokes don't leak into the .note file
│ → Scoped Pen Disable (references/patterns.md Pattern 16) + see Pattern 15 for
│ architecture and the PluginApp.showPluginView reflection release recipe
│
├─ Insert text sequentially across pages (e.g. streamed from phone/AI)
│ → Page-Anchored Sequential Insertion (references/patterns.md Pattern 13)
│
├─ OCR-recognise handwritten strokes / text boxes into a string
│ → PluginCommAPI.recognizeElements(elements, pageSize) (references/api-quick-ref.md §2)
│ 1. getLassoElements() to get the Element array
│ 2. getCurrentFilePath() + getCurrentPageNum() + getPageSize(path, page) for the full page size
│ 3. recognizeElements(elements, pageSize) → APIResponse<string>
│ 4. cancelRecognize() to abort a long-running recognition if needed
│
└─ Extract hardcoded strings / add multi-language support (i18n)
→ i18n Extract-Translate-Convert workflow (references/patterns.md Pattern 12)
Step 1: scan files → .lang intermediate format
Step 2: .lang → src/i18n/locales/{zh_CN,en_US,zh_TW,ja_JP}.json
Step 3: rewrite source files with t('key') + useTranslation hook
Common Gotchas
Forgot PluginManager.init(): All subsequent SDK calls will silently fail.
Wrong button type: type=3 (text-selection) is DOC-only. Registering it for NOTE is harmless but the button won't appear.
Coordinate mismatch: Inserting a geometry with EMR coords where pixel coords are expected (or vice versa) will place elements off-screen. Always check which coordinate system the API expects. Note: insertFiveStar uses pixel coords (not EMR).
Not recycling elements: Fetching elements without calling recycle() leaks native memory. Especially critical in loops.
Assuming full arrays: element.angles and element.contoursSrc are accessors, not arrays. Don't try to .map() or .length them — use size() and get().
Missing lasso context: Calling lasso APIs without an active lasso selection causes errors. Always verify the lasso context first.
DOC insertion limits: Trying to insert text boxes, titles, or links into DOC files will be rejected.
React Native version lock: Must use RN 0.79.2. Other versions may cause PluginHost incompatibility.
File-level API without saving: Call PluginNoteAPI.saveCurrentNote() before insertElements/modifyElements/replaceElements to persist the in-memory cache first; otherwise data may be inconsistent.
PluginFileAPI param order is inconsistent: Read-only queries put page first: getElements(page, filePath), getElementCounts(pageNum, filePath), getElementNumList(pageNum, filePath, type). Write operations put filePath first: insertElements(filePath, page, elements[]), modifyElements(filePath, page, …), replaceElements(…), deleteElements(…), getElement(filePath, page, numInPage). Always check the signature.
Lasso button always shows main screen: If registerButtonListener is set up inside App.tsx, there's a timing gap where the button event fires before the listener is registered. Use the pending button ID pattern (Pattern 5): store the pressed ID as a module-level variable in , then consume it with as the first thing in the mount .
When Helping the User
For new plugin creation: Walk through the full workflow (scaffold → init → buttons → UI → build). Generate complete index.js and App.tsx files.
For API questions: Look up the exact signature in references/api-quick-ref.md. Provide working code with proper error handling.
For debugging: Check the gotchas list first. Common issues: missing init, wrong coordinates, missing lasso context, wrong layer.
For complex features: Combine patterns from references/patterns.md. Show the full flow including error handling and resource cleanup.
For i18n / localization requests ("extract strings", "multi-language", "i18n"): Follow Pattern 12 in references/patterns.md — scan for hardcoded strings → produce .lang intermediate file → convert to i18next JSON locale files → rewrite source files with t('key'). Always output all three phases in sequence.
Always: Include TypeScript types, proper APIResponse checking, and recycle() calls where applicable.
index.js
checkPendingButton()
useEffect
Native floating window pitfalls: Permission, render timing, tap handling, stale bubbles, and foreground detection — see Pattern 6 in references/patterns.md for all details.
registerLangListener uses onMsg not onLangChange: The callback is onMsg: (msg) => {} and language code is at msg.lang. The lang value uses underscores (zh_CN) — convert with msg.lang.replace('_', '-') before passing to i18next.
registerButton name must be a JSON string for localization: Passing a plain string means the button always shows that literal text regardless of device language. For multi-language support, serialize an object: name: JSON.stringify({en: 'Sticker', zh_CN: '贴纸', ...}).
onButtonPress event has a pressEvent field: For lasso toolbar buttons, event.pressEvent === 3. Don't rely solely on id — check pressEvent to confirm the event type before routing.
NativePluginManager vs PluginManager: Two different modules. NativePluginManager.getPluginDirPath() returns the plugin's private data directory (use for databases, sticker files). Cache this value — it's a slow async native call.
Rotation needs three listeners: Use NativePluginManager.getOrientation() for initial value on mount, DeviceEventEmitter.addListener('plugin_event_rotation', ...) for rotation events, and Dimensions.addEventListener('change', ...) for updated pixel dimensions. All three are needed for correct layout.
generateStickerThumbnail takes a Size object: The third argument is {width, height}, not two separate numbers. Call PluginCommAPI.getStickerSize(path) first.
saveStickerByLasso takes a full file path: The argument is the destination file path (e.g. pluginDir + '/sticker/my.sticker'), not just a name.
PluginNoteAPI.insertText always targets the current displayed page: There is no page parameter — text is inserted into whichever page the user is currently viewing. If your plugin tracks a targetPage for sequential insertion, you must call PluginCommAPI.getCurrentPageNum() before each insertText and verify the user is on the expected page. Inserting without this check will silently place text on the wrong page.
getLastElement() takes no parameters: The official signature is getLastElement() → APIResponse<Element>. It returns the last element of the currently displayed page. Do not pass (page, filePath) — those parameters are not part of the API.
Sequential text insertion across pages needs page-wait: After insertNotePage() + reloadFile(), do NOT immediately resume inserting. The user must flip to the new page first (since insertText targets the displayed page). Use a polling loop (getCurrentPageNum) to detect when the user arrives on the target page, then resume. A naïve timeout fallback that blindly resumes will insert text onto the wrong page.
Note file switch detection: If your plugin does background work (text insertion, etc.), periodically call getCurrentFilePath() to verify the user hasn't switched to a different note. The SDK does not emit a "file changed" event — you must poll.
External page count changes: If the user manually adds or removes pages while your plugin tracks a targetPage, page indices shift and your target becomes stale. Periodically call getNoteTotalPageNum(path) and compare against your expected count to detect external changes.
recognizeElements needs full page size, not lasso rect: Pass the result of getPageSize(filePath, pageNum) as the size argument — NOT the lasso bounding rect. Passing the lasso rect causes the firmware to throw IllegalArgumentException: getRealMaxX, unknown pageSize and recognition fails entirely.
recognizeElements only supports strokes and text boxes: Other element types (geometry, pictures, five-stars, links) are silently ignored. Filter your element list or check getLassoElementTypeCounts() before calling to avoid confusing empty results.
PluginManager.closePluginView() does NOT fire notifyClientPluginState(0): The SDK skips the state-0 notification when transitioning the PluginApp to stop. Anything the note app does in response to onPluginState(state=1) (most importantly sendFullScreenDisableArea for the EMR pen lock) will not be reversed by closePluginView alone. To release such state, first call PluginApp.showPluginView(0) by reflection (see Pattern 15), then closePluginView for cleanup. Note (0.1.43):closePluginView also requires a Promise parameter in the native module — calling it via reflection with null triggers a non-fatal NPE at promise.resolve(…) after the close logic has already executed.
PluginManager.showPluginView() (0.1.43) / NativePluginManager.showPluginView() — both no-arg only: Calling either always opens the plugin view and triggers notifyPluginState(1). SDK change in 0.1.43: The PluginAppAPI abstract class removed the showPluginView(int showType) overload — the abstract signature is now showPluginView() (no-arg). However, the device-side PluginHost firmware still has the int-arg method on the concrete PluginApp class (verified on A5X2 firmware 2026-05 with sn-plugin-lib 0.1.43). The reflection trick from Pattern 15 (pluginApp.showPluginView(0)) therefore still works at runtime, but should be coded defensively (graceful fallback if the 1-arg method disappears in a future firmware update). Also note: logcat shows PluginStateTaskQueue may DISCARD state:0 tasks under certain conditions — the actual pen disable release comes from the disableAreaChanged path triggered by UI layout changes, not solely from notifyPluginState(0).
setFullAuto(false) does NOT cancel a state:1-triggered full-screen pen disable: They are independent code paths in drawAPP. setFullAuto writes drawAPP's fullAuto flag, while state:1 runs HandWriteClient.sendFullScreenDisableArea which writes the rect list. Only an explicit state:0 event (which triggers disableAreaChanged → sendDisableAreaInfo) will revoke a sendFullScreenDisableArea rect. Use setFullAuto(false) only as defensive coverage, not as the primary release.
EMR pen disable does NOT block finger touch: A TYPE_APPLICATION_OVERLAY toolbar above an EMR-disabled plugin view continues to receive touch normally. When designing a pen-lock toggle, do not hide the toolbar when entering the locked state — the user needs the same toolbar button to release the lock. The lock is on the digitizer (pen) input pipeline only.
Two pen input pipelines coexist; an overlay only gates one of them: dev/input/pen events fan out to (a) the standard input pipeline → View.dispatchTouchEvent with SOURCE_STYLUS, and (b) a hardware direct path → drawAPP native → straight into the active .note file. A WindowManager overlay can swallow (a) but never (b). Any feature where the user draws inside your plugin's own UI (pen lasso, signature pad, etc.) must engage full-screen EMR disable for the duration — see Pattern 16 — or strokes will silently land in the user's note file. PenGuard snapshot-and-cleanup is only adequate as a fallback for the rare race window.
EinkManager.enableFullUiAuto is misnamed and does NOT control the digitizer on A5X2 (firmware 2025): Despite the suggestive method name, it's e-ink regal/refresh control. Empirically verified: it does not gate pen input. Don't waste a round trip on it for pen disable scenarios — use Pattern 15's PluginApp.showPluginView state pair instead.
PluginHost ignores MainApplication.getPackages() — only PluginConfig.jsonreactPackages matters: PluginHost loads NativeModules via the "reactPackages" array in PluginConfig.json, NOT through the standard RN MainApplication → ReactNativeHost → getPackages() path. The build script auto-discovers third-party packages from node_modules/, but your own ReactPackage (the one registering custom NativeModules) must be explicitly included. If missing, all custom NativeModules.* will be null at runtime — code compiles, JS executes, but every native call silently fails. When renaming, consolidating, or refactoring Package classes, always verify the fully-qualified class name appears in build/generated/PluginConfig.json after build.
logcat chatty filter hides PluginHost init logs: Android's chatty mechanism drops repeated lines. PluginHost startup triggers this heavily, hiding diagnostic Log.i() output. Disable with adb logcat -P "" before capturing, or filter by PID: adb logcat --pid=$(adb shell pidof com.ratta.supernote.pluginhost).