initial commit v 0.2.0
Verify source and build Android / android (push) Canceled after 0s

This commit is contained in:
2026-09-24 22:49:27 +02:00
commit db873063f8
129 changed files with 7512 additions and 0 deletions
+101
View File
@@ -0,0 +1,101 @@
# Architecture
## Boundaries
```text
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
- `FileClassifier` combines bounded header evidence, name/MIME hints and package entry inspection.
- `SafetyPolicy` centralizes ceilings and is shared by readers and archive adapters.
- `ArchiveContainer` exposes metadata and bounded part reads; it must never resolve external URLs.
- `FormatHandler` is the internal extension seam for package-reading preview handlers.
- `DocumentPreview` declares its fidelity, section/table/block data, warnings and archive metadata.
- `PreviewSafety` limits aggregate output amplification, including repeated shared strings.
- `PreviewText` performs literal searches and a text export; no formula, script or macro execution.
- `CoreApi` provides 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.
+147
View File
@@ -0,0 +1,147 @@
# Lightweight file browser — 0.2.0
This is implemented source, not a device-verified APK. See `VALIDATION.md` for the executed checks.
## Everyday workflow
Open **Browse → Add folder**, select an allowed folder in Android's system UI, and grant read access.
The folder becomes a saved location when the provider offers a persistable grant. Tap it to browse
its children. Folder rows navigate; file rows open the existing read-only preview. **Files** / Back
returns to the browser with its location, filter, and saved Compose scroll state. **Up** and the
breadcrumb buttons navigate visited folders. Breadcrumbs represent the visited route, not a guessed
filesystem path. An incoming nested directory link can have a direct root-to-target breadcrumb.
Use **Open file** for a single file anywhere the system picker can access, including individual
Downloads items. That system UI does not require opening Google Docs or Microsoft Office. A provider
may need its own network/account; Fileworkbench itself has neither an account nor network permission.
The **Recent** tab contains opened previews. Removing a recent item or clearing history does not
revoke saved folder grants. Removing a saved folder releases its tree read grant and does not delete
files. Removing a folder can make recent documents relying on that grant inaccessible.
## Browser controls
- Saved locations, nested folder navigation, breadcrumbs, Up, refresh, and explicit access errors.
- Literal, case-insensitive filename search **inside the loaded current folder only**.
- Sort by name, modified date, size, or MIME type. Folders stay first; unknown sizes/dates stay last.
- Dotfile visibility toggle. No recursive content indexing or background storage crawl.
- File type badges, provider-supplied size/date, and file-details dialog.
- **More → Preview** uses the existing PDF/image/text/Office/CSV/ZIP preview path.
- **More → Open in another app** hands off a read-only copy using a chooser that excludes this viewer.
This also works for unsupported preview formats when their bytes fit the copy limit.
- Opt-in provider thumbnails. Off by default; enable them in a folder's **Options** menu. Only visible
rows whose provider advertises thumbnail support are requested. Other files retain type badges.
No automatic full-file PDF/Office rendering is performed while listing a folder.
Office support remains **text/table reading previews**, not full document-layout rendering. Formats
unsupported by the existing viewer have not gained rendering engines merely by appearing in a list.
## Registration as a files application
The main manifest declares:
| Entry point | Behaviour |
|---|---|
| `MAIN` + `LAUNCHER` | Normal application entry. |
| `MAIN` + `android.intent.category.APP_FILES` | Android's files-app launch category, introduced in API 29. Opens browser locations. |
| `VIEW` + content-directory MIME types | Opens a usable granted document tree; otherwise explains how to select a folder. |
| Existing file `VIEW` / single-file `SEND` | Existing file-preview entry points remain present. |
| `GET_CONTENT` + `OPENABLE` + `*/*` | Separate `FilePickerActivity`, used by apps asking a third-party picker for file contents. |
`APP_FILES` is **not** the web-browser role. Registration makes the app an eligible handler; it does
not guarantee that an OEM or caller exposes a default-files-app switch or chooser.
`ACTION_OPEN_DOCUMENT` and `ACTION_OPEN_DOCUMENT_TREE` remain system-owned. No activity claims those
actions. Fileworkbench is not a general `DocumentsProvider` re-exporting the user's saved folders.
The debug variant alone has a protected synthetic-fixture DocumentsProvider for instrumentation;
it contains no user's files and is excluded from release builds.
## Acting as another app's file picker
The incoming request's base MIME filter and optional `EXTRA_MIME_TYPES` are checked; extra filters
narrow the base filter. Invalid requests fail closed. Folders remain navigable but cannot be selected.
Virtual documents cannot be returned as ordinary byte streams; export them in their provider first.
Single selection replaces the previous file; multi-selection is allowed only when requested.
Selections can span folders and be reviewed/removed. **Nothing is returned automatically:** the user
must press **Use file(s)**. Previewing is available through a row's More menu; the preview footer can
select/deselect that file. Cancelling returns `RESULT_CANCELED` and no file grant.
For `EXTRA_LOCAL_ONLY`, saved provider folders are deliberately not browsed: this app cannot prove
that an arbitrary provider's documents are already offline. **Open file** delegates to the system
picker with LOCAL_ONLY and the caller's MIME/multiple constraints. Provider/platform conformance to
that hint still matters; this is not a hard network sandbox around other installed providers.
At confirmation the app re-queries the selected documents and validates their types/capabilities.
Actual bytes are copied with per-file and aggregate limits. Results contain fresh narrow
FileProvider URIs, with only `FLAG_GRANT_READ_URI_PERMISSION`. Multi-results use ClipData, and a
single result also has a data URI/type. No original tree URI, prefix, write, or persistable grant is
returned. MIME metadata is preserved for copied extensionless files.
These are **temporary imported copies**, not in-place edit handles or permanent cloud links. Copies
may expire after a day, be evicted by Android, or be removed by explicit cache clearing. Receivers
should import/read them promptly. Browser external-open copies have the same limits and semantics.
Exported activities sanitize intent extras before ViewModel construction. External callers cannot
seed internal SavedStateHandle keys such as `sourceUri` or `browser.selection` to silently preselect
files from previously granted storage. Only the expected standard request parameters survive.
## Bounds and trade-offs
| Resource | Current bound |
|---|---|
| Saved folders | 24 |
| Navigation depth | 64 visited levels; repeated-folder loops rejected |
| Listing | 5,000 visited provider rows; truncation is explicitly shown |
| Search | Current loaded directory; query capped at 256 characters |
| Selection | 1 file, or up to 16 when multiple selection is requested |
| Preview / picker / external-open copy | 64 MiB per file |
| Picker request aggregate | 128 MiB |
| Picker-copy disk cache | 256 MiB and 32 sessions; new copies refused when full |
| Thumbnails | 96-pixel requested size, 4 MiB memory cache, two concurrent requests |
| Provider metadata/thumbnail queries | Bounded worker pool and 20-second awaiting timeout with CancellationSignal |
A truncated directory is not a complete device search: sort/filter only affect the loaded subset.
Use the system picker to reach other entries. Refresh is manual, including when a cloud provider
reports that more entries are still loading. There is no claim of live watching or recursive search.
Blocking provider streams and thumbnail/native decoders still need device-level cancellation,
resource-pressure, and hostile-provider testing. A timeout can stop the UI waiting; it cannot force
an arbitrary external provider to stop executing. See the remaining work in `SECURITY.md`.
## Storage that is deliberately unavailable
There is no all-files-access permission. Android 11+ restricts granting the internal-storage root,
reliable SD-card roots, and the Downloads root via a directory grant; Android/data and Android/obb
also have restrictions. Other apps' private storage is not accessible. A permitted Downloads
subfolder can be bookmarked where offered; individual Downloads files use **Open file**.
There are no rename/delete/move/create-folder controls, broad MediaStore gallery index, raw-path
browser, SMB/WebDAV/SFTP client, or archive extraction operations in this release.
## Reuse on iOS
`core/commonMain/browser` contains real shared entry/location/listing models, `DirectoryReader`,
filename filters/sorting, MIME validation, selection policy, safe labels, and streamed-copy budgets.
IDs remain opaque strings. Android ContentResolver, Uri grants, thumbnails, FileProvider, and Compose
remain in `androidApp`. The future iOS host supplies security-scoped URL/bookmark access and a native
browser/preview UI; Android picker registration is not an iOS feature. No Apple binary was built here.
## Folder-listing correction in 0.2.2
Versions 0.2.0 and 0.2.1 can reject a valid folder with "The storage provider did
not return a folder listing." A nullable `use` result, caused by the absence of an
optional provider error message, was incorrectly treated as a missing cursor.
0.2.2 moves the null check to the query itself. This is not a request for wider
permissions. Rebuild/install the fix and reopen or refresh the saved folder.
See `HOTFIX-0.2.2.md` and `VALIDATION.md` for the change and verification boundary.
## SAF-D1 diagnostics integrated in 0.2.3
The continuing user-reported listing failure is unresolved. This release keeps
the 0.2.2 null-check correction and adds a distinct `[SAF-D1/NULL_CURSOR]` error
only when the query itself returns no cursor. It reports the provider authority,
Android API and retained-read-grant status, not file paths or contents. The
Locations footer includes the app version and `Browser diagnostics SAF-D1` so
the installed source can be identified. Query behaviour and permissions remain
unchanged. See `RELEASE-0.2.3.md` and `BUILD.md` before reproducing the failure.
+85
View File
@@ -0,0 +1,85 @@
# Browser acceptance plan — device execution pending
Run the normal build before claiming any row below passes:
```sh
./scripts/verify-android.sh
./gradlew :androidApp:connectedDebugAndroidTest
```
The added `BrowserIntegrationTest` sources cover synthetic-provider listings, a 5,000-row cap,
nested IDs, MIME-preserving read-only copies, multi-result ClipData, manifest resolution, local-only
request forwarding, virtual-file rejection, and malicious restore-key stripping. They have **not**
been compiled or run in the authoring environment. `ViewerSmokeTest` includes Browse/Recent/Add
folder checks and retains the isolated PDF-service test.
The debug variant exposes **Fileworkbench test files** through a protected fixture DocumentsProvider.
This is intentional testing support and absent from the release variant. It does not expose the
app's real documents or browser grants. Same-UID instrumentation checks result shape/bytes, not
cross-UID enforcement; perform the external-caller checks below on a real phone.
| Case | Expected result | Status |
|---|---|---|
| First launch | Browse/Recent, Open file/Add folder, no account or broad permission prompt | Not run |
| Add Documents/test folder | One system consent, saved location, files shown within app | Not run |
| Restart/reboot | Granted folder reopens; otherwise explicit renewal path | Not run |
| Download root on Android 11+ | Directory restriction visible; individual file open works | Not run |
| SD/USB removed or provider logged out | Recoverable error, no false empty-folder claim | Not run |
| Large/nested folder | Responsive bounded listing, folders first, explicit truncation | Not run |
| Search/sort/hidden files | Correct current-folder filtering, no claim of global results | Not run |
| Preview and Back | Same folder/filter/scroll; supported PDF/image/text/Office views | Not run |
| Provider thumbnails on/off | Only opted-in visible-row requests; type badge fallback | Not run |
| Clear recents/cache | Files remain, saved folder grants remain, temporary copies removed | Not run |
| Remove folder | Bookmark/tree grant removed, source files remain | Not run |
| Files-app category | Correct manifest handler where platform/caller supports it | Not run |
| Directory VIEW with tree/read grant | Target folder or explicit reselection recovery | Not run |
| Directory VIEW without usable grant | No raw path crawling; asks user to add folder | Not run |
| Separate caller GET_CONTENT single MIME | Only matching files selectable; cancel grants nothing | Not run |
| GET_CONTENT multiple MIME / multiple files | Reviewed files only; ClipData readable from caller | Not run |
| Caller attempts write / sibling / tree access | Denied; only each selected copy is read-granted | Not run |
| Extensionless copied content | Resolver MIME matches preserved metadata | Not run |
| Caller injects sourceUri/browser.selection extras | No unsolicited preview or preselection | Not run |
| Caller LOCAL_ONLY | System delegation preserves hint; saved roots not queried | Not run |
| Caller rotates / process dies / changes MIME | Request constraints survive; stale grants revalidated | Not run |
| Provider changes file MIME/size/virtual status | Revalidated; mismatches refused before return | Not run |
| Provider dishonest size / overlong stream | Actual 64/128 MiB budgets stop copying | Not run |
| Cancel while copying / provider hang | No result/grant; eventual partial-copy cleanup | Not run |
| Main viewer and picker coexist | New picker does not delete live viewer snapshot | Not run |
| Open unsupported file in another app | Explicit read-only copy; viewer excluded from chooser | Not run |
| TalkBack / large fonts / portrait/landscape | Usable labels, checks, focus and scrolling | Not run |
For a launch-only smoke check after installation:
```sh
adb shell am start -a android.intent.action.MAIN \
-c android.intent.category.APP_FILES -p dev.fileworkbench.android
adb shell am start -a android.intent.action.GET_CONTENT \
-c android.intent.category.OPENABLE -t 'image/*' -p dev.fileworkbench.android
```
`am start` is not a real result-receiving application. It cannot replace verification that a distinct
UID receives exactly the granted result URIs and cannot read siblings/write originals. Record actual
SDK, device, command, build commit, and outcome for every completed test. Do not mark the whole plan
passed merely because an APK builds or the offline core tests succeed.
## 0.2.2 listing regression
After installing this build over the previous app, reopen a saved folder and
refresh. Confirm normal and empty folders do not show the no-listing exception,
then navigate into a subfolder and preview a file. Reselect through Add folder
only when the provider's original permission was temporary or has been revoked.
The debug provider includes additional synthetic folders for empty, info-only,
loading (with and without cached rows), and provider-error listings. The updated
`BrowserIntegrationTest` covers these cases; run `:androidApp:connectedDebugAndroidTest`
with a device/emulator. Host cursor tests in `scripts/test-folder-listing.py` do not
replace those Android tests or verify real storage-provider permissions.
## 0.2.3 diagnostic reproduction
Install the newly built APK over the existing app using the same signing identity.
Do not uninstall/clear app data merely to reproduce the failure. Confirm the
Locations footer reads `Fileworkbench 0.2.3 · Browser diagnostics SAF-D1` before
reopening the previously failing folder. Record the whole new on-screen error,
including marker/provider/API/grant status. A missing retained grant is not proof
that temporary access is absent. This is a diagnostic test, not a claimed fix.
+204
View File
@@ -0,0 +1,204 @@
# Building and running — Fileworkbench 0.2.3
## Pinned baseline
| Component | Selected version |
|---|---|
| Gradle build JVM / daemon | Installed JDK **17**, any vendor |
| Java/Kotlin compilation toolchains and JVM bytecode target | **17** |
| Gradle wrapper distribution | **8.14.5** |
| Android Gradle Plugin | 8.11.1 (unchanged) |
| Kotlin / Compose compiler plugin | 2.2.21 (unchanged) |
| Compose BOM | 2025.08.00 (unchanged) |
| Android compile / target SDK | 36 / 36 |
| Android minimum SDK | 26 |
| Android Build Tools | 35.0.0 (AGP default) |
These are deliberate project pins, not a recommendation to use the newest releases.
This source package is **not a verified Android APK**. The current execution results
and limitations are recorded in `VALIDATION.md`.
## Start with a fresh extraction
This is the whole project, not a patch. Extract it to a new directory and open the
contained `fileworkbench` directory (the one containing `settings.gradle.kts`).
Keep your existing checkout and any local changes separately. Do not copy the old
`build.gradle.kts`, `settings.gradle.kts`, version catalog, wrapper files,
`gradle/gradle-daemon-jvm.properties`, `.idea`, `.gradle` or `.kotlin` over this release.
Otherwise old Gradle 9.x / Java 25 / duplicate-plugin settings can be reintroduced.
There are no machine-specific IDE files or absolute SDK/JDK paths in this archive.
For your Gitea repository, review the new files against your checkout after the
fresh project imports successfully; do not overwrite uncommitted work blindly.
## Install/select JDK 17 and the SDK
Install a **JDK 17**, not only a JRE. In Android Studio/IntelliJ, open
**Settings → Build, Execution, Deployment → Build Tools → Gradle**, use the wrapper
from this project, and select that JDK 17 as the **Gradle JDK/JVM**. On IDE versions
that expose daemon JVM criteria, leave this project's version-17 criteria intact.
You do not need to change the IDE's own boot runtime or uninstall other JDKs.
In SDK Manager install Android SDK Platform 36, Build Tools 35.0.0 and Platform
Tools. Accept any SDK licences yourself. Let the IDE generate a new
`local.properties` with `sdk.dir`, or use your existing `ANDROID_HOME` environment
setting. Do not commit local.properties, signing keys or private documents.
Set terminal `JAVA_HOME` to your installed JDK 17 directory (not its `bin` folder).
The project also recognises an optional `JDK17_HOME` for toolchain discovery.
The checked-in `gradle/gradle-daemon-jvm.properties` contains `toolchainVersion=17`;
module toolchains also select 17. No vendor or absolute installation path is pinned.
Automatic JDK downloads are disabled: install a JDK 17 before building.
### Linux/macOS terminal
```bash
cd /path/to/new/fileworkbench
# Replace this example with the path of your actual JDK 17 installation.
export JAVA_HOME="/path/to/jdk-17"
export PATH="$JAVA_HOME/bin:$PATH"
chmod +x gradlew scripts/*.sh
java -version
./gradlew --version
./gradlew help
./gradlew :core:jvmTest :platform-jvm:test \
:androidApp:assembleDebug :androidApp:assembleDebugAndroidTest :androidApp:lintDebug
```
### Windows PowerShell
```powershell
cd C:\path\to\new\fileworkbench
# Replace this example with the path of your actual JDK 17 installation.
$env:JAVA_HOME = "C:\path\to\jdk-17"
$env:Path = "$env:JAVA_HOME\bin;$env:Path"
java -version
.\gradlew.bat --version
.\gradlew.bat help
.\gradlew.bat :core:jvmTest :platform-jvm:test :androidApp:assembleDebug :androidApp:assembleDebugAndroidTest :androidApp:lintDebug
```
The environment changes above apply only to the current terminal. Verify that
`--version` reports **Gradle 8.14.5**, with the intended Java 17 launcher and daemon
criteria. A version command alone does not prove that the application compiles.
## First-run wrapper bootstrap
The external `gradle-wrapper.jar` could not be downloaded in the authoring runtime,
so it is **not bundled**. All project source, Gradle configuration, launchers and
bootstrap scripts are present. The first `./gradlew --version` invocation (Windows:
`.\gradlew.bat --version`) downloads the wrapper binary and verifies its SHA-256.
Do this once **before importing into the IDE**, so its Gradle integration can see
the wrapper JAR. An internet connection is required for this and dependency setup.
Both shell and PowerShell bootstrap scripts now request the 8.14.5 source tag and
release distribution. A downloaded JAR with the wrong checksum is not executed;
the script falls back to the separately verified distribution and checks its
embedded wrapper. The fallback needs `unzip` on Unix-like systems or .NET ZIP
support in PowerShell. A failed initial network request still aborts; resolve that
network problem or bootstrap from an official local Gradle installation.
Official **8.14.5 wrapper JAR SHA-256**:
```text
7d3a4ac4de1c32b59bc6a4eb8ecb8e612ccd0cf1ae1e99f66902da64df296172
```
Official **8.14.5 binary distribution SHA-256**:
```text
6f74b601422d6d6fc4e1f9a1ab6522f642c2fdcbc15ae33ebd30ba3d7198e854
```
Hashes were verified against Gradle's release-checksum reference on 2026-09-24;
this is not a claim that the binary downloads succeeded here.
Once the official wrapper runs, the standard upstream launchers can be generated:
```sh
./gradlew wrapper --gradle-version 8.14.5 --distribution-type bin \
--gradle-distribution-sha256-sum 6f74b601422d6d6fc4e1f9a1ab6522f642c2fdcbc15ae33ebd30ba3d7198e854
./gradlew wrapper
```
Commit the verified wrapper JAR and generated scripts in your own repository.
A network-free build is possible only after the required tooling and dependencies
are locally available. The installed application's main manifest has no network
permission; an external storage provider may use its own network connection.
## Install and identify this diagnostic build
After successful APK assembly, with a connected and authorised device:
```sh
adb install -r androidApp/build/outputs/apk/debug/androidApp-debug.apk
```
Use your existing debug signing identity when installing over the current app.
Changing source directories on the same machine normally keeps the same user
keystore; this archive does not contain a signing key. Do not uninstall or clear
app data just to test the folder issue. A signing mismatch needs to be resolved
with the existing signing key rather than silently discarding application data.
The package remains `dev.fileworkbench.android`, version **0.2.3**, code **5**.
In **Browse → Locations**, verify the footer:
```text
Fileworkbench 0.2.3 · Browser diagnostics SAF-D1 · read-only
```
Then open the previously failing folder. A genuinely null cursor now produces
`[SAF-D1/NULL_CURSOR]`, the storage-provider authority, Android API level, and
retained-grant status. Send the full new message for diagnosis. A false retained
grant does not establish a permission failure: temporary access may still exist.
The query, successful listing behaviour and storage permissions are unchanged.
## Verification and troubleshooting
With a device/emulator, execute the Android instrumentation sources:
```sh
./gradlew :androidApp:connectedDebugAndroidTest
```
`./scripts/verify-android.sh` runs source checks plus Gradle tests, APK assembly and
lint. The existing optional GitHub workflow uses Java 17 and these same commands;
it has not been run here. These commands can also run on your local machine or a
suitably configured Gitea runner; no Gitea workflow is introduced in this release.
For offline **host-only** checks with Python 3, Kotlin compiler and an installed JVM:
```sh
python3 scripts/check-project.py
python3 scripts/test-build-config.py
bash scripts/test-core.sh
python3 scripts/test-folder-listing.py
bash scripts/check-kotlin-syntax.sh
```
Host test bytecode targets 17, but the scripts run on the local JVM; see the
validation report for the actual host version. They do not compile Android APIs.
**A duplicate `org.jetbrains.kotlin.jvm` plugin request:** this archive's root
script contains five distinct plugin aliases, with no extra `id("...")` or
`kotlin("jvm")` declarations. Do not append a second request for the JVM plugin.
Its use in `platform-jvm/build.gradle.kts` is intentional. The offline
`scripts/check-build-config.py` verifies the root aliases and catalog IDs.
**Gradle still selects 9.x or Java 25:** confirm that you opened this new directory,
are using its wrapper and selected JDK 17 in the IDE. Inspect user-level Gradle
properties/init scripts and environment variables for local overrides. The new
settings guards reject a different Gradle version or daemon JVM rather than
silently building with an unintended toolchain.
**Could not find a matching JDK / no download URL:** install JDK 17 and make it
discoverable using JAVA_HOME/JDK17_HOME. The package deliberately has no per-machine
path or automatically provisioned JDK download.
## Primary references
- Gradle release hashes: https://gradle.org/release-checksums/
- Gradle 8.14.5 release notes: https://docs.gradle.org/8.14.5/release-notes.html
- Daemon JVM criteria: https://docs.gradle.org/current/userguide/gradle_daemon.html
- Android build JVM selection: https://developer.android.com/build/jdks
- Android command-line builds: https://developer.android.com/build/building-cmdline
+19
View File
@@ -0,0 +1,19 @@
Fileworkbench 0.2.0 actual Android build attempt — 2026-09-23
Command: ./scripts/verify-android.sh
Environment:
openjdk version "21.0.11" 2026-04-21
OpenJDK Runtime Environment (build 21.0.11+10-1-deb13u2-Debian)
OpenJDK 64-Bit Server VM (build 21.0.11+10-1-deb13u2-Debian, mixed mode, sharing)
info: kotlinc-jvm 1.9.0 (JRE 21.0.11+10-1-deb13u2-Debian)
ANDROID_HOME: not configured
Android SDK/emulator: not installed in this authoring environment
PASS project invariants: XML, permission-free manifest, isolated PDF service, narrow FileProvider, portable core, no WebView/log calls.
PASS browser source invariants: APP_FILES/GET_CONTENT, standard picker activity, no system-picker impersonation, narrow copied paths, read-only results, sanitized activity defaults, debug fixture excluded from main manifest.
Fetching the official Gradle 8.13 wrapper; a pinned SHA-256 is required.
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
␍ 0 0 0 0 0 0 0 0 --:--:-- --:--:-- --:--:-- 0curl: (6) Could not resolve host: raw.githubusercontent.com
Build command exit status: 6
No Android compiler, D8, lint or device execution was reached when bootstrap failed.
+68
View File
@@ -0,0 +1,68 @@
# Continuation brief
## User objective
Replace the routine act of opening mobile file attachments in Google/Microsoft apps with an
account-free local viewer. Android first; preserve an iOS path through reusable core logic.
Do not turn this into a storage cloud or a new full Office suite as the first deliverable.
## Current source
Read `README.md`, `FEATURES.md`, `ARCHITECTURE.md`, `SECURITY.md`, `BUILD.md` and `VALIDATION.md`.
Android Kotlin/Compose implementation, isolated PDF service, JVM ZIP adapter and genuine common
Kotlin reading-preview parsers are present. 112 offline core/JVM checks passed. Android and Apple
builds have not completed; Android bootstrap attempts fail before compilation. The archive contains no APK and no LibreOffice binary.
## Browser addition in 0.2.0
Read `BROWSER.md` and `BROWSER_ACCEPTANCE.md`. Native SAF folder browsing and a separate GET_CONTENT
picker are implemented, with APP_FILES/directory VIEW registration. Shared browser models, sort/filter,
MIME/selection policy and streamed-copy limits are in commonMain. Android adapters own grants, metadata,
opt-in thumbnails and copied FileProvider results. Exported activity extras are sanitized before
SavedStateHandle defaults are created. Do not remove that protection or forward directory grants.
No broad storage access, all-files permission, raw-path enumeration, file mutation, persistent picker
result or replacement system DocumentsUI is implemented. Android 11+ root/Downloads restrictions are
intentional and explained in the UI. The debug-only fixture DocumentsProvider is for tests only.
## First actions
Bootstrap the official checksum-verified Gradle wrapper on a connected build machine; install the
pinned Android SDK/JDK tooling; resolve/build/test/lint. Fix all actual compile/runtime issues before
extending formats. Run `ViewerSmokeTest`, `BrowserIntegrationTest`, and the manual distinct-UID real-provider flows. Preserve known limitations
in the UI and documentation. Update the validation report only with commands actually run.
## Non-negotiable boundaries
Keep originals read-only; explicit copy/export/share only. Do not add INTERNET, all-files access,
telemetry or account requirements to solve a document-opening problem. A selected provider may
have independent networking. Do not execute scripts/macros/formulas or load external resources.
Keep input/output/ZIP/XML/repetition limits enforced during parsing. Never extract archive paths to
the filesystem. Preserve capability/fidelity labels and explicit error recovery.
Keep Android types out of commonMain. `platform-jvm` is not the iOS implementation. Use the configured
Apple framework and build a native ZIP/file/PDF adapter later. Do not call an untested interface or
placeholder native bridge an implemented engine.
## Known areas for review
Custom preview XML parser is bounded but not schema/namespace validating. Office previews omit
substantial content. PDF IPC, worker cancellation/lifecycle and actual device memory need testing.
Image EXIF/animation handling is absent. Export/share jobs and cache cleanup need race tests. UI
strings are currently English and mostly inline; move them to a localization layer before adding
languages. Dependency locking/verification/SBOM requires an actual resolved build.
Follow the implementation slices in `ROADMAP.md`, with measurable acceptance results. Keep source
and documentation complete in each release ZIP. Do not commit user documents, local SDK paths,
private signing keys or generated build caches.
## Continuation from 0.2.3
Current requested baseline: Gradle 8.14.5, installed JDK 17, Kotlin 2.2.21, AGP 8.11.1.
All sources and SAF-D1 diagnostics are integrated; no patch step remains. Check
`docs/VALIDATION.md` before claiming build/device success. The persistent folder
failure is unresolved; obtain the exact new message after confirming the 0.2.3 /
SAF-D1 footer. Do not infer permission failure from retained-grant=false alone.
Do not upgrade the build toolchain or add a duplicate root Kotlin JVM plugin to
investigate the storage-provider issue. Current pins are checked offline by
`scripts/check-build-config.py` as well as by Gradle settings at configuration time.
+35
View File
@@ -0,0 +1,35 @@
# Office engine decision record
## Decision in 0.1.0
Ship an Android source implementation with local reading previews and a reusable Kotlin core.
Do not claim the LibreOfficeKit engine is bundled. Do not claim a reading preview preserves document
layout, all text, formula results or embedded objects.
The earlier high-level discussion made embedding LibreOfficeKit sound closer to adding a small
library dependency than it really is. LibreOffice's own Android notes describe bundling a native
LibreOffice core, bootstrapping it, a dedicated LOK thread, native callbacks and tiled rendering.
This is a substantive integration/build/security/licensing track; `SOURCES.md` points to the primary
upstream documentation. Its existing work remains valuable and is the preferred direction to
investigate rather than writing a new compatible Office layout engine.
## Required prototype deliverables
1. A pinned upstream revision, licence inventory and documented build recipe with real Android
binaries for the required ABIs. Do not assume a maintained public AAR exists.
2. A local open/page/tile/render prototype on actual hardware, including startup resources/fonts.
Do not redistribute fonts copied from the authoring environment.
3. Single-thread/queue ownership rules for engine calls and callbacks, a crash-isolated service,
and a bounded transport for page metadata, pixels, text and search results.
4. Explicit macro/external-link/font/network policies enforced inside the chosen engine and verified
against hostile documents, not just disabled in the host UI.
5. Fidelity tests across DOCX/XLSX/PPTX/ODF and only then legacy formats. Account for missing fonts,
unsupported layout behaviours, cached vs calculated spreadsheet values, and revision semantics.
6. Independent assessment of Apple build/distribution integration. The common Kotlin preview model
remains reusable; the native engine may need different platform bootstrapping/rendering adapters.
## Not done in this repository
There is no `liblo-native-code.so`, LOK JNI bridge, Collabora build, converted PDF cache, native engine
licence bundle or Office layout validation. The `FormatHandler` interface is an extension seam for
reading previews; it is not evidence of a full engine implementation.
+62
View File
@@ -0,0 +1,62 @@
# Feature and format matrix — 0.2.0
“Implemented” below means implementation source is present. Android runtime features remain
unverified until a real SDK build and device run. Core/JVM behaviour has the executed tests recorded
in `VALIDATION.md`.
| Area / format | Implemented source behaviour | Not implemented / important limits |
|---|---|---|
| Android file opening | System picker, VIEW intent, single-file SEND, read grants, bounded snapshots | No multi-file SEND, raw file paths, virtual-file conversion or cloud API |
| Native file browser | Saved folders, nested navigation, breadcrumbs/Up, metadata, search/sort/dotfiles, opt-in provider thumbnails | Scoped folders only; no root/all-files access, global index, live watch, file mutation or grid mode |
| Files-app registration | APP_FILES, content-directory VIEW, separate GET_CONTENT picker | Does not replace system OPEN_DOCUMENT / OPEN_DOCUMENT_TREE or guarantee a device default-files setting |
| File picker | MIME constraints, single/multiple selection, review/confirmation, read-only copied URI results, LOCAL_ONLY system delegation | No directory results, in-place editing, permanent URI promise, cloud-native conversion or original tree grants |
| External opening | Read-only copy and chooser excluding this viewer | 64 MiB limit; edits by recipients cannot update the original |
| Recent files | Up to 20 URI/name entries; permission retention when granted; remove/clear | Not copies retained forever; temporary attachment access can expire |
| PDF | Isolated Android renderer, one page at a time, previous/next/jump, pinch zoom | No text search/selection, continuous page scrolling, OCR, forms, annotations, passwords or printing |
| JPEG/PNG/WebP/GIF/BMP | Platform bitmap decoding, bounds checks/downsampling, zoom | First frame only; no EXIF orientation, animated playback, image editing or HEIC guarantee |
| Plain text and source | UTF-8 / BOM-tagged UTF-16, selectable text, literal search | No syntax highlighting, arbitrary legacy encodings, hex editor or giant-file streaming |
| Markdown | Heading/list/code-block emphasis; original inline markup retained | Not CommonMark-complete; no rich inline rendering, HTML execution or remote resources |
| CSV/TSV | Quoted fields, escaped quotes, multiline fields, comma/semicolon/tab detection, table view | Literal values only; no formulas, editing or schema inference |
| DOCX | Body paragraphs, basic heading/list recognition, flattened table rows | Not page layout; images, fields/revisions, comments, drawings, headers/footers and advanced structures incomplete |
| XLSX | Named sheets, relationships, sparse cell positions, shared/inline strings, stored values, formula strings | No calculation; cached values may be stale; date serials may show raw; no number styles/charts/merged cells/filter fidelity |
| PPTX | Presentation relationship order, slide sections, text paragraphs | Shape/XML text order not visual reading order; no graphical slides, charts, animations, speaker notes |
| ODT | Basic paragraphs/headings and mixed text | No layout fidelity, embedded graphics, revision semantics or full list/table formatting |
| ODS | Named sheets, stored values, bounded row/cell repeats, sparse blank-padding skip | No calculations/styles/charts; hidden content semantics incomplete |
| ODP | Named slide sections, text paragraphs | No full slide rendering or object-placement fidelity |
| ZIP | Entry/path/size listing and filename search; no extraction | No TAR/7z/RAR, encrypted ZIP, nested opening, selective extraction or compression |
| HTML/SVG/XML/JSON/YAML/etc. | Inert source text when recognized | No WebView, embedded scripting, SVG rasterization or structural tree editor |
| ICS/VCF | Raw text by extension | No calendar/contact import or structured card UI |
| Legacy DOC/XLS/PPT/RTF | Explicit unsupported message | No legacy rendering; no LibreOffice native engine |
| Macro-enabled Office | Known macro extensions and VBA packages rejected | Never execute macros; no automatic conversion |
| Encrypted Office | Unsupported package/binary error | No password flow or decryption |
| Share/save | Share an original snapshot, save a copy, export labelled preview text | Never edit original; no general conversion or Office-to-PDF export |
| Search | Supported extracted blocks/rows, literal case-insensitive matching, bounded hits | No PDF/OCR search; counts refer to matching blocks/rows, not all word occurrences |
| iOS | Common Kotlin core and opt-in framework targets | No app, ZIP adapter, bookmark implementation, native renderer or tested Apple build |
Browser limits: 24 saved folders, 64 visited levels, 5,000 listing rows, 16 selected files when allowed,
64 MiB per copied file, 128 MiB per selection, 256 MiB picker-copy cache, and 4 MiB thumbnail cache.
These are additional to the preview/parser limits below. See `BROWSER.md`.
## Initial policy ceilings
64 MiB imported file; 4 MiB plain-text bytes / expanded preview characters; 8 MiB per inflated ZIP
part; 32 MiB cumulative inflation; compression ratio up to 300:1; 4,096 archive entries; XML depth
96; 100,000 XML nodes; 64 attributes per element; 20,000 blocks; 32,768 characters per block/cell;
2,000 rows per displayed table; 128 columns; 50,000 total table cells; 200 sections; 1,000 search
result blocks/rows. Native PDF output is limited to 3 million pixels, width 2,048 and height 4,096.
Images over 100 million declared input pixels are rejected and decoded previews are downsampled.
These are conservative ceilings, not performance guarantees. A file below a byte ceiling can still
be rejected for structure, content, missing parts, encoding or a secondary limit. ZIP indexes do
not inflate file entries; expansion ceilings apply when a part is actually read. Very compressible
legitimate XML can hit the same ratio limit as malicious content and be refused.
## Fidelity contract
The app does not silently present a reading preview as the complete original. A visible banner
identifies Office previews, and a tap shows detailed caveats. Text exports retain notices.
Original-file sharing sends the untouched snapshot, not converted/extracted text.
Documents requiring exact placement, complete revision history, financial recalculation, or legal/
scientific completeness must be checked in an appropriate full-fidelity application. This release
is intended for opening and inspecting routine files, not certifying their complete contents.
+53
View File
@@ -0,0 +1,53 @@
# 0.2.1 — PDF instrumentation-test compilation fix
The reported failure was in `:androidApp:compileDebugAndroidTestKotlin`, at the
`PdfDocument().use` expression in `ViewerSmokeTest.kt`.
## Cause
Android's `android.graphics.pdf.PdfDocument` has a `close()` method, but does not
implement `java.io.Closeable` or `java.lang.AutoCloseable`. The Kotlin standard
library's `use` extensions require one of these interfaces. A method named `close`
is not sufficient. The type-parameter R diagnostic is a consequence of the invalid
receiver; explicit type arguments or dependency upgrades are not the fix.
## Correction
The PDF fixture now uses explicit `try/finally`: finish the started page, write the
PDF through an output stream (which can still use `use`), and close the document.
A unique temporary fixture is deleted even when setup or rendering throws; the
render client also retains explicit cleanup. Existing test assertions are retained.
No runtime feature or dependency changes were made. The complete source release
increments versionCode to 3 / versionName to 0.2.1 and updates validation documents.
The one-file patch changes only ViewerSmokeTest.kt, so it can be applied without
updating version metadata or replacing other locally modified files.
## Apply to a local 0.2.0 checkout
Replace `androidApp/src/androidTest/kotlin/dev/fileworkbench/android/ViewerSmokeTest.kt`
with the supplied fixed file, or run `git apply` with the supplied one-file patch.
The patch was checked against the exact test in the supplied 0.2.0 source archive.
Then run from PowerShell at the repository root:
```powershell
.\gradlew.bat :androidApp:compileDebugAndroidTestKotlin
.\gradlew.bat :core:jvmTest :platform-jvm:test :androidApp:assembleDebug :androidApp:assembleDebugAndroidTest :androidApp:lintDebug
```
The first command retries the failed compilation task. The second continues full
build validation. These do not run the instrumentation tests on a device; that
requires `:androidApp:connectedDebugAndroidTest` with a connected device/emulator.
## Verification boundary
See `VALIDATION.md`. The portable checks and syntax checks passed here. The Android
compilation retry stopped at Gradle-wrapper download with a DNS error before the
compiler ran; no APK or device-test success is claimed.
## Primary API references
- Android PdfDocument API: https://developer.android.com/reference/android/graphics/pdf/PdfDocument
- Kotlin Closeable.use: https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.io/use.html
- Kotlin AutoCloseable.use: https://kotlinlang.org/api/core/kotlin-stdlib/kotlin/use.html
+61
View File
@@ -0,0 +1,61 @@
# 0.2.2 — Valid folder listings incorrectly treated as missing
## Runtime correction
File: `androidApp/src/main/kotlin/dev/fileworkbench/android/browser/SafBrowserStore.kt`.
Before, the no-listing check followed the complete resource-handling expression:
```kotlin
resolver.query(children, PROJECTION, null, null, null, signal)?.use { cursor ->
// Read rows and optional status messages.
cursor.extras.getString(DocumentsContract.EXTRA_ERROR)?.let { information = safeFileLabel(it) }
} ?: throw PreviewException("The storage provider did not return a folder listing.")
```
The last expression returns null when no error is reported, and `use` returns the
result of its block. Thus success triggered the no-listing error. This was a
Fileworkbench control-flow bug, not a requirement for broader storage access.
After, check only the cursor returned by the provider:
```kotlin
val queryResult = resolver.query(children, PROJECTION, null, null, null, signal)
?: throw PreviewException("The storage provider did not return a folder listing.")
queryResult.use { cursor ->
// The existing row/extras handling stays unchanged.
}
```
A genuinely null query result still fails; an empty cursor is a valid empty listing.
The change does not swallow exceptions or treat missing access as an empty folder.
## Apply without replacing local build configuration
Use either the supplied replacement `SafBrowserStore.kt` at the path above or
`git apply` with the supplied one-file patch. Do not apply both. The patch was
verified against the shipped 0.2.0 and 0.2.1 source, whose browser file is identical.
It changes no Gradle/JDK settings, dependencies, manifest permissions, stored data,
or document contents. Applying only the patch does not change the displayed
application version. The complete source release sets version 0.2.2 / code 4.
Rebuild and install over the old debug app, then open/refresh the saved folder.
There is no migration and no reason to clear app data for this fix. Temporary grants
may require selecting a folder again; no new broad permission is requested.
## Regression coverage
`scripts/test-folder-listing.py` extracts the actual cursor-reading block and runs
14 JVM checks on test doubles. The old source fails 9 of these with the reported
message; the corrected source passes all 14. This is not an Android API/device test.
The debug fixture and `BrowserIntegrationTest` add five real Android test cases;
those still need a connected device/emulator to execute. The existing normal,
nested and large-folder tests also cover the corrected path.
See `VALIDATION.md` for the executed checks and their exact limitations.
## Primary references
- Kotlin use returns the block result: https://kotlinlang.org/api/core/kotlin-stdlib/kotlin.io/use.html
- Android EXTRA_ERROR is optional: https://developer.android.com/reference/android/provider/DocumentsContract#EXTRA_ERROR
- ContentResolver query result: https://developer.android.com/reference/android/content/ContentResolver
+21
View File
@@ -0,0 +1,21 @@
# Fileworkbench 0.2.0
Complete source update of the 0.1.0 project, not an APK or a patch-only archive.
Added a native read-only folder browser and Android file-picker registration. The app can browse
chosen document trees, preview supported files with the existing viewer, return to the browser,
and hand read-only copies to other apps. See BROWSER.md for the UI and exact integration contract.
Android integration: APP_FILES category, granted content-directory VIEW, and GET_CONTENT with
single/multiple MIME-filtered user-confirmed selections. System OPEN_DOCUMENT/OPEN_DOCUMENT_TREE
are not replaced. No unrestricted storage access, editing or file deletion was added.
Shared core: browser data models, directory-reader boundary, filename filtering/sorting, MIME
matching, selection validation and streaming copy budgets. Android UI/provider code stays separate.
Validation: 112 offline core/JVM checks passed; 44 Kotlin/Gradle-Kotlin sources parsed without syntax
errors; source/XML invariants passed. Real Android build failed before compilation at wrapper DNS
resolution. Twelve Android instrumentation tests are included but unexecuted. No APK is included.
Build on the same network-capable JDK 17 / SDK 36 machine or runner described in BUILD.md, then execute
BROWSER_ACCEPTANCE.md. No GitHub or Gitea connection, repository upload or remote CI run was performed.
+41
View File
@@ -0,0 +1,41 @@
# Fileworkbench 0.2.3 — complete diagnostic source release
This is a full source project with the previously separate SAF-D1 changes already
integrated. It is not a patch and does not require an existing checkout.
## Included changes
- Preserve all 0.2.2 modules, browser/viewer features, native iOS-facing core,
fixtures, earlier fixes and regression tests.
- Integrate the SAF-D1 null-cursor diagnostic and its updated 16-case host harness.
- Display `Fileworkbench 0.2.3 · Browser diagnostics SAF-D1` in the Locations footer.
- Set Android versionName to 0.2.3 and versionCode to 5, retaining the package ID.
- Pin Gradle 8.14.5 in the distribution URL, shell/PowerShell bootstraps and hashes.
- Add checked-in JDK-17 daemon criteria. All module toolchains and bytecode targets
use 17; the settings script rejects conflicting Gradle/JVM versions.
- Keep the original five distinct root plugin aliases and Kotlin/AGP versions;
add an offline source check to catch alias duplication and mismatched pins.
- Update setup instructions for a fresh extraction and preserve historical evidence.
No extra permissions, cloud services, Office rendering engines, JDK, SDK, signing
keys or compiled APK are added. The external wrapper JAR still needs first-run
bootstrap on a network-capable machine.
## Folder issue status
The earlier nullable-`use` result bug was corrected, but the user still reports a
listing failure. There is no device trace establishing why it persists. SAF-D1
identifies an actual null query result separately, with provider authority, API
level and retained-read-grant status. It does not change how files are queried or
claim to repair provider behaviour. No paths, document IDs or contents are added
to the diagnostic. Temporary grants remain distinct from persistent grants.
After building and installing, verify the 0.2.3/SAF-D1 footer, then reproduce the
folder problem and capture the full new error. Do not clear app data for this test.
## Start here
Extract into a fresh directory, install/select JDK 17, and run the supplied wrapper
once before IDE import. Use `docs/BUILD.md` for Windows and Linux/macOS commands.
Do not copy old IDE/Gradle state or build configuration over the new source.
The updated `docs/VALIDATION.md` distinguishes host tests from Android verification.
+72
View File
@@ -0,0 +1,72 @@
# Next implementation slices
The source release is an MVP implementation baseline, not a declaration that every runtime gate
has passed. Complete A before treating it as an installable/public release. Version 0.2.0 adds the requested
lightweight browser and picker in source; this does not waive the uncompleted Android runtime gate.
## A — Build and Android runtime gate
Resolve the pinned dependencies, run unit tests/lint, build debug and instrumented APKs, and execute
`ViewerSmokeTest` and `BrowserIntegrationTest` on an emulator/device. Test real picker and incoming VIEW/SEND flows from at least
a file manager, a browser and a mail app. Cover current Android and API 26. Fix compilation/API
mismatches before adding features. Record exact SDK/device/build results in `VALIDATION.md`.
Also execute `BROWSER_ACCEPTANCE.md`: folder grants/revocation, picker MIME/multi-selection, distinct-UID
copy/read-only grants, local-only requests, injected restore keys, and return-to-browser lifecycle.
Acceptance: a real APK opens PDF, image, text, CSV and each supported Office/ODF preview; search,
page changes, rotation, two successive incoming documents, back, save/share and cache clearing work.
## B — Lifecycle, storage and security hardening
Extend the new debug provider fixtures with doubles for non-seekable streams, unknown or dishonest sizes, revoked grants,
slow/blocked reads and inaccessible virtual documents. Test process death, cancelled requests,
renderer crash/hang, rapid opens, exports interrupted by a new open, memory pressure and cleanup.
Use dedicated jobs/budgets for export/share operations and robust cancellation/worker recycling.
Fuzz the bounded XML/ZIP/Office core and compare accepted structures with mature implementations.
Acceptance: no original is modified, no wrong document is exported, leaked grant/cache growth is
bounded, malformed files fail clearly, and no failed render leaves the UI permanently busy.
## C — Better everyday reading
Apply EXIF orientation, add richer Markdown parsing without web execution, improve CSV dialect
selection and syntax/source presentation, table navigation, reader font/spacing controls, accessibility,
localization, and preserved reader positions. Consider safe ZIP-entry preview without extraction.
Add PDF text extraction/search with version-aware capability detection and careful engine evaluation.
Acceptance: capabilities are visible per format; unsupported actions are not presented as available;
real users can read/search without confusing raw source, cached values and graphical layout.
## D — Full-fidelity Office engine integration
Do not enlarge the hand-written preview parser into a full Office renderer. Evaluate/build a real
LibreOfficeKit-based Android engine (or a suitable maintained alternative) against the current
Android SDK/NDK, ABI, packaging, security and licence requirements. Pin source revisions, establish
reproducible native builds and collect a representative Office/ODF fidelity corpus. Use native engine
thread ownership and bounded tile/page delivery; preserve the phone UI and the shared core models.
Acceptance: genuine native rendering from real DOCX/XLSX/PPTX/ODF files on arm64 hardware; measured
startup, memory, installation size and known fidelity differences; no host networking/macros/external
resource access. Legacy DOC/XLS/PPT are enabled only when actually rendered and tested. Maintain the
reading-preview mode as a separate labelled choice. Never ship a placeholder JNI/empty engine as support.
See `ENGINE_INTEGRATION.md` for the decision record.
## E — iOS shell
Build the actual common framework on compatible Mac/Xcode tooling. Add SwiftUI file UI, scoped URL
access/bookmarks, an equivalent bounded ZIP adapter, native PDF/image handling, search, shares and
cache cleanup. Reuse common readers instead of rewriting them in Swift.
Acceptance: equivalent first-file-open and expired-grant recovery; common fixture parity; framework
and native host tests. An Android isolated service is not an available iOS feature and needs a separate
platform security design.
## Later, optional
Annotations with a clear original/sidecar/export distinction; PDF forms; EPUB; EML; structured ICS/VCF;
certificates/fonts; media and more archive formats; printing and conversion; simple text editing;
then focused Office editing only when a tested engine and deliberate mobile UX exist. Storage
connectors or broader file management must remain separate from document rendering. Collaboration/accounts
are not necessary to achieve the original goal and are not MVP prerequisites.
+91
View File
@@ -0,0 +1,91 @@
# Security and privacy model
This is an unaudited MVP source release. “No network permission” does not mean “no possible
vulnerability,” and passing the included tests does not prove safety for arbitrary hostile files.
## Trust boundaries
An external app/provider controls document bytes, MIME type, metadata and timing. Name/MIME hints
are not treated as proof of format. The app accepts external `content://` URIs, not arbitrary
filesystem paths or HTTP URLs. Its own share-provider URIs are rejected as incoming documents to
reduce confused-deputy access to private cached data.
Providers and the Android platform are still dependencies. A chosen cloud provider can fetch bytes
using its own network permissions. Provider reads/queries may block; a coroutine cancellation is
not a universal hard timeout for a malfunctioning provider.
## Implemented controls
- Main manifest requests no INTERNET, broad storage, contacts or account permissions.
- Originals are read-only; bounded private copies provide stable/seekable inputs.
- Known macro-enabled files and VBA packages are rejected; no formula/macro/script engine exists.
- HTML, XML and SVG can be shown as source, never executed in a WebView.
- The document core does not fetch embedded external resources, relationships, fonts, images or URLs.
User-chosen provider files and opt-in provider thumbnails can involve the provider's own networking.
- ZIP entries are never extracted to filesystem paths. Traversal/absolute/ambiguous paths and
duplicate names are rejected; inflated bytes, entry count, ratio, cumulative expansion and CRC
are checked when applicable. Listing does not inflate large entries.
- XML DTD, custom/general entities and other declarations are disabled. No network/entity resolver
exists. Nesting/node/attribute/output limits reduce denial-of-service opportunities.
- Repeated ODF rows/cells and OOXML shared strings have output budgets.
- PDF input/output are confined to an isolated Android service using file descriptors. The host
accepts only dimension-checked raw ARGB output, not attacker-provided executable objects/HTML.
- The nonexported share FileProvider exposes only `cache/shared/` and `cache/picked/`, not the app root,
sessions, recents or saved folder grants. Outgoing intents grant individual copies read-only access.
- Picker MIME filters are checked before selection and rechecked against fresh metadata at confirmation.
Actual streamed byte counts enforce per-file and aggregate budgets regardless of declared sizes.
- LOCAL_ONLY picker requests delegate to Android with the same constraints rather than query arbitrary
saved provider folders. No URI supplied in incoming picker extras is automatically selected/read.
- Exported activity intents are sanitized before ViewModel creation to prevent caller-supplied saved-state
keys such as sourceUri/browser.selection from seeding previews or selections using existing grants.
- Document-history grant cleanup is separate from bookmark tree-grant cleanup. Originals are never deleted.
- Provider query jobs have a bounded worker pool, CancellationSignal, and an awaiting timeout. Listings,
labels, thumbnails, selections and copied bytes have explicit bounds. No recursive scan occurs.
- Active preview snapshots are tracked across concurrent viewer/picker activities; one viewer's startup
no longer unconditionally deletes another viewer's live input file.
- Backup and device-transfer exclusions are configured. No content/URI logging or analytics calls
are present in application source.
## Explicit remaining work
Native image decoding is not separately sandboxed. The common XML/Office code is a limited custom
parser, not independently audited. Complete namespace/schema semantics are not checked. Unicode
and malformed-package fuzzing needs broader coverage. Rich Office rendering will substantially
increase the native attack surface and requires a fresh review.
Android compilation, IPC, lifecycle, provider behaviour, device-memory pressure, process death,
export races, accessibility and actual permission/backup behaviour have not been runtime-tested
here. The PDF timeout handles the awaiting client; it is not a proof of immediate worker termination
on every device or cancellation path. Run the acceptance checks before ordinary distribution.
There is no application-level encryption, master password or secure-window mode. File names/URIs
in recents can be sensitive. Displayed content can appear in screenshots/app-switcher previews.
Copying selected text and explicit shares pass content to platform/recipient surfaces. Cache cleanup
is not cryptographic erasure. Uninstall behaviour, device backups and rooted-device access are not
security guarantees provided by this project.
Dependencies are directly version-pinned but no resolved dependency verification manifest, SBOM or
transitive lockfile is included. Generate and review these after a real build. Release signing keys
must be created and controlled by the project owner, not bundled into this repository.
## Before sensitive production use
Require successful Android tests on at least the oldest supported API and a current device, a
reviewed hostile-document corpus, memory/timeout tests, IPC failure tests, dependency scanning,
licence review and an external security review appropriate to the intended users.
## Browser-specific residual risks
FileProvider/intent grants, actual Android SavedStateHandle behaviour, external caller access, and
provider query cancellation have not been device-verified. Same-UID fixture tests are not proof of
cross-UID grant isolation. Copied content can change between metadata retrieval and streaming; the
copy is bounded, not a transactional snapshot of an arbitrary external provider. MIME is metadata,
not proof of safe bytes. Native thumbnail/image decoding needs independent hostile-input review.
Temporary GET_CONTENT copies are for prompt import, not permanent links. Explicit cache clearing,
Android cache eviction or expiry can invalidate them. A receiver cannot write the source document.
No immutable/versioned cloud snapshot semantics are claimed. Shared MIME metadata is private,
bounded and cleared alongside snapshots. Persistent browser bookmarks contain sensitive names/URIs.
A debug-only DocumentsProvider supplies synthetic fixture files for tests. It is protected by the
system document-provider permission and is absent from release builds; it never serves user storage.
+60
View File
@@ -0,0 +1,60 @@
# Primary technical references
Consulted for design/tooling verification on 2026-09-23. These references explain platform APIs and
upstream architecture; they do not verify this project's Android/iOS runtime behaviour.
- Android Storage Access Framework: user-selected provider files, grants, metadata, byte streams,
and special handling required for virtual documents.
https://developer.android.com/training/data-storage/shared/documents-files
- Android PdfRenderer: seekable descriptors, lifecycle and recommendation to use an isolated process
for untrusted input.
https://developer.android.com/reference/android/graphics/pdf/PdfRenderer
- Kotlin Multiplatform overview and platform-independent source sets.
https://kotlinlang.org/docs/multiplatform/get-started.html
https://kotlinlang.org/docs/multiplatform/multiplatform-discover-project.html
- Kotlin Multiplatform compatibility table: Kotlin 2.2.21, Gradle, AGP and Xcode ranges.
https://kotlinlang.org/docs/multiplatform/multiplatform-compatibility-guide.html
- Android Gradle Plugin 8.11 release notes: Gradle 8.13, JDK 17, API 36 and Build Tools 35 baseline.
https://developer.android.com/build/releases/agp-8-11-0-release-notes
- Compose BOM / compiler configuration.
https://developer.android.com/develop/ui/compose/bom
https://developer.android.com/jetpack/androidx/releases/compose
https://developer.android.com/jetpack/androidx/releases/compose-kotlin
- Gradle official distribution and wrapper checksums (8.13 entries).
https://gradle.org/release-checksums/
- LibreOffice Android architecture and bootstrapping notes: real native engine, LOK thread and tiles.
https://raw.githubusercontent.com/LibreOffice/core/master/android/README.md
Versions in this project are pinned development choices, not “latest” recommendations. Check current
compatibility/security information before upgrading or distributing. The new preview parsers were
written for this project; they are neither upstream LibreOffice components nor standards-certified
implementations.
## Browser and picker implementation references (reviewed 2026-09-23)
- Android SAF folder access and persistable grants: https://developer.android.com/training/data-storage/shared/documents-files
- Android 11 directory restrictions: https://developer.android.com/about/versions/11/privacy/storage
- Intent categories / GET_CONTENT / LOCAL_ONLY / multi-result contracts: https://developer.android.com/reference/android/content/Intent
- System-owned OPEN_DOCUMENT: https://developer.android.com/guide/components/intents-common#Storage
- FileProvider sharing and temporary URI grants: https://developer.android.com/training/secure-file-sharing/share-file
- Provider metadata / thumbnails: https://developer.android.com/reference/android/provider/DocumentsContract
- Root capabilities / LOCAL_ONLY: https://developer.android.com/reference/android/provider/DocumentsContract.Root
- AndroidX ComponentActivity lifecycle / ViewModel default arguments (inspect the pinned dependency version): https://developer.android.com/reference/androidx/activity/ComponentActivity
These references establish API contracts, not proof that this source has passed a real Android build.
## 0.2.3 tooling update (verified 2026-09-24)
The current project changes the previous Gradle 8.13 baseline to the user's requested
8.14.5 while retaining JDK 17, Kotlin 2.2.21 and AGP 8.11.1.
- Official 8.14.5 binary distribution and wrapper JAR SHA-256 hashes:
https://gradle.org/release-checksums/
- Gradle 8.14.5 release:
https://docs.gradle.org/8.14.5/release-notes.html
- Daemon JVM criteria (`toolchainVersion=17`), local detection and precedence:
https://docs.gradle.org/current/userguide/gradle_daemon.html
Official documentation and published hashes were accessible through web retrieval;
the local build runtime could not resolve the download host. These references do
not establish that the project has compiled or that the folder issue is fixed.
+3
View File
@@ -0,0 +1,3 @@
PASS project invariants: XML, permission-free manifest, isolated PDF service, narrow FileProvider, portable core, no WebView/log calls.
PASS browser source invariants: APP_FILES/GET_CONTENT, standard picker activity, no system-picker impersonation, narrow copied paths, read-only results, sanitized activity defaults, debug fixture excluded from main manifest.
Kotlin PSI syntax: 44 files, 0 parse errors. Android/Compose type resolution is NOT tested.
+114
View File
@@ -0,0 +1,114 @@
Prepared 112 production-core/JVM checks.
PASS CoreTests.pdfMagicWinsOverName
PASS CoreTests.imageMagicWinsOverName
PASS CoreTests.caseInsensitiveExtensions
PASS CoreTests.mimeFallbackHandlesMissingExtension
PASS CoreTests.htmlAndSvgAreOnlyText
PASS CoreTests.unknownBinaryIsNotAssumedText
PASS CoreTests.legacyBinaryDetected
PASS CoreTests.zipMagicDisallowsTextPretence
PASS CoreTests.extensionlessOfficeInspected
PASS CoreTests.utf8BomAndUnicode
PASS CoreTests.utf16LittleEndian
PASS CoreTests.utf16BigEndian
PASS CoreTests.utf16RejectsOddBytes
PASS CoreTests.utf16RejectsLoneSurrogate
PASS CoreTests.invalidUtf8Rejected
PASS CoreTests.nulBinaryRejected
PASS CoreTests.xmlPreservesMixedContent
PASS CoreTests.xmlDecodesSafeEntities
PASS CoreTests.xmlCdataIsLiteral
PASS CoreTests.xmlRejectsDtd
PASS CoreTests.xmlRejectsCustomEntity
PASS CoreTests.xmlRejectsMismatchedTags
PASS CoreTests.xmlRejectsMultipleRoots
PASS CoreTests.xmlRejectsDeepNesting
PASS CoreTests.xmlRejectsTooManyNodes
PASS CoreTests.xmlRejectsTooManyAttributes
PASS CoreTests.xmlRejectsDuplicateAttribute
PASS CoreTests.xmlRejectsUnquotedAttribute
PASS CoreTests.xmlRejectsInvalidCharacterReference
PASS CoreTests.packagePathsStayInsideRoot
PASS CoreTests.packageRootEscapeRejected
PASS CoreTests.externalRelationshipsRejected
PASS CoreTests.csvQuotedDelimitersAndNewlines
PASS CoreTests.csvEscapedQuotes
PASS CoreTests.csvEmptyTrailingCells
PASS CoreTests.csvWindowsNewlines
PASS CoreTests.csvSemicolonDetection
PASS CoreTests.csvFormulaRemainsLiteral
PASS CoreTests.csvBadQuotesFailClearly
PASS CoreTests.csvRowLimit
PASS CoreTests.csvColumnLimit
PASS CoreTests.basicMarkdownHasHeadingsAndCode
PASS CoreTests.textByteLimit
PASS CoreTests.columnLabelsBeyondZ
PASS CoreTests.docxReadingTextAndTables
PASS CoreTests.xlsxSparseCellsAndSharedStrings
PASS CoreTests.xlsxTooWideRejected
PASS CoreTests.pptxFollowsRelationshipOrderNotFilenames
PASS CoreTests.odtMixedTextSpacesAndHeadings
PASS CoreTests.odsRepeatedPaddingDoesNotExplode
PASS CoreTests.odsRepeatedNonemptyRowsRejected
PASS CoreTests.odpSlideNamesPreserved
PASS CoreTests.malformedOfficePartRejected
PASS CoreTests.zipIndexDoesNotReadEntries
PASS CoreTests.literalSearchAndHitCap
PASS CoreTests.tableSearchUsesActualRows
PASS CoreTests.textExportContainsSectionNamesAndValues
PASS CoreTests.repeatedContentAmplificationBounded
PASS CoreTests.veryLongSingleBlockRejected
PASS CoreTests.macroExtensionsNotReclassifiedAsZip
PASS CoreTests.renamedMacroPackageRejected
PASS CoreTests.publicCoreFacadeReadsText
PASS CoreTests.publicCoreFacadeRejectsBinary
PASS BrowserTests.directoriesRemainFirstWhenDescending
PASS BrowserTests.filtersNamesCaseInsensitively
PASS BrowserTests.searchIsLiteralNotRegex
PASS BrowserTests.hidesDotFilesUnlessEnabled
PASS BrowserTests.unknownSizesRemainLastInBothDirections
PASS BrowserTests.modifiedSortingUsesTimestamps
PASS BrowserTests.typeSortingIsDeterministic
PASS BrowserTests.duplicateProviderRowsDoNotDuplicateComposeKeys
PASS BrowserTests.listingProcessingHasAnUpperBound
PASS BrowserTests.labelsStripSpoofingButLeaveIdsUntouched
PASS BrowserTests.labelsAreBoundedAndNonempty
PASS BrowserTests.mimeMatchingHandlesWildcardsAndParameters
PASS BrowserTests.malformedMimeRequestsFailClosed
PASS BrowserTests.wildcardIsNotAnActualFileMimeType
PASS BrowserTests.extraMimeTypesNarrowTheBaseType
PASS BrowserTests.excessiveMimeTypesAreRejected
PASS BrowserTests.foldersStayNavigableInFilteredPicker
PASS BrowserTests.directoriesAndVirtualDocumentsCannotBeReturned
PASS BrowserTests.singleSelectionReplacesPreviousSelection
PASS BrowserTests.togglingSelectedFileRemovesIt
PASS BrowserTests.multipleSelectionIsBounded
PASS BrowserTests.unsupportedSelectionIsRejected
PASS BrowserTests.emptySelectionIsRejected
PASS BrowserTests.duplicateSelectionIsRejected
PASS BrowserTests.singleRequestCannotReturnTwoFiles
PASS BrowserTests.individualSizeBudgetIsChecked
PASS BrowserTests.aggregateSizeBudgetIsChecked
PASS BrowserTests.negativeSizeIsNotTrusted
PASS BrowserTests.unknownSizesAreAllowedPendingStreamingLimit
PASS BrowserTests.comparatorRemainsAntisymmetric
PASS BrowserTests.actualStreamBytesAreLimitedEvenWithUnknownMetadata
PASS BrowserTests.totalByteBudgetSpansFiles
PASS BrowserTests.newFileResetsOnlyTheFileCounter
PASS BrowserTests.zeroByteFilesAreAllowed
PASS BrowserTests.negativeCopyCountsAreRejected
PASS BrowserTests.byteCountersRejectHugeChunksWithoutOverflow
PASS BrowserTests.invalidCopyBudgetsAreRejected
PASS ZipTests.archiveRoundtrip
PASS ZipTests.traversalRejected
PASS ZipTests.absolutePathsRejected
PASS ZipTests.backslashPathsRejected
PASS ZipTests.tooManyEntriesRejected
PASS ZipTests.oversizedEntryRejectedOnlyWhenRead
PASS ZipTests.compressionBombRejected
PASS ZipTests.cumulativeExpansionBudgetEnforced
PASS ZipTests.repeatedReadsConsumeBudget
PASS ZipTests.extensionlessOfficeLoadedThroughFullAdapter
PASS ZipTests.textByteBudgetEnforcedBeforeRead
PASS ZipTests.htmlRemainsInertSource
RESULT: 112/112 checks passed
+75
View File
@@ -0,0 +1,75 @@
# Validation report — Fileworkbench source release 0.2.0
Date: 2026-09-23. This is an implemented-source release, **not a verified Android APK**.
## Executed
| Check | Result | Scope |
|---|---|---|
| Offline production core/JVM compilation and checks | **112 / 112 passed** | Kotlin/JVM 1.9.0, OpenJDK 21.0.11; not the pinned Android toolchain |
| Android resource/manifest XML and source invariants | Passed | Source-level checks only |
| Kotlin PSI parse | **44 files; 0 parse errors** | Syntax only; does not resolve Android/Compose/Gradle symbols |
| Android build entry point `./scripts/verify-android.sh` | **Failed at wrapper download, exit 6** | DNS lookup for raw.githubusercontent.com failed before Android compilation |
| APK generation | **No APK produced** | No Android SDK/Gradle dependencies/emulator available |
The 112 checks comprise the retained 75 document-core/JVM checks plus 37 new browser/MIME/selection/
byte-budget checks. The offline runner invokes the same unit-test method bodies used by the Gradle
suite after removing only test-runner annotations/imports. It does not substitute for a resolved
Gradle build, Android instrumentation, or Apple framework compilation.
Executed commands:
```sh
./scripts/test-core.sh
python3 scripts/check-project.py
./scripts/check-kotlin-syntax.sh
./scripts/verify-android.sh
```
Raw evidence is in `TEST_RESULTS.txt`, `SOURCE_CHECKS.txt`, and `BUILD_ATTEMPT.txt`. The actual build
attempt reached project source checks and the Gradle wrapper bootstrap, then failed with:
```text
curl: (6) Could not resolve host: raw.githubusercontent.com
```
No Android compiler, D8, lint, packaging, installation, or emulator ran after that failure.
## What the structural checks establish
The main manifest is parseable and declares no uses-permission entries; the PDF service is marked
isolated/nonexported; the production FileProvider is nonexported and its allowlist contains only
shared/ and picked/ caches. APP_FILES and a separate standard-launch GET_CONTENT activity are
registered, and no activity declares OPEN_DOCUMENT/OPEN_DOCUMENT_TREE. The debug fixture provider
is absent from the main manifest. Source assertions check read-only copied-result flags, use of
streaming byte budgets, and intent sanitization before activity ViewModel creation.
Common Kotlin files have no Android/Java imports. There is no WebView instantiation or Log call in
main Android source. These are useful invariants, **not** runtime grant-isolation or security proofs.
## Written but not executed
There are **12 Android instrumentation test methods**: the two retained/extended viewer smoke tests
and ten browser/picker integration tests. They cover UI entry points, PDF IPC, synthetic SAF metadata,
large/nested folders, copied MIME metadata, single/multiple result structure, intent registration,
LOCAL_ONLY/MIME forwarding, virtual documents, and rejection of caller-supplied restore-state keys.
These Android tests have not been compiled, installed or run. The debug-only synthetic document
provider is test support, not a user-storage provider. Same-UID result checks cannot establish
cross-UID grants; a separate caller app is explicitly required by `BROWSER_ACCEPTANCE.md`.
## Remaining verification gates
- Resolve pinned Gradle/Kotlin/Android dependencies under JDK 17; compile, lint, D8 and assemble both
debug APKs. Fix actual compiler/API/packaging errors before claiming an installable release.
- Run instrumented tests and `BROWSER_ACCEPTANCE.md` on real devices/emulators, including API 26,
Android 11+ storage restrictions, a current phone, and an independent result-receiving app.
- Verify preview-to-browser navigation/scroll, rotation/process restoration, separate picker/viewer
lifetimes, folder grant renewal/removal, cache/URI invalidation, and existing preview regressions.
- Verify cancellation with slow/unresponsive providers, truthful and dishonest sizes, truncated
listings, malformed documents, thumbnail decoding, memory pressure and copied-file limits.
- Verify accessibility, large fonts, portrait/landscape layouts, and actual visual rendering.
- Build the Apple framework and implement/test its native browser/file-access adapters separately.
No device screenshots, performance measurements, whole-phone storage access, perfect Office
rendering, security audit, iOS app, or successfully executed CI build are claimed by this release.
+47
View File
@@ -0,0 +1,47 @@
# Validation report — Fileworkbench source release 0.2.1
Date: 2026-09-24. **Source hotfix, not a verified Android APK.**
## Change scope
Corrected the invalid `PdfDocument().use` call in the Android instrumentation smoke
test. Added explicit page/document/client cleanup and unique temporary-file cleanup.
No production feature or dependency changes. Updated version metadata and documentation,
and added a narrow source-pattern regression guard. Details: `HOTFIX-0.2.1.md`.
## Executed in this session
| Check | Result | Boundary |
|---|---|---|
| Portable core/JVM checks | **112/112 passed** | Kotlin/JVM 1.9.0, JDK 21.0.11; not pinned Android/Gradle toolchain |
| Kotlin PSI syntax | **44 files, zero parse errors** | Does not resolve Android or Compose types |
| XML and source invariants | Passed | Includes guard against the exact invalid constructor/use expression |
| Regression guard positive/negative check | Passed | Rejects original source; accepts fixed source; not type checking |
| One-file patch application and reversal | Passed | Exact original 0.2.0 ViewerSmokeTest; output matches supplied fixed file |
| `:androidApp:compileDebugAndroidTestKotlin` retry | **Blocked before compilation** | Gradle-wrapper download failed at DNS resolution; exit 6 |
| APK / installation / device tests | **Not produced / not run** | No Android SDK or emulator installed here |
The new source guard catches the reported expression only; it does not establish
that arbitrary `use` calls have valid receivers. No Android build is claimed.
Raw evidence is under `validation/0.2.1/`. Historical 0.2.0 evidence remains in its
original files; the previous report is preserved as `VALIDATION-0.2.0.md`.
The attempted Android command was:
```sh
./gradlew --no-daemon --console=plain :androidApp:compileDebugAndroidTestKotlin
```
The failure was:
```text
curl: (6) Could not resolve host: raw.githubusercontent.com
```
## Still required on a configured build machine
Retry instrumentation compilation, then assemble the debug app and test APKs, run
unit tests and lint, and run instrumented tests on a phone or emulator. Follow the
existing `BROWSER_ACCEPTANCE.md` checklist. iOS framework/application verification
is unchanged and remains outstanding.
+75
View File
@@ -0,0 +1,75 @@
# Validation report — Fileworkbench source release 0.2.2
Date: 2026-09-24. **Folder-listing source hotfix; not a verified new Android APK.**
The user reported a successful local build of the preceding source, then a runtime
failure when adding a folder: "The storage provider did not return a folder listing."
The local device, provider and Android version have not been inspected here.
## Reproduced cause
`SafBrowserStore.list` used `query(...)?.use { ... } ?: throw ...`. The final
expression inside `use` was a nullable read of `DocumentsContract.EXTRA_ERROR`.
When the provider returned a valid cursor without that optional message, the block
returned null and the trailing Elvis operator raised the no-listing exception,
even after successfully reading all rows. Empty directories had the same issue.
The fix checks the query result for null **before** entering `use`. A genuinely
missing cursor still raises the original exception. Cursor closure, row limits,
loading/info/error messages, URI checks and permission handling are unchanged.
Only `SafBrowserStore.kt` needs to change to apply the runtime correction.
## Executed checks
| Check | Result | Boundary |
|---|---|---|
| Extracted original production listing block | **9 failures / 14 checks** | Reproduces the exact reported exception on valid cursors without EXTRA_ERROR |
| Extracted fixed production listing block | **14/14 passed** | JVM control-flow/resource checks using real common models and Android test doubles |
| Shared-core/JVM adapter suite | **112/112 passed** | Local Kotlin/JVM compiler, not the Android/Gradle toolchain |
| Kotlin PSI syntax | **44 files, zero parse errors** | Syntax only; no Android/Compose type resolution |
| XML and existing source invariants | Passed | No new storage permissions or dependencies |
| One-file patch | Applies to both shipped 0.2.0 and 0.2.1 source | Exact checked archive baselines, not an unseen locally modified checkout |
| New Android instrumentation tests | **5 added; not run here** | Empty, info-only, loading with/without cached rows, and provider-error listings |
| Android APK/test-APK build attempt | **Blocked before compilation** | Gradle-wrapper bootstrap cannot resolve raw.githubusercontent.com; exit 6 |
| Phone/emulator/iOS checks | Not performed here | No APK or device-test result supplied |
The host harness copies the cursor-reading block directly out of the selected
production source and compiles it with the real common models. The resolver,
cursor, extras, cancellation signal and URI builder are lightweight test doubles.
It does **not** test Android API compatibility, Binder, real permissions, provider
behaviour, asynchronous scheduling, or the screen. It is not a substitute for the
instrumentation tests or the local Android build.
Raw logs are in `validation/0.2.2/`. The preceding report is preserved as
`VALIDATION-0.2.1.md`; older logs retain their original provenance.
## Reproduce the host checks
With a local Kotlin compiler, Java and Python 3:
```sh
python3 scripts/test-folder-listing.py
bash scripts/test-core.sh
bash scripts/check-kotlin-syntax.sh
python3 scripts/check-project.py
```
To demonstrate the regression against the preceding file, pass its actual local
path to `python3 scripts/test-folder-listing.py --source /path/to/old/SafBrowserStore.kt`.
That command should fail on the old implementation; the failure is expected.
## Local Android acceptance
Keep the local Gradle/JDK configuration that already builds. Apply the one-file fix,
then rebuild and install over the existing debug app using the same signing key:
```powershell
.\gradlew.bat :androidApp:assembleDebug :androidApp:assembleDebugAndroidTest
adb install -r .\androidApp\build\outputs\apk\debug\androidApp-debug.apk
```
Open the saved folder and refresh, or select it again if its grant was temporary.
Check a normal folder, an empty folder, a nested folder and a document preview.
With the complete updated source and a device/emulator, run
`:androidApp:connectedDebugAndroidTest` to exercise the Android regression cases.
The separate broader browser checklist remains in `BROWSER_ACCEPTANCE.md`.
+75
View File
@@ -0,0 +1,75 @@
# Validation — Fileworkbench 0.2.3
Date: 2026-09-24. **Complete diagnostic source release; no verified APK supplied.**
## Scope and provenance
The shipped 0.2.2 source ZIP was extracted into a fresh working directory. The
previously supplied SAF-D1 diagnostic changes were applied there and incorporated
into this complete project. The current source retains the earlier PDF lifecycle
and nullable-cursor-result corrections. Gradle is pinned to 8.14.5; the daemon,
module toolchains and bytecode targets select 17. Root plugin aliases are distinct.
The user still reports the folder-listing failure. No subsequent device trace has
established its cause. **This release instruments that failure; it does not claim
to fix it.** A versioned footer identifies the diagnostic source, and a genuinely
null query cursor has the distinct `[SAF-D1/NULL_CURSOR]` message.
## Executed checks for this source
| Check | Result | Boundary |
|---|---|---|
| Common core + JVM adapter suite | **112/112 passed** | Local Kotlin compiler; not Gradle/Android compilation |
| Actual production listing block + diagnostic harness | **16/16 passed** | Real shared models and Android test doubles; no Binder/real provider/permission validation |
| Build-configuration invariants | **18 passed** | Static pins/aliases/hashes/markers, not Gradle plugin resolution |
| Build-guard regression cases | **8/8 passed** | Accepts the baseline with local SDK/IDE files; rejects wrong Gradle/JVM, duplicate plugin aliases/IDs and stale shell/Windows bootstrap tags |
| Kotlin PSI syntax | **44 files, zero parse errors** | Includes Kotlin source and Gradle Kotlin scripts; no Android/Compose/Gradle type resolution |
| Existing project/XML/security invariants | Passed | Permission-free main manifest, isolated PDF service, read-only picker boundaries, portable core, prior PDF regression guard |
| Shell syntax | Passed | Bash/sh syntax only; PowerShell not executed |
| Android build attempt | **Blocked before Gradle/Android compilation** | Wrapper fetch: `curl: (6) Could not resolve host: raw.githubusercontent.com` |
| Device/emulator tests | Not executed here | The test sources are retained; no device is attached |
| iOS build/tests | Not executed here | Core architecture retained; no iOS shell added |
The local host has **OpenJDK 21.0.11 and kotlinc 1.9.0**, not JDK 17 or the
project's Kotlin 2.2.21 toolchain. The host harnesses now emit JVM-17 bytecode but
run on that local JVM 21. **Do not interpret their results as a JDK-17 Android
build.** The actual Gradle configuration requests an installed JDK 17; no JDK-21
fallback has been added to the project. No Android SDK or downloaded wrapper is
available in this authoring runtime.
The full Android command was attempted using the newly pinned launcher. Its
bootstrap failed before the settings guards or compiler could run. No APK or
successful Gradle sync is claimed. The network-dependent bootstrap fallback and
Windows PowerShell bootstrap remain unexecuted here.
Raw evidence for this release is under `validation/0.2.3/`. Previous results retain
their versioned paths; the old report is preserved as `VALIDATION-0.2.2.md`. Old
`BUILD_ATTEMPT.txt`/test outputs elsewhere in docs are historical, not new results.
## Packaging checks
The full archive is assembled from this source tree, excluding `out`, build caches,
local IDE/SDK state, keystores and generated test JARs. Prior source modules,
resources, fixtures, licence, documentation and examples remain present. It ships
no APK, wrapper JAR, JDK or Android SDK. The first wrapper invocation bootstraps the
verified external JAR. See `SOURCE-MANIFEST.sha256` for per-file integrity hashes
and `validation/0.2.3/archive-checks.txt` for extraction/integrity checks.
## Reproduce host checks
```sh
python3 scripts/check-project.py
python3 scripts/test-build-config.py
bash scripts/test-core.sh
python3 scripts/test-folder-listing.py
bash scripts/check-kotlin-syntax.sh
```
## Next local acceptance step
Follow `BUILD.md`: fresh extraction, installed JDK 17, wrapper Gradle 8.14.5,
SDK 36, then tests/assembly/lint. Install the resulting APK with the existing
signing identity, confirm **Fileworkbench 0.2.3 · Browser diagnostics SAF-D1** in
Browse → Locations, and reproduce the folder problem. Preserve the existing app
data. Capture the full new error instead of inferring permission or provider
failure from the earlier ambiguous wording.