6.2 KiB
Architecture
Boundaries
Android picker / content URI / single-file open-with
|
DocumentStore (Android)
metadata -> size-limited private snapshot -> detector
|
+----------------+------------------+
| | |
PDF images text / packages
| | |
read-only PFD ImageLoader LocalPreviewLoader
| (Android bitmap) | |
isolated Service bytes JvmZipArchive
| | |
bounded raw ARGB +------+-----+
| |
PdfRenderClient common Kotlin core
| XML / Office / CSV / Markdown
| models / policy / search / export
+------------------- Compose UI -------------+
Native PDF data travels through granted file descriptors, not provider URIs or filesystem paths. The isolated worker cannot browse the app's private files. The host validates dimensions, byte length and page metadata before copying raw pixels to a bitmap. There is no PNG decoder in that return path. General image decoding still runs in the host process, and common parsers run on an IO dispatcher in the host; do not describe all parsing as process-isolated.
Why Kotlin Multiplatform first, not a new C++ format stack
The current reusable work is format identification, reading-preview extraction, policy, models,
search and export. It needs neither Android APIs nor a handwritten native Office rendering engine.
Those pieces live in core/src/commonMain, which also enables direct JVM tests. Kotlin common code
can be compiled for Apple targets without passing Android URI or Bitmap types through the API.
A later LibreOfficeKit or other native renderer can be a separate native adapter. This does not require the pure common core or native user interfaces to be rewritten in C++. Conversely, the JVM ZIP implementation is explicitly not portable to iOS; an equivalent Apple adapter is required.
Common model
FileClassifiercombines bounded header evidence, name/MIME hints and package entry inspection.SafetyPolicycentralizes ceilings and is shared by readers and archive adapters.ArchiveContainerexposes metadata and bounded part reads; it must never resolve external URLs.FormatHandleris the internal extension seam for package-reading preview handlers.DocumentPreviewdeclares its fidelity, section/table/block data, warnings and archive metadata.PreviewSafetylimits aggregate output amplification, including repeated shared strings.PreviewTextperforms literal searches and a text export; no formula, script or macro execution.CoreApiprovides a small public boundary useful to a native host.
SafeXml is a bounded preview-specific XML subset, not a validating standards-complete XML library.
It rejects DTD/custom entities and verifies basic syntax/nesting. It does not resolve namespace
URIs, validate schemas, establish signature trust or guarantee complete OOXML/ODF semantics.
Fuzzing and comparative testing against well-maintained parsers remain release gates.
Android lifecycle
ViewerViewModel owns the current document, page bitmap, reading preview, recents and loading/error
state. Rotation retains the ViewModel; saved provider URI/page state allows process-restoration
attempts. External attachment grants may expire, so reselection is an explicit recovery path.
A processing mutex serializes active document operations. New opens cancel earlier requests; input copy loops observe coroutine cancellation, and stale staged files are cleaned. Common parsing is bounded but not a hard-preemptible sandbox. PDF rendering requests have a timeout; cancellation and worker death need further real-device stress testing.
Snapshots use random internal names. Display names are sanitized separately. The original provider file is never opened for writing. Share exports go into a separate narrow FileProvider directory; that provider is not exported. Save/preview-export callbacks check the expected document identity so a new incoming document does not silently replace the intended export.
Storage contract
The native browser lists user-chosen document trees through SafBrowserStore. The system picker
still supplies user-mediated directory grants and standalone files. There is no whole-device crawl,
proprietary cloud or direct WebDAV/SFTP client. The app has no network permission; a provider may.
BrowserViewModel owns locations, visited breadcrumbs, options, selection and provider jobs, separate
from ViewerViewModel. Shared BrowserEntry / BrowserLocation / DirectoryReader types, sorting,
MIME/selection rules and SelectionCopyBudget live in commonMain. Android URI values are opaque IDs,
not parsed filesystem paths in the core. A saveable Compose state holder retains browser scroll
when the viewer replaces that screen. Provider metadata work has bounded workers and cancellation.
FilePickerActivity is a separate standard-launch activity. It accepts GET_CONTENT and returns only
user-confirmed, bounded snapshots via WorkbenchFileProvider, a narrow FileProvider subclass that
preserves MIME metadata. A single read grant applies to each selected copy, never to a source tree.
Only the requested standard extras survive activity-intent sanitization before ViewModel creation.
System OPEN_DOCUMENT is used for standalone selection, preventing GET_CONTENT picker recursion.
Folder bookmarks own tree grants. Document-history eviction only releases separate document grants. The per-process active-snapshot set protects a live MainActivity preview when a picker viewer opens; explicit clear-cache remains an intentional global invalidation of exported copies. Registry/cache cleanup and blocked-stream cancellation still require device stress tests.
Persistent recents store URI, display name, persistence flag and timestamp, up to 20 entries. No content indexing is performed across unopened files. Cache files are plaintext inside the Android sandbox; cache deletion is logical deletion, not guaranteed secure erasure.