Android ACL_CONNECTED & DISCONNECTED firing up only once per app lifecycle - android

I've currently run into a strange problem. I'm developing an app which needs to communicate with Bluetooth and I'm listening for Bluetooth changes as follows:
private fun createBluetoothIntentFilter() {
var bluetoothFilter = IntentFilter()
bluetoothFilter.addAction("android.bluetooth.device.action.ACL_CONNECTED")
bluetoothFilter.addAction("android.bluetooth.device.action.ACL_DISCONNECTED")
bluetoothFilter.addAction(BluetoothDevice.ACTION_FOUND)
bluetoothFilter.addAction(BluetoothAdapter.ACTION_STATE_CHANGED)
this.registerReceiver(mReceiver, bluetoothFilter)
}
private var mReceiver = object: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val action = intent!!.action
if (action.equals("android.bluetooth.device.action.ACL_CONNECTED")) {
Log.d("Z","Received: Bluetooth Connected");
}
if(action.equals("android.bluetooth.device.action.ACL_DISCONNECTED")){
Log.d("Z","Received: Bluetooth Disconnected");
isBluetoothConnected = false
}
// When the user turn the bluetooth on / off
if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
val state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE, -1)
if (state == BluetoothAdapter.STATE_OFF) {
isBluetoothConnected = false
}
else if (state == BluetoothAdapter.STATE_ON) {
isBluetoothConnected = true
}
}
}
}
In my manifest file I've included needed permissions
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.INTERNET"/>
The problem is that ACL_CONNECTED & DISCONNECTED are called only once per app lifecycle i.e. I open an app, here we get ACL_CONNECTED because the tablet is already paired with Bluetooth device, then I disconnect the Bluetooth device so the app will lose connection with BT device, the ACL_DISCONNECTED is fired up, then I'm connecting the device again, but neither ACL_CONNECTED or DISCONNECTEED is called any more on this app lifecycle.
I would also appreciate the clarification on when these ACLs are exactly called? Is it when socket.connect() happen? Because in android docs it's pretty hard to find.
Thanks for any help!

protected val mReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val action: String? = intent.action
when(action!!) {
//STATUS_CONNECTING
BluetoothDevice.ACTION_ACL_CONNECTED -> {
//TODO anything
//for example
val device: BluetoothDevice? =
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
}
BluetoothDevice.ACTION_ACL_DISCONNECTED -> {
//TODO anything
//for example
val device: BluetoothDevice? =
intent.getParcelableExtra(BluetoothDevice.EXTRA_DEVICE)
}
}
}
}
//then
bluetoothFilter = IntentFilter(BluetoothDevice.ACTION_ACL_CONNECTED)
this.registerReceiver(mReceiver, bluetoothFilter)
bluetoothFilter = IntentFilter(BluetoothDevice.ACTION_ACL_DISCONNECTED)
this.registerReceiver(mReceiver, bluetoothFilter)

Related

Android CompanionDeviceManager Never Finds Any Nearby Bluetooth Devices

