mirror of
https://github.com/kavishdevar/librepods.git
synced 2026-01-31 15:19:11 +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)
|
||||
|
||||
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)
|
||||
|
||||
@@ -19,19 +19,8 @@ qt_add_qml_module(applinux
|
||||
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
|
||||
PRIVATE Qt6::Quick Qt6::Widgets Qt6::Bluetooth
|
||||
PRIVATE Qt6::Quick Qt6::Widgets Qt6::Bluetooth Qt6::Multimedia Qt6::DBus
|
||||
)
|
||||
|
||||
include(GNUInstallDirs)
|
||||
|
||||
@@ -7,19 +7,24 @@ ApplicationWindow {
|
||||
height: 300
|
||||
title: "AirPods Settings"
|
||||
property bool ignoreNoiseControlChange: false
|
||||
property bool isPlaying: false
|
||||
|
||||
Component.onCompleted: {
|
||||
caToggle.checked = airPodsTrayApp.loadConversationalAwarenessState()
|
||||
}
|
||||
|
||||
Column {
|
||||
spacing: 20
|
||||
padding: 20
|
||||
|
||||
Text {
|
||||
text: "Ear Detection Status: "
|
||||
id: earDetectionStatus
|
||||
text: "Battery Status: "
|
||||
id: batteryStatus
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Battery Status: "
|
||||
id: batteryStatus
|
||||
text: "Ear Detection Status: "
|
||||
id: earDetectionStatus
|
||||
}
|
||||
|
||||
ComboBox {
|
||||
@@ -33,7 +38,7 @@ ApplicationWindow {
|
||||
}
|
||||
Connections {
|
||||
target: airPodsTrayApp
|
||||
function onNoiseControlModeChanged(mode) {
|
||||
onNoiseControlModeChanged: {
|
||||
ignoreNoiseControlChange = true
|
||||
noiseControlMode.currentIndex = mode;
|
||||
ignoreNoiseControlChange = false
|
||||
@@ -44,8 +49,10 @@ ApplicationWindow {
|
||||
Switch {
|
||||
id: caToggle
|
||||
text: "Conversational Awareness"
|
||||
checked: isPlaying
|
||||
onCheckedChanged: {
|
||||
airPodsTrayApp.setConversationalAwareness(checked)
|
||||
airPodsTrayApp.saveConversationalAwarenessState(checked)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
174
linux/main.cpp
174
linux/main.cpp
@@ -16,6 +16,14 @@
|
||||
#include <QTimer>
|
||||
#include <QPainter>
|
||||
#include <QPalette>
|
||||
#include <QDBusInterface>
|
||||
#include <QDBusReply>
|
||||
#include <QDBusConnectionInterface>
|
||||
#include <QProcess>
|
||||
#include <QRegularExpression>
|
||||
#include <QFile>
|
||||
#include <QTextStream>
|
||||
#include <QStandardPaths>
|
||||
|
||||
Q_LOGGING_CATEGORY(airpodsApp, "airpodsApp")
|
||||
|
||||
@@ -65,6 +73,7 @@ public:
|
||||
connect(this, &AirPodsTrayApp::noiseControlModeChanged, this, &AirPodsTrayApp::updateNoiseControlMenu);
|
||||
connect(this, &AirPodsTrayApp::batteryStatusChanged, this, &AirPodsTrayApp::updateBatteryTooltip);
|
||||
connect(this, &AirPodsTrayApp::batteryStatusChanged, this, &AirPodsTrayApp::updateTrayIcon);
|
||||
connect(this, &AirPodsTrayApp::earDetectionStatusChanged, this, &AirPodsTrayApp::handleEarDetection);
|
||||
|
||||
trayIcon->setContextMenu(trayMenu);
|
||||
trayIcon->show();
|
||||
@@ -77,7 +86,6 @@ public:
|
||||
discoveryAgent->start();
|
||||
LOG_INFO("AirPodsTrayApp initialized and started device discovery");
|
||||
|
||||
// Check for already connected devices
|
||||
QBluetoothLocalDevice localDevice;
|
||||
connect(&localDevice, &QBluetoothLocalDevice::deviceConnected, this, &AirPodsTrayApp::onDeviceConnected);
|
||||
connect(&localDevice, &QBluetoothLocalDevice::deviceDisconnected, this, &AirPodsTrayApp::onDeviceDisconnected);
|
||||
@@ -90,6 +98,7 @@ public:
|
||||
return;
|
||||
}
|
||||
}
|
||||
initializeMprisInterface();
|
||||
}
|
||||
|
||||
public slots:
|
||||
@@ -119,16 +128,16 @@ public slots:
|
||||
LOG_INFO("Setting noise control mode to: " << mode);
|
||||
QByteArray packet;
|
||||
switch (mode) {
|
||||
case 0: // Off
|
||||
case 0:
|
||||
packet = QByteArray::fromHex("0400040009000D01000000");
|
||||
break;
|
||||
case 1: // Noise Cancellation
|
||||
case 1:
|
||||
packet = QByteArray::fromHex("0400040009000D02000000");
|
||||
break;
|
||||
case 2: // Transparency
|
||||
case 2:
|
||||
packet = QByteArray::fromHex("0400040009000D03000000");
|
||||
break;
|
||||
case 3: // Adaptive
|
||||
case 3:
|
||||
packet = QByteArray::fromHex("0400040009000D04000000");
|
||||
break;
|
||||
}
|
||||
@@ -156,8 +165,19 @@ public slots:
|
||||
for (QAction *action : actions) {
|
||||
action->setChecked(false);
|
||||
}
|
||||
if (mode >= 0 && mode < actions.size()) {
|
||||
actions[mode]->setChecked(true);
|
||||
switch (mode) {
|
||||
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));
|
||||
}
|
||||
|
||||
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:
|
||||
void onTrayIconActivated(QSystemTrayIcon::ActivationReason reason) {
|
||||
if (reason == QSystemTrayIcon::Trigger) {
|
||||
LOG_INFO("Tray icon activated");
|
||||
// Show settings window
|
||||
QQuickWindow *window = qobject_cast<QQuickWindow *>(QGuiApplication::topLevelWindows().first());
|
||||
if (window) {
|
||||
window->show();
|
||||
@@ -245,7 +327,7 @@ private slots:
|
||||
QBluetoothSocket *localSocket = new QBluetoothSocket(QBluetoothServiceInfo::L2capProtocol);
|
||||
connect(localSocket, &QBluetoothSocket::connected, this, [this, localSocket]() {
|
||||
LOG_INFO("Connected to device, sending initial packets");
|
||||
discoveryAgent->stop(); // Stop discovering once connected
|
||||
discoveryAgent->stop();
|
||||
|
||||
QByteArray handshakePacket = QByteArray::fromHex("00000400010002000000000000000000");
|
||||
QByteArray setSpecificFeaturesPacket = QByteArray::fromHex("040004004d00ff00000000000000");
|
||||
@@ -286,6 +368,7 @@ private slots:
|
||||
|
||||
localSocket->connectToService(device.address(), QBluetoothUuid("74ec2172-0bad-4d01-8f77-997b2be0722a"));
|
||||
socket = localSocket;
|
||||
connectedDeviceMacAddress = device.address().toString().replace(":", "_");
|
||||
}
|
||||
|
||||
void parseData(const QByteArray &data) {
|
||||
@@ -316,6 +399,73 @@ private slots:
|
||||
.arg(caseLevel);
|
||||
LOG_INFO("Battery status: " << 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;
|
||||
QBluetoothDeviceDiscoveryAgent *discoveryAgent;
|
||||
QBluetoothSocket *socket = nullptr;
|
||||
QDBusInterface *mprisInterface;
|
||||
QString connectedDeviceMacAddress;
|
||||
};
|
||||
|
||||
int main(int argc, char *argv[]) {
|
||||
QApplication app(argc, argv);
|
||||
|
||||
QQmlApplicationEngine engine;
|
||||
engine.loadFromModule("linux", "Main");
|
||||
|
||||
AirPodsTrayApp trayApp;
|
||||
|
||||
engine.rootContext()->setContextProperty("airPodsTrayApp", &trayApp);
|
||||
engine.loadFromModule("linux", "Main");
|
||||
|
||||
QObject::connect(&trayApp, &AirPodsTrayApp::noiseControlModeChanged, [&engine](int mode) {
|
||||
LOG_DEBUG("Received noiseControlModeChanged signal with mode: " << mode);
|
||||
|
||||
Reference in New Issue
Block a user