also looking at this
feat(android): the contacts pair in Zig — ratchet 79 → 77
#166
7 files
+974
-3
| @@ -49,6 +49,7 @@ const events = ; | ||
| 49 | 49 | const calendar = ; |
| 50 | 50 | const db = ; |
| 51 | 51 | const shareditem = ; |
| 52 | const contacts = ; | |
| 52 | 53 | |
| 53 | 54 | const Jni = jni.Jni; |
| 54 | 55 | |
| @@ -77,6 +78,9 @@ const request_calendar: i32 = 1005; | ||
| 77 | 78 | /// The shim's default event length: `System.currentTimeMillis() + 3600000`. |
| 78 | 79 | const one_hour_ms: i64 = 60 * 60 * 1000; |
| 79 | 80 | |
| 81 | /// `CraftBridge.REQUEST_CONTACTS`. | |
| 82 | const request_contacts: i32 = 1004; | |
| 83 | ||
| 80 | 84 | /// The VM, captured at load so a later callback on a framework thread can ask |
| 81 | 85 | /// it for that thread's `JNIEnv`. |
| 82 | 86 | /// |
| @@ -278,6 +282,16 @@ const natives = [_]jni.JNINativeMethod{ | ||
| 278 | 282 | .signature = "(Landroid/app/Activity;Ljava/lang/String;Ljava/lang/String;)Z", |
| 279 | 283 | .fnPtr = @ptrCast(&nativeRemoveSharedItem), |
| 280 | 284 | }, |
| 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 | }, | |
| 281 | 295 | }; |
| 282 | 296 | |
| 283 | 297 | /// How binding went. A value rather than a log line, so every path is |
| @@ -982,6 +996,97 @@ fn nativeRemoveSharedItem( | ||
| 982 | 996 | return jni.JNI_TRUE; |
| 983 | 997 | } |
| 984 | 998 | |
| 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. | |
| 1006 | fn 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. | |
| 1046 | fn 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 | ||
| 985 | 1090 | // ============================================================================= |
| 986 | 1091 | // Tests |
| 987 | 1092 | // ============================================================================= |
| @@ -1093,7 +1198,7 @@ test "the registered natives name methods the Kotlin actually declares" { | ||
| 1093 | 1198 | // A descriptor is checked by the JVM at registration, so a wrong one fails |
| 1094 | 1199 | // at load rather than at call — but only if the *name* matches something. |
| 1095 | 1200 | // 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); | |
| 1097 | 1202 | try testing.expectEqualStrings("nativeGetDeviceInfo", std.mem.span(natives[0].name)); |
| 1098 | 1203 | |
| 1099 | 1204 | // And the class they bind to is the fixed one, not the templated bridge. |