UPDATE: Added Main Activity code which contains Bluetooth permissions logic
I'm trying to utilize Android's CompanionDeviceManager API to find nearby bluetooth (non LE) devices on my Pixel 5 running Android 13, but it only ever seems to find nearby WiFi networks. I'm suspicious that the deviceFilter isn't working properly.
Initially, my code to configure the BluetoothDeviceFilter looked like this:
private val deviceFilter: BluetoothDeviceFilter = BluetoothDeviceFilter.Builder()
// Match only Bluetooth devices whose name matches the pattern
.setNamePattern(Pattern.compile("(?i)\\b(Certain Device Name)\\b"))
.build()
private val pairingRequest: AssociationRequest = AssociationRequest.Builder()
// Find only devices that match our request filter
.addDeviceFilter(deviceFilter)
// Don't stop scanning as soon as one device matching the filter is found.
.setSingleDevice(false)
.build()
With this code, however, no devices ever appear within the system generated Companion Device Pairing screen. The spinner spins until timeout
Thinking maybe my regex was unintentionally too restrictive, I changed the filter to use a regexp that allows everything, like so:
.setNamePattern(Pattern.compile(".*"))
But even this filter fails to allow any nearby bluetooth devices to appear in the Pairing screen.
When I intentionally don't add any filter all I see are WiFi networks, so the Companion Device Manager can work, it's just seemingly misconfigured for Bluetooth results.
private val pairingRequest: AssociationRequest = AssociationRequest.Builder()
// No filter, let's see it all!
.setSingleDevice(false)
.build()
Using the Android OS's system Bluetooth menu I clearly see there are Bluetooth devices within range of my device, and I can even connect to them, but the same devices never appear within my app.
What am I doing wrong that's causing no nearby Bluetooth devices to appear in my CompanionDeviceManager Pairing Screen?
Code below:
HomeFragment.kt
class HomeFragment : Fragment() {
//Filter visible Bluetooth devices so only Mozis within range are displayed
private val deviceFilter: BluetoothDeviceFilter = BluetoothDeviceFilter.Builder()
// Match only Bluetooth devices whose name matches the pattern.
.setNamePattern(Pattern.compile(BLUETOOTH_DEVICE_NAME_REGEX_TO_FILTER_FOR))
.build()
private val pairingRequest: AssociationRequest = AssociationRequest.Builder()
// Find only devices that match this request filter.
.addDeviceFilter(deviceFilter)
// Don't stop scanning as soon as one device matching the filter is found.
.setSingleDevice(false)
.build()
private val deviceManager: CompanionDeviceManager by lazy {
requireContext().getSystemService(Context.COMPANION_DEVICE_SERVICE) as CompanionDeviceManager
}
private val executor: Executor = Executor { it.run() }
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View {
setupPairingButton()
}
/**
* This callback listens for the result of connection attempts to our Mozi Bluetooth devices
*/
#Deprecated("Deprecated in Java")
#SuppressLint("MissingPermission")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when (requestCode) {
SELECT_DEVICE_REQUEST_CODE -> when (resultCode) {
Activity.RESULT_OK -> {
// The user chose to pair the app with a Bluetooth device.
val deviceToPair: BluetoothDevice? =
data?.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE)
deviceToPair?.createBond()
}
}
else -> super.onActivityResult(requestCode, resultCode, data)
}
}
private fun setupPairingButton() {
binding.buttonPair.setOnClickListener {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
/**
* This is the approach to show a pairing dialog for Android 33+
*/
deviceManager.associate(pairingRequest, executor,
object : CompanionDeviceManager.Callback() {
// Called when a device is found. Launch the IntentSender so the user
// can select the device they want to pair with
override fun onAssociationPending(intentSender: IntentSender) {
intentSender.let { sender ->
activity?.let { fragmentActivity ->
startIntentSenderForResult(
fragmentActivity,
sender,
SELECT_DEVICE_REQUEST_CODE,
null,
0,
0,
0,
null
)
}
}
}
override fun onAssociationCreated(associationInfo: AssociationInfo) {
// Association created.
// AssociationInfo object is created and get association id and the
// macAddress.
var associationId = associationInfo.id
var macAddress: MacAddress? = associationInfo.deviceMacAddress
}
override fun onFailure(errorMessage: CharSequence?) {
// Handle the failure.
showBluetoothErrorMessage(errorMessage)
}
})
} else {
/**
* This is the approach to show a pairing dialog for Android 32 and below
*/
// When the app tries to pair with a Bluetooth device, show the
// corresponding dialog box to the user.
deviceManager.associate(
pairingRequest,
object : CompanionDeviceManager.Callback() {
override fun onDeviceFound(chooserLauncher: IntentSender) {
startIntentSenderForResult(
chooserLauncher,
SELECT_DEVICE_REQUEST_CODE,
null,
0,
0,
0,
null
)
}
override fun onFailure(error: CharSequence?) {
// Handle the failure.
showBluetoothErrorMessage(error)
}
}, null
)
}
}
}
companion object {
private const val SELECT_DEVICE_REQUEST_CODE = 0
private const val BLUETOOTH_DEVICE_NAME_REGEX_TO_FILTER_FOR = "(?i)\\bCertain Device Name\\b"
}}
MainActivity.kt
class MainActivity : AppCompatActivity() {
private val enableBluetoothIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
private var bluetoothEnableResultLauncher =
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
binding.loadingSpinner.hide()
when (result.resultCode) {
Activity.RESULT_OK -> {
Snackbar.make(
binding.root,
resources.getString(R.string.bluetooth_enabled_lets_pair_with_your_mozi),
Snackbar.LENGTH_SHORT
).show()
}
Activity.RESULT_CANCELED -> {
Snackbar.make(
binding.root,
getString(R.string.without_bluetooth_you_cant_pair_with_your_mozi),
Snackbar.LENGTH_INDEFINITE
)
.setAction(resources.getString(R.string._retry)) {
ensureBluetoothIsEnabled()
}
.show()
}
}
}
private val requestBluetoothPermissionLauncher =
registerForActivityResult(
ActivityResultContracts.RequestPermission()
) { isGranted: Boolean ->
if (isGranted) {
bluetoothEnableResultLauncher.launch(enableBluetoothIntent)
} else {
// Explain to the user that the feature is unavailable because the
// feature requires a permission that the user has denied. At the
// same time, respect the user's decision. Don't link to system
// settings in an effort to convince the user to change their
// decision.
Snackbar.make(
binding.root,
getString(R.string.without_bluetooth_you_cant_pair_with_your_mozi),
Snackbar.LENGTH_INDEFINITE
)
.setAction(resources.getString(R.string._retry)) {
ensureBluetoothIsEnabled()
}
.show()
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setupViews()
ensureBluetoothIsEnabled()
}
private fun setupViews() {
//Here we setup the behavior of the button in our rationale dialog: basically we need to
// rerun the permissions check logic if it was already denied
binding.bluetoothPermissionsRationaleDialogButton.setOnClickListener {
binding.permissionsRationaleDialog.animateShow(false)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
requestBluetoothPermissionLauncher.launch(Manifest.permission.BLUETOOTH_CONNECT)
} else {
requestBluetoothPermissionLauncher.launch(Manifest.permission.BLUETOOTH)
}
}
}
private fun ensureBluetoothIsEnabled() {
binding.loadingSpinner.show()
val bluetoothManager: BluetoothManager = getSystemService(BluetoothManager::class.java)
val bluetoothAdapter: BluetoothAdapter? = bluetoothManager.adapter
if (bluetoothAdapter == null) {
// Device doesn't support Bluetooth
binding.loadingSpinner.hide()
Snackbar.make(
binding.root,
resources.getString(R.string.you_need_a_bluetooth_enabled_device),
Snackbar.LENGTH_INDEFINITE
).show()
}
if (bluetoothAdapter?.isEnabled == false) {
// Check if Bluetooth permissions have been granted before we try to enable the
// device
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.BLUETOOTH_CONNECT //TODO: test if this needs variant for legacy devices
) != PackageManager.PERMISSION_GRANTED
) {
/**
* We DON'T have Bluetooth permissions. We have to get them before we can ask the
* user to enable Bluetooth
*/
binding.loadingSpinner.hide()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
if (shouldShowRequestPermissionRationale(Manifest.permission.BLUETOOTH_CONNECT)) {
binding.permissionsRationaleDialog.animateShow(true)
} else {
requestBluetoothPermissionLauncher.launch(Manifest.permission.BLUETOOTH_CONNECT)
}
} else {
if (shouldShowRequestPermissionRationale(Manifest.permission.BLUETOOTH)) {
binding.permissionsRationaleDialog.animateShow(true)
} else {
requestBluetoothPermissionLauncher.launch(Manifest.permission.BLUETOOTH)
}
}
return
} else {
/**
* We DO have Bluetooth permissions. Now let's prompt the user to enable their
* Bluetooth radio
*/
binding.loadingSpinner.hide()
bluetoothEnableResultLauncher.launch(enableBluetoothIntent)
}
} else {
/**
* Bluetooth is enabled, we're good to continue with normal app flow
*/
binding.loadingSpinner.hide()
}
}
}
Android Manifest
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<!-- Bluetooth Permissions -->
<uses-feature android:name="android.software.companion_device_setup" android:required="true"/>
<uses-feature android:name="android.hardware.bluetooth" android:required="true"/>
<!-- Request legacy Bluetooth permissions on older devices. -->
<uses-permission android:name="android.permission.BLUETOOTH"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"
android:maxSdkVersion="30" />
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
<!-- Needed only if your app looks for Bluetooth devices.
If your app doesn't use Bluetooth scan results to derive physical
location information, you can strongly assert that your app
doesn't derive physical location. -->
<uses-permission android:name="android.permission.BLUETOOTH_SCAN"
android:usesPermissionFlags= "neverForLocation"
tools:targetApi="s" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
...
</manifest>
You could try using an empty BluetoothDeviceFilter like this:
private val deviceFilter: BluetoothDeviceFilter = BluetoothDeviceFilter.Builder().build()
to signal to the API that you want Bluetooth devices, and see if at least the phone sees your device.
Then you could try again with the name filter, this time adding a service UUID filter with BluetoothDeviceFilter.Builder.addServiceUuid.
If you don't know the UUID of your device or don't want to use it as a filter, you can use an arbitrary one and set the mask to all zeros (the docs suggest that it might also work using null values).
This is a hackish solution, but it might help you move a step further
It might be a permission issue.
In the docs, I read:
The BLUETOOTH_ADVERTISE, BLUETOOTH_CONNECT, and BLUETOOTH_SCAN permissions are runtime permissions. Therefore, you must explicitly request user approval in your app before you can look for Bluetooth devices, make a device discoverable to other devices, or communicate with already-paired Bluetooth devices.
So you could to add the following code in your HomeFragment class:
private val requestMultiplePermissions = registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
permissions.entries.forEach {
Log.d("Permission Request", "${it.key} = ${it.value}")
}
}
private val requestBluetooth = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
if (result.resultCode == RESULT_OK) {
// granted
} else {
// denied
}
}
and in the onCreateView method:
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
requestMultiplePermissions.launch(arrayOf(
Manifest.permission.BLUETOOTH_SCAN,
Manifest.permission.BLUETOOTH_CONNECT
))
} else {
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
requestBluetooth.launch(enableBtIntent)
}
to request the permissions at runtime.
The documentation does not mention it, but it appears that even with the CompanionDeviceManager the location access must be enabled on the device.
The app does not need the location permission anymore, but it must be enabled.

