mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-01-28 22:01:50 +00:00
add cross device stuff (screenshots) in readme
This commit is contained in:
25
README.md
25
README.md
@@ -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.).  | Call received or media started playing; phone took over audio and disconnected AirPods from linux.  | |
|
||||
|
||||
## 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
|
||||
|  |  |  |
|
||||
|  |  |  |
|
||||
|  |  |  |
|
||||
|  | | |
|
||||
|
||||
### Installation
|
||||
|
||||
|
||||
@@ -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)")
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
android/imgs/audio-connected-island.png
Normal file
BIN
android/imgs/audio-connected-island.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
BIN
android/imgs/cd-connected-remotely-island.png
Normal file
BIN
android/imgs/cd-connected-remotely-island.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
BIN
android/imgs/cd-moved-to-phone-island.png
Normal file
BIN
android/imgs/cd-moved-to-phone-island.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 70 KiB |
Binary file not shown.
@@ -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}")
|
||||
@@ -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]() {
|
||||
|
||||
Reference in New Issue
Block a user