mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-03-07 06:46:07 +00:00
smth works :D
This commit is contained in:
@@ -4,7 +4,7 @@ project(linux VERSION 0.1 LANGUAGES CXX)
|
|||||||
|
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|
||||||
find_package(Qt6 6.5 REQUIRED COMPONENTS Quick Widgets Bluetooth)
|
find_package(Qt6 6.5 REQUIRED COMPONENTS Quick Widgets Bluetooth Multimedia DBus)
|
||||||
|
|
||||||
qt_standard_project_setup(REQUIRES 6.5)
|
qt_standard_project_setup(REQUIRES 6.5)
|
||||||
|
|
||||||
@@ -19,19 +19,8 @@ qt_add_qml_module(applinux
|
|||||||
Main.qml
|
Main.qml
|
||||||
)
|
)
|
||||||
|
|
||||||
# Qt for iOS sets MACOSX_BUNDLE_GUI_IDENTIFIER automatically since Qt 6.1.
|
|
||||||
# If you are developing for iOS or macOS you should consider setting an
|
|
||||||
# explicit, fixed bundle identifier manually though.
|
|
||||||
set_target_properties(applinux PROPERTIES
|
|
||||||
# MACOSX_BUNDLE_GUI_IDENTIFIER com.example.applinux
|
|
||||||
MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
|
|
||||||
MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
|
|
||||||
MACOSX_BUNDLE TRUE
|
|
||||||
WIN32_EXECUTABLE TRUE
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(applinux
|
target_link_libraries(applinux
|
||||||
PRIVATE Qt6::Quick Qt6::Widgets Qt6::Bluetooth
|
PRIVATE Qt6::Quick Qt6::Widgets Qt6::Bluetooth Qt6::Multimedia Qt6::DBus
|
||||||
)
|
)
|
||||||
|
|
||||||
include(GNUInstallDirs)
|
include(GNUInstallDirs)
|
||||||
|
|||||||
@@ -7,19 +7,24 @@ ApplicationWindow {
|
|||||||
height: 300
|
height: 300
|
||||||
title: "AirPods Settings"
|
title: "AirPods Settings"
|
||||||
property bool ignoreNoiseControlChange: false
|
property bool ignoreNoiseControlChange: false
|
||||||
|
property bool isPlaying: false
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
caToggle.checked = airPodsTrayApp.loadConversationalAwarenessState()
|
||||||
|
}
|
||||||
|
|
||||||
Column {
|
Column {
|
||||||
spacing: 20
|
spacing: 20
|
||||||
padding: 20
|
padding: 20
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "Ear Detection Status: "
|
text: "Battery Status: "
|
||||||
id: earDetectionStatus
|
id: batteryStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
Text {
|
Text {
|
||||||
text: "Battery Status: "
|
text: "Ear Detection Status: "
|
||||||
id: batteryStatus
|
id: earDetectionStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
ComboBox {
|
ComboBox {
|
||||||
@@ -33,7 +38,7 @@ ApplicationWindow {
|
|||||||
}
|
}
|
||||||
Connections {
|
Connections {
|
||||||
target: airPodsTrayApp
|
target: airPodsTrayApp
|
||||||
function onNoiseControlModeChanged(mode) {
|
onNoiseControlModeChanged: {
|
||||||
ignoreNoiseControlChange = true
|
ignoreNoiseControlChange = true
|
||||||
noiseControlMode.currentIndex = mode;
|
noiseControlMode.currentIndex = mode;
|
||||||
ignoreNoiseControlChange = false
|
ignoreNoiseControlChange = false
|
||||||
@@ -44,8 +49,10 @@ ApplicationWindow {
|
|||||||
Switch {
|
Switch {
|
||||||
id: caToggle
|
id: caToggle
|
||||||
text: "Conversational Awareness"
|
text: "Conversational Awareness"
|
||||||
|
checked: isPlaying
|
||||||
onCheckedChanged: {
|
onCheckedChanged: {
|
||||||
airPodsTrayApp.setConversationalAwareness(checked)
|
airPodsTrayApp.setConversationalAwareness(checked)
|
||||||
|
airPodsTrayApp.saveConversationalAwarenessState(checked)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
174
linux/main.cpp
174
linux/main.cpp
@@ -16,6 +16,14 @@
|
|||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPalette>
|
#include <QPalette>
|
||||||
|
#include <QDBusInterface>
|
||||||
|
#include <QDBusReply>
|
||||||
|
#include <QDBusConnectionInterface>
|
||||||
|
#include <QProcess>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QTextStream>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
|
||||||
Q_LOGGING_CATEGORY(airpodsApp, "airpodsApp")
|
Q_LOGGING_CATEGORY(airpodsApp, "airpodsApp")
|
||||||
|
|
||||||
@@ -65,6 +73,7 @@ public:
|
|||||||
connect(this, &AirPodsTrayApp::noiseControlModeChanged, this, &AirPodsTrayApp::updateNoiseControlMenu);
|
connect(this, &AirPodsTrayApp::noiseControlModeChanged, this, &AirPodsTrayApp::updateNoiseControlMenu);
|
||||||
connect(this, &AirPodsTrayApp::batteryStatusChanged, this, &AirPodsTrayApp::updateBatteryTooltip);
|
connect(this, &AirPodsTrayApp::batteryStatusChanged, this, &AirPodsTrayApp::updateBatteryTooltip);
|
||||||
connect(this, &AirPodsTrayApp::batteryStatusChanged, this, &AirPodsTrayApp::updateTrayIcon);
|
connect(this, &AirPodsTrayApp::batteryStatusChanged, this, &AirPodsTrayApp::updateTrayIcon);
|
||||||
|
connect(this, &AirPodsTrayApp::earDetectionStatusChanged, this, &AirPodsTrayApp::handleEarDetection);
|
||||||
|
|
||||||
trayIcon->setContextMenu(trayMenu);
|
trayIcon->setContextMenu(trayMenu);
|
||||||
trayIcon->show();
|
trayIcon->show();
|
||||||
@@ -77,7 +86,6 @@ public:
|
|||||||
discoveryAgent->start();
|
discoveryAgent->start();
|
||||||
LOG_INFO("AirPodsTrayApp initialized and started device discovery");
|
LOG_INFO("AirPodsTrayApp initialized and started device discovery");
|
||||||
|
|
||||||
// Check for already connected devices
|
|
||||||
QBluetoothLocalDevice localDevice;
|
QBluetoothLocalDevice localDevice;
|
||||||
connect(&localDevice, &QBluetoothLocalDevice::deviceConnected, this, &AirPodsTrayApp::onDeviceConnected);
|
connect(&localDevice, &QBluetoothLocalDevice::deviceConnected, this, &AirPodsTrayApp::onDeviceConnected);
|
||||||
connect(&localDevice, &QBluetoothLocalDevice::deviceDisconnected, this, &AirPodsTrayApp::onDeviceDisconnected);
|
connect(&localDevice, &QBluetoothLocalDevice::deviceDisconnected, this, &AirPodsTrayApp::onDeviceDisconnected);
|
||||||
@@ -90,6 +98,7 @@ public:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
initializeMprisInterface();
|
||||||
}
|
}
|
||||||
|
|
||||||
public slots:
|
public slots:
|
||||||
@@ -119,16 +128,16 @@ public slots:
|
|||||||
LOG_INFO("Setting noise control mode to: " << mode);
|
LOG_INFO("Setting noise control mode to: " << mode);
|
||||||
QByteArray packet;
|
QByteArray packet;
|
||||||
switch (mode) {
|
switch (mode) {
|
||||||
case 0: // Off
|
case 0:
|
||||||
packet = QByteArray::fromHex("0400040009000D01000000");
|
packet = QByteArray::fromHex("0400040009000D01000000");
|
||||||
break;
|
break;
|
||||||
case 1: // Noise Cancellation
|
case 1:
|
||||||
packet = QByteArray::fromHex("0400040009000D02000000");
|
packet = QByteArray::fromHex("0400040009000D02000000");
|
||||||
break;
|
break;
|
||||||
case 2: // Transparency
|
case 2:
|
||||||
packet = QByteArray::fromHex("0400040009000D03000000");
|
packet = QByteArray::fromHex("0400040009000D03000000");
|
||||||
break;
|
break;
|
||||||
case 3: // Adaptive
|
case 3:
|
||||||
packet = QByteArray::fromHex("0400040009000D04000000");
|
packet = QByteArray::fromHex("0400040009000D04000000");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -156,8 +165,19 @@ public slots:
|
|||||||
for (QAction *action : actions) {
|
for (QAction *action : actions) {
|
||||||
action->setChecked(false);
|
action->setChecked(false);
|
||||||
}
|
}
|
||||||
if (mode >= 0 && mode < actions.size()) {
|
switch (mode) {
|
||||||
actions[mode]->setChecked(true);
|
case 0:
|
||||||
|
actions[0]->setChecked(true);
|
||||||
|
break;
|
||||||
|
case 1:
|
||||||
|
actions[3]->setChecked(true);
|
||||||
|
break;
|
||||||
|
case 2:
|
||||||
|
actions[1]->setChecked(true);
|
||||||
|
break;
|
||||||
|
case 3:
|
||||||
|
actions[2]->setChecked(true);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,11 +204,73 @@ public slots:
|
|||||||
trayIcon->setIcon(QIcon(pixmap));
|
trayIcon->setIcon(QIcon(pixmap));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void handleEarDetection(const QString &status) {
|
||||||
|
static bool wasPausedByApp = false;
|
||||||
|
|
||||||
|
QStringList parts = status.split(", ");
|
||||||
|
bool primaryInEar = parts[0].contains("In Ear");
|
||||||
|
bool secondaryInEar = parts[1].contains("In Ear");
|
||||||
|
|
||||||
|
if (primaryInEar && secondaryInEar) {
|
||||||
|
if (wasPausedByApp) {
|
||||||
|
QProcess::execute("playerctl", QStringList() << "play");
|
||||||
|
LOG_INFO("Resumed playback via Playerctl");
|
||||||
|
wasPausedByApp = false;
|
||||||
|
}
|
||||||
|
LOG_INFO("Both AirPods are in ear");
|
||||||
|
activateA2dpProfile();
|
||||||
|
} else {
|
||||||
|
LOG_INFO("At least one AirPod is out of ear");
|
||||||
|
QProcess process;
|
||||||
|
process.start("playerctl", QStringList() << "status");
|
||||||
|
process.waitForFinished();
|
||||||
|
QString playbackStatus = process.readAllStandardOutput().trimmed();
|
||||||
|
LOG_DEBUG("Playback status: " << playbackStatus);
|
||||||
|
if (playbackStatus == "Playing") {
|
||||||
|
QProcess::execute("playerctl", QStringList() << "pause");
|
||||||
|
LOG_INFO("Paused playback via Playerctl");
|
||||||
|
wasPausedByApp = true;
|
||||||
|
}
|
||||||
|
if (!primaryInEar && !secondaryInEar) {
|
||||||
|
removeAudioOutputDevice();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void activateA2dpProfile() {
|
||||||
|
LOG_INFO("Activating A2DP profile for AirPods");
|
||||||
|
QProcess::execute("pactl", QStringList() << "set-card-profile" << "bluez_card." + connectedDeviceMacAddress << "a2dp-sink");
|
||||||
|
}
|
||||||
|
|
||||||
|
void removeAudioOutputDevice() {
|
||||||
|
LOG_INFO("Removing AirPods as audio output device");
|
||||||
|
QProcess::execute("pactl", QStringList() << "set-card-profile" << "bluez_card." + connectedDeviceMacAddress << "off");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool loadConversationalAwarenessState() {
|
||||||
|
QFile file(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/ca_state.txt");
|
||||||
|
if (file.open(QIODevice::ReadOnly)) {
|
||||||
|
QTextStream in(&file);
|
||||||
|
QString state = in.readLine();
|
||||||
|
file.close();
|
||||||
|
return state == "true";
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void saveConversationalAwarenessState(bool state) {
|
||||||
|
QFile file(QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + "/ca_state.txt");
|
||||||
|
if (file.open(QIODevice::WriteOnly)) {
|
||||||
|
QTextStream out(&file);
|
||||||
|
out << (state ? "true" : "false");
|
||||||
|
file.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onTrayIconActivated(QSystemTrayIcon::ActivationReason reason) {
|
void onTrayIconActivated(QSystemTrayIcon::ActivationReason reason) {
|
||||||
if (reason == QSystemTrayIcon::Trigger) {
|
if (reason == QSystemTrayIcon::Trigger) {
|
||||||
LOG_INFO("Tray icon activated");
|
LOG_INFO("Tray icon activated");
|
||||||
// Show settings window
|
|
||||||
QQuickWindow *window = qobject_cast<QQuickWindow *>(QGuiApplication::topLevelWindows().first());
|
QQuickWindow *window = qobject_cast<QQuickWindow *>(QGuiApplication::topLevelWindows().first());
|
||||||
if (window) {
|
if (window) {
|
||||||
window->show();
|
window->show();
|
||||||
@@ -245,7 +327,7 @@ private slots:
|
|||||||
QBluetoothSocket *localSocket = new QBluetoothSocket(QBluetoothServiceInfo::L2capProtocol);
|
QBluetoothSocket *localSocket = new QBluetoothSocket(QBluetoothServiceInfo::L2capProtocol);
|
||||||
connect(localSocket, &QBluetoothSocket::connected, this, [this, localSocket]() {
|
connect(localSocket, &QBluetoothSocket::connected, this, [this, localSocket]() {
|
||||||
LOG_INFO("Connected to device, sending initial packets");
|
LOG_INFO("Connected to device, sending initial packets");
|
||||||
discoveryAgent->stop(); // Stop discovering once connected
|
discoveryAgent->stop();
|
||||||
|
|
||||||
QByteArray handshakePacket = QByteArray::fromHex("00000400010002000000000000000000");
|
QByteArray handshakePacket = QByteArray::fromHex("00000400010002000000000000000000");
|
||||||
QByteArray setSpecificFeaturesPacket = QByteArray::fromHex("040004004d00ff00000000000000");
|
QByteArray setSpecificFeaturesPacket = QByteArray::fromHex("040004004d00ff00000000000000");
|
||||||
@@ -286,6 +368,7 @@ private slots:
|
|||||||
|
|
||||||
localSocket->connectToService(device.address(), QBluetoothUuid("74ec2172-0bad-4d01-8f77-997b2be0722a"));
|
localSocket->connectToService(device.address(), QBluetoothUuid("74ec2172-0bad-4d01-8f77-997b2be0722a"));
|
||||||
socket = localSocket;
|
socket = localSocket;
|
||||||
|
connectedDeviceMacAddress = device.address().toString().replace(":", "_");
|
||||||
}
|
}
|
||||||
|
|
||||||
void parseData(const QByteArray &data) {
|
void parseData(const QByteArray &data) {
|
||||||
@@ -316,6 +399,73 @@ private slots:
|
|||||||
.arg(caseLevel);
|
.arg(caseLevel);
|
||||||
LOG_INFO("Battery status: " << batteryStatus);
|
LOG_INFO("Battery status: " << batteryStatus);
|
||||||
emit batteryStatusChanged(batteryStatus);
|
emit batteryStatusChanged(batteryStatus);
|
||||||
|
|
||||||
|
} else if (data.size() == 10 && data.startsWith(QByteArray::fromHex("040004004B00020001"))) {
|
||||||
|
LOG_INFO("Received conversational awareness data");
|
||||||
|
handleConversationalAwareness(data);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void handleConversationalAwareness(const QByteArray &data) {
|
||||||
|
LOG_DEBUG("Handling conversational awareness data: " << data.toHex());
|
||||||
|
static int initialVolume = -1;
|
||||||
|
bool lowered = data[9] == 0x01;
|
||||||
|
LOG_INFO("Conversational awareness: " << (lowered ? "enabled" : "disabled"));
|
||||||
|
|
||||||
|
if (lowered) {
|
||||||
|
if (initialVolume == -1) {
|
||||||
|
QProcess process;
|
||||||
|
process.start("pactl", QStringList() << "get-sink-volume" << "@DEFAULT_SINK@");
|
||||||
|
process.waitForFinished();
|
||||||
|
QString output = process.readAllStandardOutput();
|
||||||
|
// Volume: front-left: 12843 / 20% / -42.47 dB, front-right: 12843 / 20% / -42.47 dB
|
||||||
|
// balance 0.00
|
||||||
|
|
||||||
|
QRegularExpression re("front-left: \\d+ /\\s*(\\d+)%");
|
||||||
|
QRegularExpressionMatch match = re.match(output);
|
||||||
|
if (match.hasMatch()) {
|
||||||
|
LOG_DEBUG("Matched: " << match.captured(1));
|
||||||
|
initialVolume = match.captured(1).toInt();
|
||||||
|
} else {
|
||||||
|
LOG_ERROR("Failed to parse initial volume from output: " << output);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
QProcess::execute("pactl", QStringList() << "set-sink-volume" << "@DEFAULT_SINK@" << QString::number(initialVolume * 0.20) + "%");
|
||||||
|
LOG_INFO("Volume lowered to 0.20 of initial which is " << initialVolume * 0.20 << "%");
|
||||||
|
} else {
|
||||||
|
if (initialVolume != -1) {
|
||||||
|
QProcess::execute("pactl", QStringList() << "set-sink-volume" << "@DEFAULT_SINK@" << QString::number(initialVolume) + "%");
|
||||||
|
LOG_INFO("Volume restored to " << initialVolume << "%");
|
||||||
|
initialVolume = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void initializeMprisInterface() {
|
||||||
|
QStringList services = QDBusConnection::sessionBus().interface()->registeredServiceNames();
|
||||||
|
QString mprisService;
|
||||||
|
|
||||||
|
foreach (const QString &service, services) {
|
||||||
|
if (service.startsWith("org.mpris.MediaPlayer2.") && service != "org.mpris.MediaPlayer2") {
|
||||||
|
mprisService = service;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!mprisService.isEmpty()) {
|
||||||
|
mprisInterface = new QDBusInterface(mprisService,
|
||||||
|
"/org/mpris/MediaPlayer2",
|
||||||
|
"org.mpris.MediaPlayer2.Player",
|
||||||
|
QDBusConnection::sessionBus(),
|
||||||
|
this);
|
||||||
|
if (!mprisInterface->isValid()) {
|
||||||
|
LOG_ERROR("Failed to initialize MPRIS interface for service: " << mprisService);
|
||||||
|
} else {
|
||||||
|
LOG_INFO("Connected to MPRIS service: " << mprisService);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LOG_WARN("No active MPRIS media players found");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -329,17 +479,17 @@ private:
|
|||||||
QMenu *trayMenu;
|
QMenu *trayMenu;
|
||||||
QBluetoothDeviceDiscoveryAgent *discoveryAgent;
|
QBluetoothDeviceDiscoveryAgent *discoveryAgent;
|
||||||
QBluetoothSocket *socket = nullptr;
|
QBluetoothSocket *socket = nullptr;
|
||||||
|
QDBusInterface *mprisInterface;
|
||||||
|
QString connectedDeviceMacAddress;
|
||||||
};
|
};
|
||||||
|
|
||||||
int main(int argc, char *argv[]) {
|
int main(int argc, char *argv[]) {
|
||||||
QApplication app(argc, argv);
|
QApplication app(argc, argv);
|
||||||
|
|
||||||
QQmlApplicationEngine engine;
|
QQmlApplicationEngine engine;
|
||||||
engine.loadFromModule("linux", "Main");
|
|
||||||
|
|
||||||
AirPodsTrayApp trayApp;
|
AirPodsTrayApp trayApp;
|
||||||
|
|
||||||
engine.rootContext()->setContextProperty("airPodsTrayApp", &trayApp);
|
engine.rootContext()->setContextProperty("airPodsTrayApp", &trayApp);
|
||||||
|
engine.loadFromModule("linux", "Main");
|
||||||
|
|
||||||
QObject::connect(&trayApp, &AirPodsTrayApp::noiseControlModeChanged, [&engine](int mode) {
|
QObject::connect(&trayApp, &AirPodsTrayApp::noiseControlModeChanged, [&engine](int mode) {
|
||||||
LOG_DEBUG("Received noiseControlModeChanged signal with mode: " << mode);
|
LOG_DEBUG("Received noiseControlModeChanged signal with mode: " << mode);
|
||||||
|
|||||||
Reference in New Issue
Block a user