Skip to content

Releases: sayanthns/filebridge

File Bridge 1.19.0 — the cable pairs itself

Choose a tag to compare

@sayanthns sayanthns released this 18 Sep 11:56

The cable pairs itself now. Server and Mac app only — the APK is unchanged from
v1.17.0, attached
here so the newest release always carries a working one.

Autopair

This is the whole reason the watch loop exists rather than a button alone.

On the one occasion this phone did publish an adb interface, it was there for
less time than it takes to notice and click. The tunnel came up — armed was
populated — and the pairing went unused because nobody was fast enough. A human
cannot reliably catch a five-second window; a watchdog can.

The loop now arms the reverse mapping and fires the deep link on its own, once
per appearance
. Once matters: am start every 5 s would keep yanking the app
to the foreground. A serial is forgotten when it disappears, so replugging pairs
again. --no-autopair turns it off.

--adb

Points at a different adb binary. Anyone whose SDK sits somewhere find_adb()
does not look needs it — and it is how the cable path finally got tested without
a phone at all: point it at a stub that reports one device, accepts reverse,
and refuses am start the way the real phone did.

That stub is what confirmed autopair fires exactly once across two ticks, clears
on unplug, re-fires on replug, and that the AM start: log line keeps the
refusal text while redacting the access key.

This does not make the cable work by itself

Being plain about it: the phone still has to publish an adb interface, and no
code here can influence that. What this removes is the human timing problem,
which was a real reason a working tunnel got wasted.

Also recorded this release: the phone re-enables USB tethering by itself, seen
three times returning to idProduct 4234 with live RNDIS nodes minutes after
being confirmed off at 4221. That is probably the whole explanation for the adb
interface never appearing — MagicOS looks to auto-enable tethering when a host
attaches, and the RNDIS composition it builds carries no adb interface. The one
adb sighting came seconds after a manual toggle: a transition window, not a
steady state. So the thing to hunt on the phone is whatever re-arms tethering on
connect, not the USB debugging switch alone.

Where both wired paths stand

Code complete, tested to the socket, and neither works on the hardware they were
built against:

Path Blocked by What would unblock it
adb reverse the phone never publishes the adb interface (255/66/1) stopping whatever re-arms tethering on connect
USB tethering macOS has no RNDIS driver, and never has a phone that tethers over CDC ECM or NCM

HANDOFF.md records this so nobody re-runs the exercise expecting a different
answer, and docs/handoffs/ now carries a session handoff with the open items.

Wifi remains the working transport and is not a consolation prize — it moved
this project's own 6.3 MB APK at 17.34 MB/s, against a cable that negotiates
USB 2.0 High Speed in every mode observed.

Versions

Piece Version
Server 1.19.0
Mac app 1.19.0
Android app 1.13.0 (versionCode 16) — unchanged

Debug-signed. Installs and upgrades in place, not Play Store distributable.

File Bridge 1.18.1 — the panel stops reporting a version it is not

Choose a tag to compare

@sayanthns sayanthns released this 08 Sep 14:37

A one-line fix with a lesson attached, and a note on tag numbering.

The panel stopped reporting a version it is not

__VER__ is substituted when the HTML is served, and poll() never touched it.
The panel window is deliberately built to outlive a server restart — that is
what reviving on poll is for — so a panel left open across a Quit and relaunch
went on claiming whichever version served its page. Observed reading 1.17.0
against a 1.18.0 server
.

That matters more here than it looks. The whole convention in this repo is three
independently versioned pieces, bump only what changed — and the panel is where
you read one of them. /api/status already returned version; the footer takes
it from there now.

Verified the way it actually breaks: serve the page from a 9.9.9 build, swap
the server underneath the still-open window, watch the footer correct itself with
no reload.

Filed as a dead end because it generalises: anything baked into that template
becomes a lie the moment the process is replaced.
__LINK__ only gets away
with it because poll() overwrites it every 2.5 s.

Tag numbering realigns here

The repo tag and the component versions had drifted apart:

Tag shipped server
v1.17.0 1.18.0
v1.16.0 1.15.0
v1.15.0 1.14.0
v1.14.0 1.13.0

Tagging this v1.18.1 makes the repo tag equal the server version again. The
tag has never tracked any one component and still does not promise to, but the
gap was a reliable way to misread which release contained what.

Carried from v1.17.0, worth repeating

That release contains the fix for the access key being written into
~/.filebridge/gui.log — the live key sat there 762 times at mode 644 while
~/.filebridge/key held the same secret at mode 600. A gui.log written
before v1.17.0 has your key in it in the clear.
It is re-chmodded to 600 on
the next launch, but delete it if it was ever shared:

rm ~/.filebridge/gui.log

Two copies remain in a fresh log by design, from the startup banner printing the
full link — that banner is how you recover the link when the panel will not
open, so redacting it would remove the log's only real job. Two copies behind
mode 600 is the same protection the key file itself gets.

Versions

Piece Version
Server 1.18.1
Mac app 1.18.1
Android app 1.13.0 (versionCode 16) — unchanged

Debug-signed. Installs and upgrades in place, not Play Store distributable.

File Bridge 1.17.0 — the key stops leaking into the log, and tethering is finalised

Choose a tag to compare

@sayanthns sayanthns released this 08 Sep 13:50

Closes out the wired work. Contains a security fix that predates all of it
and matters whether or not you care about the cable.

The access key was being written into gui.log

log_message logged self.path verbatim, and every phone request carries
?t=<key>. So the live access key sat 758 times in a mode 644
~/.filebridge/gui.log, while ~/.filebridge/key holds the identical secret at
mode 600. Same secret, a fifth of the protection — in the file every note in
this repo calls "the first place to look", and the file someone pastes when
asking for help.

  • Request lines now read t=<key>.
  • The launcher chmod 600s the log on every launch, so logs written by older
    versions get tightened too.
  • One hide_key() redacts both the request log and the am start output.

The lookbehind in it is load-bearing: a bare t= also occurs inside act= and
dat=, which am prints, and blanking those destroys the diagnostic it was
added to capture.

A gui.log written before this release has your key in it in the clear. It
is re-chmodded on the next launch, but delete it if it was ever shared.

Tethering is finalised

It is the wired path to reach for first, because it asks nothing of Developer
options.

--tether-net makes it testable for real. It was the least-verified thing here
for a bad reason: exercising it needed a sed on TETHER_NET in the source
plus a copy of tools/ beside the temporary file. Point the flag at a subnet
this Mac is already on and the whole path runs properly — detection, the
tether status block, all three QR variants decoded to three different hosts,
and the security gate that has to treat a caller on that subnet as a phone
rather than as this machine (200 for /api/list?t=, 403 for /api/status
and /connect).

The startup banner prints the tethered link, and stops implying one claim covers
both cable paths — a VPN cannot swallow loopback, it can absolutely swallow a
tethered address. The RNDIS message names what would work rather than only
what does not.

Where the cable actually stands

Both paths are code complete, tested to the socket, and neither works on the
hardware they were built against. HANDOFF.md records what would unblock each,
so nobody re-derives it:

Path Blocked by What would unblock it
Tethering macOS has no RNDIS driver, and never has a phone that tethers over CDC ECM or NCM
adb reverse the phone never publishes the adb interface (255/66/1) USB debugging that actually re-enumerates — then find out why am start refused, which 1.17.0 now logs

adb is parked, not abandoned: it was seen working once, tunnel and all, so
the transport is sound and only the launch shortcut failed.

Wifi remains the working transport and is not a consolation prize — it moved
this project's own 6.3 MB APK at 17.34 MB/s, against a cable that negotiates
USB 2.0 High Speed.

Versions

Piece Version
Server 1.18.0
Mac app 1.18.0
Android app 1.13.0 (versionCode 16) — unchanged

Debug-signed. Installs and upgrades in place, not Play Store distributable.

File Bridge 1.16.0 — the panel names the reason the cable is dead

Choose a tag to compare

@sayanthns sayanthns released this 08 Sep 13:13

The panel now tells you why the cable is not working, instead of saying
something true and useless.

