From 346b5aa32fc4204c6c0fd726e2ff410a2cd78eaf Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 19 Nov 2025 16:05:33 +0100 Subject: [PATCH 1/7] fix(file-sort): concurrent modification exception Signed-off-by: alperozturk --- .../com/nextcloud/utils/FileSortOrderTests.kt | 251 ++++++++++++++++++ .../android/utils/FileSortOrderByDate.kt | 15 +- .../android/utils/FileSortOrderByName.kt | 21 +- .../android/utils/FileSortOrderBySize.kt | 18 +- 4 files changed, 284 insertions(+), 21 deletions(-) create mode 100644 app/src/androidTest/java/com/nextcloud/utils/FileSortOrderTests.kt diff --git a/app/src/androidTest/java/com/nextcloud/utils/FileSortOrderTests.kt b/app/src/androidTest/java/com/nextcloud/utils/FileSortOrderTests.kt new file mode 100644 index 000000000000..b2f41b06fa87 --- /dev/null +++ b/app/src/androidTest/java/com/nextcloud/utils/FileSortOrderTests.kt @@ -0,0 +1,251 @@ +/* + * Nextcloud - Android Client + * + * SPDX-FileCopyrightText: 2025 Alper Ozturk + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +package com.nextcloud.utils + +import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.utils.FileSortOrder.Companion.SORT_A_TO_Z_ID +import com.owncloud.android.utils.FileSortOrder.Companion.SORT_BIG_TO_SMALL_ID +import com.owncloud.android.utils.FileSortOrder.Companion.SORT_NEW_TO_OLD_ID +import com.owncloud.android.utils.FileSortOrder.Companion.SORT_OLD_TO_NEW_ID +import com.owncloud.android.utils.FileSortOrder.Companion.SORT_SMALL_TO_BIG_ID +import com.owncloud.android.utils.FileSortOrder.Companion.SORT_Z_TO_A_ID +import com.owncloud.android.utils.FileSortOrderByDate +import com.owncloud.android.utils.FileSortOrderByName +import com.owncloud.android.utils.FileSortOrderBySize +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import java.io.File +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit + +class FileSortOrderTests { + + private fun createTempFile(prefix: String, lastModified: Long? = null, sizeBytes: Int? = null): File { + return File.createTempFile(prefix, ".txt").apply { + lastModified?.let { setLastModified(it) } + sizeBytes?.let { writeBytes(ByteArray(it)) } + } + } + + private fun createTempFolder(prefix: String): File { + return File.createTempFile(prefix, "").apply { + delete() + mkdir() + } + } + + private fun createOCFile(path: String, modTime: Long? = null, size: Long? = null): OCFile { + return OCFile(path).apply { + modTime?.let { modificationTimestamp = it } + size?.let { fileLength = it } + } + } + + private fun testConcurrentModification( + files: MutableList, + sorter: com.owncloud.android.utils.FileSortOrder, + iterations: Int = 50 + ) { + val executor = Executors.newFixedThreadPool(2) + try { + repeat(iterations) { i -> + // modifying and sorting files + executor.submit { sorter.sortLocalFiles(files) } + executor.submit { + files.add(createTempFile("temp$i", lastModified = i.toLong())) + if (files.size > 20) files.removeAt(0) + } + } + } finally { + executor.shutdown() + executor.awaitTermination(5, TimeUnit.SECONDS) + } + assertTrue(true) + } + + @Test + fun testSortLocalFilesAscending() { + val file1 = createTempFile("file1", lastModified = 1000) + val file2 = createTempFile("file2", lastModified = 2000) + val file3 = createTempFile("file3", lastModified = 1500) + + val files = mutableListOf(file1, file2, file3) + val sorter = FileSortOrderByDate(SORT_OLD_TO_NEW_ID, ascending = true) + + val sorted = sorter.sortLocalFiles(files) + + assertEquals(listOf(file1, file3, file2), sorted) + } + + @Test + fun testSortLocalFilesAscendingFalse() { + val file1 = createTempFile("file1", lastModified = 1000) + val file2 = createTempFile("file2", lastModified = 2000) + val file3 = createTempFile("file3", lastModified = 1500) + + val files = mutableListOf(file1, file2, file3) + val sorter = FileSortOrderByDate(SORT_OLD_TO_NEW_ID, ascending = false) + + val sorted = sorter.sortLocalFiles(files) + + assertEquals(listOf(file2, file3, file1), sorted) + } + + @Test + fun testSortLocalFilesDescending() { + val file1 = createTempFile("file1", lastModified = 1000) + val file2 = createTempFile("file2", lastModified = 2000) + val file3 = createTempFile("file3", lastModified = 1500) + + val files = mutableListOf(file1, file2, file3) + val sorter = FileSortOrderByDate(SORT_NEW_TO_OLD_ID, ascending = false) + + val sorted = sorter.sortLocalFiles(files) + + assertEquals(listOf(file2, file3, file1), sorted) + } + + @Test + fun testSortLocalFilesNoConcurrentModification() { + val files = mutableListOf( + createTempFile("file1", lastModified = 1000), + createTempFile("file2", lastModified = 2000), + createTempFile("file3", lastModified = 1500) + ) + val sorter = FileSortOrderByDate(SORT_OLD_TO_NEW_ID, ascending = true) + + testConcurrentModification(files, sorter, iterations = 100) + } + + @Test + fun testSortCloudFilesByDate() { + val f1 = createOCFile("/123.txt", modTime = 1000) + val f2 = createOCFile("/124.txt", modTime = 3000) + val f3 = createOCFile("/125.txt", modTime = 2000) + + val files = mutableListOf(f1, f2, f3) + val sorter = FileSortOrderByDate(SORT_OLD_TO_NEW_ID, ascending = true) + + val sorted = sorter.sortCloudFiles(files, foldersBeforeFiles = false, favoritesFirst = false) + + assertEquals(listOf(f1, f3, f2), sorted) + } + + @Test + fun testSortLocalFilesByNameAscending() { + val folder = createTempFolder("folder") + val file1 = createTempFile("apple") + val file2 = createTempFile("banana") + val file3 = createTempFile("cherry") + + val files = mutableListOf(file3, folder, file1, file2) + val sorter = FileSortOrderByName(SORT_A_TO_Z_ID, ascending = true) + + val sorted = sorter.sortLocalFiles(files) + + assertEquals(listOf(folder, file1, file2, file3), sorted) + } + + @Test + fun testSortLocalFilesByNameDescending() { + val file1 = createTempFile("apple") + val file2 = createTempFile("banana") + val file3 = createTempFile("cherry") + + val files = mutableListOf(file1, file2, file3) + val sorter = FileSortOrderByName(SORT_Z_TO_A_ID, ascending = false) + + val sorted = sorter.sortLocalFiles(files) + + assertEquals(listOf(file3, file2, file1), sorted) + } + + @Test + fun testSortCloudFilesByName() { + val f1 = createOCFile("/b.txt") + val f2 = createOCFile("/a.txt") + val f3 = createOCFile("/c.txt") + + val files = mutableListOf(f1, f2, f3) + val sorter = FileSortOrderByName(SORT_A_TO_Z_ID, ascending = true) + + val sorted = sorter.sortCloudFiles(files, foldersBeforeFiles = false, favoritesFirst = false) + + assertEquals(listOf(f2, f1, f3), sorted) + } + + @Test + fun testSortLocalFilesByNameNoConcurrentModification() { + val files = mutableListOf( + createTempFile("apple"), + createTempFile("banana"), + createTempFile("cherry"), + createTempFolder("folder") + ) + val sorter = FileSortOrderByName(SORT_A_TO_Z_ID, ascending = true) + + testConcurrentModification(files, sorter) + } + + @Test + fun testSortLocalFilesBySizeAscending() { + val file1 = createTempFile("file1", sizeBytes = 100) + val file2 = createTempFile("file2", sizeBytes = 300) + val file3 = createTempFile("file3", sizeBytes = 200) + + val files = mutableListOf(file1, file2, file3) + val sorter = FileSortOrderBySize(SORT_SMALL_TO_BIG_ID, ascending = true) + + val sorted = sorter.sortLocalFiles(files) + + assertEquals(listOf(file1, file3, file2), sorted) + } + + @Test + fun testSortLocalFilesBySizeDescending() { + val file1 = createTempFile("file1", sizeBytes = 100) + val file2 = createTempFile("file2", sizeBytes = 300) + val file3 = createTempFile("file3", sizeBytes = 200) + val folder1 = createTempFolder("folder1") + val folder2 = createTempFolder("folder2") + + val files = mutableListOf(file1, file2, file3, folder1, folder2) + val sorter = FileSortOrderBySize(SORT_BIG_TO_SMALL_ID, ascending = false) + + val sorted = sorter.sortLocalFiles(files) + + assertEquals(listOf(folder1, folder2, file2, file3, file1), sorted) + } + + @Test + fun testSortCloudFilesBySize() { + val f1 = createOCFile("/file1.txt", size = 100) + val f2 = createOCFile("/file2.txt", size = 300) + val f3 = createOCFile("/file3.txt", size = 200) + + val files = mutableListOf(f1, f2, f3) + val sorter = FileSortOrderBySize(SORT_SMALL_TO_BIG_ID, ascending = true) + + val sorted = sorter.sortCloudFiles(files, foldersBeforeFiles = false, favoritesFirst = false) + + assertEquals(listOf(f1, f3, f2), sorted) + } + + @Test + fun testSortLocalFilesBySizeNoConcurrentModification() { + val files = mutableListOf( + createTempFile("file1", sizeBytes = 100), + createTempFile("file2", sizeBytes = 200), + createTempFile("file3", sizeBytes = 300), + createTempFolder("folder") + ) + val sorter = FileSortOrderBySize(SORT_SMALL_TO_BIG_ID, ascending = true) + + testConcurrentModification(files, sorter) + } +} diff --git a/app/src/main/java/com/owncloud/android/utils/FileSortOrderByDate.kt b/app/src/main/java/com/owncloud/android/utils/FileSortOrderByDate.kt index 6d59836a9764..da88a68a81ee 100644 --- a/app/src/main/java/com/owncloud/android/utils/FileSortOrderByDate.kt +++ b/app/src/main/java/com/owncloud/android/utils/FileSortOrderByDate.kt @@ -26,10 +26,11 @@ class FileSortOrderByDate(name: String, ascending: Boolean) : FileSortOrder(name favoritesFirst: Boolean ): MutableList { val multiplier = if (isAscending) 1 else -1 - files.sortWith { o1: OCFile, o2: OCFile -> + val copy = files.toMutableList() + copy.sortWith { o1: OCFile, o2: OCFile -> multiplier * o1.modificationTimestamp.compareTo(o2.modificationTimestamp) } - return super.sortCloudFiles(files, foldersBeforeFiles, favoritesFirst) + return super.sortCloudFiles(copy, foldersBeforeFiles, favoritesFirst) } /** @@ -39,10 +40,11 @@ class FileSortOrderByDate(name: String, ascending: Boolean) : FileSortOrder(name */ override fun sortTrashbinFiles(files: MutableList): List { val multiplier = if (isAscending) 1 else -1 - files.sortWith { o1: TrashbinFile, o2: TrashbinFile -> + val copy = files.toMutableList() + copy.sortWith { o1: TrashbinFile, o2: TrashbinFile -> multiplier * o1.deletionTimestamp.compareTo(o2.deletionTimestamp) } - return super.sortTrashbinFiles(files) + return super.sortTrashbinFiles(copy) } /** @@ -52,9 +54,10 @@ class FileSortOrderByDate(name: String, ascending: Boolean) : FileSortOrder(name */ override fun sortLocalFiles(files: MutableList): List { val multiplier = if (isAscending) 1 else -1 - files.sortWith { o1: File, o2: File -> + val copy = files.toMutableList() + copy.sortWith { o1: File, o2: File -> multiplier * o1.lastModified().compareTo(o2.lastModified()) } - return files + return copy } } diff --git a/app/src/main/java/com/owncloud/android/utils/FileSortOrderByName.kt b/app/src/main/java/com/owncloud/android/utils/FileSortOrderByName.kt index cb39a2a03303..48f4ed3b5f6e 100644 --- a/app/src/main/java/com/owncloud/android/utils/FileSortOrderByName.kt +++ b/app/src/main/java/com/owncloud/android/utils/FileSortOrderByName.kt @@ -29,7 +29,8 @@ class FileSortOrderByName internal constructor(name: String?, ascending: Boolean foldersBeforeFiles: Boolean, favoritesFirst: Boolean ): MutableList { - val sortedByName = sortOnlyByName(files) + val copy = files.toMutableList() + val sortedByName = sortOnlyByName(copy) return super.sortCloudFiles(sortedByName, foldersBeforeFiles, favoritesFirst) } @@ -39,12 +40,14 @@ class FileSortOrderByName internal constructor(name: String?, ascending: Boolean * @param files files to sort */ override fun sortTrashbinFiles(files: MutableList): List { - val sortedByName = sortServerFiles(files) + val copy = files.toMutableList() + val sortedByName = sortServerFiles(copy) return super.sortTrashbinFiles(sortedByName) } private fun sortServerFiles(files: MutableList): MutableList { - files.sortWith { o1: ServerFileInterface, o2: ServerFileInterface -> + val copy = files.toMutableList() + copy.sortWith { o1: ServerFileInterface, o2: ServerFileInterface -> when { o1.isFolder && o2.isFolder -> sortMultiplier * AlphanumComparator.compare(o1, o2) o1.isFolder -> -1 @@ -52,12 +55,13 @@ class FileSortOrderByName internal constructor(name: String?, ascending: Boolean else -> sortMultiplier * AlphanumComparator.compare(o1, o2) } } - return files + return copy } private fun sortOnlyByName(files: MutableList): MutableList { - files.sortWith { o1: OCFile, o2: OCFile -> sortMultiplier * AlphanumComparator.compare(o1, o2) } - return files + val copy = files.toMutableList() + copy.sortWith { o1: OCFile, o2: OCFile -> sortMultiplier * AlphanumComparator.compare(o1, o2) } + return copy } /** @@ -66,7 +70,8 @@ class FileSortOrderByName internal constructor(name: String?, ascending: Boolean * @param files files to sort */ override fun sortLocalFiles(files: MutableList): List { - files.sortWith { o1: File, o2: File -> + val copy = files.toMutableList() + copy.sortWith { o1: File, o2: File -> when { o1.isDirectory && o2.isDirectory -> sortMultiplier * o1.path.lowercase(Locale.getDefault()) .compareTo(o2.path.lowercase(Locale.getDefault())) @@ -78,6 +83,6 @@ class FileSortOrderByName internal constructor(name: String?, ascending: Boolean ) } } - return files + return copy } } diff --git a/app/src/main/java/com/owncloud/android/utils/FileSortOrderBySize.kt b/app/src/main/java/com/owncloud/android/utils/FileSortOrderBySize.kt index 5b188f1c4e86..e4f9e4689e8e 100644 --- a/app/src/main/java/com/owncloud/android/utils/FileSortOrderBySize.kt +++ b/app/src/main/java/com/owncloud/android/utils/FileSortOrderBySize.kt @@ -22,17 +22,20 @@ class FileSortOrderBySize internal constructor(name: String?, ascending: Boolean foldersBeforeFiles: Boolean, favoritesFirst: Boolean ): MutableList { - val sortedBySize = sortServerFiles(files) + val copy = files.toMutableList() + val sortedBySize = sortServerFiles(copy) return super.sortCloudFiles(sortedBySize, foldersBeforeFiles, favoritesFirst) } override fun sortTrashbinFiles(files: MutableList): List { - val sortedBySize = sortServerFiles(files) + val copy = files.toMutableList() + val sortedBySize = sortServerFiles(copy) return super.sortTrashbinFiles(sortedBySize) } private fun sortServerFiles(files: MutableList): MutableList { - files.sortWith { o1: ServerFileInterface, o2: ServerFileInterface -> + val copy = files.toMutableList() + copy.sortWith { o1: ServerFileInterface, o2: ServerFileInterface -> when { o1.isFolder && o2.isFolder -> sortMultiplier * o1.fileLength.compareTo(o2.fileLength) o1.isFolder -> -1 @@ -40,14 +43,15 @@ class FileSortOrderBySize internal constructor(name: String?, ascending: Boolean else -> sortMultiplier * o1.fileLength.compareTo(o2.fileLength) } } - return files + return copy } override fun sortLocalFiles(files: MutableList): List { + val copy = files.toMutableList() val folderSizes = - files.associateWith { file -> FileStorageUtils.getFolderSize(file) } + copy.associateWith { file -> FileStorageUtils.getFolderSize(file) } - files.sortWith { o1: File, o2: File -> + copy.sortWith { o1: File, o2: File -> when { o1.isDirectory && o2.isDirectory -> sortMultiplier * (folderSizes[o1] ?: 0L).compareTo( folderSizes[o2] ?: 0L @@ -57,6 +61,6 @@ class FileSortOrderBySize internal constructor(name: String?, ascending: Boolean else -> sortMultiplier * o1.length().compareTo(o2.length()) } } - return files + return copy } } From e58932f95c1286dc2f3356dd13fc33c5ef46631f Mon Sep 17 00:00:00 2001 From: alperozturk Date: Thu, 20 Nov 2025 08:45:55 +0100 Subject: [PATCH 2/7] fix(file-sort): concurrent modification exception Signed-off-by: alperozturk --- .../com/nextcloud/utils/FileSortOrderTests.kt | 369 ++++++++++-------- 1 file changed, 199 insertions(+), 170 deletions(-) diff --git a/app/src/androidTest/java/com/nextcloud/utils/FileSortOrderTests.kt b/app/src/androidTest/java/com/nextcloud/utils/FileSortOrderTests.kt index b2f41b06fa87..3940e70a4269 100644 --- a/app/src/androidTest/java/com/nextcloud/utils/FileSortOrderTests.kt +++ b/app/src/androidTest/java/com/nextcloud/utils/FileSortOrderTests.kt @@ -7,245 +7,274 @@ package com.nextcloud.utils import com.owncloud.android.datamodel.OCFile +import com.owncloud.android.utils.FileSortOrder import com.owncloud.android.utils.FileSortOrder.Companion.SORT_A_TO_Z_ID import com.owncloud.android.utils.FileSortOrder.Companion.SORT_BIG_TO_SMALL_ID import com.owncloud.android.utils.FileSortOrder.Companion.SORT_NEW_TO_OLD_ID import com.owncloud.android.utils.FileSortOrder.Companion.SORT_OLD_TO_NEW_ID import com.owncloud.android.utils.FileSortOrder.Companion.SORT_SMALL_TO_BIG_ID -import com.owncloud.android.utils.FileSortOrder.Companion.SORT_Z_TO_A_ID import com.owncloud.android.utils.FileSortOrderByDate import com.owncloud.android.utils.FileSortOrderByName import com.owncloud.android.utils.FileSortOrderBySize import org.junit.Assert.assertEquals -import org.junit.Assert.assertTrue import org.junit.Test import java.io.File -import java.util.concurrent.Executors +import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit +import kotlin.concurrent.thread class FileSortOrderTests { - private fun createTempFile(prefix: String, lastModified: Long? = null, sizeBytes: Int? = null): File { - return File.createTempFile(prefix, ".txt").apply { + private fun tmpFile(prefix: String, lastModified: Long? = null, size: Int? = null): File = + File.createTempFile(prefix, ".txt").apply { lastModified?.let { setLastModified(it) } - sizeBytes?.let { writeBytes(ByteArray(it)) } + size?.let { writeBytes(ByteArray(it)) } } - } - private fun createTempFolder(prefix: String): File { - return File.createTempFile(prefix, "").apply { - delete() - mkdir() - } + private fun tmpFolder(prefix: String): File = File.createTempFile(prefix, "").apply { + delete() + mkdir() } - private fun createOCFile(path: String, modTime: Long? = null, size: Long? = null): OCFile { - return OCFile(path).apply { - modTime?.let { modificationTimestamp = it } - size?.let { fileLength = it } - } + private fun ocFile(path: String, mod: Long? = null, size: Long? = null) = OCFile(path).apply { + mod?.let { modificationTimestamp = it } + size?.let { fileLength = it } } - private fun testConcurrentModification( - files: MutableList, - sorter: com.owncloud.android.utils.FileSortOrder, - iterations: Int = 50 + private fun runSortTest( + name: String, + items: MutableList, + sorter: FileSortOrder, + expected: List, + isCloud: Boolean = false ) { - val executor = Executors.newFixedThreadPool(2) - try { - repeat(iterations) { i -> - // modifying and sorting files - executor.submit { sorter.sortLocalFiles(files) } - executor.submit { - files.add(createTempFile("temp$i", lastModified = i.toLong())) - if (files.size > 20) files.removeAt(0) - } - } - } finally { - executor.shutdown() - executor.awaitTermination(5, TimeUnit.SECONDS) + val actual = if (isCloud) { + sorter.sortCloudFiles(items as MutableList, false, false) + } else { + sorter.sortLocalFiles(items as MutableList) } - assertTrue(true) - } - @Test - fun testSortLocalFilesAscending() { - val file1 = createTempFile("file1", lastModified = 1000) - val file2 = createTempFile("file2", lastModified = 2000) - val file3 = createTempFile("file3", lastModified = 1500) - - val files = mutableListOf(file1, file2, file3) - val sorter = FileSortOrderByDate(SORT_OLD_TO_NEW_ID, ascending = true) - - val sorted = sorter.sortLocalFiles(files) - - assertEquals(listOf(file1, file3, file2), sorted) + assertEquals(name, expected, actual) } - @Test - fun testSortLocalFilesAscendingFalse() { - val file1 = createTempFile("file1", lastModified = 1000) - val file2 = createTempFile("file2", lastModified = 2000) - val file3 = createTempFile("file3", lastModified = 1500) - - val files = mutableListOf(file1, file2, file3) - val sorter = FileSortOrderByDate(SORT_OLD_TO_NEW_ID, ascending = false) - - val sorted = sorter.sortLocalFiles(files) - - assertEquals(listOf(file2, file3, file1), sorted) - } + private fun testConcurrency(files: MutableList, sorter: FileSortOrder, iterations: Int = 50) { + val latch = CountDownLatch(iterations * 2) + val errors = mutableListOf() + + repeat(iterations) { i -> + thread { + try { + sorter.sortLocalFiles(files) + } catch (e: Throwable) { + errors.add(e) + } finally { + latch.countDown() + } + } - @Test - fun testSortLocalFilesDescending() { - val file1 = createTempFile("file1", lastModified = 1000) - val file2 = createTempFile("file2", lastModified = 2000) - val file3 = createTempFile("file3", lastModified = 1500) + thread { + try { + files.add(tmpFile("tmp$i", lastModified = i.toLong())) + if (files.size > 20) files.removeAt(0) + } catch (e: Throwable) { + errors.add(e) + } finally { + latch.countDown() + } + } + } - val files = mutableListOf(file1, file2, file3) - val sorter = FileSortOrderByDate(SORT_NEW_TO_OLD_ID, ascending = false) + // Wait for threads to finish + val completed = latch.await(5, TimeUnit.SECONDS) - val sorted = sorter.sortLocalFiles(files) + if (errors.isNotEmpty()) { + throw AssertionError("Exceptions occurred in background threads: ${errors.first()}", errors.first()) + } - assertEquals(listOf(file2, file3, file1), sorted) + if (!completed) { + throw AssertionError("Concurrent test timed out") + } } @Test - fun testSortLocalFilesNoConcurrentModification() { - val files = mutableListOf( - createTempFile("file1", lastModified = 1000), - createTempFile("file2", lastModified = 2000), - createTempFile("file3", lastModified = 1500) + fun sortDateOldToNew() { + val file1 = tmpFile("file1", 1000) + val file2 = tmpFile("file2", 2000) + val file3 = tmpFile("file3", 1500) + + runSortTest( + "old→new asc", + mutableListOf(file1, file2, file3), + FileSortOrderByDate(SORT_OLD_TO_NEW_ID, true), + listOf(file1, file3, file2) ) - val sorter = FileSortOrderByDate(SORT_OLD_TO_NEW_ID, ascending = true) - testConcurrentModification(files, sorter, iterations = 100) + runSortTest( + "old→new desc", + mutableListOf(file1, file2, file3), + FileSortOrderByDate(SORT_OLD_TO_NEW_ID, false), + listOf(file2, file3, file1) + ) } @Test - fun testSortCloudFilesByDate() { - val f1 = createOCFile("/123.txt", modTime = 1000) - val f2 = createOCFile("/124.txt", modTime = 3000) - val f3 = createOCFile("/125.txt", modTime = 2000) - - val files = mutableListOf(f1, f2, f3) - val sorter = FileSortOrderByDate(SORT_OLD_TO_NEW_ID, ascending = true) - - val sorted = sorter.sortCloudFiles(files, foldersBeforeFiles = false, favoritesFirst = false) + fun sortDateNewToOld() { + val file1 = tmpFile("file1", 1000) + val file2 = tmpFile("file2", 2000) + val file3 = tmpFile("file3", 1500) + + runSortTest( + "new→old asc", + mutableListOf(file1, file2, file3), + FileSortOrderByDate(SORT_NEW_TO_OLD_ID, true), + listOf(file1, file3, file2) + ) - assertEquals(listOf(f1, f3, f2), sorted) + runSortTest( + "new→old desc", + mutableListOf(file1, file2, file3), + FileSortOrderByDate(SORT_NEW_TO_OLD_ID, false), + listOf(file2, file3, file1) + ) } @Test - fun testSortLocalFilesByNameAscending() { - val folder = createTempFolder("folder") - val file1 = createTempFile("apple") - val file2 = createTempFile("banana") - val file3 = createTempFile("cherry") - - val files = mutableListOf(file3, folder, file1, file2) - val sorter = FileSortOrderByName(SORT_A_TO_Z_ID, ascending = true) - - val sorted = sorter.sortLocalFiles(files) - - assertEquals(listOf(folder, file1, file2, file3), sorted) + fun sortDateCloud() { + val file1 = ocFile("/1", mod = 1000) + val file2 = ocFile("/2", mod = 3000) + val file3 = ocFile("/3", mod = 2000) + + runSortTest( + "cloud old→new asc", + mutableListOf(file1, file2, file3), + FileSortOrderByDate(SORT_OLD_TO_NEW_ID, true), + listOf(file1, file3, file2), + isCloud = true + ) } @Test - fun testSortLocalFilesByNameDescending() { - val file1 = createTempFile("apple") - val file2 = createTempFile("banana") - val file3 = createTempFile("cherry") - - val files = mutableListOf(file1, file2, file3) - val sorter = FileSortOrderByName(SORT_Z_TO_A_ID, ascending = false) - - val sorted = sorter.sortLocalFiles(files) - - assertEquals(listOf(file3, file2, file1), sorted) + fun sortDateConcurrency() { + val items = mutableListOf( + tmpFile("file1", 1000), + tmpFile("file2", 2000), + tmpFile("file3", 1500) + ) + testConcurrency(items, FileSortOrderByDate(SORT_OLD_TO_NEW_ID, true)) } @Test - fun testSortCloudFilesByName() { - val f1 = createOCFile("/b.txt") - val f2 = createOCFile("/a.txt") - val f3 = createOCFile("/c.txt") - - val files = mutableListOf(f1, f2, f3) - val sorter = FileSortOrderByName(SORT_A_TO_Z_ID, ascending = true) - - val sorted = sorter.sortCloudFiles(files, foldersBeforeFiles = false, favoritesFirst = false) + fun sortNameLocal() { + val folder = tmpFolder("folder") + val file1 = tmpFile("apple") + val file2 = tmpFile("banana") + val file3 = tmpFile("cherry") + + runSortTest( + "A→Z asc", + mutableListOf(file3, folder, file1, file2), + FileSortOrderByName(SORT_A_TO_Z_ID, true), + listOf(folder, file1, file2, file3) + ) - assertEquals(listOf(f2, f1, f3), sorted) + runSortTest( + "A→Z desc", + mutableListOf(file3, folder, file1, file2), + FileSortOrderByName(SORT_A_TO_Z_ID, false), + listOf(folder, file3, file2, file1) + ) } @Test - fun testSortLocalFilesByNameNoConcurrentModification() { - val files = mutableListOf( - createTempFile("apple"), - createTempFile("banana"), - createTempFile("cherry"), - createTempFolder("folder") + fun sortNameCloud() { + val file1 = ocFile("/b.txt") + val file2 = ocFile("/a.txt") + val file3 = ocFile("/c.txt") + + runSortTest( + "cloud A→Z asc", + mutableListOf(file1, file2, file3), + FileSortOrderByName(SORT_A_TO_Z_ID, true), + listOf(file2, file1, file3), + isCloud = true ) - val sorter = FileSortOrderByName(SORT_A_TO_Z_ID, ascending = true) - - testConcurrentModification(files, sorter) } @Test - fun testSortLocalFilesBySizeAscending() { - val file1 = createTempFile("file1", sizeBytes = 100) - val file2 = createTempFile("file2", sizeBytes = 300) - val file3 = createTempFile("file3", sizeBytes = 200) - - val files = mutableListOf(file1, file2, file3) - val sorter = FileSortOrderBySize(SORT_SMALL_TO_BIG_ID, ascending = true) - - val sorted = sorter.sortLocalFiles(files) - - assertEquals(listOf(file1, file3, file2), sorted) + fun sortNameConcurrency() { + testConcurrency( + mutableListOf( + tmpFile("apple"), + tmpFile("banana"), + tmpFile("cherry"), + tmpFolder("folder") + ), + FileSortOrderByName(SORT_A_TO_Z_ID, true) + ) } @Test - fun testSortLocalFilesBySizeDescending() { - val file1 = createTempFile("file1", sizeBytes = 100) - val file2 = createTempFile("file2", sizeBytes = 300) - val file3 = createTempFile("file3", sizeBytes = 200) - val folder1 = createTempFolder("folder1") - val folder2 = createTempFolder("folder2") + fun sortSizeLocal() { + val file1 = tmpFile("file1", size = 100) + val file2 = tmpFile("file2", size = 300) + val file3 = tmpFile("file3", size = 200) + val d1 = tmpFolder("folder1") + val d2 = tmpFolder("folder2") + + runSortTest( + "small→big asc", + mutableListOf(file1, file2, file3), + FileSortOrderBySize(SORT_SMALL_TO_BIG_ID, true), + listOf(file1, file3, file2) + ) - val files = mutableListOf(file1, file2, file3, folder1, folder2) - val sorter = FileSortOrderBySize(SORT_BIG_TO_SMALL_ID, ascending = false) + runSortTest( + "small→big desc", + mutableListOf(file1, file2, file3), + FileSortOrderBySize(SORT_SMALL_TO_BIG_ID, false), + listOf(file2, file3, file1) + ) - val sorted = sorter.sortLocalFiles(files) + runSortTest( + "big→small asc (folders first)", + mutableListOf(file1, file2, file3, d1, d2), + FileSortOrderBySize(SORT_BIG_TO_SMALL_ID, true), + listOf(d1, d2, file1, file3, file2) + ) - assertEquals(listOf(folder1, folder2, file2, file3, file1), sorted) + runSortTest( + "big→small desc (folders first)", + mutableListOf(file1, file2, file3, d1, d2), + FileSortOrderBySize(SORT_BIG_TO_SMALL_ID, false), + listOf(d1, d2, file2, file3, file1) + ) } @Test - fun testSortCloudFilesBySize() { - val f1 = createOCFile("/file1.txt", size = 100) - val f2 = createOCFile("/file2.txt", size = 300) - val f3 = createOCFile("/file3.txt", size = 200) - - val files = mutableListOf(f1, f2, f3) - val sorter = FileSortOrderBySize(SORT_SMALL_TO_BIG_ID, ascending = true) - - val sorted = sorter.sortCloudFiles(files, foldersBeforeFiles = false, favoritesFirst = false) - - assertEquals(listOf(f1, f3, f2), sorted) + fun sortSizeCloud() { + val file1 = ocFile("/1", size = 100) + val file2 = ocFile("/2", size = 300) + val file3 = ocFile("/3", size = 200) + + runSortTest( + "cloud small→big asc", + mutableListOf(file1, file2, file3), + FileSortOrderBySize(SORT_SMALL_TO_BIG_ID, true), + listOf(file1, file3, file2), + isCloud = true + ) } @Test - fun testSortLocalFilesBySizeNoConcurrentModification() { - val files = mutableListOf( - createTempFile("file1", sizeBytes = 100), - createTempFile("file2", sizeBytes = 200), - createTempFile("file3", sizeBytes = 300), - createTempFolder("folder") + fun sortSizeConcurrency() { + testConcurrency( + mutableListOf( + tmpFile("file1", size = 100), + tmpFile("file2", size = 200), + tmpFile("file3", size = 300), + tmpFolder("folder") + ), + FileSortOrderBySize(SORT_SMALL_TO_BIG_ID, true) ) - val sorter = FileSortOrderBySize(SORT_SMALL_TO_BIG_ID, ascending = true) - - testConcurrentModification(files, sorter) } } From c47a42b4945c0597c97be7ba2b52a3eb15ef8090 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Thu, 20 Nov 2025 08:55:07 +0100 Subject: [PATCH 3/7] fix(file-sort): concurrent modification exception Signed-off-by: alperozturk --- .../androidTest/java/com/nextcloud/utils/FileSortOrderTests.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/app/src/androidTest/java/com/nextcloud/utils/FileSortOrderTests.kt b/app/src/androidTest/java/com/nextcloud/utils/FileSortOrderTests.kt index 3940e70a4269..0514c2fcfed4 100644 --- a/app/src/androidTest/java/com/nextcloud/utils/FileSortOrderTests.kt +++ b/app/src/androidTest/java/com/nextcloud/utils/FileSortOrderTests.kt @@ -23,6 +23,7 @@ import java.util.concurrent.CountDownLatch import java.util.concurrent.TimeUnit import kotlin.concurrent.thread +@Suppress("MagicNumber", "TooManyFunctions") class FileSortOrderTests { private fun tmpFile(prefix: String, lastModified: Long? = null, size: Int? = null): File = @@ -57,6 +58,7 @@ class FileSortOrderTests { assertEquals(name, expected, actual) } + @Suppress("TooGenericExceptionCaught") private fun testConcurrency(files: MutableList, sorter: FileSortOrder, iterations: Int = 50) { val latch = CountDownLatch(iterations * 2) val errors = mutableListOf() From c551da4bd6c7377ff613281cb9ebb00caf0dc070 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Thu, 20 Nov 2025 08:58:33 +0100 Subject: [PATCH 4/7] fix(file-sort): concurrent modification exception Signed-off-by: alperozturk --- .../com/nextcloud/utils/FileSortOrderTests.kt | 55 ++++++++++--------- 1 file changed, 28 insertions(+), 27 deletions(-) diff --git a/app/src/androidTest/java/com/nextcloud/utils/FileSortOrderTests.kt b/app/src/androidTest/java/com/nextcloud/utils/FileSortOrderTests.kt index 0514c2fcfed4..c2a7e54c2d5a 100644 --- a/app/src/androidTest/java/com/nextcloud/utils/FileSortOrderTests.kt +++ b/app/src/androidTest/java/com/nextcloud/utils/FileSortOrderTests.kt @@ -42,19 +42,23 @@ class FileSortOrderTests { size?.let { fileLength = it } } - private fun runSortTest( + private fun runSortFiles( name: String, - items: MutableList, + items: MutableList, sorter: FileSortOrder, - expected: List, - isCloud: Boolean = false + expected: List ) { - val actual = if (isCloud) { - sorter.sortCloudFiles(items as MutableList, false, false) - } else { - sorter.sortLocalFiles(items as MutableList) - } + val actual = sorter.sortLocalFiles(items) + assertEquals(name, expected, actual) + } + private fun runSortCloudFiles( + name: String, + items: MutableList, + sorter: FileSortOrder, + expected: List + ) { + val actual = sorter.sortCloudFiles(items, foldersBeforeFiles = false, favoritesFirst = false) assertEquals(name, expected, actual) } @@ -104,14 +108,14 @@ class FileSortOrderTests { val file2 = tmpFile("file2", 2000) val file3 = tmpFile("file3", 1500) - runSortTest( + runSortFiles( "old→new asc", mutableListOf(file1, file2, file3), FileSortOrderByDate(SORT_OLD_TO_NEW_ID, true), listOf(file1, file3, file2) ) - runSortTest( + runSortFiles( "old→new desc", mutableListOf(file1, file2, file3), FileSortOrderByDate(SORT_OLD_TO_NEW_ID, false), @@ -125,14 +129,14 @@ class FileSortOrderTests { val file2 = tmpFile("file2", 2000) val file3 = tmpFile("file3", 1500) - runSortTest( + runSortFiles( "new→old asc", mutableListOf(file1, file2, file3), FileSortOrderByDate(SORT_NEW_TO_OLD_ID, true), listOf(file1, file3, file2) ) - runSortTest( + runSortFiles( "new→old desc", mutableListOf(file1, file2, file3), FileSortOrderByDate(SORT_NEW_TO_OLD_ID, false), @@ -146,12 +150,11 @@ class FileSortOrderTests { val file2 = ocFile("/2", mod = 3000) val file3 = ocFile("/3", mod = 2000) - runSortTest( + runSortCloudFiles( "cloud old→new asc", mutableListOf(file1, file2, file3), FileSortOrderByDate(SORT_OLD_TO_NEW_ID, true), listOf(file1, file3, file2), - isCloud = true ) } @@ -172,14 +175,14 @@ class FileSortOrderTests { val file2 = tmpFile("banana") val file3 = tmpFile("cherry") - runSortTest( + runSortFiles( "A→Z asc", mutableListOf(file3, folder, file1, file2), FileSortOrderByName(SORT_A_TO_Z_ID, true), listOf(folder, file1, file2, file3) ) - runSortTest( + runSortFiles( "A→Z desc", mutableListOf(file3, folder, file1, file2), FileSortOrderByName(SORT_A_TO_Z_ID, false), @@ -193,12 +196,11 @@ class FileSortOrderTests { val file2 = ocFile("/a.txt") val file3 = ocFile("/c.txt") - runSortTest( + runSortCloudFiles( "cloud A→Z asc", mutableListOf(file1, file2, file3), FileSortOrderByName(SORT_A_TO_Z_ID, true), - listOf(file2, file1, file3), - isCloud = true + listOf(file2, file1, file3) ) } @@ -223,28 +225,28 @@ class FileSortOrderTests { val d1 = tmpFolder("folder1") val d2 = tmpFolder("folder2") - runSortTest( + runSortFiles( "small→big asc", mutableListOf(file1, file2, file3), FileSortOrderBySize(SORT_SMALL_TO_BIG_ID, true), listOf(file1, file3, file2) ) - runSortTest( + runSortFiles( "small→big desc", mutableListOf(file1, file2, file3), FileSortOrderBySize(SORT_SMALL_TO_BIG_ID, false), listOf(file2, file3, file1) ) - runSortTest( + runSortFiles( "big→small asc (folders first)", mutableListOf(file1, file2, file3, d1, d2), FileSortOrderBySize(SORT_BIG_TO_SMALL_ID, true), listOf(d1, d2, file1, file3, file2) ) - runSortTest( + runSortFiles( "big→small desc (folders first)", mutableListOf(file1, file2, file3, d1, d2), FileSortOrderBySize(SORT_BIG_TO_SMALL_ID, false), @@ -258,12 +260,11 @@ class FileSortOrderTests { val file2 = ocFile("/2", size = 300) val file3 = ocFile("/3", size = 200) - runSortTest( + runSortCloudFiles( "cloud small→big asc", mutableListOf(file1, file2, file3), FileSortOrderBySize(SORT_SMALL_TO_BIG_ID, true), - listOf(file1, file3, file2), - isCloud = true + listOf(file1, file3, file2) ) } From 016909804a1c68066151065d0804b4a0b300992a Mon Sep 17 00:00:00 2001 From: alperozturk Date: Tue, 25 Nov 2025 08:18:41 +0100 Subject: [PATCH 5/7] fix codacy Signed-off-by: alperozturk --- .../java/com/nextcloud/utils/FileSortOrderTests.kt | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/app/src/androidTest/java/com/nextcloud/utils/FileSortOrderTests.kt b/app/src/androidTest/java/com/nextcloud/utils/FileSortOrderTests.kt index c2a7e54c2d5a..0ba6eaa98647 100644 --- a/app/src/androidTest/java/com/nextcloud/utils/FileSortOrderTests.kt +++ b/app/src/androidTest/java/com/nextcloud/utils/FileSortOrderTests.kt @@ -42,12 +42,7 @@ class FileSortOrderTests { size?.let { fileLength = it } } - private fun runSortFiles( - name: String, - items: MutableList, - sorter: FileSortOrder, - expected: List - ) { + private fun runSortFiles(name: String, items: MutableList, sorter: FileSortOrder, expected: List) { val actual = sorter.sortLocalFiles(items) assertEquals(name, expected, actual) } @@ -154,7 +149,7 @@ class FileSortOrderTests { "cloud old→new asc", mutableListOf(file1, file2, file3), FileSortOrderByDate(SORT_OLD_TO_NEW_ID, true), - listOf(file1, file3, file2), + listOf(file1, file3, file2) ) } From 5534284b35f17e6851e1ebe5b1d8000ba72fc523 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Wed, 26 Nov 2025 09:24:04 +0100 Subject: [PATCH 6/7] dont change sorting logic Signed-off-by: alperozturk # Conflicts: # app/src/main/java/com/owncloud/android/ui/fragment/LocalFileListFragment.java --- .../com/nextcloud/utils/FileSortOrderTests.kt | 278 ------------------ .../ui/adapter/LocalFileListAdapter.java | 28 +- .../android/utils/FileSortOrderByDate.kt | 15 +- .../android/utils/FileSortOrderByName.kt | 21 +- .../android/utils/FileSortOrderBySize.kt | 18 +- 5 files changed, 43 insertions(+), 317 deletions(-) delete mode 100644 app/src/androidTest/java/com/nextcloud/utils/FileSortOrderTests.kt diff --git a/app/src/androidTest/java/com/nextcloud/utils/FileSortOrderTests.kt b/app/src/androidTest/java/com/nextcloud/utils/FileSortOrderTests.kt deleted file mode 100644 index 0ba6eaa98647..000000000000 --- a/app/src/androidTest/java/com/nextcloud/utils/FileSortOrderTests.kt +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Nextcloud - Android Client - * - * SPDX-FileCopyrightText: 2025 Alper Ozturk - * SPDX-License-Identifier: AGPL-3.0-or-later - */ -package com.nextcloud.utils - -import com.owncloud.android.datamodel.OCFile -import com.owncloud.android.utils.FileSortOrder -import com.owncloud.android.utils.FileSortOrder.Companion.SORT_A_TO_Z_ID -import com.owncloud.android.utils.FileSortOrder.Companion.SORT_BIG_TO_SMALL_ID -import com.owncloud.android.utils.FileSortOrder.Companion.SORT_NEW_TO_OLD_ID -import com.owncloud.android.utils.FileSortOrder.Companion.SORT_OLD_TO_NEW_ID -import com.owncloud.android.utils.FileSortOrder.Companion.SORT_SMALL_TO_BIG_ID -import com.owncloud.android.utils.FileSortOrderByDate -import com.owncloud.android.utils.FileSortOrderByName -import com.owncloud.android.utils.FileSortOrderBySize -import org.junit.Assert.assertEquals -import org.junit.Test -import java.io.File -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import kotlin.concurrent.thread - -@Suppress("MagicNumber", "TooManyFunctions") -class FileSortOrderTests { - - private fun tmpFile(prefix: String, lastModified: Long? = null, size: Int? = null): File = - File.createTempFile(prefix, ".txt").apply { - lastModified?.let { setLastModified(it) } - size?.let { writeBytes(ByteArray(it)) } - } - - private fun tmpFolder(prefix: String): File = File.createTempFile(prefix, "").apply { - delete() - mkdir() - } - - private fun ocFile(path: String, mod: Long? = null, size: Long? = null) = OCFile(path).apply { - mod?.let { modificationTimestamp = it } - size?.let { fileLength = it } - } - - private fun runSortFiles(name: String, items: MutableList, sorter: FileSortOrder, expected: List) { - val actual = sorter.sortLocalFiles(items) - assertEquals(name, expected, actual) - } - - private fun runSortCloudFiles( - name: String, - items: MutableList, - sorter: FileSortOrder, - expected: List - ) { - val actual = sorter.sortCloudFiles(items, foldersBeforeFiles = false, favoritesFirst = false) - assertEquals(name, expected, actual) - } - - @Suppress("TooGenericExceptionCaught") - private fun testConcurrency(files: MutableList, sorter: FileSortOrder, iterations: Int = 50) { - val latch = CountDownLatch(iterations * 2) - val errors = mutableListOf() - - repeat(iterations) { i -> - thread { - try { - sorter.sortLocalFiles(files) - } catch (e: Throwable) { - errors.add(e) - } finally { - latch.countDown() - } - } - - thread { - try { - files.add(tmpFile("tmp$i", lastModified = i.toLong())) - if (files.size > 20) files.removeAt(0) - } catch (e: Throwable) { - errors.add(e) - } finally { - latch.countDown() - } - } - } - - // Wait for threads to finish - val completed = latch.await(5, TimeUnit.SECONDS) - - if (errors.isNotEmpty()) { - throw AssertionError("Exceptions occurred in background threads: ${errors.first()}", errors.first()) - } - - if (!completed) { - throw AssertionError("Concurrent test timed out") - } - } - - @Test - fun sortDateOldToNew() { - val file1 = tmpFile("file1", 1000) - val file2 = tmpFile("file2", 2000) - val file3 = tmpFile("file3", 1500) - - runSortFiles( - "old→new asc", - mutableListOf(file1, file2, file3), - FileSortOrderByDate(SORT_OLD_TO_NEW_ID, true), - listOf(file1, file3, file2) - ) - - runSortFiles( - "old→new desc", - mutableListOf(file1, file2, file3), - FileSortOrderByDate(SORT_OLD_TO_NEW_ID, false), - listOf(file2, file3, file1) - ) - } - - @Test - fun sortDateNewToOld() { - val file1 = tmpFile("file1", 1000) - val file2 = tmpFile("file2", 2000) - val file3 = tmpFile("file3", 1500) - - runSortFiles( - "new→old asc", - mutableListOf(file1, file2, file3), - FileSortOrderByDate(SORT_NEW_TO_OLD_ID, true), - listOf(file1, file3, file2) - ) - - runSortFiles( - "new→old desc", - mutableListOf(file1, file2, file3), - FileSortOrderByDate(SORT_NEW_TO_OLD_ID, false), - listOf(file2, file3, file1) - ) - } - - @Test - fun sortDateCloud() { - val file1 = ocFile("/1", mod = 1000) - val file2 = ocFile("/2", mod = 3000) - val file3 = ocFile("/3", mod = 2000) - - runSortCloudFiles( - "cloud old→new asc", - mutableListOf(file1, file2, file3), - FileSortOrderByDate(SORT_OLD_TO_NEW_ID, true), - listOf(file1, file3, file2) - ) - } - - @Test - fun sortDateConcurrency() { - val items = mutableListOf( - tmpFile("file1", 1000), - tmpFile("file2", 2000), - tmpFile("file3", 1500) - ) - testConcurrency(items, FileSortOrderByDate(SORT_OLD_TO_NEW_ID, true)) - } - - @Test - fun sortNameLocal() { - val folder = tmpFolder("folder") - val file1 = tmpFile("apple") - val file2 = tmpFile("banana") - val file3 = tmpFile("cherry") - - runSortFiles( - "A→Z asc", - mutableListOf(file3, folder, file1, file2), - FileSortOrderByName(SORT_A_TO_Z_ID, true), - listOf(folder, file1, file2, file3) - ) - - runSortFiles( - "A→Z desc", - mutableListOf(file3, folder, file1, file2), - FileSortOrderByName(SORT_A_TO_Z_ID, false), - listOf(folder, file3, file2, file1) - ) - } - - @Test - fun sortNameCloud() { - val file1 = ocFile("/b.txt") - val file2 = ocFile("/a.txt") - val file3 = ocFile("/c.txt") - - runSortCloudFiles( - "cloud A→Z asc", - mutableListOf(file1, file2, file3), - FileSortOrderByName(SORT_A_TO_Z_ID, true), - listOf(file2, file1, file3) - ) - } - - @Test - fun sortNameConcurrency() { - testConcurrency( - mutableListOf( - tmpFile("apple"), - tmpFile("banana"), - tmpFile("cherry"), - tmpFolder("folder") - ), - FileSortOrderByName(SORT_A_TO_Z_ID, true) - ) - } - - @Test - fun sortSizeLocal() { - val file1 = tmpFile("file1", size = 100) - val file2 = tmpFile("file2", size = 300) - val file3 = tmpFile("file3", size = 200) - val d1 = tmpFolder("folder1") - val d2 = tmpFolder("folder2") - - runSortFiles( - "small→big asc", - mutableListOf(file1, file2, file3), - FileSortOrderBySize(SORT_SMALL_TO_BIG_ID, true), - listOf(file1, file3, file2) - ) - - runSortFiles( - "small→big desc", - mutableListOf(file1, file2, file3), - FileSortOrderBySize(SORT_SMALL_TO_BIG_ID, false), - listOf(file2, file3, file1) - ) - - runSortFiles( - "big→small asc (folders first)", - mutableListOf(file1, file2, file3, d1, d2), - FileSortOrderBySize(SORT_BIG_TO_SMALL_ID, true), - listOf(d1, d2, file1, file3, file2) - ) - - runSortFiles( - "big→small desc (folders first)", - mutableListOf(file1, file2, file3, d1, d2), - FileSortOrderBySize(SORT_BIG_TO_SMALL_ID, false), - listOf(d1, d2, file2, file3, file1) - ) - } - - @Test - fun sortSizeCloud() { - val file1 = ocFile("/1", size = 100) - val file2 = ocFile("/2", size = 300) - val file3 = ocFile("/3", size = 200) - - runSortCloudFiles( - "cloud small→big asc", - mutableListOf(file1, file2, file3), - FileSortOrderBySize(SORT_SMALL_TO_BIG_ID, true), - listOf(file1, file3, file2) - ) - } - - @Test - fun sortSizeConcurrency() { - testConcurrency( - mutableListOf( - tmpFile("file1", size = 100), - tmpFile("file2", size = 200), - tmpFile("file3", size = 300), - tmpFolder("folder") - ), - FileSortOrderBySize(SORT_SMALL_TO_BIG_ID, true) - ) - } -} diff --git a/app/src/main/java/com/owncloud/android/ui/adapter/LocalFileListAdapter.java b/app/src/main/java/com/owncloud/android/ui/adapter/LocalFileListAdapter.java index b8c8423226b4..91bff4ac8bee 100644 --- a/app/src/main/java/com/owncloud/android/ui/adapter/LocalFileListAdapter.java +++ b/app/src/main/java/com/owncloud/android/ui/adapter/LocalFileListAdapter.java @@ -43,6 +43,7 @@ import java.util.List; import java.util.Locale; import java.util.Set; +import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import androidx.annotation.NonNull; @@ -67,6 +68,7 @@ public class LocalFileListAdapter extends RecyclerView.Adapter checkedFiles; private ViewThemeUtils viewThemeUtils; private boolean isWithinEncryptedFolder; + private final ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor(); private static final int VIEWTYPE_ITEM = 0; private static final int VIEWTYPE_FOOTER = 1; @@ -372,7 +374,7 @@ public void swapDirectory(final File directory) { mFiles.clear(); mFilesAll.clear(); - Executors.newSingleThreadExecutor().execute(() -> { + singleThreadExecutor.execute(() -> { // Load first page of folders List firstPage = FileHelper.INSTANCE.listDirectoryEntries(directory, currentOffset, PAGE_SIZE, true); @@ -442,15 +444,25 @@ private void notifyItemRange(List updatedList) { }); } + private final Object filesLock = new Object(); + @SuppressLint("NotifyDataSetChanged") public void setSortOrder(FileSortOrder sortOrder) { localFileListFragmentInterface.setLoading(true); - final Handler uiHandler = new Handler(Looper.getMainLooper()); - Executors.newSingleThreadExecutor().execute(() -> { - preferences.setSortOrder(FileSortOrder.Type.localFileListView, sortOrder); - mFiles = sortOrder.sortLocalFiles(mFiles); + singleThreadExecutor.execute(() -> { + List sortedCopy; + synchronized (filesLock) { + sortedCopy = new ArrayList<>(mFiles); + } + + sortedCopy = sortOrder.sortLocalFiles(sortedCopy); - uiHandler.post(() -> { + List finalSortedCopy = sortedCopy; + new Handler(Looper.getMainLooper()).post(() -> { + synchronized (filesLock) { + mFiles = finalSortedCopy; + mFilesAll = new ArrayList<>(finalSortedCopy); + } notifyDataSetChanged(); localFileListFragmentInterface.setLoading(false); }); @@ -591,4 +603,8 @@ public void setFiles(List newFiles) { notifyDataSetChanged(); localFileListFragmentInterface.setLoading(false); } + + public void cleanup() { + singleThreadExecutor.shutdown(); + } } diff --git a/app/src/main/java/com/owncloud/android/utils/FileSortOrderByDate.kt b/app/src/main/java/com/owncloud/android/utils/FileSortOrderByDate.kt index da88a68a81ee..6d59836a9764 100644 --- a/app/src/main/java/com/owncloud/android/utils/FileSortOrderByDate.kt +++ b/app/src/main/java/com/owncloud/android/utils/FileSortOrderByDate.kt @@ -26,11 +26,10 @@ class FileSortOrderByDate(name: String, ascending: Boolean) : FileSortOrder(name favoritesFirst: Boolean ): MutableList { val multiplier = if (isAscending) 1 else -1 - val copy = files.toMutableList() - copy.sortWith { o1: OCFile, o2: OCFile -> + files.sortWith { o1: OCFile, o2: OCFile -> multiplier * o1.modificationTimestamp.compareTo(o2.modificationTimestamp) } - return super.sortCloudFiles(copy, foldersBeforeFiles, favoritesFirst) + return super.sortCloudFiles(files, foldersBeforeFiles, favoritesFirst) } /** @@ -40,11 +39,10 @@ class FileSortOrderByDate(name: String, ascending: Boolean) : FileSortOrder(name */ override fun sortTrashbinFiles(files: MutableList): List { val multiplier = if (isAscending) 1 else -1 - val copy = files.toMutableList() - copy.sortWith { o1: TrashbinFile, o2: TrashbinFile -> + files.sortWith { o1: TrashbinFile, o2: TrashbinFile -> multiplier * o1.deletionTimestamp.compareTo(o2.deletionTimestamp) } - return super.sortTrashbinFiles(copy) + return super.sortTrashbinFiles(files) } /** @@ -54,10 +52,9 @@ class FileSortOrderByDate(name: String, ascending: Boolean) : FileSortOrder(name */ override fun sortLocalFiles(files: MutableList): List { val multiplier = if (isAscending) 1 else -1 - val copy = files.toMutableList() - copy.sortWith { o1: File, o2: File -> + files.sortWith { o1: File, o2: File -> multiplier * o1.lastModified().compareTo(o2.lastModified()) } - return copy + return files } } diff --git a/app/src/main/java/com/owncloud/android/utils/FileSortOrderByName.kt b/app/src/main/java/com/owncloud/android/utils/FileSortOrderByName.kt index 48f4ed3b5f6e..cb39a2a03303 100644 --- a/app/src/main/java/com/owncloud/android/utils/FileSortOrderByName.kt +++ b/app/src/main/java/com/owncloud/android/utils/FileSortOrderByName.kt @@ -29,8 +29,7 @@ class FileSortOrderByName internal constructor(name: String?, ascending: Boolean foldersBeforeFiles: Boolean, favoritesFirst: Boolean ): MutableList { - val copy = files.toMutableList() - val sortedByName = sortOnlyByName(copy) + val sortedByName = sortOnlyByName(files) return super.sortCloudFiles(sortedByName, foldersBeforeFiles, favoritesFirst) } @@ -40,14 +39,12 @@ class FileSortOrderByName internal constructor(name: String?, ascending: Boolean * @param files files to sort */ override fun sortTrashbinFiles(files: MutableList): List { - val copy = files.toMutableList() - val sortedByName = sortServerFiles(copy) + val sortedByName = sortServerFiles(files) return super.sortTrashbinFiles(sortedByName) } private fun sortServerFiles(files: MutableList): MutableList { - val copy = files.toMutableList() - copy.sortWith { o1: ServerFileInterface, o2: ServerFileInterface -> + files.sortWith { o1: ServerFileInterface, o2: ServerFileInterface -> when { o1.isFolder && o2.isFolder -> sortMultiplier * AlphanumComparator.compare(o1, o2) o1.isFolder -> -1 @@ -55,13 +52,12 @@ class FileSortOrderByName internal constructor(name: String?, ascending: Boolean else -> sortMultiplier * AlphanumComparator.compare(o1, o2) } } - return copy + return files } private fun sortOnlyByName(files: MutableList): MutableList { - val copy = files.toMutableList() - copy.sortWith { o1: OCFile, o2: OCFile -> sortMultiplier * AlphanumComparator.compare(o1, o2) } - return copy + files.sortWith { o1: OCFile, o2: OCFile -> sortMultiplier * AlphanumComparator.compare(o1, o2) } + return files } /** @@ -70,8 +66,7 @@ class FileSortOrderByName internal constructor(name: String?, ascending: Boolean * @param files files to sort */ override fun sortLocalFiles(files: MutableList): List { - val copy = files.toMutableList() - copy.sortWith { o1: File, o2: File -> + files.sortWith { o1: File, o2: File -> when { o1.isDirectory && o2.isDirectory -> sortMultiplier * o1.path.lowercase(Locale.getDefault()) .compareTo(o2.path.lowercase(Locale.getDefault())) @@ -83,6 +78,6 @@ class FileSortOrderByName internal constructor(name: String?, ascending: Boolean ) } } - return copy + return files } } diff --git a/app/src/main/java/com/owncloud/android/utils/FileSortOrderBySize.kt b/app/src/main/java/com/owncloud/android/utils/FileSortOrderBySize.kt index e4f9e4689e8e..5b188f1c4e86 100644 --- a/app/src/main/java/com/owncloud/android/utils/FileSortOrderBySize.kt +++ b/app/src/main/java/com/owncloud/android/utils/FileSortOrderBySize.kt @@ -22,20 +22,17 @@ class FileSortOrderBySize internal constructor(name: String?, ascending: Boolean foldersBeforeFiles: Boolean, favoritesFirst: Boolean ): MutableList { - val copy = files.toMutableList() - val sortedBySize = sortServerFiles(copy) + val sortedBySize = sortServerFiles(files) return super.sortCloudFiles(sortedBySize, foldersBeforeFiles, favoritesFirst) } override fun sortTrashbinFiles(files: MutableList): List { - val copy = files.toMutableList() - val sortedBySize = sortServerFiles(copy) + val sortedBySize = sortServerFiles(files) return super.sortTrashbinFiles(sortedBySize) } private fun sortServerFiles(files: MutableList): MutableList { - val copy = files.toMutableList() - copy.sortWith { o1: ServerFileInterface, o2: ServerFileInterface -> + files.sortWith { o1: ServerFileInterface, o2: ServerFileInterface -> when { o1.isFolder && o2.isFolder -> sortMultiplier * o1.fileLength.compareTo(o2.fileLength) o1.isFolder -> -1 @@ -43,15 +40,14 @@ class FileSortOrderBySize internal constructor(name: String?, ascending: Boolean else -> sortMultiplier * o1.fileLength.compareTo(o2.fileLength) } } - return copy + return files } override fun sortLocalFiles(files: MutableList): List { - val copy = files.toMutableList() val folderSizes = - copy.associateWith { file -> FileStorageUtils.getFolderSize(file) } + files.associateWith { file -> FileStorageUtils.getFolderSize(file) } - copy.sortWith { o1: File, o2: File -> + files.sortWith { o1: File, o2: File -> when { o1.isDirectory && o2.isDirectory -> sortMultiplier * (folderSizes[o1] ?: 0L).compareTo( folderSizes[o2] ?: 0L @@ -61,6 +57,6 @@ class FileSortOrderBySize internal constructor(name: String?, ascending: Boolean else -> sortMultiplier * o1.length().compareTo(o2.length()) } } - return copy + return files } } From 56f5516de5c65c834441444209e164131884bbd2 Mon Sep 17 00:00:00 2001 From: alperozturk Date: Mon, 1 Dec 2025 08:18:08 +0100 Subject: [PATCH 7/7] fix git conflict Signed-off-by: alperozturk --- .../owncloud/android/ui/fragment/LocalFileListFragment.java | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/app/src/main/java/com/owncloud/android/ui/fragment/LocalFileListFragment.java b/app/src/main/java/com/owncloud/android/ui/fragment/LocalFileListFragment.java index ba732d1b788a..26b3f8de3d6a 100644 --- a/app/src/main/java/com/owncloud/android/ui/fragment/LocalFileListFragment.java +++ b/app/src/main/java/com/owncloud/android/ui/fragment/LocalFileListFragment.java @@ -425,4 +425,10 @@ public interface ContainerActivity { public void setupStoragePermissionWarningBanner() { mAdapter.notifyDataSetChanged(); } + + @Override + public void onDestroyView() { + mAdapter.cleanup(); + super.onDestroyView(); + } }