@@ -11,7 +11,9 @@ import org.fossify.filemanager.R
1111import org.fossify.filemanager.databinding.ActivitySaveAsBinding
1212import org.fossify.filemanager.extensions.config
1313import java.io.File
14+ import java.io.IOException
1415
16+ @Suppress(" TooManyFunctions" )
1517class SaveAsActivity : SimpleActivity () {
1618 private val binding by viewBinding(ActivitySaveAsBinding ::inflate)
1719
@@ -33,50 +35,185 @@ class SaveAsActivity : SimpleActivity() {
3335 }
3436
3537 private fun saveAsDialog () {
36- if (intent.action == Intent .ACTION_SEND && intent.extras?.containsKey(Intent .EXTRA_STREAM ) == true ) {
37- FilePickerDialog (this , pickFile = false , showHidden = config.shouldShowHidden(), showFAB = true , showFavoritesButton = true ) {
38- val destination = it
39- handleSAFDialog(destination) {
40- toast(R .string.saving)
41- ensureBackgroundThread {
42- try {
43- if (! getDoesFilePathExist(destination)) {
44- if (needsStupidWritePermissions(destination)) {
45- val document = getDocumentFile(destination)
46- document!! .createDirectory(destination.getFilenameFromPath())
47- } else {
48- File (destination).mkdirs()
49- }
50- }
51-
52- val source = intent.getParcelableExtra<Uri >(Intent .EXTRA_STREAM )!!
53- val originalFilename = getFilenameFromContentUri(source)
54- ? : source.toString().getFilenameFromPath()
55- val filename = sanitizeFilename(originalFilename)
56- val mimeType = contentResolver.getType(source)
57- ? : intent.type?.takeIf { it != " */*" }
58- ? : filename.getMimeType()
59- val inputStream = contentResolver.openInputStream(source)
60-
61- val destinationPath = getAvailablePath(" $destination /$filename " )
62- val outputStream = getFileOutputStreamSync(destinationPath, mimeType, null )!!
63- inputStream!! .copyTo(outputStream)
64- rescanPaths(arrayListOf (destinationPath))
65- toast(R .string.file_saved)
66- finish()
67- } catch (e: Exception ) {
68- showErrorToast(e)
69- finish()
70- }
38+ when {
39+ intent.action == Intent .ACTION_SEND && intent.extras?.containsKey(Intent .EXTRA_STREAM ) == true -> {
40+ handleSingleFile()
41+ }
42+ intent.action == Intent .ACTION_SEND_MULTIPLE && intent.extras?.containsKey(Intent .EXTRA_STREAM ) == true -> {
43+ handleMultipleFiles()
44+ }
45+ else -> {
46+ toast(R .string.unknown_error_occurred)
47+ finish()
48+ }
49+ }
50+ }
51+
52+ private fun handleSingleFile () {
53+ FilePickerDialog (this , pickFile = false , showHidden = config.shouldShowHidden(), showFAB = true , showFavoritesButton = true ) {
54+ val destination = it
55+ handleSAFDialog(destination) {
56+ toast(R .string.saving)
57+ ensureBackgroundThread {
58+ try {
59+ createDestinationIfNeeded(destination)
60+
61+ val source = intent.getParcelableExtra<Uri >(Intent .EXTRA_STREAM )!!
62+ val originalFilename = getFilenameFromContentUri(source)
63+ ? : source.toString().getFilenameFromPath()
64+ val filename = sanitizeFilename(originalFilename)
65+ val mimeType = contentResolver.getType(source)
66+ ? : intent.type?.takeIf { it != " */*" }
67+ ? : filename.getMimeType()
68+ val inputStream = contentResolver.openInputStream(source)
69+
70+ val destinationPath = getAvailablePath(" $destination /$filename " )
71+ val outputStream = getFileOutputStreamSync(destinationPath, mimeType, null )!!
72+ inputStream!! .copyTo(outputStream)
73+ rescanPaths(arrayListOf (destinationPath))
74+ toast(R .string.file_saved)
75+ finish()
76+ } catch (e: IOException ) {
77+ showErrorToast(e)
78+ finish()
79+ } catch (e: SecurityException ) {
80+ showErrorToast(e)
81+ finish()
7182 }
7283 }
7384 }
74- } else {
75- toast(R .string.unknown_error_occurred)
85+ }
86+ }
87+
88+ private fun handleMultipleFiles () {
89+ FilePickerDialog (this , pickFile = false , showHidden = config.shouldShowHidden(), showFAB = true , showFavoritesButton = true ) { destination ->
90+ handleSAFDialog(destination) {
91+ toast(R .string.saving)
92+ ensureBackgroundThread {
93+ processMultipleFiles(destination)
94+ }
95+ }
96+ }
97+ }
98+
99+ private fun processMultipleFiles (destination : String ) {
100+ try {
101+ createDestinationIfNeeded(destination)
102+
103+ val uriList = intent.getParcelableArrayListExtra<Uri >(Intent .EXTRA_STREAM )
104+ if (uriList.isNullOrEmpty()) {
105+ runOnUiThread {
106+ toast(R .string.no_items_found)
107+ finish()
108+ }
109+ return
110+ }
111+
112+ val result = saveAllFiles(destination, uriList)
113+ showFinalResult(result)
114+ } catch (e: IOException ) {
115+ runOnUiThread {
116+ showErrorToast(e)
117+ finish()
118+ }
119+ } catch (e: SecurityException ) {
120+ runOnUiThread {
121+ showErrorToast(e)
122+ finish()
123+ }
124+ }
125+ }
126+
127+ private fun saveAllFiles (destination : String , uriList : ArrayList <Uri >): SaveResult {
128+ val mimeTypes = intent.getStringArrayListExtra(Intent .EXTRA_MIME_TYPES )
129+ val savedPaths = mutableListOf<String >()
130+ var successCount = 0
131+ var errorCount = 0
132+
133+ for ((index, source) in uriList.withIndex()) {
134+ if (saveSingleFileItem(destination, source, index, mimeTypes)) {
135+ successCount++
136+ savedPaths.add(destination)
137+ } else {
138+ errorCount++
139+ }
140+ }
141+
142+ if (savedPaths.isNotEmpty()) {
143+ rescanPaths(ArrayList (savedPaths))
144+ }
145+
146+ return SaveResult (successCount, errorCount)
147+ }
148+
149+ private fun saveSingleFileItem (
150+ destination : String ,
151+ source : Uri ,
152+ index : Int ,
153+ mimeTypes : ArrayList <String >? ): Boolean {
154+ return try {
155+ val originalFilename = getFilenameFromContentUri(source)
156+ ? : source.toString().getFilenameFromPath()
157+ ? : " file_$index "
158+ val filename = originalFilename.replace(" [/\\\\ <>:\" |?*\u0000 -\u001F ]" .toRegex(), " _" )
159+ .takeIf { it.isNotBlank() } ? : " unnamed_file"
160+
161+ val mimeType = contentResolver.getType(source)
162+ ? : mimeTypes?.getOrNull(index)?.takeIf { it != " */*" }
163+ ? : intent.type?.takeIf { it != " */*" }
164+ ? : filename.getMimeType()
165+
166+ val inputStream = contentResolver.openInputStream(source)
167+ ? : throw IOException (" Cannot open input stream" )
168+
169+ val destinationPath = getAvailablePath(" $destination /$filename " )
170+ val outputStream = getFileOutputStreamSync(destinationPath, mimeType, null )
171+ ? : throw IOException (" Cannot create output stream" )
172+
173+ inputStream.use { input ->
174+ outputStream.use { output ->
175+ input.copyTo(output)
176+ }
177+ }
178+ true
179+ } catch (e: IOException ) {
180+ showErrorToast(e)
181+ false
182+ } catch (e: SecurityException ) {
183+ showErrorToast(e)
184+ false
185+ }
186+ }
187+
188+ private fun showFinalResult (result : SaveResult ) {
189+ runOnUiThread {
190+ when {
191+ result.successCount > 0 && result.errorCount == 0 -> {
192+ toast(getString(R .string.file_saved))
193+ }
194+ result.successCount > 0 && result.errorCount > 0 -> {
195+ toast(getString(R .string.files_saved_partially))
196+ }
197+ else -> {
198+ toast(R .string.error)
199+ }
200+ }
76201 finish()
77202 }
78203 }
79204
205+ private data class SaveResult (val successCount : Int , val errorCount : Int )
206+ private fun createDestinationIfNeeded (destination : String ) {
207+ if (! getDoesFilePathExist(destination)) {
208+ if (needsStupidWritePermissions(destination)) {
209+ val document = getDocumentFile(destination)
210+ document!! .createDirectory(destination.getFilenameFromPath())
211+ } else {
212+ File (destination).mkdirs()
213+ }
214+ }
215+ }
216+
80217 override fun onResume () {
81218 super .onResume()
82219 setupTopAppBar(binding.activitySaveAsAppbar, NavigationIcon .Arrow )
0 commit comments