Pad/cmd/pad/impl_android.go
Greg Pomerantz e3690d6b98 Restore last file, cursor, scroll, selection and find state on relaunch (spec §7)
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.
2026-08-20 16:01:02 -04:00

120 lines
2.9 KiB
Go

//+build !darwin !linux
package main
/*
#cgo LDFLAGS: -landroid -llog
#include <stdlib.h>
#include "jni_android.h"
*/
import "C"
import (
"errors"
"fmt"
"runtime"
"unsafe"
"gioui.org/app"
"gioui.org/io/event"
_ "gioui.org/app/permission/storage"
)
type JNIEnv = C.JNIEnv
var (
startpath="/storage/emulated/0/Notes"
jvm uintptr
theJVM *C.JavaVM
)
func impl_start() { }
func handleEvent(e event.Event) {
switch e := e.(type) {
case app.AndroidViewEvent:
theJVM = (*C.JavaVM)(unsafe.Pointer(app.JavaVM()))
RunInJVM(func(env *JNIEnv) {
C.registerFragment(env, (C.jobject)(unsafe.Pointer(e.View)))
})
}
}
func RunInJVM(f func(env *C.JNIEnv)) {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
var env *C.JNIEnv
var detach bool
if res := C.GetEnv(theJVM, &env, C.JNI_VERSION_1_6); res != C.JNI_OK {
if res != C.JNI_EDETACHED {
panic(fmt.Errorf("JNI GetEnv failed with error %d", res))
}
if C.AttachCurrentThread(theJVM, &env, nil) != C.JNI_OK {
panic(errors.New("runInJVM: AttachCurrentThread failed"))
}
detach = true
}
if detach {
defer func() {
C.DetachCurrentThread(theJVM)
}()
}
f(env)
}
// SetGestureExclusions forwards the selection-handle grab boxes (view-local
// px) to View.setSystemGestureExclusionRects on Android so drags starting on
// an edge handle are not stolen by the system back gesture (see
// jni_android.c). It is called from the frame loop, which runs on the Android
// UI thread. No-op (C-side) before API 29.
func SetGestureExclusions(rects [][4]int) {
if theJVM == nil {
return
}
RunInJVM(func(env *C.JNIEnv) {
if len(rects) == 0 {
C.SetGestureExclusions(env, 0, nil)
return
}
buf := make([]C.jint, len(rects)*4)
for i, rc := range rects {
for j, v := range rc {
buf[i*4+j] = C.jint(v)
}
}
C.SetGestureExclusions(env, C.jint(len(rects)), &buf[0])
})
}
// sessionFilePath is where the relaunch session file (spec §7) lives. The
// primary storage dir already carries the app's caches (.pad browser index
// dirs, PadPerf), and the app holds the storage permission.
func sessionFilePath() string {
return "/storage/emulated/0/Pad/session.json"
}
func OpenFile(path string) {
var env *C.JNIEnv
var detach bool
if res := C.GetEnv(theJVM, &env, C.JNI_VERSION_1_6); res != C.JNI_OK {
if res != C.JNI_EDETACHED {
panic(fmt.Errorf("JNI GetEnv failed with error %d", res))
}
if C.AttachCurrentThread(theJVM, &env, nil) != C.JNI_OK {
panic(errors.New("OpenFile: AttachCurrentThread failed"))
}
detach = true
}
if detach {
defer func() {
C.DetachCurrentThread(theJVM)
}()
}
cpath := C.CString(path)
C.open_file_in_termux(env, cpath)
C.free(unsafe.Pointer(cpath))
}