From af36ae566363076afa5620365ffebd592a56395a Mon Sep 17 00:00:00 2001 From: Kyle Bolen Date: Tue, 28 Jul 2026 04:22:23 +0000 Subject: [PATCH] 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. --- .../photophetch/device/AndroidConnector.kt | 32 ++++++++++++++----- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/src/main/kotlin/com/bolenpad/photophetch/device/AndroidConnector.kt b/src/main/kotlin/com/bolenpad/photophetch/device/AndroidConnector.kt index c54f51f..a7b0a09 100644 --- a/src/main/kotlin/com/bolenpad/photophetch/device/AndroidConnector.kt +++ b/src/main/kotlin/com/bolenpad/photophetch/device/AndroidConnector.kt @@ -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() - 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() + + 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"))