1// Editor's natives: crates/mobile's C surface for the Java host.
2#include <android/log.h>
3#include <android/native_window_jni.h>
4#include <jni.h>
5#include <pthread.h>
6#include <stdint.h>
7#include <stdio.h>
8#include <stdlib.h>
9#include <string.h>
10#include <unistd.h>
11#include "snowbound.h"
12
13#define JNI(name) Java_net_paperclover_snowbound_Editor_##name
14
15// Rust reports problems on stderr, which Android discards; these lines go to logcat instead.
16static void *log_stderr(void *pipe) {
17 FILE *lines = fdopen((int)(intptr_t)pipe, "r");
18 char line[1024];
19 while (lines && fgets(line, sizeof line, lines)) __android_log_write(ANDROID_LOG_WARN, "snowbound", line);
20 return NULL;
21}
22
23static JavaVM *machine;
24static jclass editor, provider;
25static jmethodID woken_method, document_method, children_method, open_method, close_method, rename_method;
26static const sb_provider documents;
27
28JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
29 (void)reserved;
30 machine = vm;
31 JNIEnv *env;
32 if ((*vm)->GetEnv(vm, (void **)&env, JNI_VERSION_1_6) != JNI_OK) return JNI_ERR;
33 jclass local = (*env)->FindClass(env, "net/paperclover/snowbound/Editor");
34 if (!local) return JNI_ERR;
35 editor = (*env)->NewGlobalRef(env, local);
36 (*env)->DeleteLocalRef(env, local);
37 woken_method = (*env)->GetStaticMethodID(env, editor, "woken", "()V");
38 local = (*env)->FindClass(env, "net/paperclover/snowbound/Provider");
39 if (!woken_method || !local) return JNI_ERR;
40 provider = (*env)->NewGlobalRef(env, local);
41 (*env)->DeleteLocalRef(env, local);
42 document_method = (*env)->GetStaticMethodID(env, provider, "document", "(Ljava/lang/String;JJ)I");
43 children_method = (*env)->GetStaticMethodID(env, provider, "children", "(Ljava/lang/String;JJ)I");
44 open_method = (*env)->GetStaticMethodID(env, provider, "open", "(Ljava/lang/String;Ljava/lang/String;[I[J)I");
45 close_method = (*env)->GetStaticMethodID(env, provider, "close", "(J)I");
46 rename_method = (*env)->GetStaticMethodID(env, provider, "rename", "(Ljava/lang/String;Ljava/lang/String;JJ)I");
47 if (!document_method || !children_method || !open_method || !close_method || !rename_method) return JNI_ERR;
48 sb_set_provider(&documents);
49 int ends[2];
50 pthread_t reader;
51 if (!pipe(ends) && dup2(ends[1], STDERR_FILENO) >= 0 &&
52 !pthread_create(&reader, NULL, log_stderr, (void *)(intptr_t)ends[0]))
53 pthread_detach(reader);
54 return JNI_VERSION_1_6;
55}
56
57// JNI's own string calls speak modified UTF-8, which differs from Rust's for NUL and
58// characters beyond the BMP.
59static jstring java_text(JNIEnv *env, const char *text) {
60 jsize size = (jsize)strlen(text);
61 jbyteArray bytes = (*env)->NewByteArray(env, size);
62 if (!bytes) return NULL;
63 (*env)->SetByteArrayRegion(env, bytes, 0, size, (const jbyte *)text);
64 jclass string = (*env)->FindClass(env, "java/lang/String");
65 jmethodID decode = (*env)->GetMethodID(env, string, "<init>", "([BLjava/lang/String;)V");
66 jstring encoding = (*env)->NewStringUTF(env, "UTF-8");
67 jstring result = (*env)->NewObject(env, string, decode, bytes, encoding);
68 (*env)->DeleteLocalRef(env, bytes);
69 (*env)->DeleteLocalRef(env, encoding);
70 (*env)->DeleteLocalRef(env, string);
71 return result;
72}
73
74// Throws IllegalStateException with `error`, which Rust allocated, or `fallback` without one.
75static void fail(JNIEnv *env, char *error, const char *fallback) {
76 jstring message = java_text(env, error ? error : fallback);
77 if (error) sb_string_free(error);
78 if (!message) return;
79 jclass type = (*env)->FindClass(env, "java/lang/IllegalStateException");
80 jmethodID make = (*env)->GetMethodID(env, type, "<init>", "(Ljava/lang/String;)V");
81 jthrowable exception = (*env)->NewObject(env, type, make, message);
82 if (exception) (*env)->Throw(env, exception);
83 (*env)->DeleteLocalRef(env, type);
84 (*env)->DeleteLocalRef(env, message);
85}
86
87// A Java string as NUL-terminated UTF-8 to free, or null, throwing, if it holds a NUL or an
88// unpaired surrogate.
89static char *utf8(JNIEnv *env, jstring text) {
90 jsize count = (*env)->GetStringLength(env, text);
91 const jchar *units = (*env)->GetStringChars(env, text, NULL);
92 if (!units) return NULL;
93 char *result = malloc((size_t)count * 3 + 1);
94 size_t used = 0;
95 bool valid = result != NULL;
96 for (jsize i = 0; valid && i < count; i++) {
97 uint32_t c = units[i];
98 if (c >= 0xd800 && c <= 0xdbff && i + 1 < count && units[i + 1] >= 0xdc00 && units[i + 1] <= 0xdfff)
99 c = 0x10000 + ((c - 0xd800) << 10) + (units[++i] - 0xdc00);
100 else if (!c || (c >= 0xd800 && c <= 0xdfff)) {
101 valid = false;
102 break;
103 }
104 if (c < 0x80) {
105 result[used++] = (char)c;
106 } else if (c < 0x800) {
107 result[used++] = (char)(0xc0 | c >> 6);
108 result[used++] = (char)(0x80 | (c & 63));
109 } else if (c < 0x10000) {
110 result[used++] = (char)(0xe0 | c >> 12);
111 result[used++] = (char)(0x80 | (c >> 6 & 63));
112 result[used++] = (char)(0x80 | (c & 63));
113 } else {
114 result[used++] = (char)(0xf0 | c >> 18);
115 result[used++] = (char)(0x80 | (c >> 12 & 63));
116 result[used++] = (char)(0x80 | (c >> 6 & 63));
117 result[used++] = (char)(0x80 | (c & 63));
118 }
119 }
120 (*env)->ReleaseStringChars(env, text, units);
121 if (valid) {
122 result[used] = 0;
123 return result;
124 }
125 free(result);
126 fail(env, NULL, "Text holds a NUL or an unpaired surrogate");
127 return NULL;
128}
129
130// Rust's string as Java's, freeing it.
131static jstring output(JNIEnv *env, char *text) {
132 if (!text) {
133 fail(env, NULL, "Snowbound returned nothing");
134 return NULL;
135 }
136 jstring result = java_text(env, text);
137 sb_string_free(text);
138 return result;
139}
140
141// The JVM for a thread Rust may have started; `attached` says to detach once done.
142static JNIEnv *jvm(bool *attached) {
143 JNIEnv *env;
144 *attached = (*machine)->GetEnv(machine, (void **)&env, JNI_VERSION_1_6) == JNI_EDETACHED;
145 return *attached && (*machine)->AttachCurrentThread(machine, &env, NULL) != JNI_OK ? NULL : env;
146}
147
148static void woken(uintptr_t token) {
149 (void)token;
150 bool attached;
151 JNIEnv *env = jvm(&attached);
152 if (!env) return;
153 (*env)->CallStaticVoidMethod(env, editor, woken_method);
154 if ((*env)->ExceptionCheck(env)) (*env)->ExceptionDescribe(env);
155 if (attached) (*machine)->DetachCurrentThread(machine);
156}
157
158// sb_provider, through Provider.java's methods of the same names; 4 where Java throws.
159static int32_t provided(JNIEnv *env, bool attached, jint status) {
160 if ((*env)->ExceptionCheck(env)) {
161 (*env)->ExceptionDescribe(env);
162 status = 4;
163 }
164 if (attached) (*machine)->DetachCurrentThread(machine);
165 return status;
166}
167
168static int32_t listed(jmethodID method, const char *id, sb_document_found found, void *context) {
169 bool attached;
170 JNIEnv *env = jvm(&attached);
171 if (!env) return 4;
172 jstring i = java_text(env, id);
173 jint status = i ? (*env)->CallStaticIntMethod(env, provider, method, i, (jlong)(intptr_t)found,
174 (jlong)(intptr_t)context) : 4;
175 if (i) (*env)->DeleteLocalRef(env, i);
176 return provided(env, attached, status);
177}
178
179static int32_t provider_document(const char *id, sb_document_found found, void *context) {
180 return listed(document_method, id, found, context);
181}
182
183static int32_t provider_children(const char *id, sb_document_found found, void *context) {
184 return listed(children_method, id, found, context);
185}
186
187static int32_t provider_open(const char *id, const char *mode, int32_t *fd, uint64_t *handle) {
188 bool attached;
189 JNIEnv *env = jvm(&attached);
190 if (!env) return 4;
191 jstring i = java_text(env, id), m = i ? java_text(env, mode) : NULL;
192 jintArray fds = m ? (*env)->NewIntArray(env, 1) : NULL;
193 jlongArray handles = fds ? (*env)->NewLongArray(env, 1) : NULL;
194 jint status = handles ? (*env)->CallStaticIntMethod(env, provider, open_method, i, m, fds, handles) : 4;
195 if (!status && !(*env)->ExceptionCheck(env)) {
196 jint f;
197 jlong h;
198 (*env)->GetIntArrayRegion(env, fds, 0, 1, &f);
199 (*env)->GetLongArrayRegion(env, handles, 0, 1, &h);
200 *fd = f;
201 *handle = (uint64_t)h;
202 }
203 if (i) (*env)->DeleteLocalRef(env, i);
204 if (m) (*env)->DeleteLocalRef(env, m);
205 if (fds) (*env)->DeleteLocalRef(env, fds);
206 if (handles) (*env)->DeleteLocalRef(env, handles);
207 return provided(env, attached, status);
208}
209
210static int32_t provider_close(uint64_t handle) {
211 bool attached;
212 JNIEnv *env = jvm(&attached);
213 if (!env) return 4;
214 return provided(env, attached, (*env)->CallStaticIntMethod(env, provider, close_method, (jlong)handle));
215}
216
217static int32_t provider_rename(const char *id, const char *name, void (*renamed)(void *, const char *), void *context) {
218 bool attached;
219 JNIEnv *env = jvm(&attached);
220 if (!env) return 4;
221 jstring i = java_text(env, id), n = i ? java_text(env, name) : NULL;
222 jint status = n ? (*env)->CallStaticIntMethod(env, provider, rename_method, i, n, (jlong)(intptr_t)renamed,
223 (jlong)(intptr_t)context) : 4;
224 if (i) (*env)->DeleteLocalRef(env, i);
225 if (n) (*env)->DeleteLocalRef(env, n);
226 return provided(env, attached, status);
227}
228
229static const sb_provider documents = {provider_document, provider_children, provider_open, provider_close,
230 provider_rename};
231
232#define PROVIDER(name) Java_net_paperclover_snowbound_Provider_##name
233
234JNIEXPORT void JNICALL PROVIDER(found)(JNIEnv *env, jclass cls, jlong found, jlong context, jstring id, jstring name,
235 jboolean directory, jlong size, jlong modified, jboolean writable) {
236 (void)cls;
237 char *i = utf8(env, id), *n = i ? utf8(env, name) : NULL;
238 if (n) ((sb_document_found)(intptr_t)found)((void *)(intptr_t)context, i, n, directory, size, modified, writable);
239 free(i), free(n);
240}
241
242JNIEXPORT void JNICALL PROVIDER(renamed)(JNIEnv *env, jclass cls, jlong renamed, jlong context, jstring id) {
243 (void)cls;
244 char *i = utf8(env, id);
245 if (i) ((void (*)(void *, const char *))(intptr_t)renamed)((void *)(intptr_t)context, i);
246 free(i);
247}
248
249JNIEXPORT jlong JNICALL JNI(provided)(JNIEnv *env, jclass cls, jstring root, jboolean folder, jboolean local,
250 jstring device, jstring cache) {
251 (void)cls;
252 char *r = utf8(env, root), *d = r ? utf8(env, device) : NULL, *c = d ? utf8(env, cache) : NULL;
253 char *error = NULL;
254 const Library *library = c ? sb_library_provider(r, folder, local, d, c, &error) : NULL;
255 if (c && !library) fail(env, error, "The notebook can't be opened");
256 free(r), free(d), free(c);
257 return (jlong)(intptr_t)library;
258}
259
260JNIEXPORT jstring JNICALL JNI(syncStatus)(JNIEnv *env, jclass cls, jlong library) {
261 (void)cls;
262 return output(env, sb_library_sync_status((const Library *)(intptr_t)library));
263}
264
265JNIEXPORT void JNICALL JNI(create)(JNIEnv *env, jclass cls, jstring notebook, jstring cache, jstring date, jstring time) {
266 (void)cls;
267 char *n = utf8(env, notebook), *c = n ? utf8(env, cache) : NULL, *d = c ? utf8(env, date) : NULL,
268 *t = d ? utf8(env, time) : NULL, *error = NULL;
269 if (t && !sb_notebook_create(n, c, "", d, t, &error)) fail(env, error, "The notebook can't be created");
270 free(n), free(c), free(d), free(t);
271}
272
273JNIEXPORT jlong JNICALL JNI(library)(JNIEnv *env, jclass cls, jstring notebook, jstring cache) {
274 (void)cls;
275 char *n = utf8(env, notebook), *c = n ? utf8(env, cache) : NULL, *error = NULL;
276 const Library *library = c ? sb_library_open(n, c, true, &error) : NULL;
277 if (c && !library) fail(env, error, "The notebook can't be opened");
278 free(n), free(c);
279 return (jlong)(intptr_t)library;
280}
281
282JNIEXPORT jstring JNICALL JNI(sections)(JNIEnv *env, jclass cls, jlong library) {
283 (void)cls;
284 return output(env, sb_library_sections((const Library *)(intptr_t)library));
285}
286
287JNIEXPORT jlong JNICALL JNI(section)(JNIEnv *env, jclass cls, jlong library, jstring path) {
288 (void)cls;
289 char *p = utf8(env, path), *error = NULL;
290 Section *section = p ? sb_section_open((const Library *)(intptr_t)library, p, "", woken, 0, &error) : NULL;
291 if (p && !section) fail(env, error, "The section can't be opened");
292 free(p);
293 return (jlong)(intptr_t)section;
294}
295
296JNIEXPORT jstring JNICALL JNI(pages)(JNIEnv *env, jclass cls, jlong section) {
297 (void)cls;
298 return output(env, sb_section_pages((const Section *)(intptr_t)section));
299}
300
301JNIEXPORT jlong JNICALL JNI(view)(JNIEnv *env, jclass cls, jobject surface, jlong section, jstring page,
302 jfloat width, jfloat height, jfloat scale) {
303 (void)cls;
304 char *id = utf8(env, page);
305 ANativeWindow *window = id ? ANativeWindow_fromSurface(env, surface) : NULL;
306 View *view = window ? sb_view_new_android(window, (const Section *)(intptr_t)section, id, width, height, scale) : NULL;
307 if (window) ANativeWindow_release(window);
308 if (id && !view) fail(env, NULL, "The page can't be drawn");
309 free(id);
310 return (jlong)(intptr_t)view;
311}
312
313JNIEXPORT void JNICALL JNI(attach)(JNIEnv *env, jclass cls, jlong view, jobject surface) {
314 (void)cls;
315 ANativeWindow *window = ANativeWindow_fromSurface(env, surface);
316 bool attached = window && sb_view_attach_android((View *)(intptr_t)view, window);
317 if (window) ANativeWindow_release(window);
318 if (!attached) fail(env, NULL, "The page can't be drawn");
319}
320
321JNIEXPORT void JNICALL JNI(detach)(JNIEnv *env, jclass cls, jlong view) {
322 (void)env, (void)cls;
323 sb_view_detach_android((View *)(intptr_t)view);
324}
325
326JNIEXPORT void JNICALL JNI(resize)(JNIEnv *env, jclass cls, jlong view, jfloat width, jfloat height, jfloat scale) {
327 (void)env, (void)cls;
328 sb_view_resize((View *)(intptr_t)view, width, height, scale);
329}
330
331JNIEXPORT void JNICALL JNI(render)(JNIEnv *env, jclass cls, jlong view) {
332 (void)env, (void)cls;
333 sb_view_render((View *)(intptr_t)view);
334}
335
336JNIEXPORT jboolean JNICALL JNI(framePending)(JNIEnv *env, jclass cls, jlong view) {
337 (void)env, (void)cls;
338 return sb_view_frame_pending((const View *)(intptr_t)view);
339}
340
341JNIEXPORT void JNICALL JNI(focus)(JNIEnv *env, jclass cls, jlong view, jboolean focused) {
342 (void)env, (void)cls;
343 sb_view_focus((View *)(intptr_t)view, focused);
344}
345
346JNIEXPORT void JNICALL JNI(press)(JNIEnv *env, jclass cls, jlong view, jfloat x, jfloat y) {
347 (void)env, (void)cls;
348 sb_view_press((View *)(intptr_t)view, x, y);
349}
350
351JNIEXPORT void JNICALL JNI(drag)(JNIEnv *env, jclass cls, jlong view, jfloat x, jfloat y) {
352 (void)env, (void)cls;
353 sb_view_drag((View *)(intptr_t)view, x, y);
354}
355
356JNIEXPORT void JNICALL JNI(release)(JNIEnv *env, jclass cls, jlong view) {
357 (void)env, (void)cls;
358 sb_view_release((View *)(intptr_t)view);
359}
360
361JNIEXPORT jint JNICALL JNI(length)(JNIEnv *env, jclass cls, jlong view) {
362 (void)env, (void)cls;
363 return (jint)sb_text_length((const View *)(intptr_t)view);
364}
365
366JNIEXPORT jstring JNICALL JNI(text)(JNIEnv *env, jclass cls, jlong view, jint start, jint end) {
367 (void)cls;
368 return output(env, sb_text((const View *)(intptr_t)view, (uint32_t)start, (uint32_t)end));
369}
370
371static jintArray range(JNIEnv *env, const uint32_t range[2]) {
372 jint values[2] = {(jint)range[0], (jint)range[1]};
373 jintArray result = (*env)->NewIntArray(env, 2);
374 if (result) (*env)->SetIntArrayRegion(env, result, 0, 2, values);
375 return result;
376}
377
378JNIEXPORT jintArray JNICALL JNI(selection)(JNIEnv *env, jclass cls, jlong view) {
379 (void)cls;
380 uint32_t selected[2] = {0, 0};
381 sb_selection((const View *)(intptr_t)view, selected);
382 return range(env, selected);
383}
384
385JNIEXPORT jintArray JNICALL JNI(marked)(JNIEnv *env, jclass cls, jlong view) {
386 (void)cls;
387 uint32_t marked[2];
388 return sb_marked((const View *)(intptr_t)view, marked) ? range(env, marked) : NULL;
389}
390
391JNIEXPORT jboolean JNICALL JNI(select)(JNIEnv *env, jclass cls, jlong view, jint start, jint end) {
392 (void)env, (void)cls;
393 return sb_select((View *)(intptr_t)view, (uint32_t)start, (uint32_t)end);
394}
395
396JNIEXPORT jboolean JNICALL JNI(insert)(JNIEnv *env, jclass cls, jlong view, jstring text) {
397 (void)cls;
398 char *t = utf8(env, text);
399 bool changed = t && sb_insert((View *)(intptr_t)view, t);
400 free(t);
401 return changed;
402}
403
404JNIEXPORT jboolean JNICALL JNI(setMarked)(JNIEnv *env, jclass cls, jlong view, jstring text, jint start, jint end) {
405 (void)cls;
406 char *t = utf8(env, text);
407 bool changed = t && sb_set_marked((View *)(intptr_t)view, t, (uint32_t)start, (uint32_t)end);
408 free(t);
409 return changed;
410}
411
412JNIEXPORT void JNICALL JNI(unmark)(JNIEnv *env, jclass cls, jlong view) {
413 (void)env, (void)cls;
414 sb_unmark((View *)(intptr_t)view);
415}
416
417JNIEXPORT jboolean JNICALL JNI(deleteBackward)(JNIEnv *env, jclass cls, jlong view) {
418 (void)env, (void)cls;
419 return sb_delete_backward((View *)(intptr_t)view);
420}
421
422// The caret at `start` when `end` is the same, else the rectangles covering the range: each
423// `x, y, width, height` in the view's points.
424JNIEXPORT jfloatArray JNICALL JNI(rects)(JNIEnv *env, jclass cls, jlong view, jint start, jint end) {
425 (void)cls;
426 const View *v = (const View *)(intptr_t)view;
427 float caret[1][4];
428 size_t count = start == end ? sb_caret_rect(v, (uint32_t)start, caret[0])
429 : sb_range_rects(v, (uint32_t)start, (uint32_t)end, NULL, 0);
430 float (*rects)[4] = start == end ? caret : malloc(sizeof(*rects) * (count ? count : 1));
431 if (!rects) return NULL;
432 if (start != end) {
433 size_t written = sb_range_rects(v, (uint32_t)start, (uint32_t)end, rects, count);
434 if (written < count) count = written;
435 }
436 jfloatArray result = (*env)->NewFloatArray(env, (jsize)(count * 4));
437 if (result) (*env)->SetFloatArrayRegion(env, result, 0, (jsize)(count * 4), (const jfloat *)rects);
438 if (rects != caret) free(rects);
439 return result;
440}
441
442JNIEXPORT jint JNICALL JNI(closest)(JNIEnv *env, jclass cls, jlong view, jfloat x, jfloat y) {
443 (void)env, (void)cls;
444 return (jint)sb_closest((const View *)(intptr_t)view, x, y);
445}
446
447// The section's events, then its saving status.
448JNIEXPORT jintArray JNICALL JNI(poll)(JNIEnv *env, jclass cls, jlong section) {
449 (void)cls;
450 uint8_t status = 0;
451 uint32_t polled[2] = {sb_section_poll((const Section *)(intptr_t)section, &status), 0};
452 polled[1] = status;
453 return range(env, polled);
454}
455
456JNIEXPORT jboolean JNICALL JNI(flush)(JNIEnv *env, jclass cls, jlong section, jdouble seconds) {
457 (void)env, (void)cls;
458 return sb_section_flush((const Section *)(intptr_t)section, seconds);
459}
460
461JNIEXPORT jboolean JNICALL JNI(keep)(JNIEnv *env, jclass cls, jlong view) {
462 (void)env, (void)cls;
463 return sb_view_keep((const View *)(intptr_t)view);
464}
465
466JNIEXPORT jboolean JNICALL JNI(reloadReady)(JNIEnv *env, jclass cls, jlong view) {
467 (void)env, (void)cls;
468 return sb_view_reload_ready((const View *)(intptr_t)view);
469}
470
471JNIEXPORT void JNICALL JNI(reload)(JNIEnv *env, jclass cls, jlong view, jboolean discard) {
472 (void)env, (void)cls;
473 sb_view_reload((View *)(intptr_t)view, discard, NULL);
474}
475
476JNIEXPORT void JNICALL JNI(freeView)(JNIEnv *env, jclass cls, jlong view) {
477 (void)env, (void)cls;
478 sb_view_free((View *)(intptr_t)view);
479}
480
481// Publishes the notebook's edits, the open section's included, then frees the section and lets go
482// of the notebook, keeping its offline copies: false, the edits kept to publish when it next
483// opens, when they don't publish within `seconds`. Finishing the activity is not Close Notebook
484// (sb_library_close).
485JNIEXPORT jboolean JNICALL JNI(close)(JNIEnv *env, jclass cls, jlong library, jlong section, jdouble seconds) {
486 (void)env, (void)cls;
487 char *error = NULL;
488 bool closed = sb_library_flush((const Library *)(intptr_t)library, seconds, &error);
489 if (section) sb_section_free((Section *)(intptr_t)section);
490 if (error) {
491 fprintf(stderr, "snowbound: %s\n", error);
492 sb_string_free(error);
493 }
494 sb_library_free((const Library *)(intptr_t)library);
495 return closed;
496}