adolfintel / OpenPods

The Free and Open Source app for monitoring your AirPods on Android
https://fdossena.com/?p=openPods/index.frag
GNU General Public License v3.0
969 stars 164 forks source link

Ideas for new features #58

Open altShiftDev opened 4 years ago

altShiftDev commented 4 years ago

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...

  1. Overlayed battery indicator on case open
  2. In your always-on notification battery indicator add a toggle for airpod pro features (Noise Cancellation, Transparency mode and Off). I know you can long press on the Airpod Pros to toggle between NC and Transparency mode but just OFF seems to be an impossible state to gain access to without software help, here's how it's shown to iOS users.
  3. Rebinding of multi-clicks. I've seen other apps that allow you to rebind what happens when you click once/twice/three times, etc...
  4. Ear Detection: When you take out either Airpod, any music/media playing on the device will pause and restart when you put the Airpod back in.
Domi04151309 commented 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.

altShiftDev commented 4 years ago

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 .

adolfintel commented 4 years ago

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.

zombzo commented 4 years ago

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

altShiftDev commented 4 years ago

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).

gji commented 4 years ago

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.

Username123456765645 commented 4 years ago

Quick question Is there anyway I can use OpenPods without GPS location sorry for being a pain

adolfintel commented 4 years ago

@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.

steam3d commented 4 years ago

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.

Electric1447 commented 3 years ago

I managed to reverse engineer the ear detection and. Will commit when I finish to implement it

adolfintel commented 3 years ago

@Electric1447 well done, congratulations! :tada:

azsde commented 3 years ago

@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 commented 3 years ago

@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

schweppes-0x commented 3 years ago

Is there any progress made in the last 1 year ? Im curious as I'm willing to contribute.

adolfintel commented 3 years ago

@schweppes-0x No, but contributions are welcome

schweppes-0x commented 3 years ago

Why has that stopped. It's a project full of potential..

adolfintel commented 3 years ago

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.

steam3d commented 2 years ago

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)

bho3538 commented 5 months ago

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?

steam3d commented 5 months ago

@bho3538 No. I already make this for Steam Deck couple month ago, but for Android I did not find a way

bho3538 commented 5 months ago

@steam3d that's too bad...

steam3d commented 5 months ago

@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.

grishka commented 1 month ago

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.

bho3538 commented 1 month ago

@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.

grishka commented 1 month ago

@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.

bho3538 commented 1 month ago

@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.

photo

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.

grishka commented 1 month ago

@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

bho3538 commented 1 month ago

@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.

grishka commented 1 month ago

Maybe we should ask Apple to fix it in their firmware 😅

steam3d commented 1 month ago

@grishka The companies that restrict the availability of features are the problem.

grishka commented 1 month ago

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.

steam3d commented 1 month ago

@bho3538 How did you connect from Android?

bho3538 commented 1 month ago

@steam3d Does this mean connecting an Android phone to a MacBook or AirPods connected to an Android phone?

kavishdevar commented 1 week ago

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.

grishka commented 1 week ago

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?

kavishdevar commented 1 week ago

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.

grishka commented 1 week ago

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

steam3d commented 1 week ago

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?

kavishdevar commented 1 week ago

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.

steam3d commented 1 week ago

@kavishdevar Each bluetooth classic profile can only have 1 connection, right?

kavishdevar commented 1 week ago

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?

steam3d commented 1 week ago

@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.

AirPods Pro

AirPods 2

AirPods Max

AirPods 3

Beats Fit Pro

AirPods Pro 2

kavishdevar commented 1 week ago

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
vulpes2 commented 1 week ago

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.

steam3d commented 1 week ago

@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.

kavishdevar commented 1 week ago

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

steam3d commented 1 week ago

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.

vulpes2 commented 1 week ago

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.

steam3d commented 1 week ago

@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.

steam3d commented 6 days ago

@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

Screenshot_2024-09-25-181344