Fix Android file listing: replace stat with ls -la for compatibility
Samsung (and other Android vendors using toybox) do not support GNU stat -c flag syntax. Switch to find for file discovery + ls -la per file for size/date metadata, which works on all Android versions.
This commit is contained in:
@@ -90,32 +90,34 @@ class AndroidConnector(
|
||||
val check = runAdb(serial, "shell", "ls", dir, "2>/dev/null")
|
||||
if (check.contains("No such file") || check.isBlank()) continue
|
||||
|
||||
val statOutput = runAdb(
|
||||
// Use `find` to get filenames, then `ls -la` for size/date.
|
||||
// Avoids relying on `stat` which has inconsistent flag support across
|
||||
// Android versions (Samsung/toybox vs GNU coreutils).
|
||||
val findOutput = runAdb(
|
||||
serial, "shell",
|
||||
"find", dir, "-maxdepth", "1", "-type", "f",
|
||||
"-exec", "stat", "-c", "%s %Y %n", "{}", ";"
|
||||
"find", dir, "-maxdepth", "1", "-type", "f"
|
||||
)
|
||||
|
||||
for (line in statOutput.lines()) {
|
||||
val trimmed = line.trim()
|
||||
if (trimmed.isBlank()) continue
|
||||
val parts = trimmed.split(" ", limit = 3)
|
||||
if (parts.size < 3) continue
|
||||
for (line in findOutput.lines()) {
|
||||
val path = line.trim()
|
||||
if (path.isBlank() || path.contains("No such file")) continue
|
||||
|
||||
val sizeBytes = parts[0].toLongOrNull() ?: continue
|
||||
val epochSecs = parts[1].toLongOrNull() ?: continue
|
||||
val path = parts[2]
|
||||
val filename = path.substringAfterLast("/")
|
||||
val ext = filename.substringAfterLast(".", "").lowercase()
|
||||
|
||||
if (ext !in mediaExtensions) continue
|
||||
|
||||
// Get size and modification time via ls -la on the specific file
|
||||
val lsLine = runAdb(serial, "shell", "ls", "-la", path).trim()
|
||||
// ls -la format: -rw-rw---- 1 root everybody 3821234 2026-07-15 14:22 /sdcard/...
|
||||
val sizeBytes = parseLsSize(lsLine) ?: 0L
|
||||
val dateTaken = parseLsDate(lsLine) ?: java.time.Instant.EPOCH
|
||||
|
||||
photos.add(
|
||||
PhonePhoto(
|
||||
devicePath = path,
|
||||
filename = filename,
|
||||
sizeBytes = sizeBytes,
|
||||
dateTaken = Instant.ofEpochSecond(epochSecs),
|
||||
dateTaken = dateTaken,
|
||||
exifMake = device.displayName,
|
||||
exifModel = device.displayName,
|
||||
mediaType = if (ext in setOf("mp4", "mov", "3gp", "avi"))
|
||||
@@ -173,6 +175,36 @@ class AndroidConnector(
|
||||
private fun requireSerial(device: DeviceInfo): String =
|
||||
device.adbSerial ?: throw DeviceException("No ADB serial for device: ${device.displayName}")
|
||||
|
||||
/**
|
||||
* Parse file size from `ls -la` output line.
|
||||
* Format: -rw-rw---- 1 root everybody 3821234 2026-07-15 14:22 filename
|
||||
* Size is the 5th whitespace-separated token.
|
||||
*/
|
||||
private fun parseLsSize(lsLine: String): Long? {
|
||||
val parts = lsLine.trim().split(Regex("\\s+"))
|
||||
// Size is at index 4 (0-based): permissions, links, user, group, SIZE, date, time, name
|
||||
return parts.getOrNull(4)?.toLongOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse modification date from `ls -la` output line.
|
||||
* Date token is at index 5 (yyyy-MM-dd) and time at index 6 (HH:mm).
|
||||
*/
|
||||
private fun parseLsDate(lsLine: String): java.time.Instant? {
|
||||
return try {
|
||||
val parts = lsLine.trim().split(Regex("\\s+"))
|
||||
val datePart = parts.getOrNull(5) ?: return null
|
||||
val timePart = parts.getOrNull(6) ?: "00:00"
|
||||
val ldt = java.time.LocalDateTime.parse(
|
||||
"$datePart $timePart",
|
||||
java.time.format.DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
|
||||
)
|
||||
ldt.atZone(java.time.ZoneId.systemDefault()).toInstant()
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun runAdb(serial: String?, vararg args: String): String {
|
||||
val cmd = buildList {
|
||||
add(adbBin)
|
||||
|
||||
Reference in New Issue
Block a user