smth works :D

This commit is contained in:
Kavish Devar
2025-01-17 04:28:58 +05:30
parent c9b04b24c0
commit 4b3e1130e8
3 changed files with 176 additions and 30 deletions

View File

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

View File

@@ -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)
}
}
}

View File

@@ -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);