also looking at this
feat(android): getCalendarEvents and createCalendarEvent in Zig — ratchet 86 → 84
#158
7 files
+1167
-20
| @@ -36,6 +36,7 @@ | ||
| 36 | 36 | const std = ; |
| 37 | 37 | const builtin = ; |
| 38 | 38 | const jni = ; |
| 39 | const permissions = ; | |
| 39 | 40 | const device = ; |
| 40 | 41 | const system = ; |
| 41 | 42 | const clipboard = ; |
| @@ -63,6 +64,17 @@ const Jni = jni.Jni; | ||
| 63 | 64 | /// individually on the way out. |
| 64 | 65 | const backing = std.heap.page_allocator; |
| 65 | 66 | |
| 67 | /// `CraftBridge.REQUEST_CALENDAR`, the request code the shim passes. | |
| 68 | /// | |
| 69 | /// It has to match, because it is the only thing that would tell an | |
| 70 | /// `onRequestPermissionsResult` which request it is answering — and if the | |
| 71 | /// shim ever grows one, a code Zig invented would arrive as a request nothing | |
| 72 | /// asked for. | |
| 73 | const request_calendar: i32 = 1005; | |
| 74 | ||
| 75 | /// The shim's default event length: `System.currentTimeMillis() + 3600000`. | |
| 76 | const one_hour_ms: i64 = 60 * 60 * 1000; | |
| 77 | ||
| 66 | 78 | /// The VM, captured at load so a later callback on a framework thread can ask |
| 67 | 79 | /// it for that thread's `JNIEnv`. |
| 68 | 80 | /// |
| @@ -226,6 +238,16 @@ const natives = [_]jni.JNINativeMethod{ | ||
| 226 | 238 | .signature = "(Landroid/app/Activity;Ljava/lang/String;)Z", |
| 227 | 239 | .fnPtr = @ptrCast(&nativeDeleteCalendarEvent), |
| 228 | 240 | }, |
| 241 | .{ | |
| 242 | .name = "nativeGetCalendarEvents", | |
| 243 | .signature = "(Landroid/app/Activity;JJ)Z", | |
| 244 | .fnPtr = @ptrCast(&nativeGetCalendarEvents), | |
| 245 | }, | |
| 246 | .{ | |
| 247 | .name = "nativeCreateCalendarEvent", | |
| 248 | .signature = "(Landroid/app/Activity;Ljava/lang/String;)Z", | |
| 249 | .fnPtr = @ptrCast(&nativeCreateCalendarEvent), | |
| 250 | }, | |
| 229 | 251 | }; |
| 230 | 252 | |
| 231 | 253 | /// How binding went. A value rather than a log line, so every path is |
| @@ -642,6 +664,131 @@ fn nativeDeleteCalendarEvent( | ||
| 642 | 664 | return jni.JNI_TRUE; |
| 643 | 665 | } |
| 644 | 666 | |
| 667 | /// `nativeGetCalendarEvents(activity, startDateMs, endDateMs)`. | |
| 668 | /// | |
| 669 | /// Reads the calendar and settles `_craftCalendarResolve` with the array. | |
| 670 | /// | |
| 671 | /// ## Where this returns false | |
| 672 | /// | |
| 673 | /// Only before anything has been sent. A failure after the reply channel has | |
| 674 | /// been used would settle the page's promise twice, since a false sends the | |
| 675 | /// shim down the same path — so the JNI work runs to completion or reports | |
| 676 | /// nothing at all, and the shim answers instead. | |
| 677 | /// | |
| 678 | /// That includes the case where the query itself throws. The shim wraps none | |
| 679 | /// of `getCalendarEvents` in a try/catch, so a `SecurityException` from a | |
| 680 | /// permission revoked between the check and the query propagates out of the | |
| 681 | /// `@JavascriptInterface` method and the promise hangs. Rejecting here would | |
| 682 | /// be kinder and would also be a divergence nothing records, so this falls | |
| 683 | /// through and lets the shim behave as it does. See #157. | |
| 684 | fn nativeGetCalendarEvents( | |
| 685 | env: jni.JNIEnv, | |
| 686 | _: jni.jobject, | |
| 687 | activity: jni.jobject, | |
| 688 | start_date_ms: jni.jlong, | |
| 689 | end_date_ms: jni.jlong, | |
| 690 | ) callconv(.c) jni.jboolean { | |
| 691 | const j = Jni.init(env); | |
| 692 | var arena = std.heap.ArenaAllocator.init(backing); | |
| 693 | defer arena.deinit(); | |
| 694 | const allocator = arena.allocator(); | |
| 695 | ||
| 696 | const granted = permissions.isGranted(j, activity, permissions.read_calendar) catch |err| { | |
| 697 | std.log.warn("craft: getCalendarEvents fell through to the shim ({s})", .{@errorName(err)}); | |
| 698 | return jni.JNI_FALSE; | |
| 699 | }; | |
| 700 | ||
| 701 | if (!granted) { | |
| 702 | // The shim asks and rejects in the same breath: the answer arrives at | |
| 703 | // `onRequestPermissionsResult`, which nothing here implements, so the | |
| 704 | // request is what makes a *later* call work rather than this one. | |
| 705 | permissions.request(j, activity, permissions.read_calendar, request_calendar) catch |err| { | |
| 706 | std.log.warn("craft: getCalendarEvents fell through to the shim ({s})", .{@errorName(err)}); | |
| 707 | return jni.JNI_FALSE; | |
| 708 | }; | |
| 709 | events.settle(allocator, calendar.list_reject_global, "\"Permission denied\"") catch return jni.JNI_FALSE; | |
| 710 | return jni.JNI_TRUE; | |
| 711 | } | |
| 712 | ||
| 713 | const now = calendar.currentTimeMillis(j) catch |err| { | |
| 714 | std.log.warn("craft: getCalendarEvents fell through to the shim ({s})", .{@errorName(err)}); | |
| 715 | return jni.JNI_FALSE; | |
| 716 | }; | |
| 717 | const window = calendar.windowFor(start_date_ms, end_date_ms, now); | |
| 718 | ||
| 719 | var payload: std.ArrayListUnmanaged(u8) = .empty; | |
| 720 | defer payload.deinit(allocator); | |
| 721 | calendar.queryEvents(j, allocator, activity, window, &payload) catch |err| { | |
| 722 | std.log.warn("craft: getCalendarEvents fell through to the shim ({s})", .{@errorName(err)}); | |
| 723 | return jni.JNI_FALSE; | |
| 724 | }; | |
| 725 | ||
| 726 | events.settle(allocator, calendar.list_resolve_global, payload.items) catch return jni.JNI_FALSE; | |
| 727 | return jni.JNI_TRUE; | |
| 728 | } | |
| 729 | ||
| 730 | /// `nativeCreateCalendarEvent(activity, eventJson)`. | |
| 731 | /// | |
| 732 | /// Returns false — leaving the shim to serve it — for any payload outside the | |
| 733 | /// shape `NewCalendarEvent` declares. `org.json` coerces a `startDate` sent as | |
| 734 | /// a string and a `title` sent as a number, and the string forms it produces | |
| 735 | /// for a `Double` are not something to reproduce from memory. Declining is | |
| 736 | /// cheap here because nothing has been sent yet. | |
| 737 | fn nativeCreateCalendarEvent( | |
| 738 | env: jni.JNIEnv, | |
| 739 | _: jni.jobject, | |
| 740 | activity: jni.jobject, | |
| 741 | event_json: jni.jstring, | |
| 742 | ) callconv(.c) jni.jboolean { | |
| 743 | const j = Jni.init(env); | |
| 744 | var arena = std.heap.ArenaAllocator.init(backing); | |
| 745 | defer arena.deinit(); | |
| 746 | const allocator = arena.allocator(); | |
| 747 | ||
| 748 | const granted = permissions.isGranted(j, activity, permissions.write_calendar) catch |err| { | |
| 749 | std.log.warn("craft: createCalendarEvent fell through to the shim ({s})", .{@errorName(err)}); | |
| 750 | return jni.JNI_FALSE; | |
| 751 | }; | |
| 752 | ||
| 753 | if (!granted) { | |
| 754 | permissions.request(j, activity, permissions.write_calendar, request_calendar) catch |err| { | |
| 755 | std.log.warn("craft: createCalendarEvent fell through to the shim ({s})", .{@errorName(err)}); | |
| 756 | return jni.JNI_FALSE; | |
| 757 | }; | |
| 758 | events.settle(allocator, calendar.create_reject_global, "\"Permission denied\"") catch return jni.JNI_FALSE; | |
| 759 | return jni.JNI_TRUE; | |
| 760 | } | |
| 761 | ||
| 762 | const text = j.stringToUtf8(allocator, event_json) catch return jni.JNI_FALSE; | |
| 763 | ||
| 764 | // `JSONTokener` is lenient where `std.json` is strict — unquoted keys, | |
| 765 | // single quotes, a trailing comma. Everything strict JSON accepts it | |
| 766 | // accepts too, so the difference only ever sends work back to the shim. | |
| 767 | var parsed = std.json.parseFromSlice(std.json.Value, allocator, text, .{}) catch | |
| 768 | return jni.JNI_FALSE; | |
| 769 | defer parsed.deinit(); | |
| 770 | ||
| 771 | const start_default = calendar.currentTimeMillis(j) catch return jni.JNI_FALSE; | |
| 772 | const end_default = calendar.currentTimeMillis(j) catch return jni.JNI_FALSE; | |
| 773 | const event = calendar.parseNewEvent( | |
| 774 | parsed.value, | |
| 775 | start_default, | |
| 776 | end_default +% one_hour_ms, | |
| 777 | ) orelse return jni.JNI_FALSE; | |
| 778 | ||
| 779 | const id = calendar.insertEvent(j, allocator, activity, event) catch |err| { | |
| 780 | // The shim catches here and rejects with the exception's message. The | |
| 781 | // throwable was described to logcat and cleared by `Jni.check` before | |
| 782 | // this point, so the error name is what is left to say. | |
| 783 | calendar.rejectOn(allocator, calendar.create_reject_global, @errorName(err)) catch {}; | |
| 784 | return jni.JNI_TRUE; | |
| 785 | }; | |
| 786 | ||
| 787 | const payload = calendar.jsonString(allocator, id) catch return jni.JNI_FALSE; | |
| 788 | events.settle(allocator, calendar.create_resolve_global, payload) catch return jni.JNI_FALSE; | |
| 789 | return jni.JNI_TRUE; | |
| 790 | } | |
| 791 | ||
| 645 | 792 | // ============================================================================= |
| 646 | 793 | // Tests |
| 647 | 794 | // ============================================================================= |
| @@ -753,7 +900,7 @@ test "the registered natives name methods the Kotlin actually declares" { | ||
| 753 | 900 | // A descriptor is checked by the JVM at registration, so a wrong one fails |
| 754 | 901 | // at load rather than at call — but only if the *name* matches something. |
| 755 | 902 | // These two strings are the contract with CraftBridge.kt. |
| 756 | try testing.expectEqual(@as(usize, 17), natives.len); | |
| 903 | try testing.expectEqual(@as(usize, 19), natives.len); | |
| 757 | 904 | try testing.expectEqualStrings("nativeGetDeviceInfo", std.mem.span(natives[0].name)); |
| 758 | 905 | |
| 759 | 906 | // And the class they bind to is the fixed one, not the templated bridge. |