Fix Android photo discovery: dynamically scan all DCIM subdirs

Instead of hardcoding /sdcard/DCIM/Camera, list all subdirectories
under /sdcard/DCIM and /storage/emulated/0/DCIM dynamically. This
handles Samsung, Pixel, OnePlus and other vendors that use different
DCIM subfolder names. Falls back to hardcoded list if discovery finds
nothing.
This commit is contained in:
Kyle Bolen
2026-07-28 04:22:23 +00:00
parent 5b17d28f66
commit af36ae5663

View File

@@ -24,8 +24,9 @@ class AndroidConnector(
private val log = LoggerFactory.getLogger(AndroidConnector::class.java)
// Directories on Android that contain camera-roll originals
private val cameraDirs = listOf(
// Directories on Android that contain camera-roll originals.
// Used as fallback if dynamic DCIM discovery finds nothing.
private val fallbackCameraDirs = listOf(
"/sdcard/DCIM/Camera",
"/sdcard/DCIM/100ANDRO",
"/sdcard/Pictures/Camera",
@@ -65,13 +66,30 @@ class AndroidConnector(
val serial = requireSerial(device)
val photos = mutableListOf<PhonePhoto>()
for (dir in cameraDirs) {
// Check if directory exists first
// Dynamically discover all subdirectories under DCIM on common storage paths
val dcimRoots = listOf("/sdcard/DCIM", "/storage/emulated/0/DCIM")
val cameraDirs = mutableListOf<String>()
for (root in dcimRoots) {
val lsOutput = runAdb(serial, "shell", "ls", root, "2>/dev/null")
if (lsOutput.contains("No such file") || lsOutput.isBlank()) continue
lsOutput.lines()
.map { it.trim() }
.filter { it.isNotBlank() && !it.contains("No such file") }
.forEach { subdir -> cameraDirs.add("$root/$subdir") }
}
// Also check Pictures/Camera (some Samsung models store there)
cameraDirs.add("/sdcard/Pictures/Camera")
cameraDirs.add("/storage/emulated/0/Pictures/Camera")
// Fall back to hardcoded list if dynamic discovery found nothing
val dirsToScan = cameraDirs.ifEmpty { fallbackCameraDirs }
for (dir in dirsToScan) {
val check = runAdb(serial, "shell", "ls", dir, "2>/dev/null")
if (check.contains("No such file") || check.isBlank()) continue
// List files with metadata: size, date, time, name
// `stat` format: size=%s mtime=%Y path=%n
val statOutput = runAdb(
serial, "shell",
"find", dir, "-maxdepth", "1", "-type", "f",
@@ -98,8 +116,6 @@ class AndroidConnector(
filename = filename,
sizeBytes = sizeBytes,
dateTaken = Instant.ofEpochSecond(epochSecs),
// Android stat doesn't give EXIF; we infer Make/Model from
// the device itself rather than per-file EXIF
exifMake = device.displayName,
exifModel = device.displayName,
mediaType = if (ext in setOf("mp4", "mov", "3gp", "avi"))