The shim opens a preferences file in five places, and each does the same four calls: getSharedPreferences, edit, putString/remove, apply. Two of those are ported and had a copy each. This is the sequence once.
What the helper does not own
The file's name. That is the part that differs and the part that goes wrong:
craft_shared_<group>is built from page text, so it needs its own tested naming function;craft_widget_prefshas to match whatCraftWidgetProviderreads back;craft_voice_actionsis read by nothing at all.
A helper that also chose the name would be four helpers wearing one signature.
Why it lands with two callers instead of three
The third was going to be registerVoiceAction. Writing the port turned up three separate dead links:
- the row it writes (
craft_voice_actions) is written in two places and read in zero —handleVoiceActionreads the incoming intent's own action and data and never opens the file; - the reply goes to
window._craftVoiceResolve, which appears three times in this repository and all three are reads inside the&&guard — the injected bridge has novoicenamespace to assign it; - neither method is in
craft.d.ts, in the injected JS, or on iOS.
Migrating that would have moved the silence rather than removed it — the same reason setBadge is not migrated (#148). Filed as #169 with the evidence; the port is deleted rather than shipped.
The craftVoiceAction event is real and untouched — it is dispatched from the intent, declared in craft.d.ts, and cross-platform. That is the type #135 restored after I deleted it on an iOS-only scan, which is why I checked the whole repository this time before concluding anything was dead.
Testing
zig build test:android passes. No behaviour change: the shared-item and widget tests are the same tests, and the MODE_PRIVATE assertion moved to the helper rather than being duplicated in both.