Following a lot of answers here, I am able to build the list of connected bluetooth devices with the help of a BroadcastReceiver. Now my question is how do I know which device supports which profile. I want to be able to pick the devices based on the profile, for example, get a list of currently connected devices and their profile, and pick one of them. I don't see how I can get such info if I have the instance of BluetoothDevice.
On this page there are some codes illustrating how to work with a bluetooth headset profile: http://developer.android.com/guide/topics/connectivity/bluetooth.html#Profiles. But it doesn't solve my problem. If you think I am missing anything, please help me and point it out.
Thanks a lot in advance.
I've run into the same problem. It doesn't appear that you can get the available profiles from the BluetoothDevice class.
But there is a long way around by getting a List of BluetoothDevices from the getDevicesMatchingConnectionStates method in the BluetoothProfile class.
For example if you want to find which BluetoothDevices support A2DP, first create a custom BluetoothProfile.ServiceListener
public class cServiceListener implements BluetoothProfile.ServiceListener {
private static final int[] states={ BluetoothProfile.STATE_DISCONNECTING,
BluetoothProfile.STATE_DISCONNECTED,
BluetoothProfile.STATE_CONNECTED,
BluetoothProfile.STATE_CONNECTING};
#Override
public void onServiceConnected(int profile, BluetoothProfile bluetoothProfile) {
List<BluetoothDevice> Devices=bluetoothProfile.getDevicesMatchingConnectionStates(states);
for (BluetoothDevice loop:Devices){
Log.i("myTag",loop.getName());
}
}
#Override
public void onServiceDisconnected(int profile) {
}
}
Then attach it to the profile you want to check, in this example A2DP
mBluetoothAdapter = BluetoothAdapter.getDefaultAdapter();
cServiceListener mServiceListener=new cServiceListener();
mBluetoothAdapter.getProfileProxy(thisContext,mServiceListener, BluetoothProfile.A2DP);
This will logcat all the bluetooth devices that support A2DP which are in the requested states. In this example it includes all devices which are currently connected and previously paired devices which are disconnected.
Looking at the Android source code, you can guess which profiles are available for a device by looking at its UUIDs, and then connect each profile one by one.
Step 0 : Copy the _PROFILE_UUIDS constants from there : https://android.googlesource.com/platform/packages/apps/Settings/+/9ad703cdb9a8d0972c123b041d18aa7bbeb391a4/src/com/android/settings/bluetooth/LocalBluetoothProfileManager.java
Step 1 : get your BluetoothDevice, via scanning for instance. Assure that it's properly bonded.
Step 2 : register a BroadcastReceiver for the android.bluetooth.BluetoothDevice.ACTION_UUID action intent
Step 3 : on your device, call the fetchUuidsWithSdp method
Step 4 : you will recieve a ACTION_UUID broadcast : in the onReceive method you can unregister the receiver, and get the list of profiles like so :
ArrayList<Integer> profiles = new ArrayList<>();
ParcelUuid[] uuids = device.getUuids();
if (BluetoothUuid.containsAnyUuid(uuids, HEADSET_PROFILE_UUIDS))
{
profiles.add(BluetoothProfile.HEADSET);
}
if (BluetoothUuid.containsAnyUuid(uuids, A2DP_PROFILE_UUIDS))
{
profiles.add(BluetoothProfile.A2DP);
}
if (BluetoothUuid.containsAnyUuid(uuids, OPP_PROFILE_UUIDS))
{
//OPP doesn't have any BluetoothProfile value
}
if (BluetoothUuid.containsAnyUuid(uuids, HID_PROFILE_UUIDS))
{
//You will need system privileges in order to use this one
profiles.add(BluetoothProfile.INPUT_DEVICE);
}
if (BluetoothUuid.containsAnyUuid(uuids, PANU_PROFILE_UUIDS))
{
profiles.add(BluetoothProfile.PAN);
}
Step 5 : get the proxies for the profiles, one by one :
for (int profile : profiles)
{
if (!adapter.getProfileProxy(context, listener, profile))
{
//Do something
}
}
Step 6 : do anything with each proxy retrieved in the onServiceConnected method of your listener. You can access the connect method using relfection.
Related
Im developing an app which uses androids nearby connections to connect two mobile units.
When I test the scenario with 3 mobile units, where two are advertising and one is discovering, it chooses to make a connection to one of them.
How do I generate a list of found endpoints instead so that the user can choose between them and select the right one to connect to?
My code is the following when an endpoint is found:
private final EndpointDiscoveryCallback mEndpointDiscoveryCallback = new EndpointDiscoveryCallback() {
#Override
public void onEndpointFound(String endpointId, DiscoveredEndpointInfo info) {
Toast.makeText(MainActivity.this,"onEndpointFound",Toast.LENGTH_SHORT).show();
connect(endpointId);
}
#Override
public void onEndpointLost(String endpointId) {
Toast.makeText(MainActivity.this,"onEndpointLost",Toast.LENGTH_SHORT).show();
}
};
Following the documentation https://developers.google.com/nearby/connections/android/manage-connections it states "Depending on your use case, you may wish to instead display a list of discovered devices to the user, allowing them to choose which devices to connect to." But it does not state how to do this.
I use a LeScanCallback (can not use the newer scan methods because I'm developing for api 18. Not that it matters, since the android 5.0+ apis don't offer this functionality either) to detect when a nearby BLE device is detected:
private BluetoothAdapter.LeScanCallback bleCallback = new BluetoothAdapter.LeScanCallback() {
#Override
public void onLeScan(BluetoothDevice bluetoothDevice, int i, byte[] bytes) {
discoveredDevices.add(bluetoothDevice);
}
};
I am not pairing or connecting with the devices because that's not required, I simply want to see which devices are nearby.
I'm trying to make a service that, every 5 mins or so, calls a webserver to update which devices are nearby at that moment.
Tricky part is that the android device will be moving, so a bluetooth device that is nearby right now, might not be in 5 mins. In that case I need to remove it from discoveredDevices.
Ideally, I would like to receive a callback when a bluetooth device was in range before, but is not anymore. This callback doesn't exist though.
(I'm aware of the android.bluetooth.device.action.ACL_CONNECTED and android.bluetooth.device.action.ACL_DISCONNECTED broadcasts, but those are for when you connect to a bluetooth device, which I don't want.)
An option is to do a fresh scan every 5 mins, but you can't tell when all nearby devices have been discovered, so you would have to do a timed scan, e.g. scan for 5 seconds and then send the collected data to the webservice.
This sounds dirty and risky because you can never know for sure all nearby devices were discovered within the allotted time, so I would very much like to avoid doing it like that.
Is there another way to do this?
Edit
Some devices continuously report discovery of nearby bluetooth devices, even if they were already discovered before. If that functionality was universal I could solve my problem, however this is device specific.
My phone's bluetooth adapter for example only discovers nearby devices once. Some other devices I have tested with do continuously report the same nearby devices, but not all devices do, so I can't rely on that unfortunately.
This sounds dirty and risky because you can never know for sure all nearby devices were discovered within the allotted time, so I would very much like to avoid doing it like that.
That sounds like a reasonable assumption, but it's wrong.
Bluetooth low energy works in a particular way and BLE devices have some limits. For instance, they have a fixed range of possible advertising frequencies, ranging from 20 milliseconds to 10.24 seconds, in steps of 0.625 milliseconds. See here and here for more detailed information.
This means that it can take at most 10.24 seconds before a device will broadcast a new advertisement package. BLE devices generally, if not always, provide a way for their owner to adjust their advertising frequency, so the frequency can of course vary.
In cases where you are periodically collecting data about nearby devices, like yours, it is fine to use a scan with a fixed time limit, save that data somewhere, restart the scan, collect new data, compare with old data --> get results.
For example, if a device was found in scan 1 but not in scan 2, you can conclude that the device was in range, but is not anymore.
Same goes for the other way around: if a device was found in scan 4 but not in scan 3, it is a newly discovered device.
Finally, if a device was found in scan 5, was not found in scan 6, but was again found in scan 7, it is rediscovered and can be handled as such if need be.
Because I'm answering my own question here, I'll add the code that I used to implement this.
I have the scanning done in a background service, and communicate to other parts of the app using BroadcastReceivers. Asset is a custom class of mine that holds some data. DataManager is a custom class of mine that - how did you guess it - manages data.
public class BLEDiscoveryService extends Service {
// Broadcast identifiers.
public static final String EVENT_NEW_ASSET = "EVENT_NEW_ASSET ";
public static final String EVENT_LOST_ASSET = "EVENT_LOST_ASSET ";
private static Handler handler;
private static final int BLE_SCAN_TIMEOUT = 11000; // 11 seconds
// Lists to keep track of current and previous detected devices.
// Used to determine which are in range and which are not anymore.
private List<Asset> previouslyDiscoveredAssets;
private List<Asset> currentlyDiscoveredAssets;
private BluetoothAdapter bluetoothAdapter;
private BluetoothAdapter.LeScanCallback BLECallback = new BluetoothAdapter.LeScanCallback() {
#Override
public void onLeScan(BluetoothDevice bluetoothDevice, int i, byte[] bytes) {
Asset asset = DataManager.getAssetForMACAddress(bluetoothDevice.getAddress());
handleDiscoveredAsset(asset);
}
};
#Override
public void onCreate() {
super.onCreate();
BluetoothManager manager = (BluetoothManager) getSystemService(BLUETOOTH_SERVICE);
bluetoothAdapter = manager.getAdapter();
previouslyDiscoveredAssets = new ArrayList<>();
currentlyDiscoveredAssets = new ArrayList<>();
handler = new Handler();
}
#Override
public int onStartCommand(Intent intent, int flags, int startId) {
// Start scanning.
startBLEScan();
// After a period of time, stop the current scan and start a new one.
// This is used to detect when assets are not in range anymore.
handler.postDelayed(new Runnable() {
#Override
public void run() {
performRepeatingTask();
// Repeat.
handler.postDelayed(this, BLE_SCAN_TIMEOUT);
}
}, BLE_SCAN_TIMEOUT);
// Service is not restarted if it gets terminated.
return Service.START_NOT_STICKY;
}
#Override
public IBinder onBind(Intent intent) {
return null;
}
#Override
public void onDestroy() {
handler.removeCallbacksAndMessages(null);
stopBLEScan();
super.onDestroy();
}
private void startBLEScan() {
bluetoothAdapter.startLeScan(BLECallback);
}
private void stopBLEScan() {
bluetoothAdapter.stopLeScan(BLECallback);
}
private void handleDiscoveredAsset(Asset asset) {
currentlyDiscoveredAssets.add(asset);
// Notify observers that we have a new asset discovered, but only if it was not
// discovered previously.
if (currentlyDiscoveredAssets.contains(asset) &&
!previouslyDiscoveredAssets.contains(asset)) {
notifyObserversOfNewAsset(asset);
}
}
private void performRepeatingTask() {
// Check if a previously discovered asset is not discovered this scan round,
// meaning it's not in range anymore.
for (Asset asset : previouslyDiscoveredAssets) {
if (!currentlyDiscoveredAssets.contains(asset)) {
notifyObserversOfLostAsset(asset);
}
}
// Update lists for a new round of scanning.
previouslyDiscoveredAssets.clear();
previouslyDiscoveredAssets.addAll(currentlyDiscoveredAssets);
currentlyDiscoveredAssets.clear();
// Reset the scan.
stopBLEScan();
startBLEScan();
}
private void notifyObserversOfNewAsset(Asset asset) {
Intent intent = new Intent();
intent.putExtra("macAddress", asset.MAC_address);
intent.setAction(EVENT_NEW_ASSET);
sendBroadcast(intent);
}
private void notifyObserversOfLostAsset(Asset asset) {
Intent intent = new Intent();
intent.putExtra("macAddress", asset.MAC_address);
intent.setAction(EVENT_LOST_ASSET);
sendBroadcast(intent);
}
}
This code is not perfect and might even be buggy, but it will at least give you an idea or example of how this can be implemented.
I can recommend this approach:
Use Map<BluetoothDevice, Long> structure to store the discovered devices, where Long is the time of detection of the device (can be System.currentTimeMillis() for example).
Then in your service (as far as I understand from the question there will be implemented some kind of repeated task) just extract actual devices based on the time of their detection.
And you are absolutely right, there are no guarantee that all nearby devices were discovered within the allotted time. Especially this is actual for the Android devices.
iOS devices in it's turn have another issue - they can change their BluetoothDevice's adress in runtime without apparent external cause.
Hope this will help you to save the time during debugging.
Edit
As a result of research of this topic found this discussion on code.google.com
Issue is still open and seems that it is related to the hardware features and can't be fixed programmatically. Moreover, it seems that bug will remains on problem devices even after a system updates.
So restarting the scan periodically might be acceptable workaround for this case.
Am i the only one having problems connecting to the bluetooth with startBluetoothSco? This works fine in all versions of Android except 4.4.2 (kitkat). Any suggestions? And yes, I have verified that I am connected to Bluetooth before I call this. Did something changed in 4.4.2?
Here is my code:
am = (AudioManager)getSystemService(Context.AUDIO_SERVICE);
am.setMode(AudioManager.MODE_IN_CALL);
am.setBluetoothScoOn(true);
am.startBluetoothSco();
Following your suggestion i did the following, but this is driving me nuts! What am I doing wrong. I have the listener in my MainActivity as follows...
private final BluetoothHandler.Listener mBluetoothListener = new BluetoothHandler.Listener() {
#Override
public void onConnectionComplete() {
final BluetoothHandler bluetoothHandler = mBluetoothHandler;
if (bluetoothHandler != null) {
am.setMode(AudioManager.MODE_IN_COMMUNICATION);
}
}
};
Then in my OnCreate I initialize the BluetoothHandler
if(mBluetoothHandler == null){
mBluetoothHandler = new BluetoothHandler(5000, mBluetoothListener);
} else {
mBluetoothHandler.stopSco();
mBluetoothHandler.stop();
mBluetoothHandler = null;
}
if (!mBluetoothHandler.isAudioConnected()) {
mBluetoothHandler.start(mContext);
}
The problem I'm having is that the listener doesn't detect when a BT device connects or even says that one is connected. Any suggestions? I appreciate your help...
The functionality of startBluetoothSco() changed between API 17 and API 18. In API 17, this function initiates a virtual call via SCO. In API 18, the function opens a raw SCO link. Some Bluetooth units will only respond to a virtual call.
Unfortunately it doesn't seem that Google have given us an option of choosing whether to open a virtual call or raw link so if you require a virtual call, you will need to ensure your app is built with API 17.
From the Android Developer reference:
"NOTE: up to and including API version JELLY_BEAN_MR1, this method
initiates a virtual voice call to the bluetooth headset. After API
version JELLY_BEAN_MR2 only a raw SCO audio connection is
established."
http://developer.android.com/reference/android/media/AudioManager.html#startBluetoothSco()
Which device do you use? I know that there is sometimes a Bluetooth problem with nexus devices (nexus 5 with kit at also) as described here:
http://www.androidpolice.com/2013/12/28/bug-watch-many-nexus-devices-still-suffer-from-assorted-bluetooth-issues/
Had the same problem with an app I was developming. Upong updating my phone to KitKat, the SCO connection to my car stopped working.
I finally come up with a solultion using a somehow private api in the bluetooth headset profile, while keeping my code compatible with API 19.
Fist I'm checking if current version is API 17, in that case, I use the standard startBluetoothSco from AudioManager, if not the case, I get the current BluetoothHeadset profile and use the following method to create the virtual call sco link (I can't take credit for this, I found it in the Google TalkBack application):
class BluetoothHeadsetCompatWrapper {
private static final Class<?> CLASS_BluetoothHeadset = BluetoothHeadset.class;
private static final Method METHOD_startScoUsingVirtualVoiceCall = CompatUtils.getMethod(
CLASS_BluetoothHeadset, "startScoUsingVirtualVoiceCall", BluetoothDevice.class);
private static final Method METHOD_stopScoUsingVirtualVoiceCall = CompatUtils.getMethod(
CLASS_BluetoothHeadset, "stopScoUsingVirtualVoiceCall", BluetoothDevice.class);
private final BluetoothHeadset mHeadset;
public BluetoothHeadsetCompatWrapper(BluetoothHeadset headset) {
mHeadset = headset;
}
public boolean startScoUsingVirtualVoiceCall(BluetoothDevice device) {
return (Boolean) CompatUtils.invoke(mHeadset, false, METHOD_startScoUsingVirtualVoiceCall,
device);
}
public boolean stopScoUsingVirtualVoiceCall(BluetoothDevice device) {
return (Boolean) CompatUtils.invoke(mHeadset, false, METHOD_stopScoUsingVirtualVoiceCall,
device);
}
}
I found this solution when I was looking into the BluetoothHeadset code and found out the method actually exists, but it's hidden to the compiler https://android.googlesource.com/platform/frameworks/base/+/master/core/java/android/bluetooth/BluetoothHeadset.java
I know my anwser may be late, but I posted anyway to help others.
After reading the dot42 comments and trolling Java examples I managed to setup a Bluetooth connection but fail to open the connection. I cannot determine the problem. I followed the docs step by step.
My target device is a HTC Explorer running on 2.3 Gingerbread. Here is my code.
//Target 2.3 (Gingerbread)
[assembly: Application("dot42Application1")]
[assembly: UsesPermission(Android.Manifest.Permission.BLUETOOTH)]
[assembly: UsesPermission(Android.Manifest.Permission.BLUETOOTH_ADMIN)]
namespace dot42Application1
{
[Activity]
public class MainActivity : Activity
{
private TextView txStatus;
protected override void OnCreate(Bundle savedInstance)
{
base.OnCreate(savedInstance);
SetContentView(R.Layouts.MainLayout);
// Find UI controls
txStatus = FindViewById<TextView>(R.Ids.txStatus);
Intent enableIntent = new Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE);
var bt = BluetoothAdapter.GetDefaultAdapter();
if (bt != null) //If device has not Bluetooth this will be null
{
if (bt.IsEnabled()) //Is Bluetooth device enabled?
{
var BT_My_Addr = bt.Address; //Get the devices MAC
var BT_Bonded = bt.GetBondedDevices().ToList(); //Get a list of bonded devices- I bonded to a BT2TTL Board earlier.
txStatus.Text = BT_My_Addr + System.Environment.NewLine; //Shows my MAC on screen.
string BT_Remote_Address = string.Empty;
foreach (var BTDevice in BT_Bonded) //Just searchging for string in bonded list
{
if (BTDevice.Name.Contains("linvor"))
{
BT_Remote_Address = BTDevice.Address;
}
}
//Gets remote device
var BT_Remote_Device = bt.GetRemoteDevice(BT_Remote_Address);
//Create a RFCOMM Socket to remote device using popular UUID ofr BT Serial boards
var BTsocket = BT_Remote_Device.CreateInsecureRfcommSocketToServiceRecord(Java.Util.UUID.FromString("00001101-0000-1000-8000-00805F9B34FB"));
//Call anyway to make sure there is no discvoerry in the backgorund. It slows stuff down.
bt.CancelDiscovery();
//Exception here? Dont know why :(
BTsocket.Connect();
//Suppsoed to dump 0 to 99999 to my listening serial device but I never get this far.
var BT_Out = BTsocket.GetOutputStream();
for (int i = 0; i < 99999; i++)
{
BT_Out.Write(Encoding.ASCII.GetBytes(i.ToString()));
}
}
else
{
txStatus.Text = "Bluetooth is disabled :(";
}
}
}
}
And this is what it shows after the socket creation
and the error...
What am I doing wrong? :(
I seem to have solved the problem by analysing various code snippets on the internet. I think the problem was trying to do everything in the OnCreate method. The steps I followed are the following:
Created a button on the main view (MainActivity.xml) and attached a onClick method.
Moved all the code OUT of the OnCreate method. (I think this allows the application to fully initialise.) Created an event handler for the button with two methods.
The two methods are the same as the code I posted in my original question. Just they are separated out and called when the user clicks the button.
findBT() Gets the default adapter. Checks if Bluetooth is enabled if not does the intent filter. Or if it is it will cycle through the bonded list and match a device name and store the BluetoohDevice in a variable. This is another thing that is different from my code. I do not use GetRemoteDevice I just assign the device from the BondedList to my global variable.
openBT() creates the RFCOMM socket (this did not work with unsecure - it threw an exception but using the secure method worked!)
You have to pair to the remote device using the Androids Bluetooth control panel. This code will not scan or connect to devices that are not paired. It will just throw null exceptions.
Also I left the target SDK 2.3.x but I am using the 4.x API.
-Disclosure. I am not a seasoned Android developer and just learning about the life cycle of Java applications in the Android context. I hope this can help other C# developers trying to do the same.
I have 2 Android devices using WiFi Direct. On one device I can get information about the other device using the WifiP2pManager class, and request a connection to the other device. However when I request a connection, the other device pops up a little window and asks the user if they want to accept the connection request.
Is it possible to auto-accept these connection requests? I.E to be able to connect to the other device without user confirmation?
It can be easily done with the help of Xposed framework. You just need to replace the single method inside one of android java classes (see the link from snihalani's answer). But of course to use Xposed your device must be rooted. The main idea can be expressed in the following code (using Xposed)
#Override
public void handleLoadPackage(LoadPackageParam lpparam) {
try {
Class<?> wifiP2pService = Class.forName("android.net.wifi.p2p.WifiP2pService", false, lpparam.classLoader);
for (Class<?> c : wifiP2pService.getDeclaredClasses()) {
//XposedBridge.log("inner class " + c.getSimpleName());
if ("P2pStateMachine".equals(c.getSimpleName())) {
XposedBridge.log("Class " + c.getName() + " found");
Method notifyInvitationReceived = c.getDeclaredMethod("notifyInvitationReceived");
final Method sendMessage = c.getMethod("sendMessage", int.class);
XposedBridge.hookMethod(notifyInvitationReceived, new XC_MethodReplacement() {
#Override
protected Object replaceHookedMethod(MethodHookParam param) throws Throwable {
final int PEER_CONNECTION_USER_ACCEPT = 0x00023000 + 2;
sendMessage.invoke(param.thisObject, PEER_CONNECTION_USER_ACCEPT);
return null;
}
});
break;
}
}
} catch (Throwable t) {
XposedBridge.log(t);
}
}
I tested it on SGS4 stock 4.2.2 ROM and it worked.
I guess the same could be done with the help of Substrate for android.
From my current understanding of the API, You cannot really accept connections automatically without user's intervention. You can initiate a connection, that doesn't require user intervention. If both of your devices are mobile devices, you will have to accept connection request on one end.
I have put this as a feature request in android project hosting.
You can monitor their response here: https://code.google.com/p/android/issues/detail?id=30880
Based on the comments, do you really need to connect to the devices if you just want to track and log the vehicles around you ?
I don't know the scope of the project, but you could simply use the WifiP2pDeviceList that you get when you request the peers in the WifiP2pManager. You could get the list of the devices (~= vehicles) around you and could log this.
Connection is useful if you want to send more detailed information I guess.
If you can modify the framework, you can ignore the accept window and direct send the "PEER_CONNECTION_USER_ACCEPT".
Base on Android 5.0, "frameworks/opt/net/wifi/service/java/com/android/server/wifi/p2p/WifiP2pServiceImpl.java".
You must find the "notifyInvitationReceived", and modify to ...
private void notifyInvitationReceived() {
/*Direct sends the accept message.*/
sendMessage(PEER_CONNECTION_USER_ACCEPT);
/*
... old code
*/
}