Skip to content

SimpleStorage 3.0.0-beta01: StorageFile API, Android 17 (API 37), minSdk 26#156

Merged
anggrayudi merged 11 commits into
masterfrom
release/3.0.0
Jul 11, 2026
Merged

SimpleStorage 3.0.0-beta01: StorageFile API, Android 17 (API 37), minSdk 26#156
anggrayudi merged 11 commits into
masterfrom
release/3.0.0

Conversation

@anggrayudi

@anggrayudi anggrayudi commented Jul 11, 2026

Copy link
Copy Markdown
Owner

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.

// The whole pitch in three lines:
val result = file.copyTo(targetFolder) {
  onConflict { ConflictResolution.REPLACE }
  onProgress { progressBar.progress = it.percent.toInt() }
}

Platform

  • AGP 9.2.1 + Gradle 9.4.1 (AGP 9 built-in Kotlin; KGP plugin and Jetifier removed)
  • compileSdk/targetSdk 37 — Android 17 has zero storage/SAF/MediaStore behavior changes (targeting-37, all apps)
  • minSdk 23 → 26 — every pre-O branch removed

New API (additive)

  • StorageFile — sealed abstraction over DocumentFile/MediaFile/java.io.File with a single factory surface, context held internally, null instead of "" for unresolvable paths. StoragePath replaces FileFullPath + "simple path" strings.
  • Transfer vocabularyTransferEvent/TransferResult<T>/TransferSpec replace the five parallel result hierarchies; conflicts are a suspend lambda (ConflictResolver).
  • OperationscopyTo/moveTo/zipTo/unzipTo/deleteRecursively as main-safe one-shot suspend functions plus *AsFlow variants, built on the proven v2 engine via adapters.
  • StorageAccessManager — contracts-only successor to SimpleStorageHelper with suspend ensureAccess/pickFolder/pickFiles/createFile, and pickMedia() for the system Photo Picker. Compose gains rememberLauncherForMediaPicker().

Deprecations

SimpleStorage, SimpleStorageHelper, and the four picker/access callback interfaces are @Deprecated with ReplaceWith. 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:

  • Conflict-resolution bridge is sound: a suspending resolver (withContext(Main) { delay(300) }) completed in ~305 ms with no deadlock — the exact path that could not be tested under Robolectric.
  • MediaStore backend verified byte-for-byte (insert → wrap → copy → checksum).
  • zip/unzip round-trip, cancellation, progress events, and the on-device regression of the 2.x recursive-search duplication fix all pass.
  • One real library bug found and fixed (449d90e): folder merges with ≥1 resolved file conflict copied correctly on disk but reported a false-negative failure, because copyFolderTo's finalize() gated its terminal event on a stale conflictedFiles list. One-line fix + regression test.

Known findings documented for RC: single-file SKIP resolution deterministically surfaces as Failure(UNKNOWN_IO_ERROR) (needs an API-shape decision for a proper "skipped" result), and the same finalize pattern exists unverified in the multi-file engine (unreachable from v3 API).

Release plan

3.0.0-beta01 will 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

anggrayudi.hardiannico and others added 11 commits July 11, 2026 20:36
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>
@anggrayudi anggrayudi changed the title SimpleStorage 3.0.0-alpha01: StorageFile API, Android 17 (API 37), minSdk 26 SimpleStorage 3.0.0-beta01: StorageFile API, Android 17 (API 37), minSdk 26 Jul 11, 2026
@anggrayudi
anggrayudi merged commit bd51ae4 into master Jul 11, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant