Commit Graph

20 Commits

Author SHA1 Message Date
SoftwareApp cd0fdff31b Unpack .tar.bz2 and .tar.xz, which only half-extracted
formatOf matched .tar.gz/.tgz as a combined format and then fell through to the bare .bz2 and .xz wrappers, so a .tar.bz2 was read as one compressed file: extracting it decompressed a single layer and left a .tar to extract again, while the same archive named .tar.gz unpacked in one step. The store listing advertised "extract ZIP, TAR, TAR.GZ, GZ, BZ2, XZ and 7z", which reads as though they all behave alike.

Found by extracting one archive of every supported format through the UI on the API 35 AVD rather than trusting the suite. A real z4.tar.bz2 left z4/z4.tar - raw ustar headers and all - beside z3.tar.gz fully unpacked to z3/a.txt and z3/sub/b.txt.

TAR_BZIP2 and TAR_XZ now sit alongside TAR_GZIP, detection consumes the whole tar suffix (.tar.bz2/.tbz2, .tar.xz/.txz) before the bare compressor, and the engine gains the two readArchive branches. The bare spellings still mean a single compressed file.

The suite had a gap that let this hide: only ZIP, GZIP, XZ and 7z had round-trip tests, so neither plain TAR nor TAR_GZIP did, and there was nothing for the asymmetry to contradict. All four tar spellings have one now, built in memory with the same library that reads them. 212 JVM tests, lint clean.

Also records the format sweep in EDGE_CASE_TESTING and re-measures the release artefacts (aab 4 838 235 / sha256 e1d23130..., apk 2 155 455 / sha256 37fb46cb...). The APK byte count happened to land on the same figure as the previous build, so the new literals were confirmed present in the shipped dex rather than trusted to the size.
2026-10-15 12:14:39 +02:00
SoftwareApp bb1187881b Make the Android 13+ pass, and fix what it found
The phone runs Android 12, so the questions that matter most for a release were exactly the ones it could not answer. Installed an API 35 AVD (sdkmanager was already present and dl.google.com reachable) rather than waiting for a public test track to report back. It found two user-visible defects, neither of which any test could have caught.

On Android 13+ the app asked for no runtime permission at all, so the progress notification was dead. StorageAccessPolicy.missingPermissions returned an empty list from API 33 on the reasoning that "all files access" is a special permission and browsing therefore needs no runtime grant — true as far as browsing goes, but the early return also dropped POST_NOTIFICATIONS, and nothing else in the app requested it. startForeground still ran, so nothing crashed and all 203 tests passed, while dumpsys showed granted=false and importance=NONE. Settings could not fix that either, because Android will not let a user grant a permission the app has never asked for. The notification grant is now requested alongside the storage grant, which is the only moment Android accepts it.

The four READ_MEDIA_* declarations were in the same position, and the manifest comment claiming they back thumbnails and viewers on Android 13+ was wrong. Coil loads both from a real filesystem path — the code comments already say "all-files mode only" — and the one MediaStore use is a best-effort delete request that "all files access" already covers. They were never requested, so they were never granted, so nothing could have depended on them. Removed, which also settles the checklist rule that declaring an unused permission is itself a policy violation.

The selection bar drew its last three actions off the screen on a phone. With one archive selected it holds ten icon buttons, ~480 dp at the 48 dp minimum touch size against the 411 dp a phone has, and BottomAppBar is a plain Row that does not scroll. Compose laid the surplus out past the right edge: measured on a 1080 px screen the row ran Copy(167) to Compress(923-980, itself squeezed to 57 px) and stopped, so Extract here, Select all and Clear selection were unreachable, while the same selection in a 2400 px landscape window showed all ten. The logic was never wrong — ArchiveFormats.isExtractable had judged the .zip correctly — the app just looked like it could not extract. The action row now scrolls, verified by swiping it in portrait and seeing all ten appear.

