Skip to content

Commit 6221663

Browse files
committed
Cancellable file transfer jobs
- Update to Gradle 9 - Fix decompress not working on remote, but need to find a new way to check for ZIP path traversal vulnerability - Add back rename option, but only for local files, will fix later - Properties dialog works on remotes now - getMimeTypeExt tries BOTH internal and system mime type list before giving up, improving extension support - FileService and runFileJob to run a transfer task in background w/ progress notification and cancel button - Lots of improvements for the mid-transfer cancellation system - copyToInter, it's like copyTo but interruptible - OperationCanceledExceptions get logged but not shown to user - Remove "show hidden" button from file picker dialog (use the one in settings or "temporarily show hidden" in menu)
1 parent cf1f212 commit 6221663

28 files changed

+624
-205
lines changed

README.md

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,29 @@
11
# About this fork
22

3-
**The plan is to add network file sharing (SMB/SFTP)**. The official maintainers do not approve of the app containing any networking functionality. That is, perhaps, understandable, but also sadly makes the app mostly useless for my purposes.
3+
This fork adds **network file sharing via SMB** *(more protocols eg. SFTP/FTPS may be added in the future)*, among many other new features and improvements! Don't expect this to get merged into the base branch any time soon. The official maintainers take a stance against the app offering any networking functionality whatsoever.
44

