ReviewOS

also looking at this

craft-native/craft

fix(android): resolve share() with whether the person picked an app

#210
Merged glennmichael123 wants to merge fix/android-share-outcome into main
12 files +399 -56
9 days ago

Closes #203. Stacked on #208 (the iOS half); once that merges, this rebases down to its own two commits.

What was wrong

On Android, craft.share(text, title) returned undefined, and craft.share.share(options) wrapped that in a promise that resolved immediately. Whatever happened next, the page heard nothing: the person could share, dismiss the menu, or there could be nothing to share at all.

What it answers now

share returns a promise that:

  • resolves true when the person picks an app;
  • resolves false when they dismiss the menu;
  • rejects with "Nothing to share" for an empty string, the same line iOS draws;
  • rejects with the exception message when the menu cannot open.

false is not a rejection because dismissing the menu is the person's choice, not an error. iOS already resolves the same completed flag the same way.

How Android can tell

Android's share menu (the chooser) gives two partial signals:

  • The IntentSender passed to Intent.createChooser(intent, title, sender) fires when the person picks an app. It fires for nothing else.
  • The activity result arrives both when the person picks and when they dismiss, and it is RESULT_CANCELED in both cases.

Neither answers the question alone. So the IntentSender's broadcast sets a flag, and the result, which always arrives, settles the promise with that flag. Everything that touches the flag runs on the main thread: the launch is posted there, and broadcasts and activity results are delivered there.

Details:

  • The IntentSender is an explicit, immutable broadcast PendingIntent. It is explicit so it reaches this app only, and immutable because only the fact that it fired matters, not the component the chooser adds. Its receiver is registered through ContextCompat.registerReceiver(..., RECEIVER_NOT_EXPORTED) and unregistered in close().
  • The request code is REQUEST_SHARE = 1011. It is routed in onActivityResult, including the closed-bridge path.
  • Zig launches the same chooser for the same result. nativeShare now takes the sender and the request code from Kotlin and calls startActivityForResult. Kotlin chooses the code because CraftBridge is what receives the result; a copy of the constant in Zig would be a second place for it to drift.

craft.d.ts and the SDK's share.share now type the answer as Promise<boolean>. The Web Share API fallback also answers false for AbortError rather than throwing.

The first real cancellation path in the E2E suite

The first E2E PR left out a true cancellation case. The realistic candidate, the camera, depends on a camera app being installed and on keyevent timing. The share menu has neither problem: the system always provides one. The Android suite gains:

  • share.empty.rejects: share('') must reject with "Nothing to share".
  • share.dismissed.resolvesFalse: the page announces that it is about to open the menu, then calls share. The harness waits until dumpsys window shows ChooserActivity holding input focus, saves share-menu.png and presses Back. The promise must then resolve false.

false is also what a share would answer if no menu ever opened. So the harness separately fails the run when the page asked for a dismissal and no menu came to the front, which the page cannot see from where it sits. The harness presses Back only once it has seen the menu; pressing on a timer could close the app instead, and the page would never report.

shareMenuInFront requires input focus, not just a resumed activity, because Back goes to whichever window holds focus. The first CI run showed the gap: the activity manager marked the chooser resumed at 50.37s, and its window took focus at 52.63s. Back pressed in that gap would have closed the app, and that run passed only because the harness happened to poll late.

It matches both chooser locations: android/com.android.internal.app.ChooserActivity up to Android 13 and com.android.intentresolver/com.android.intentresolver.ChooserActivity from Android 14. Unit tests cover both spellings and a chooser that is resumed but not yet focused.

On the runtime leg, Zig must launch the chooser without falling back to Kotlin. The leg already fails on any decline, so a descriptor mismatch or a JNI failure in the new nativeShare would turn it red.

Not done here

Android does not gate share on enableShare. It turns out Android gates almost nothing on its enable* flags, apart from push, which gets a generated rejection. That is a wider question than this issue and is filed as #209.

Verification

  • Locally: bun run test, bun run typecheck, bun run verify:packages, Pickier, 37 protocol tests, and the test:android and test:ios gates, including new Zig tests. Those tests check that the chooser is launched for a result with the sender and code it was given, and that a launch failure is reported rather than swallowed.
  • Only in this PR's CI: the Kotlin compile and the emulator run. There is no Android toolchain on the machine this was written on.

12 changed files on the files tab, with 0 review threads.

Sign in to comment on this pull request.