setShortcuts and clearShortcuts — the launcher's long-press menu.
The version check is not symmetric, and it is ported that way
Dynamic shortcuts arrived in API 25. Below it:
setShortcutsrejects with"App shortcuts require Android 7.1+";clearShortcutsresolves withtrue.
Each is defensible alone — there is nothing to set, and nothing to clear is the same as having cleared it. Together they mean a page cannot use one answer to predict the other. Recorded in a test rather than reconciled, and the rejection message is asserted character for character because there is no code in that rejection — a page matching on it has only the string.
getString throws where optString defaults
type and title are read with getString, which raises JSONException when the key is missing. So a shortcut without a title does not become a shortcut with an empty title: the whole call rejects, and no shortcut is set — including the entries that parsed, because setDynamicShortcuts runs once after the loop.
This is the first migrated action where a missing field is an error rather than a shape to hand back, and the distinction is visible in the code: required returns error.MissingField, optional returns a null. subtitle is guarded by has, so it is genuinely optional — and an absent one means setLongLabel is never called, which is not the same as calling it with an empty string.
The id and the extra are the same string
ShortcutInfo.Builder(activity, type) uses type as the shortcut's id, and putExtra("shortcut_type", type) puts it in the intent as well. So two shortcuts sharing a type are one shortcut, silently — the framework keys the list by id and the second replaces the first. Worth knowing before a page ships four shortcuts and sees three.
The class object is the one FindClass already returned
getSystemService(ShortcutManager::class.java) wants a java.lang.Class, and the jclass FindClass hands back is one — so there is no second call to reach for it. getSystemService can still return null, and the shim's ?. then does nothing and still resolves, so that path is kept rather than treated as an error.
Intent.ACTION_VIEW is read off Intent rather than spelled as a literal, the same way bridge_android_intents reads it.
Testing
zig build test:android passes with the ratchet at 73. Both mutations fired before this was committed: defaulting a missing required field instead of raising, and dropping the "null" coercion for subtitle, each fail a named test. Both libcraft.so targets still build with no NDK.