BluetoothAdapter is a system Android class that represents the local Bluetooth adapter of a device. BluetoothAdapter is the entry point for all Bluetooth operations on Android: enabling the radio (enable), scanning devices, managing visibility (setScanMode), and retrieving adapter information (getName, getAddress, getState). The class is available via BluetoothManager.getAdapter() (API 18+) or BluetoothAdapter.getDefaultAdapter(). On devices without a Bluetooth module, getDefaultAdapter() returns null. According to Android Developers, 2026, BluetoothAdapter is a mandatory component for any BLE application on Android, starting from API 5.
Key Takeaways
BluetoothAdapter represents the physical Bluetooth adapter of an Android device. Each device has exactly one adapter (except Android Automotive with multiple Bluetooth chips, which uses BluetoothManager.getAdapterList()). BluetoothAdapter encapsulates the radio state: STATE_OFF (0), STATE_TURNING_ON (1), STATE_ON (2), STATE_TURNING_OFF (3). The state is tracked via BroadcastReceiver on ACTION_STATE_CHANGED.
Obtaining a BluetoothAdapter instance is the first step of any BLE application on Android. The recommended method is via BluetoothManager.getAdapter() from API 18+. The alternative is the static method BluetoothAdapter.getDefaultAdapter(), which works from API 5 but is less flexible. Both methods return null if the device does not have a Bluetooth module (Wi-Fi-only tablets, emulator). Null check is mandatory: the application must gracefully shut down or disable BLE features.
// Getting BluetoothAdapter (recommended)
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
import android.content.Context
class BluetoothHelper(context: Context) {
private val bluetoothAdapter: BluetoothAdapter?
init {
// Method 1: via BluetoothManager (API 18+)
val manager = context.getSystemService(Context.BLUETOOTH_SERVICE)
as BluetoothManager?
bluetoothAdapter = manager?.adapter
// Method 2: via static method (API 5+)
// val adapter = BluetoothAdapter.getDefaultAdapter()
// Null check
if (bluetoothAdapter == null) {
// Device does not support Bluetooth
}
}
// Check Bluetooth state
fun isBluetoothEnabled(): Boolean {
return bluetoothAdapter?.isEnabled == true
}
// Get adapter info
fun getAdapterInfo(): Map<String, String> {
return mapOf(
"name" to (bluetoothAdapter?.name ?: "N/A"),
"address" to (bluetoothAdapter?.address ?: "N/A"),
"state" to (bluetoothAdapter?.state?.toString() ?: "N/A"),
"scanMode" to (bluetoothAdapter?.scanMode?.toString() ?: "N/A")
)
}
}
The BluetoothHelper class demonstrates obtaining BluetoothAdapter via BluetoothManager with a subsequent null check. isBluetoothEnabled checks isEnabled — a mandatory condition before any BLE operations. getAdapterInfo returns the device name, MAC address, state, and scan mode. Important: on Android 10+ (API 29+), the system service returns a fake MAC address (02:00:00:00:00:00) if the app does not have BLUETOOTH_ADMIN and ACCESS_FINE_LOCATION permissions.
BluetoothAdapter provides methods for controlling the Bluetooth radio. enable() and disable() turn Bluetooth on and off. Both methods require the BLUETOOTH_ADMIN permission and execute asynchronously: after calling enable(), the system starts the radio enabling process, and the status is tracked via BroadcastReceiver with the action BluetoothAdapter.ACTION_STATE_CHANGED. On Android 10+, enable() and disable() require additional system privilege — regular apps cannot programmatically control Bluetooth without user dialog.
getState() returns the current adapter state: STATE_OFF (10), STATE_TURNING_ON (11), STATE_ON (12), STATE_TURNING_OFF (13). getAddress() returns the Bluetooth adapter MAC address. On Android 6+, ACCESS_FINE_LOCATION (or ACCESS_COARSE_LOCATION for API 31+) is required to request the MAC address. On Android 10+, getAddress() returns the constant address 02:00:00:00:00:00 — the real address is not available through the public API.
getScanMode() determines the adapter visibility mode: SCAN_MODE_NONE (invisible), SCAN_MODE_CONNECTABLE (visible to connected devices), SCAN_MODE_CONNECTABLE_DISCOVERABLE (visible to all). Visibility mode is limited in time (usually 60–300 seconds) for security. Setting the mode via setScanMode() requires BLUETOOTH_ADMIN and system permission on Android 10+.
| Method | Description | Required Permission |
|---|---|---|
| enable() | Turn Bluetooth radio on | BLUETOOTH_ADMIN |
| disable() | Turn Bluetooth radio off | BLUETOOTH_ADMIN |
| getState() | Current adapter state | BLUETOOTH |
| getAddress() | Adapter MAC address | BLUETOOTH + ACCESS_FINE_LOCATION (API 23+) |
| getScanMode() | Device visibility mode | BLUETOOTH |
| setScanMode() | Set visibility mode | BLUETOOTH_ADMIN |
BluetoothAdapter supports two types of scanning. Classic Bluetooth scanning (BR/EDR) is launched via startDiscovery() — it discovers all types of Bluetooth devices, including phones and headsets. Results are returned via BroadcastReceiver with the action BluetoothDevice.ACTION_FOUND. startDiscovery() runs for 12 seconds and can be cancelled by calling cancelDiscovery(). This method is deprecated for BLE — use BluetoothLeScanner.
BLE scanning via BluetoothAdapter uses the deprecated method startLeScan(LeScanCallback). Starting from API 21, Google recommends using BluetoothLeScanner, obtained via BluetoothAdapter.getBluetoothLeScanner(). BluetoothLeScanner provides a more flexible API: scanning configuration via ScanSettings (mode, callback type, match mode), filtering via ScanFilter (by service UUID, device name, MAC address), and support for PendingIntent for background scanning.
// Old (deprecated) vs new BLE scanning API
import android.bluetooth.BluetoothAdapter
import android.bluetooth.le.*
class BLEScanner(private val bluetoothAdapter: BluetoothAdapter?) {
// DEPRECATED: startLeScan (API 18+, API 21)
@Suppress("DEPRECATION")
fun legacyScan() {
bluetoothAdapter?.startLeScan { device, rssi, scanRecord ->
print("Found (LE Scan): $device.name, RSSI: $rssi")
}
}
// NEW: BluetoothLeScanner (API 21+)
fun modernScan() {
val scanner = bluetoothAdapter?.bluetoothLeScanner
?: return
// Scan settings
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setCallbackType(ScanSettings.CALLBACK_TYPE_ALL_MATCHES)
.setMatchMode(ScanSettings.MATCH_MODE_AGGRESSIVE)
.build()
// Filter by service (Heart Rate UUID)
val filters = listOf(
ScanFilter.Builder()
.setServiceUuid(ParcelUuid.fromString("0000180D-0000-1000-8000-00805F9B34FB"))
.build()
)
// Start scanning
scanner.startScan(filters, settings, object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
val device = result.device
val rssi = result.rssi
print("Found (BLE Scanner): ${device.name}, RSSI: $rssi, address: ${device.address}")
}
override fun onScanFailed(errorCode: Int) {
print("Scan error: $errorCode")
}
})
}
}
The BLEScanner class compares the deprecated startLeScan and the modern BluetoothLeScanner. In legacyScan, the LeScanCallback receives BluetoothDevice, RSSI, and raw scanRecord. In modernScan, ScanSettings with LOW_LATENCY mode (maximum discovery speed) and ScanFilter for filtering by Heart Rate Service UUID (0x180D) are used. ScanCallback provides onScanResult with a ScanResult object containing extended information: name, RSSI, advertising data, connection type.
BluetoothManager is a system Android service, introduced in API 18 (Android 4.3), for managing Bluetooth operations. Before API 18, the only way to obtain BluetoothAdapter was the static method getDefaultAdapter(). BluetoothManager provides: adapter — a BluetoothAdapter instance, getConnectedDevices() — a list of connected devices, getDevicesMatchingConnectionStates() — filtering by state. BluetoothManager is also used to obtain BluetoothLeScanner on older APIs.
Advantages of BluetoothManager over directly calling BluetoothAdapter.getDefaultAdapter(): the app does not depend on a static singleton, the manager respects context (Activity/Application), which is important for multi-account Android Enterprise scenarios. On Android Automotive with multiple Bluetooth chips, BluetoothManager.getAdapterList() returns all available adapters — BluetoothAdapter.getDefaultAdapter() returns only the first one.
// Using BluetoothManager for BLE
class BLEConnection(context: Context) {
private val bluetoothManager: BluetoothManager =
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
private val adapter: BluetoothAdapter? = bluetoothManager.adapter
// Get connected BLE devices list
fun getConnectedDevices(): List<BluetoothDevice> {
return bluetoothManager.getConnectedDevices(
BluetoothProfile.GATT
)
}
// Filter devices by state
fun getDevicesByState(states: IntArray): List<BluetoothDevice> {
return bluetoothManager.getDevicesMatchingConnectionStates(
BluetoothProfile.GATT, states
)
}
// Check BLE support on device
fun isBLESupported(): Boolean {
return adapter != null && context.packageManager
.hasSystemFeature(PackageManager.FEATURE_BLUETOOTH_LE)
}
// Request Bluetooth enable via system dialog
fun requestEnableBluetooth(activity: MainActivity) {
if (adapter?.isEnabled == false) {
val intent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
activity.startActivityForResult(intent, REQUEST_ENABLE_BT)
}
}
companion object {
const val REQUEST_ENABLE_BT = 1001
}
}
The BLEConnection class uses BluetoothManager to access BluetoothAdapter and retrieve the list of connected GATT devices. isBLESupported checks for BLE radio via PackageManager.hasSystemFeature(FEATURE_BLUETOOTH_LE) — an important check for devices with Bluetooth Classic but no BLE. requestEnableBluetooth shows the system Bluetooth enable dialog (ACTION_REQUEST_ENABLE), requiring no BLUETOOTH_ADMIN permission — this is the only legal way to enable Bluetooth on Android 10+ without a system app.
Permissions for BluetoothAdapter have evolved with each Android version. On Android 6–11 (API 23–30), BLUETOOTH, BLUETOOTH_ADMIN, and ACCESS_FINE_LOCATION are mandatory for BLE scanning. On Android 12+ (API 31+), Google split permissions: ACCESS_FINE_LOCATION is replaced by BLUETOOTH_SCAN (scanning), BLUETOOTH_CONNECT (connecting), BLUETOOTH_ADVERTISE (advertising). For BLE device discovery, BLUETOOTH_SCAN is sufficient, location is not required.
Permission table by Android version:
| Operation | API 23–30 | API 31+ |
|---|---|---|
| BLE scanning | ACCESS_FINE_LOCATION | BLUETOOTH_SCAN (no location) |
| BLE connection | ACCESS_FINE_LOCATION | BLUETOOTH_CONNECT |
| BLE advertising | ACCESS_FINE_LOCATION | BLUETOOTH_ADVERTISE |
| Enable/disable | BLUETOOTH_ADMIN | BLUETOOTH_ADMIN (system) |
| Get MAC address | ACCESS_FINE_LOCATION | BLUETOOTH_CONNECT (fake address) |
On Android 12+, all Bluetooth permissions are runtime permissions — they must be requested at runtime via ActivityResultContracts.RequestMultiplePermissions. BLUETOOTH_SCAN and BLUETOOTH_ADVERTISE belong to the NEARBY_DEVICES group, BLUETOOTH_CONNECT belongs to the same group. BLUETOOTH and BLUETOOTH_ADMIN permissions remain in the manifest for compatibility with API < 31, but for API 31+ they are ignored — Google requires explicitly specifying the new permissions.
// Request Bluetooth permissions on Android 12+
import android.Manifest
import android.content.pm.PackageManager
import android.os.Build
import androidx.core.content.ContextCompat
class PermissionHelper(context: Context) {
fun getRequiredPermissions(): Array<String> {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
// Android 12+: BLE-
arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT,
Manifest.permission.BLUETOOTH_ADVERTISE
)
} else {
// Android 6-11: BLE
arrayOf(
Manifest.permission.ACCESS_FINE_LOCATION,
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN
)
}
}
// Check all permissions
fun hasPermissions(context: Context): Boolean {
return getRequiredPermissions().all { permission ->
ContextCompat.checkSelfPermission(context, permission)
== PackageManager.PERMISSION_GRANTED
}
}
}
The PermissionHelper class returns the correct set of permissions depending on the API Level. On Android 12+, BLUETOOTH_SCAN, BLUETOOTH_CONNECT, BLUETOOTH_ADVERTISE without location are used. On Android 6–11, ACCESS_FINE_LOCATION is still required for BLE scanning. The developer must account for both scenarios when requesting permissions via ActivityResultContracts or RxPermissions.
Complete example of a BLE application in Kotlin using BluetoothAdapter for scanning, connecting, and reading data from a BLE device. The example covers permission checking, obtaining the adapter, scanning via BluetoothLeScanner, and connecting via BluetoothDevice.connectGatt.
// Full BLE manager in Kotlin
class BLEManager(private val context: Context) {
private val bluetoothManager: BluetoothManager =
context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
private val adapter: BluetoothAdapter? = bluetoothManager.adapter
private var scanner: BluetoothLeScanner? = adapter?.bluetoothLeScanner
private var gatt: BluetoothGatt? = null
// 1. Service discovery
fun canScan(): Boolean {
return adapter?.isEnabled == true
&& scanner != null
&& PermissionHelper(context).hasPermissions(context)
}
// 2. with filter
fun startScanning(callback: (BluetoothDevice, Int) -> Unit) {
if (!canScan()) return
val settings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.setReportDelay(0)
.build()
scanner?.startScan(null, settings, object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
callback(result.device, result.rssi)
}
})
}
// 3. Stop scanning
fun stopScanning() {
scanner?.stopScan(object : ScanCallback() {})
}
// 4. Connect to BLE device
fun connectToDevice(device: BluetoothDevice) {
if (adapter?.isEnabled != true) return
gatt = device.connectGatt(
context,
false,
object : BluetoothGattCallback() {
override fun onConnectionStateChange(gatt: BluetoothGatt, status: Int, newState: Int) {
if (newState == BluetoothProfile.STATE_CONNECTED) {
gatt.discoverServices()
}
}
override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
// Services found, can read characteristics
}
},
BluetoothDevice.TRANSPORT_LE
)
}
// 5. Release resources
fun disconnect() {
gatt?.disconnect()
gatt?.close()
gatt = null
}
}
The BLEManager class unifies the full BLE cycle on Android: checking adapter and permissions (canScan), scanning via BluetoothLeScanner with ScanSettings (startScanning), connecting via BluetoothDevice.connectGatt with TRANSPORT_LE (connectToDevice), and releasing resources (disconnect). All BLE operations execute on the UI thread — Android calls BluetoothGattCallback callbacks on the main thread. For performance-critical BLE tasks, it is recommended to offload GATT operations to a background HandlerThread.
Frequently Asked Questions
BluetoothAdapter is a class representing the local Bluetooth adapter of an Android device. It is obtained via BluetoothManager.getAdapter() (API 18+) or BluetoothAdapter.getDefaultAdapter(). It provides methods for enabling/disabling Bluetooth, scanning devices, managing visibility, and retrieving adapter information. Returns null on devices without a Bluetooth module.
The reason is the absence of a Bluetooth radio on the device. Typical for Wi-Fi-only tablets, Android emulator, and Android TV without Bluetooth. Check getDefaultAdapter() for null at app startup and disable BLE features if the adapter is absent. An alternative is checking via PackageManager.hasSystemFeature(FEATURE_BLUETOOTH_LE) for more precise detection.
BluetoothLeScanner (API 21+) is the modern API for BLE scanning with support for ScanFilter, ScanSettings, and PendingIntent. startLeScan (API 18+) is a deprecated BluetoothAdapter method that accepts LeScanCallback with a limited data set. BluetoothLeScanner is recommended by Google for all new projects, allows filtering by UUID, configuring power consumption mode, and background operation via PendingIntent.
On Android 12+ (API 31), BLE scanning requires BLUETOOTH_SCAN, connecting requires BLUETOOTH_CONNECT, and advertising requires BLUETOOTH_ADVERTISE. Location permission ACCESS_FINE_LOCATION is no longer required for BLE. On Android 6–11, ACCESS_FINE_LOCATION is required. All permissions are requested at runtime via ActivityResultContracts.
On Android 10+, programmatic Bluetooth enable without a system dialog is only available to system apps with BLUETOOTH_PRIVILEGED permission. Regular apps must use Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) and startActivityForResult — the user confirms enabling in the system dialog. BLUETOOTH_ADMIN in the manifest does not grant enable() rights on Android 10+.
Summary
We will develop a mobile application turnkey
IT Sectr creates iOS and Android applications for startups and businesses since 2017. We will advise you and propose the best solution.
Read also