ReviewOS

also looking at this

craft-native/craft

feat(android): getCalendarEvents and createCalendarEvent in Zig — ratchet 86 → 84

#158
Merged glennmichael123 wants to merge feat/android-calendar-events into main
7 files +1167 -20
packages/zig/src/android_permissions.zigadded+98-0
Changes to packages/zig/src/android_permissions.zig
@@ -0,0 +1,98 @@
1//! Runtime permissions, the two calls every guarded action starts with.
2//!
3//! The Kotlin reaches these through AndroidX:
4//!
5//! ```kotlin
6//! if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_CALENDAR)
7//! != PackageManager.PERMISSION_GRANTED) {
8//! ActivityCompat.requestPermissions(activity, arrayOf(...), REQUEST_CALENDAR)
9//! ```
10//!
11//! Zig calls the framework methods those two delegate to. That is not a
12//! shortcut around a dependency — `ContextCompat.checkSelfPermission` *is*
13//! `Context.checkSelfPermission` from API 23, and `ActivityCompat`'s
14//! `requestPermissions` is `Activity.requestPermissions` from the same level,
15//! with the pre-23 branches dead on any modern `minSdk`. Going through the
16//! framework also means no `androidx.core` on the JNI side, which matters
17//! because `libcraft.so` is built with no NDK and links nothing.
18//!
19//! ## Which thread
20//!
21//! `requestPermissions` is documented as main-thread, and this is called from
22//! the WebView's JavaBridge thread — because the shim calls it from there too.
23//! Hoisting it onto the main looper here would be a divergence, and a silent
24//! one: the two would then differ in when the dialog appears relative to the
25//! rejection the page already received.
26
27const std = ;
28const jni = ;
29
30const Jni = jni.Jni;
31const jobject = jni.jobject;
32
33/// `PackageManager.PERMISSION_GRANTED`.
34///
35/// A compile-time constant in Java, so the shim's DEX holds the literal `0`
36/// rather than a field read — which is why it is a literal here as well.
37pub const granted: i32 = 0;
38
39pub const read_calendar = "android.permission.READ_CALENDAR";
40pub const write_calendar = "android.permission.WRITE_CALENDAR";
41
42/// `activity.checkSelfPermission(permission) == PERMISSION_GRANTED`.
43pub fn isGranted(j: Jni, activity: jobject, permission: [*:0]const u8) !bool {
44 try j.pushLocalFrame(8);
45 defer _ = j.popLocalFrame(null);
46
47 const name = try j.newStringUtf(permission);
48 const activity_cls = try j.objectClass(activity);
49 const result = try j.callIntMethodA(
50 activity,
51 try j.methodId(activity_cls, "checkSelfPermission", "(Ljava/lang/String;)I"),
52 &.{.{ .l = name }},
53 );
54 return result == granted;
55}
56
57/// `activity.requestPermissions(new String[]{permission}, code)`.
58///
59/// The result arrives at `onRequestPermissionsResult`, which the shim does not
60/// implement for these codes — so the request is what makes the *next* call
61/// succeed, and this one still fails. That is the shim's behaviour and the
62/// reason the caller rejects immediately after.
63pub fn request(j: Jni, activity: jobject, permission: [*:0]const u8, code: i32) !void {
64 try j.pushLocalFrame(8);
65 defer _ = j.popLocalFrame(null);
66
67 const string_cls = try j.findClass("java/lang/String");
68 const names = try j.newObjectArray(1, string_cls);
69 try j.setObjectArrayElement(names, 0, try j.newStringUtf(permission));
70
71 const activity_cls = try j.objectClass(activity);
72 try j.callVoidMethodA(
73 activity,
74 try j.methodId(activity_cls, "requestPermissions", "([Ljava/lang/String;I)V"),
75 &.{ .{ .l = names }, .{ .i = code } },
76 );
77}
78
79// =============================================================================
80// Tests
81// =============================================================================
82
83const testing = std.testing;
84
85test "the permission names match Manifest.permission exactly" {
86 // `Manifest.permission.READ_CALENDAR` is a compile-time constant, so the
87 // shim compares against these exact strings. A typo here is a permission
88 // that is never granted and an action that always rejects.
89 try testing.expectEqualStrings("android.permission.READ_CALENDAR", read_calendar);
90 try testing.expectEqualStrings("android.permission.WRITE_CALENDAR", write_calendar);
91}
92
93test "PERMISSION_GRANTED is zero, and PERMISSION_DENIED is not" {
94 // -1 is PERMISSION_DENIED. The comparison is `== granted` rather than
95 // `!= denied` because a future third value would then read as granted.
96 try testing.expectEqual(@as(i32, 0), granted);
97 try testing.expect(granted != -1);
98}