Skip to content

Copy operations calculate required storage before conflict resolution dialog #474

Description

@akbudakanl

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:

  1. Call checkConflicts() to let the user choose how to handle duplicate files.
  2. Calculate the effective transfer size based on the user's choices (e.g. skipped files should not be included in the total).
  3. 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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is not workingneeds triageIssue is not yet ready for PR authors to take up

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions