Skip to content

Rahuletto/droidmount

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

23 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

AndroidMount

A native macOS menu-bar app that auto-mounts your Android phone as a real Finder volume over MTP, using libmtp + macFUSE.

No window. No drag-and-drop app. Plug your phone in, pick "File Transfer" on the device, and use the menu bar item → Open in Finder. The mount lives under ~/.AndroidMount/<device-name>/ (a hidden folder in your home directory; volname still labels the volume in Finder).

┌──────────────────────┐    USB    ┌─────────────┐
│  AndroidMount.app    │◄─────────►│  Android    │
│  (Swift, menu bar)   │   IOKit   │  phone      │
│        │             │           │  (MTP mode) │
│        ▼             │           └─────────────┘
│  spawns `mtpfuse`    │
│  (C, libmtp + FUSE)  │──► ~/.AndroidMount/... (macFUSE volume)
└──────────────────────┘

Prerequisites

  1. Xcode command line toolsxcode-select --install
  2. Homebrew + libmtpbrew install libmtp
  3. macFUSE – download from https://osxfuse.github.io, install, and reboot (it needs to load a kernel extension).
  4. Android device with MTP / "File Transfer" mode enabled (not PTP or charge-only).

Build and run

From the directory that contains this Makefile (repository root):

make check-deps     # verifies macFUSE + libmtp are reachable
make                # builds build/AndroidMount.app
make run            # rebuild bundle, kill AndroidMount/mtpfuse/Finder, open the app
./scripts/run.sh    # clean + full build + open

Use ANDROIDMOUNT_SKIP_KILL=1 make run to open without killing processes first.

Using it: unlock the phone, set USB to File Transfer / MTP, then launch the app (or keep it running). When the menu shows Connected, choose Open in Finder (⌘O). Eject before unplugging.

Debug mtpfuse: to see [LOAD] / libmtp logs on stderr, run the helper by hand after the phone is connected (adjust paths if needed):

mkdir -p "$HOME/.AndroidMount/Debug"
./build/mtpfuse -f -o volname=Debug "$HOME/.AndroidMount/Debug"

The Makefile compiles two binaries:

