From a51efe35dc749963a349217566e4c8f0a1adab35 Mon Sep 17 00:00:00 2001 From: Henry Cheng <39224097+jazzysoggy@users.noreply.github.com> Date: Thu, 24 Apr 2025 19:16:24 -0400 Subject: [PATCH 1/3] [Linux] Add reopen to tray options, enhance app tray, add ability to detect duplicate app instances, prevent duplicate app instances, and allow for original instance to be brought to front using the sockets --- linux/Main.qml | 24 ++++++++++++ linux/main.cpp | 78 ++++++++++++++++++++++++++++++++++++++- linux/trayiconmanager.cpp | 17 +++++++++ linux/trayiconmanager.h | 2 + 4 files changed, 119 insertions(+), 2 deletions(-) diff --git a/linux/Main.qml b/linux/Main.qml index f4650b2..de3f502 100644 --- a/linux/Main.qml +++ b/linux/Main.qml @@ -9,9 +9,33 @@ ApplicationWindow { width: 400 height: 300 title: "AirPods Settings" + objectName: "mainWindowObject" onClosing: mainWindow.visible = false + function reopen(pageToLoad) { + if (pageToLoad == "settings") + { + if (stackView.depth == 1) + { + stackView.push(settingsPage) + } + } + else + { + if (stackView.depth > 1) + { + stackView.pop() + } + } + + if (!mainWindow.visible) { + mainWindow.visible = true + } + raise() + requestActivate() + } + // Mouse area for handling back/forward navigation MouseArea { anchors.fill: parent diff --git a/linux/main.cpp b/linux/main.cpp index ae5a10f..3a0dbb8 100644 --- a/linux/main.cpp +++ b/linux/main.cpp @@ -1,5 +1,6 @@ #include - +#include +#include #include "main.h" #include "airpods_packets.h" #include "logger.h" @@ -38,7 +39,7 @@ class AirPodsTrayApp : public QObject { Q_PROPERTY(bool hideOnStart READ hideOnStart CONSTANT) public: - AirPodsTrayApp(bool debugMode, bool hideOnStart, QObject *parent = nullptr) + AirPodsTrayApp(bool debugMode, bool hideOnStart, QQmlApplicationEngine *parent = nullptr) : QObject(parent) , debugMode(debugMode) , m_battery(new Battery(this)) @@ -46,6 +47,7 @@ public: , m_settings(new QSettings("AirPodsTrayApp", "AirPodsTrayApp")) , m_autoStartManager(new AutoStartManager(this)) , m_hideOnStart(hideOnStart) + , parent(parent) { if (debugMode) { QLoggingCategory::setFilterRules("airpodsApp.debug=true"); @@ -58,6 +60,8 @@ public: trayManager = new TrayIconManager(this); trayManager->setNotificationsEnabled(loadNotificationsEnabled()); connect(trayManager, &TrayIconManager::trayClicked, this, &AirPodsTrayApp::onTrayIconActivated); + connect(trayManager, &TrayIconManager::openApp, this, &AirPodsTrayApp::onOpenApp); + connect(trayManager, &TrayIconManager::openSettings, this, &AirPodsTrayApp::onOpenSettings); connect(trayManager, &TrayIconManager::noiseControlChanged, this, qOverload(&AirPodsTrayApp::setNoiseControlMode)); connect(trayManager, &TrayIconManager::conversationalAwarenessToggled, this, &AirPodsTrayApp::setConversationalAwareness); connect(this, &AirPodsTrayApp::batteryStatusChanged, trayManager, &TrayIconManager::updateBatteryStatus); @@ -146,6 +150,9 @@ public: private: bool debugMode; bool isConnectedLocally = false; + + QQmlApplicationEngine *parent = nullptr; + struct { bool isAvailable = true; bool isEnabled = true; // Ability to disable the feature @@ -352,6 +359,30 @@ private slots: } } + void onOpenApp() + { + QObject *rootObject = parent->rootObjects().first(); + if (rootObject) { + QMetaObject::invokeMethod(rootObject, "reopen", Q_ARG(QVariant, "app")); + } + else + { + parent->loadFromModule("linux", "Main"); + } + } + + void onOpenSettings() + { + QObject *rootObject = parent->rootObjects().first(); + if (rootObject) { + QMetaObject::invokeMethod(rootObject, "reopen", Q_ARG(QVariant, "settings")); + } + else + { + parent->loadFromModule("linux", "Main"); + } + } + void sendHandshake() { LOG_INFO("Connected to device, sending initial packets"); writePacketToSocket(AirPodsPackets::Connection::HANDSHAKE, "Handshake packet written: "); @@ -918,6 +949,25 @@ private: int main(int argc, char *argv[]) { QApplication app(argc, argv); + + // Check if app is already open + QSharedMemory sharedMemory; + sharedMemory.setKey("TcpServer-Key"); + if(sharedMemory.create(1) == false) + { + LOG_INFO("Another instance already running! Opening App Window Instead"); + QLocalSocket socket; + // Connect to the original app, then trigger the reopen signal + socket.connectToServer("app_server"); + if (socket.waitForConnected(500)) { + socket.write("reopen"); + socket.flush(); + socket.waitForBytesWritten(500); + socket.disconnectFromServer(); + } + app.exit(); // exit already a process running + return 0; + } app.setQuitOnLastWindowClosed(false); bool debugMode = false; @@ -935,6 +985,30 @@ int main(int argc, char *argv[]) { AirPodsTrayApp *trayApp = new AirPodsTrayApp(debugMode, hideOnStart, &engine); engine.rootContext()->setContextProperty("airPodsTrayApp", trayApp); engine.loadFromModule("linux", "Main"); + + QLocalServer server; + QLocalServer::removeServer("app_server"); + + server.listen("app_server"); + QObject::connect(&server, &QLocalServer::newConnection, [&]() { + QLocalSocket* socket = server.nextPendingConnection(); + QObject::connect(socket, &QLocalSocket::readyRead, [socket, &engine]() { + QString msg = socket->readAll(); + // Check if the message is "reopen", if so, trigger onOpenApp function + if (msg == "reopen") { + LOG_INFO("Reopening app window"); + QObject *rootObject = engine.rootObjects().first(); + if (rootObject) { + QMetaObject::invokeMethod(rootObject, "reopen", Q_ARG(QVariant, "app")); + } + else + { + engine.loadFromModule("linux", "Main"); + } + } + socket->disconnectFromServer(); + }); + }); return app.exec(); } diff --git a/linux/trayiconmanager.cpp b/linux/trayiconmanager.cpp index 137a841..57c0a68 100644 --- a/linux/trayiconmanager.cpp +++ b/linux/trayiconmanager.cpp @@ -56,6 +56,19 @@ void TrayIconManager::updateConversationalAwareness(bool enabled) void TrayIconManager::setupMenuActions() { + // Open action + QAction *openAction = new QAction("Open", trayMenu); + trayMenu->addAction(openAction); + connect(openAction, &QAction::triggered, qApp, [this](){emit openApp();}); + + // Settings Menu + + QAction *settingsMenu = new QAction("Settings", trayMenu); + trayMenu->addAction(settingsMenu); + connect(settingsMenu, &QAction::triggered, qApp, [this](){emit openSettings();}); + + trayMenu->addSeparator(); + // Conversational Awareness Toggle caToggleAction = new QAction("Toggle Conversational Awareness", trayMenu); caToggleAction->setCheckable(true); @@ -63,6 +76,8 @@ void TrayIconManager::setupMenuActions() connect(caToggleAction, &QAction::triggered, this, [this](bool checked) { emit conversationalAwarenessToggled(checked); }); + trayMenu->addSeparator(); + // Noise Control Options noiseControlGroup = new QActionGroup(trayMenu); const QPair noiseOptions[] = { @@ -82,6 +97,8 @@ void TrayIconManager::setupMenuActions() { emit noiseControlChanged(mode); }); } + trayMenu->addSeparator(); + // Quit action QAction *quitAction = new QAction("Quit", trayMenu); trayMenu->addAction(quitAction); diff --git a/linux/trayiconmanager.h b/linux/trayiconmanager.h index adcdbdb..a6384e3 100644 --- a/linux/trayiconmanager.h +++ b/linux/trayiconmanager.h @@ -54,4 +54,6 @@ signals: void trayClicked(); void noiseControlChanged(AirpodsTrayApp::Enums::NoiseControlMode); void conversationalAwarenessToggled(bool enabled); + void openApp(); + void openSettings(); }; \ No newline at end of file From 6376240ce0ba825e54e819f6f17f6265bdd58d94 Mon Sep 17 00:00:00 2001 From: Henry Cheng <39224097+jazzysoggy@users.noreply.github.com> Date: Thu, 24 Apr 2025 19:51:29 -0400 Subject: [PATCH 2/3] Add environmental variable check for phone MAC Address --- linux/main.cpp | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/linux/main.cpp b/linux/main.cpp index 3a0dbb8..787a133 100644 --- a/linux/main.cpp +++ b/linux/main.cpp @@ -712,8 +712,12 @@ private slots: LOG_INFO("Already connected to the phone"); return; } - QBluetoothAddress phoneAddress(PHONE_MAC_ADDRESS); + QProcessEnvironment env = QProcessEnvironment::systemEnvironment(); + if (!env.value("PHONE_MAC_ADDRESS").isEmpty()) + { + QBluetoothAddress phoneAddress = QBluetoothAddress(env.value("PHONE_MAC_ADDRESS")); + } phoneSocket = new QBluetoothSocket(QBluetoothServiceInfo::L2capProtocol); connect(phoneSocket, &QBluetoothSocket::connected, this, [this]() { LOG_INFO("Connected to phone"); @@ -965,7 +969,13 @@ int main(int argc, char *argv[]) { socket.waitForBytesWritten(500); socket.disconnectFromServer(); } - app.exit(); // exit already a process running + else + { + // Failed connection, log and abort + LOG_ERROR("Failed to connect to the original app instance"); + LOG_DEBUG("Socket error: " << socket.errorString()); + } + app.exit(); // exit; process already running return 0; } app.setQuitOnLastWindowClosed(false); @@ -989,9 +999,18 @@ int main(int argc, char *argv[]) { QLocalServer server; QLocalServer::removeServer("app_server"); - server.listen("app_server"); + if (!server.listen("app_server")) + { + LOG_ERROR("Unable to start the listening server"); + LOG_DEBUG("Server error: " << server.errorString()); + } + else + { + LOG_DEBUG("Server started, waiting for connections..."); + } QObject::connect(&server, &QLocalServer::newConnection, [&]() { QLocalSocket* socket = server.nextPendingConnection(); + // Handles Proper Connection QObject::connect(socket, &QLocalSocket::readyRead, [socket, &engine]() { QString msg = socket->readAll(); // Check if the message is "reopen", if so, trigger onOpenApp function @@ -1006,8 +1025,23 @@ int main(int argc, char *argv[]) { engine.loadFromModule("linux", "Main"); } } + else + { + LOG_ERROR("Unknown message received: " << msg); + } socket->disconnectFromServer(); }); + // Handles connection errors + QObject::connect(socket, &QLocalSocket::errorOccurred, [socket]() { + LOG_ERROR("Failed to connect to the duplicate app instance"); + LOG_DEBUG("Connection error: " << socket->errorString()); + }); + + // Handle server-level errors + QObject::connect(&server, &QLocalServer::serverError, [&]() { + LOG_ERROR("Server failed to accept a new connection"); + LOG_DEBUG("Server error: " << server.errorString()); + }); }); return app.exec(); } From a8f87f37f6c71751c17bf4c0d0fe214f475f9a05 Mon Sep 17 00:00:00 2001 From: Henry Cheng <39224097+jazzysoggy@users.noreply.github.com> Date: Thu, 24 Apr 2025 20:04:27 -0400 Subject: [PATCH 3/3] Proper handling of direct kill --- linux/main.cpp | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/linux/main.cpp b/linux/main.cpp index 787a133..280d5d3 100644 --- a/linux/main.cpp +++ b/linux/main.cpp @@ -954,9 +954,10 @@ private: int main(int argc, char *argv[]) { QApplication app(argc, argv); - // Check if app is already open QSharedMemory sharedMemory; sharedMemory.setKey("TcpServer-Key"); + + // Check if app is already open if(sharedMemory.create(1) == false) { LOG_INFO("Another instance already running! Opening App Window Instead"); @@ -968,15 +969,15 @@ int main(int argc, char *argv[]) { socket.flush(); socket.waitForBytesWritten(500); socket.disconnectFromServer(); + app.exit(); // exit; process already running + return 0; } else { - // Failed connection, log and abort - LOG_ERROR("Failed to connect to the original app instance"); + // Failed connection, log and open the app (assume it's not running) + LOG_ERROR("Failed to connect to the original app instance. Assuming it is not running."); LOG_DEBUG("Socket error: " << socket.errorString()); } - app.exit(); // exit; process already running - return 0; } app.setQuitOnLastWindowClosed(false); @@ -1043,6 +1044,11 @@ int main(int argc, char *argv[]) { LOG_DEBUG("Server error: " << server.errorString()); }); }); + + QObject::connect(&app, &QCoreApplication::aboutToQuit, [&]() { + LOG_DEBUG("Application is about to quit. Cleaning up..."); + sharedMemory.detach(); + }); return app.exec(); }