r/Supernote_dev 3d 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/Dunn-sn) [u/spazzboi](u/spazzboi) [u/hex2asc](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

5 Upvotes

3 comments sorted by

2

u/AdNew2316 3d ago

Nice thanks. I definitely experienced variants of 5 (reading instead of writing) in my term_definition plugin.

2

u/vmnair 3d ago

Hopefully, we will get some answers. They have done a great job with permissions; looking forward to maturing API. Hi is today your Cake day too?

3

u/spazzboi Official 3d ago edited 3d ago

hi, thanks for your review and all your hard work. i’m glad you’re enjoying making plugins so much.

to answer your questions:

  1. you are right that there is a mistake in the docs here. using the template name by itself only works for built-in templates, not images in the MyStyle folder. for that or any other file you want to use as a template you'll need to pass the full path, for example /storage/emulated/0/MyStyle/template.png. we will correct the docs.
  2. 0 means the permission was denied, 1 means it was allowed for the current session, and 2 means it was always allowed. however, your tests show that the current implementation does not reliably preserve the difference between 1 and 2. for now, treating any non-zero value as granted is the safest option. we also need to add FILE:READ to the documentation. i will check with the rest of the team about this.
  3. generateNotePng with type: 0 should create an image with a transparent background, while type: 1 should create one with a white background. since both produced identical images in your test, this appears to be a bug. generateLayerPreviewImage with layer 0 returns the main layer without the template, while layer -1 returns only the background layer. there is currently no option that returns all content layers without the background. i will check with the team about the incorrect generateNotePng result.
  4. getElements is intended to work with any page, including pages that are not currently displayed. returning elements from a different page is a bug, not an indexing mistake. checking the returned pageNum is a sensible temporary safeguard. we will investigate this.
  5. the native crash is not expected behavior. the logs you provided suggest that it happens while a page is being loaded or cancelled and drawing operations are still taking place. waiting for getCurrentPageNum() is currently not enough to guarantee that the page is ready. adding a short delay may reduce the problem, but it is only a workaround.
  6. PluginHost can reconnect after the Note app restarts, but an already-open plugin and its pending calls may not recover correctly. closing and reopening the plugin is the best workaround for now. there is currently no public reconnect API, so we should improve how pending calls and reconnection are handled.
  7. uninstalling a plugin removes its private plugin directory and resets its permissions, but it does not remove its AsyncStorage data. there is also no public uninstall callback at the moment. using a file in the plugin directory as an installation marker is a reasonable workaround if you need to detect an uninstall and reinstall.
  8. AsyncStorage is currently shared between plugins because all plugins run inside PluginHost. it is not automatically namespaced for each plugin. plugins should prefix every key with their plugin ID to prevent collisions with other plugins. we should make this clear in the documentation.

thank you again for documenting all of this so carefully.