From Doubt to Demo: A BLE File Transfer PoC for Android and macOS

From Doubt to Demo: A BLE File Transfer PoC for Android and macOS

GitHub - LethalMaus/BluetoothFileTransfer

Contribute to LethalMaus/BluetoothFileTransfer development by creating an account on GitHub.

github.com

This repo exists because I needed a proof of concept for work.

I cannot go into the wider product context, but the technical question was clear enough: can we transfer files to and from an Android app over Bluetooth with another machine acting as the peer?

I did not need a perfect product answer first. I needed a hands-on answer.

More specifically, I needed something concrete enough to say: here is a working example of an Android app and a laptop moving files back and forth over Bluetooth. It is not the finished solution, but it proves the concept and gives us something real to tune from.

That is what this repo became.

The Android side acts as a BLE peripheral. The laptop side runs a Python client as the BLE central. The two can connect, exchange status, browse one shared folder on Android, push files into an incoming folder, and pull files back out.

This article is really about why I built the PoC this way, what it proved, and what I had to change before it became a convincing enough demonstration.

The Point Was to Prove Feasibility, Not to Find the Perfect Architecture Up Front

I think this matters, because it is easy to write about proof of concepts as if they begin after a full survey of every possible option.

That is not how I tend to work.

I usually do enough research to avoid walking in blind, then I build something quickly enough to answer the core technical question. Once there is a real result on the table, I can evaluate whether the direction is good enough, whether a different transport would be better, or whether existing software already solves the problem more cleanly.

So I do not want to overstate the claim here.

I am not saying this repo proves BLE is the best way to do file transfer between Android and another machine. I am not saying there is not better ready-made software. There may well be.

What I am saying is narrower and more useful for the problem I had:

  • yes, it can be done
  • yes, it can be done in both directions
  • yes, you can show it working between an Android app and a laptop with code you control

For a PoC, that is already a meaningful result.

The First Important Result Was a Real BLE Link Between the Phone and the Laptop

Before file transfer matters, the BLE model has to make sense.

In this repo:

  • Android exposes a custom GATT service
  • one characteristic carries control messages as JSON
  • one characteristic carries raw file data
  • the laptop discovers the Android device and connects as the central

If the embedded code does not load, open it on GitHub Gist.

That was enough to get to the first real checkpoint: the Mac could connect and ask the Android app for status, and the app would answer with its current configuration.

That may sound small, but it was the first moment where the concept stopped being theoretical.

At that point, I knew the Android app and the laptop could speak to each other over BLE in a way that was structured enough to build on.

The First Failure Was Useful Because It Was Real

One thing I like about building a PoC quickly is that the failures become specific very fast.

The Android app originally looked like it was starting correctly, but it shut down almost immediately after registering the GATT server. The logs showed the service being added successfully and then the server closing.

That turned out to be an advertising payload problem, not a GATT service problem.

I was trying to advertise:

  • the full device name
  • and a 128-bit service UUID

in the main advertising packet.

That is the sort of mistake that is easy to make if you are still thinking in abstractions instead of platform limits. Once I moved the device name into the scan response and kept the service UUID in the primary advertising payload, the behavior became much more reliable.

If the embedded code does not load, open it on GitHub Gist.

This was exactly the kind of result I wanted from the PoC. Not just “it failed,” but “it failed for a concrete, understandable reason, and now we know what shape a working solution needs to take.”

A Single Shared File Proved Downloads. It Did Not Prove a Useful Workflow

The first Android version let the user pick one file for the laptop to download.

That was enough to prove the basic download path, but it was too narrow to serve as a convincing demonstration of the concept. If the point is to show that Bluetooth can support this kind of interaction between the app and another machine, then the workflow has to feel at least somewhat real.

So I changed the model from:

  • one selected outgoing file

to:

  • one shared folder for browsing and downloads
  • one incoming folder for uploads from the laptop

That pushed the desktop client forward too. Instead of only `upload` and `download`, it grew into:

  • `list`
  • `push`
  • `pull`

with path-based behavior and recursive folder handling where it made sense.

If the embedded code does not load, open it on GitHub Gist.

This was an important shift in the PoC.

It meant I was no longer just proving that one byte stream could cross BLE. I was proving that a small but believable file-transfer workflow could exist between Android and a machine over Bluetooth.

That was much closer to the actual value of the exercise.

The PoC Needed to Show More Than a Happy Path With the App Open

Another thing that became obvious once the core transfer path worked was that activity-owned BLE state was too fragile.

If the only thing I wanted was a very controlled demo, keeping all BLE ownership inside the activity would have been enough. But I needed something stronger than that. I needed to be able to point at the repo and say this concept works in a form that looks like the beginning of a real implementation.

That meant the Android side had to survive normal lifecycle pressure better than a toy demo.

So the BLE ownership moved into a foreground service.

If the embedded code does not load, open it on GitHub Gist.

That made the design much easier to defend:

  • the service owns the BLE runtime
  • the activity handles permissions, folder selection, and UI
  • the foreground notification makes the long-running BLE work explicit

I do not think that makes the app “finished.” It just makes the PoC more credible, because it is no longer depending on a fragile activity-only lifecycle to keep the Bluetooth session alive.

Some of the Most Useful Bugs Were State-Ownership Bugs

One of the stranger failures looked like a UI problem at first:

  • selecting the shared folder updated the wrong row
  • selecting the incoming folder looked like it did nothing
  • the Python client still reported that no shared folder was configured

The fix was not just a UI text update. The underlying issue was state ownership during the folder-picker round-trip. The picker result and the service binding were not synchronized tightly enough, so the visible state and the BLE state could drift apart.

The eventual fix was straightforward once the problem was clear:

  • the activity owns the folder-selection state it shows
  • the selected URI and label are persisted immediately
  • the service is synchronized from that stored state

That made the behavior much more predictable.

I mention this because it is another example of what I wanted from the PoC. The goal was not only to get a green path. The goal was to understand what kinds of engineering problems show up as soon as the feature becomes even slightly real.

What the Repo Actually Proves

For me, the most important outcome is still the simplest one.

This repo proves that I can:

  • run an Android app as a BLE peripheral
  • connect to it from a laptop
  • browse files from a shared Android folder
  • upload files from the laptop into Android
  • download files from Android back to the laptop

In other words, it proves the exact thing I needed the PoC to prove: transferring files to and from Android via Bluetooth between the app and a machine is possible.

That does not settle every design question that comes after.

It does not tell me whether BLE is the final transport choice.It does not tell me whether a different protocol would be better.It does not tell me whether an existing product or library might solve the same problem more efficiently.

But that is fine. The PoC was not supposed to answer every future question.

It was supposed to remove doubt around the core concept and replace it with a working example.

Closing Thoughts

That is probably the main thing I would want another developer to take from this repo.

Sometimes the right next step is not a perfect design. Sometimes it is a proof that the thing can be done at all, in a way that is concrete enough to discuss seriously.

That was the role of this project.

I needed a working answer to a very specific question: can an Android app and another machine transfer files to and from each other over Bluetooth?

Now I have one.

It is not claiming to be the final product. It is not claiming to be the best possible implementation. It is a practical, working demonstration that the concept holds up, and that was exactly what I needed.