Open altShiftDev opened 4 years ago
This is possible but a lot of work because a lot of reverse engineering is required. You would have to analyze the communication between iPhones and AirPods and find out which messages are used for that.
I don't have an iPhone but I suspect it would be easier to analyze the communications between these other Android apps and the Airpods to find out how they've done it.
If you can tell me how to capture and log these comms I'll bite the bullet and buy a few of these apps.
I suspect the case opening one would be easier though as simply opening the case should transmit a signal that isn't normally sent and can be isolated with repeated openings.
Once that signal is found, the data the overlay is showing is just battery and charge status which you already have access to so not too much more complexity beyond finding out how to make an android app overlay globally over other apps.
On Sat, Feb 1, 2020, 1:43 PM Dominik notifications@github.com wrote:
This is possible but a lot of work because a lot of reverse engineering is required. You would have to analyze the communication between iPhones and AirPods and find out which messages are used for that.
— You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub https://github.com/adolfintel/OpenPods/issues/58?email_source=notifications&email_token=AF6SYTD7M4VOF7HIHYWOXADRAW7EVA5CNFSM4KOUWHIKYY3PNVWWK3TUL52HS4DFVREXG43VMVBW63LNMVXHJKTDN5WW2ZLOORPWSZGOEKRD2YQ#issuecomment-581057890, or unsubscribe https://github.com/notifications/unsubscribe-auth/AF6SYTDQOROCERTY4QMA5YLRAW7EVANCNFSM4KOUWHIA .
As @Domi04151309 said, all this is possible but requires extensive reverse engineering. I don't have the skills required to do it, but I'll leave this issue open in case someone wants to work on the project.
Toggling between the different states seems to be sent in an l2cap packet. Using PacketLogger and this github project (AirControl) on macOS. The payload that it sends is in the form 04 00 04 00 09 00 0D XX 00 00 00 where XX can be 01 for off, 02 for ANC, and 03 for transparency
Since this issue is now being used to track new feature requests I updated my original comment with a to include "Ear Detection".
It seems to be rather simple in how it works, when you take out either earbud an l2cap packet will be sent by the Airpod at which point an iPhone or OSX daemon would issue a pause media command to the OS. The opposite happens when you put the removed Airpod back in.
Take out Airpod: [ 04 00 04 00 06 00 00 01 ]
(Length: 0x0008 (08))
Put in Airpod: [ 04 00 04 00 06 00 00 00 ]
(Length: 0x0008 (08))
I can also confirm @emeyer1 findings for the mode toggling (xCode's PacketLogger is very useful for this kind of thing).
I looked into this a little and it seems like though the actual signalling is straightforward, the l2cap communication happens across a dynamically-allocated channel. Looking at the bluetooth logs, the host sends a l2cap echo with a message body that includes the host channel id on bytes 6 and 7. The airpods respond with a message with the device channel id on bytes 4 and 5.
There's an api call(sendL2CAPEchoRequest) on iOS to do this, but not on Android, so I'm not sure it's possible at the moment to negotiate the channel id.
Quick question Is there anyway I can use OpenPods without GPS location sorry for being a pain
@Username123456765645 unfortunately no. Airpods send their status via bluetooth low energy (BLE), Android requires apps to have location permission for that.
If you're worried about privacy, don't be, the app has no internet access and you can read the source code if you don't trust me.
AirPods Pro and AirPods Gen2 has the AAP server. Does anyone know what is it? Apple has Apple Accessory Protocol for their iPods family but i am not sure that apple use it for airpods.
I managed to reverse engineer the ear detection and. Will commit when I finish to implement it
@Electric1447 well done, congratulations! :tada:
@Electric1447 I've looked into ear detection based on your commits, it seems we can only do ear detection when a beacon is sent to us, and it can take a long time, correct ?
@Electric1447 I've looked into ear detection based on your commits, it seems we can only do ear detection when a beacon is sent to us, and it can take a long time, correct ?
Yes
Is there any progress made in the last 1 year ? Im curious as I'm willing to contribute.
@schweppes-0x No, but contributions are welcome
Why has that stopped. It's a project full of potential..
Why has that stopped. It's a project full of potential..
There is no reason, reverse engineering is hard and I no longer have my airpods so someone else will have to do it.
Denis was able to get the data using wire https://twitter.com/ttdennis/status/1249975453219258374?s=20
But I have no idea on what protocol the iPhone and AirPods communicate using Bluetooth. I tried to get this data from Linux machine where i have access to each layer of Bluetooth stack
Take out Airpod: [ 04 00 04 00 06 00 00 01 ] (Length: 0x0008 (08)) Put in Airpod: [ 04 00 04 00 06 00 00 00 ] (Length: 0x0008 (08))
But i got nothing.
Now there are fake AirPods pro on the market that work like original ones in everything except BLE, they send wrong BLE packets (not the same as original), but iPhone still recognize battery level right.
Maybe the Chinese Internet has some more detailed information.
If anyone has experience with Bluetooth stack on mac, I will be glad for your help. I have no experience with mac at all, I have no idea where to start. (Except for AirControl which was mentioned above)
I found a way to check the exact amount of battery and control ANC in AirPods and implemented it in Windows. (https://github.com/bho3538/Airpods-Windows-Control)
It can work by sending a special packet at the Bluetooth L2CAP level to the connected AirPod (psm: 0x1001). It was implemented by referring to the article at https://github.com/tyalie/AAP-Protocol-Defintion and Mac OS PacketLogger
I wanted to add this feature and contribute to the OpenPods project, so I tried Bluetooth L2CAP connection on Android, but it failed. The API (createInsecureL2capChannel) officially provided by Google supports only BLE L2CAP communication and does not seem to support classic Bluetooth L2CAP. (I may be wrong as this is my first time developing Android)
Does anyone know how to do Bluetooth L2CAP communication on Android?
@bho3538 No. I already make this for Steam Deck couple month ago, but for Android I did not find a way
@steam3d that's too bad...
@bho3538 I am working on Windows. I researched AirPods Gen 2, AirPods 3, AirPods Pro, AirPods Pro 2 and AirPods Max and most of the features work on Linux and trying to port them on Windows.
The API (createInsecureL2capChannel) officially provided by Google supports only BLE L2CAP communication and does not seem to support classic Bluetooth L2CAP.
There's a hidden method createL2capSocket()
on BluetoothDevice: https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Bluetooth/framework/java/android/bluetooth/BluetoothDevice.java;drc=efb735f4d5a2f04550e33e8aa9485f906018fe4e;l=2840
I tried calling it, and it does return a socket, but when you try to connect()
it, it blocks indefinitely. I did it too many times and my phone decided to spontaneously reboot, lol.
There's also a service with UUID 74ec2172-0bad-4d01-8f77-997b2be0722a
that this app already knows about but only uses for making sure the device it's dealing with is actually AirPods. I feel like this service might also be related to Apple's control protocol, but I need to dig deeper into SDP. I wasn't able to connect to this service so far.
@grishka I tried L2CAP communication on Android using hidden api a few months ago. As you wrote, calling 'Connect' blocks forever.
Android's 'BluetoothSocket::Connect' function seems to read the channel value after connecting the socket. https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Bluetooth/framework/java/android/bluetooth/BluetoothSocket.java;l=474;drc=efb735f4d5a2f04550e33e8aa9485f906018fe4e?q
As a result of checking on the Windows platform, if you try to receive data from AirPods without first sending a valid message to AirPods, no results are returned.
To solve this problem (blocking due to read inside the Connect function), I directly created a socket using lower-level hidden APIs and immediately sent data to Airpods after connecting the socket. (Used https://github.com/LSPosed/AndroidHiddenApiBypass library)
Here is the code:
public void TestConnect(View view) throws ClassNotFoundException, IOException {
byte[] data = new byte[]{ 0x00,0x00,0x04,0x00,0x01,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00};
try{
LocalSocket sendSoc = CreateSocket();
sendSoc.getOutputStream().write(data); // not work
byte[] recv = new byte[128];
sendSoc.getInputStream().read(recv);
sendSoc.close();
}
catch(Exception e){
Log.d("","");
}
}
private LocalSocket CreateSocket() throws ClassNotFoundException, ErrnoException {
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
Object bluetoothService = HiddenApiBypass.invoke(adapter.getClass(), adapter, "getBluetoothService");
if(bluetoothService != null){
Object socketManager = HiddenApiBypass.invoke(Class.forName("android.bluetooth.IBluetooth$Stub$Proxy"), bluetoothService,"getSocketManager");
if(socketManager != null){
int flags = 1 << 2;
ParcelFileDescriptor parceDes = (ParcelFileDescriptor)HiddenApiBypass.invoke(Class.forName("android.bluetooth.IBluetoothSocketManager$Stub$Proxy"), socketManager,"connectSocket", adapter.getRemoteDevice("<Airpods Mac Addr>"), 3, null, 0x1001, flags);
if(parceDes != null){
FileDescriptor des = parceDes.getFileDescriptor();
if(des != null){
LocalSocket soc = (LocalSocket) HiddenApiBypass.newInstance(LocalSocket.class, des);
if(soc != null){
return soc;
}
}
}
}
}
return null;
}
However, it didn't work and the following message was output in logcat: 'L2CAP - Peer does not support our desired channel types' (I think it's this part of the source code: https://cs.android.com/android/platform/superproject/+/android14-qpr3-release:packages/modules/Bluetooth/system/stack/l2cap/l2c_fcr.cc;l=1607?q=l2c_fcr.c)
The strange thing is that when I tried l2cap communication after pairing a MacBook and an Android phone (not AirPods), the connection was successful and error log was not displayed.
@bho3538 Your example doesn't go that far for me (wanted to see what difference the flags make, if any). I get this exception that strongly suggests that it wants a UUID:
java.lang.reflect.InvocationTargetException
at java.lang.reflect.Method.invoke(Native Method)
at org.lsposed.hiddenapibypass.HiddenApiBypass.invoke(Unknown Source:102)
at me.grishka.bluetoothtest.MainActivity.createSocket(MainActivity.java:152)
at me.grishka.bluetoothtest.MainActivity.lambda$connectDirectly$4(MainActivity.java:174)
at me.grishka.bluetoothtest.MainActivity.$r8$lambda$-g_HdkOptQquShamwJPdJCH5sX0(Unknown Source:0)
at me.grishka.bluetoothtest.MainActivity$$ExternalSyntheticLambda4.run(D8$$SyntheticClass:0)
at java.lang.Thread.run(Thread.java:1012)
Caused by: java.lang.NullPointerException: Attempt to invoke virtual method 'java.util.UUID android.os.ParcelUuid.getUuid()' on a null object reference
at android.os.Parcel.createExceptionOrNull(Parcel.java:3017)
at android.os.Parcel.createException(Parcel.java:2995)
at android.os.Parcel.readException(Parcel.java:2978)
at android.os.Parcel.readException(Parcel.java:2920)
at android.bluetooth.IBluetoothSocketManager$Stub$Proxy.connectSocket(IBluetoothSocketManager.java:154)
at java.lang.reflect.Method.invoke(Native Method)
at org.lsposed.hiddenapibypass.HiddenApiBypass.invoke(Unknown Source:102)
at me.grishka.bluetoothtest.MainActivity.createSocket(MainActivity.java:152)
at me.grishka.bluetoothtest.MainActivity.lambda$connectDirectly$4(MainActivity.java:174)
at me.grishka.bluetoothtest.MainActivity.$r8$lambda$-g_HdkOptQquShamwJPdJCH5sX0(Unknown Source:0)
at me.grishka.bluetoothtest.MainActivity$$ExternalSyntheticLambda4.run(D8$$SyntheticClass:0)
at java.lang.Thread.run(Thread.java:1012)
However, if I specify the UUID 74ec2172-0bad-4d01-8f77-997b2be0722a
, no exceptions are thrown. I can write data and it appears to work, but I'm like 99% sure the write isn't going anywhere. Here's my code:
private void connectDirectly(){
if(device==null){
Toast.makeText(this, "No device", Toast.LENGTH_SHORT).show();
return;
}
new Thread(()->{
Log.i(TAG, "Creating socket");
try(LocalSocket socket=createSocket()){
Log.i(TAG, "Socket created");
byte[] data = new byte[]{ 0x00,0x00,0x04,0x00,0x01,0x00,0x02,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00};
socket.getOutputStream().write(data);
Log.i(TAG, "Data written");
byte[] buf=new byte[128];
int numRead=socket.getInputStream().read(buf);
Log.i(TAG, "Read "+numRead+" bytes: "+Arrays.toString(buf));
}catch(Exception x){
Log.w(TAG, x);
}
}).start();
}
private LocalSocket createSocket() throws Exception{
BluetoothAdapter adapter = BluetoothAdapter.getDefaultAdapter();
Object bluetoothService = HiddenApiBypass.invoke(adapter.getClass(), adapter, "getBluetoothService");
if(bluetoothService != null){
Object socketManager = HiddenApiBypass.invoke(Class.forName("android.bluetooth.IBluetooth$Stub$Proxy"), bluetoothService,"getSocketManager");
if(socketManager != null){
int flags = 1 << 2;
Log.i(TAG, "Connecting socket");
ParcelFileDescriptor parceDes = (ParcelFileDescriptor)HiddenApiBypass.invoke(Class.forName("android.bluetooth.IBluetoothSocketManager$Stub$Proxy"), socketManager,"connectSocket", device, 3, ParcelUuid.fromString("74ec2172-0bad-4d01-8f77-997b2be0722a"), 0x1001, flags);
if(parceDes != null){
FileDescriptor des = parceDes.getFileDescriptor();
if(des != null){
LocalSocket soc = (LocalSocket) HiddenApiBypass.newInstance(LocalSocket.class, des);
if(soc != null){
return soc;
}
}
}
}
}
return null;
}
Logcat output (notice that same "desired channel types" thing):
2024-08-11 02:13:40.793 16483-16525 MainActivity me.grishka.bluetoothtest I Creating socket
2024-08-11 02:13:40.797 16483-16525 MainActivity me.grishka.bluetoothtest I Connecting socket
2024-08-11 02:13:40.802 3126-3617 bt_btif_sock com.google.android.bluetooth I packages/modules/Bluetooth/system/btif/src/btif_sock.cc:306 btsock_connect: btsock_connect
2024-08-11 02:13:40.802 3126-3617 bt_btif_sock com.google.android.bluetooth I packages/modules/Bluetooth/system/btif/src/btif_sock.cc:155 btif_sock_connection_logger: address=xx:xx:xx:xx:aa:b5, role=2, state=2
2024-08-11 02:13:40.802 3126-3617 bluetooth com.google.android.bluetooth I packages/modules/Bluetooth/system/btif/src/btif_sock_l2cap.cc:325 btsock_l2cap_alloc_l: Allocated l2cap socket structure socket_id:13
2024-08-11 02:13:40.802 3126-3606 bt_l2cap com.google.android.bluetooth I packages/modules/Bluetooth/system/stack/l2cap/l2c_api.cc:168 L2CA_Register: L2CAP Registered service classic PSM: 0x1001
2024-08-11 02:13:40.803 3126-3606 bt_btm_pm com.google.android.bluetooth I packages/modules/Bluetooth/system/stack/acl/btm_pm.cc:284 BTM_SetPowerMode: Setting power mode for peer:xx:xx:xx:xx:aa:b5 current_mode:immediate:sniff[2] new_mode:forced:active[0]
2024-08-11 02:13:40.803 3126-3606 bt_btm_pm com.google.android.bluetooth I packages/modules/Bluetooth/system/stack/acl/btm_pm.cc:590 btm_pm_snd_md_req: Switching from immediate:sniff[0x02] to immediate:active[0x00]
2024-08-11 02:13:40.803 3126-3606 l2c_csm com.google.android.bluetooth W packages/modules/Bluetooth/system/stack/l2cap/l2c_csm.cc:243 l2c_csm_closed: Unable to set link policy active
2024-08-11 02:13:40.803 3126-3606 bt_l2cap com.google.android.bluetooth W packages/modules/Bluetooth/system/main/bte_logmsg.cc:194 LogMsg: L2CAP - Peer does not support our desired channel types
2024-08-11 02:13:40.804 16483-16525 MainActivity me.grishka.bluetoothtest I Socket created
2024-08-11 02:13:40.804 16483-16525 MainActivity me.grishka.bluetoothtest I Data written
So back to my initial idea about testing flags, setting BTSOCK_FLAG_AUTH
or BTSOCK_FLAG_ENCRYPT
(in addition to BTSOCK_FLAG_NO_SDP
you already set) yields an exception:
2024-08-11 02:20:24.225 17624-17661 MainActivity me.grishka.bluetoothtest I Connecting socket
2024-08-11 02:20:24.227 3126-22303 bt_btif_sock com.google.android.bluetooth I packages/modules/Bluetooth/system/btif/src/btif_sock.cc:306 btsock_connect: btsock_connect
2024-08-11 02:20:24.227 3126-22303 bt_btif_sock com.google.android.bluetooth I packages/modules/Bluetooth/system/btif/src/btif_sock.cc:155 btif_sock_connection_logger: address=xx:xx:xx:xx:aa:b5, role=2, state=2
2024-08-11 02:20:24.227 3126-22303 bluetooth com.google.android.bluetooth I packages/modules/Bluetooth/system/btif/src/btif_sock_l2cap.cc:325 btsock_l2cap_alloc_l: Allocated l2cap socket structure socket_id:16
2024-08-11 02:20:24.227 3126-3606 bt_l2cap com.google.android.bluetooth W packages/modules/Bluetooth/system/stack/l2cap/l2c_api.cc:163 L2CA_Register: L2CAP - no RCB available, PSM: 0x1001 vPSM: 0x1008
2024-08-11 02:20:24.227 3126-3606 bt_stack com.google.android.bluetooth E [ERROR:gap_conn.cc(248)] GAP_ConnOpen: Failure registering PSM 0x1001
2024-08-11 02:20:24.227 3126-3606 bt_l2cap com.google.android.bluetooth W packages/modules/Bluetooth/system/main/bte_logmsg.cc:194 LogMsg: L2CAP - PSM: 0x0000 not found for deregistration
2024-08-11 02:20:24.227 3126-3606 bluetooth com.google.android.bluetooth E packages/modules/Bluetooth/system/btif/src/btif_sock_l2cap.cc:430 on_cl_l2cap_init: Initialization status failed socket_id:16
2024-08-11 02:20:24.227 3126-3606 bt_btif_sock com.google.android.bluetooth I packages/modules/Bluetooth/system/btif/src/btif_sock.cc:155 btif_sock_connection_logger: address=xx:xx:xx:xx:aa:b5, role=5, state=2
2024-08-11 02:20:24.227 3126-3606 bluetooth com.google.android.bluetooth I packages/modules/Bluetooth/system/btif/src/btif_sock_l2cap.cc:234 btsock_l2cap_free_l: Application has already closed l2cap socket socket_id:16
2024-08-11 02:20:24.228 3126-3606 bt_btif_sock com.google.android.bluetooth I packages/modules/Bluetooth/system/btif/src/btif_sock.cc:155 btif_sock_connection_logger: address=00:00:00:00:00:00, role=4, state=1
2024-08-11 02:20:24.228 3126-3606 bt_btif_sock com.google.android.bluetooth I packages/modules/Bluetooth/system/btif/src/btif_sock.cc:155 btif_sock_connection_logger: address=00:00:00:00:00:00, role=5, state=1
2024-08-11 02:20:24.228 3126-3606 bluetooth com.google.android.bluetooth I packages/modules/Bluetooth/system/btif/src/btif_sock_l2cap.cc:234 btsock_l2cap_free_l: Application has already closed l2cap socket socket_id:15
2024-08-11 02:20:24.228 17624-17661 MainActivity me.grishka.bluetoothtest I Socket created
2024-08-11 02:20:24.228 3126-17489 ObexServerSockets6 com.google.android.bluetooth W Accept exception for ServerSocket: Type: TYPE_L2CAP Channel: 4097
java.io.IOException: read failed, socket might closed or timeout, read ret: -1
at android.bluetooth.BluetoothSocket.readAll(BluetoothSocket.java:816)
at android.bluetooth.BluetoothSocket.waitSocketSignal(BluetoothSocket.java:769)
at android.bluetooth.BluetoothSocket.accept(BluetoothSocket.java:544)
at android.bluetooth.BluetoothServerSocket.accept(BluetoothServerSocket.java:171)
at android.bluetooth.BluetoothServerSocket.accept(BluetoothServerSocket.java:157)
at com.android.bluetooth.ObexServerSockets$SocketAcceptThread.run(ObexServerSockets.java:321)
And finally, not specifying a PSM also yields an exception but a different one:
2024-08-11 02:26:07.859 18327-18416 MainActivity me.grishka.bluetoothtest I Creating socket
2024-08-11 02:26:07.863 18327-18416 MainActivity me.grishka.bluetoothtest I Connecting socket
2024-08-11 02:26:07.866 3126-6589 bt_btif_sock com.google.android.bluetooth I packages/modules/Bluetooth/system/btif/src/btif_sock.cc:306 btsock_connect: btsock_connect
2024-08-11 02:26:07.866 3126-6589 bt_btif_sock com.google.android.bluetooth I packages/modules/Bluetooth/system/btif/src/btif_sock.cc:155 btif_sock_connection_logger: address=xx:xx:xx:xx:aa:b5, role=2, state=2
2024-08-11 02:26:07.866 3126-6589 bluetooth com.google.android.bluetooth I packages/modules/Bluetooth/system/btif/src/btif_sock_l2cap.cc:325 btsock_l2cap_alloc_l: Allocated l2cap socket structure socket_id:23
2024-08-11 02:26:07.867 3126-3606 bluetooth com.google.android.bluetooth E packages/modules/Bluetooth/system/btif/src/btif_sock_l2cap.cc:430 on_cl_l2cap_init: Initialization status failed socket_id:23
2024-08-11 02:26:07.867 3126-3606 bt_btif_sock com.google.android.bluetooth I packages/modules/Bluetooth/system/btif/src/btif_sock.cc:155 btif_sock_connection_logger: address=xx:xx:xx:xx:aa:b5, role=5, state=2
2024-08-11 02:26:07.867 3126-3606 bluetooth com.google.android.bluetooth I packages/modules/Bluetooth/system/btif/src/btif_sock_l2cap.cc:234 btsock_l2cap_free_l: Application has already closed l2cap socket socket_id:23
2024-08-11 02:26:07.867 3126-3606 bt_btif_sock com.google.android.bluetooth I packages/modules/Bluetooth/system/btif/src/btif_sock.cc:155 btif_sock_connection_logger: address=00:00:00:00:00:00, role=4, state=1
2024-08-11 02:26:07.867 3126-3606 bt_btif_sock com.google.android.bluetooth I packages/modules/Bluetooth/system/btif/src/btif_sock.cc:155 btif_sock_connection_logger: address=00:00:00:00:00:00, role=5, state=1
2024-08-11 02:26:07.867 3126-3606 bluetooth com.google.android.bluetooth I packages/modules/Bluetooth/system/btif/src/btif_sock_l2cap.cc:234 btsock_l2cap_free_l: Application has already closed l2cap socket socket_id:22
2024-08-11 02:26:07.867 3126-18257 ObexServerSockets10 com.google.android.bluetooth W Accept exception for ServerSocket: Type: TYPE_L2CAP Channel: 4097
java.io.IOException: read failed, socket might closed or timeout, read ret: -1
at android.bluetooth.BluetoothSocket.readAll(BluetoothSocket.java:816)
at android.bluetooth.BluetoothSocket.waitSocketSignal(BluetoothSocket.java:769)
at android.bluetooth.BluetoothSocket.accept(BluetoothSocket.java:544)
at android.bluetooth.BluetoothServerSocket.accept(BluetoothServerSocket.java:171)
at android.bluetooth.BluetoothServerSocket.accept(BluetoothServerSocket.java:157)
at com.android.bluetooth.ObexServerSockets$SocketAcceptThread.run(ObexServerSockets.java:321)
2024-08-11 02:26:07.867 3126-18257 ObexServerSockets10 com.google.android.bluetooth D shutdown(block = false)
2024-08-11 02:26:07.867 3126-18257 BluetoothSocket com.google.android.bluetooth D close() this: XX:XX:XX:00:00:00, channel: 4, mSocketIS: android.net.LocalSocketImpl$SocketInputStream@667b0ac, mSocketOS: android.net.LocalSocketImpl$SocketOutputStream@302d175, mSocket: android.net.LocalSocket@983050a impl:android.net.LocalSocketImpl@80e617b fd:java.io.FileDescriptor@ff52b98, mSocketState: LISTENING
2024-08-11 02:26:07.867 3126-3608 bt_btif_sock com.google.android.bluetooth I packages/modules/Bluetooth/system/btif/src/btif_sock.cc:155 btif_sock_connection_logger: address=00:00:00:00:00:00, role=5, state=1
2024-08-11 02:26:07.867 3126-18256 ObexServerSockets10 com.google.android.bluetooth D AcceptThread ended for: ServerSocket: Type: TYPE_RFCOMM Channel: 4
2024-08-11 02:26:07.868 18327-18416 MainActivity me.grishka.bluetoothtest I Socket created
2024-08-11 02:26:07.868 3126-3606 bt_stack com.google.android.bluetooth I [INFO:port_api.cc(293)] RFCOMM_RemoveServer: handle=26
2024-08-11 02:26:07.868 3126-18257 ObexServerSockets10 com.google.android.bluetooth D shutdown called from another thread - interrupt().
2024-08-11 02:26:07.868 3126-18257 BluetoothSocket com.google.android.bluetooth D close() this: XX:XX:XX:00:00:00, channel: 4097, mSocketIS: android.net.LocalSocketImpl$SocketInputStream@e3c0ff1, mSocketOS: android.net.LocalSocketImpl$SocketOutputStream@1d467d6, mSocket: android.net.LocalSocket@4979a57 impl:android.net.LocalSocketImpl@bdd6944 fd:java.io.FileDescriptor@1f0ba2d, mSocketState: LISTENING
2024-08-11 02:26:07.868 3126-18257 ObexServerSockets10 com.google.android.bluetooth D onAcceptFailed() calling shutdown...
2024-08-11 02:26:07.868 3126-18257 BluetoothM...sInstance0 com.google.android.bluetooth E Failed to accept incomming connection - restarting
2024-08-11 02:26:07.868 3126-18257 ObexServerSockets com.google.android.bluetooth D create(rfcomm = -2, l2capPsm = -2)
2024-08-11 02:26:07.869 18327-18416 MainActivity me.grishka.bluetoothtest W java.io.IOException: Broken pipe
at android.net.LocalSocketImpl.writeba_native(Native Method)
at android.net.LocalSocketImpl.-$$Nest$mwriteba_native(Unknown Source:0)
at android.net.LocalSocketImpl$SocketOutputStream.write(LocalSocketImpl.java:144)
at android.net.LocalSocketImpl$SocketOutputStream.write(LocalSocketImpl.java:131)
at me.grishka.bluetoothtest.MainActivity.lambda$connectDirectly$4(MainActivity.java:162)
at me.grishka.bluetoothtest.MainActivity.$r8$lambda$-g_HdkOptQquShamwJPdJCH5sX0(Unknown Source:0)
at me.grishka.bluetoothtest.MainActivity$$ExternalSyntheticLambda4.run(D8$$SyntheticClass:0)
at java.lang.Thread.run(Thread.java:1012)
2024-08-11 02:26:07.871 3126-18257 bt_btif_sock com.google.android.bluetooth I packages/modules/Bluetooth/system/btif/src/btif_
I'm testing on a Pixel 4a running stock Android 13 and Airpods Pro 2.
@grishka UUID is required. I uploaded the wrong code while modifying the code during several tests.
This photo is Windows client and Android client communicating with the Macbook and L2cap in the past. (Macbook acts as a server, psm: 0x1003) I tested using psm 0x1003 because I couldn't connect to the Macbook using psm 0x1001.
Unlike the Windows client version, Android additionally transmits a packet called 'Information Request', and it seems that Airpods does not support this packet. (It seems to receive information about L2CAP - Peer does not support our desired channel types here.)
If you communicate with Airpods without going through the 'Information Request' request process in Android, the connect process will be successful. However, I don't know how to do this.
@bho3538 Now that's interesting. Here's the function that sends these information requests: https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Bluetooth/system/stack/l2cap/l2c_utils.cc;drc=69fe17309810b18f66f443265d9f723a0c9be600;l=1016 Here's where it's unfortunately unconditionally called presumably after the connection is established: https://cs.android.com/android/platform/superproject/main/+/main:packages/modules/Bluetooth/system/stack/l2cap/l2c_link.cc;drc=efb735f4d5a2f04550e33e8aa9485f906018fe4e;l=115
@grishka Unfortunately, I haven't been able to find a way to get around these restrictions. I checked the source code, but it seems impossible.
Maybe we should ask Apple to fix it in their firmware 😅
@grishka The companies that restrict the availability of features are the problem.
I feel like in this case they don't restrict it intentionally, they just never implemented the part of Bluetooth spec that Apple host devices never use.
It goes both ways — I made a Nearby Share app for macOS, and it turns out macOS doesn't allow you to set the "service data" field on BLE advertisements because reasons (Android listens for a BLE advertisement with a specific byte pattern in the service data to become visible over MDNS). Somehow, it just so happens that when it comes to adversarial interoperability, all Bluetooth implementations suck, in different ways. I don't know why. Somehow this doesn't happen to other industry-standard protocol suites.
@bho3538 How did you connect from Android?
@steam3d Does this mean connecting an Android phone to a MacBook or AirPods connected to an Android phone?
Finally got around this, by replacing the bluetooth stack itself. i compiled and replaced the library where the stack is implemented, (EDIT: with root, ofc). the easiest solution would ofc be to just comment out the line for these "desired channel" check and "send_peer_info_req" function. but that might break other apps. so the stack could check if the UUID is advevrtised and then decide whether or not to cal these. i guess the entire library can be used as is in a native c++ app, which could directly interact with the stack..? but for now, i got the connection established. implement basic ear detection and battery status, working on a quick tile for anc settings...
one thing i haven't been able to figure out is how conversation awareness works, on my mac the airpods send a packet when it's been activated, but not on other devices, so there's no way to lower the volume automatically.
by replacing the bluetooth stack itself
But that requires a rooted device, doesn't it? Or is it possible send raw HCI commands to the bluetooth adapter right from an unprivileged app?
yup, root.. sorry, forgot to mention that. now i'm trying to figure out if i can use that library for my own app only, instead of replacing the system's lib. it might be possible, if android allows for loading the whole stack again at an app level.
Considering that the normal bluetooth APIs go through an RPC boundary into a system daemon, I'd say it's unlikely. The daemon runs under a dedicated user too:
sunfish:/ $ ps -A | grep bluetooth
bluetooth 1065 1 10979712 2728 0 0 S android.hardware.bluetooth@1.0-service-qti
bluetooth 2773 1043 15390360 94840 0 0 S com.google.android.bluetooth
And there are devices owned by this user and only accessible to it (or its group) which look like one of them may be for those HCI commands:
sunfish:/ $ ls -l /dev | grep bluetooth
crw-rw---- 1 bluetooth net_bt 498, 0 1974-01-03 17:18 btpower
crw-rw---- 1 bluetooth bluetooth 235, 3 1974-01-03 17:18 smd7
crw-rw---- 1 bluetooth net_bt 510, 0 2024-09-23 16:56 ttyHS0
crw------- 1 bluetooth bluetooth 511, 0 1974-01-03 17:18 ttyMSM0
But a solution that requires root would still be better than the current situation.
Taking the subway with current AirPods apps that use BLE is always fun, they keep picking up other people's headphones :D
Taking the subway with current AirPods apps that use BLE is always fun, they keep picking up other people's headphones :D
I think it's possible to resolve the random BLE MAC address. As noted, the iPhone can detect the accurate battery level even when you are using AirPods on Windows right now. The main question is: what does Apple use to generate the random BLE MAC address, and where is the battery information stored? Is it inside the encrypted part of the BLE packet?
Has anyone figured out multi-device connections yet? I got 2 L2CAP connections established with 2 linux PCs at the same time, twice, but it was very random.
@kavishdevar Each bluetooth classic profile can only have 1 connection, right?
I am not quite sure about that, but I am sure that I had 2 machines connected to the airpods and receiving data, and able to send too!
also, can we have a different place for reverse engineering the AAP protocol, there's a lot more to it than just battery, in-ear, and setting ANC modes?
@kavishdevar AAP is only a control protocol. I checked all AirPods models. There are no extra settings like Handoff or Spatial Audio.
Here is the list of options that I have already figured out and implemented:
Battery, Anc and ear detection notifications work for all headphones.
Oh, that's a great list! I wonder how handoff works though.
Rename for AirPods Pro 2 is just this:
fun setNameByteArray(name: String): ByteArray {
val nameBytes = name.toByteArray()
val bytes = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x1a, 0x01,
nameBytes.size.toByte(), 0x00) + nameBytes
return bytes
}
val AFTER_SET_NAME = byteArrayOf(0x04, 0x00, 0x04, 0x00, 0x03, 0x00, 0xFF.toByte()) // not sure if this is necessary
I think it's possible to resolve the random BLE MAC address
Correct, you need the IRK which can be obtained during the bonding process. Obtaining this is very annoying with BlueZ because the bonding process happens over BR/EDR, not LE, and classic Bluetooth does not have RPA support. The easiest way to get it is by reading it from your macOS keychain.
By replacing the Bluetooth stack itself
You cannot replace Fluoride on an Android device, and running two Bluetooth stacks on a device simultaneously on one single controller is absolutely not supported anywhere. The Bluetooth stack retains exclusive control to the HCI socket, any unexpected events or ACL packets will definitely upset the stack.
I'm going to publish some documentation on AAP in a few weeks, it will have most of the framing information, opcodes and configuration definitions. We can collaborate on this if you don't mind waiting for a little while.
@vulpes2 I plan to rewrite MagicPodsCore in Python and include documentation for all of the above features. I'm already using it on Windows, but still haven't gotten around to updating MagicPodsCore for Linux.
You cannot replace Fluoride on an Android device
I replaced the libbluetooth_jni.so
.
I'm going to publish some documentation on AAP in a few weeks, it will have most of the framing information, opcodes and configuration definitions. We can collaborate on this if you don't mind waiting for a little while.
That'd be great!
I will try to implement all functions on my android app, and maybe publish the way to make L2CAP sockets work on android. My app doesn't require any root permissions.. Maybe someone could even PR to fix this. (Maybe something that prevents sending any data if the service uuid is present, or at least return with a successfully connected socjet even if no data is received for those initial packets.)
I plan to rewrite MagicPodsCore in Python and include documentation for all of the above features. I'm already using it on Windows, but still haven't gotten around to updating MagicPodsCore for Linux.
I have made a small python script for linux for renaming, setting ANC mode, I could convert it into a library like you have as the MagicPodsCore. Another way could be to make a kernel module that would manage the socket, if multiple different apps, like a command line tool, a UI app, etc..
Another idea: easy handoff (and reading/writing to airpods) between devices that all use a common app either through firebase or some other way locally.
See also: https://github.com/TheParasiteProject/packages_apps_BtHelper
I have made a small python script for linux for renaming, setting ANC mode, I could convert it into a library like you have as the MagicPodsCore
You can submit a pull request to MagicPodsCore when I add the documented SDK. I will let you know.
Would you consider licensing your Python based MagicPodsCore rewrite with a more permissive license such as GPLv3? The AGPL license is the main reason I haven't sent any pull requests, I can't work on something that I can't reuse for my other projects. It turns out Python has cross-platform BR/EDR support in stdlib, and I was able to set up a simple async IO demo with very little effort to send/receive data without blocking. For BLE (Nearby) we can use Bleak, it also has proper async support and is very well maintained.
@vulpes2 Yes, I am planning to change the license to GPLv3. It's a long story why the AGPL license came about. My idea was to make MagicPodsCore a backend so that everyone could write their frontend part. For example, I am developing MagicPodsDecky for Steam Deck, which uses MagicPodsCore as the backend.
@vulpes2 @kavishdevar I wrote some information about AAP and changed the license to GPLv3 as requested. Each file has a detailed description of the packet. We can move on to reverse engineering here https://github.com/steam3d/MagicPodsCore
I haven't yet figured out how to do asynchronous reads and writes to the socket using python.
Update:
I fixed reading and writing to the socket
Loving the app so far, thanks for your efforts.
Here's a small wish-list of features that I've seen on other play store apps so I'm fairly certain they're all possible although how difficult I can't say...