Binary Source Purpose
AndroidMount AndroidMount/*.swift menu-bar app, USB watcher
mtpfuse MTPFuse/*.c FUSE daemon (libmtp bridge)

Both are bundled into build/AndroidMount.app. The app finds the helper via Bundle.main.bundleURL/Contents/MacOS/mtpfuse.

Developers & coding agents: see AGENTS.md (repo overview) and the project Cursor skill .cursor/skills/droidmount-mtp/SKILL.md (MTP/FUSE behavior, env vars, partial vs full reads, mutex rules). Update those when you change bridge or FUSE semantics.

The build is ad-hoc signed (codesign -s -). No Apple Developer account is required, and Gatekeeper will allow the app on the machine that built it. macOS may still prompt the first time you launch.

How it works

  • USBWatcher.swiftIOServiceAddMatchingNotification on kIOUSBDeviceClassName, filtering by known Android vendor IDs (Google 0x18D1, Samsung 0x04E8, etc.) and "MTP" interface strings.
  • MountManager.swift – on connect, spawns mtpfuse -f -o … volname=… on ~/.AndroidMount/<device>/. On disconnect (or "Eject"), runs diskutil unmount force then terminate() + SIGKILL fallback.
  • mtp_bridge.c – wraps libmtp: LIBMTP_Init, Detect_Raw_Devices / Open_Raw_Device_Uncached, Get_Storage, Get_Files_And_Folders one folder level at a time (lazy tree under path → object_id).
  • fs_ops.c – FUSE 2.x getattr / readdir / read / write / rename / unlink / mkdir / rmdir / create / release / truncate. Reads/writes are staged through a per-handle temp file because MTP is request/response and not seekable; on release() a dirty handle is streamed back to the device with LIBMTP_Send_File_From_File_Descriptor (no full-file malloc).

Limitations

  • Several phones at once: the menu-bar app starts one mtpfuse per USB locationID under ~/.AndroidMount/<name>_<locationHex>/. Each helper sets MTP_USB_BUS_LOCATION so libmtp opens the matching raw device (bus_location). If a device is not found, set MTP_RAW_INDEX or check stderr for the listed bus_location values.
  • Writes happen on release(), so very large copies still need enough free disk space for the staging file under /tmp.
  • Rename / move uses LIBMTP_Set_Object_Filename and LIBMTP_Move_Object; moving a folder to another parent refreshes the in-memory tree. Some vendor MTP stacks may behave oddly.
  • There is no widely used “modern” replacement for libmtp on macOS for Android file transfer; this project stays on libmtp for device compatibility.
  • macFUSE is required; if the kext is missing the app pops a one-shot alert with a link to the installer instead of crashing.

Project layout

AndroidMount/
├── Makefile
├── README.md
├── AndroidMount/         ← Swift menu-bar app
│   ├── AppDelegate.swift
│   ├── USBWatcher.swift
│   ├── MountManager.swift
│   └── Info.plist        ← LSUIElement = YES
└── MTPFuse/              ← C FUSE daemon
    ├── main.c
    ├── fs_ops.c
    ├── fs_ops.h
    ├── mtp_bridge.c
    └── mtp_bridge.h

Troubleshooting

  • Action log (FUSE + MTP): the menu-bar app sets MTPFUSE_LOG=1 and MTPFUSE_LOG_PATH=~/.AndroidMount/mtpfuse.log (append, session banners per process). Each FUSE op is logged with op=…, path=…, rc=, and dt=…ms (and read/write include size and offset). The bridge’s existing mtp_log lines (MTP I/O) go to the same file when enabled. Watch live:

    tail -F ~/.AndroidMount/mtpfuse.log

    A stable symlink is created at /tmp/mtpfuse-debug-latest.log when the log file opens. Disable file logging: MTPFUSE_LOG=0. Enable from the shell without the app: MTPFUSE_LOG=1 and optionally MTPFUSE_LOG_PATH=~/.AndroidMount/mtpfuse.log (same rules as MTPFUSE_DEBUG_LOG for a custom path). MTPFUSE_LOG_SYNC=1 flushes every line (slower, safer if the process is killed). MTPFUSE_DEBUG=1 also turns logging on. Hot-path volume queries: if statfs lines are too noisy, we can add a filter in a follow-up.

  • Finder stuck on “Loading…” – use the log above. Long gaps between readdir_snapshot and Get_Files_And_Folders mean the phone or MTP listing is slow for that folder; many fuse op=read / bridge mtp_read lines mean Finder is reading data (e.g. previews).

  • “Mount point is already in use” – usually a leftover macFUSE mount after Finder or the app hung. On the next connect, the app force-unmounts that path and stops matching mtpfuse processes automatically; if it still fails, run Eject in the menu or diskutil unmount force ~/.AndroidMount/<device>.

  • "MTPFuse helper binary not found" – run make (or make run); the app looks for mtpfuse next to its own executable.

  • fuse: device not found, try 'modprobe fuse' first – macFUSE kext was blocked. Open System Settings → Privacy & Security and click Allow next to "System software from developer …", then reboot.

  • No raw devices found when the daemon starts – the phone is still in Charging mode. Pull down the notification shade on the device and switch USB usage to File Transfer.

  • LIBMTP_Open_Raw_Device fails while the phone shows as connected – another app may already hold the MTP USB session (same class of issue SwiftMTP documents for LIBUSB_ERROR_NOT_FOUND): quit Preview, Image Capture, and Android File Transfer (including its background agent in Activity Monitor), then unplug/replug. mtpfuse retries detection/open a few times with a short delay to ride out USB settle.

  • Optional environment (passed to mtpfuse): set MTP_SKIP_HIDDEN=1 or MTP_HIDE_DOTFILES=1 to omit device entries whose names start with . (Finder-style “no dotfiles” listing; off by default).

  • Mount appears but is empty – tap Allow on the phone the very first time the Mac connects; MTP requires per-host authorization.

About

Connect your android device with macOS like its another external drive

Topics

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Contributors