From 238e86c930d40f01bfc051e93bd8ba642a10e680 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20St=C3=BCrz?= Date: Tue, 9 Dec 2025 16:32:59 +0100 Subject: [PATCH] iOS: Update bluetooth permission handling to the new QPermission system --- libnymea-app/jsonrpc/jsonrpcclient.cpp | 10 -- libnymea-app/wifisetup/bluetoothdiscovery.cpp | 31 +++-- nymea-app/nymea-app.pro | 18 +-- .../ios/platformpermissionsios.cpp | 11 +- .../ios/platformpermissionsios.h | 9 +- .../ios/platformpermissionsios.mm | 116 ++++++++++++++++-- packaging/ios/Info.plist.cmake.in | 8 ++ version.txt | 2 +- 8 files changed, 160 insertions(+), 45 deletions(-) diff --git a/libnymea-app/jsonrpc/jsonrpcclient.cpp b/libnymea-app/jsonrpc/jsonrpcclient.cpp index 0687bd0c..4f637bfd 100644 --- a/libnymea-app/jsonrpc/jsonrpcclient.cpp +++ b/libnymea-app/jsonrpc/jsonrpcclient.cpp @@ -77,16 +77,6 @@ void JsonRpcClient::registerNotificationHandler(QObject *handler, const QString m_notificationHandlers.insert(nameSpace, handler); m_notificationHandlerMethods.insert(handler, method); - // Clean up if the handler gets destroyed so we don't dereference dangling pointers when - // processing notifications. - connect(handler, &QObject::destroyed, this, [this](QObject *obj){ - for (const QString &ns : m_notificationHandlers.keys(obj)) { - m_notificationHandlers.remove(ns, obj); - } - m_notificationHandlerMethods.remove(obj); - setNotificationsEnabled(); - }); - setNotificationsEnabled(); } diff --git a/libnymea-app/wifisetup/bluetoothdiscovery.cpp b/libnymea-app/wifisetup/bluetoothdiscovery.cpp index f1c20f55..a384c73f 100644 --- a/libnymea-app/wifisetup/bluetoothdiscovery.cpp +++ b/libnymea-app/wifisetup/bluetoothdiscovery.cpp @@ -28,6 +28,8 @@ #include #include #include +#include +#include #include Q_DECLARE_LOGGING_CATEGORY(dcBluetoothDiscovery); @@ -64,8 +66,6 @@ BluetoothDiscovery::BluetoothDiscovery(QObject *parent) : #else // Note: on iOS there is no QBluetoothLocalDevice available, therefore we have to assume there is one and // start the discovery agent with the default constructor. - // https://bugreports.qt.io/browse/QTBUG-65547 - m_bluetoothAvailable = true; // Always start with assuming BT is enabled @@ -88,13 +88,16 @@ bool BluetoothDiscovery::bluetoothEnabled() const #ifdef Q_OS_IOS return m_bluetoothAvailable && m_bluetoothEnabled; #endif + qCDebug(dcBluetoothDiscovery) << "bluetoothEnabled(): m_bluetoothAvailable:" << m_bluetoothAvailable; return m_bluetoothAvailable && m_localDevice->hostMode() != QBluetoothLocalDevice::HostPoweredOff; } -void BluetoothDiscovery::setBluetoothEnabled(bool bluetoothEnabled) { - if (!m_bluetoothAvailable) { + +void BluetoothDiscovery::setBluetoothEnabled(bool bluetoothEnabled) +{ + if (!m_bluetoothAvailable) return; - } + if (bluetoothEnabled) { if (m_localDevice->hostMode() == QBluetoothLocalDevice::HostPoweredOff) { m_localDevice->powerOn(); @@ -151,26 +154,30 @@ void BluetoothDiscovery::onBluetoothHostModeChanged(const QBluetoothLocalDevice: #endif emit bluetoothEnabledChanged(false); break; + default: // Note: discovery works in all other modes #ifdef Q_OS_IOS m_bluetoothEnabled = true; #endif emit bluetoothEnabledChanged(hostMode != QBluetoothLocalDevice::HostPoweredOff); + if (!m_discoveryAgent) { #ifdef Q_OS_ANDROID m_discoveryAgent = new QBluetoothDeviceDiscoveryAgent(m_localDevice->address(), this); #else m_discoveryAgent = new QBluetoothDeviceDiscoveryAgent(this); #endif - connect(m_discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &BluetoothDiscovery::deviceDiscovered); #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) connect(m_discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceUpdated, this, &BluetoothDiscovery::deviceDiscovered); -#elif (QT_VERSION >= QT_VERSION_CHECK(6, 2, 0)) +#endif + +#if (QT_VERSION >= QT_VERSION_CHECK(6, 2, 0)) connect(m_discoveryAgent, &QBluetoothDeviceDiscoveryAgent::errorOccurred, this, &BluetoothDiscovery::onError); #else connect(m_discoveryAgent, SIGNAL(error(QBluetoothDeviceDiscoveryAgent::Error)), this, SLOT(onError(QBluetoothDeviceDiscoveryAgent::Error))); #endif + connect(m_discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &BluetoothDiscovery::deviceDiscovered); connect(m_discoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, this, &BluetoothDiscovery::discoveryFinished); connect(m_discoveryAgent, &QBluetoothDeviceDiscoveryAgent::canceled, this, &BluetoothDiscovery::discoveryCancelled); } @@ -241,13 +248,11 @@ void BluetoothDiscovery::onError(const QBluetoothDeviceDiscoveryAgent::Error &er void BluetoothDiscovery::start() { - if (!m_discoveryAgent || !bluetoothEnabled()) { + if (!m_discoveryAgent || !bluetoothEnabled()) return; - } - if (m_discoveryAgent->isActive()) { + if (m_discoveryAgent->isActive()) m_discoveryAgent->stop(); - } foreach (const QBluetoothDeviceInfo &info, m_discoveryAgent->discoveredDevices()) { qCDebug(dcBluetoothDiscovery()) << "Already discovered device:" << info.name(); @@ -255,7 +260,9 @@ void BluetoothDiscovery::start() } qCDebug(dcBluetoothDiscovery) << "Starting discovery."; - m_discoveryAgent->start(); + + // Since we are only interested in low energy results, this speed up the result significantly + m_discoveryAgent->start(QBluetoothDeviceDiscoveryAgent::LowEnergyMethod); emit discoveringChanged(); } diff --git a/nymea-app/nymea-app.pro b/nymea-app/nymea-app.pro index fa60ddf3..a8604654 100644 --- a/nymea-app/nymea-app.pro +++ b/nymea-app/nymea-app.pro @@ -176,9 +176,8 @@ ios: { OTHER_FILES += $${OBJECTIVE_SOURCES} LIBS += -framework CoreLocation \ - -framework CoreBluetooth \ - -framework CoreNFC - + -framework CoreBluetooth \ + -framework CoreNFC # Add Firebase SDK QMAKE_LFLAGS += -ObjC $(inherited) @@ -186,8 +185,10 @@ ios: { firebase_files.files += $$files($${IOS_PACKAGE_DIR}/GoogleService-Info.plist) QMAKE_BUNDLE_DATA += firebase_files INCLUDEPATH += ../3rdParty/ios/ + LIBS += -F$$PWD/../3rdParty/ios/Firebase/FirebaseAnalytics/ \ -F$$PWD/../3rdParty/ios/Firebase/FirebaseMessaging + LIBS += -framework "FirebaseMessaging" \ -framework "GoogleUtilities" \ -framework "Protobuf" \ @@ -196,13 +197,13 @@ ios: { -framework "FirebaseInstallations" \ -framework "PromisesObjC" \ - LIBS += -L$$top_builddir/libnymea-app -lnymea-app \ - -L$$top_builddir/experiences/airconditioning -lnymea-app-airconditioning \ + -L$$top_builddir/experiences/airconditioning -lnymea-app-airconditioning \ + -L$$top_builddir/experiences/evdash -lnymea-app-evdash PRE_TARGETDEPS += $$top_builddir/libnymea-app/libnymea-app.a \ - $$top_builddir/experiences/airconditioning/libnymea-app-airconditioning.a - + $$top_builddir/experiences/airconditioning/libnymea-app-airconditioning.a \ + $$top_builddir/experiences/evdash/libnymea-app-evdash.a # Configure generated xcode project to have our bundle id QMAKE_TARGET_BUNDLE_PREFIX=$${IOS_BUNDLE_PREFIX} @@ -220,6 +221,9 @@ ios: { ios_launch_images.files += $${IOS_PACKAGE_DIR}/NymeaLaunchScreen.storyboard QMAKE_BUNDLE_DATA += ios_launch_images + DEFINES += QT_STATICPLUGIN + QTPLUGIN += qdarwinbluetoothpermission + IOS_DEVELOPMENT_TEAM.name = DEVELOPMENT_TEAM IOS_DEVELOPMENT_TEAM.value = $$IOS_TEAM_ID QMAKE_MAC_XCODE_SETTINGS += IOS_DEVELOPMENT_TEAM diff --git a/nymea-app/platformintegration/ios/platformpermissionsios.cpp b/nymea-app/platformintegration/ios/platformpermissionsios.cpp index 755c93d6..80efd006 100644 --- a/nymea-app/platformintegration/ios/platformpermissionsios.cpp +++ b/nymea-app/platformintegration/ios/platformpermissionsios.cpp @@ -26,6 +26,11 @@ #include #include +#include +#include + +#include "logging.h" +NYMEA_LOGGING_CATEGORY(dcPlatformPermissions, "PlatformPermissions") PlatformPermissionsIOS *PlatformPermissionsIOS::s_instance = nullptr; @@ -61,13 +66,13 @@ PlatformPermissions::PermissionStatus PlatformPermissionsIOS::checkPermission(Pe case PermissionBluetooth: return checkBluetoothPermission(); default: - return PermissionStatusGranted; + return PermissionStatusGranted; } } -void PlatformPermissionsIOS::requestPermission(Permission permission) +void PlatformPermissionsIOS::requestPermission(Permission platformPermission) { - switch (permission) { + switch (platformPermission) { case PermissionNone: break; case PermissionLocalNetwork: diff --git a/nymea-app/platformintegration/ios/platformpermissionsios.h b/nymea-app/platformintegration/ios/platformpermissionsios.h index 7b847118..fe7bbc47 100644 --- a/nymea-app/platformintegration/ios/platformpermissionsios.h +++ b/nymea-app/platformintegration/ios/platformpermissionsios.h @@ -32,9 +32,11 @@ #if __OBJC__ @class CLLocationManager; @class CBCentralManager; +@class BluetoothManagerDelegate; #else typedef void CLLocationManager; typedef void CBCentralManager; +typedef void BluetoothManagerDelegate; #endif class PlatformPermissionsIOS : public PlatformPermissions @@ -44,8 +46,8 @@ public: explicit PlatformPermissionsIOS(QObject *parent = nullptr); static PlatformPermissionsIOS *instance(); - PermissionStatus checkPermission(Permission permission) const override; - void requestPermission(Permission permission) override; + PermissionStatus checkPermission(Permission ) const override; + void requestPermission(Permission platformPermission) override; void openPermissionSettings() override; private: @@ -62,14 +64,15 @@ private: void requestLocalNetworkPermission(); void requestNotificationPermission(); void requestBluetoothPermission(); + void requestBluetoothPermissionLegacy(); void requestLocationPermission(); void requestBackgroundLocationPermission(); PermissionStatus m_notificationPermissions = PermissionStatusNotDetermined; - CLLocationManager *m_locationManager = nullptr; CBCentralManager *m_bluetoothManager = nullptr; + BluetoothManagerDelegate *m_bluetoothDelegate = nullptr; }; #endif // PLATFORMPERMISSIONSIOS_H diff --git a/nymea-app/platformintegration/ios/platformpermissionsios.mm b/nymea-app/platformintegration/ios/platformpermissionsios.mm index 55129169..a2422c77 100644 --- a/nymea-app/platformintegration/ios/platformpermissionsios.mm +++ b/nymea-app/platformintegration/ios/platformpermissionsios.mm @@ -1,11 +1,25 @@ #include "platformpermissionsios.h" +#include +#include +#include +#include +#include +#include + #import #import #import #import #import +#include "logging.h" +Q_DECLARE_LOGGING_CATEGORY(dcPlatformPermissions) + +#ifdef QT_STATICPLUGIN +Q_IMPORT_PLUGIN(QDarwinBluetoothPermissionPlugin) +#endif + @interface LocationManagerPermissionDelegate : NSObject @end @implementation LocationManagerPermissionDelegate @@ -69,28 +83,112 @@ void PlatformPermissionsIOS::requestNotificationPermission() PlatformPermissions::PermissionStatus PlatformPermissionsIOS::checkBluetoothPermission() const { - // iOS 13.0 would have an api but it's more complicated and also deprecated... Ignoring... + qCDebug(dcPlatformPermissions()) << "Checking bluetooth permission..."; + QBluetoothPermission btPermission; + btPermission.setCommunicationModes(QBluetoothPermission::Access); + const auto qtStatus = qGuiApp->checkPermission(btPermission); + if (qtStatus == Qt::PermissionStatus::Granted) { + qCDebug(dcPlatformPermissions()) << "Bluetooth permisson granted (Qt plugin)"; + return PermissionStatusGranted; + } else { + qCDebug(dcPlatformPermissions()) << "Bluetooth permisson NOT granted (Qt plugin)"; + } + + PermissionStatus fallbackStatus = PermissionStatusGranted; if (@available(iOS 13.1, *)) { switch (CBCentralManager.authorization) { case CBManagerAuthorizationAllowedAlways: + fallbackStatus = PermissionStatusGranted; + break; case CBManagerAuthorizationRestricted: - return PermissionStatusGranted; + fallbackStatus = PermissionStatusGranted; + break; case CBManagerAuthorizationDenied: - return PermissionStatusDenied; + fallbackStatus = PermissionStatusDenied; + break; case CBManagerAuthorizationNotDetermined: - return PermissionStatusNotDetermined; + fallbackStatus = PermissionStatusNotDetermined; + break; } + } else { + // Before iOS 13, Bluetooth permissions are not required + fallbackStatus = PermissionStatusGranted; } - // Before iOS 13, Bluetooth permissions are not required - return PermissionStatusGranted; + + switch (qtStatus) { + case Qt::PermissionStatus::Denied: + qCWarning(dcPlatformPermissions()) << "Bluetooth permission denied by Qt plugin, fallback reports" << fallbackStatus; + break; + case Qt::PermissionStatus::Undetermined: + qCWarning(dcPlatformPermissions()) << "QBluetoothPermission status Undetermined...using fallback."; + break; + case Qt::PermissionStatus::Granted: + break; + } + + return fallbackStatus; } void PlatformPermissionsIOS::requestBluetoothPermission() { - // Instantiating a Bluetooth manager just trigger the popup... + qCDebug(dcPlatformPermissions()) << "Requesting bluetooth permission..."; + auto handlePermissionResult = [](const QPermission &permission) { + switch (permission.status()) { + case Qt::PermissionStatus::Granted: + qCDebug(dcPlatformPermissions()) << "Bluetooth permission granted."; + emit s_instance->bluetoothPermissionChanged(); + return; + case Qt::PermissionStatus::Denied: + if (s_instance->checkBluetoothPermission() == PermissionStatusNotDetermined) { + qCWarning(dcPlatformPermissions()) << "Bluetooth permission plugin unavailable, falling back to CoreBluetooth request."; + s_instance->requestBluetoothPermissionLegacy(); + return; + } + qCWarning(dcPlatformPermissions()) << "Bluetooth permission denied."; + emit s_instance->bluetoothPermissionChanged(); + return; + case Qt::PermissionStatus::Undetermined: + qCWarning(dcPlatformPermissions()) << "Bluetooth permission plugin unavailable, falling back to CoreBluetooth request."; + s_instance->requestBluetoothPermissionLegacy(); + return; + } + }; + + QBluetoothPermission btPermission; + btPermission.setCommunicationModes(QBluetoothPermission::Access); + + if (qApp->checkPermission(btPermission) == Qt::PermissionStatus::Undetermined) { + auto permissionHandled = QSharedPointer::create(false); + + qApp->requestPermission(btPermission, [handlePermissionResult, permissionHandled](const QPermission &permission) { + *permissionHandled = true; + handlePermissionResult(permission); + }); + + // The Qt permission plugin might be missing from certain builds. If we still don't have + // a decision after giving it a moment, fall back to the CoreBluetooth prompt. + QTimer::singleShot(2000, this, [this, permissionHandled]() { + if (*permissionHandled) { + return; + } + if (checkBluetoothPermission() == PermissionStatusNotDetermined) { + qCWarning(dcPlatformPermissions()) << "Bluetooth permission plugin unavailable, falling back to CoreBluetooth request."; + requestBluetoothPermissionLegacy(); + } + }); + return; + } + + handlePermissionResult(btPermission); +} + +void PlatformPermissionsIOS::requestBluetoothPermissionLegacy() +{ + qCDebug(dcPlatformPermissions()) << "Requesting bluetooth permission legacy..."; + // Instantiating a Bluetooth manager triggers the native dialog on first use. if (!m_bluetoothManager) { - BluetoothManagerDelegate *delegate = [[BluetoothManagerDelegate alloc] init]; - m_bluetoothManager = [[CBCentralManager alloc] initWithDelegate:delegate queue:nil]; + m_bluetoothDelegate = [[BluetoothManagerDelegate alloc] init]; + m_bluetoothManager = [[CBCentralManager alloc] initWithDelegate:m_bluetoothDelegate queue:nil]; } } diff --git a/packaging/ios/Info.plist.cmake.in b/packaging/ios/Info.plist.cmake.in index 54c03110..dff7cba0 100644 --- a/packaging/ios/Info.plist.cmake.in +++ b/packaging/ios/Info.plist.cmake.in @@ -48,6 +48,14 @@ _jsonrpc._tcp _ws._tcp + NSAppTransportSecurity + + + NSAllowsArbitraryLoads + + NSAllowsLocalNetworking + + UIViewControllerBasedStatusBarAppearance XSAppIconAssets diff --git a/version.txt b/version.txt index 82cfbbbb..490dd04c 100644 --- a/version.txt +++ b/version.txt @@ -1,2 +1,2 @@ 1.11.1 -687 +689