Repository navigation
Copy operations calculate required storage before conflict resolution dialog #474
Description
Checklist
- I can reproduce the bug with the latest version given here.
- I made sure that there are no existing issues - open or closed - to which I could contribute my information.
- I made sure that there are no existing discussions - open or closed - to which I could contribute my information.
- I have read the FAQs inside the app (Menu -> About -> FAQs) and my problem isn't listed.
- I have taken the time to fill in all the required details. I understand that the bug report will be dismissed otherwise.
- This issue contains only one bug.
- I have read and understood the contribution guidelines.
Affected app version
1.6.1 (Latest)
Affected Android/Custom ROM version
Android 13
Affected device model
Samsung Galaxy S20
How did you install the app?
None
Steps to reproduce the bug
Scenario: Copy blocked despite skippable duplicates
- Have a device/emulator with 30 GB free space
- Create 5 files (A, B, C, D, E), each 10 GB
- Place A, B, C in Folder1
- Place A, B, C, D, E in Folder2 (identical A, B, C files)
- Select all files in Folder2 → Copy to → select Folder1
- Expected: Conflict dialog appears for A, B, C. If user selects "Skip" + "Apply to all", only D and E (20 GB) are copied → fits in 30 GB free space
- Actual: "Not enough space." error. Conflict dialog never appears
Expected behavior
Copy operations should not calculate transfer size before conflict resolution — The space check should not use the full resource size before the user has a chance to resolve conflicts (skip/overwrite/keep both). Files to be skipped should not be included in the space calculation beforehand.
Actual behavior
Copy operations calculate transfer size before conflict resolution — The space check uses the full source size before the user has a chance to resolve conflicts (skip/overwrite/keep both). Files that would be skipped are still counted toward the required space.
Screenshots/Screen recordings
Since I reset my phone some time after identifying this problem, I currently have plenty of free space, and unfortunately, I cannot reproduce the issue. I apologize for not having a screenshot.
Additional information
Root Cause Analysis
The bug originates in the Fossify Commons library, specifically in the startCopyMove() function inside BaseSimpleActivity.kt.
The sumToCopy is calculated unconditionally as the total size of all source files:
private fun startCopyMove(...) {
val availableSpace = destinationPath.getAvailableStorageB()
val sumToCopy = files.sumByLong { it.getProperSize(applicationContext, copyHidden) }
// ❌ Space check happens BEFORE checkConflicts
if (availableSpace == -1L || sumToCopy < availableSpace) {
checkConflicts(files, destinationPath, 0, LinkedHashMap()) { conflictResolutions ->
// ...
}
} else {
val text = String.format(getString(R.string.no_space), ...)
toast(text, Toast.LENGTH_LONG)
}
}Because the space check happens before checkConflicts() is called, if the raw total exceeds available space, the operation is rejected immediately.
The correct flow should be:
- Call
checkConflicts()to let the user choose how to handle duplicate files. - Calculate the effective transfer size based on the user's choices (e.g. skipped files should not be included in the total).
- Compare the effective size against the available space.
Suggested Fix
The fix should be applied in BaseSimpleActivity.startCopyMove() in the Fossify Commons library. The space check logic needs to be moved inside the checkConflicts callback:
private fun startCopyMove(...) {
// Step 1: Always resolve conflicts first
checkConflicts(files, destinationPath, 0, LinkedHashMap()) { conflictResolutions ->
// Step 2: Calculate effective transfer size based on conflict resolutions
var effectiveSize = 0L
for (file in files) {
val newPath = "$destinationPath/${file.name}"
val resolution = getConflictResolution(conflictResolutions, newPath)
val fileExists = getDoesFilePathExist(newPath)
when {
fileExists && resolution == CONFLICT_SKIP -> { /* exclude skipped files */ }
fileExists && resolution == CONFLICT_OVERWRITE -> { /* exclude overwritten bytes */ }
else -> effectiveSize += file.getProperSize(applicationContext, copyHidden)
}
}
val availableSpace = destinationPath.getAvailableStorageB()
// Step 3: Check space using the effective size
if (availableSpace != -1L && effectiveSize >= availableSpace) {
val text = String.format(
getString(R.string.no_space),
effectiveSize.formatSize(),
availableSpace.formatSize()
)
toast(text, Toast.LENGTH_LONG)
return@checkConflicts
}
// Step 4: Proceed with operation
toast(if (isCopyOperation) R.string.copying else R.string.moving)
val pair = Pair(files, destinationPath)
CopyMoveTask(
activity = this,
isCopyOperation = isCopyOperation,
copyPhotoVideoOnly = copyPhotoVideoOnly,
conflictResolutions = conflictResolutions,
listener = copyMoveListener,
copyHidden = copyHidden
).execute(pair)
}
}Related Files
| File | Repository | Role |
|---|---|---|
BaseSimpleActivity.kt |
FossifyOrg/commons | Contains startCopyMove() where the order of operations is flawed |
CopyMoveTask.kt |
FossifyOrg/commons | AsyncTask that performs the actual copy/move (which correctly respects resolutions, but is blocked from running) |
Activity