add cross device stuff (screenshots) in readme

This commit is contained in:
Kavish Devar
2025-01-31 03:37:02 +05:30
parent de53e840ed
commit c7ef31cba6
9 changed files with 80 additions and 42 deletions

View File

@@ -13,8 +13,28 @@ Other devices might work too. Features like ear detection and battery should be
Check the [pinned issue](https://github.com/kavishdevar/aln/issues/20) for a list.
## Linux — Deprecated, awaiting a rewrite!
ANY ISSUES ABOUT THE LINUX VERSION WILL BE CLOSED.
## CrossDevice Stuff
> [!IMPORTANT]
> This feature is still in development and might not work as expected. No support is provided for this feature.
### Features
- **Battery Status**: Get battery status on your Android device when you connect your AirPods to your Linux device.
- **Control AirPods**: Control your AirPods from your Android device when connected to your Linux device, like changing the noise control mode, toggling conversational awareness, and more.
- **Automatic Device Switching**: Automatically switch between your Linux and Android device when you connect your AirPods to one of them.
> [!NOTE]
> All this currently works only one way, Linux to Android, i.e. if you connect your AirPods to your Linux device, the features mentioned. The Android app can automaticaly connect to your AirPods when *receiving calls* or *starting media playback*.
| | | |
|-------------------|-------------------|-------------------|
| Connected to Linux, all features of the app available (setting Noise Control mode, battery status, etc.). ![Connected Remotely](/android/imgs/cd-connected-remotely-island.png) | Call received or media started playing; phone took over audio and disconnected AirPods from linux. ![Moved to Phone](/android/imgs/cd-moved-to-phone-island.png) | |
## Linux — Deprecated, rewrite WIP!
> No support will be provided for the old version of the Linux app. The new version is still in development and might not work as expected. No support is provided for the new version either.
Check out the README file in [linux](/linux) folder for more info.
This tray app communicates with a daemon with the help of a UNIX socket. The daemon is responsible for the actual communication with the AirPods. The tray app is just a frontend for the daemon, that does ear-detection, conversational awareness, setting the noise-cancellation mode, and more.
@@ -39,6 +59,7 @@ https://github.com/user-attachments/assets/eb7eebc2-fecf-410d-a363-0a5fd3a7af30
| ![Settings 1](/android/imgs/settings-1.png) | ![Settings 2](/android/imgs/settings-2.png) | ![Debug Screen](/android/imgs/debug.png) |
| ![Battery Notification](/android/imgs/notification.png) | ![Popup](/android/imgs/popup.png) | ![QuickSetting Tile](/android/imgs/qstile.png) |
| ![Long Press Configuration](/android/imgs/long-press.png) | ![Widget](/android/imgs/widget.png) | ![Customizations](/android/imgs/customizations.png) |
| ![audio-popup](/android/imgs/audio-connected-island.png) | | |
### Installation

View File

@@ -797,6 +797,8 @@ class AirPodsService : Service() {
Log.d("AirPodsService", "Taking over audio")
CrossDevice.sendRemotePacket(CrossDevicePackets.REQUEST_DISCONNECT.packet)
Log.d("AirPodsService", macAddress)
CrossDevice.isAvailable = false
sharedPreferences.edit { putBoolean("CrossDeviceIsAvailable", false) }
device = getSystemService<BluetoothManager>(BluetoothManager::class.java).adapter.bondedDevices.find {
it.address == macAddress
}
@@ -823,9 +825,7 @@ class AirPodsService : Service() {
0x1001,
uuid
) as BluetoothSocket
} catch (
e: Exception
) {
} catch (e: Exception) {
e.printStackTrace()
try {
socket = HiddenApiBypass.newInstance(
@@ -838,9 +838,7 @@ class AirPodsService : Service() {
0x1001,
uuid
) as BluetoothSocket
} catch (
e: Exception
) {
} catch (e: Exception) {
e.printStackTrace()
}
}
@@ -850,10 +848,6 @@ class AirPodsService : Service() {
this@AirPodsService.device = device
isConnectedLocally = true
socket.let { it ->
// sometimes doesn't work ;-;
// i though i move it to the coroutine
// but, the socket sometimes disconnects if i don't send a packet outside of the routine first
// so, sending *again*, with a delay, in the coroutine
it.outputStream.write(Enums.HANDSHAKE.value)
it.outputStream.flush()
it.outputStream.write(Enums.SET_SPECIFIC_FEATURES.value)
@@ -861,7 +855,6 @@ class AirPodsService : Service() {
it.outputStream.write(Enums.REQUEST_NOTIFICATIONS.value)
it.outputStream.flush()
CoroutineScope(Dispatchers.IO).launch {
// this is so stupid, why does it disconnect if i don't send a packet outside of the coroutine first
it.outputStream.write(Enums.HANDSHAKE.value)
it.outputStream.flush()
delay(200)
@@ -871,7 +864,6 @@ class AirPodsService : Service() {
it.outputStream.write(Enums.REQUEST_NOTIFICATIONS.value)
it.outputStream.flush()
delay(200)
// just in case this doesn't work, send all three after 5 seconds again
Handler(Looper.getMainLooper()).postDelayed({
it.outputStream.write(Enums.HANDSHAKE.value)
it.outputStream.flush()
@@ -898,6 +890,11 @@ class AirPodsService : Service() {
val bytes = buffer.copyOfRange(0, bytesRead)
val formattedHex = bytes.joinToString(" ") { "%02X".format(it) }
CrossDevice.sendReceivedPacket(bytes)
updateNotificationContent(
true,
sharedPreferences.getString("name", device.name),
batteryNotification.getBattery()
)
Log.d("AirPods Data", "Data received: $formattedHex")
} else if (bytesRead == -1) {
Log.d("AirPods Service", "Socket closed (bytesRead = -1)")

View File

@@ -84,6 +84,7 @@ object CrossDevice {
@SuppressLint("MissingPermission")
private fun startServer() {
CoroutineScope(Dispatchers.IO).launch {
if (!bluetoothAdapter.isEnabled) return@launch
serverSocket = bluetoothAdapter.listenUsingRfcommWithServiceRecord("ALNCrossDevice", uuid)
Log.d("CrossDevice", "Server started")
while (serverSocket != null) {
@@ -237,4 +238,4 @@ object CrossDevice {
logPacket(byteArray, "Sent")
Log.d("CrossDevice", "Sent packet to remote device")
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

View File

@@ -1,26 +0,0 @@
# AirPods like Normal (ALN) - Bringing Apple-only features to Linux and Android for seamless AirPods functionality!
#
# Copyright (C) 2024 Kavish Devar
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
import bluetooth
address="28:2D:7F:C2:05:5B"
try:
sock = bluetooth.BluetoothSocket(bluetooth.L2CAP)
sock.connect((address, 0x1001))
sock.send(b"\x00\x00\x04\x00\x01\x00\x02\x00\x00\x00\x00\x00\x00\x00\x00\x00")
except bluetooth.btcommon.BluetoothError as e:
print(f"Error: {e}")

View File

@@ -51,7 +51,17 @@ public:
trayIcon = new QSystemTrayIcon(QIcon(":/icons/airpods.png"));
trayMenu = new QMenu();
// Initialize conversational awareness state
bool caState = loadConversationalAwarenessState();
QAction *caToggleAction = new QAction("Toggle Conversational Awareness", trayMenu);
caToggleAction->setCheckable(true);
caToggleAction->setChecked(caState);
connect(caToggleAction, &QAction::triggered, this, [this, caToggleAction]() {
bool newState = !caToggleAction->isChecked();
setConversationalAwareness(newState);
saveConversationalAwarenessState(newState);
caToggleAction->setChecked(newState);
});
trayMenu->addAction(caToggleAction);
QAction *offAction = new QAction("Off", trayMenu);
@@ -120,6 +130,9 @@ public:
} else {
LOG_WARN("Service record not found, waiting for BLE broadcast");
}
// Set up device listening
listenForDeviceConnections();
}
public slots:
@@ -203,7 +216,7 @@ public slots:
}
void updateBatteryTooltip(const QString &status) {
trayIcon->setToolTip(status);
trayIcon->setToolTip("Battery Status: " + status);
}
void updateTrayIcon(const QString &status) {
@@ -600,6 +613,38 @@ public slots:
QMetaObject::invokeMethod(this, "handlePhonePacket", Qt::QueuedConnection, Q_ARG(QByteArray, data));
}
void listenForDeviceConnections() {
QDBusConnection systemBus = QDBusConnection::systemBus();
systemBus.connect(QString(), QString(), "org.freedesktop.DBus.Properties", "PropertiesChanged", this, SLOT(onDevicePropertiesChanged(QString, QVariantMap, QStringList)));
systemBus.connect(QString(), QString(), "org.freedesktop.DBus.ObjectManager", "InterfacesAdded", this, SLOT(onInterfacesAdded(QString, QVariantMap)));
}
void onDevicePropertiesChanged(QString interface, QVariantMap changed, QStringList invalidated) {
if (changed.contains("Connected") && changed["Connected"].toBool()) {
QString path = interface.split("/").last();
QString addr = path.replace("_", ":").replace("dev:", "");
QBluetoothAddress btAddress(addr);
QBluetoothDeviceInfo device(btAddress, "", 0);
if (device.serviceUuids().contains(QBluetoothUuid("74ec2172-0bad-4d01-8f77-997b2be0722a"))) {
connectToDevice(device);
}
}
}
void onInterfacesAdded(QString path, QVariantMap interfaces) {
if (interfaces.contains("org.bluez.Device1")) {
QVariantMap deviceProps = interfaces["org.bluez.Device1"].toMap();
if (deviceProps.contains("Connected") && deviceProps["Connected"].toBool()) {
QString addr = deviceProps["Address"].toString();
QBluetoothAddress btAddress(addr);
QBluetoothDeviceInfo device(btAddress, "", 0);
if (device.serviceUuids().contains(QBluetoothUuid("74ec2172-0bad-4d01-8f77-997b2be0722a"))) {
connectToDevice(device);
}
}
}
}
public: void followMediaChanges() {
QProcess *playerctlProcess = new QProcess(this);
connect(playerctlProcess, &QProcess::readyReadStandardOutput, this, [this, playerctlProcess]() {