Scanning Bluetooth Low Energy in Background Android 11

I'm developing an app in which I want to continuously scan for Ble advertisement packets, even if the user locks the screen. With my current implementation this works fine with Android 10, but with Android 11 it stops once the user locks the screen. For scanning Ble packets I first request a few permissions, namely:
coarse and fine location
bluetooth scan
access background location
I start a simple foreground service (also added foreground service permission to my Manifest) with:
private fun startBleService() {
serviceIntent = Intent(baseContext, ScanService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Log.i("Background Act", "Starting foreground service for android 8.0+")
applicationContext.startForegroundService(serviceIntent)
} else {
Log.i("Background Act", "Starting foreground service for versions < android 8.0")
applicationContext.startService(serviceIntent)
}
}
This will call startForeground(notificationID, notification) in the onStartCommand function of my ScanService, thus requesting to run in foreground. After this I start the actual Ble scan functionalities. I also added android:foregroundServiceType="location" to the service in the Manifest.
My ScanService Code:
class ScanService : Service() {
private val channelID = "CustomChannelID"
private val notificationID = 7
private lateinit var bluetoothManager: BluetoothManager
private lateinit var bluetoothAdapter: BluetoothAdapter
private lateinit var bluetoothLeScanner: BluetoothLeScanner
private var scanCounter = 0
private var _bleSingleScanResults = HashMap<String, MutableList<String>>()
// all scans combined in an array, internally used
private var _bleAllScanResults = arrayListOf<HashMap<String, MutableList<String>>>()
// only starts a new scan if its not already scanning
private var scanning = false
private val notificationManager by lazy {getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager}
#RequiresApi(Build.VERSION_CODES.M)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i("OnStartCommand Service", "Is started")
val notification: Notification =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createChannel(notificationManager)
Notification.Builder(this, channelID)
.setContentTitle("Content Title")
.setContentText("Content Text")
.setTicker("Ticker")
.build()
} else {
NotificationCompat.Builder(this, channelID)
.setContentTitle("BLE Scanning Service")
.setContentText("Scanning BLE in the background")
.setTicker("Ticker")
.build()
}
// use custom non-zero notification ID
startForeground(notificationID, notification)
//TODO: start in new thread
scanBle()
// If we get killed, after returning from here, restart, recreating notification again though
return START_STICKY
}
#RequiresApi(Build.VERSION_CODES.M)
fun scanBle() {
bluetoothManager = this.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothAdapter = bluetoothManager.adapter
if (bluetoothAdapter.bluetoothLeScanner == null) {
Log.d("BLE", "Device doesn't support BLE")
Toast.makeText(
this,
"It seems like your device does not support BLE. This is a crucial part of this app. \n " +
"Unfortunately you can't contribute to the dataset of scanned locations.",
Toast.LENGTH_LONG
).show()
return
}
bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner
runBLEScan()
}
#SuppressLint("MissingPermission") //since we check beforehand in the MainActivity for permissions already
private fun runBLEScan() {
val scanSettings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
val scanFilters = listOf(ScanFilter.Builder().build())
if (!scanning) {
scanning = true
Log.i("BLE", "--- STARTING BLE SCAN ---")
bluetoothLeScanner.startScan(scanFilters, scanSettings, scanCallBackLe)
} else Log.d("BLE Scan", "Called scanning function but is currently already scanning!")
}
// ALWAYS ON UI-THREAD
private val scanCallBackLe = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
super.onScanResult(callbackType, result)
// discard result if payload is null
if(result.scanRecord == null || result.scanRecord!!.bytes == null) {
return
}
println("Payload: ${result.scanRecord?.bytes?.toHexString()}")
// check if device already discovered in a scan, if so increase counter, else make new
// entry in the result HashMap
if (_bleSingleScanResults.isEmpty() || !_bleSingleScanResults.containsKey(result.device.toString())) {
// device wasn't seen before
_bleSingleScanResults[result.device.toString()] =
mutableListOf(result.rssi.toString(), result.scanRecord!!.bytes.toHexString(), "1")
} else {
// update already existing entry
val cntr = _bleSingleScanResults[result.device.toString()]!![2].toInt() + 1
_bleSingleScanResults[result.device.toString()]!![2] = cntr.toString()
}
}
override fun onScanFailed(errorCode: Int) {
super.onScanFailed(errorCode)
Log.d("BLE ScanResult", "Scan failed code: $errorCode")
}
}
private fun ByteArray.toHexString() = joinToString("", "[0x", "]") { "%02X".format(it) }
private fun createChannel(notificationManager: NotificationManager) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return
}
val channel =
NotificationChannel(channelID, "Scan Service", NotificationManager.IMPORTANCE_DEFAULT)
channel.description = "Hello! This is a notification."
notificationManager.createNotificationChannel(channel)
}
#SuppressLint("MissingPermission")
override fun onDestroy() {
Log.d("Destroyed Service", "That's even worse")
bluetoothLeScanner.stopScan(scanCallBackLe)
super.onDestroy()
}
#SuppressLint("MissingPermission")
override fun stopService(name: Intent?): Boolean {
Log.d("Stopped Service", "That's bad")
bluetoothLeScanner.stopScan(scanCallBackLe)
stopSelf()
return super.stopService(name)
}
override fun onBind(intent: Intent): IBinder {
TODO("Return the communication channel to the service.")
}
Parts of my Manifest:
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
...
<service
android:name=".ScanService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="location" />
...
</application>
This works fine with Android 10 (tested on a Huawei device), but unfortunately not on Android 11 (Samsung A22). Is there any other permission I need to be able to keep scanning even if the user locks the screen in Android 11?

Android BluetoothHeadset getConnectedDevices() list is empty

In my app, I want to get certain details about the connected bluetooth headset.
First, I thought of getting the connected devices whose profile is headset.
val result = BluetoothAdapter.getDefaultAdapter()
.getProfileProxy(context, mProfileListener, BluetoothProfile.HEADSET)
The listener snippet is as follows :
private var mBluetoothHeadset: BluetoothHeadset? = null
private val mProfileListener = object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
if (profile == BluetoothProfile.HEADSET) {
mBluetoothHeadset = proxy as BluetoothHeadset
val devices = mBluetoothHeadset?.connectedDevices
devices?.forEach {
println(it.name)
}
}
}
override fun onServiceDisconnected(profile: Int) {
if (profile == BluetoothProfile.HEADSET) {
mBluetoothHeadset = null
}
}
}
I have declared the necessary permissions in the manifest
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
But mBluetoothHeadset?.connectedDevices is always returning an empty list. But in my tablet, the device is already connected to a bluetooth headset. Am I missing anything here?
It looks like we can get the list of connected devices by filtering it based on the various connection states. The following snippet worked for me
private val states = intArrayOf(
BluetoothProfile.STATE_DISCONNECTING,
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING
)
private val mProfileListener = object : BluetoothProfile.ServiceListener {
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
if (profile == BluetoothProfile.HEADSET) {
mBluetoothHeadset = proxy as BluetoothHeadset
val devices = mBluetoothHeadset?.getDevicesMatchingConnectionStates(states)
devices?.forEach {
println("${it.name} ${it.bondState}")
}
}
}

