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 throwsSecurityException; - a calendar provider that dies mid-query surfaces as
DeadObjectExceptionwrapped inRuntimeException; - 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.