ReviewOS

craft-native/craft

getCalendarEvents has no catch, so a provider failure hangs the promise

#157
Closed glennmichael123 opened this 9 days ago ·
9 days ago

CraftBridge.getCalendarEvents is the only calendar action with no try/catch:

@JavascriptInterface
fun getCalendarEvents(startDateMs: Long, endDateMs: Long) {
    if (ContextCompat.checkSelfPermission(...) != PERMISSION_GRANTED) { ...reject...; return }

    val cursor = activity.contentResolver.query(...)   // <- unguarded
    cursor?.use { while (it.moveToNext()) { ... } }

    activity.runOnUiThread {
        webView.evaluateJavascript("window._craftCalendarResolve && window._craftCalendarResolve($events)", null)
    }
}

createCalendarEvent and deleteCalendarEvent both wrap their body and reject on the exception. This one does not, so anything the provider throws propagates out of the @JavascriptInterface method. The WebView logs it and drops it, neither the resolve nor the reject runs, and the injected JS builds this promise by hand with no timeout — so the page waits forever.

It is reachable

The permission check does not make the query safe:

  • the permission can be revoked between the check and the query (a settings change, or another process calling revokeSelfPermissionOnKill), and the query then throws SecurityException;
  • a calendar provider that dies mid-query surfaces as DeadObjectException wrapped in RuntimeException;
  • a corrupt calendar database surfaces as SQLiteException.

None of these are exotic on a device with a syncing calendar account.

The same shape, once more

This is the fourth instance of the family in #127, #148 and #154: a path where the page's promise can never settle because the code that would settle it is skipped rather than failed. The common cause is the hand-built promise — _createCallback has a timeout and these do not.

Suggested fix

Wrap the body the way its two neighbours are wrapped:

} catch (e: Exception) {
    activity.runOnUiThread {
        webView.evaluateJavascript("window._craftCalendarReject && window._craftCalendarReject(${jsQuote(e.message)})", null)
    }
}

_craftCalendarReject is already assigned by the injected JS for the permission path, so nothing else has to change.

Why Zig does not paper over it

nativeGetCalendarEvents returns false when its query fails, which sends the shim down this exact path rather than inventing a rejection the shim would never send. A divergence that nothing records is how the reasons in these files go stale — the fix belongs here.

9 days ago

Implemented and pushed in 1f7075e. The Android getCalendarEvents fallback now catches provider/query/cursor failures and rejects through the existing escaped _craftCalendarReject channel, so its JavaScript promise always settles. Added a generator regression test that pins the guarded query and rejection path. Local Android build/tests and the full Zig suite passed; exact-commit CI, SBOM/vulnerability scan, Binary Size, and Benchmark workflows all completed successfully.

Sign in to comment on this issue.