Detecting whether a headset is plugged into an Android device or not.

How can I determine whether a headset is plugged into an Android device or not?
You can use the broadcast receiver.
So, You might write this code in "AndroidManifest.xml"
<receiver android:name="com.juno.brheadset.HeadsetStateReceiver">
<intent-filter>
<action android:name="android.intent.action.HEADSET_PLUG"/>
</intent-filter>
</receiver>-->
But, This doesn't work. When OS send this "HEADSET_PLUG" intent, OS set the flag "Intent.FLAG_RECEIVER_REGISTERED_ONLY" So, You should write the code like below in Activity or Service class instead of "AndroidManifest" things.
public class BRHeadsetActivity extends Activity {
/** Called when the activity is first created. */
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
IntentFilter receiverFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
HeadsetStateReceiver receiver = new HeadsetStateReceiver();
registerReceiver( receiver, receiverFilter );
}
I hope this article help you. Bye!
This is the part of "HeadsetObserver.java", Android SDK Source.
private final void sendIntent(int headset, int headsetState, int prevHeadsetState, String headsetName) {
if ((headsetState & headset) != (prevHeadsetState & headset)) {
// Pack up the values and broadcast them to everyone
Intent intent = new Intent(Intent.ACTION_HEADSET_PLUG);
**intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);**
int state = 0;
int microphone = 0;
if ((headset & HEADSETS_WITH_MIC) != 0) {
microphone = 1;
}
if ((headsetState & headset) != 0) {
state = 1;
}
intent.putExtra("state", state);
intent.putExtra("name", headsetName);
intent.putExtra("microphone", microphone);
if (LOG) Slog.v(TAG, "Intent.ACTION_HEADSET_PLUG: state: "+state+" name: "+headsetName+" mic: "+microphone);
// TODO: Should we require a permission?
ActivityManagerNative.broadcastStickyIntent(intent, null);
}
}
When you say "headset", do you mean "wired headset"? If so, there's an intent to detect whether or not one is being plugged or unplugged: ACTION_HEADSET_PLUG.
To check the status, you can use AudioManager.isWiredHeadsetOn(), although that may return false if there is also a bluetooth headset, and audio is routed to that instead.
AudioManager.isWiredHeadsetOn() always return false because it requires user-permission MODIFY_AUDIO_SETTINGS.
I spent several days while found answer. There are no info about this in official documentation. And this bug already registered in BugTracker.
This should help you : http://developer.android.com/reference/android/content/Intent.html#ACTION_HEADSET_PLUG
You can create this kind of receiver class (Kotlin with Flow) in your project:
class HeadsetPlugReceiver : BroadcastReceiver() {
private val _isPlugged = MutableStateFlow<Boolean>(false)
val isPlugged: StateFlow<Boolean> = _isPlugged.asStateFlow()
override fun onReceive(context: Context, intent: Intent) {
context.appComponent.inject(this)
val action = intent.action
Log.i(TAG, "onReceive: $action")
when (action) {
Intent.ACTION_HEADSET_PLUG -> sendEvent(intent)
else -> checkStateOff(intent)
}
}
private fun checkStateOff(intent: Intent) {
Log.i(TAG, "onReceive: the local Bluetooth adapter is off")
}
private fun sendEvent(intent: Intent) {
val isPlugged = intent.getIntExtra(HEADSET_STATE, 0) == 1
Log.i(TAG, "sendEvent: $isPlugged")
_isPlugged.value = isPlugged
}
private companion object {
private const val TAG = "HeadsetPlugReceiver"
// Headset constant
private const val HEADSET_STATE = "state"
}
}
then register this receiver in some class with context:
val headsetReceiver = HeadsetPlugReceiver()
val headsetFilter = IntentFilter(Intent.ACTION_HEADSET_PLUG)
context.registerReceiver(headsetReceiver, headsetFilter)
then collect isPlugged and you get the state of your wire headset connecting state
PS: don't forget to unregister your receiver when it no needs context.unregisterReceiver(headsetReceiver)
First , create receiver in your manifest:
<receiver android:name="com.yourapplication.BroadcastReceiver">
<intent-filter>
<action android:name="android.intent.action.HEADSET_PLUG"/>
</intent-filter>
</receiver>
don't forget to change com.yourapplication according to your project name
Create two variables in the head of your activity :
private BroadcastReceiver mReceiver ;
boolean Microphone_Plugged_in = false;
Define your receiver inside onCreate of your activity :
mReceiver = new BroadcastReceiver() {
#Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
int iii=2;
if (Intent.ACTION_HEADSET_PLUG.equals(action)) {
iii=intent.getIntExtra("state", -1);
if(Integer.valueOf(iii)==0){
Microphone_Plugged_in = false;
Toast.makeText(getApplicationContext(),"microphone not plugged in",Toast.LENGTH_LONG).show();
}if(Integer.valueOf(iii)==1){
Microphone_Plugged_in = true;
Toast.makeText(getApplicationContext(),"microphone plugged in",Toast.LENGTH_LONG).show();
}
}
}};
IntentFilter receiverFilter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
registerReceiver( mReceiver, receiverFilter );
add onResume and onStope :
#Override
protected void onResume() {
super.onResume();
IntentFilter filter = new IntentFilter(Intent.ACTION_HEADSET_PLUG);
getApplicationContext().registerReceiver(mReceiver, filter);
}
#Override
protected void onStop() {
super.onStop();
getApplicationContext().unregisterReceiver(mReceiver);
}
To add to the other answers, from Android documentation:
Warning: Limit how many broadcast receivers you set in your app.
Having too many broadcast receivers can affect your app's performance
and the battery life of users' devices. For more information about
APIs you can use instead of the BroadcastReceiver class for scheduling
background work, see Background Optimizations.
https://developer.android.com/guide/topics/manifest/receiver-element
Which means that you should create a small number of broadcast receivers as possible, to prevent memory issues with your app.
I will suggest using a singleton class with this receiver. In Kotlin:
class HeadsetReceiver private constructor(): BroadcastReceiver() {
// instances
var callback: HeadsetReceiverCallback? = null
//region singleton
private object HOLDER {
val INSTANCE = HeadsetReceiver()
}
companion object {
val instance: HeadsetReceiver by lazy { HOLDER.INSTANCE }
}
//endregion
override fun onReceive(context: Context, intent: Intent) {
if (intent.action == Intent.ACTION_HEADSET_PLUG) {
if(intent.getIntExtra("state", -1) == 0) {
callback?.onHeadsetDisconnected()
} else {
callback?.onHeadsetConnected()
}
}
}
fun register(context: Context) {
val receiverFilter = IntentFilter(Intent.ACTION_HEADSET_PLUG)
context.registerReceiver(this, receiverFilter)
}
fun unregister(context: Context) {
context.unregisterReceiver(this)
callback = null
}
interface HeadsetReceiverCallback {
fun onHeadsetConnected()
fun onHeadsetDisconnected()
}
}
Register:
HeadsetReceiver.instance.register(context)
HeadsetReceiver.instance.callback = object : HeadsetReceiver.HeadsetReceiverCallback {
override fun onHeadsetConnected() {
println("connected!")
}
override fun onHeadsetDisconnected() {
println("disonnected!")
}
}
Unregister:
HeadsetReceiver.instance.unregister(context)