Server and Mac app only. The APK is unchanged from
v1.15.0
—
Android is still 1.13.0 (versionCode 16), attached here for convenience so the
newest release always carries a working one.

What happened

USB tethering was turned on, and it worked — on the phone. Unlike the adb
attempt, the device genuinely rebuilt its USB function set (idProduct
4221 → 4234) and published:

RNDIS Communications Control@0   239/4/1
RNDIS Ethernet Data@1             10/0/0

Android tethers over RNDIS. macOS has never shipped a driver for it —
/System/Library/Extensions holds AppleUSBECM.kext and AppleUSBNCM.kext,
for CDC ECM and NCM, and nothing else. Both interfaces matched only the generic
IOUSBHostInterface, zero network driver nodes bound, no enX appeared,
and tether_ip() correctly found nothing.

The phone believes it is tethering. The Mac cannot see it.

Why this is a code change and not just a note

Without it the Cable card said "No phone on the cable" — true, and enough to
send someone hunting a driver that does not exist. rndis_on_cable() separates
"a phone is tethering and this Mac cannot use it" from "nobody turned
tethering on"
, and the card now reads:

Tethering, but macOS cannot use it
The phone is sharing over RNDIS. macOS ships ECM and NCM drivers and never
shipped one for RNDIS, so no network interface appears. Turn tethering off
and use USB debugging instead, or stay on wifi.

Confirmed against real hardware: rndis_on_cable() → True in 23 ms.

Detected by node name rather than by descriptor, deliberately. The
authoritative query (ioreg -l -c IOUSBHostInterface) costs 350 ms and
5 MB
; the names cost 25 ms. The names are safe to trust — they come from
the Linux kernel's f_rndis gadget, not from a vendor. It runs in the watch
thread and never in a request, because /api/status is polled every 2.5 s. That
thread now starts with or without adb, since a tethering phone is worth watching
for either way.

Where the cable stands

Both wired paths are complete, tested to the socket, and dead on the hardware
they were built against
:

Path Result
adb reverse The phone never publishes an adb interface (255/66/1). MagicOS gate — USB debugging on, "Transfer files" selected, replugged, idProduct unchanged.
USB tethering The phone publishes RNDIS. macOS has no driver.

HANDOFF.md now says so plainly, so nobody re-runs the exercise expecting a
different answer, and docs/ARCHITECTURE.md carries both dead ends with the
descriptor evidence and the commands to read it yourself. The code is waiting on
a phone that publishes adb, or one that tethers over NCM.

Wifi remains the working transport, and it is not a consolation prize: it moved
this project's own 6.3 MB APK at 17.34 MB/s, against a cable that negotiates
USB 2.0 High Speed in both modes.

Versions

Piece Version
Server 1.15.0
Mac app 1.15.0
Android app 1.13.0 (versionCode 16) — unchanged

File Bridge 1.15.0 — a second wired path, when the phone will not give you adb

Choose a tag to compare

@sayanthns sayanthns released this 08 Sep 12:59

A second wired path, because the first one did not survive contact with the phone.

The problem

adb reverse is the better cable transport — loopback cannot be routed, so no
VPN can touch it. But the Honor this was built against never publishes an adb
interface. With USB debugging on, "Transfer files" selected and the cable
replugged, ioreg shows only:

MTP@0           255/255/0   vendor MTP
Mass Storage@1  8/6/80      "Linux File-CD Gadget" — HiSuite's autorun disc

adb's interface is class 255 / subclass 66 / protocol 1. It is not there, and
idProduct was identical before and after the replug, so the USB function set
was never rebuilt. adb devices stayed empty across a twelve-minute poll;
adb kill-server, adb usb and adb reconnect change nothing, because there
is no interface for them to talk to.

Read the descriptors before debugging the host. An absent interface and a
refused authorisation both look like "no devices" from adb devices and have
nothing in common — unauthorized means the interface is there and only the
Allow tap is missing.

USB tethering

Needs nothing from Developer options. Turn it on, and the panel spots the
192.168.42.x link by itself and offers a QR for it.

