Skip to content

Instantly share code, notes, and snippets.

@LethalMaus
Created March 19, 2026 19:47
Show Gist options
  • Select an option

  • Save LethalMaus/735113cc81c66351496dbbe6a29d22ec to your computer and use it in GitHub Desktop.

Select an option

Save LethalMaus/735113cc81c66351496dbbe6a29d22ec to your computer and use it in GitHub Desktop.
Android BLE file transfer manager with folder-based protocol
private val advertiseCallback = object : AdvertiseCallback() {
override fun onStartSuccess(settingsInEffect: AdvertiseSettings) {
pendingStart = false
log("Advertising started for service $SERVICE_UUID")
}
override fun onStartFailure(errorCode: Int) {
pendingStart = false
log("Advertising failed: ${advertiseFailureMessage(errorCode)}")
stopServer()
}
}
@RequiresPermission(Manifest.permission.BLUETOOTH_CONNECT)
private fun startAdvertising() {
val currentAdvertiser = advertiser ?: run {
pendingStart = false
stopServer()
return
}
val settings = AdvertiseSettings.Builder()
.setAdvertiseMode(AdvertiseSettings.ADVERTISE_MODE_LOW_LATENCY)
.setConnectable(true)
.setTimeout(0)
.setTxPowerLevel(AdvertiseSettings.ADVERTISE_TX_POWER_HIGH)
.build()
val data = AdvertiseData.Builder()
.addServiceUuid(ParcelUuid(SERVICE_UUID))
.build()
val scanResponse = AdvertiseData.Builder()
.setIncludeDeviceName(true)
.build()
currentAdvertiser.startAdvertising(settings, data, scanResponse, advertiseCallback)
}
private fun sendStatus() {
val payload = JSONObject()
.put("type", "status")
.put("chunkSize", mtuPayloadSize)
.put("sharedFolder", outgoingTreeName ?: JSONObject.NULL)
.put("incomingFolderConfigured", incomingTreeUri != null)
.put("sharedFolderConfigured", outgoingTreeUri != null)
.put("commands", JSONArray().put("status").put("list").put("put").put("get").put("cancel"))
sendControl(payload)
}
private fun listDirectory(message: JSONObject) {
val outgoingRoot = outgoingTreeUri?.let { DocumentFile.fromTreeUri(context, it) }
?: run {
sendError("Select a shared folder in Android first")
return
}
val pathParts = parseRelativePath(message.optString("path")) ?: run {
sendError("Invalid list path")
return
}
val target = resolvePath(outgoingRoot, pathParts) ?: run {
sendError("Path not found")
return
}
if (!target.isDirectory) {
sendError("Requested path is not a directory")
return
}
sendControl(JSONObject().put("type", "list_begin").put("path", buildRelativePath(pathParts)))
for (child in target.listFiles().sortedBy { it.name.orEmpty().lowercase(Locale.US) }) {
val childName = child.name ?: continue
sendControl(
JSONObject()
.put("type", "list_entry")
.put("name", childName)
.put("path", buildRelativePath(pathParts, childName))
.put("directory", child.isDirectory)
.put("size", if (child.isFile) child.length() else JSONObject.NULL)
)
}
sendControl(
JSONObject()
.put("type", "list_complete")
.put("path", buildRelativePath(pathParts))
)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment