diff --git a/linux/CMakeLists.txt b/linux/CMakeLists.txt index 629abd85..c3fb5d12 100644 --- a/linux/CMakeLists.txt +++ b/linux/CMakeLists.txt @@ -41,6 +41,7 @@ qt_add_executable(librepods thirdparty/QR-Code-generator/qrcodegen.hpp QRCodeImageProvider.hpp eardetection.hpp + dbusadaptor.hpp media/playerstatuswatcher.cpp media/playerstatuswatcher.h systemsleepmonitor.hpp diff --git a/linux/ble/blemanager.cpp b/linux/ble/blemanager.cpp index d75a2a62..91bb1873 100644 --- a/linux/ble/blemanager.cpp +++ b/linux/ble/blemanager.cpp @@ -18,7 +18,9 @@ AirpodsTrayApp::Enums::AirPodsModel getModelName(quint16 modelId) {0x1F20, AirPodsModel::AirPodsMaxUSBC}, {0x0E20, AirPodsModel::AirPodsPro}, {0x1420, AirPodsModel::AirPodsPro2Lightning}, - {0x2420, AirPodsModel::AirPodsPro2USBC} + {0x2420, AirPodsModel::AirPodsPro2USBC}, + {0x2720, AirPodsModel::AirPodsPro3}, + {0x3F20, AirPodsModel::AirPodsPro3} }; return modelMap.value(modelId, AirPodsModel::Unknown); diff --git a/linux/dbusadaptor.hpp b/linux/dbusadaptor.hpp new file mode 100644 index 00000000..fdefb3f7 --- /dev/null +++ b/linux/dbusadaptor.hpp @@ -0,0 +1,111 @@ +#pragma once + +#include +#include +#include +#include +#include "battery.hpp" +#include "deviceinfo.hpp" + +class BatteryDBusAdaptor : public QDBusAbstractAdaptor +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "me.kavishdevar.librepods.Battery") + + // Battery levels (0-100) + Q_PROPERTY(int LeftLevel READ leftLevel NOTIFY BatteryChanged) + Q_PROPERTY(int RightLevel READ rightLevel NOTIFY BatteryChanged) + Q_PROPERTY(int CaseLevel READ caseLevel NOTIFY BatteryChanged) + Q_PROPERTY(int HeadsetLevel READ headsetLevel NOTIFY BatteryChanged) + + // Charging status + Q_PROPERTY(bool LeftCharging READ leftCharging NOTIFY BatteryChanged) + Q_PROPERTY(bool RightCharging READ rightCharging NOTIFY BatteryChanged) + Q_PROPERTY(bool CaseCharging READ caseCharging NOTIFY BatteryChanged) + Q_PROPERTY(bool HeadsetCharging READ headsetCharging NOTIFY BatteryChanged) + + // Availability (connected/detected) + Q_PROPERTY(bool LeftAvailable READ leftAvailable NOTIFY BatteryChanged) + Q_PROPERTY(bool RightAvailable READ rightAvailable NOTIFY BatteryChanged) + Q_PROPERTY(bool CaseAvailable READ caseAvailable NOTIFY BatteryChanged) + Q_PROPERTY(bool HeadsetAvailable READ headsetAvailable NOTIFY BatteryChanged) + + // Device info + Q_PROPERTY(QString DeviceName READ deviceName NOTIFY DeviceChanged) + Q_PROPERTY(bool Connected READ connected NOTIFY DeviceChanged) + +public: + BatteryDBusAdaptor(Battery *battery, DeviceInfo *deviceInfo, QObject *parent) + : QDBusAbstractAdaptor(parent), m_battery(battery), m_deviceInfo(deviceInfo) + { + setAutoRelaySignals(true); + + // Connect battery signals to our relay + connect(m_battery, &Battery::batteryStatusChanged, this, [this]() { + emit BatteryChanged(); + }); + + connect(m_deviceInfo, &DeviceInfo::batteryStatusChanged, this, [this]() { + emit BatteryChanged(); + }); + + connect(m_deviceInfo, &DeviceInfo::deviceNameChanged, this, [this]() { + emit DeviceChanged(); + }); + } + + // Battery levels + int leftLevel() const { return m_battery->getLeftPodLevel(); } + int rightLevel() const { return m_battery->getRightPodLevel(); } + int caseLevel() const { return m_battery->getCaseLevel(); } + int headsetLevel() const { return m_battery->getHeadsetLevel(); } + + // Charging status + bool leftCharging() const { return m_battery->isLeftPodCharging(); } + bool rightCharging() const { return m_battery->isRightPodCharging(); } + bool caseCharging() const { return m_battery->isCaseCharging(); } + bool headsetCharging() const { return m_battery->isHeadsetCharging(); } + + // Availability + bool leftAvailable() const { return m_battery->isLeftPodAvailable(); } + bool rightAvailable() const { return m_battery->isRightPodAvailable(); } + bool caseAvailable() const { return m_battery->isCaseAvailable(); } + bool headsetAvailable() const { return m_battery->isHeadsetAvailable(); } + + // Device info - connected if device name is set and any battery is available + QString deviceName() const { return m_deviceInfo->deviceName(); } + bool connected() const { + return !m_deviceInfo->deviceName().isEmpty() && + (leftAvailable() || rightAvailable() || headsetAvailable()); + } + +public slots: + // Method to get all battery info at once (useful for waybar) + QVariantMap GetBatteryInfo() + { + QVariantMap info; + info["left_level"] = leftLevel(); + info["left_charging"] = leftCharging(); + info["left_available"] = leftAvailable(); + info["right_level"] = rightLevel(); + info["right_charging"] = rightCharging(); + info["right_available"] = rightAvailable(); + info["case_level"] = caseLevel(); + info["case_charging"] = caseCharging(); + info["case_available"] = caseAvailable(); + info["headset_level"] = headsetLevel(); + info["headset_charging"] = headsetCharging(); + info["headset_available"] = headsetAvailable(); + info["device_name"] = deviceName(); + info["connected"] = connected(); + return info; + } + +signals: + void BatteryChanged(); + void DeviceChanged(); + +private: + Battery *m_battery; + DeviceInfo *m_deviceInfo; +}; diff --git a/linux/enums.h b/linux/enums.h index 815415db..2c3708c3 100644 --- a/linux/enums.h +++ b/linux/enums.h @@ -30,6 +30,7 @@ namespace AirpodsTrayApp AirPodsPro, AirPodsPro2Lightning, AirPodsPro2USBC, + AirPodsPro3, AirPodsMaxLightning, AirPodsMaxUSBC, AirPods4, @@ -63,7 +64,10 @@ namespace AirpodsTrayApp {"A3054", AirPodsModel::AirPods4}, {"A3056", AirPodsModel::AirPods4ANC}, {"A3055", AirPodsModel::AirPods4ANC}, - {"A3057", AirPodsModel::AirPods4ANC}}; + {"A3057", AirPodsModel::AirPods4ANC}, + {"A3063", AirPodsModel::AirPodsPro3}, + {"A3064", AirPodsModel::AirPodsPro3}, + {"A3065", AirPodsModel::AirPodsPro3}}; return modelNumberMap.value(modelNumber, AirPodsModel::Unknown); } @@ -82,6 +86,7 @@ namespace AirpodsTrayApp case AirPodsModel::AirPodsPro: case AirPodsModel::AirPodsPro2Lightning: case AirPodsModel::AirPodsPro2USBC: + case AirPodsModel::AirPodsPro3: return {"podpro.png", "podpro_case.png"}; case AirPodsModel::AirPodsMaxLightning: case AirPodsModel::AirPodsMaxUSBC: diff --git a/linux/main.cpp b/linux/main.cpp index 94d341ea..73972190 100644 --- a/linux/main.cpp +++ b/linux/main.cpp @@ -30,6 +30,8 @@ #include "ble/bleutils.h" #include "QRCodeImageProvider.hpp" #include "systemsleepmonitor.hpp" +#include "dbusadaptor.hpp" +#include using namespace AirpodsTrayApp::Enums; @@ -49,17 +51,17 @@ class AirPodsTrayApp : public QObject { Q_PROPERTY(bool hearingAidEnabled READ hearingAidEnabled WRITE setHearingAidEnabled NOTIFY hearingAidEnabledChanged) public: - AirPodsTrayApp(bool debugMode, bool hideOnStart, QQmlApplicationEngine *parent = nullptr) + AirPodsTrayApp(bool debugMode, bool hideOnStart, bool noTray = false, QQmlApplicationEngine *parent = nullptr) : QObject(parent), debugMode(debugMode), m_settings(new QSettings("AirPodsTrayApp", "AirPodsTrayApp")) - , m_autoStartManager(new AutoStartManager(this)), m_hideOnStart(hideOnStart), parent(parent) + , m_autoStartManager(new AutoStartManager(this)), m_hideOnStart(hideOnStart), m_noTray(noTray), parent(parent) , m_deviceInfo(new DeviceInfo(this)), m_bleManager(new BleManager(this)) , m_systemSleepMonitor(new SystemSleepMonitor(this)) { QLoggingCategory::setFilterRules(QString("librepods.debug=%1").arg(debugMode ? "true" : "false")); LOG_INFO("Initializing LibrePods"); - // Initialize tray icon and connect signals - trayManager = new TrayIconManager(this); + // Initialize tray icon and connect signals (skip if --no-tray) + trayManager = new TrayIconManager(this, noTray); trayManager->setNotificationsEnabled(loadNotificationsEnabled()); connect(trayManager, &TrayIconManager::trayClicked, this, &AirPodsTrayApp::onTrayIconActivated); connect(trayManager, &TrayIconManager::openApp, this, &AirPodsTrayApp::onOpenApp); @@ -149,7 +151,31 @@ class AirPodsTrayApp : public QObject { bool isEnabled = true; // Ability to disable the feature } CrossDevice; - void initializeDBus() { } + void initializeDBus() { + // Create D-Bus adaptor for battery info + new BatteryDBusAdaptor(m_deviceInfo->getBattery(), m_deviceInfo, this); + + // Register on session bus + QDBusConnection sessionBus = QDBusConnection::sessionBus(); + if (!sessionBus.isConnected()) { + LOG_ERROR("Cannot connect to D-Bus session bus"); + return; + } + + // Register service + if (!sessionBus.registerService("me.kavishdevar.librepods")) { + LOG_ERROR("Cannot register D-Bus service: " << sessionBus.lastError().message()); + return; + } + + // Register object + if (!sessionBus.registerObject("/battery", this)) { + LOG_ERROR("Cannot register D-Bus object: " << sessionBus.lastError().message()); + return; + } + + LOG_INFO("D-Bus service registered: me.kavishdevar.librepods at /battery"); + } bool isAirPodsDevice(const QBluetoothDeviceInfo &device) { @@ -982,6 +1008,7 @@ private slots: AutoStartManager *m_autoStartManager; int m_retryAttempts = 3; bool m_hideOnStart = false; + bool m_noTray = false; DeviceInfo *m_deviceInfo; BleManager *m_bleManager; SystemSleepMonitor *m_systemSleepMonitor = nullptr; @@ -1034,18 +1061,22 @@ int main(int argc, char *argv[]) { bool debugMode = false; bool hideOnStart = false; + bool noTray = false; for (int i = 1; i < argc; ++i) { if (QString(argv[i]) == "--debug") debugMode = true; if (QString(argv[i]) == "--hide") hideOnStart = true; + + if (QString(argv[i]) == "--no-tray") + noTray = true; } QQmlApplicationEngine engine; qmlRegisterType("me.kavishdevar.Battery", 1, 0, "Battery"); qmlRegisterType("me.kavishdevar.DeviceInfo", 1, 0, "DeviceInfo"); - AirPodsTrayApp *trayApp = new AirPodsTrayApp(debugMode, hideOnStart, &engine); + AirPodsTrayApp *trayApp = new AirPodsTrayApp(debugMode, hideOnStart, noTray, &engine); engine.rootContext()->setContextProperty("airPodsTrayApp", trayApp); // Expose PHONE_MAC_ADDRESS environment variable to QML for placeholder in settings diff --git a/linux/trayiconmanager.cpp b/linux/trayiconmanager.cpp index 738feecf..70c5e960 100644 --- a/linux/trayiconmanager.cpp +++ b/linux/trayiconmanager.cpp @@ -11,7 +11,7 @@ using namespace AirpodsTrayApp::Enums; -TrayIconManager::TrayIconManager(QObject *parent) : QObject(parent) +TrayIconManager::TrayIconManager(QObject *parent, bool noTray) : QObject(parent), m_noTray(noTray) { // Initialize tray icon trayIcon = new QSystemTrayIcon(QIcon(":/icons/assets/airpods.png"), this); @@ -24,7 +24,10 @@ TrayIconManager::TrayIconManager(QObject *parent) : QObject(parent) trayIcon->setContextMenu(trayMenu); connect(trayIcon, &QSystemTrayIcon::activated, this, &TrayIconManager::onTrayIconActivated); - trayIcon->show(); + // Only show tray icon if not disabled + if (!noTray) { + trayIcon->show(); + } } void TrayIconManager::showNotification(const QString &title, const QString &message) diff --git a/linux/trayiconmanager.h b/linux/trayiconmanager.h index 25c15304..65ecf323 100644 --- a/linux/trayiconmanager.h +++ b/linux/trayiconmanager.h @@ -13,7 +13,7 @@ class TrayIconManager : public QObject Q_PROPERTY(bool notificationsEnabled READ notificationsEnabled WRITE setNotificationsEnabled NOTIFY notificationsEnabledChanged) public: - explicit TrayIconManager(QObject *parent = nullptr); + explicit TrayIconManager(QObject *parent = nullptr, bool noTray = false); void updateBatteryStatus(const QString &status); @@ -51,6 +51,7 @@ private slots: QAction *caToggleAction; QActionGroup *noiseControlGroup; bool m_notificationsEnabled = true; + bool m_noTray = false; void setupMenuActions();