Natural file name sorting

This commit is contained in:
Matéo Duparc 2022-01-13 19:23:37 +01:00
parent f15b17c936
commit 95eed07719
Signed by untrusted user: hardcoresushi
GPG Key ID: 007F84120107191E
5 changed files with 241 additions and 3 deletions

View File

@ -151,7 +151,8 @@ Thanks to these open source projects that DroidFS uses:
### Modified code: ### Modified code:
- [gocryptfs](https://github.com/rfjakob/gocryptfs) to encrypt your data - [gocryptfs](https://github.com/rfjakob/gocryptfs) to encrypt your data
### Borrowed code:
- [MaterialFiles](https://github.com/zhanghai/MaterialFiles) for kotlin natural sorting implementation
### Libraries: ### Libraries:
- [Cyanea](https://github.com/jaredrummler/Cyanea) to customize UI
- [Glide](https://github.com/bumptech/glide/) to display pictures - [Glide](https://github.com/bumptech/glide/) to display pictures
- [ExoPlayer](https://github.com/google/ExoPlayer) to play media files - [ExoPlayer](https://github.com/google/ExoPlayer) to play media files

View File

@ -0,0 +1,72 @@
/*
* Code borrowed from the awesome Material Files app (https://github.com/zhanghai/MaterialFiles)
*
* Copyright (c) 2019 Hai Zhang <dreaming.in.code.zh@gmail.com>
* All Rights Reserved.
*/
package sushi.hardcore.droidfs.collation
import kotlin.math.min
class ByteString internal constructor(
private val bytes: ByteArray
) : Comparable<ByteString> {
fun borrowBytes(): ByteArray = bytes
private var stringCache: String? = null
override fun toString(): String {
// We are okay with the potential race condition here.
var string = stringCache
if (string == null) {
// String() uses replacement char instead of throwing exception.
string = String(bytes)
stringCache = string
}
return string
}
override fun equals(other: Any?): Boolean {
if (this === other) {
return true
}
if (javaClass != other?.javaClass) {
return false
}
other as ByteString
return bytes contentEquals other.bytes
}
override fun hashCode(): Int = bytes.contentHashCode()
override fun compareTo(other: ByteString): Int = bytes.compareTo(other.bytes)
private fun ByteArray.compareTo(other: ByteArray): Int {
val size = size
val otherSize = other.size
for (index in 0 until min(size, otherSize)) {
val byte = this[index]
val otherByte = other[index]
val result = byte - otherByte
if (result != 0) {
return result
}
}
return size - otherSize
}
companion object {
fun fromBytes(bytes: ByteArray, start: Int = 0, end: Int = bytes.size): ByteString =
ByteString(bytes.copyOfRange(start, end))
fun fromString(string: String): ByteString =
ByteString(string.toByteArray()).apply { stringCache = string }
}
}
fun ByteArray.toByteString(start: Int = 0, end: Int = size): ByteString =
ByteString.fromBytes(this, start, end)
fun String.toByteString(): ByteString = ByteString.fromString(this)

View File

@ -0,0 +1,47 @@
/*
* Code borrowed from the awesome Material Files app (https://github.com/zhanghai/MaterialFiles)
*
* Copyright (c) 2019 Hai Zhang <dreaming.in.code.zh@gmail.com>
* All Rights Reserved.
*/
package sushi.hardcore.droidfs.collation
class ByteStringBuilder(capacity: Int = 16) {
private var bytes = ByteArray(capacity)
var length = 0
private set
fun append(byte: Byte): ByteStringBuilder {
ensureCapacity(length + 1)
bytes[length] = byte
++length
return this
}
fun append(bytes: ByteArray, start: Int = 0, end: Int = bytes.size): ByteStringBuilder {
val newLength = length + (end - start)
ensureCapacity(newLength)
bytes.copyInto(this.bytes, length, start, end)
length = newLength
return this
}
fun append(byteString: ByteString): ByteStringBuilder = append(byteString.borrowBytes())
private fun ensureCapacity(minimumCapacity: Int) {
val capacity = bytes.size
if (minimumCapacity > capacity) {
var newCapacity = (capacity shl 1) + 2
if (newCapacity < minimumCapacity) {
newCapacity = minimumCapacity
}
bytes = bytes.copyOf(newCapacity)
}
}
fun toByteString(): ByteString = bytes.toByteString(0, length)
override fun toString(): String = String(bytes, 0, length)
}

View File

@ -0,0 +1,115 @@
/*
* Code borrowed from the awesome Material Files app (https://github.com/zhanghai/MaterialFiles)
*
* Copyright (c) 2020 Hai Zhang <dreaming.in.code.zh@gmail.com>
* All Rights Reserved.
*/
package sushi.hardcore.droidfs.collation
import java.text.CollationKey
import java.text.Collator
import kotlin.math.min
private val COLLATION_SENTINEL = byteArrayOf(1, 1, 1)
// @see https://github.com/GNOME/glib/blob/mainline/glib/gunicollate.c
// g_utf8_collate_key_for_filename()
fun Collator.getCollationKeyForFileName(source: String): CollationKey {
val result = ByteStringBuilder()
val suffix = ByteStringBuilder()
var previousIndex = 0
var index = 0
val endIndex = source.length
while (index < endIndex) {
when {
source[index] == '.' -> {
if (previousIndex != index) {
val collationKey = getCollationKey(source.substring(previousIndex, index))
result.append(collationKey.toByteArray())
}
result.append(COLLATION_SENTINEL).append(1)
previousIndex = index + 1
}
source[index].isAsciiDigit() -> {
if (previousIndex != index) {
val collationKey = getCollationKey(source.substring(previousIndex, index))
result.append(collationKey.toByteArray())
}
result.append(COLLATION_SENTINEL).append(2)
previousIndex = index
var leadingZeros: Int
var digits: Int
if (source[index] == '0') {
leadingZeros = 1
digits = 0
} else {
leadingZeros = 0
digits = 1
}
while (++index < endIndex) {
if (source[index] == '0' && digits == 0) {
++leadingZeros
} else if (source[index].isAsciiDigit()) {
++digits
} else {
if (digits == 0) {
++digits
--leadingZeros
}
break
}
}
while (digits > 1) {
result.append(':'.code.toByte())
--digits
}
if (leadingZeros > 0) {
suffix.append(leadingZeros.toByte())
previousIndex += leadingZeros
}
result.append(source.substring(previousIndex, index).toByteString())
previousIndex = index
--index
}
else -> {}
}
++index
}
if (previousIndex != index) {
val collationKey = getCollationKey(source.substring(previousIndex, index))
result.append(collationKey.toByteArray())
}
result.append(suffix.toByteString())
return ByteArrayCollationKey(source, result.toByteString().borrowBytes())
}
private fun Char.isAsciiDigit(): Boolean = this in '0'..'9'
private class ByteArrayCollationKey(
@Suppress("CanBeParameter")
private val source: String,
private val bytes: ByteArray
) : CollationKey(source) {
override fun compareTo(other: CollationKey): Int {
other as ByteArrayCollationKey
return bytes.unsignedCompareTo(other.bytes)
}
override fun toByteArray(): ByteArray = bytes.copyOf()
}
private fun ByteArray.unsignedCompareTo(other: ByteArray): Int {
val size = size
val otherSize = other.size
for (index in 0 until min(size, otherSize)) {
val byte = this[index].toInt() and 0xFF
val otherByte = other[index].toInt() and 0xFF
if (byte < otherByte) {
return -1
} else if (byte > otherByte) {
return 1
}
}
return size - otherSize
}

View File

@ -1,11 +1,14 @@
package sushi.hardcore.droidfs.explorers package sushi.hardcore.droidfs.explorers
import sushi.hardcore.droidfs.collation.getCollationKeyForFileName
import sushi.hardcore.droidfs.util.PathUtils import sushi.hardcore.droidfs.util.PathUtils
import java.text.Collator
import java.util.* import java.util.*
class ExplorerElement(val name: String, val elementType: Short, var size: Long = -1, mTime: Long = -1, val parentPath: String) { class ExplorerElement(val name: String, val elementType: Short, var size: Long = -1, mTime: Long = -1, val parentPath: String) {
val mTime = Date((mTime * 1000).toString().toLong()) val mTime = Date((mTime * 1000).toString().toLong())
val fullPath: String = PathUtils.pathJoin(parentPath, name) val fullPath: String = PathUtils.pathJoin(parentPath, name)
val collationKey = Collator.getInstance().getCollationKeyForFileName(fullPath)
val isDirectory: Boolean val isDirectory: Boolean
get() = elementType.toInt() == DIRECTORY_TYPE get() = elementType.toInt() == DIRECTORY_TYPE
@ -53,7 +56,7 @@ class ExplorerElement(val name: String, val elementType: Short, var size: Long =
when (sortOrder) { when (sortOrder) {
"name" -> { "name" -> {
explorerElements.sortWith { a, b -> explorerElements.sortWith { a, b ->
doSort(a, b, foldersFirst) { a.fullPath.compareTo(b.fullPath, true) } doSort(a, b, foldersFirst) { a.collationKey.compareTo(b.collationKey) }
} }
} }
"size" -> { "size" -> {
@ -68,7 +71,7 @@ class ExplorerElement(val name: String, val elementType: Short, var size: Long =
} }
"name_desc" -> { "name_desc" -> {
explorerElements.sortWith { a, b -> explorerElements.sortWith { a, b ->
doSort(a, b, foldersFirst) { b.fullPath.compareTo(a.fullPath, true) } doSort(a, b, foldersFirst) { b.collationKey.compareTo(a.collationKey) }
} }
} }
"size_desc" -> { "size_desc" -> {