Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]
### Added
- Added multiple files "save as"-intent ([#345])

## [1.5.0] - 2025-12-16
### Changed
Expand Down Expand Up @@ -119,6 +121,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#250]: https://github.com/FossifyOrg/File-Manager/issues/250
[#251]: https://github.com/FossifyOrg/File-Manager/issues/251
[#267]: https://github.com/FossifyOrg/File-Manager/issues/267
[#345]: https://github.com/FossifyOrg/File-Manager/issues/345

[Unreleased]: https://github.com/FossifyOrg/File-Manager/compare/1.5.0...HEAD
[1.5.0]: https://github.com/FossifyOrg/File-Manager/compare/1.4.0...1.5.0
Expand Down
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@
android:label="@string/save_as">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />

<data android:mimeType="*/*" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import org.fossify.filemanager.R
import org.fossify.filemanager.databinding.ActivitySaveAsBinding
import org.fossify.filemanager.extensions.config
import java.io.File
import java.io.IOException

@Suppress("TooManyFunctions")
class SaveAsActivity : SimpleActivity() {
private val binding by viewBinding(ActivitySaveAsBinding::inflate)

Expand All @@ -33,50 +35,183 @@ class SaveAsActivity : SimpleActivity() {
}

private fun saveAsDialog() {
if (intent.action == Intent.ACTION_SEND && intent.extras?.containsKey(Intent.EXTRA_STREAM) == true) {
FilePickerDialog(this, pickFile = false, showHidden = config.shouldShowHidden(), showFAB = true, showFavoritesButton = true) {
val destination = it
handleSAFDialog(destination) {
toast(R.string.saving)
ensureBackgroundThread {
try {
if (!getDoesFilePathExist(destination)) {
if (needsStupidWritePermissions(destination)) {
val document = getDocumentFile(destination)
document!!.createDirectory(destination.getFilenameFromPath())
} else {
File(destination).mkdirs()
}
}

val source = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)!!
val originalFilename = getFilenameFromContentUri(source)
?: source.toString().getFilenameFromPath()
val filename = sanitizeFilename(originalFilename)
val mimeType = contentResolver.getType(source)
?: intent.type?.takeIf { it != "*/*" }
?: filename.getMimeType()
val inputStream = contentResolver.openInputStream(source)

val destinationPath = getAvailablePath("$destination/$filename")
val outputStream = getFileOutputStreamSync(destinationPath, mimeType, null)!!
inputStream!!.copyTo(outputStream)
rescanPaths(arrayListOf(destinationPath))
toast(R.string.file_saved)
finish()
} catch (e: Exception) {
showErrorToast(e)
finish()
}
when {
intent.action == Intent.ACTION_SEND && intent.extras?.containsKey(Intent.EXTRA_STREAM) == true -> {
handleSingleFile()
}
intent.action == Intent.ACTION_SEND_MULTIPLE && intent.extras?.containsKey(Intent.EXTRA_STREAM) == true -> {
handleMultipleFiles()
}
else -> {
toast(R.string.unknown_error_occurred)
finish()
}
}
}

private fun handleSingleFile() {
FilePickerDialog(this, pickFile = false, showHidden = config.shouldShowHidden(), showFAB = true, showFavoritesButton = true) {
val destination = it
handleSAFDialog(destination) {
toast(R.string.saving)
ensureBackgroundThread {
try {
createDestinationIfNeeded(destination)

val source = intent.getParcelableExtra<Uri>(Intent.EXTRA_STREAM)!!
val originalFilename = getFilenameFromContentUri(source)
?: source.toString().getFilenameFromPath()
val filename = sanitizeFilename(originalFilename)
val mimeType = contentResolver.getType(source)
?: intent.type?.takeIf { it != "*/*" }
?: filename.getMimeType()
val inputStream = contentResolver.openInputStream(source)

val destinationPath = getAvailablePath("$destination/$filename")
val outputStream = getFileOutputStreamSync(destinationPath, mimeType, null)!!
inputStream!!.copyTo(outputStream)
rescanPaths(arrayListOf(destinationPath))
toast(R.string.file_saved)
finish()
} catch (e: IOException) {
showErrorToast(e)
finish()
} catch (e: SecurityException) {
showErrorToast(e)
finish()
}
}
}
} else {
toast(R.string.unknown_error_occurred)
}
}

private fun handleMultipleFiles() {
FilePickerDialog(this, pickFile = false, showHidden = config.shouldShowHidden(), showFAB = true, showFavoritesButton = true) { destination ->
handleSAFDialog(destination) {
toast(R.string.saving)
ensureBackgroundThread {
processMultipleFiles(destination)
}
}
}
}

private fun processMultipleFiles(destination: String) {
try {
createDestinationIfNeeded(destination)

val uriList = intent.getParcelableArrayListExtra<Uri>(Intent.EXTRA_STREAM)
if (uriList.isNullOrEmpty()) {
runOnUiThread {
toast(R.string.no_items_found)
finish()
}
return
}

val result = saveAllFiles(destination, uriList)
showFinalResult(result)
} catch (e: IOException) {
runOnUiThread {
showErrorToast(e)
finish()
}
} catch (e: SecurityException) {
runOnUiThread {
showErrorToast(e)
finish()
}
}
}

private fun saveAllFiles(destination: String, uriList: ArrayList<Uri>): SaveResult {
val mimeTypes = intent.getStringArrayListExtra(Intent.EXTRA_MIME_TYPES)
val savedPaths = mutableListOf<String>()
var successCount = 0
var errorCount = 0

for ((index, source) in uriList.withIndex()) {
if (saveSingleFileItem(destination, source, index, mimeTypes)) {
successCount++
savedPaths.add(destination)
} else {
errorCount++
}
}

if (savedPaths.isNotEmpty()) {
rescanPaths(ArrayList(savedPaths))
}

return SaveResult(successCount, errorCount)
}

private fun saveSingleFileItem(
destination: String,
source: Uri,
index: Int,
mimeTypes: ArrayList<String>?): Boolean {
return try {
val originalFilename = getFilenameFromContentUri(source)
?: source.toString().getFilenameFromPath()
val filename = sanitizeFilename(originalFilename)

val mimeType = contentResolver.getType(source)
?: mimeTypes?.getOrNull(index)?.takeIf { it != "*/*" }
?: intent.type?.takeIf { it != "*/*" }
?: filename.getMimeType()

val inputStream = contentResolver.openInputStream(source)
?: throw IOException(getString(R.string.error, source))

val destinationPath = getAvailablePath("$destination/$filename")
val outputStream = getFileOutputStreamSync(destinationPath, mimeType, null)
?: throw IOException(getString(R.string.error, source))

inputStream.use { input ->
outputStream.use { output ->
input.copyTo(output)
}
}
true
} catch (e: IOException) {
showErrorToast(e)
false
} catch (e: SecurityException) {
showErrorToast(e)
false
}
}

private fun showFinalResult(result: SaveResult) {
runOnUiThread {
when {
result.successCount > 0 && result.errorCount == 0 -> {
toast(getString(R.string.file_saved))
}
result.successCount > 0 && result.errorCount > 0 -> {
toast(getString(R.string.files_saved_partially))
}
else -> {
toast(R.string.error)
}
}
finish()
}
}

private data class SaveResult(val successCount: Int, val errorCount: Int)
private fun createDestinationIfNeeded(destination: String) {
if (!getDoesFilePathExist(destination)) {
if (needsStupidWritePermissions(destination)) {
val document = getDocumentFile(destination)
document!!.createDirectory(destination.getFilenameFromPath())
} else {
File(destination).mkdirs()
}
}
}

override fun onResume() {
super.onResume()
setupTopAppBar(binding.activitySaveAsAppbar, NavigationIcon.Arrow)
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
<string name="recents">Recents</string>
<string name="show_recents">Show recents</string>
<string name="invert_colors">Invert colors</string>
<string name="files_saved">Files saved successfully</string>
<string name="files_saved_partially">Files saved partially</string>

<!-- Open as -->
<string name="open_as">Open as</string>
Expand Down
Loading