ReviewOS

also looking at this

craft-native/craft

feat(android): the contacts pair in Zig — ratchet 79 → 77

#166
Merged chris wants to merge feat/android-contacts into main
7 files +974 -3
packages/zig/src/android_dispatch.zigmodified+106-1
Changes to packages/zig/src/android_dispatch.zig
@@ -49,6 +49,7 @@ const events = ;
4949const calendar = ;
5050const db = ;
5151const shareditem = ;
52const contacts = ;
5253
5354const Jni = jni.Jni;
5455
@@ -77,6 +78,9 @@ const request_calendar: i32 = 1005;
7778/// The shim's default event length: `System.currentTimeMillis() + 3600000`.
7879const one_hour_ms: i64 = 60 * 60 * 1000;
7980
81/// `CraftBridge.REQUEST_CONTACTS`.
82const request_contacts: i32 = 1004;
83
8084/// The VM, captured at load so a later callback on a framework thread can ask
8185/// it for that thread's `JNIEnv`.
8286///
@@ -278,6 +282,16 @@ const natives = [_]jni.JNINativeMethod{
278282 .signature = "(Landroid/app/Activity;Ljava/lang/String;Ljava/lang/String;)Z",
279283 .fnPtr = @ptrCast(&nativeRemoveSharedItem),
280284 },
285 .{
286 .name = "nativeGetContacts",
287 .signature = "(Landroid/app/Activity;)Z",
288 .fnPtr = @ptrCast(&nativeGetContacts),
289 },
290 .{
291 .name = "nativeAddContact",
292 .signature = "(Landroid/app/Activity;Ljava/lang/String;)Z",
293 .fnPtr = @ptrCast(&nativeAddContact),
294 },
281295};
282296
283297/// How binding went. A value rather than a log line, so every path is
@@ -982,6 +996,97 @@ fn nativeRemoveSharedItem(
982996 return jni.JNI_TRUE;
983997}
984998
999/// `nativeGetContacts(activity)`.
1000///
1001/// Falls through when the query fails, for the reason `getCalendarEvents`
1002/// does: the shim wraps none of `getContacts` in a try/catch, so an exception
1003/// escapes the `@JavascriptInterface` method and the promise hangs. Inventing
1004/// a rejection here would be kinder and would be a divergence nothing
1005/// records. See #157.
1006fn nativeGetContacts(
1007 env: jni.JNIEnv,
1008 _: jni.jobject,
1009 activity: jni.jobject,
1010) callconv(.c) jni.jboolean {
1011 const j = Jni.init(env);
1012 var arena = std.heap.ArenaAllocator.init(backing);
1013 defer arena.deinit();
1014 const allocator = arena.allocator();
1015
1016 const granted = permissions.isGranted(j, activity, permissions.read_contacts) catch |err| {
1017 std.log.warn("craft: getContacts fell through to the shim ({s})", .{@errorName(err)});
1018 return jni.JNI_FALSE;
1019 };
1020
1021 if (!granted) {
1022 permissions.request(j, activity, permissions.read_contacts, request_contacts) catch |err| {
1023 std.log.warn("craft: getContacts fell through to the shim ({s})", .{@errorName(err)});
1024 return jni.JNI_FALSE;
1025 };
1026 events.settle(allocator, contacts.reject_global, "\"Permission denied\"") catch return jni.JNI_FALSE;
1027 return jni.JNI_TRUE;
1028 }
1029
1030 var payload: std.ArrayListUnmanaged(u8) = .empty;
1031 defer payload.deinit(allocator);
1032 contacts.queryContacts(j, allocator, activity, &payload) catch |err| {
1033 std.log.warn("craft: getContacts fell through to the shim ({s})", .{@errorName(err)});
1034 return jni.JNI_FALSE;
1035 };
1036
1037 events.settle(allocator, contacts.resolve_global, payload.items) catch return jni.JNI_FALSE;
1038 return jni.JNI_TRUE;
1039}
1040
1041/// `nativeAddContact(activity, contactJson)`.
1042///
1043/// Resolves with the new contact's id as a JSON *string*, because the shim
1044/// interpolated a `Long` into a quoted JavaScript string and the page has
1045/// always received text.
1046fn nativeAddContact(
1047 env: jni.JNIEnv,
1048 _: jni.jobject,
1049 activity: jni.jobject,
1050 contact_json: jni.jstring,
1051) callconv(.c) jni.jboolean {
1052 const j = Jni.init(env);
1053 var arena = std.heap.ArenaAllocator.init(backing);
1054 defer arena.deinit();
1055 const allocator = arena.allocator();
1056
1057 const granted = permissions.isGranted(j, activity, permissions.write_contacts) catch |err| {
1058 std.log.warn("craft: addContact fell through to the shim ({s})", .{@errorName(err)});
1059 return jni.JNI_FALSE;
1060 };
1061
1062 if (!granted) {
1063 permissions.request(j, activity, permissions.write_contacts, request_contacts) catch |err| {
1064 std.log.warn("craft: addContact fell through to the shim ({s})", .{@errorName(err)});
1065 return jni.JNI_FALSE;
1066 };
1067 events.settle(allocator, contacts.add_reject_global, "\"Permission denied\"") catch return jni.JNI_FALSE;
1068 return jni.JNI_TRUE;
1069 }
1070
1071 const text = j.stringToUtf8(allocator, contact_json) catch return jni.JNI_FALSE;
1072 var parsed = std.json.parseFromSlice(std.json.Value, allocator, text, .{}) catch
1073 return jni.JNI_FALSE;
1074 defer parsed.deinit();
1075
1076 const contact = contacts.parseNewContact(parsed.value) orelse return jni.JNI_FALSE;
1077
1078 const id = contacts.addContact(j, allocator, activity, contact) catch |err| {
1079 calendar.rejectOn(allocator, contacts.add_reject_global, @errorName(err)) catch {};
1080 return jni.JNI_TRUE;
1081 };
1082
1083 var buf: [24]u8 = undefined;
1084 const digits = std.fmt.bufPrint(&buf, "{d}", .{id}) catch return jni.JNI_FALSE;
1085 const payload = calendar.jsonString(allocator, digits) catch return jni.JNI_FALSE;
1086 events.settle(allocator, contacts.add_resolve_global, payload) catch return jni.JNI_FALSE;
1087 return jni.JNI_TRUE;
1088}
1089
9851090// =============================================================================
9861091// Tests
9871092// =============================================================================
@@ -1093,7 +1198,7 @@ test "the registered natives name methods the Kotlin actually declares" {
10931198 // A descriptor is checked by the JVM at registration, so a wrong one fails
10941199 // at load rather than at call — but only if the *name* matches something.
10951200 // These two strings are the contract with CraftBridge.kt.
1096 try testing.expectEqual(@as(usize, 24), natives.len);
1201 try testing.expectEqual(@as(usize, 26), natives.len);
10971202 try testing.expectEqualStrings("nativeGetDeviceInfo", std.mem.span(natives[0].name));
10981203
10991204 // And the class they bind to is the fixed one, not the templated bridge.