Verified on the AVD rather than only built: the notification dialog now appears and importance goes NONE -> DEFAULT, a real 220 MB copy posts notification 1001 on channel file-operations with ONGOING_EVENT|FOREGROUND_SERVICE and tears it down plus stops the service when done, edge-to-edge is correct under API 35s enforcement, predictive back returns from a folder and keeps the activity resumed, and Compress to ZIP produced an archive the host unzip read back with the right relative paths and contents. 204 JVM tests, lint clean, release artefacts re-measured.
2026-10-15 11:39:48 +02:00
SoftwareApp 5786b618b7 Bring the release documents back in line with what the app now does
The checklist still recorded the first artefacts (aab 4 682 814, apk 2 081 955); re-measured at 4 836 490 and 2 155 563 and noted why they moved, that the permission set did not change, and that a stale hash is worse than no hash because it looks checked.

PRIVACY.md said the app keeps no copies of your files and deletes what you select directly. Both stopped being true when deletes started staging into the trash: the item is moved, not erased, and stays on the volume for 24 hours. A privacy policy that misdescribes deletion is not a small thing on a paid listing, so the deleting and retention sections now describe the trash, the index it keeps, and the one caveat that clearing app storage forgets the trash without emptying it.

Also flags that the screenshots show none of search, undo, trash or receiving a share.
2026-10-15 10:44:57 +02:00
SoftwareApp 667a85f80c Remember what is in the trash, and add a screen to restore from it
The staged-delete work left a hole. The undo handle lived in BrowserUiState, so a delete from an earlier session was unreachable even though the files were sitting on the volume; and because nothing on disk recorded which folder an item came from, not even the user could put one back by hand with confidence. Adding a Trash screen alone would not have fixed it: the recovery data did not survive the process. So the origin is now written at delete time.

data/ gains its first persistence: a SQLite index (staged path, original path, name, type, size, timestamp). SQLite rather than Room because an annotation processor is a large addition to this build for one table and three queries, and behind a TrashIndex port so domain/Trash keeps no Context while the tests drive it with a map and the real one is still tested against real SQLite. New Trash entry in the overflow menu lists batches and restores a whole one.

The index is an index, not the truth: the staged files are the truth, and the two can disagree if a user empties the folder by hand or pulls a card. Reconciled both ways - restorableFrom drops entries whose file has gone, purgeExpired forgets entries whose folder has gone and sweeps folders no entry mentions - and rows are written after the move, so a crash leaves an unindexed folder (harmless, swept) rather than an entry that could never restore. Verified on hardware: delete, force-stop, relaunch, restore.

Also corrects the docs where they claimed input swipe x y x y 900 is delivered as a plain tap on MIUI. It does open the selection bar - it was driven that way - but it has also landed as a tap, so it is flaky rather than impossible.
2026-10-15 10:25:37 +02:00
SoftwareApp cc085b43de Receive shares and serve file requests, so other apps can reach this one
The manifest had nothing but a LAUNCHER filter, so no other app could hand Plain Files anything. It now registers for ACTION_SEND and SEND_MULTIPLE (a bar over the browser: navigate to the folder, then Save here) and for GET_CONTENT and OPEN_DOCUMENT, where tapping a file hands it back through the existing FileProvider.

Both reuse the browser instead of a second screen, because both are navigate-to-a-folder with a different verb at the end. data/ gains one port, IncomingSource, to keep a ContentResolver out of the ViewModel the way DeleteRequestFactory keeps one out of the delete path. Shared files are named from the sender OpenableColumns, allocated by the same NameAllocator a paste uses, and deleted again if they cannot be read rather than left as zero-byte stubs. While providing, a tap on a file chooses it but a tap on a folder still navigates, or the user could never reach the file.

ACTION_VIEW is deliberately not registered: the app already hands files out through the system chooser, and registering as a viewer would offer Plain Files back to itself. No new permissions - the merged release manifest still declares no INTERNET.
2026-10-15 09:18:10 +02:00
SoftwareApp ba92cf392d Support 7z extraction, and fix .xz extraction which would have crashed at runtime
Commons Compress declares its LZMA/LZMA2 support against org.tukaani:xz as an *optional* dependency, so nothing pulled it in transitively and nothing failed at build time. The app nevertheless advertised .xz as extractable, so the first user to open one would have got NoClassDefFoundError: org/tukaani/xz/FilterOptions. XZCompressorInputStream references org/tukaani/xz/XZInputStream directly. The library is now declared, and a round-trip test covers the path that was silently missing.

That same library is what 7z needs, and Commons Compress already ships a pure-Java 7z reader, so 7z extraction costs no native code and no further dependency. The docs had assumed the format was off limits and said so in three places, including the customer-facing store copy.

7z is the only format here that cannot arrive through a stream: its header sits at the end of the file, so the reader has to seek. That is now a capability the backend advertises, StorageSource.openSeekable: the all-files backend answers with a real FileChannel, the SAF fallback with null, so folder mode reports needs-a-seekable-file instead of failing obscurely. Entry names go through the same ArchivePath.safeRelativePath zip-slip guard. 7z creation stays deferred.
2026-10-15 08:47:37 +02:00
SoftwareApp a461be2a66 Docs: M10 complete, dual-pane no longer deferred, correct the 7z rationale
The last commit shipped the screenshots and the two-pane layout but left the roadmap and README saying otherwise. Also corrects the stated reason 7z is unsupported: RAR really does need native code, but SevenZFile is already inside the Commons Compress jar this app ships.
2026-10-15 00:25:09 +02:00
SoftwareApp 55dc2293b2 Add a two-pane layout for large screens, and re-shoot the tablet sets
A window at or above Material expanded breakpoint (840dp) now splits into a master pane and a detail pane: tapping a row highlights it and the second pane shows a folder children, or a preview plus the metadata for a file. Opening moves to a double tap on wide windows only; phones (~411dp) and the 7-inch in portrait (~600dp) keep their layout and their tap behaviour.

Found while taking the tablet screenshots: the app had no large-screen layout at all, so on a 2560x1600 canvas every row was 2560px wide with its content stranded in the left ~240px.

The tablet sets are re-shot on this build. That also caught a blank capture (7-inch 01-roots) that had already been committed: a headless AVD draws its system bars before Compose composes its first frame, so the file was not obviously empty. make-store-screenshots.sh now measures each capture app area and refuses to normalise one that is blank.
2026-10-15 00:18:38 +02:00
SoftwareApp 67c2cd61db Screenshots: demo-library generator, per-form-factor targets, listing notes 2026-10-14 17:52:40 +02:00
SoftwareApp d845a7646e M10: store assets - Plain Files naming, generated icon/feature graphic, listing copy 2026-10-14 17:47:02 +02:00
SoftwareApp e3de9a2fc0 M9: monetisation decision - one-time paid, no billing code, verified in release APK 2026-10-14 17:40:29 +02:00
SoftwareApp bf909b3485 M8: localisation groundwork - resource the file-kind labels, pseudo-locale check 2026-10-14 17:12:02 +02:00
SoftwareApp a79b7eb58e M7: on-device validation - fix usage-screen latency and first-run copy 2026-10-14 17:05:03 +02:00
SoftwareApp e2cfb5d9ca M6: release signing, privacy policy and Play release checklist 2026-10-14 16:44:58 +02:00
SoftwareApp 3e39fc9b86 M5: storage usage, ZIP/TAR/GZ archives with zip-slip guard, thumbnails, viewers + 26 tests 2026-10-14 16:29:08 +02:00
SoftwareApp 2c86f3dc67 M4: browser UI (navigation, sort, selection, dialogs, permission prompts, platform delete) 2026-10-14 16:07:08 +02:00
SoftwareApp 993cac6e5b M3: operations engine (copy/move/delete, progress, cancellation, conflicts) + foreground service 2026-10-14 15:40:48 +02:00
SoftwareApp 0863b37d70 M2: storage layer (all-files + SAF backends, volumes, access policy) + 32 JVM tests 2026-10-14 15:17:51 +02:00
SoftwareApp 6d4c2c3f87 M1: pure-Kotlin core engine (paths, classification, sorting, search, transfers) + 44 JVM tests 2026-10-14 15:05:14 +02:00
SoftwareApp adfd95fc38 M0: project scaffold (Gradle 9.3.1 / AGP 9.1.0, Compose shell, storage permissions) 2026-10-14 14:55:10 +02:00