tether_ip() matches that fixed subnet — the one Android's USB tethering always
builds, phone at .129 — rather than guessing from interface names, which
differ per Mac. No second listener is needed: it is a real address on a real
interface, so the existing socket already answers there and correctly treats the
caller as a phone. Only the advertised address had to change, since lan_ip()
asks the routing table for one address and would keep printing the wifi one.

/qr.png?tether=1 encodes the tethered link. Needed because the adb path pairs
by firing a deep link at the phone, and with no adb there is nothing to fire
it with — so tethered pairing is a scan again. With no tether up, ?tether=1
falls back to the wifi address rather than emitting a broken link.

It is the weaker of the two, and the panel says so

Tethering is plain IP, so a full-tunnel VPN on the phone can swallow it exactly
as it swallows wifi. adb reverse cannot be routed at all. Prefer adb whenever
the phone will allow it.

Wifi Cable, via adb Cable, via tethering
Needs same wifi USB debugging + one Allow tap nothing in Developer options
Pairs by QR deep link fired over adb QR (?tether=1)
A VPN can break it yes no yes
Radio can sleep yes no no

Also

  • Android's isCable() counts a tethered address. It matched only loopback,
    so a 192.168.42.x link would have been filed in the wifi slot and
    overwritten the real one — the exact bug the two saved slots exist to prevent.

Still not faster

One hard number, from this release's own delivery: the phone pulled the 6.3 MB
APK over wifi at 17.34 MB/s. The cable negotiates USB 2.0 High Speed with
adb port-forwarding overhead on top. Wired is for not depending on the network.

Not verified

Tethering on real hardware. Detection, the tether status block, the tethered
QR and its decode were all exercised — but by pointing TETHER_NET at a subnet
the Mac was already on. Whether macOS brings up an interface for this phone at
all is unknown: it has no native RNDIS, though CDC-NCM it does support. The two
adb calls have never run against hardware either, for the reason above.
HANDOFF.md keeps both as the top open item.

Versions

Piece Version
Server 1.14.0
Mac app 1.14.0
Android app 1.13.0 (versionCode 16)

Debug-signed. Installs and upgrades in place, not Play Store distributable.

File Bridge 1.14.0 — move files over the USB cable

Choose a tag to compare

@sayanthns sayanthns released this 08 Sep 12:41

Move files between the Mac and the phone over the USB cable, not just wifi.

The phone dials its own 127.0.0.1:8001; adb reverse carries that to a second
listener bound to loopback on the Mac. No wifi is involved at any point, which
removes the two failure classes that have cost this project the most time: a
full-tunnel VPN cannot swallow loopback, and there is no radio to fall asleep.

It is not faster. The cable tested here negotiates USB 2.0 High Speed
(480 Mbit/s) against an 802.11ax link already running at 600 Mbit/s. Wired is
for reliability, not throughput — and the docs say so rather than letting anyone
hope otherwise.

Pairing needs no QR and no typing

Press Pair over cable in the Mac panel. It arms the reverse mapping and then
fires the same filebridge:// deep link the QR encodes straight at the phone, so
the app opens already connected.

Needs USB debugging on the phone, and adb on the Mac.

The security part

_local() decided "is this the Mac?" by comparing the client address to
127.0.0.1. That is right with one listener and wrong with two, because
adb reverse delivers the phone's requests from 127.0.0.1.

Trust now follows the socket. The wired listener is a separate, flagged socket
that is never treated as local, so it answers 403 to /connect, /qr.png,
/api/status, /api/stop, /api/start, /api/quit, /api/open and
/api/usb. Measured with the guard removed, the same socket served /connect
with the access key rendered into the page, handed the key over again in
/api/status, and stopped the server with one POST /api/quit.

Pause covers the cable too, since it runs through the same check.

