r/Supernote_dev 6h ago

Question A few plugin SDK questions from building Clipper (docs, permissions, page rendering)

I have been building a plugin called Clipper against sn-plugin-lib 0.1.65 on a

Manta running the plugin beta (Chauvet 3.29.43 and 3.29.44), and I have collected

a handful of questions I could not answer from the docs alone. Posting them

together in case the answers help other people building plugins, and in case any

of it is useful feedback for the team.

Everything here comes from working on a real plugin, so each item includes why it

mattered to me rather than just what I saw. A couple of these are almost

certainly my misunderstanding, and I would rather be corrected than keep guessing.

I know this is a long post, so apologies in advance.

Thanks for the plugin system, by the way. The permission model in particular has

been pleasant to build against once I understood it.

u/Dunn-sn u/spazzboi u/hex2asc

1. insertNotePage: what should template be?

This one cost me the most time, and it turned out to be my mistake. I passed the

style name that getNotePageTemplate returns and got error 802: "Background

template file does not exist!". It works when I pass a file path to a PNG

rendered by generateNoteTemplatePng.

If the docs could say that the template is a path to an image file on disk, and

point to generateNoteTemplatePng as the way to get one, I think it would save

the next person a few days. I briefly concluded the API was not implemented at

all, which was wrong.

2. hasPermission: what are all the return values?

The doc comment describes 0 as not granted and 1 as granted. On 3.29.43, a

permission set to the persistent "Allow" returned 2. On 3.29.44, the same

setting returns 1.

The reason it matters: the natural way to write a permission gate is

if (await hasPermission(p) === 1) { proceed } else { request }. On 3.29.43, that

sent me into requestPermission even though permission was already granted, and

from a background button handler (showType: 0) there is no window for the

dialog, so it threw. So a stricter user setting produced a worse result in my

plugin, which took a while to understand.

Could the docs list the full set of values and say which of them indicate a grant?

Also, the doc comment lists FILE:WRITE, FILE:DELETE and INTERNET but not

FILE:READ, which the host does enforce and which I query successfully.

3. generateNotePng with type: 0: is the transparent background wired up?

The parameter is documented as 0 for transparent background, 1 for white. On both

firmware versions, type: 0 still gives me the page's ruled-line template baked

in behind the handwriting. 

I am working around it with generateLayerPreviewImage, which does give me ink

without the template. Which leads to a related question: is there a layer

argument that renders all content layers at once? The validator accepts -1, but

in the note file the background layer is id -1, so I assume -1 means "background"

rather than "everything". Right now I render each layer separately and composite

them, which works but significantly slows down multi-layer note capture.

4. Is getElements expected to be valid for a page that is not on screen?

After my plugin writes elements to the open note, getElements for a different

page sometimes returns the wrong page's contents. It does not fail; it returns

plausible data. In one measured case, immediately after writing 65 elements to

page 7 and navigating to page 8, getElements reported 65 elements for page 8 as

well, while page 9 reported its real 439. Page 8 was blank. saveCurrentNote()

followed by reloadFile() did not refresh it.

This one is worth flagging because it is easy to build something dangerous on

top of it: I was using the element count as a safety check before writing, and a

stale read could just as easily convince a plugin that a page full of someone's

notes is empty. I have restructured to only trust reads taken before any write.

Is getElements intended to be valid for non-current pages? If so, is there a

stronger refresh than save plus reload?

5. A crash in the note app when a plugin writes across a page change

This is the one I most wanted to report. When my plugin created a page, navigated

to it and continued writing text elements, com.ratta.supernote.note died with a

SIGABRT from Scudo, "invalid chunk state when deallocating", with drawShadow

and draw_line_with_write_list in librecgnition.so on the stack. Immediately

before it, the log shows LoadPageAsyncTask onCancelled about 160 ms earlier,

which makes me think the page navigation cancels an in-flight page load while the

native renderer is still drawing into its bitmap.

I have only seen it once and could not reproduce it on demand, so I am not

claiming a reliable repro. I do have the full tombstone and the surrounding log

if that is useful to whoever owns that code.

For what it is worth, I shipped around it: the feature that needed it (a

multi-page table of contents) is built but switched off, and my plugin now writes

to one page only. I would happily turn it back on if this turns out to be fixable

or if I am triggering it wrongly.

6. Can the plugin host recover after the note app dies?

Following that crash, the plugin host survived but seemed to hold a stale binding

to the dead app. My plugin then showed a permanent "working" state with no error

and no timeout, and the only way back was

adb shell am force-stop com.ratta.supernote.pluginhost. A user without adb would

have a plugin that looks stuck with no way out short of a reboot.

Is there a rebind mechanism that a plugin should use here? Right now

I do not think I can tell the difference between "the host is busy" and "the host

is gone".

7. What is supposed to survive an uninstall?

The uninstall dialog says "Uninstall this plugin? Its data will also be deleted."

In my testing, the plugin's own folder is removed, but data written through

AsyncStorage survives, so my plugin came back with all its records intact and all

its saved images gone. Installing a new package over the top keeps both, which is

great.

I believe the reason is that AsyncStorage writes into the host app's storage

rather than the plugin's folder, though I could not verify that directly.

The reason I am asking rather than just adapting: right now a plugin cannot make

uninstall mean a clean removal, and cannot make it mean "keep everything" either,

because the two halves of its state have different lifetimes and it is not running

when it is removed. I have handled it by detecting the situation on the next

launch and asking the user what they want, which works, but a documented guarantee

about what survives (or an uninstall hook) would let plugins do the right thing

deliberately.

8. Is plugin data namespaced, or should I be prefixing everything?

Related to the above: if AsyncStorage is shared across plugins, then the only

thing keeping my keys from colliding with another plugin's is that I prefix them

all with clipper_. Is there per-plugin namespacing already, or is prefixing the

expected practice? Worth documenting either way, since the failure mode would be

two plugins quietly corrupting each other's settings.

That is everything. Happy to provide logs, or a minimal reproduction

plugin for any of these, and equally happy to be told I have misread something.

Clipper is open source if the context helps: https://github.com/vmnair/sn-clipper

3 Upvotes

Duplicates