Viewing is open to everyone. Downloading and starring need a GitHub sign-in. Sign in with GitHub

ckey_jni.cpp

v0.1.0-build.85 Latest Commit ede38f5

Size
49 KB
Lines
976
Language
cpp
SHA-256
f998e68cac07…
View on GitHub

Sign in with GitHub to download. Each download is recorded with your GitHub username.

1// JNI bridge for the CKey Android SDK (org.openckey:ckey).
2//
3// A thin layer over the C ABI in include/ckey/ckey.h and include/ckey/telemetry.h.
4// It never reimplements encoding, decoding or rendering: it converts Java
5// arrays to C buffers, calls the core, and converts the results back.
6//
7// The Kotlin side is org.openckey.ckey.CKeyNative (an `object` whose
8// @JvmStatic external funs compile to static native methods). Every function
9// here must match one of those declarations; sdk/android/jni-host-test checks
10// the signatures against javac -h output and runs them on a desktop JVM.
11//
12// Build switches (set by src/main/cpp/CMakeLists.txt from Gradle properties):
13// CKEY_JNI_SCAN grayscale localization + decoding (ckey_scan)
14// CKEY_JNI_RENDER styles, raster, PNG, SVG, PDF (ckey_render)
15// CKEY_JNI_TELEMETRY aggregate counters (ckey_telemetry)
16// CKEY_JNI_CERTIFY optical certification (needs scan + render)
17// The codec (encode, capacities) is always present. A function whose module is
18// compiled out is simply absent; the Kotlin wrapper checks nativeFeatures()
19// first and reports UnsupportedOperationException instead of linking errors.
20//
21// Symbols are passed between Java and C as an opaque byte[] that holds a
22// ckey_scalable_symbol verbatim. That avoids native handles (no leaks, no
23// finalizers); every incoming blob is size- and ABI-checked before use.
24
25#include <jni.h>
26
27#include <algorithm>
28#include <cstdint>
29#include <cstring>
30#include <limits>
31#include <memory>
32#include <new>
33#include <string>
34#include <vector>
35
36#ifndef CKEY_JNI_SCAN
37#define CKEY_JNI_SCAN 1
38#endif
39#ifndef CKEY_JNI_RENDER
40#define CKEY_JNI_RENDER 1
41#endif
42#ifndef CKEY_JNI_TELEMETRY
43#define CKEY_JNI_TELEMETRY 1
44#endif
45#ifndef CKEY_JNI_CERTIFY
46#define CKEY_JNI_CERTIFY 0
47#endif
48
49#include "ckey/ckey.h"
50#if CKEY_JNI_TELEMETRY
51#include "ckey/telemetry.h"
52#endif
53
54
55#define CKEY_JNI_FN(name) JNIEXPORT JNICALL Java_org_openckey_ckey_CKeyNative_##name
56
57namespace {
58
59// Feature bits returned by nativeFeatures(); mirrored in CKeyNative.kt.
60constexpr jint kFeatureScan = 1;
61constexpr jint kFeatureRender = 2;
62constexpr jint kFeatureTelemetry = 4;
63constexpr jint kFeatureCertify = 8;
64
65// Style marshalling: ints and doubles, in this order (mirrored in CKeyStyle.kt).
66// ints: 0 background, 1 background_edge, 2 ink, 3 ink_shade (0xRRGGBB),
67// 4..7 accents[0..3], 8 accent_count, 9 accent_percent,
68// 10 modules, 11 rings, 12 rays, 13 logo_plate, 14 policy
69// doubles: 0 bevel, 1 shadow, 2 texture, 3 logo_radius
70constexpr jsize kStyleInts = 15;
71constexpr jsize kStyleDoubles = 4;
72
73// Decode info (mirrored in CKeyDecoder.kt):
74// 0 profile code (ckey_telemetry_profile numbering: 0 P0, 1 P0.1, 2 P1, 3 P2, 4 P3)
75// 1 version (Profile 0 only, else 0), 2 layout (Profile 0 only, else 0),
76// 3 border candidates found, 4 last core status
77constexpr jsize kDecodeInfo = 5;
78constexpr size_t kMaxCandidates = 16;
79// Profile 0 payloads reach 4,619 bytes (v31 at level M); like the iOS reader,
80// give the decoder a 64 KB buffer so no valid symbol is refused for space.
81constexpr size_t kScalableOutput = 65535;
82
83jclass g_ckey_exception = nullptr; // org.openckey.ckey.CKeyException
84jmethodID g_ckey_exception_ctor = nullptr; // (ILjava/lang/String;)V
85jclass g_string = nullptr;
86jmethodID g_string_from_bytes = nullptr; // String(byte[], String)
87jmethodID g_string_get_bytes = nullptr; // byte[] getBytes(String)
88jstring g_utf8 = nullptr; // "UTF-8"
89
90// ---- Exceptions -------------------------------------------------------
91
92void throw_class(JNIEnv *env, const char *cls, const char *message) {
93 if (env->ExceptionCheck()) return;
94 jclass c = env->FindClass(cls);
95 if (c) env->ThrowNew(c, message);
96}
97
98jstring new_utf8_string(JNIEnv *env, const char *text, size_t length);
99
100void throw_ckey(JNIEnv *env, int status, const char *message) {
101 if (env->ExceptionCheck()) return;
102 if (!g_ckey_exception || !g_ckey_exception_ctor) {
103 throw_class(env, "java/lang/IllegalStateException", message);
104 return;
105 }
106 const size_t len = message ? strnlen(message, 1024) : 0;
107 jstring jmessage = new_utf8_string(env, message ? message : "", len);
108 if (!jmessage) return; // OutOfMemoryError pending
109 jobject ex = env->NewObject(g_ckey_exception, g_ckey_exception_ctor, static_cast<jint>(status), jmessage);
110 if (ex) env->Throw(static_cast<jthrowable>(ex));
111}
112
113void throw_ckey(JNIEnv *env, int status, const ckey_diagnostics &diag, const char *fallback) {
114 char message[sizeof(diag.message) + 1];
115 std::memcpy(message, diag.message, sizeof(diag.message));
116 message[sizeof(diag.message)] = '\0';
117 throw_ckey(env, status, message[0] ? message : fallback);
118}
119
120void throw_iae(JNIEnv *env, const char *message) { throw_class(env, "java/lang/IllegalArgumentException", message); }
121void throw_oom(JNIEnv *env) { throw_class(env, "java/lang/OutOfMemoryError", "CKey native allocation failed"); }
122
123// Run a body and convert any C++ exception into a Java exception, so nothing
124// ever unwinds through the JNI boundary.
125template <typename R, typename F>
126R guarded(JNIEnv *env, R fallback, F &&body) {
127 try {
128 return body();
129 } catch (const std::bad_alloc &) {
130 throw_oom(env);
131 } catch (...) {
132 throw_class(env, "java/lang/IllegalStateException", "CKey native error");
133 }
134 return fallback;
135}
136
137// ---- Strings ------------------------------------------------------------
138// NewStringUTF/GetStringUTFChars use *modified* UTF-8, which differs from
139// standard UTF-8 for supplementary characters and NUL. Diagnostics, SVG and
140// logo URLs are converted through java.lang.String's UTF-8 codec instead.
141
142jstring new_utf8_string(JNIEnv *env, const char *text, size_t length) {
143 if (length > static_cast<size_t>(std::numeric_limits<jsize>::max())) { throw_oom(env); return nullptr; }
144 jbyteArray bytes = env->NewByteArray(static_cast<jsize>(length));
145 if (!bytes) return nullptr;
146 if (length) env->SetByteArrayRegion(bytes, 0, static_cast<jsize>(length), reinterpret_cast<const jbyte *>(text));
147 jstring s = static_cast<jstring>(env->NewObject(g_string, g_string_from_bytes, bytes, g_utf8));
148 env->DeleteLocalRef(bytes);
149 return s;
150}
151
152// Returns false with a Java exception pending on failure. A null jstring
153// yields ok = true and present = false.
154bool utf8_from_jstring(JNIEnv *env, jstring s, std::string &out, bool &present) {
155 out.clear();
156 present = s != nullptr;
157 if (!s) return true;
158 jbyteArray bytes = static_cast<jbyteArray>(env->CallObjectMethod(s, g_string_get_bytes, g_utf8));
159 if (env->ExceptionCheck() || !bytes) return false;
160 const jsize n = env->GetArrayLength(bytes);
161 out.resize(static_cast<size_t>(n));
162 if (n) env->GetByteArrayRegion(bytes, 0, n, reinterpret_cast<jbyte *>(&out[0]));
163 env->DeleteLocalRef(bytes);
164 if (out.find('\0') != std::string::npos) { throw_iae(env, "string contains NUL"); return false; }
165 return true;
166}
167
168jbyteArray new_byte_array(JNIEnv *env, const uint8_t *data, size_t length) {
169 if (length > static_cast<size_t>(std::numeric_limits<jsize>::max())) { throw_oom(env); return nullptr; }
170 jbyteArray out = env->NewByteArray(static_cast<jsize>(length));
171 if (out && length) env->SetByteArrayRegion(out, 0, static_cast<jsize>(length), reinterpret_cast<const jbyte *>(data));
172 return out;
173}
174
175// ---- Symbol blob --------------------------------------------------------
176
177bool read_symbol(JNIEnv *env, jbyteArray blob, ckey_scalable_symbol &symbol) {
178 if (!blob) { throw_class(env, "java/lang/NullPointerException", "symbol"); return false; }
179 if (env->GetArrayLength(blob) != static_cast<jsize>(sizeof(ckey_scalable_symbol))) {
180 throw_iae(env, "not a CKey symbol (size mismatch)");
181 return false;
182 }
183 env->GetByteArrayRegion(blob, 0, static_cast<jsize>(sizeof symbol), reinterpret_cast<jbyte *>(&symbol));
184 if (symbol.abi_version != CKEY_ABI_VERSION || symbol.version < 1 || symbol.version > CKEY_SCALABLE_VERSION_COUNT ||
185 symbol.ecc_level > CKEY_ECC_H || symbol.codeword_length > CKEY_SCALABLE_MAX_CODEWORD_BYTES) {
186 throw_iae(env, "not a CKey symbol (header mismatch)");
187 return false;
188 }
189 return true;
190}
191
192#if CKEY_JNI_RENDER
193// ---- Style --------------------------------------------------------------
194
195ckey_rgb rgb_from_int(jint v) {
196 const uint32_t u = static_cast<uint32_t>(v);
197 return ckey_rgb{static_cast<uint8_t>((u >> 16) & 0xff), static_cast<uint8_t>((u >> 8) & 0xff), static_cast<uint8_t>(u & 0xff)};
198}
199jint int_from_rgb(ckey_rgb c) {
200 return static_cast<jint>((static_cast<uint32_t>(c.r) << 16) | (static_cast<uint32_t>(c.g) << 8) | c.b);
201}
202
203// A style read from Java, keeping its logo URL alive for the duration of a call.
204struct StyleArg {
205 ckey_style style{};
206 std::string logo;
207 bool present = false; // false: use NULL (the core's default style)
208 const ckey_style *get() const { return present ? &style : nullptr; }
209};
210
211bool read_style(JNIEnv *env, jintArray ints, jdoubleArray doubles, jstring logo_href, StyleArg &out) {
212 ckey_style_default(&out.style); // never hard-code fields the ABI may add
213 out.present = false;
214 if (!ints && !doubles && !logo_href) return true;
215 if (!ints || !doubles || env->GetArrayLength(ints) != kStyleInts || env->GetArrayLength(doubles) != kStyleDoubles) {
216 throw_iae(env, "style arrays have the wrong shape");
217 return false;
218 }
219 jint i[kStyleInts];
220 jdouble d[kStyleDoubles];
221 env->GetIntArrayRegion(ints, 0, kStyleInts, i);
222 env->GetDoubleArrayRegion(doubles, 0, kStyleDoubles, d);
223 ckey_style &s = out.style;
224 s.background = rgb_from_int(i[0]);
225 s.background_edge = rgb_from_int(i[1]);
226 s.ink = rgb_from_int(i[2]);
227 s.ink_shade = rgb_from_int(i[3]);
228 for (int k = 0; k < 4; ++k) s.accents[k] = rgb_from_int(i[4 + k]);
229 // Range checks are the core's job (ckey_check_scalable_style); negative
230 // values are mapped to an out-of-range value so the core refuses them.
231 auto u32 = [](jint v) { return v < 0 ? 0xffffffffu : static_cast<uint32_t>(v); };
232 s.accent_count = u32(i[8]);
233 s.accent_percent = u32(i[9]);
234 s.modules = static_cast<ckey_module_shape>(u32(i[10]));
235 s.rings = static_cast<ckey_ring_style>(u32(i[11]));
236 s.rays = static_cast<ckey_ray_cap>(u32(i[12]));
237 s.logo_plate = u32(i[13]);
238 s.policy = u32(i[14]);
239 s.bevel = d[0];
240 s.shadow = d[1];
241 s.texture = d[2];
242 s.logo_radius = d[3];
243 bool has_logo = false;
244 if (!utf8_from_jstring(env, logo_href, out.logo, has_logo)) return false;
245 s.logo_href = has_logo && !out.logo.empty() ? out.logo.c_str() : nullptr;
246 out.present = true;
247 return true;
248}
249
250void write_style(JNIEnv *env, const ckey_style &s, jintArray ints, jdoubleArray doubles) {
251 jint i[kStyleInts];
252 jdouble d[kStyleDoubles];
253 i[0] = int_from_rgb(s.background);
254 i[1] = int_from_rgb(s.background_edge);
255 i[2] = int_from_rgb(s.ink);
256 i[3] = int_from_rgb(s.ink_shade);
257 for (int k = 0; k < 4; ++k) i[4 + k] = int_from_rgb(s.accents[k]);
258 i[8] = static_cast<jint>(s.accent_count);
259 i[9] = static_cast<jint>(s.accent_percent);
260 i[10] = static_cast<jint>(s.modules);
261 i[11] = static_cast<jint>(s.rings);
262 i[12] = static_cast<jint>(s.rays);
263 i[13] = static_cast<jint>(s.logo_plate);
264 i[14] = static_cast<jint>(s.policy);
265 d[0] = s.bevel;
266 d[1] = s.shadow;
267 d[2] = s.texture;
268 d[3] = s.logo_radius;
269 env->SetIntArrayRegion(ints, 0, kStyleInts, i);
270 env->SetDoubleArrayRegion(doubles, 0, kStyleDoubles, d);
271}
272
273bool check_style_shape(JNIEnv *env, jintArray ints, jdoubleArray doubles) {
274 if (!ints || !doubles || env->GetArrayLength(ints) != kStyleInts || env->GetArrayLength(doubles) != kStyleDoubles) {
275 throw_iae(env, "style arrays have the wrong shape");
276 return false;
277 }
278 return true;
279}
280
281// Raster options + optional logo bitmap (straight RGBA8, tightly packed).
282struct RasterArg {
283 ckey_raster_options options{};
284 ckey_logo_image logo{};
285 std::vector<uint8_t> logo_pixels;
286};
287
288bool read_raster(JNIEnv *env, jint size_px, jint policy, bool gray, jboolean transparent, jint supersample,
289 jdouble min_feature_px, jboolean materials, jbyteArray logo_rgba, jint logo_w, jint logo_h,
290 RasterArg &out) {
291 ckey_raster_options_default(&out.options); // sets abi_version = CKEY_ABI_VERSION
292 if (size_px <= 0 || policy < 0 || supersample <= 0) { throw_iae(env, "invalid raster options"); return false; }
293 out.options.size_px = static_cast<uint32_t>(size_px);
294 out.options.policy = static_cast<uint32_t>(policy);
295 out.options.format = gray ? CKEY_PIXELS_GRAY8 : CKEY_PIXELS_RGBA8;
296 out.options.transparent = transparent ? 1u : 0u;
297 out.options.supersample = static_cast<uint32_t>(supersample);
298 out.options.min_feature_px = min_feature_px;
299 out.options.materials = materials ? 1u : 0u;
300 out.options.logo = nullptr;
301 if (logo_rgba) {
302 if (logo_w <= 0 || logo_h <= 0 ||
303 static_cast<int64_t>(env->GetArrayLength(logo_rgba)) != static_cast<int64_t>(logo_w) * logo_h * 4) {
304 throw_iae(env, "logo pixels must be width * height * 4 bytes");
305 return false;
306 }
307 out.logo_pixels.resize(static_cast<size_t>(logo_w) * static_cast<size_t>(logo_h) * 4u);
308 env->GetByteArrayRegion(logo_rgba, 0, static_cast<jsize>(out.logo_pixels.size()),
309 reinterpret_cast<jbyte *>(out.logo_pixels.data()));
310 out.logo.rgba = out.logo_pixels.data();
311 out.logo.width = static_cast<uint32_t>(logo_w);
312 out.logo.height = static_cast<uint32_t>(logo_h);
313 out.logo.stride = 0;
314 out.options.logo = &out.logo;
315 }
316 return true;
317}
318
319bool render_raster(JNIEnv *env, const ckey_scalable_symbol &symbol, const StyleArg &style, const RasterArg &raster,
320 std::vector<uint8_t> &pixels) {
321 size_t required = 0;
322 ckey_status st = ckey_render_scalable_raster(&symbol, style.get(), &raster.options, nullptr, 0, &required);
323 if (st != CKEY_OK && st != CKEY_INSUFFICIENT_BUFFER) {
324 throw_ckey(env, st, "render refused (style failed the safety check or invalid options)");
325 return false;
326 }
327 pixels.resize(required);
328 st = ckey_render_scalable_raster(&symbol, style.get(), &raster.options, pixels.data(), pixels.size(), &required);
329 if (st != CKEY_OK) { throw_ckey(env, st, "render failed"); return false; }
330 return true;
331}
332
333// Explain a refused style with the core's own diagnostic (the raster API has
334// no diagnostics parameter).
335void explain_style(JNIEnv *env, const ckey_scalable_symbol &symbol, const StyleArg &style, int status) {
336 if (!style.present) { throw_ckey(env, status, "render failed"); return; }
337 ckey_diagnostics diag{};
338 diag.abi_version = CKEY_ABI_VERSION;
339 const ckey_status st = ckey_check_scalable_style(&symbol, style.get(), &diag);
340 if (st != CKEY_OK) throw_ckey(env, st, diag, "style refused");
341 else throw_ckey(env, status, "render failed");
342}
343#endif // CKEY_JNI_RENDER
344
345#if CKEY_JNI_SCAN
346// ---- Scanning -----------------------------------------------------------
347// The pipeline of ios/CKeyReader/CameraScanner.swift: find border candidates
348// (both polarities), then for each candidate, most border-like first, refine
349// it to Profile 0's field and decode Profile 0 (identifies version and layout
350// from the keys); then refine and decode Profiles 1, 2 and 3, each against its
351// own field (never using one profile's fit for another).
352
353struct Decoded {
354 std::vector<uint8_t> bytes;
355 int profile = -1;
356 uint32_t version = 0, layout = 0;
357 double quad[8] = {0};
358 int last_status = CKEY_UNCORRECTABLE;
359};
360
361bool refine(const uint8_t *gray, size_t w, size_t h, size_t stride, const double *border, ckey_field field, double *out) {
362 return ckey_refine_quad_gray8(gray, w, h, stride, border, field, out) >= 0.0;
363}
364
365size_t decode_frame(const uint8_t *gray, size_t w, size_t h, size_t stride, size_t max_candidates, Decoded &out) {
366 double borders[kMaxCandidates * 8];
367 const size_t count = std::min(ckey_find_borders_gray8(gray, w, h, stride, borders, max_candidates), max_candidates);
368 std::vector<uint8_t> buffer(kScalableOutput);
369 for (size_t c = 0; c < count; ++c) {
370 const double *border = borders + c * 8;
371 double quad[8];
372 ckey_diagnostics diag{};
373 diag.abi_version = CKEY_ABI_VERSION;
374 size_t length = 0;
375
376 if (refine(gray, w, h, stride, border, CKEY_FIELD_SCALABLE, quad)) {
377 uint32_t version = 0, layout = 0;
378 const ckey_status st = ckey_decode_scalable_gray8(gray, w, h, stride, quad, buffer.data(), buffer.size(),
379 &length, &version, &layout, &diag);
380 out.last_status = st;
381 if (st == CKEY_OK) {
382 out.bytes.assign(buffer.begin(), buffer.begin() + static_cast<std::ptrdiff_t>(std::min(length, buffer.size())));
383 out.profile = layout == CKEY_LAYOUT_PRINT ? 1 : 0;
384 out.version = version;
385 out.layout = layout;
386 std::memcpy(out.quad, quad, sizeof quad);
387 return count;
388 }
389 }
390 if (refine(gray, w, h, stride, border, CKEY_FIELD_LOCATOR, quad)) {
391 const ckey_status st = ckey_decode_gray8(gray, w, h, stride, quad, buffer.data(), CKEY_LOCATOR_PAYLOAD_BYTES, &diag);
392 out.last_status = st;
393 if (st == CKEY_OK) {
394 out.bytes.assign(buffer.begin(), buffer.begin() + CKEY_LOCATOR_PAYLOAD_BYTES);
395 out.profile = 2;
396 std::memcpy(out.quad, quad, sizeof quad);
397 return count;
398 }
399 }
400 if (refine(gray, w, h, stride, border, CKEY_FIELD_TEXT, quad)) {
401 length = 0;
402 const ckey_status st = ckey_decode_text_gray8(gray, w, h, stride, quad, buffer.data(),
403 CKEY_EXTENDED_PAYLOAD_MAX_BYTES, &length, &diag);
404 out.last_status = st;
405 if (st == CKEY_OK) {
406 out.bytes.assign(buffer.begin(), buffer.begin() + static_cast<std::ptrdiff_t>(std::min<size_t>(length, CKEY_EXTENDED_PAYLOAD_MAX_BYTES)));
407 out.profile = 3;
408 std::memcpy(out.quad, quad, sizeof quad);
409 return count;
410 }
411 }
412 if (refine(gray, w, h, stride, border, CKEY_FIELD_EXTENDED, quad)) {
413 length = 0;
414 const ckey_status st = ckey_decode_extended_gray8(gray, w, h, stride, quad, buffer.data(),
415 CKEY_EXTENDED_PAYLOAD_MAX_BYTES, &length, &diag);
416 out.last_status = st;
417 if (st == CKEY_OK) {
418 out.bytes.assign(buffer.begin(), buffer.begin() + static_cast<std::ptrdiff_t>(std::min<size_t>(length, CKEY_EXTENDED_PAYLOAD_MAX_BYTES)));
419 out.profile = 4;
420 std::memcpy(out.quad, quad, sizeof quad);
421 return count;
422 }
423 }
424 }
425 return count;
426}
427
428// Validates a frame description against the bytes available after offset.
429bool check_frame(JNIEnv *env, int64_t available, jint offset, jint width, jint height, jint stride, jint max_candidates,
430 jintArray out_info, jdoubleArray out_quad) {
431 if (width <= 0 || height <= 0 || stride < width || offset < 0 || max_candidates <= 0) {
432 throw_iae(env, "invalid frame geometry (width, height > 0; stride >= width; offset >= 0)");
433 return false;
434 }
435 const int64_t needed = static_cast<int64_t>(offset) + static_cast<int64_t>(height - 1) * stride + width;
436 if (needed > available) { throw_iae(env, "frame buffer is smaller than offset + (height - 1) * stride + width"); return false; }
437 if (!out_info || env->GetArrayLength(out_info) < kDecodeInfo || !out_quad || env->GetArrayLength(out_quad) < 8) {
438 throw_iae(env, "decode output arrays are too small");
439 return false;
440 }
441 return true;
442}
443
444jbyteArray finish_decode(JNIEnv *env, size_t candidates, const Decoded &d, jintArray out_info, jdoubleArray out_quad) {
445 jint info[kDecodeInfo] = {d.profile, static_cast<jint>(d.version), static_cast<jint>(d.layout),
446 static_cast<jint>(candidates), d.last_status};
447 env->SetIntArrayRegion(out_info, 0, kDecodeInfo, info);
448 if (d.profile < 0) return nullptr;
449 env->SetDoubleArrayRegion(out_quad, 0, 8, d.quad);
450 return new_byte_array(env, d.bytes.data(), d.bytes.size());
451}
452#endif // CKEY_JNI_SCAN
453
454} // namespace
455
456extern "C" {
457
458JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void *) {
459 JNIEnv *env = nullptr;
460 if (vm->GetEnv(reinterpret_cast<void **>(&env), JNI_VERSION_1_6) != JNI_OK) return JNI_ERR;
461 jclass s = env->FindClass("java/lang/String");
462 if (!s) return JNI_ERR;
463 g_string = static_cast<jclass>(env->NewGlobalRef(s));
464 g_string_from_bytes = env->GetMethodID(g_string, "<init>", "([BLjava/lang/String;)V");
465 g_string_get_bytes = env->GetMethodID(g_string, "getBytes", "(Ljava/lang/String;)[B");
466 jstring utf8 = env->NewStringUTF("UTF-8");
467 if (!g_string_from_bytes || !g_string_get_bytes || !utf8) return JNI_ERR;
468 g_utf8 = static_cast<jstring>(env->NewGlobalRef(utf8));
469 jclass e = env->FindClass("org/openckey/ckey/CKeyException");
470 if (e) {
471 g_ckey_exception = static_cast<jclass>(env->NewGlobalRef(e));
472 g_ckey_exception_ctor = env->GetMethodID(g_ckey_exception, "<init>", "(ILjava/lang/String;)V");
473 }
474 if (env->ExceptionCheck()) env->ExceptionClear(); // fall back to IllegalStateException
475 return JNI_VERSION_1_6;
476}
477
478// ---- Library ------------------------------------------------------------
479
480jint CKEY_JNI_FN(nativeFeatures)(JNIEnv *, jclass) {
481 jint f = 0;
482#if CKEY_JNI_SCAN
483 f |= kFeatureScan;
484#endif
485#if CKEY_JNI_RENDER
486 f |= kFeatureRender;
487#endif
488#if CKEY_JNI_TELEMETRY
489 f |= kFeatureTelemetry;
490#endif
491#if CKEY_JNI_CERTIFY
492 f |= kFeatureCertify;
493#endif
494 return f;
495}
496
497jstring CKEY_JNI_FN(nativeSoftwareVersion)(JNIEnv *env, jclass) {
498 const char *v = ckey_get_software_version();
499 return new_utf8_string(env, v ? v : "", v ? std::strlen(v) : 0);
500}
501
502jint CKEY_JNI_FN(nativeAbiVersion)(JNIEnv *, jclass) { return static_cast<jint>(ckey_get_abi_version()); }
503
504// ---- Codec --------------------------------------------------------------
505
506jint CKEY_JNI_FN(nativeCapacity)(JNIEnv *, jclass, jint version, jint level, jint layout) {
507 if (version < 1 || level < 0 || layout < 0) return 0;
508 const size_t c = ckey_scalable_capacity(static_cast<uint32_t>(version), static_cast<ckey_ecc_level>(level),
509 static_cast<ckey_scalable_layout>(layout));
510 return static_cast<jint>(std::min<size_t>(c, static_cast<size_t>(std::numeric_limits<jint>::max())));
511}
512
513jint CKEY_JNI_FN(nativeModulesAcross)(JNIEnv *, jclass, jint version) {
514 return version < 1 ? 0 : static_cast<jint>(ckey_scalable_modules_across(static_cast<uint32_t>(version)));
515}
516
517jbyteArray CKEY_JNI_FN(nativeEncode)(JNIEnv *env, jclass, jbyteArray payload, jint version, jint level, jint layout) {
518 return guarded<jbyteArray>(env, nullptr, [&]() -> jbyteArray {
519 if (!payload) { throw_class(env, "java/lang/NullPointerException", "payload"); return nullptr; }
520 if (version < 0 || level < 0 || layout < 0) { throw_iae(env, "invalid version, level or layout"); return nullptr; }
521 const jsize n = env->GetArrayLength(payload);
522 std::vector<uint8_t> bytes(static_cast<size_t>(n));
523 if (n) env->GetByteArrayRegion(payload, 0, n, reinterpret_cast<jbyte *>(bytes.data()));
524 auto symbol = std::make_unique<ckey_scalable_symbol>();
525 std::memset(symbol.get(), 0, sizeof(ckey_scalable_symbol));
526 ckey_diagnostics diag{};
527 diag.abi_version = CKEY_ABI_VERSION;
528 const ckey_status st = ckey_encode_scalable(bytes.data(), bytes.size(), static_cast<uint32_t>(version),
529 static_cast<ckey_ecc_level>(level),
530 static_cast<ckey_scalable_layout>(layout), symbol.get(), &diag);
531 if (st != CKEY_OK) { throw_ckey(env, st, diag, "encode failed"); return nullptr; }
532 return new_byte_array(env, reinterpret_cast<const uint8_t *>(symbol.get()), sizeof(ckey_scalable_symbol));
533 });
534}
535
536// [version, ecc_level, layout, payload_length, codeword_length]
537jintArray CKEY_JNI_FN(nativeSymbolInfo)(JNIEnv *env, jclass, jbyteArray blob) {
538 return guarded<jintArray>(env, nullptr, [&]() -> jintArray {
539 auto symbol = std::make_unique<ckey_scalable_symbol>();
540 if (!read_symbol(env, blob, *symbol)) return nullptr;
541 const jint info[5] = {static_cast<jint>(symbol->version), static_cast<jint>(symbol->ecc_level),
542 static_cast<jint>(symbol->layout), static_cast<jint>(symbol->payload_length),
543 static_cast<jint>(symbol->codeword_length)};
544 jintArray out = env->NewIntArray(5);
545 if (out) env->SetIntArrayRegion(out, 0, 5, info);
546 return out;
547 });
548}
549
550// ---- Safe Actions (spec/CKey-Safe-Actions.md) ---------------------------
551// A refusal is an ordinary outcome here, not an exception: out_status[0]
552// receives the core status and the returned string is the core's diagnostic.
553// On CKEY_OK the string is the verified fields, one per line (every field is
554// restricted ASCII, params are hex): scope, issuer, app, action, key_id,
555// request_id, issued_at, expires_at, params. A null public key parses only;
556// CKeySafeActions never treats a parsed-only envelope as verified.
557
558jstring CKEY_JNI_FN(nativeSafeAction)(JNIEnv *env, jclass, jbyteArray payload, jbyteArray public_key, jlong now_seconds,
559 jint max_lifetime, jintArray out_status) {
560 return guarded<jstring>(env, nullptr, [&]() -> jstring {
561 if (!payload || !out_status) { throw_class(env, "java/lang/NullPointerException", "payload"); return nullptr; }
562 if (env->GetArrayLength(out_status) < 1) { throw_iae(env, "out_status needs one element"); return nullptr; }
563 if (public_key && env->GetArrayLength(public_key) != 32) { throw_iae(env, "a public key is 32 bytes"); return nullptr; }
564 if (now_seconds < 0 || max_lifetime < 1) { throw_iae(env, "invalid time or lifetime"); return nullptr; }
565 const jsize n = env->GetArrayLength(payload);
566 std::vector<uint8_t> bytes(static_cast<size_t>(n));
567 if (n) env->GetByteArrayRegion(payload, 0, n, reinterpret_cast<jbyte *>(bytes.data()));
568 uint8_t key[32];
569 if (public_key) env->GetByteArrayRegion(public_key, 0, 32, reinterpret_cast<jbyte *>(key));
570 auto sa = std::make_unique<ckey_safe_action>();
571 std::memset(sa.get(), 0, sizeof(ckey_safe_action));
572 ckey_diagnostics diag{};
573 diag.abi_version = CKEY_ABI_VERSION;
574 const ckey_status st = public_key
575 ? ckey_safe_action_verify(bytes.data(), bytes.size(), key, static_cast<uint64_t>(now_seconds),
576 static_cast<uint32_t>(max_lifetime), sa.get(), &diag)
577 : ckey_safe_action_parse(bytes.data(), bytes.size(), sa.get(), &diag);
578 const jint status = static_cast<jint>(st);
579 env->SetIntArrayRegion(out_status, 0, 1, &status);
580 if (st != CKEY_OK) {
581 const size_t len = strnlen(diag.message, sizeof(diag.message));
582 return new_utf8_string(env, diag.message, len);
583 }
584 std::string out;
585 out.reserve(1024 + 2 * sa->params_len);
586 auto line = [&](const std::string &v) { out += v; out += '\n'; };
587 line(std::to_string(sa->scope));
588 line(sa->issuer);
589 line(sa->app);
590 line(sa->action);
591 line(sa->key_id);
592 line(sa->request_id);
593 line(std::to_string(sa->issued_at));
594 line(std::to_string(sa->expires_at));
595 static const char digits[] = "0123456789abcdef";
596 for (size_t i = 0; i < sa->params_len && i < sizeof(sa->params); ++i) {
597 out += digits[sa->params[i] >> 4];
598 out += digits[sa->params[i] & 15];
599 }
600 return new_utf8_string(env, out.data(), out.size());
601 });
602}
603
604// ---- Rendering ----------------------------------------------------------
605#if CKEY_JNI_RENDER
606
607void CKEY_JNI_FN(nativeStylePreset)(JNIEnv *env, jclass, jint preset, jintArray out_ints, jdoubleArray out_doubles) {
608 if (!check_style_shape(env, out_ints, out_doubles)) return;
609 ckey_style s;
610 ckey_style_default(&s);
611 const ckey_status st = ckey_style_preset_fill(&s, static_cast<ckey_style_preset>(preset));
612 if (st != CKEY_OK) { throw_ckey(env, st, "unknown style preset"); return; }
613 write_style(env, s, out_ints, out_doubles);
614}
615
616jstring CKEY_JNI_FN(nativeCheckStyle)(JNIEnv *env, jclass, jbyteArray blob, jintArray ints, jdoubleArray doubles,
617 jstring logo_href) {
618 return guarded<jstring>(env, nullptr, [&]() -> jstring {
619 auto symbol = std::make_unique<ckey_scalable_symbol>();
620 StyleArg style;
621 if (!read_symbol(env, blob, *symbol) || !read_style(env, ints, doubles, logo_href, style)) return nullptr;
622 ckey_style fallback;
623 ckey_style_default(&fallback);
624 ckey_diagnostics diag{};
625 diag.abi_version = CKEY_ABI_VERSION;
626 const ckey_status st = ckey_check_scalable_style(symbol.get(), style.present ? &style.style : &fallback, &diag);
627 if (st != CKEY_OK) { throw_ckey(env, st, diag, "style refused"); return nullptr; }
628 return new_utf8_string(env, diag.message, strnlen(diag.message, sizeof diag.message));
629 });
630}
631
632// Straight RGBA8 (or GRAY8). premultiply = true converts RGBA to the
633// premultiplied form Bitmap.copyPixelsFromBuffer expects for ARGB_8888.
634jbyteArray CKEY_JNI_FN(nativeRenderRaster)(JNIEnv *env, jclass, jbyteArray blob, jintArray ints, jdoubleArray doubles,
635 jstring logo_href, jint size_px, jint policy, jboolean gray,
636 jboolean transparent, jboolean premultiply, jint supersample,
637 jdouble min_feature_px, jboolean materials, jbyteArray logo_rgba,
638 jint logo_w, jint logo_h) {
639 return guarded<jbyteArray>(env, nullptr, [&]() -> jbyteArray {
640 auto symbol = std::make_unique<ckey_scalable_symbol>();
641 StyleArg style;
642 RasterArg raster;
643 if (!read_symbol(env, blob, *symbol) || !read_style(env, ints, doubles, logo_href, style) ||
644 !read_raster(env, size_px, policy, gray, transparent, supersample, min_feature_px, materials, logo_rgba,
645 logo_w, logo_h, raster))
646 return nullptr;
647 std::vector<uint8_t> pixels;
648 size_t required = 0;
649 ckey_status st = ckey_render_scalable_raster(symbol.get(), style.get(), &raster.options, nullptr, 0, &required);
650 if (st != CKEY_OK && st != CKEY_INSUFFICIENT_BUFFER) { explain_style(env, *symbol, style, st); return nullptr; }
651 if (!render_raster(env, *symbol, style, raster, pixels)) return nullptr;
652 if (premultiply && !gray) {
653 for (size_t p = 0; p + 3 < pixels.size(); p += 4) {
654 const uint32_t a = pixels[p + 3];
655 if (a == 255) continue;
656 for (int c = 0; c < 3; ++c) pixels[p + c] = static_cast<uint8_t>((pixels[p + c] * a + 127) / 255);
657 }
658 }
659 return new_byte_array(env, pixels.data(), pixels.size());
660 });
661}
662
663jbyteArray CKEY_JNI_FN(nativeRenderPng)(JNIEnv *env, jclass, jbyteArray blob, jintArray ints, jdoubleArray doubles,
664 jstring logo_href, jint size_px, jint policy, jboolean transparent,
665 jint supersample, jdouble min_feature_px, jboolean materials,
666 jbyteArray logo_rgba, jint logo_w, jint logo_h, jdouble dpi) {
667 return guarded<jbyteArray>(env, nullptr, [&]() -> jbyteArray {
668 auto symbol = std::make_unique<ckey_scalable_symbol>();
669 StyleArg style;
670 RasterArg raster;
671 if (!read_symbol(env, blob, *symbol) || !read_style(env, ints, doubles, logo_href, style) ||
672 !read_raster(env, size_px, policy, false, transparent, supersample, min_feature_px, materials, logo_rgba,
673 logo_w, logo_h, raster))
674 return nullptr;
675 size_t required = 0;
676 ckey_status st = ckey_render_scalable_raster(symbol.get(), style.get(), &raster.options, nullptr, 0, &required);
677 if (st != CKEY_OK && st != CKEY_INSUFFICIENT_BUFFER) { explain_style(env, *symbol, style, st); return nullptr; }
678 std::vector<uint8_t> pixels;
679 if (!render_raster(env, *symbol, style, raster, pixels)) return nullptr;
680 const uint32_t side = raster.options.size_px;
681 size_t png_size = 0;
682 st = ckey_encode_png(pixels.data(), side, side, static_cast<size_t>(side) * 4u, CKEY_PNG_RGBA8, dpi, nullptr, 0, &png_size);
683 if (st != CKEY_OK && st != CKEY_INSUFFICIENT_BUFFER) { throw_ckey(env, st, "PNG encoding failed"); return nullptr; }
684 std::vector<uint8_t> png(png_size);
685 st = ckey_encode_png(pixels.data(), side, side, static_cast<size_t>(side) * 4u, CKEY_PNG_RGBA8, dpi, png.data(),
686 png.size(), &png_size);
687 if (st != CKEY_OK) { throw_ckey(env, st, "PNG encoding failed"); return nullptr; }
688 return new_byte_array(env, png.data(), std::min(png_size, png.size()));
689 });
690}
691
692jstring CKEY_JNI_FN(nativeRenderSvg)(JNIEnv *env, jclass, jbyteArray blob, jintArray ints, jdoubleArray doubles,
693 jstring logo_href, jint scale) {
694 return guarded<jstring>(env, nullptr, [&]() -> jstring {
695 auto symbol = std::make_unique<ckey_scalable_symbol>();
696 StyleArg style;
697 if (!read_symbol(env, blob, *symbol) || !read_style(env, ints, doubles, logo_href, style)) return nullptr;
698 if (scale <= 0) { throw_iae(env, "scale must be positive"); return nullptr; }
699 ckey_style fallback;
700 ckey_style_default(&fallback);
701 const ckey_style *st_ptr = style.present ? &style.style : &fallback;
702 size_t required = 0;
703 ckey_status st = ckey_render_scalable_styled_svg(symbol.get(), st_ptr, static_cast<uint32_t>(scale), nullptr, 0, &required);
704 if (st != CKEY_OK && st != CKEY_INSUFFICIENT_BUFFER) { explain_style(env, *symbol, style, st); return nullptr; }
705 std::vector<char> svg(required + 1);
706 st = ckey_render_scalable_styled_svg(symbol.get(), st_ptr, static_cast<uint32_t>(scale), svg.data(), svg.size(), &required);
707 if (st != CKEY_OK) { throw_ckey(env, st, "SVG rendering failed"); return nullptr; }
708 return new_utf8_string(env, svg.data(), strnlen(svg.data(), svg.size()));
709 });
710}
711
712jbyteArray CKEY_JNI_FN(nativeRenderPdf)(JNIEnv *env, jclass, jbyteArray blob, jintArray ints, jdoubleArray doubles,
713 jstring logo_href, jdouble width_pt, jboolean transparent) {
714 return guarded<jbyteArray>(env, nullptr, [&]() -> jbyteArray {
715 auto symbol = std::make_unique<ckey_scalable_symbol>();
716 StyleArg style;
717 if (!read_symbol(env, blob, *symbol) || !read_style(env, ints, doubles, logo_href, style)) return nullptr;
718 size_t required = 0;
719 ckey_status st = ckey_render_scalable_pdf(symbol.get(), style.get(), width_pt, transparent ? 1u : 0u, nullptr, 0, &required);
720 if (st != CKEY_OK && st != CKEY_INSUFFICIENT_BUFFER) { explain_style(env, *symbol, style, st); return nullptr; }
721 std::vector<uint8_t> pdf(required);
722 st = ckey_render_scalable_pdf(symbol.get(), style.get(), width_pt, transparent ? 1u : 0u, pdf.data(), pdf.size(), &required);
723 if (st != CKEY_OK) { throw_ckey(env, st, "PDF rendering failed"); return nullptr; }
724 return new_byte_array(env, pdf.data(), std::min(required, pdf.size()));
725 });
726}
727
728#endif // CKEY_JNI_RENDER
729
730// ---- Certification (optional) ------------------------------------------
731#if CKEY_JNI_CERTIFY && CKEY_JNI_RENDER && CKEY_JNI_SCAN
732// model: null for the default, else [px_per_module, blur_px, noise, glare, trials]
733// out_cert: [status, certified, policy, adjustments, trials, trials_passed,
734// min_contrast, feature_separation, logo_load]
735// Returns the certification message; never throws for a refusal (status says).
736jstring CKEY_JNI_FN(nativeCertify)(JNIEnv *env, jclass, jbyteArray blob, jintArray ints, jdoubleArray doubles,
737 jstring logo_href, jboolean transparent, jintArray backdrop_colors,
738 jbyteArray backdrop_rgba, jint backdrop_w, jint backdrop_h, jdoubleArray model,
739 jintArray out_style_ints, jdoubleArray out_style_doubles, jdoubleArray out_cert) {
740 return guarded<jstring>(env, nullptr, [&]() -> jstring {
741 auto symbol = std::make_unique<ckey_scalable_symbol>();
742 StyleArg style;
743 if (!read_symbol(env, blob, *symbol) || !read_style(env, ints, doubles, logo_href, style)) return nullptr;
744 if (!check_style_shape(env, out_style_ints, out_style_doubles)) return nullptr;
745 if (!out_cert || env->GetArrayLength(out_cert) < 9) { throw_iae(env, "certification output array too small"); return nullptr; }
746 std::vector<ckey_rgb> colours;
747 if (backdrop_colors) {
748 const jsize n = env->GetArrayLength(backdrop_colors);
749 if (n > 16) { throw_iae(env, "at most 16 backdrop colours"); return nullptr; }
750 std::vector<jint> raw(static_cast<size_t>(n));
751 if (n) env->GetIntArrayRegion(backdrop_colors, 0, n, raw.data());
752 for (jint c : raw) colours.push_back(rgb_from_int(c));
753 }
754 std::vector<uint8_t> pixels;
755 if (backdrop_rgba) {
756 if (backdrop_w <= 0 || backdrop_h <= 0 ||
757 static_cast<int64_t>(env->GetArrayLength(backdrop_rgba)) != static_cast<int64_t>(backdrop_w) * backdrop_h * 4) {
758 throw_iae(env, "backdrop pixels must be width * height * 4 bytes");
759 return nullptr;
760 }
761 pixels.resize(static_cast<size_t>(backdrop_w) * static_cast<size_t>(backdrop_h) * 4u);
762 env->GetByteArrayRegion(backdrop_rgba, 0, static_cast<jsize>(pixels.size()), reinterpret_cast<jbyte *>(pixels.data()));
763 }
764 ckey_backdrop backdrop{};
765 backdrop.colours = colours.empty() ? nullptr : colours.data();
766 backdrop.colour_count = static_cast<uint32_t>(colours.size());
767 backdrop.rgba = pixels.empty() ? nullptr : pixels.data();
768 backdrop.width = pixels.empty() ? 0u : static_cast<uint32_t>(backdrop_w);
769 backdrop.height = pixels.empty() ? 0u : static_cast<uint32_t>(backdrop_h);
770 backdrop.stride = 0;
771 const bool has_backdrop = !colours.empty() || !pixels.empty();
772 ckey_scan_model scan;
773 ckey_scan_model_default(&scan);
774 if (model) {
775 if (env->GetArrayLength(model) != 5) { throw_iae(env, "scan model needs 5 values"); return nullptr; }
776 jdouble m[5];
777 env->GetDoubleArrayRegion(model, 0, 5, m);
778 scan.px_per_module = m[0];
779 scan.blur_px = m[1];
780 scan.noise = m[2];
781 scan.glare = m[3];
782 scan.trials = m[4] >= 0 && m[4] <= 1e6 ? static_cast<uint32_t>(m[4]) : 0u;
783 }
784 ckey_style effective;
785 ckey_style_default(&effective);
786 ckey_certification cert{};
787 cert.abi_version = CKEY_ABI_VERSION;
788 const ckey_status st = ckey_certify_scalable_style(symbol.get(), style.get(), transparent ? 1u : 0u,
789 has_backdrop ? &backdrop : nullptr, &scan, &effective, &cert);
790 // The effective style may point at our logo string; Java gets the URL
791 // back from its own input, so only the plain fields are copied.
792 write_style(env, effective, out_style_ints, out_style_doubles);
793 const jdouble result[9] = {static_cast<jdouble>(st), static_cast<jdouble>(cert.certified),
794 static_cast<jdouble>(cert.policy), static_cast<jdouble>(cert.adjustments),
795 static_cast<jdouble>(cert.trials), static_cast<jdouble>(cert.trials_passed),
796 cert.min_contrast, cert.feature_separation, cert.logo_load};
797 env->SetDoubleArrayRegion(out_cert, 0, 9, result);
798 return new_utf8_string(env, cert.message, strnlen(cert.message, sizeof cert.message));
799 });
800}
801#endif
802
803// ---- Scanning -----------------------------------------------------------
804#if CKEY_JNI_SCAN
805
806jbyteArray CKEY_JNI_FN(nativeDecodeArray)(JNIEnv *env, jclass, jbyteArray gray, jint offset, jint width, jint height,
807 jint stride, jint max_candidates, jintArray out_info, jdoubleArray out_quad) {
808 return guarded<jbyteArray>(env, nullptr, [&]() -> jbyteArray {
809 if (!gray) { throw_class(env, "java/lang/NullPointerException", "gray"); return nullptr; }
810 if (!check_frame(env, env->GetArrayLength(gray), offset, width, height, stride, max_candidates, out_info, out_quad))
811 return nullptr;
812 // GetByteArrayElements (not a critical section): decoding can take tens
813 // of milliseconds and must not stall the garbage collector.
814 jbyte *data = env->GetByteArrayElements(gray, nullptr);
815 if (!data) return nullptr;
816 Decoded d;
817 size_t candidates = 0;
818 try {
819 candidates = decode_frame(reinterpret_cast<const uint8_t *>(data) + offset, static_cast<size_t>(width),
820 static_cast<size_t>(height), static_cast<size_t>(stride),
821 std::min<size_t>(static_cast<size_t>(max_candidates), kMaxCandidates), d);
822 } catch (...) {
823 env->ReleaseByteArrayElements(gray, data, JNI_ABORT);
824 throw;
825 }
826 env->ReleaseByteArrayElements(gray, data, JNI_ABORT);
827 return finish_decode(env, candidates, d, out_info, out_quad);
828 });
829}
830
831// A direct ByteBuffer (for example android.media.Image's Y plane): no copy.
832jbyteArray CKEY_JNI_FN(nativeDecodeBuffer)(JNIEnv *env, jclass, jobject buffer, jint offset, jint width, jint height,
833 jint stride, jint max_candidates, jintArray out_info, jdoubleArray out_quad) {
834 return guarded<jbyteArray>(env, nullptr, [&]() -> jbyteArray {
835 if (!buffer) { throw_class(env, "java/lang/NullPointerException", "buffer"); return nullptr; }
836 auto *data = static_cast<const uint8_t *>(env->GetDirectBufferAddress(buffer));
837 const jlong capacity = env->GetDirectBufferCapacity(buffer);
838 if (!data || capacity < 0) { throw_iae(env, "decodeBuffer needs a direct ByteBuffer"); return nullptr; }
839 if (!check_frame(env, capacity, offset, width, height, stride, max_candidates, out_info, out_quad)) return nullptr;
840 Decoded d;
841 const size_t candidates = decode_frame(data + offset, static_cast<size_t>(width), static_cast<size_t>(height),
842 static_cast<size_t>(stride),
843 std::min<size_t>(static_cast<size_t>(max_candidates), kMaxCandidates), d);
844 return finish_decode(env, candidates, d, out_info, out_quad);
845 });
846}
847
848#endif // CKEY_JNI_SCAN
849
850// ---- Telemetry ----------------------------------------------------------
851#if CKEY_JNI_TELEMETRY
852
853jint CKEY_JNI_FN(nativeTelemetrySetMode)(JNIEnv *, jclass, jint mode) { return ckey_telemetry_set_mode(mode); }
854jint CKEY_JNI_FN(nativeTelemetryGetMode)(JNIEnv *, jclass) { return ckey_telemetry_get_mode(); }
855
856jint CKEY_JNI_FN(nativeTelemetrySetClient)(JNIEnv *env, jclass, jstring sdk, jstring version, jstring platform) {
857 return guarded<jint>(env, CKEY_INTERNAL_ERROR, [&]() -> jint {
858 std::string s, v, p;
859 bool hs = false, hv = false, hp = false;
860 if (!utf8_from_jstring(env, sdk, s, hs) || !utf8_from_jstring(env, version, v, hv) ||
861 !utf8_from_jstring(env, platform, p, hp))
862 return CKEY_INVALID_ARGUMENT;
863 if (!hs || !hv || !hp) return CKEY_INVALID_ARGUMENT;
864 return ckey_telemetry_set_client(s.c_str(), v.c_str(), p.c_str());
865 });
866}
867
868static uint64_t ms(jlong v) { return v < 0 ? 0u : static_cast<uint64_t>(v); }
869
870void CKEY_JNI_FN(nativeTelemetrySessionBegin)(JNIEnv *, jclass, jlong now_ms) { ckey_telemetry_session_begin(ms(now_ms)); }
871
872void CKEY_JNI_FN(nativeTelemetryFrame)(JNIEnv *, jclass, jlong now_ms, jint processing_ms) {
873 ckey_telemetry_frame(ms(now_ms), processing_ms < 0 ? 0u : static_cast<uint32_t>(processing_ms));
874}
875
876jint CKEY_JNI_FN(nativeTelemetrySessionSuccess)(JNIEnv *, jclass, jlong now_ms, jint profile) {
877 if (profile < 0) return CKEY_INVALID_ARGUMENT;
878 return ckey_telemetry_session_success(ms(now_ms), static_cast<uint32_t>(profile));
879}
880
881void CKEY_JNI_FN(nativeTelemetrySessionEnd)(JNIEnv *, jclass, jlong now_ms) { ckey_telemetry_session_end(ms(now_ms)); }
882
883jboolean CKEY_JNI_FN(nativeTelemetryShouldSend)(JNIEnv *, jclass, jlong now_ms) {
884 return ckey_telemetry_should_send(ms(now_ms)) ? JNI_TRUE : JNI_FALSE;
885}
886
887// Returns the batch JSON and writes its id to out_id[0]; null when there is
888// nothing to send.
889jstring CKEY_JNI_FN(nativeTelemetryTakeBatch)(JNIEnv *env, jclass, jstring utc_day, jlongArray out_id) {
890 return guarded<jstring>(env, nullptr, [&]() -> jstring {
891 std::string day;
892 bool has_day = false;
893 if (!utf8_from_jstring(env, utc_day, day, has_day)) return nullptr;
894 if (!has_day || !out_id || env->GetArrayLength(out_id) < 1) { throw_iae(env, "takeBatch needs a day and an id array"); return nullptr; }
895 std::vector<char> json(CKEY_TELEMETRY_MAX_BODY_BYTES + 1);
896 size_t len = 0;
897 uint64_t id = 0;
898 ckey_status st = ckey_telemetry_take_batch(day.c_str(), json.data(), json.size(), &len, &id);
899 if (st == CKEY_INSUFFICIENT_BUFFER) {
900 json.resize(len + 1);
901 st = ckey_telemetry_take_batch(day.c_str(), json.data(), json.size(), &len, &id);
902 }
903 if (st != CKEY_OK) { throw_ckey(env, st, "telemetry batch refused (invalid day?)"); return nullptr; }
904 if (len == 0 || id == 0) return nullptr;
905 const jlong jid = static_cast<jlong>(id);
906 env->SetLongArrayRegion(out_id, 0, 1, &jid);
907 return new_utf8_string(env, json.data(), std::min(len, json.size()));
908 });
909}
910
911jint CKEY_JNI_FN(nativeTelemetryBatchResult)(JNIEnv *, jclass, jlong batch_id, jint http_status, jlong now_ms) {
912 return ckey_telemetry_batch_result(static_cast<uint64_t>(batch_id), http_status, ms(now_ms));
913}
914
915static jlong clamp64(uint64_t v) {
916 return v > static_cast<uint64_t>(std::numeric_limits<jlong>::max()) ? std::numeric_limits<jlong>::max() : static_cast<jlong>(v);
917}
918
919// out (33 longs): started, succeeded, failed, frames, decoded[5],
920// time_to_decode {count, sum, buckets[10]}, frame_ms {count, sum, buckets[10]}
921jint CKEY_JNI_FN(nativeTelemetrySnapshot)(JNIEnv *env, jclass, jlongArray out) {
922 constexpr jsize kLen = 4 + CKEY_TELEMETRY_PROFILES + 2 * (2 + CKEY_TELEMETRY_BUCKETS);
923 if (!out || env->GetArrayLength(out) < kLen) { throw_iae(env, "snapshot array too small"); return CKEY_INVALID_ARGUMENT; }
924 ckey_telemetry_counters c{};
925 const ckey_status st = ckey_telemetry_snapshot(&c);
926 if (st != CKEY_OK) return st;
927 jlong v[kLen];
928 jsize k = 0;
929 v[k++] = clamp64(c.sessions_started);
930 v[k++] = clamp64(c.sessions_succeeded);
931 v[k++] = clamp64(c.sessions_failed);
932 v[k++] = clamp64(c.frames);
933 for (unsigned p = 0; p < CKEY_TELEMETRY_PROFILES; ++p) v[k++] = clamp64(c.decoded[p]);
934 for (const ckey_telemetry_histogram *h : {&c.time_to_decode_ms, &c.frame_ms}) {
935 v[k++] = clamp64(h->count);
936 v[k++] = clamp64(h->sum);
937 for (unsigned b = 0; b < CKEY_TELEMETRY_BUCKETS; ++b) v[k++] = clamp64(h->buckets[b]);
938 }
939 env->SetLongArrayRegion(out, 0, kLen, v);
940 return CKEY_OK;
941}
942
943jstring CKEY_JNI_FN(nativeTelemetryExportState)(JNIEnv *env, jclass) {
944 return guarded<jstring>(env, nullptr, [&]() -> jstring {
945 size_t len = 0;
946 std::vector<char> out(4096);
947 ckey_status st = ckey_telemetry_export_state(out.data(), out.size(), &len);
948 if (st == CKEY_INSUFFICIENT_BUFFER) {
949 out.resize(len + 1);
950 st = ckey_telemetry_export_state(out.data(), out.size(), &len);
951 }
952 if (st != CKEY_OK) { throw_ckey(env, st, "telemetry export failed"); return nullptr; }
953 return new_utf8_string(env, out.data(), std::min(len, out.size()));
954 });
955}
956
957jint CKEY_JNI_FN(nativeTelemetryImportState)(JNIEnv *env, jclass, jstring json) {
958 return guarded<jint>(env, CKEY_INTERNAL_ERROR, [&]() -> jint {
959 std::string s;
960 bool has = false;
961 if (!utf8_from_jstring(env, json, s, has)) return CKEY_INVALID_ARGUMENT;
962 if (!has) return CKEY_INVALID_ARGUMENT;
963 return ckey_telemetry_import_state(s.c_str());
964 });
965}
966
967jstring CKEY_JNI_FN(nativeTelemetryEndpoint)(JNIEnv *env, jclass) {
968 return new_utf8_string(env, CKEY_TELEMETRY_ENDPOINT, std::strlen(CKEY_TELEMETRY_ENDPOINT));
969}
970
971// TEST ONLY (instrumented and host tests): ckey_telemetry_reset_for_testing.
972void CKEY_JNI_FN(nativeTelemetryResetForTesting)(JNIEnv *, jclass) { ckey_telemetry_reset_for_testing(); }
973
974#endif // CKEY_JNI_TELEMETRY
975
976} // extern "C"