Also in this release

  • The phone keeps one saved link per transport and falls back between them.
    Pairing over the cable used to overwrite the only saved link, leaving the app
    stranded on a 127.0.0.1 that nothing answers once unplugged.
  • A VPN is no longer blamed for the cable. No VPN can reach loopback, so
    naming one there would send you off to debug the wrong thing.
  • The wired bind is probed, not attempted. SO_REUSEADDR lets a
    127.0.0.1:8001 bind succeed underneath a live *:8001, and the narrower
    socket then quietly takes every loopback connection — including the panel's.
  • build-android.sh is pinned to Gradle 8. A cached 9.7.1 appeared and
    newest-wins picked it; AGP 8.2.1 dies on Gradle 9 reaching for
    org/gradle/api/internal/HasConvention.
  • The released APK now lives in dist/ and in git, because a loose
    gitignored copy at the repo root went missing and could not be recovered.

Not verified

The two adb calls have never run against real hardware. USB debugging was off on
the test phone, so adb devices was empty and everything measured was the
loopback socket driven by curl. HANDOFF.md says so, and makes turning it on
the top open item.

Versions

Piece Version
Server 1.13.0
Mac app 1.13.0
Android app 1.12.0 (versionCode 15)

The APK is debug-signed. It installs and upgrades in place, but is not Play
Store distributable.

File Bridge 1.13.1 — a VPN no longer looks like a wifi problem

Choose a tag to compare

@sayanthns sayanthns released this 11 Aug 14:45

Server 1.12.0 (unchanged) · Mac app 1.12.0 (unchanged) · Android app
1.11.1 (versionCode 14)

Android-only. One diagnostic fix, from a real failure.

The error blamed the wifi, and the wifi was fine

Connecting failed with:

Could not reach the Mac. Check both are on the same wifi and that File Bridge
is open with Start sharing pressed.

Both were true. Same SSID, sharing on, server answering 200 on its LAN IP with
the macOS firewall off. What was actually happening: a full-tunnel VPN on the
phone, which routes even 192.168.x.x out to the exit server — so a Mac two
metres away is unreachable, and every symptom points at the network.

That took a session to find, off a VPN badge visible in a screenshot.

Now it says so

A VPN holding the default route is detectable, not guesswork —
TRANSPORT_VPN on the active network answers it. When one is up:

  • the connect error names the VPN first, and gives the fix that works: the VPN
    app's own "Allow LAN connections" (Proton, Mullvad, WireGuard and most
    others all have it), or disconnect;
  • Settings shows the same warning while the VPN is up. It is a live condition,
    not a permission, so there is nothing to grant and nothing to remember.

With no VPN present, nothing changes. ACCESS_NETWORK_STATE was already
granted, so no new permission is requested.

Honest note

The detection uses the documented API and the string is compiled into the APK,
but the warning has not yet been seen on a screen with a VPN up — that needs a
device with one running. It is listed under "not verified" in HANDOFF.md.

Installing

Attached FileBridge-1.11.1.apk, versionCode 14 — upgrades in place. The Mac
side does not change; server 1.12.0 is still current.

File Bridge 1.13.0 — send a file by right-clicking it

Choose a tag to compare

@sayanthns sayanthns released this 11 Aug 01:02

Server 1.12.0 · Mac app 1.12.0 · Android app 1.11.0 (versionCode 13,
unchanged since v1.12.0 — the attached APK is the same build)

Mac-side release. Sending a file no longer starts with opening the app.

Finder right-click → Quick Actions → Copy to Phone

Two items, on any file or selection: Copy to Phone and Move to Phone.
The file lands in to-phone and the app picks it up as usual.

They are two separate items rather than one that guesses. Moving a file into
to-phone and letting the phone collect it removes the only copy from where it
was — that should be something you chose, not a default you discovered.

Details that matter when a folder is a staging area:

  • A name already in to-phone gets a numbered sibling (report-2.pdf) instead
    of being overwritten. Silently replacing a file the phone is midway through
    collecting is not a trade worth making.
  • Anything already inside the served folder is skipped. Moving a file onto
    itself is how you lose it.
  • Files with spaces in their names work, which is the usual way these break.

Installed by the app, and removable

Both workflows ship inside FileBridge.app and are installed to
~/Library/Services on launch, then left alone until the shipped version
changes. To take them back out:

python3 scripts/make_quick_actions.py --uninstall

The served folder is not a constant

