Skip to content

Commit d48d278

Browse files
committed
Save files to Downloads via MediaStore on Android 10+ (fix #85)
1 parent b149b39 commit d48d278

File tree

1 file changed

+48
-5
lines changed

1 file changed

+48
-5
lines changed

app/src/main/java/org/fairscan/app/ui/screens/export/ExportViewModel.kt

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,14 @@
1414
*/
1515
package org.fairscan.app.ui.screens.export
1616

17+
import android.content.ContentValues
1718
import android.content.Context
1819
import android.media.MediaScannerConnection
1920
import android.net.Uri
21+
import android.os.Build
22+
import android.os.Environment
23+
import android.provider.MediaStore
24+
import androidx.annotation.RequiresApi
2025
import androidx.core.net.toUri
2126
import androidx.documentfile.provider.DocumentFile
2227
import androidx.lifecycle.ViewModel
@@ -39,6 +44,7 @@ import org.fairscan.app.data.ImageRepository
3944
import org.fairscan.app.ui.screens.settings.ExportFormat
4045
import java.io.File
4146
import java.io.FileInputStream
47+
import java.io.IOException
4248
import kotlin.coroutines.resume
4349
import kotlin.coroutines.suspendCoroutine
4450

@@ -184,11 +190,21 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit
184190

185191
for (file in result.files) {
186192
val saved = if (exportDir == null) {
187-
val out = fileManager.copyToExternalDir(file)
188-
filesForMediaScan.add(out)
189-
SavedItem(out.toUri(), out.name, exportFormat)
193+
// No export dir defined -> save to Downloads
194+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
195+
// Android 10+: use MediaStore API
196+
val uri = saveViaMediaStore(context, file, exportFormat)
197+
SavedItem(uri, file.name, exportFormat)
198+
} else {
199+
// Android 8 and 9: use File API
200+
// (MediaStore doesn't allow to choose Downloads for Android<10)
201+
val out = fileManager.copyToExternalDir(file)
202+
filesForMediaScan.add(out)
203+
SavedItem(out.toUri(), out.name, exportFormat)
204+
}
190205
} else {
191-
val safFile = copyViaSaf(context, file, exportDir, exportFormat)
206+
// Use Storage Access Framework to save to the chosen directory
207+
val safFile = saveViaSaf(context, file, exportDir, exportFormat)
192208
SavedItem(safFile.uri, safFile.name ?: file.name, exportFormat)
193209
}
194210
savedItems += saved
@@ -221,7 +237,34 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit
221237
}
222238
}
223239

224-
private fun copyViaSaf(
240+
@RequiresApi(Build.VERSION_CODES.Q)
241+
private fun saveViaMediaStore(
242+
context: Context,
243+
source: File,
244+
format: ExportFormat
245+
): Uri {
246+
val resolver = context.contentResolver
247+
248+
val values = ContentValues().apply {
249+
put(MediaStore.MediaColumns.DISPLAY_NAME, source.name)
250+
put(MediaStore.MediaColumns.MIME_TYPE, format.mimeType)
251+
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
252+
}
253+
254+
val collection = MediaStore.Downloads.EXTERNAL_CONTENT_URI
255+
val uri = resolver.insert(collection, values)
256+
?: throw IOException("Failed to create MediaStore entry")
257+
258+
resolver.openOutputStream(uri)?.use { out ->
259+
source.inputStream().use { input ->
260+
input.copyTo(out)
261+
}
262+
} ?: throw IOException("Failed to open output stream")
263+
264+
return uri
265+
}
266+
267+
private fun saveViaSaf(
225268
context: Context,
226269
source: File,
227270
exportDirUri: Uri,

0 commit comments

Comments
 (0)