SimpleStorage 3.0.0-beta01: StorageFile API, Android 17 (API 37), minSdk 26#156
Merged
Conversation
compileSdk 37 requires AGP 9.1.1+, and the latest stable AGP 9.2.1 requires Gradle 9.4.1. AGP 9 ships built-in Kotlin support, so the org.jetbrains.kotlin.android plugin is removed from all modules (the plugin now fails the build by design). Jetifier is dropped as well: AGP 9 removed it and all dependencies are already AndroidX. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Android 17 (API 37) is stable since June 2026. Its behavior changes contain nothing that affects storage, SAF, MediaStore, URI permissions, or window insets, so no code changes are required for the bump. Noted for later: Android 18 will restrict implicit URI grants on ACTION_SEND, which touches the file-receiver path (checkIfFileReceived). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Removes every pre-O code path: - EXTRA_INITIAL_URI is now set unconditionally in picker intents (previously guarded by SDK_INT >= 26), and the "only takes effect on API 26+" KDoc notes are gone since it always applies. - DocumentsContract.moveDocument() fast path no longer checks for API 24+ in moveFolderTo/moveFileTo. - getSdCardRootAccessIntent() drops @RequiresApi(N); the dead "SDK_INT < N" arm in RequestStorageAccessContract disappears. - Context.dataDirectory maps straight to Context.getDataDir(). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
StorageFile is a sealed abstraction over the three file worlds (DocumentFile/SAF, MediaFile/MediaStore, java.io.File) with a single factory surface (from(uri)/from(file)/fromPath/fromPublicDirectory), context held internally, and null — not empty string — for paths that cannot be resolved. Escape hatches (asDocumentFile/asMediaFile/ asRawFile) keep interop with the v2 API. StoragePath is a context-free value type replacing FileFullPath and "simple path" strings. The transfer package introduces one shared vocabulary for all long-running operations, replacing the five parallel v2 result hierarchies: TransferEvent (PhaseChanged/Progress/Completed), TransferResult<T> (Success/Failure with cause and partial stats), TransferSpec (DSL for updateInterval, space check, conflict and progress callbacks), and ConflictResolver as a suspend fun interface — no more abstract callback classes carrying a CoroutineScope. Covered by StorageFileTest (Robolectric, raw-file backend). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Every operation ships in two forms built on the proven v2 engine: - One-shot suspend functions — copyTo, moveTo, zipTo, unzipTo, deleteRecursively — that are main-safe, return TransferResult, and accept a TransferSpec lambda for conflict/progress/options. - Flow forms (copyToAsFlow, moveToAsFlow, zipToAsFlow, unzipToAsFlow, search) emitting the unified TransferEvent stream. The v2 conflict callback classes are bridged to the new suspend ConflictResolver via adapters, so no public API exposes CoroutineScope or GlobalScope anymore. v2's per-interval writeSpeed is converted to bytesPerSecond. All five v2 result hierarchies map onto TransferEvent/TransferResult, including cause and partial stats. Covered by StorageFileTransferTest: single-file copy/move, recursive folder copy, zip/unzip round-trip, recursive delete, search, and the invalid-target failure path. Conflict resolution requires a live main looper, so it is left to instrumentation tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
StorageAccessManager is the contracts-only successor to SimpleStorageHelper for the Views world: no request codes, no onActivityResult forwarding, no onSaveInstanceState plumbing, and no baked-in dialogs. All interactions are suspend functions that resume when the user answers: - ensureAccess(path) checks the URI grant, walks the user through SAF when missing (including the API 26-29 runtime-permission dance), and returns Granted/WrongRootSelected/CanceledByUser/PermissionDenied. - pickFolder/pickFiles/createFile wrap the existing contracts and return their existing result types. - pickMedia opens the system Photo Picker (PickVisualMedia) with no permission required, returning StorageFiles. storage-compose gains rememberLauncherForMediaPicker() for the same Photo Picker flow in Compose. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
SimpleStorage, SimpleStorageHelper, and the four picker/access callback interfaces are now @deprecated with pointers to their v3 replacements. They keep working through 3.x for gradual migration; removal is planned for the first 4.0 development cycle. The DocumentFile extension operations are intentionally NOT deprecated yet — they act as the compatibility layer while v3's StorageFile API matures. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- MIGRATION.md maps every 2.x API to its v3 replacement, documents the TransferSpec options, the conflict-resolver change, and the deprecation timeline (alpha: access layer; rc: DocumentFile ops; 4.0: removal). - README gains a "Version 3 (alpha)" section linking to the guide. - Library modules compile with -Xexplicit-api=warning so missing visibility/return-type declarations surface during the 3.x cycle; strict mode comes once the 2.x surface is removed. - VERSION_NAME=3.0.0-alpha01. Not published — pending review. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds storage/src/androidTest infrastructure (AndroidJUnitRunner, androidx.test deps) and instrumented tests for V3_TEST_CASES.md Groups 1-6, covering StorageFile factories, one-shot transfers, the suspend->callback conflict resolver bridge (the main goal - this path deadlocks under Robolectric since Dispatchers.Main never pumps there, so it had never actually been exercised), MediaStore transfers, Flow event streams/cancellation, and recursive search. Also drives Group 7 (sample app install/launch, legacy SAF folder picker) via adb/uiautomator and records all 61 test case results in V3_TEST_CASES.md, including a confirmed library bug found by TC-24 (see the following commit) and several "document the behavior" findings (TC-12 empty-folder handling, TC-15 progress-event cadence, TC-22 SKIP result shape, TC-41 cancellation leftover state). All 21 instrumented tests pass on emulator-5554 (Pixel 10 API 37). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
copyFolderTo()'s finalize() lambda gated sending the terminal SingleFolderResult.Completed event on conflictedFiles.isEmpty(). That list is never cleared after its filtered contents (solutions) are resolved and copied, so the second, unconditional call to finalize() after conflict processing always saw a stale non-empty list and skipped sending Completed - the flow then closed with no terminal event at all, even though every file, including the conflicted ones, had already been copied successfully on disk. Through the v3 wrapper this surfaced as copyTo()/moveTo() returning Failure(UNKNOWN_IO_ERROR, "Transfer finished without a terminal event") for any folder merge with at least one resolved file conflict - a false negative found by the new ConflictResolutionTest.tc24_folderMerge instrumented test (previously impossible to catch: this path deadlocks under Robolectric). moveFolderTo() shares this engine and was equally affected. Fix: clear conflictedFiles once its filtered copy has been derived, so the second finalize() call correctly recognizes that all conflicts are resolved. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All 23 on-device test cases in V3_TEST_CASES.md pass on an API 37 emulator, including the previously unexercised conflict-resolution bridge (no deadlock with a suspending resolver) and the MediaStore backend. The folder-merge false-negative found by that pass is fixed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
First beta of the v3 redesign: one file abstraction, one operation vocabulary, one access entry point — while the entire 2.x API keeps compiling for gradual migration. See MIGRATION.md for the full 2.x → 3.0 mapping.
Platform
New API (additive)
StorageFile— sealed abstraction overDocumentFile/MediaFile/java.io.Filewith a single factory surface, context held internally,nullinstead of""for unresolvable paths.StoragePathreplacesFileFullPath+ "simple path" strings.TransferEvent/TransferResult<T>/TransferSpecreplace the five parallel result hierarchies; conflicts are asuspendlambda (ConflictResolver).copyTo/moveTo/zipTo/unzipTo/deleteRecursivelyas main-safe one-shot suspend functions plus*AsFlowvariants, built on the proven v2 engine via adapters.StorageAccessManager— contracts-only successor toSimpleStorageHelperwith suspendensureAccess/pickFolder/pickFiles/createFile, andpickMedia()for the system Photo Picker. Compose gainsrememberLauncherForMediaPicker().Deprecations
SimpleStorage,SimpleStorageHelper, and the four picker/access callback interfaces are@DeprecatedwithReplaceWith. Timeline in MIGRATION.md: DocumentFile op extensions get deprecated at rc; removal lands in 4.0.On-device verification (API 37 emulator)
All 23 test cases in V3_TEST_CASES.md PASS — 21 instrumented tests (
storage/src/androidTest/) + 2 adb-driven sample smoke tests:withContext(Main) { delay(300) }) completed in ~305 ms with no deadlock — the exact path that could not be tested under Robolectric.449d90e): folder merges with ≥1 resolved file conflict copied correctly on disk but reported a false-negative failure, becausecopyFolderTo'sfinalize()gated its terminal event on a staleconflictedFileslist. One-line fix + regression test.Known findings documented for RC: single-file
SKIPresolution deterministically surfaces asFailure(UNKNOWN_IO_ERROR)(needs an API-shape decision for a proper "skipped" result), and the samefinalizepattern exists unverified in the multi-file engine (unreachable from v3 API).Release plan
3.0.0-beta01will be published to Maven Central and left to soak for ~1 month before the stable 3.0.0 release (RC phase adds the DocumentFile-ops deprecation + compat layer).🤖 Generated with Claude Code