It is a command-line argument, so hardcoding ~/FileBridge would have worked
only for people using the default. Server 1.12.0 records the real root in
~/.filebridge/root, and the actions read it.

Verified

Both workflows were run through automator -i against a throwaway root before
going near real files: copy leaves the original, move does not, a collision
becomes name-2.ext, and a file already inside the root is skipped. The
installed pair was then run against the real folder.

Installing

Update the Mac app with ./scripts/build-mac-app.sh and launch it once — that
is what installs the Quick Actions.

The attached FileBridge-1.11.0.apk (versionCode 13) is byte-identical to the
one on v1.12.0 and is here only so a fresh install has an APK to hand. If it is
already on your phone, skip it.

File Bridge 1.12.0 — sending gets progress, and finished files open

Choose a tag to compare

@sayanthns sayanthns released this 10 Aug 20:49

Server 1.11.1 (unchanged) · Mac app 1.11.1 (unchanged) · Android app
1.11.0 (versionCode 13)

Android-only release. Three things the app claimed to be doing and was not.

Sending had the same fault that killed downloads

Uploads ran on the Activity's own thread pool with no wifi lock and no
notification
. That is exactly what killed large downloads before 1.10.0 —
uploads simply still had it, and it stayed invisible because the screen read
sending 1 file(s)… whether the transfer was alive or dead. A 1.3 GB send at
740 KB/s needs half an hour; with the screen off it was dying silently.

Both directions now run in the service, which holds the radio awake and shows
real progress with a transfer rate and a Cancel action.

A queued transfer could be dropped

The worker polled the queue, found it empty and exited — while a job arriving in
that window saw the thread still alive, added itself, and was never claimed. The
tap looked accepted and nothing happened. The queue is now only declared empty
while holding the lock a new job must also take.

Transfers run one at a time on purpose: two large ones over a single wifi link
finish later than the same two back to back, and one progress bar cannot honestly
describe both. The notification now names the file actually in flight and says
how many are waiting behind it.

Tapping a finished file now opens it

The completion notification reopened File Bridge instead of playing the video,
and tapping an already-downloaded row silently fetched it again. Now the
notification fires ACTION_VIEW, and a ticked row offers Open or Download
again
. Android 9 and older reach the file through a FileProvider; from 10 the
MediaStore uri is already openable.

DownloadService is renamed TransferService, since it no longer only downloads.

Known gap

Uploads cannot resume. The server takes a single multipart POST with no
offset endpoint, so an interrupted send starts over — downloads have resumed
since 1.11.0. The failure now says so, and says how far it got. A server endpoint
that accepts a byte offset and appends is the top open item.

Installing

Attached APK is FileBridge-1.11.0.apk, versionCode 13 — upgrades in place.
Debug-signed, so Android warns about an unknown source. The Mac side does not
need updating for this one; server 1.11.1 is still current.

File Bridge 1.11.1 — the panel stops lying mid-download

Choose a tag to compare

@sayanthns sayanthns released this 10 Aug 17:49

Server 1.11.1 · Mac app 1.11.1 · Android app 1.10.1 (versionCode 12)

Follow-up to v1.11.0,
which is where the actual download fix lives. This one is cosmetic — three
things that told the user something untrue.

The Mac panel claimed the phone had left, mid-transfer

The connected-client marker was only written by /api/list. A download is one
long request with nothing else on the wire, so after the 45 second freshness
window the panel decided no phone was there and put the QR back — while the
file was still arriving.

/file now marks the client when it starts and every 10 seconds while bytes
move. Transfers were never affected; only the panel was wrong.

On the phone

  • The Camera permission row was drawn with the files icon. The icon set
    never contained a camera and the placeholder was never replaced.
  • The unrestricted-background explanation still quoted DownloadManager's
    "Unable to download" — an error this app can no longer produce, since 1.10.0
    does its own downloading.

Installing

Attached APK is FileBridge-1.10.1.apk, versionCode 12 — upgrades in place.
Debug-signed, so Android warns about an unknown source. Nothing here changes the
transfer behaviour, so this one is optional if 1.10.0 is already on the phone.