5-
There's a **BIG** difference between an app connecting to some Big Data cloud you don't control, and connecting to your *own* network shares (local or remote). This app's advantage over similar open-source options like Amaze is its simplicity and the clean interface, which I do enjoy, but sometimes, things can be a little *too* simple.
5+
### Other features/improvements include
6+
- Favorites have their own tab instead of a tiny menu bar option
7+
- Favorites can be removed easily from the tab instead of via `Menu -> Settings -> Manage Favorites`
8+
- Improved sharing support to more apps and MIME type detection (eg. HEIC images don't share correctly on main branch)
9+
- Reverse sharing supported too! Share **TO** File Explore as a destination, allowing you to import images and files in from other apps/instant messengers instantly and choose where to save them.
10+
- Improved thumbnail performance, also fully supports **thumbnails on remote shares!** (A very rare feature in file manager apps I've tried)
11+
- **Vastly** improved search performance, searches show results asynchronously and can be cancelled at any time w/ back button
12+
- Bulk jobs (copy/move/delete/etc) run in background and show a progress notification that allows cancelling them at any time
13+
- Runs faster, many methods are more optimized
14+
- Cleaner edge-to-edge UI w/ full display notch/cutout support
15+
- Better touch keyboard handling (doesn't move tab bar up and block UI)
16+
- Physical keyboard shortcuts (tested with Samsung DeX/Desktop Mode!)
17+
- Stays in selection mode when rotating screen
18+
- And more!
619

7-
While I'm at it, I'm also making tweaks and improvements, and merging any other good forks I see to make this the comprehensive best version of the app. In the future, network file syncing is a possibility. There are other options for that, but having both a great file manager and sync client in one app would be convenient.
20+
### TODO
21+
- Fix file rename on remote
22+
- Fix "Open with" in ReadTextActivity (MainActivity "Open with" works fine)
23+
- Fix clicking "cancel" in overwrite dialog breaking the transfer (use the "skip" option for now)
24+
- Add recycle bin if that gets pushed to main
25+
- Add support for more protocols
26+
- Various bugfixes
827

928
# Fossify File Manager (Chu Edition :3)
1029

app/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@ android {
3535
versionCode = project.property("VERSION_CODE").toString().toInt()
3636
multiDexEnabled = true
3737
vectorDrawables.useSupportLibrary = true
38-
setProperty("archivesBaseName", "file-manager-$versionCode")
3938
}
4039

4140
signingConfigs {

app/src/main/AndroidManifest.xml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
99
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
1010
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_MEDIA_PLAYBACK"/>
11+
<uses-permission android:name="android.permission.RUN_USER_INITIATED_JOBS"/>
1112
<uses-feature android:name="android.hardware.faketouch" android:required="false"/>
1213
<queries>
1314
<package android:name="org.fossify.filemanager.debug"/>
@@ -157,6 +158,14 @@
157158
</provider>
158159
<service android:name=".helpers.RemoteService"
159160
android:foregroundServiceType="mediaPlayback"/>
161+
<service android:name=".models.FileService"
162+
android:permission="android.permission.BIND_JOB_SERVICE"
163+
android:exported="false"/>
164+
<receiver android:name=".models.FileJobNotify" android:exported="false">
165+
<intent-filter>
166+
<action android:name=".models.FileJobNotify.ACTION_STOP_JOB"/>
167+
</intent-filter>
168+
</receiver>
160169
<activity-alias
161170
android:name=".activities.SplashActivity.Red"
162171
android:enabled="false"

app/src/main/kotlin/org/fossify/filemanager/activities/DecompressActivity.kt

Lines changed: 40 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ package org.fossify.filemanager.activities
22

33
import android.net.Uri
44
import android.os.Bundle
5-
import android.util.Log
65
import net.lingala.zip4j.exception.ZipException
76
import net.lingala.zip4j.exception.ZipException.Type
87
import net.lingala.zip4j.io.inputstream.ZipInputStream
@@ -23,8 +22,11 @@ import org.fossify.filemanager.databinding.ActivityDecompressBinding
2322
import org.fossify.filemanager.dialogs.FilePickerDialog
2423
import org.fossify.filemanager.extensions.config
2524
import org.fossify.filemanager.extensions.error
26-
import org.fossify.filemanager.extensions.setLastModified
25+
import org.fossify.filemanager.extensions.isRemotePath
26+
import org.fossify.filemanager.extensions.readablePath
2727
import org.fossify.filemanager.models.ListItem
28+
import org.fossify.filemanager.models.copyToInter
29+
import org.fossify.filemanager.models.runFileJob
2830
import java.io.BufferedInputStream
2931
import java.io.File
3032
import java.io.InputStream
@@ -122,43 +124,45 @@ class DecompressActivity: SimpleActivity() {
122124
private fun openInputStream() = if(path != null) ListItem.getInputStream(this, path!!)
123125
else contentResolver.openInputStream(uri!!)
124126

125-
private fun decompressTo(dest: String) = ensureBackgroundThread {
126-
config.reloadPath = true
127-
var inStream: InputStream? = null
128-
var zipStream: ZipInputStream? = null
129-
try {
130-
inStream = openInputStream()
131-
zipStream = ZipInputStream(BufferedInputStream(inStream))
132-
if(password != null) zipStream.setPassword(password?.toCharArray())
133-
val buffer = ByteArray(1024)
134-
while(true) {
135-
val entry = zipStream.nextEntry?:break
136-
val parent = "$dest/${filename.substringBeforeLast('.')}"
137-
val newPath = "$parent/${entry.fileName.trimEnd('/')}"
138-
139-
ListItem.mkDir(this, parent, true)
140-
if(entry.isDirectory) continue
141-
//Check if vulnerable for ZIP path traversal
142-
val outFile = File(newPath)
143-
if(!outFile.canonicalPath.startsWith(parent)) continue //TODO Test if works with remote
144-
145-
ListItem.getOutputStream(this, newPath).use {
146-
var count: Int
147-
while(true) {
148-
count = zipStream.read(buffer)
149-
if(count == -1) break
150-
it.write(buffer, 0, count)
127+
private fun decompressTo(dest: String) {
128+
val remoteDest = if(isRemotePath(dest)) config.getRemoteForPath(dest) else null
129+
runFileJob(this, getString(R.string.job_decompress).format(readablePath(dest)),
130+
remoteDest != null || path?.let {isRemotePath(it)} == true) {cancel ->
131+
config.reloadPath = true
132+
var inStream: InputStream? = null
133+
var zipStream: ZipInputStream? = null
134+
try {
135+
inStream = openInputStream()
136+
zipStream = ZipInputStream(BufferedInputStream(inStream))
137+
if(password != null) zipStream.setPassword(password?.toCharArray())
138+
while(true) {
139+
val entry = zipStream.nextEntry?:break
140+
val parent = "$dest/${filename.substringBeforeLast('.')}"
141+
val newPath = "$parent/${entry.fileName.trimEnd('/')}"
142+
143+
ListItem.mkDir(this, parent, true)
144+
if(entry.isDirectory) continue
145+
//Check if vulnerable for ZIP path traversal
146+
val outFile = File(newPath)
147+
//TODO Doesn't work with remote
148+
// if(!outFile.canonicalPath.startsWith(parent)) continue
149+
150+
ListItem.getOutputStream(this, newPath).use {zipStream.copyToInter(it, cancel)}
151+
152+
//Set mod time
153+
if(entry.lastModifiedTimeEpoch != 0L) {
154+
if(remoteDest != null) remoteDest.setLastModified(newPath, entry.lastModifiedTimeEpoch)
155+
else outFile.setLastModified(entry.lastModifiedTimeEpoch)
151156
}
152157
}
153-
outFile.setLastModified(entry) //TODO Won't work with remote, needs fix
158+
toast(R.string.decompression_successful)
159+
finish()
160+
} catch(e: Throwable) {
161+
error(e)
162+
} finally {
163+
zipStream?.close()
164+
inStream?.close()
154165
}
155-
toast(R.string.decompression_successful)
156-
finish()
157-
} catch(e: Throwable) {
158-
error(e)
159-
} finally {
160-
zipStream?.close()
161-
inStream?.close()
162166
}
163167
}
164168

app/src/main/kotlin/org/fossify/filemanager/activities/MainActivity.kt

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import android.media.RingtoneManager
77
import android.net.Uri
88
import android.os.Bundle
99
import android.os.Handler
10+
import android.os.Looper
1011
import android.provider.MediaStore.MediaColumns
1112
import android.util.AttributeSet
1213
import android.util.Xml
@@ -44,6 +45,8 @@ import org.fossify.filemanager.dialogs.SaveAsDialog
4445
import org.fossify.filemanager.extensions.*
4546
import org.fossify.filemanager.helpers.*
4647
import org.fossify.filemanager.models.ListItem
48+
import org.fossify.filemanager.models.copyToInter
49+
import org.fossify.filemanager.models.runFileJob
4750
import java.util.Date
4851
import java.util.Timer
4952
import kotlin.concurrent.fixedRateTimer
@@ -170,7 +173,7 @@ class MainActivity: SimpleActivity() {
170173
if (!wasBackJustPressed && config.pressBackTwice) {
171174
wasBackJustPressed = true
172175
toast(R.string.press_back_again)
173-
Handler().postDelayed({wasBackJustPressed = false},
176+
Handler(Looper.getMainLooper()).postDelayed({wasBackJustPressed = false},
174177
BACK_PRESS_TIMEOUT.toLong())
175178
return true
176179
} else {
@@ -406,19 +409,18 @@ class MainActivity: SimpleActivity() {
406409
fun handleSendIntent(mimeType: String?, text: String?, uris: List<Uri>?) {
407410
val root = getItemsFragment()?.currentPath?:""
408411
if(text != null) SaveAsDialog(this, "$root/${getCurrentFormattedDateTime()}.txt", false) {path, _ ->
409-
//TODO Run as task
410-
ensureBackgroundThread {
412+
runFileJob(this, getString(R.string.job_text).format(readablePath(path)), isRemotePath(path)) {cancel ->
411413
try {
412414
//TODO Note: Text can be a web URL to download
415+
//TODO Make cancellable
413416
val oStr = ListItem.getOutputStream(this, path)
414417
oStr.use {it.bufferedWriter().write(text)}
415418
toast(org.fossify.commons.R.string.copying_success)
416419
finish()
417420
} catch(e: Throwable) {error(e)}
418421
}
419422
} else if(uris != null) FilePickerDialog(this, root, false, true) {dir ->
420-
//TODO Run as task
421-
ensureBackgroundThread {
423+
runFileJob(this, getString(R.string.job_share).format(readablePath(dir)), isRemotePath(dir)) {cancel ->
422424
try {
423425
for(uri in uris) {
424426
//Get name from content query, uri, then date
@@ -433,7 +435,7 @@ class MainActivity: SimpleActivity() {
433435
//Copy data
434436
val iStr = contentResolver.openInputStream(uri)
435437
val oStr = ListItem.getOutputStream(this, "$dir/$name")
436-
oStr.use {iStr?.use {it.copyTo(oStr)}}
438+
oStr.use {iStr?.use {it.copyToInter(oStr, cancel)}}
437439
}
438440
toast(org.fossify.commons.R.string.copying_success)
439441
finish()
@@ -700,7 +702,7 @@ class MainActivity: SimpleActivity() {
700702
private fun finishCreateDocumentIntent(path: String, filename: String) {
701703
val resultIntent = Intent()
702704
val uri = getFilePublicUri(File(path, filename), BuildConfig.APPLICATION_ID)
703-
val type = path.getMimeType()
705+
val type = path.getMimeTypeExt()
704706
resultIntent.setDataAndType(uri, type)
705707
resultIntent.flags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
706708
Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION

app/src/main/kotlin/org/fossify/filemanager/activities/ReadTextActivity.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ class ReadTextActivity: SimpleActivity() {
108108
R.id.menu_search -> openSearch()
109109
R.id.menu_save -> saveText()
110110
R.id.menu_save_as -> saveAsText()
111-
R.id.menu_open_with -> launchPath(intent.dataString!!, true)
111+
R.id.menu_open_with -> launchPath(intent.dataString!!, true) //TODO Doesn't work w/ remote files
112112
R.id.menu_print -> printText()
113113
else -> return@setOnMenuItemClickListener false
114114
}

0 commit comments

Comments
 (0)