Skip to content

Commit ffc4725

Browse files
anggrayudi.hardiannicoclaude
andcommitted
Prepare 3.0.0-alpha01: migration guide, explicit API warnings
- 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>
1 parent bd20514 commit ffc4725

5 files changed

Lines changed: 133 additions & 3 deletions

File tree

MIGRATION.md

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
# Migrating from SimpleStorage 2.x to 3.0
2+
3+
Version 3.0 introduces a single abstraction over Android's three file worlds and one vocabulary
4+
for every long-running operation. The 2.x API keeps compiling throughout the 3.x cycle (parts of
5+
it as `@Deprecated`), so you can migrate incrementally.
6+
7+
Platform changes:
8+
9+
| | 2.x | 3.0 |
10+
|---|---|---|
11+
| minSdk | 23 | **26** |
12+
| compileSdk / targetSdk | 36 | **37** (Android 17) |
13+
| AGP / Gradle (to build this repo) | 8.13 / 8.14 | 9.2 / 9.4 |
14+
15+
## The one-minute overview
16+
17+
```kotlin
18+
// 2.x
19+
ioScope.launch {
20+
file.copyFileTo(context, targetFolder,
21+
onConflict = object : SingleFileConflictCallback<DocumentFile>(uiScope) {
22+
override fun onFileConflict(destFile: DocumentFile, action: FileConflictAction) {
23+
action.confirmResolution(ConflictResolution.REPLACE)
24+
}
25+
}
26+
).collect { result -> when (result) { /* 8 branches */ } }
27+
}
28+
29+
// 3.0 — from any thread, no scope juggling
30+
val result = file.copyTo(targetFolder) {
31+
onConflict { ConflictResolution.REPLACE }
32+
onProgress { progressBar.progress = it.percent.toInt() }
33+
}
34+
when (result) {
35+
is TransferResult.Success -> toast("Copied ${result.result.name}")
36+
is TransferResult.Failure -> log(result.errorCode, result.cause)
37+
}
38+
```
39+
40+
## API mapping
41+
42+
### Obtaining files
43+
44+
| 2.x | 3.0 |
45+
|---|---|
46+
| `DocumentFileCompat.fromUri(context, uri)` | `StorageFile.from(context, uri)` |
47+
| `DocumentFileCompat.fromFile(context, file)` | `StorageFile.from(context, file)` |
48+
| `DocumentFileCompat.fromFullPath(context, path)` | `StorageFile.fromPath(context, absolutePath)` |
49+
| `DocumentFileCompat.fromSimplePath(context, storageId, basePath)` | `StorageFile.fromPath(context, StoragePath(storageId, basePath))` |
50+
| `DocumentFileCompat.fromPublicFolder(context, type)` | `StorageFile.fromPublicDirectory(context, type)` |
51+
| `MediaStoreCompat.fromMediaId(context, ...)``MediaFile` | `StorageFile.from(context, mediaUri)` |
52+
| `FileFullPath(context, storageId, basePath)` | `StoragePath(storageId, basePath)` — no `Context` needed |
53+
54+
`StorageFile` holds its `Context` internally: none of its members ask for one. `absolutePath`
55+
returns `null` (not `""`) when a physical path cannot be resolved. Escape hatches:
56+
`asDocumentFile()`, `asMediaFile()`, `asRawFile()`.
57+
58+
### File operations
59+
60+
| 2.x | 3.0 one-shot | 3.0 Flow |
61+
|---|---|---|
62+
| `DocumentFile.copyFileTo(context, target, …)` | `StorageFile.copyTo(target) { }` | `copyToAsFlow(target)` |
63+
| `DocumentFile.moveFileTo(context, target, …)` | `StorageFile.moveTo(target) { }` | `moveToAsFlow(target)` |
64+
| `DocumentFile.copyFolderTo/moveFolderTo(…)` | same `copyTo`/`moveTo` — folders are detected | same |
65+
| `List<DocumentFile>.compressToZip(context, zip, …)` | `List<StorageFile>.zipTo(zipFile) { }` | `zipToAsFlow(zipFile)` |
66+
| `DocumentFile.decompressZip(context, folder, …)` | `StorageFile.unzipTo(folder) { }` | `unzipToAsFlow(folder)` |
67+
| `DocumentFile.deleteRecursively(context)` | `StorageFile.deleteRecursively()` (suspend) ||
68+
| `DocumentFile.search(…)` || `StorageFile.search(…)` |
69+
70+
Options that used to be positional parameters (`updateInterval`, `skipEmptyFiles`,
71+
`fileDescription`, space checking) now live in the `TransferSpec` lambda.
72+
73+
### Results
74+
75+
| 2.x | 3.0 |
76+
|---|---|
77+
| `SingleFileResult` / `SingleFolderResult` / `MultipleFilesResult` / `ZipCompressionResult` / `ZipDecompressionResult` | `TransferEvent` (`PhaseChanged`, `Progress`, `Completed`) |
78+
| `...Result.Completed(result: Any)` + casting | `TransferResult.Success<StorageFile>` — typed |
79+
| `...Result.Error(errorCode, message, cause)` | `TransferResult.Failure(errorCode, message, cause, partialStats)` |
80+
| `writeSpeed: Int` (bytes per update interval) | `Progress.bytesPerSecond: Long` |
81+
82+
### Conflict handling
83+
84+
| 2.x | 3.0 |
85+
|---|---|
86+
| `object : SingleFileConflictCallback<DocumentFile>(uiScope) { override fun onFileConflict(...) { action.confirmResolution(...) } }` | `onConflict { conflict -> ConflictResolution.REPLACE }` |
87+
| `SingleFolderConflictCallback.onParentConflict/onContentConflict` | same single resolver — receives `Conflict.TargetFolder(canMerge)` first, then a `Conflict.TargetFile` per conflicting child |
88+
89+
The resolver is a `suspend` function: show a dialog with
90+
`withContext(Dispatchers.Main) { … }` and simply return the answer. There is no `uiScope`, no
91+
`GlobalScope` default, and no zombie-thread hazard.
92+
93+
### Storage access & pickers (Views)
94+
95+
| 2.x (`@Deprecated`) | 3.0 |
96+
|---|---|
97+
| `SimpleStorageHelper(activity)` + 4 callbacks + `onSaveInstanceState` + `onActivityResult` | `StorageAccessManager(activity)` — suspend functions, nothing to forward |
98+
| `helper.requestStorageAccess()` + `onStorageAccessGranted` | `val access = manager.ensureAccess(StoragePath.primary("Documents"))` |
99+
| `helper.openFolderPicker()` + `onFolderSelected` | `val result = manager.pickFolder()` |
100+
| `helper.openFilePicker()` + `onFileSelected` | `val result = manager.pickFiles(allowMultiple = true)` |
101+
| `helper.createFile(mimeType)` + `onFileCreated` | `val result = manager.createFile(mimeType)` |
102+
|| `manager.pickMedia()` — system Photo Picker, no permission needed |
103+
104+
`StorageAccessManager` has no built-in dialogs: `ensureAccess` returns `WrongRootSelected` and you
105+
decide how to explain and retry. If you want ready-made dialogs, keep using `SimpleStorageHelper`
106+
until you migrate.
107+
108+
### Compose
109+
110+
Existing `rememberLauncherFor*` composables are unchanged. New in 3.0:
111+
`rememberLauncherForMediaPicker(maxItems) { files -> … }` for the system Photo Picker.
112+
113+
## Deprecation timeline
114+
115+
| Phase | What happens |
116+
|---|---|
117+
| 3.0.0-alpha | `SimpleStorage`, `SimpleStorageHelper`, and the picker/access callback interfaces are `@Deprecated` |
118+
| 3.0.0-rc | `DocumentFile`/`MediaFile` operation extensions become `@Deprecated`, delegating to the v3 engine |
119+
| 4.0 | Deprecated 2.x API is removed |

README.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,15 @@ allprojects {
6666
}
6767
```
6868

69+
### Version 3 (alpha)
70+
71+
Version `3.0.0-alpha01` introduces a redesigned API: one [`StorageFile`](storage/src/main/java/com/anggrayudi/storage/StorageFile.kt)
72+
abstraction over `DocumentFile`/`MediaFile`/`java.io.File`, one-shot suspend operations
73+
(`copyTo`, `moveTo`, `zipTo`, `unzipTo`) with a unified `TransferResult`, suspend-lambda conflict
74+
resolution, and [`StorageAccessManager`](storage/src/main/java/com/anggrayudi/storage/access/StorageAccessManager.kt)
75+
replacing `SimpleStorageHelper`. It targets Android 17 (API 37) with minSdk 26. The 2.x API keeps
76+
working during the 3.x cycle. Read the [migration guide](MIGRATION.md).
77+
6978
### Java Compatibility
7079

7180
Simple Storage is built in Kotlin. Follow this [documentation](JAVA_COMPATIBILITY.md) to use it in your Java project.

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ kotlin.code.style=official
2020
org.jetbrains.dokka.experimental.gradle.pluginMode=V2EnabledWithHelpers
2121
# For publishing:
2222
GROUP=com.anggrayudi
23-
VERSION_NAME=2.4.0-SNAPSHOT
23+
VERSION_NAME=3.0.0-alpha01
2424
POM_NAME=storage
2525
POM_DESCRIPTION=Simplify Android Storage Access Framework for file management across API levels.
2626
POM_INCEPTION_YEAR=2020

storage-compose/build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ android {
3636
compilerOptions {
3737
jvmTarget = JvmTarget.JVM_11
3838
// Support @JvmDefault
39-
freeCompilerArgs = listOf("-Xjvm-default=all", "-opt-in=kotlin.RequiresOptIn")
39+
freeCompilerArgs =
40+
listOf("-Xjvm-default=all", "-opt-in=kotlin.RequiresOptIn", "-Xexplicit-api=warning")
4041
}
4142
}
4243
}

storage/build.gradle.kts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ android {
3737
compilerOptions {
3838
jvmTarget = JvmTarget.JVM_11
3939
// Support @JvmDefault
40-
freeCompilerArgs = listOf("-Xjvm-default=all", "-opt-in=kotlin.RequiresOptIn")
40+
freeCompilerArgs =
41+
listOf("-Xjvm-default=all", "-opt-in=kotlin.RequiresOptIn", "-Xexplicit-api=warning")
4142
}
4243
}
4344
}

0 commit comments

Comments
 (0)