Save files to Downloads via MediaStore on Android 10+ (fix #85)

This commit is contained in:
Pierre-Yves Nicolas
2026-01-05 18:40:11 +01:00
parent b149b39873
commit d48d2784cd

View File

@@ -14,9 +14,14 @@
*/ */
package org.fairscan.app.ui.screens.export package org.fairscan.app.ui.screens.export
import android.content.ContentValues
import android.content.Context import android.content.Context
import android.media.MediaScannerConnection import android.media.MediaScannerConnection
import android.net.Uri import android.net.Uri
import android.os.Build
import android.os.Environment
import android.provider.MediaStore
import androidx.annotation.RequiresApi
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.documentfile.provider.DocumentFile import androidx.documentfile.provider.DocumentFile
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@@ -39,6 +44,7 @@ import org.fairscan.app.data.ImageRepository
import org.fairscan.app.ui.screens.settings.ExportFormat import org.fairscan.app.ui.screens.settings.ExportFormat
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.IOException
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
@@ -184,11 +190,21 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit
for (file in result.files) { for (file in result.files) {
val saved = if (exportDir == null) { val saved = if (exportDir == null) {
val out = fileManager.copyToExternalDir(file) // No export dir defined -> save to Downloads
filesForMediaScan.add(out) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
SavedItem(out.toUri(), out.name, exportFormat) // Android 10+: use MediaStore API
val uri = saveViaMediaStore(context, file, exportFormat)
SavedItem(uri, file.name, exportFormat)
} else {
// Android 8 and 9: use File API
// (MediaStore doesn't allow to choose Downloads for Android<10)
val out = fileManager.copyToExternalDir(file)
filesForMediaScan.add(out)
SavedItem(out.toUri(), out.name, exportFormat)
}
} else { } else {
val safFile = copyViaSaf(context, file, exportDir, exportFormat) // Use Storage Access Framework to save to the chosen directory
val safFile = saveViaSaf(context, file, exportDir, exportFormat)
SavedItem(safFile.uri, safFile.name ?: file.name, exportFormat) SavedItem(safFile.uri, safFile.name ?: file.name, exportFormat)
} }
savedItems += saved savedItems += saved
@@ -221,7 +237,34 @@ class ExportViewModel(container: AppContainer, val imageRepository: ImageReposit
} }
} }
private fun copyViaSaf( @RequiresApi(Build.VERSION_CODES.Q)
private fun saveViaMediaStore(
context: Context,
source: File,
format: ExportFormat
): Uri {
val resolver = context.contentResolver
val values = ContentValues().apply {
put(MediaStore.MediaColumns.DISPLAY_NAME, source.name)
put(MediaStore.MediaColumns.MIME_TYPE, format.mimeType)
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
}
val collection = MediaStore.Downloads.EXTERNAL_CONTENT_URI
val uri = resolver.insert(collection, values)
?: throw IOException("Failed to create MediaStore entry")
resolver.openOutputStream(uri)?.use { out ->
source.inputStream().use { input ->
input.copyTo(out)
}
} ?: throw IOException("Failed to open output stream")
return uri
}
private fun saveViaSaf(
context: Context, context: Context,
source: File, source: File,
exportDirUri: Uri, exportDirUri: Uri,