Crash on device (Pixel 9 Pro, Android 17) when swiping the app away
from recents:
JNI DETECTED ERROR: java_object == null in call to GetObjectClass
#06 libgio.so (registerFragment+104)
Root cause: Gio's window.detach sends an EMPTY AndroidViewEvent
(View == 0) as its detach signal (os_android.go: window.detach ->
processEvent(AndroidViewEvent{})), which fires when the GioView is
destroyed — i.e. the activity going away on a recents-wipe. Our
handleEvent passed that null ref straight into registerFragment,
whose GetObjectClass(null) aborts. Pre-existing latent bug; the
emulator never delivered a detach event in testing (home keeps the
view attached, force-stop kills before the event dispatches).
Guard on both sides: handleEvent ignores the View == 0 detach
signal (a re-attach arrives as a fresh event with a live view), and
registerFragment returns early on a null view as defense in depth.
Verified on the crashing device: recents swipe now closes the app
cleanly, crash buffer empty.
The OS provides no user-space hook for a process kill, but the
activity onStop fires on every 'going away' transition the framework
still controls: entering recents (the swipe-wipe path), app switch,
and home. Recents-wipe then kills the process right after onStop
returns, so that moment is the last reliable flush.
- Logic.FlushSession (any goroutine, buffered, non-blocking) ->
flushSessionSave on the owner: persist the snapshot now, bypassing
the rate limit (still honoring the restore-pending suppression).
saveSessionIfChanged's persist tail is deduplicated into writeSession.
- JNI: GioActivity.onStop (patched into the smali by the build
scripts, in sync) now calls the static native padFlushSession, which
maps to the pad_flush_session cgo export.
Verified on emulator: the flush fires on home/app-switch and when the
app is backgrounded into recents before a kill; a state change made
inside the 250ms rate window and then backed out of the app persists
the latest position. A hard kill while foregrounded (force-stop,
memory pressure) still has no hook — the immediate edit saves plus the
250ms rate window bound that loss.
Persist a tiny JSON snapshot (SessionState) written by the cmd layer
($HOME/.pad/session.json off-Android, /storage/emulated/0/Pad/ on
Android) and call the logic-owned snapshot rate-limited (<=1/s, on
change) from emitFrame plus unconditionally at Shutdown. On launch the
cmd layer hands the snapshot to Logic.BeginRestore before Run; the
file re-opens through the normal openFile path and lands straight on
the editor page.
- Cursor/selection land with the content, clamped to a shrunk file
(path-guarded so a late result for a replaced file cannot apply the
snapshot to the wrong buffer); a missing file falls back to the
browser.
- Scroll is applied only after the first ScaleEvent has been laid out:
the size ConfigEvent precedes it and the one-way MaxScroll clamp in a
wrong-unit layout would corrupt the offset (found by e2e).
- Find: query + open/closed + current match are persisted; results are
regenerated by re-scanning and the saved current match is re-selected
by byte offset (Find.Restoring/RestoreMatch) without re-scrolling the
restored viewport. A closed-bar query re-scans on the next bar open
instead (an eager scan would be dropped and leave Scanning stuck).
- The main-owned find_bar widget is seeded with the restored query so
its first frame matches the logic-side query.
Docs: spec.md gains §2.4 and drops the §7 row; invariant 5 updated;
architecture.md gains §6.7. Tests: 7 e2e tests covering cursor/scroll/
selection restore, clamping, missing-file fallback, find-bar restore
(open/closed), and the saver/shutdown persist paths.
Three user-reported selection bugs, one root cause each:
1. Start handle ungrabbable at line start. Two interacting causes:
a) The 48dp grab box straddles two visual lines; a finger in the
lower half mapped (by y-to-line) to the neighbouring line, whose
byte past the other handle clamped to a zero-length selection ->
cleared on the first drag event. The cleared selection
un-registered the drag op, so the router silently stopped
delivering drag events (the observed 'stream cutoff'). Fix:
handle drags now project the finger's x onto the anchor's own
visual line (visualLineOfByte + textPosOnLineAtX); the anchor
never crosses lines during a handle drag.
b) A horizontal flick from the line-start handle (screen x~26px)
started the system back gesture, which cancelled the touch
stream. Fix: report the handle grab rects as system gesture
exclusion rects (setSystemGestureExclusionRects, API 29+),
marshalled to the UI thread via a PadExcl smali Runnable
(generated identically by build_emu.sh/build_phone.sh).
2. End-handle drag downward made the menu chase the finger and cover
the selection. Fix: the menu anchors to the STABLE end of the
selection (the end not being dragged), so it stays parked by the
selection start, clear of the finger and the highlighted text.
3. Menu above the selection vanished permanently when the selection
was extended onto the top line. Fix: off-window anchors no longer
hide the menu while any part of the selection is visible (keep-last
rect, clamped); hiding happens only for fully off-window selections.
Also: registerDrag simplified (single shared drag path, body before
handles in z-order), debug logging removed, regression tests
(mutation-verified) for line projection and menu anchoring, docs
section 17. Verified on device: start-handle drag shrinks the word
without clearing or triggering back navigation; end-handle vertical
drag leaves menu/highlight/handles undisturbed; menu stays visible
with the selection at the top line.
Drop the log.Printf/fmt.Printf traces that fired on every config/input/
click/scroll/open/worker-result during IME and tap debugging. These were
noise (and some were commented out). Error, limit, and recovery logs are
kept; the default-off profiler and gated IME debug remain untouched.
- JNI: open_file_in_termux via ACTION_SEND intent (text/plain + file:// uri),
global context ref kept from registerFragment
- impl_android.go: OpenFile(path) attaches current thread if needed
- NewLogic takes openfunc; State.open + ui.OpenFile now func(string)
- ChunkedBuffer.VisibleByteRange: word-wrap path using
GlyphLayout.VisualLineStarts (+byteOffset, lineHeight, visual index param)
- GlyphLayout gains LineHeight; drawWrappedText records VisualLineStarts
- WordWrap default true; State.ByteOffset tracks first visible line
- types: VisualLineIndex
- scroll_fix_test.go (new)
- debug prints left in place (WIP; cleanup in later phase)
- Add WriteFile method to mock filesystem (non-atomic path, creates files)
- Implement WriteFileAtomic in mock with temp file + rename pattern
using .tmp/ directory, matching real filesystem semantics
- Update WriteFileTask.Execute() to use WriteFileAtomic
- Update FlushAll() to use WriteFileAtomic
- Fix mock to create files on write (matching os.WriteFile behavior)
- Update tests to match new semantics (create-if-not-exists)