Read this in other languages: English, 简体中文.
BluetoothDebug is an Android tool for debugging Bluetooth Low Energy (BLE). It helps you discover nearby devices, inspect manufacturer data and advertising payloads, simulate BLE behavior, and sync configuration over WebDAV.
Do not use this project for attendance or check-in purposes.
Some check-in apps silently upload location data in the background. If you get caught because of that, it has nothing to do with this project.
| Area | What it does |
|---|---|
| Scan | Live BLE scan with company-name lookup, RSSI display, and filters (company keyword, empty name, signal strength) |
| Simulate → Local device | Injects fake scan results into the system Bluetooth stack via LSPosed (com.android.bluetooth); the injection point varies by OS version, see OS version differences |
| Simulate → External broadcast | Runs a foreground BLE advertiser using MAC, advertising data, and RSSI from the Simulate tab |
| WebDAV → Send | Periodically scans with the same filters as the Scan tab, then uploads the first matching device |
| WebDAV → Sync | Pulls remote Bluetooth data into local preferences on a schedule |
| Settings | WebDAV credentials, theme, language, optional Hook debug logs |
- Android 12+ (API 30+)
- Bluetooth LE hardware
- LSPosed with API 93+ (required for Local device simulation)
- Location / Bluetooth runtime permissions (requested by the app when needed)
- Install the latest APK from Releases.
- Enable the module in LSPosed.
- Set scope to:
com.android.bluetooth(system Bluetooth process — required for local simulation)net.ankio.bluetooth(this app)
- Restart the Bluetooth stack (toggle Bluetooth or reboot) after installing or updating the module.
External broadcast and WebDAV modes do not require the Xposed hook. Local device simulation does.
- Open the Bluetooth tab and grant scan permissions.
- Optionally open Filter to set company keyword, RSSI threshold, or hide unnamed devices.
- Tap a scan result — MAC, advertising data, and RSSI are saved to the Simulate tab.
- On Simulate, review or edit MAC, broadcast data, and RSSI.
- On Home, set Simulation to Local device.
- Other apps that scan BLE on this phone will see the configured device.
Enable Hook debug logs in Settings if you need Xposed-side troubleshooting logs.
Starting with Android 15, AOSP has been gradually moving BLE scan handling out of GattService into le_scan.ScanController, so the injection point differs on every release:
| OS | Hook entry | Actual injection target | Posting thread |
|---|---|---|---|
| Android 14 | GattService.start / stop |
GattService itself |
Main thread |
| Android 15 | GattService.start / stop |
getTransitionalScanHelper() |
Main thread |
| Android 16 | GattService constructors / cleanup |
getScanController() |
Main thread |
| Android 17 | ScanController constructors / cleanup |
ScanController itself |
ScanController.mHandler |
The implementation does not branch on OS version. It hooks both GattService and ScanController, then probes getScanController → getTransitionalScanHelper → the entry object itself at runtime and takes the first object that actually declares onScanResult. The repeating task is attached to that resolved target, so an object discovered through two entries is never injected twice.
Key differences across versions:
onScanResultInternalis always the injection method. On Android 17,ScanController.onScanResultstarts withenforceScanThread()and throwsIllegalStateExceptionwhen called from another thread;onScanResultInternalhas no such assertion. AOSP's own test mode also fakes beacons throughonScanResultInternal.- Android 17 must post on the scan thread.
ScanControllerowns a dedicatedBluetoothScanManagerthread, so itsmHandlerfield is used for posting. Android 14–16 have neither that field nor the thread assertion and fall back to the main thread. - Method probing walks the class hierarchy.
GattService extends ProfileService, andstart()/stop()are declared in the parent on Android 14/15. Looking only atdeclaredMethodsmisses them and produces a false "unsupported device" verdict. - Constructor signatures are never hardcoded. The
GattServiceconstructor is(Context)on Android 15,(AdapterService)on Android 16, and a five-argument version on Android 17, so all constructors are hooked instead. - Android 15's
ScanControlleris a thin shell that holds noonScanResultof its own and must be drilled through viagetTransitionalScanHelper(). Class presence alone therefore cannot be used to detect the OS version.
Unsupported device in the log means neither entry class was found. No scan target under ... is expected — it simply means that entry does not carry scan logic on the current OS, and the other entry will take over.
- Configure MAC, data, and RSSI on Simulate.
- On Home, set Simulation to External broadcast.
- The app starts a foreground BLE advertiser. Nearby phones can discover the simulated device.
Advertising stops automatically after 10 minutes, or when you turn the mode off.
Configure WebDAV under Settings first.
- Set scan filters on the Bluetooth tab (same rules used for periodic upload).
- On Home, set WebDAV to Send to WebDAV.
- A foreground service scans every 5 minutes and uploads the first device that matches your filters.
- On Home, set WebDAV to Sync from WebDAV.
- A foreground service pulls remote data into local preferences every 5 minutes.
./gradlew :app:assembleDebugRelease builds are signed separately; install the APK from GitHub Releases for normal use.
Contributions are welcome.
- Fork the project
- Create a feature branch (
git checkout -b feature/my-change) - Commit your changes (
git commit -m 'feat: describe your change') - Push to the branch (
git push origin feature/my-change) - Open a Pull Request
Commit message format:
[Type]: [Description]
feat: New feature
fix: Bug fix
docs: Documentation
style: Formatting only
refactor: Code refactor
perf: Performance
test: Tests
chore: Tooling / misc
deps: Dependency updates
![]() |
![]() |
![]() |
![]() |
![]() |
|
![]() |
![]() |
GPL-3.0







