Created
March 19, 2026 19:47
-
-
Save LethalMaus/735113cc81c66351496dbbe6a29d22ec to your computer and use it in GitHub Desktop.
Android BLE file transfer manager with folder-based protocol
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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