also looking at this
fix(android): text going to the JVM is modified UTF-8, and was not being
#168
12 files
+326
-136
| @@ -66,12 +66,12 @@ fn startActivity(j: Jni, activity: jobject, intent: jobject) !void { | ||
| 66 | 66 | /// produces a Uri with a null scheme for nonsense. The failure that matters |
| 67 | 67 | /// comes later, from `startActivity` finding nothing to handle the result, and |
| 68 | 68 | /// that arrives here as `JavaException` from the `check` after the call. |
| 69 | pub fn openUrl(j: Jni, activity: jobject, url: [*:0]const u8) !void { | |
| 69 | pub fn openUrl(j: Jni, allocator: std.mem.Allocator, activity: jobject, url: []const u8) !void { | |
| 70 | 70 | try j.pushLocalFrame(16); |
| 71 | 71 | defer _ = j.popLocalFrame(null); |
| 72 | 72 | |
| 73 | 73 | const uri_cls = try j.findClass("android/net/Uri"); |
| 74 | const url_string = try j.newStringUtf(url); | |
| 74 | const url_string = try j.newStringUtf8(allocator, url); | |
| 75 | 75 | const uri = try j.callStaticObjectMethodA( |
| 76 | 76 | uri_cls, |
| 77 | 77 | try j.staticMethodId(uri_cls, "parse", "(Ljava/lang/String;)Landroid/net/Uri;"), |
| @@ -93,7 +93,7 @@ pub fn openUrl(j: Jni, activity: jobject, url: [*:0]const u8) !void { | ||
| 93 | 93 | /// |
| 94 | 94 | /// An empty `title` skips `EXTRA_SUBJECT` entirely rather than setting it to |
| 95 | 95 | /// the empty string — see the module comment. |
| 96 | pub fn share(j: Jni, activity: jobject, text: [*:0]const u8, title: [*:0]const u8) !void { | |
| 96 | pub fn share(j: Jni, allocator: std.mem.Allocator, activity: jobject, text: []const u8, title: []const u8) !void { | |
| 97 | 97 | try j.pushLocalFrame(24); |
| 98 | 98 | defer _ = j.popLocalFrame(null); |
| 99 | 99 | |
| @@ -122,12 +122,12 @@ pub fn share(j: Jni, activity: jobject, text: [*:0]const u8, title: [*:0]const u | ||
| 122 | 122 | ); |
| 123 | 123 | |
| 124 | 124 | const extra_text = try intentConstant(j, intent_cls, "EXTRA_TEXT"); |
| 125 | const body = try j.newStringUtf(text); | |
| 125 | const body = try j.newStringUtf8(allocator, text); | |
| 126 | 126 | _ = try j.callObjectMethodA(intent, put_extra, &.{ .{ .l = extra_text }, .{ .l = body } }); |
| 127 | 127 | |
| 128 | if (title[0] != 0) { | |
| 128 | if (title.len != 0) { | |
| 129 | 129 | const extra_subject = try intentConstant(j, intent_cls, "EXTRA_SUBJECT"); |
| 130 | const subject = try j.newStringUtf(title); | |
| 130 | const subject = try j.newStringUtf8(allocator, title); | |
| 131 | 131 | _ = try j.callObjectMethodA(intent, put_extra, &.{ .{ .l = extra_subject }, .{ .l = subject } }); |
| 132 | 132 | } |
| 133 | 133 | |
| @@ -188,7 +188,12 @@ fn fStaticObjectField(_: jni.JNIEnv, _: jni.jclass, _: jni.jfieldID) callconv(.c | ||
| 188 | 188 | return obj(2); |
| 189 | 189 | } |
| 190 | 190 | fn fNewStringUTF(_: jni.JNIEnv, text: [*:0]const u8) callconv(.c) jni.jstring { |
| 191 | fake_strings.append(testing.allocator, std.mem.span(text)) catch {}; | |
| 191 | // Copied, not borrowed. The caller re-encodes into a scratch buffer and | |
| 192 | // frees it as soon as the JVM has the string — which is what a real | |
| 193 | // `NewStringUTF` licenses, and what made this fake read freed memory when | |
| 194 | // it held the pointer. | |
| 195 | const copy = testing.allocator.dupe(u8, std.mem.span(text)) catch return obj(3); | |
| 196 | fake_strings.append(testing.allocator, copy) catch testing.allocator.free(copy); | |
| 192 | 197 | return obj(3); |
| 193 | 198 | } |
| 194 | 199 | fn fCallStaticObjectMethodA(_: jni.JNIEnv, _: jni.jclass, id: jni.jmethodID, _: [*]const jni.jvalue) callconv(.c) jobject { |
| @@ -246,6 +251,7 @@ fn fakeEnv(table: *jni.JNINativeInterface) void { | ||
| 246 | 251 | fn resetFakes() void { |
| 247 | 252 | fake_calls.clearRetainingCapacity(); |
| 248 | 253 | fake_field_names.clearRetainingCapacity(); |
| 254 | for (fake_strings.items) |text| testing.allocator.free(text); | |
| 249 | 255 | fake_strings.clearRetainingCapacity(); |
| 250 | 256 | fake_throw_on_start = false; |
| 251 | 257 | fake_pending = null; |
| @@ -254,6 +260,7 @@ fn resetFakes() void { | ||
| 254 | 260 | fn freeFakes() void { |
| 255 | 261 | fake_calls.deinit(testing.allocator); |
| 256 | 262 | fake_field_names.deinit(testing.allocator); |
| 263 | for (fake_strings.items) |text| testing.allocator.free(text); | |
| 257 | 264 | fake_strings.deinit(testing.allocator); |
| 258 | 265 | fake_calls = .empty; |
| 259 | 266 | fake_field_names = .empty; |
| @@ -280,7 +287,7 @@ test "openURL parses the url and hands the intent to the activity" { | ||
| 280 | 287 | fakeEnv(&table); |
| 281 | 288 | const ptr: *const jni.JNINativeInterface = &table; |
| 282 | 289 | |
| 283 | try openUrl(Jni.init(&ptr), obj(8), "https://example.com/x?y=1"); | |
| 290 | try openUrl(Jni.init(&ptr), testing.allocator, obj(8), "https://example.com/x?y=1"); | |
| 284 | 291 | |
| 285 | 292 | try testing.expect(sawCall("parse")); |
| 286 | 293 | try testing.expect(sawCall("<init>")); |
| @@ -303,7 +310,7 @@ test "a URL nothing can open reports the exception rather than success" { | ||
| 303 | 310 | fake_throw_on_start = true; |
| 304 | 311 | try testing.expectError( |
| 305 | 312 | jni.JniError.JavaException, |
| 306 | openUrl(Jni.init(&ptr), obj(8), "definitely-not-a-scheme://x"), | |
| 313 | openUrl(Jni.init(&ptr), testing.allocator, obj(8), "definitely-not-a-scheme://x"), | |
| 307 | 314 | ); |
| 308 | 315 | } |
| 309 | 316 | |
| @@ -314,7 +321,7 @@ test "share sets the text extra and the chooser" { | ||
| 314 | 321 | fakeEnv(&table); |
| 315 | 322 | const ptr: *const jni.JNINativeInterface = &table; |
| 316 | 323 | |
| 317 | try share(Jni.init(&ptr), obj(8), "hello", "Subject"); | |
| 324 | try share(Jni.init(&ptr), testing.allocator, obj(8), "hello", "Subject"); | |
| 318 | 325 | |
| 319 | 326 | try testing.expect(sawField("ACTION_SEND")); |
| 320 | 327 | try testing.expect(sawField("EXTRA_TEXT")); |
| @@ -337,7 +344,7 @@ test "an empty title omits the subject rather than setting it empty" { | ||
| 337 | 344 | fakeEnv(&table); |
| 338 | 345 | const ptr: *const jni.JNINativeInterface = &table; |
| 339 | 346 | |
| 340 | try share(Jni.init(&ptr), obj(8), "hello", ""); | |
| 347 | try share(Jni.init(&ptr), testing.allocator, obj(8), "hello", ""); | |
| 341 | 348 | |
| 342 | 349 | try testing.expect(sawField("EXTRA_TEXT")); |
| 343 | 350 | try testing.expect(!sawField("EXTRA_SUBJECT")); |