Copying pictures with ADB

Posted on January 18, 2016
Tags: haskell, software

I’ve been copying pictures from my Android phone to my Mac using Android File Transfer. It worked great when my pictures were stored in the phone’s internal storage. However, now that I’m storing my pictures on an external SD card, it has become unreliable. More and more frequently, I’ve been getting:

Can't access device storage - If your device's screen is locked, disconnect its USB cable, unlock your screen, and then reconnect the USB cable.

despite having my screen unlocked. Unplugging and reconnecting the USB cable repeatedly sometimes works eventually, but lately it’s been getting less and less likely to work. I wish software would just work correctly, instead of being so buggy and crappy. But that’s a gripe for another post sometime.

Anyway, I decided to use the Android Debug Bridge (adb) command-line tool to copy my photos. I wrote the following Haskell program to determine which photos I don’t yet have on my computer, and then copy them. It works great!

#!/usr/bin/env runhaskell

import Data.List
import System.Directory
import System.IO
import System.Process

remoteDir = "/storage/extSdCard/DCIM/Camera"
localDir = "/Users/ppelleti/Pictures/Android"

getRemoteFiles :: String -> IO [String]
getRemoteFiles remoteDir = do
  files <- readProcess "adb" ["shell", "ls", remoteDir] ""
  return $ lines $ filter (/= '\r') files

getLocalFiles :: String -> IO [String]
getLocalFiles localDir = do
  files <- getDirectoryContents localDir
  return $ filter shouldKeep files
  where shouldKeep ('.':_ ) = False
        shouldKeep _ = True

copyOneFile :: String -> String -> IO ()
copyOneFile remoteFile localFile =
  cmd ["adb", "pull", "-a", remoteFile, localFile]

cmd :: [String] -> IO ()
cmd (exe:args) = do
  putStrLn $ unwords $ exe : map show args
  callProcess exe args

copyFiles :: String -> String -> [String] -> IO ()
copyFiles rDir lDir files =
  mapM_ cfile files
  where cfile file = copyOneFile (rDir ++ "/" ++ file) (lDir ++ "/" ++ file)

copyNewFiles :: String -> String -> IO ()
copyNewFiles rDir lDir = do
  rFiles <- getRemoteFiles rDir
  lFiles <- getLocalFiles lDir
  let files = rFiles \\ lFiles
  copyFiles rDir lDir files

main = copyNewFiles remoteDir localDir

Also available as a gist.