authorclover caruso <git@paperclover.net> 2026-10-15 05:27:39-07:00
committerclover caruso <git@paperclover.net> 2026-10-15 06:30:07-07:00
log10cde9f3e120e69e22c88fe33ecfe264bd350fb0
tree72f2f26514f499d2c000ccf72273463e25a74081
parentee54ecdfad5603988e09516439764799c94202e3
signature Signed by SSH key SHA256:52mNGHRsVFBDED9IAX5pe+LRWUefqTbxEReunq21QvU (~clover)

feat(android): an APK that draws a page

apps/android is the Android host: a Java activity around a SurfaceView, and C that hands crates/mobile its ANativeWindow. On first launch it creates a notebook in app storage, its replicas in the no-backup folder, and draws New Section 1's page. The editor outlives each activity, so rotation and Home detach and reattach the same page. Touches reach the page; typing, saving on stop and section events come next. build.py makes an ARM64 APK for Android 8.0 and later without Gradle, signed with a debug key kept beside it, its library aligned for 16 KiB pages. It builds the Rust half in release, as wgpu names GPU objects in a debug build and the emulator's Vulkan driver crashes on that. Refs #24 Assisted-by: claude-opus-5.5

7 files changed, 549 insertions(+), 0 deletions(-)

apps/android/AndroidManifest.xml created+12
......@@ -0,0 +1,12 @@
1<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="net.paperclover.snowbound"
2 android:versionCode="1" android:versionName="0.1">
3 <uses-sdk android:minSdkVersion="26" android:targetSdkVersion="36" />
4 <application android:label="Snowbound" android:theme="@style/Page" android:extractNativeLibs="false">
5 <activity android:name=".PageActivity" android:exported="true">
6 <intent-filter>
7 <action android:name="android.intent.action.MAIN" />
8 <category android:name="android.intent.category.LAUNCHER" />
9 </intent-filter>
10 </activity>
11 </application>
12</manifest>
apps/android/build.py created+99
......@@ -0,0 +1,99 @@
1#!/usr/bin/env python3
2"""Builds Snowbound's ARM64 APK, signed with a debug key kept in the output folder.
3
4 python3 apps/android/build.py --sdk SDK [--sdk SDK...] --ndk NDK --java-home JDK --output DIR
5
6Each --sdk is searched for build-tools and the platform. Rust comes from the rustup on PATH,
7which needs the aarch64-linux-android target (RUSTUP_HOME can name another rustup's home).
8"""
9import argparse
10import os
11from pathlib import Path
12import shutil
13import subprocess
14import sys
15import zipfile
16
17ROOT = Path(__file__).resolve().parents[2]
18sys.path.insert(0, str(ROOT / 'tools'))
19from cargo_fresh import clean_local # noqa: E402
20
21API = 26
22TARGET = 'aarch64-linux-android'
23
24
25def run(*command, **options):
26 subprocess.run([str(part) for part in command], check=True, **options)
27
28
29def main():
30 parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
31 parser.add_argument('--sdk', type=Path, action='append', required=True)
32 parser.add_argument('--ndk', type=Path, required=True)
33 parser.add_argument('--java-home', type=Path, required=True)
34 parser.add_argument('--output', type=Path, required=True)
35 args = parser.parse_args()
36
37 def sdk(path):
38 found = [root / path for root in args.sdk if (root / path).exists()]
39 if not found:
40 sys.exit(f'No --sdk has {path}')
41 return found[0].resolve()
42
43 tools = sdk(Path('build-tools/36.0.0'))
44 android = sdk(Path('platforms/android-36/android.jar'))
45 java = args.java_home.resolve() / 'bin'
46 ndk = args.ndk.resolve()
47 llvm = ndk / 'toolchains/llvm/prebuilt' / ('darwin-x86_64' if sys.platform == 'darwin' else 'linux-x86_64') / 'bin'
48 output = args.output.resolve()
49 output.mkdir(parents=True, exist_ok=True)
50 source = Path(__file__).resolve().parent
51
52 environment = dict(os.environ, ANDROID_NDK_HOME=str(ndk))
53 environment.setdefault('CARGO_TARGET_DIR', str(ROOT / 'target/android'))
54 # A debug build has wgpu name GPU objects for debuggers, which crashes the emulator's Vulkan driver.
55 clean_local(ROOT, '--release', '--target', TARGET, environment=environment)
56 run('sh', ROOT / 'platform/android/cargo.sh', API, 'build', '--release', '-p', 'mobile', env=environment)
57 library = output / 'libsnowbound.so'
58 run(llvm / f'aarch64-linux-android{API}-clang', '-std=c11', '-Wall', '-Wextra', '-Werror', '-O2', '-fPIC',
59 '-shared', '-Wl,--no-undefined', '-Wl,--exclude-libs,ALL', '-Wl,--gc-sections', '-Wl,--strip-debug',
60 '-Wl,-z,max-page-size=16384', '-Wl,-soname,libsnowbound.so', '-I', ROOT / 'crates/mobile/include', source / 'jni/snowbound.c',
61 Path(environment['CARGO_TARGET_DIR']) / TARGET / 'release/libmobile.a',
62 '-landroid', '-llog', '-ldl', '-lm', '-o', library)
63 # Android 15 devices with 16 KiB pages load only libraries aligned to them.
64 segments = subprocess.run([llvm / 'llvm-readelf', '-lW', library], check=True, capture_output=True,
65 text=True).stdout
66 if any(int(line.split()[-1], 16) < 0x4000 for line in segments.splitlines() if line.strip().startswith('LOAD')):
67 sys.exit(f'{library} has a LOAD segment aligned below 16 KiB')
68
69 classes = output / 'classes'
70 shutil.rmtree(classes, ignore_errors=True)
71 run(java / 'javac', '-Xlint:-options', '--release', '8', '-classpath', android, '-d', classes,
72 *sorted((source / 'src').rglob('*.java')))
73 run(java / 'java', '-cp', tools / 'lib/d8.jar', 'com.android.tools.r8.D8', '--min-api', API,
74 '--lib', android, '--output', output, *sorted(classes.rglob('*.class')))
75 resources = output / 'resources.zip'
76 run(tools / 'aapt2', 'compile', '--dir', source / 'res', '-o', resources)
77 unsigned = output / 'unsigned.apk'
78 run(tools / 'aapt2', 'link', '--debug-mode', '-I', android, '--manifest', source / 'AndroidManifest.xml',
79 '-o', unsigned, resources)
80 with zipfile.ZipFile(unsigned, 'a') as apk:
81 apk.write(output / 'classes.dex', 'classes.dex', zipfile.ZIP_DEFLATED)
82 # Android maps the library from the APK, so it stays uncompressed.
83 apk.write(library, 'lib/arm64-v8a/libsnowbound.so', zipfile.ZIP_STORED)
84 aligned = output / 'aligned.apk'
85 run(tools / 'zipalign', '-f', '-P', '16', '4', unsigned, aligned)
86 keystore = output / 'debug.keystore'
87 if not keystore.exists():
88 run(java / 'keytool', '-genkeypair', '-keystore', keystore, '-storepass', 'android', '-alias', 'debug',
89 '-keyalg', 'RSA', '-validity', '10000', '-dname', 'CN=Snowbound Debug')
90 apk = output / 'Snowbound.apk'
91 run(java / 'java', '-jar', tools / 'lib/apksigner.jar', 'sign', '--ks', keystore, '--ks-pass', 'pass:android',
92 '--out', apk, aligned)
93 run(java / 'java', '-jar', tools / 'lib/apksigner.jar', 'verify', apk)
94 run(tools / 'zipalign', '-c', '-P', '16', '4', apk)
95 print(apk)
96
97
98if __name__ == '__main__':
99 main()
apps/android/jni/snowbound.c created+214
......@@ -0,0 +1,214 @@
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
23JNIEXPORT jint JNI_OnLoad(JavaVM *vm, void *reserved) {
24 (void)vm; (void)reserved;
25 int ends[2];
26 pthread_t reader;
27 if (!pipe(ends) && dup2(ends[1], STDERR_FILENO) >= 0 &&
28 !pthread_create(&reader, NULL, log_stderr, (void *)(intptr_t)ends[0]))
29 pthread_detach(reader);
30 return JNI_VERSION_1_6;
31}
32
33// JNI's own string calls speak modified UTF-8, which differs from Rust's for NUL and
34// characters beyond the BMP.
35static jstring java_text(JNIEnv *env, const char *text) {
36 jsize size = (jsize)strlen(text);
37 jbyteArray bytes = (*env)->NewByteArray(env, size);
38 if (!bytes) return NULL;
39 (*env)->SetByteArrayRegion(env, bytes, 0, size, (const jbyte *)text);
40 jclass string = (*env)->FindClass(env, "java/lang/String");
41 jmethodID decode = (*env)->GetMethodID(env, string, "<init>", "([BLjava/lang/String;)V");
42 jstring encoding = (*env)->NewStringUTF(env, "UTF-8");
43 jstring result = (*env)->NewObject(env, string, decode, bytes, encoding);
44 (*env)->DeleteLocalRef(env, bytes);
45 (*env)->DeleteLocalRef(env, encoding);
46 (*env)->DeleteLocalRef(env, string);
47 return result;
48}
49
50// Throws IllegalStateException with `error`, which Rust allocated, or `fallback` without one.
51static void fail(JNIEnv *env, char *error, const char *fallback) {
52 jstring message = java_text(env, error ? error : fallback);
53 if (error) sb_string_free(error);
54 if (!message) return;
55 jclass type = (*env)->FindClass(env, "java/lang/IllegalStateException");
56 jmethodID make = (*env)->GetMethodID(env, type, "<init>", "(Ljava/lang/String;)V");
57 jthrowable exception = (*env)->NewObject(env, type, make, message);
58 if (exception) (*env)->Throw(env, exception);
59 (*env)->DeleteLocalRef(env, type);
60 (*env)->DeleteLocalRef(env, message);
61}
62
63// A Java string as NUL-terminated UTF-8 to free, or null, throwing, if it holds a NUL or an
64// unpaired surrogate.
65static char *utf8(JNIEnv *env, jstring text) {
66 jsize count = (*env)->GetStringLength(env, text);
67 const jchar *units = (*env)->GetStringChars(env, text, NULL);
68 if (!units) return NULL;
69 char *result = malloc((size_t)count * 3 + 1);
70 size_t used = 0;
71 bool valid = result != NULL;
72 for (jsize i = 0; valid && i < count; i++) {
73 uint32_t c = units[i];
74 if (c >= 0xd800 && c <= 0xdbff && i + 1 < count && units[i + 1] >= 0xdc00 && units[i + 1] <= 0xdfff)
75 c = 0x10000 + ((c - 0xd800) << 10) + (units[++i] - 0xdc00);
76 else if (!c || (c >= 0xd800 && c <= 0xdfff)) {
77 valid = false;
78 break;
79 }
80 if (c < 0x80) {
81 result[used++] = (char)c;
82 } else if (c < 0x800) {
83 result[used++] = (char)(0xc0 | c >> 6);
84 result[used++] = (char)(0x80 | (c & 63));
85 } else if (c < 0x10000) {
86 result[used++] = (char)(0xe0 | c >> 12);
87 result[used++] = (char)(0x80 | (c >> 6 & 63));
88 result[used++] = (char)(0x80 | (c & 63));
89 } else {
90 result[used++] = (char)(0xf0 | c >> 18);
91 result[used++] = (char)(0x80 | (c >> 12 & 63));
92 result[used++] = (char)(0x80 | (c >> 6 & 63));
93 result[used++] = (char)(0x80 | (c & 63));
94 }
95 }
96 (*env)->ReleaseStringChars(env, text, units);
97 if (valid) {
98 result[used] = 0;
99 return result;
100 }
101 free(result);
102 fail(env, NULL, "Text holds a NUL or an unpaired surrogate");
103 return NULL;
104}
105
106// Rust's string as Java's, freeing it.
107static jstring output(JNIEnv *env, char *text) {
108 if (!text) {
109 fail(env, NULL, "Snowbound returned nothing");
110 return NULL;
111 }
112 jstring result = java_text(env, text);
113 sb_string_free(text);
114 return result;
115}
116
117// Nothing reads the section's events yet; the page shows only its own edits.
118static void woken(uintptr_t token) { (void)token; }
119
120JNIEXPORT void JNICALL JNI(create)(JNIEnv *env, jclass cls, jstring notebook, jstring cache, jstring date, jstring time) {
121 (void)cls;
122 char *n = utf8(env, notebook), *c = n ? utf8(env, cache) : NULL, *d = c ? utf8(env, date) : NULL,
123 *t = d ? utf8(env, time) : NULL, *error = NULL;
124 if (t && !sb_notebook_create(n, c, "", d, t, &error)) fail(env, error, "The notebook can't be created");
125 free(n), free(c), free(d), free(t);
126}
127
128JNIEXPORT jlong JNICALL JNI(library)(JNIEnv *env, jclass cls, jstring notebook, jstring cache) {
129 (void)cls;
130 char *n = utf8(env, notebook), *c = n ? utf8(env, cache) : NULL, *error = NULL;
131 const Library *library = c ? sb_library_open(n, c, true, &error) : NULL;
132 if (c && !library) fail(env, error, "The notebook can't be opened");
133 free(n), free(c);
134 return (jlong)(intptr_t)library;
135}
136
137JNIEXPORT jstring JNICALL JNI(sections)(JNIEnv *env, jclass cls, jlong library) {
138 (void)cls;
139 return output(env, sb_library_sections((const Library *)(intptr_t)library));
140}
141
142JNIEXPORT jlong JNICALL JNI(section)(JNIEnv *env, jclass cls, jlong library, jstring path) {
143 (void)cls;
144 char *p = utf8(env, path), *error = NULL;
145 Section *section = p ? sb_section_open((const Library *)(intptr_t)library, p, "", woken, 0, &error) : NULL;
146 if (p && !section) fail(env, error, "The section can't be opened");
147 free(p);
148 return (jlong)(intptr_t)section;
149}
150
151JNIEXPORT jstring JNICALL JNI(pages)(JNIEnv *env, jclass cls, jlong section) {
152 (void)cls;
153 return output(env, sb_section_pages((const Section *)(intptr_t)section));
154}
155
156JNIEXPORT jlong JNICALL JNI(view)(JNIEnv *env, jclass cls, jobject surface, jlong section, jstring page,
157 jfloat width, jfloat height, jfloat scale) {
158 (void)cls;
159 char *id = utf8(env, page);
160 ANativeWindow *window = id ? ANativeWindow_fromSurface(env, surface) : NULL;
161 View *view = window ? sb_view_new_android(window, (const Section *)(intptr_t)section, id, width, height, scale) : NULL;
162 if (window) ANativeWindow_release(window);
163 if (id && !view) fail(env, NULL, "The page can't be drawn");
164 free(id);
165 return (jlong)(intptr_t)view;
166}
167
168JNIEXPORT void JNICALL JNI(attach)(JNIEnv *env, jclass cls, jlong view, jobject surface) {
169 (void)cls;
170 ANativeWindow *window = ANativeWindow_fromSurface(env, surface);
171 bool attached = window && sb_view_attach_android((View *)(intptr_t)view, window);
172 if (window) ANativeWindow_release(window);
173 if (!attached) fail(env, NULL, "The page can't be drawn");
174}
175
176JNIEXPORT void JNICALL JNI(detach)(JNIEnv *env, jclass cls, jlong view) {
177 (void)env, (void)cls;
178 sb_view_detach_android((View *)(intptr_t)view);
179}
180
181JNIEXPORT void JNICALL JNI(resize)(JNIEnv *env, jclass cls, jlong view, jfloat width, jfloat height, jfloat scale) {
182 (void)env, (void)cls;
183 sb_view_resize((View *)(intptr_t)view, width, height, scale);
184}
185
186JNIEXPORT void JNICALL JNI(render)(JNIEnv *env, jclass cls, jlong view) {
187 (void)env, (void)cls;
188 sb_view_render((View *)(intptr_t)view);
189}
190
191JNIEXPORT jboolean JNICALL JNI(framePending)(JNIEnv *env, jclass cls, jlong view) {
192 (void)env, (void)cls;
193 return sb_view_frame_pending((const View *)(intptr_t)view);
194}
195
196JNIEXPORT void JNICALL JNI(focus)(JNIEnv *env, jclass cls, jlong view, jboolean focused) {
197 (void)env, (void)cls;
198 sb_view_focus((View *)(intptr_t)view, focused);
199}
200
201JNIEXPORT void JNICALL JNI(press)(JNIEnv *env, jclass cls, jlong view, jfloat x, jfloat y) {
202 (void)env, (void)cls;
203 sb_view_press((View *)(intptr_t)view, x, y);
204}
205
206JNIEXPORT void JNICALL JNI(drag)(JNIEnv *env, jclass cls, jlong view, jfloat x, jfloat y) {
207 (void)env, (void)cls;
208 sb_view_drag((View *)(intptr_t)view, x, y);
209}
210
211JNIEXPORT void JNICALL JNI(release)(JNIEnv *env, jclass cls, jlong view) {
212 (void)env, (void)cls;
213 sb_view_release((View *)(intptr_t)view);
214}
apps/android/res/values/themes.xml created+6
......@@ -0,0 +1,6 @@
1<resources>
2 <style name="Page" parent="@android:style/Theme.Material.Light.NoActionBar">
3 <item name="android:windowLightStatusBar">true</item>
4 <item name="android:windowLightNavigationBar">true</item>
5 </style>
6</resources>
apps/android/src/net/paperclover/snowbound/Editor.java created+85
......@@ -0,0 +1,85 @@
1package net.paperclover.snowbound;
2
3import android.content.Context;
4import android.os.Handler;
5import android.os.Looper;
6import android.util.Log;
7import android.view.Surface;
8import java.io.File;
9import java.text.DateFormat;
10import java.util.Date;
11import org.json.JSONArray;
12import org.json.JSONException;
13
14/** The page being edited, which outlives each activity showing it; touched only on the main thread. */
15final class Editor {
16 static {
17 System.loadLibrary("snowbound");
18 }
19
20 static long section, view;
21 static String page;
22 /** The view to attach once the page is open. */
23 static PageView shown;
24 private static boolean opening;
25
26 /** Opens the first page of the notebook in app storage, creating the notebook the first time. */
27 static void open(Context context) {
28 if (opening) return;
29 opening = true;
30 File notebooks = new File(context.getFilesDir(), "Notebooks");
31 File notebook = new File(notebooks, "Snowbound");
32 File cache = new File(context.getNoBackupFilesDir(), "Cache");
33 Date now = new Date();
34 String date = DateFormat.getDateInstance(DateFormat.FULL).format(now);
35 String time = DateFormat.getTimeInstance(DateFormat.SHORT).format(now);
36 Handler main = new Handler(Looper.getMainLooper());
37 new Thread(() -> {
38 if (!notebook.exists()) {
39 notebooks.mkdirs();
40 create(notebook.getPath(), cache.getPath(), date, time);
41 }
42 long library = library(notebook.getPath(), cache.getPath());
43 try {
44 long opened = section(library, new JSONArray(sections(library)).getJSONObject(0).getString("path"));
45 String first = new JSONArray(pages(opened)).getJSONObject(0).getString("id");
46 main.post(() -> {
47 section = opened;
48 page = first;
49 if (shown != null) shown.attach();
50 });
51 } catch (JSONException error) {
52 throw new IllegalStateException(error);
53 }
54 }, "Open notebook").start();
55 }
56
57 /**
58 * Draws the page into `surface`. The editor is made once and keeps its selection and Undo
59 * across surfaces; each later one starts at the last size, until SurfaceChanged resizes it.
60 */
61 static void draw(Surface surface, float width, float height, float scale) {
62 if (view != 0) {
63 attach(view, surface);
64 return;
65 }
66 view = view(surface, section, page, width, height, scale);
67 Log.i("snowbound", "Page view created");
68 }
69
70 static native void create(String notebook, String cache, String date, String time);
71 static native long library(String notebook, String cache);
72 static native String sections(long library);
73 static native long section(long library, String path);
74 static native String pages(long section);
75 static native long view(Surface surface, long section, String page, float width, float height, float scale);
76 static native void attach(long view, Surface surface);
77 static native void detach(long view);
78 static native void resize(long view, float width, float height, float scale);
79 static native void render(long view);
80 static native boolean framePending(long view);
81 static native void focus(long view, boolean focused);
82 static native void press(long view, float x, float y);
83 static native void drag(long view, float x, float y);
84 static native void release(long view);
85}
apps/android/src/net/paperclover/snowbound/PageActivity.java created+27
......@@ -0,0 +1,27 @@
1package net.paperclover.snowbound;
2
3import android.app.Activity;
4import android.os.Bundle;
5import android.widget.FrameLayout;
6
7public final class PageActivity extends Activity {
8 private PageView page;
9
10 @Override
11 protected void onCreate(Bundle state) {
12 super.onCreate(state);
13 page = new PageView(this);
14 FrameLayout frame = new FrameLayout(this);
15 frame.setFitsSystemWindows(true);
16 frame.addView(page);
17 setContentView(frame);
18 Editor.shown = page;
19 Editor.open(this);
20 }
21
22 @Override
23 protected void onDestroy() {
24 if (Editor.shown == page) Editor.shown = null;
25 super.onDestroy();
26 }
27}
apps/android/src/net/paperclover/snowbound/PageView.java created+106
......@@ -0,0 +1,106 @@
1package net.paperclover.snowbound;
2
3import android.content.Context;
4import android.graphics.Rect;
5import android.view.Choreographer;
6import android.view.MotionEvent;
7import android.view.SurfaceHolder;
8import android.view.SurfaceView;
9
10/** Draws the editor's page into its surface, a frame whenever the page changes. */
11final class PageView extends SurfaceView implements SurfaceHolder.Callback, Choreographer.FrameCallback {
12 private boolean attached, dirty;
13
14 PageView(Context context) {
15 super(context);
16 setFocusableInTouchMode(true);
17 getHolder().addCallback(this);
18 }
19
20 private float scale() {
21 return getResources().getDisplayMetrics().density;
22 }
23
24 void attach() {
25 if (attached || Editor.page == null || !getHolder().getSurface().isValid() || getWidth() == 0) return;
26 float scale = scale();
27 Editor.draw(getHolder().getSurface(), getWidth() / scale, getHeight() / scale, scale);
28 attached = true;
29 Editor.focus(Editor.view, hasFocus() && hasWindowFocus());
30 dirty = true;
31 Choreographer.getInstance().postFrameCallback(this);
32 }
33
34 @Override
35 public void surfaceCreated(SurfaceHolder holder) {
36 attach();
37 }
38
39 @Override
40 public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
41 attach();
42 if (!attached) return;
43 float scale = scale();
44 Editor.resize(Editor.view, width / scale, height / scale, scale);
45 dirty = true;
46 }
47
48 /** Android destroys the surface once this returns, so the editor lets go of it first. */
49 @Override
50 public void surfaceDestroyed(SurfaceHolder holder) {
51 if (!attached) return;
52 Choreographer.getInstance().removeFrameCallback(this);
53 Editor.detach(Editor.view);
54 attached = false;
55 }
56
57 @Override
58 public void doFrame(long nanos) {
59 Choreographer.getInstance().postFrameCallback(this);
60 if (Editor.framePending(Editor.view)) dirty = true;
61 if (!dirty) return;
62 dirty = false;
63 Editor.render(Editor.view);
64 }
65
66 @Override
67 public boolean onTouchEvent(MotionEvent event) {
68 if (!attached) return false;
69 float scale = scale(), x = event.getX() / scale, y = event.getY() / scale;
70 switch (event.getActionMasked()) {
71 case MotionEvent.ACTION_DOWN:
72 requestFocus();
73 Editor.press(Editor.view, x, y);
74 break;
75 case MotionEvent.ACTION_MOVE:
76 Editor.drag(Editor.view, x, y);
77 break;
78 case MotionEvent.ACTION_UP:
79 case MotionEvent.ACTION_CANCEL:
80 Editor.release(Editor.view);
81 break;
82 default:
83 return false;
84 }
85 dirty = true;
86 return true;
87 }
88
89 @Override
90 protected void onFocusChanged(boolean focused, int direction, Rect previous) {
91 super.onFocusChanged(focused, direction, previous);
92 focusChanged();
93 }
94
95 @Override
96 public void onWindowFocusChanged(boolean focused) {
97 super.onWindowFocusChanged(focused);
98 focusChanged();
99 }
100
101 private void focusChanged() {
102 if (!attached) return;
103 Editor.focus(Editor.view, hasFocus() && hasWindowFocus());
104 dirty = true;
105 }
106}