My countdown App uses text-to-speech to provide an audio countdown.
When the app detects a Bluetooth speaker audio is sent to that speaker.
The problem Wearables like the Samsung Watch 4 and TicWatch Pro 3 are reported as HEADSETs !
e.g. My original onCreate() included:
// Check if a speaker is connected
if (bluetoothAdapter.getProfileConnectionState(BluetoothHeadset.HEADSET) == BluetoothHeadset.STATE_CONNECTED)
sendSpeechToSpeaker()
else
sendSpeechToPhone()
Question 1
Is there a simple fix to the above which will only detect connected HEADSETs ?
My workaround involves individually checking each connected Bluetooth device and ignoring those which are not Headsets
Question 2
Can someone suggest an easier method than my Workaround?
Workaround
During initialisation each CONNECTED Bluetooth device is checked and if they are actually a headset audio is rerouted
The btServiceListener which checks each CONNECTED device
val btServiceListener: ServiceListener = object : ServiceListener {
// used to scan all CONNECTED Bluetooth devices looking for external speakers ...
override fun onServiceConnected(profile: Int, proxy: BluetoothProfile) {
if (profile == BluetoothProfile.HEADSET) {
val connectionStates = intArrayOf(BluetoothProfile.STATE_CONNECTED)
// get all connected headsets
val connectedHeadsetList = proxy.getDevicesMatchingConnectionStates(connectionStates)
for (connectedHeadset in connectedHeadsetList) {
// check each headset and check if it is ACTUALLY a headset
val majorMask = BluetoothClass.Device.Major.UNCATEGORIZED // actually want to use BITMASK but some Fwit declared it private !
val isHeadset = (connectedHeadset.bluetoothClass?.deviceClass?.and(majorMask) == BluetoothClass.Device.Major.AUDIO_VIDEO)
if (isHeadset)
sendSpeechToSpeaker()
}
}
bluetoothAdapter.closeProfileProxy(BluetoothProfile.HEADSET, proxy) // here we are finished with the proxy so clear
}
override fun onServiceDisconnected(profile: Int) {}
}
The above listener is called in onCreate()
// search the list of connected bluetooth headsets
bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothAdapter = bluetoothManager.adapter
bluetoothAdapter.getProfileProxy(this, btServiceListener, BluetoothProfile.HEADSET);
The Listener works using the Major Mask
The code for HEADSETS is (0x0400)
The Samsung Galaxy Watch is WEARABLE (0x0700)
The TicWatch Pro 3 GPS is UNCATEGORIZED (0x1F00)
For simplicity I am not showing the same code required in the BTListener to ensure if a watch disconnects it does not route audio away from the speaker !
The above issues can be avoided by checking the audio properties directly rather than parsing each connected Bluetooth Device
private fun routeTTSAudioOutput() {
// on start and when changes are detected ensure audio is being routed correctly
val bluetoothA2DPConnected = audioOutputAvailable(AudioDeviceInfo.TYPE_BLUETOOTH_A2DP)
if (bluetoothA2DPConnected) {
if (!bluetoothSCOTurnedOn) audioManager.startBluetoothSco()
audioManager.mode = AudioManager.MODE_IN_CALL
} else {
if (bluetoothSCOTurnedOn) audioManager.stopBluetoothSco()
audioManager.mode = AudioManager.MODE_NORMAL
}
audioManager.isSpeakerphoneOn = !bluetoothA2DPConnected
bluetoothSCOTurnedOn = bluetoothA2DPConnected
bundleTTS = Bundle()
bundleTTS.putInt(TextToSpeech.Engine.KEY_PARAM_STREAM, audioManager.mode)
this.runOnUiThread { bluetoothConnectedImageView.visibility = if (bluetoothA2DPConnected) ImageView.VISIBLE else ImageView.INVISIBLE }
}
The audio callback uses the above function to route text to speech
private val audioDeviceCallback : AudioDeviceCallback = object : AudioDeviceCallback() {
override fun onAudioDevicesAdded(addedDevices: Array<out AudioDeviceInfo>?) {
super.onAudioDevicesAdded(addedDevices)
routeTTSAudioOutput()
}
override fun onAudioDevicesRemoved(removedDevices: Array<out AudioDeviceInfo>?) {
super.onAudioDevicesRemoved(removedDevices)
routeTTSAudioOutput()
}
}
Related
I have an android app that supports calling capabilities. We've been experiencing speakerphone issues on android 12 and higher devices in the past few months.
When the speakerphone button is pressed, there is sometimes no effect on changing the audio path, even though the logs show the speakerphone route is being applied in all scenarios. The problem reproduces in around half of the tests. This is the code I have for the audio manager:
enum class AudioOutput {
MAIN, SPEAKER, FRONT
/// Main: Can be headphones, front ear speaker, bluetooth. (NOTE: Headphones override bluetooth) (double check on android)
/// Speaker: Used for the bottom device speaker.
/// Front: Can be the front ear speaker or headphones. Only used while bluetooth is connected and main is dedicated to bluetooth.
}
enum class AudioMode {
NORMAL, CALL, RINGER
/// Normal: Default audio mode.
/// Call: When user is on softphone or in a call.
/// Ringer: When user is receiving a call to play ring tone.
}
class AudioManager {
companion object {
val instance = AudioManager()
}
var audioMode: AudioMode = AudioMode.NORMAL; private set
var audioOutput: AudioOutput = AudioOutput.MAIN; private set
fun setAudioMode(mode: AudioMode) {
audioMode = mode
applyAudioRoute()
}
fun setAudioOutput(output: AudioOutput) {
audioOutput = output
if (audioMode != AudioMode.NORMAL) {
applyAudioRoute()
}
}
private fun applyAudioRoute() {
Timber.d("[AudioManager] ApplyRoute -> Mode: $audioMode, Output: $audioOutput")
val context = MyApplication.getAppContext()
val audioManager = context?.getSystemService(Context.AUDIO_SERVICE) as android.media.AudioManager
when (audioMode) {
AudioMode.NORMAL -> {
audioManager.setMode(android.media.AudioManager.MODE_NORMAL)
audioManager.setSpeakerphoneOn(true)
}
AudioMode.CALL -> {
when (audioOutput) {
AudioOutput.MAIN -> {
audioManager.setMode(android.media.AudioManager.MODE_IN_COMMUNICATION)
audioManager.setSpeakerphoneOn(false)
audioManager.setBluetoothScoOn(true)
audioManager.startBluetoothSco()
}
AudioOutput.SPEAKER -> {
audioManager.setMode(android.media.AudioManager.MODE_IN_COMMUNICATION)
audioManager.setSpeakerphoneOn(true)
audioManager.setBluetoothScoOn(false)
audioManager.stopBluetoothSco()
}
AudioOutput.FRONT -> {
audioManager.setMode(android.media.AudioManager.MODE_IN_COMMUNICATION)
audioManager.setSpeakerphoneOn(false)
audioManager.setBluetoothScoOn(false)
audioManager.stopBluetoothSco()
}
}
}
AudioMode.RINGER -> {
if (!MyCallStateManager.instance.isCallActive()) {
audioManager.setMode(android.media.AudioManager.MODE_RINGTONE)
audioManager.setSpeakerphoneOn(true)
}
}
}
}
}
I would appreciate any direction or assistance in tracking down this issue. In addition, this is my first stack overflow post, so I apologize if I there's something missed in the post.
I've Changed the AudioManager variables that were set statically to function calls:
audioManager.isSpeakerphoneOn = true -> setSpeakerphoneOn()
audioManager.mode = android.media.AudioManager.MODE_IN_COMMUNICATION -> audioManager.setMode(android.media.AudioManager.MODE_IN_COMMUNICATION)
I'm a beginner in the BLE area.
Using the BLE scanner, I would like to use the ScanFilter API to filter the results using the serial port UUID: 00001101-0000-1000-8000-00805f9b34fb(my target are bluetooth printers).
Currently, after starting the BLE scan, I'm not getting any results in the onScanResult method of the ScanCallback object.
Without using the filter, I'm receiving bluetooth devices correctly. I noticed that if I try to get the device UUIDs into onScanResult it returns null, while if I run the method fetchUuidsWithSdp the UUIDs are returned correctly.
Here my current code to start scanning:
val serviceUuidMaskString = "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF"
val parcelUuidMask = ParcelUuid.fromString(serviceUuidMaskString)
val filter = ScanFilter.Builder().setServiceUuid(
ParcelUuid(UUID.fromString("00001101-0000-1000-8000-00805f9b34fb")), parcelUuidMask
).build()
val settings = ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_BALANCED).build()
handler.postDelayed({
bluetoothAdapter.bluetoothLeScanner.stopScan(bleCallback)
}, 15000)
bluetoothAdapter.bluetoothLeScanner.startScan(listOf(filter), settings, bleCallback)
And here the ScanCallback:
object : ScanCallback() {
#SuppressLint("MissingPermission")
override fun onScanResult(callbackType: Int, result: ScanResult?) {
result?.let {
it.device.fetchUuidsWithSdp()
Log.i(
TAG,
"BLE device: ${it.device?.name.orEmpty()}\n UUIDS: ${it.device?.uuids?.joinToString()}"
)
Sorry for my bad english.
Thanks in advance.
i have an application, and my application can connect to a bluetooth device.
After that, i want to send message (Int) to my Blutooth Low Energy device.
I have this code, but i can't figure it out what is the problem.
If you want i have : Characteristic UUID, Service UUID.
Really, i need your help...
I've edited the question :
My code :
val filter = IntentFilter(BluetoothAdapter.ACTION_STATE_CHANGED)
lateinit var bluetoothAdapter: BluetoothAdapter
val bluetoothManager = context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothAdapter = bluetoothManager.adapter
settingViewModel.bluetooth(bluetoothAdapter = bluetoothAdapter)
val mReceiver: BroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent) {
val action = intent.action
if (action == BluetoothAdapter.ACTION_STATE_CHANGED) {
val state = intent.getIntExtra(
BluetoothAdapter.EXTRA_STATE,
BluetoothAdapter.ERROR
)
when (state) {
BluetoothAdapter.STATE_OFF -> {
settingViewModel.setIsConnected(false)
//settingViewModel.stopScan()
settingViewModel.setListDevices(null)
}
BluetoothAdapter.STATE_ON -> {
settingViewModel.setIsConnected(true)
//scan()
settingViewModel.setListDevices(bluetoothAdapter.bondedDevices)
context!!.unregisterReceiver(this)
}
}
}
}
}
context.registerReceiver(mReceiver, filter)
val SERVICE_UUID = "00000000-0001-11e1-9ab4-0002a5d5c51c"
val ConfigCharacteristic = descriptorOf(
service = SERVICE_UUID,
characteristic = "00E00000-0001-11e1-ac36-0002a5d5c51b",
descriptor = "00000000-0000-0000-0000-000000000000",
)
Button(
onClick = {
if (settingViewModel.isConnected.value == true) {
coroutine.launch(Dispatchers.IO) {
try {
settingViewModel.peripheral.write(ConfigCharacteristic, byteArrayOf(1))
} catch (e: Exception) {
Toast.makeText(context, e.message, Toast.LENGTH_SHORT).show()
}
}
}
// try {
// val Service =
// settingViewModel.deviceSocket.value.get .getService(UUID.fromString("0000ffe0-0000-1000-8000-00805f9b34fb"))
// val charac: BluetoothGattCharacteristic =
// Service.getCharacteristic(UUID.fromString("00E00000-0001-11e1-ac36-0002a5d5c51b"))
// settingViewModel.deviceSocket.value!!.outputStream.write("1".toByteArray())
// } catch (e: Exception) {
// Toast.makeText(context, e.message.toString(), Toast.LENGTH_LONG).show()
// }
}
) {
Text(text = "HelloWorld")
}
I Already have the mac adress, the caracteristic and the service UUID of the device i want to connect to.
Again, i really need your help
First of all:
When developing an app for a BLE device it is best to first use a generic BLE scanner app to test the connection and to find out which commands need to be sent. If you confirm that the BLE device works as expected you can continue with your own custom app. I would recommend nRF Connect for this task.
Regarding your problem:
There are still many things missing from your sourcecode. You said you can connect to the device but have problems sending a message. Your code does not contain anything related to a BLE connection so I can only assume that you connected to the device using the Bluetooth settings of your phone. This would be correct for Bluetooth Classic but BLE requires you to connect through your own custom app.
The Ultimate Guide to Android Bluetooth Low Energy explains all steps necessary for a successful BLE connection. These steps are:
Setting the correct permissions
Scan for nearby BLE devices
Connect to a BLE device of your choosing
Scan for Services
Read and Write a characteristic of your choosing
All these steps are explained in the Guide using Kotlin as programming language.
I am using BluetoothLeScanner to scan for BLE devices and get a list of objects representing the devices to show inside my app (not connecting to any of them).
I am interested in doing the same, but using the CompanionDeviceManager now. Its callback CompanionDeviceManager.Callback.onDeviceFound(chooserLauncher: IntentSender?) unfortunately does not return any human readable form of found devices... the closest it gets is the IntentSender.writeToParcel method, but I am not sure how to use it in this situation.
I am not constrained to use the CompanionDeviceManager but I wanted to follow the OS version specific guidelines, we are supposed to use CompanionDeviceManager for Bluetooth devices scanning starting from API 26, but it seems useless in my case... so is there any way to get devices data from that callback, or should I just ditch it and stay with BluetoothLeScanner for all OS versions?
Late answer but it might help someone else. You can create a bluetooth device picker in combination with ActivityResultContracts.StartIntentSenderForResult() in order to get the BluetoothDevice. From there you will have access to all the device info that you need. Recent changes added some Android 12 permissions like android.permission.BLUETOOTH_CONNECT. Your mileage may vary.
val context = LocalContext.current
// Get the device manager instance
val deviceManager: CompanionDeviceManager by lazy {
ContextCompat.getSystemService(
context,
CompanionDeviceManager::class.java
) as CompanionDeviceManager
}
// Create a filter of your choice. Here I just look for specific device names
val deviceFilter: BluetoothDeviceFilter by lazy {
BluetoothDeviceFilter.Builder()
.setNamePattern(Pattern.compile(supportedDevices))
.build()
}
// Create a pairing request with your filter from the last step
val pairingRequest: AssociationRequest = AssociationRequest.Builder()
.addDeviceFilter(deviceFilter)
.build()
// Create a picker for discovered bluetooth devices
val bluetoothDevicePicker = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartIntentSenderForResult(),
onResult = {
val device: BluetoothDevice? =
it.data?.getParcelableExtra(CompanionDeviceManager.EXTRA_DEVICE)
try {
// Now that you have the desired device, do what you need to with it
device?.apply {
when {
name?.matches(Regex(firstDevicePattern)) == true -> {
Log.i(TAG, "${this.name} connected")
onFirstDeviceDiscovered(device)
}
name?.matches(Regex(secondDevicePattern)) == true -> {
Log.i(TAG, "${this.name} connected")
onSecondDeviceDiscovered(device)
}
}
}
} catch (e: SecurityException) {
e.printStackTrace()
//TODO: handle the security exception (this is possibly a bug)
// https://issuetracker.google.com/issues/198986283
}
}
)
// A utility function to centralize calling associate (optional)
val associateDevice: (AssociationRequest) -> Unit = { request ->
// Attempt to associate device(s)
deviceManager.associate(
request,
object : CompanionDeviceManager.Callback() {
override fun onDeviceFound(chooserLauncher: IntentSender) {
val sender = IntentSenderRequest.Builder(chooserLauncher)
.build()
bluetoothDevicePicker.launch(sender)
}
override fun onFailure(error: CharSequence?) {
//TODO: handle association failure
}
}, null
)
}
I have an application that will listen for voice input through bluetooth if available and if not available then will read through the phone microphone. I can't seem to find a way to check if their are any bluetooth devices already connected (not just paired). Any suggestions?
The following worked for me (API >= 18):
final BluetoothManager bluetoothManager = (BluetoothManager)getSystemService(Context.BLUETOOTH_SERVICE);
List<BluetoothDevice> gattServerConnectedDevices = bluetoothManager.getConnectedDevices(BluetoothProfile.GATT_SERVER);
for (BluetoothDevice device : gattServerConnectedDevices) {
Log.d("Tag", "Found connected device: " + device.getAddress());
}
Try this method, API 18 though.
https://developer.android.com/reference/android/bluetooth/BluetoothManager.html#getConnectedDevices(int)
It's possible to list connected headset devices via BluethoothHeadset service
btAdapter.getProfileProxy(context, object : BluetoothProfile.ServiceListener {
override fun onServiceDisconnected(p0: Int) {
//
}
override fun onServiceConnected(p0: Int, headset: BluetoothProfile?) {
headset?.connectedDevices?.forEach {
Timber.d("${it.name} ${it.address}")
}
}
}, BluetoothProfile.HEADSET)