How to enable/disable bluetooth programmatically in android

I want to enable/disable bluetooth through the program. I have the following code.
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (!mBluetoothAdapter.isEnabled()) {
Intent enableBtIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT);
But this code is not working in SDK 1.5. How can I make it work?
this code worked for me..
//Disable bluetooth
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter.isEnabled()) {
mBluetoothAdapter.disable();
}
For this to work, you must have the following permissions:
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
Here is a bit more robust way of doing this, also handling the return values of enable()\disable() methods:
public static boolean setBluetooth(boolean enable) {
BluetoothAdapter bluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
boolean isEnabled = bluetoothAdapter.isEnabled();
if (enable && !isEnabled) {
return bluetoothAdapter.enable();
}
else if(!enable && isEnabled) {
return bluetoothAdapter.disable();
}
// No need to change bluetooth state
return true;
}
And add the following permissions into your manifest file:
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
But remember these important points:
This is an asynchronous call: it will return immediately, and clients
should listen for ACTION_STATE_CHANGED to be notified of subsequent
adapter state changes. If this call returns true, then the adapter
state will immediately transition from STATE_OFF to STATE_TURNING_ON,
and some time later transition to either STATE_OFF or STATE_ON. If
this call returns false then there was an immediate problem that will
prevent the adapter from being turned on - such as Airplane mode, or
the adapter is already turned on.
UPDATE:
Ok, so how to implement bluetooth listener?:
private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
#Override
public void onReceive(Context context, Intent intent) {
final String action = intent.getAction();
if (action.equals(BluetoothAdapter.ACTION_STATE_CHANGED)) {
final int state = intent.getIntExtra(BluetoothAdapter.EXTRA_STATE,
BluetoothAdapter.ERROR);
switch (state) {
case BluetoothAdapter.STATE_OFF:
// Bluetooth has been turned off;
break;
case BluetoothAdapter.STATE_TURNING_OFF:
// Bluetooth is turning off;
break;
case BluetoothAdapter.STATE_ON:
// Bluetooth is on
break;
case BluetoothAdapter.STATE_TURNING_ON:
// Bluetooth is turning on
break;
}
}
}
};
And how to register/unregister the receiver? (In your Activity class)
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// ...
// Register for broadcasts on BluetoothAdapter state change
IntentFilter filter = new IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED);
registerReceiver(mReceiver, filter);
}
#Override
public void onStop() {
super.onStop();
// ...
// Unregister broadcast listeners
unregisterReceiver(mReceiver);
}
Android BluetoothAdapter docs say it has been available since API Level 5. API Level 5 is Android 2.0.
You can try using a backport of the Bluetooth API (have not tried it personally): http://code.google.com/p/backport-android-bluetooth/
To Enable the Bluetooth you could use either of the following functions:
public void enableBT(){
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (!mBluetoothAdapter.isEnabled()){
Intent intentBtEnabled = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
// The REQUEST_ENABLE_BT constant passed to startActivityForResult() is a locally defined integer (which must be greater than 0), that the system passes back to you in your onActivityResult()
// implementation as the requestCode parameter.
int REQUEST_ENABLE_BT = 1;
startActivityForResult(intentBtEnabled, REQUEST_ENABLE_BT);
}
}
The second function is:
public void enableBT(){
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (!mBluetoothAdapter.isEnabled()){
mBluetoothAdapter.enable();
}
}
The difference is that the first function makes the app ask the user a permission to turn on the Bluetooth or to deny. The second function makes the app turn on the Bluetooth directly.
To Disable the Bluetooth use the following function:
public void disableBT(){
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter.isEnabled()){
mBluetoothAdapter.disable();
}
}
NOTE/ The first function needs only the following permission to be defined in the AndroidManifest.xml file:
<uses-permission android:name="android.permission.BLUETOOTH"/>
While, the second and third functions need the following permissions:
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
The solution of prijin worked perfectly for me. It is just fair to mention that two additional permissions are needed:
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
When these are added, enabling and disabling works flawless with the default bluetooth adapter.
I used the below code to disable BT when my app launches and works fine. Not sure if this the correct way to implement this as google recommends not using "bluetooth.disable();" without explicit user action to turn off Bluetooth.
BluetoothAdapter bluetooth = BluetoothAdapter.getDefaultAdapter();
bluetooth.disable();
I only used the below permission.
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
Updated for Android 12:
AndroidManifest.xml -
<uses-permission android:name="android.permission.BLUETOOTH_CONNECT"/>
You must perform the standard permission request for BLUETOOTH_CONNECT as you would when requesting permission for storage or other "prompted" items.
Usage (Kotlin) -
val bluetoothAdapter = (getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager).adapter
if (bluetoothAdapter.isEnabled)
bluetoothAdapter.disable()
For Android 12 and above, BLUETOOTH and BLUETOOTH_ADMIN permissions are not necessary to retrieve the current state or toggle it, unless targeting lower APIs.
I have made a class to handle almost all this in Kotlin using Coroutines
class ActivityResultHandler(
private val registry: ActivityResultRegistry
) {
private val handlers = mutableListOf<ActivityResultLauncher<*>>()
fun unregisterHandlers() {
handlers.forEach {
it.unregister()
}
}
suspend fun requestLocationPermission(): Boolean {
return suspendCoroutine<Boolean> { continuation ->
val launcher = registry.register(
LOCATION_PERMISSION_REQUEST,
// lifecycleOwner,
ActivityResultContracts.RequestPermission()
) {
continuation.resumeWith(Result.success(it))
}
handlers.add(launcher)
launcher.launch(Manifest.permission.ACCESS_FINE_LOCATION)
}
}
suspend fun requestBluetoothActivation(): Boolean {
return suspendCoroutine<Boolean> { continuation ->
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
val launcher = registry.register(
BLUETOOTH_ON_REQUEST,
// lifecycleOwner,
ActivityResultContracts.StartActivityForResult()
) { result ->
continuation.resume(
result.resultCode == Activity.RESULT_OK
)
}
handlers.add(launcher)
launcher.launch(enableBtIntent)
}
}
fun checkLocationPermission(context: Context): Boolean {
return ContextCompat.checkSelfPermission(
context,
Manifest.permission.ACCESS_FINE_LOCATION
) == PackageManager.PERMISSION_GRANTED
}
private suspend fun requestLocationActivation(
intentSenderRequest: IntentSenderRequest,
): Boolean {
return suspendCoroutine { continuation ->
val launcher = registry.register(
LOCATION_ACTIVATION_REQUEST,
// lifecycleOwner,
ActivityResultContracts.StartIntentSenderForResult()
) {
continuation.resume(it.resultCode == Activity.RESULT_OK)
}
handlers.add(launcher)
launcher.launch(intentSenderRequest)
}
}
suspend fun enableLocation(context: Context): Boolean =
suspendCoroutine { continuation ->
val locationSettingsRequest = LocationSettingsRequest.Builder()
// .setNeedBle(true)
.addLocationRequest(
LocationRequest.create().apply {
priority = LocationRequest.PRIORITY_HIGH_ACCURACY
}
)
.build()
val client: SettingsClient = LocationServices.getSettingsClient(context)
val task: Task<LocationSettingsResponse> =
client.checkLocationSettings(locationSettingsRequest)
task.addOnSuccessListener {
continuation.resume(true)
}
task.addOnFailureListener { exception ->
if (exception is ResolvableApiException &&
exception.statusCode == LocationSettingsStatusCodes.RESOLUTION_REQUIRED
) {
val intentSenderRequest =
IntentSenderRequest.Builder(exception.resolution).build()
CoroutineScope(continuation.context).launch {
val result = requestLocationActivation(intentSenderRequest)
continuation.resume(result)
}
} else {
continuation.resume(false)
}
}
}
companion object {
private const val LOCATION_PERMISSION_REQUEST = "LOCATION_REQUEST"
private const val BLUETOOTH_ON_REQUEST = "LOCATION_REQUEST"
private const val LOCATION_ACTIVATION_REQUEST = "LOCATION_REQUEST"
}
}
Use it like this:
// make sure you extend AppCompatActivity
class MainActivity : AppCompatActivity() {
private val permissionRequests = ActivityResultHandler(activityResultRegistry)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// use viewmodels and fragments instead of GlobalScope
GlobalScope.launch {
// turn on bluetooth
permissionRequests.requestBluetoothActivation()
// to be able to scan for devices you also need location permission
// also show pop up to let users know why you need location
// https://support.google.com/googleplay/android-developer/answer/9799150?hl=en
permissionRequests.requestLocationPermission()
// also you need navigation to be enabled
permissionRequests.enableLocation(this#MainActivity)
}
}
override fun onDestroy() {
super.onDestroy()
permissionRequests.unregisterHandlers()
}
}
coroutines dependency in gradle
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.2'
also add this permissions to manifest
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission-sdk-23 android:name="android.permission.ACCESS_COARSE_LOCATION" />
Add the following permissions into your manifest file:
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>
Enable bluetooth use this
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (!mBluetoothAdapter.isEnabled()) {
mBluetoothAdapter.enable();
}else{Toast.makeText(getApplicationContext(), "Bluetooth Al-Ready Enable", Toast.LENGTH_LONG).show();}
Disable bluetooth use this
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter.isEnabled()) {
mBluetoothAdapter.disable();
}
try this:
//this method to check bluetooth is enable or not: true if enable, false is not enable
public static boolean isBluetoothEnabled()
{
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (!mBluetoothAdapter.isEnabled()) {
// Bluetooth is not enable :)
return false;
}
else{
return true;
}
}
//method to enable bluetooth
public static void enableBluetooth(){
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (!mBluetoothAdapter.isEnabled()) {
mBluetoothAdapter.enable();
}
}
//method to disable bluetooth
public static void disableBluetooth(){
BluetoothAdapter mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
if (mBluetoothAdapter.isEnabled()) {
mBluetoothAdapter.disable();
}
}
Add these permissions in manifest
<uses-permission android:name="android.permission.BLUETOOTH"/>
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN"/>

Categories

Resources