also looking at this
feat(android): getCurrentPosition in Zig — ratchet 52 → 51
#188
6 files
+466
-3
| @@ -0,0 +1,207 @@ | ||
| 1 | //! `getCurrentPosition` on Android. | |
| 2 | //! | |
| 3 | //! The fifth listener shim, and the second to need `android_json_number` — | |
| 4 | //! seven numbers, six of which are doubles. | |
| 5 | //! | |
| 6 | //! ## Six doubles, and three of them started as floats | |
| 7 | //! | |
| 8 | //! `Location.accuracy`, `.speed` and `.bearing` are `Float`, but the shim | |
| 9 | //! writes them with `put(name, value)` and the overload Kotlin picks is | |
| 10 | //! `put(String, double)` — primitive widening beats boxing. So they are | |
| 11 | //! `Double`s by the time `numberToString` sees them, and print as doubles. | |
| 12 | //! | |
| 13 | //! That matters because the same float prints differently in the two places | |
| 14 | //! this bridge sends one. `bridge_android_motion` puts its floats into a | |
| 15 | //! `Map`, which boxes them as `Float`, so an accuracy of `0.1f` reads "0.1" | |
| 16 | //! there and "0.10000000149011612" here. Neither is wrong; they are different | |
| 17 | //! calls. It is the reason `android_json_number` has two functions rather than | |
| 18 | //! one widening one. | |
| 19 | //! | |
| 20 | //! ## Two rejections with different codes and different shapes | |
| 21 | //! | |
| 22 | //! `{code: 1, message: 'Permission denied'}` is a fixed object. `{code: 2, | |
| 23 | //! message: <exception>}` carries whatever Play Services said. Both are JS | |
| 24 | //! object literals in the shim with unquoted keys; here they are JSON, which | |
| 25 | //! parses to the same object. | |
| 26 | //! | |
| 27 | //! There is no code 3, and no rejection at all for the case where the fresh | |
| 28 | //! location request never produces a result — the promise simply never | |
| 29 | //! settles. That is the shim's behaviour and is left alone here; see #157 for | |
| 30 | //! the same shape elsewhere. | |
| 31 | ||
| 32 | const std = ; | |
| 33 | const bridge_error = ; | |
| 34 | ||
| 35 | pub const A = struct { | |
| 36 | pub const get_current_position = "getCurrentPosition"; | |
| 37 | }; | |
| 38 | ||
| 39 | pub const resolve_global = "_craftLocationResolve"; | |
| 40 | pub const reject_global = "_craftLocationReject"; | |
| 41 | ||
| 42 | /// `{code: 1, ...}` — the permission refusal. | |
| 43 | pub const code_permission_denied: i32 = 1; | |
| 44 | ||
| 45 | /// `{code: 2, ...}` — whatever the location client failed with. | |
| 46 | pub const code_client_failure: i32 = 2; | |
| 47 | ||
| 48 | pub const permission_denied_message = "Permission denied"; | |
| 49 | ||
| 50 | /// The seven values a position reply carries, already printed. | |
| 51 | /// | |
| 52 | /// Strings rather than numbers, because printing them is `org.json`'s job — | |
| 53 | /// see `android_json_number`. What this file owns is which keys they go under | |
| 54 | /// and in what order. | |
| 55 | pub const Printed = struct { | |
| 56 | latitude: []const u8, | |
| 57 | longitude: []const u8, | |
| 58 | accuracy: []const u8, | |
| 59 | altitude: []const u8, | |
| 60 | speed: []const u8, | |
| 61 | /// `location.bearing`, under the key `heading`. | |
| 62 | heading: []const u8, | |
| 63 | timestamp: []const u8, | |
| 64 | }; | |
| 65 | ||
| 66 | /// A position, as the shim's `JSONObject` prints it. | |
| 67 | /// | |
| 68 | /// Insertion order, because `JSONObject` is a `LinkedHashMap` — and the key | |
| 69 | /// for `location.bearing` is `heading`, which is the one name here that does | |
| 70 | /// not match the field it came from. | |
| 71 | pub fn renderPosition(allocator: std.mem.Allocator, values: Printed) ![]u8 { | |
| 72 | var out: std.ArrayListUnmanaged(u8) = .empty; | |
| 73 | errdefer out.deinit(allocator); | |
| 74 | ||
| 75 | try out.appendSlice(allocator, "{\"latitude\":"); | |
| 76 | try out.appendSlice(allocator, values.latitude); | |
| 77 | try out.appendSlice(allocator, ",\"longitude\":"); | |
| 78 | try out.appendSlice(allocator, values.longitude); | |
| 79 | try out.appendSlice(allocator, ",\"accuracy\":"); | |
| 80 | try out.appendSlice(allocator, values.accuracy); | |
| 81 | try out.appendSlice(allocator, ",\"altitude\":"); | |
| 82 | try out.appendSlice(allocator, values.altitude); | |
| 83 | try out.appendSlice(allocator, ",\"speed\":"); | |
| 84 | try out.appendSlice(allocator, values.speed); | |
| 85 | try out.appendSlice(allocator, ",\"heading\":"); | |
| 86 | try out.appendSlice(allocator, values.heading); | |
| 87 | try out.appendSlice(allocator, ",\"timestamp\":"); | |
| 88 | try out.appendSlice(allocator, values.timestamp); | |
| 89 | try out.append(allocator, '}'); | |
| 90 | return out.toOwnedSlice(allocator); | |
| 91 | } | |
| 92 | ||
| 93 | /// `{code: n, message: "…"}`. | |
| 94 | pub fn renderRejection(allocator: std.mem.Allocator, code: i32, message: []const u8) ![]u8 { | |
| 95 | var out: std.ArrayListUnmanaged(u8) = .empty; | |
| 96 | errdefer out.deinit(allocator); | |
| 97 | ||
| 98 | try out.appendSlice(allocator, "{\"code\":"); | |
| 99 | try out.print(allocator, "{d}", .{code}); | |
| 100 | try out.appendSlice(allocator, ",\"message\":\""); | |
| 101 | try bridge_error.appendJsonEscaped(allocator, &out, message); | |
| 102 | try out.appendSlice(allocator, "\"}"); | |
| 103 | return out.toOwnedSlice(allocator); | |
| 104 | } | |
| 105 | ||
| 106 | // ============================================================================= | |
| 107 | // Tests | |
| 108 | // ============================================================================= | |
| 109 | ||
| 110 | const testing = std.testing; | |
| 111 | ||
| 112 | test "a position prints seven keys, in the shim's order" { | |
| 113 | const json = try renderPosition(testing.allocator, .{ | |
| 114 | .latitude = "51.5074", | |
| 115 | .longitude = "-0.1278", | |
| 116 | .accuracy = "12.5", | |
| 117 | .altitude = "35.2", | |
| 118 | .speed = "0", | |
| 119 | .heading = "0", | |
| 120 | .timestamp = "1700000000000", | |
| 121 | }); | |
| 122 | defer testing.allocator.free(json); | |
| 123 | ||
| 124 | try testing.expectEqualStrings( | |
| 125 | \\{"latitude":51.5074,"longitude":-0.1278,"accuracy":12.5,"altitude":35.2,"speed":0,"heading":0,"timestamp":1700000000000} | |
| 126 | , json); | |
| 127 | } | |
| 128 | ||
| 129 | test "bearing is called heading, which is the one name that changes" { | |
| 130 | // `put("heading", location.bearing)`. Every other key matches its field, | |
| 131 | // so this is the one a rewrite would silently get right-looking and wrong. | |
| 132 | const json = try renderPosition(testing.allocator, .{ | |
| 133 | .latitude = "0", | |
| 134 | .longitude = "0", | |
| 135 | .accuracy = "0", | |
| 136 | .altitude = "0", | |
| 137 | .speed = "0", | |
| 138 | .heading = "271.5", | |
| 139 | .timestamp = "0", | |
| 140 | }); | |
| 141 | defer testing.allocator.free(json); | |
| 142 | ||
| 143 | var parsed = try std.json.parseFromSlice(std.json.Value, testing.allocator, json, .{}); | |
| 144 | defer parsed.deinit(); | |
| 145 | ||
| 146 | try testing.expect(parsed.value.object.get("bearing") == null); | |
| 147 | try testing.expectEqual(@as(f64, 271.5), parsed.value.object.get("heading").?.float); | |
| 148 | try testing.expectEqual(@as(usize, 7), parsed.value.object.count()); | |
| 149 | } | |
| 150 | ||
| 151 | test "org.json's integral form parses as a number, not a string" { | |
| 152 | // A stationary device reports speed and bearing of exactly zero, and | |
| 153 | // `numberToString` prints those as `0` rather than `0.0`. A page doing | |
| 154 | // arithmetic on them needs them to be numbers, which they are. | |
| 155 | const json = try renderPosition(testing.allocator, .{ | |
| 156 | .latitude = "0", | |
| 157 | .longitude = "-0", | |
| 158 | .accuracy = "9", | |
| 159 | .altitude = "0", | |
| 160 | .speed = "0", | |
| 161 | .heading = "0", | |
| 162 | .timestamp = "1700000000000", | |
| 163 | }); | |
| 164 | defer testing.allocator.free(json); | |
| 165 | ||
| 166 | var parsed = try std.json.parseFromSlice(std.json.Value, testing.allocator, json, .{}); | |
| 167 | defer parsed.deinit(); | |
| 168 | try testing.expectEqual(@as(i64, 9), parsed.value.object.get("accuracy").?.integer); | |
| 169 | try testing.expectEqual(@as(i64, 1700000000000), parsed.value.object.get("timestamp").?.integer); | |
| 170 | } | |
| 171 | ||
| 172 | test "the two rejections carry different codes" { | |
| 173 | // A page distinguishes "the user said no" from "the location client | |
| 174 | // failed" by the code alone, so the numbers are API. | |
| 175 | const denied = try renderRejection(testing.allocator, code_permission_denied, permission_denied_message); | |
| 176 | defer testing.allocator.free(denied); | |
| 177 | try testing.expectEqualStrings( | |
| 178 | \\{"code":1,"message":"Permission denied"} | |
| 179 | , denied); | |
| 180 | ||
| 181 | const failed = try renderRejection(testing.allocator, code_client_failure, "GoogleApiClient is not connected yet"); | |
| 182 | defer testing.allocator.free(failed); | |
| 183 | try testing.expectEqualStrings( | |
| 184 | \\{"code":2,"message":"GoogleApiClient is not connected yet"} | |
| 185 | , failed); | |
| 186 | ||
| 187 | try testing.expectEqual(@as(i32, 1), code_permission_denied); | |
| 188 | try testing.expectEqual(@as(i32, 2), code_client_failure); | |
| 189 | } | |
| 190 | ||
| 191 | test "a failure message carrying a quote survives as JSON" { | |
| 192 | // The message is whatever Play Services threw, which is not text this | |
| 193 | // repository controls — and it goes to the page through the same channel | |
| 194 | // that hung the promise before #154. | |
| 195 | const json = try renderRejection(testing.allocator, code_client_failure, "it's \"broken\"\n"); | |
| 196 | defer testing.allocator.free(json); | |
| 197 | ||
| 198 | var parsed = try std.json.parseFromSlice(std.json.Value, testing.allocator, json, .{}); | |
| 199 | defer parsed.deinit(); | |
| 200 | try testing.expectEqualStrings("it's \"broken\"\n", parsed.value.object.get("message").?.string); | |
| 201 | } | |
| 202 | ||
| 203 | test "the action and globals match the shim exactly" { | |
| 204 | try testing.expectEqualStrings("getCurrentPosition", A.get_current_position); | |
| 205 | try testing.expectEqualStrings("_craftLocationResolve", resolve_global); | |
| 206 | try testing.expectEqualStrings("_craftLocationReject", reject_global); | |
| 207 | } | |