iOS: Update bluetooth permission handling to the new QPermission system

This commit is contained in:
Simon Stürz 2025-12-09 16:32:59 +01:00
parent 0f8c2a879e
commit 238e86c930
8 changed files with 160 additions and 45 deletions

View File

@ -77,16 +77,6 @@ void JsonRpcClient::registerNotificationHandler(QObject *handler, const QString
m_notificationHandlers.insert(nameSpace, handler); m_notificationHandlers.insert(nameSpace, handler);
m_notificationHandlerMethods.insert(handler, method); 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(); setNotificationsEnabled();
} }

View File

@ -28,6 +28,8 @@
#include <QTimer> #include <QTimer>
#include <QBluetoothLocalDevice> #include <QBluetoothLocalDevice>
#include <QBluetoothUuid> #include <QBluetoothUuid>
#include <QBluetoothPermission>
#include <QCoreApplication>
#include <QLoggingCategory> #include <QLoggingCategory>
Q_DECLARE_LOGGING_CATEGORY(dcBluetoothDiscovery); Q_DECLARE_LOGGING_CATEGORY(dcBluetoothDiscovery);
@ -64,8 +66,6 @@ BluetoothDiscovery::BluetoothDiscovery(QObject *parent) :
#else #else
// Note: on iOS there is no QBluetoothLocalDevice available, therefore we have to assume there is one and // 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. // start the discovery agent with the default constructor.
// https://bugreports.qt.io/browse/QTBUG-65547
m_bluetoothAvailable = true; m_bluetoothAvailable = true;
// Always start with assuming BT is enabled // Always start with assuming BT is enabled
@ -88,13 +88,16 @@ bool BluetoothDiscovery::bluetoothEnabled() const
#ifdef Q_OS_IOS #ifdef Q_OS_IOS
return m_bluetoothAvailable && m_bluetoothEnabled; return m_bluetoothAvailable && m_bluetoothEnabled;
#endif #endif
qCDebug(dcBluetoothDiscovery) << "bluetoothEnabled(): m_bluetoothAvailable:" << m_bluetoothAvailable; qCDebug(dcBluetoothDiscovery) << "bluetoothEnabled(): m_bluetoothAvailable:" << m_bluetoothAvailable;
return m_bluetoothAvailable && m_localDevice->hostMode() != QBluetoothLocalDevice::HostPoweredOff; 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; return;
}
if (bluetoothEnabled) { if (bluetoothEnabled) {
if (m_localDevice->hostMode() == QBluetoothLocalDevice::HostPoweredOff) { if (m_localDevice->hostMode() == QBluetoothLocalDevice::HostPoweredOff) {
m_localDevice->powerOn(); m_localDevice->powerOn();
@ -151,26 +154,30 @@ void BluetoothDiscovery::onBluetoothHostModeChanged(const QBluetoothLocalDevice:
#endif #endif
emit bluetoothEnabledChanged(false); emit bluetoothEnabledChanged(false);
break; break;
default: default:
// Note: discovery works in all other modes // Note: discovery works in all other modes
#ifdef Q_OS_IOS #ifdef Q_OS_IOS
m_bluetoothEnabled = true; m_bluetoothEnabled = true;
#endif #endif
emit bluetoothEnabledChanged(hostMode != QBluetoothLocalDevice::HostPoweredOff); emit bluetoothEnabledChanged(hostMode != QBluetoothLocalDevice::HostPoweredOff);
if (!m_discoveryAgent) { if (!m_discoveryAgent) {
#ifdef Q_OS_ANDROID #ifdef Q_OS_ANDROID
m_discoveryAgent = new QBluetoothDeviceDiscoveryAgent(m_localDevice->address(), this); m_discoveryAgent = new QBluetoothDeviceDiscoveryAgent(m_localDevice->address(), this);
#else #else
m_discoveryAgent = new QBluetoothDeviceDiscoveryAgent(this); m_discoveryAgent = new QBluetoothDeviceDiscoveryAgent(this);
#endif #endif
connect(m_discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &BluetoothDiscovery::deviceDiscovered);
#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) #if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0))
connect(m_discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceUpdated, this, &BluetoothDiscovery::deviceDiscovered); 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); connect(m_discoveryAgent, &QBluetoothDeviceDiscoveryAgent::errorOccurred, this, &BluetoothDiscovery::onError);
#else #else
connect(m_discoveryAgent, SIGNAL(error(QBluetoothDeviceDiscoveryAgent::Error)), this, SLOT(onError(QBluetoothDeviceDiscoveryAgent::Error))); connect(m_discoveryAgent, SIGNAL(error(QBluetoothDeviceDiscoveryAgent::Error)), this, SLOT(onError(QBluetoothDeviceDiscoveryAgent::Error)));
#endif #endif
connect(m_discoveryAgent, &QBluetoothDeviceDiscoveryAgent::deviceDiscovered, this, &BluetoothDiscovery::deviceDiscovered);
connect(m_discoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, this, &BluetoothDiscovery::discoveryFinished); connect(m_discoveryAgent, &QBluetoothDeviceDiscoveryAgent::finished, this, &BluetoothDiscovery::discoveryFinished);
connect(m_discoveryAgent, &QBluetoothDeviceDiscoveryAgent::canceled, this, &BluetoothDiscovery::discoveryCancelled); connect(m_discoveryAgent, &QBluetoothDeviceDiscoveryAgent::canceled, this, &BluetoothDiscovery::discoveryCancelled);
} }
@ -241,13 +248,11 @@ void BluetoothDiscovery::onError(const QBluetoothDeviceDiscoveryAgent::Error &er
void BluetoothDiscovery::start() void BluetoothDiscovery::start()
{ {
if (!m_discoveryAgent || !bluetoothEnabled()) { if (!m_discoveryAgent || !bluetoothEnabled())
return; return;
}
if (m_discoveryAgent->isActive()) { if (m_discoveryAgent->isActive())
m_discoveryAgent->stop(); m_discoveryAgent->stop();
}
foreach (const QBluetoothDeviceInfo &info, m_discoveryAgent->discoveredDevices()) { foreach (const QBluetoothDeviceInfo &info, m_discoveryAgent->discoveredDevices()) {
qCDebug(dcBluetoothDiscovery()) << "Already discovered device:" << info.name(); qCDebug(dcBluetoothDiscovery()) << "Already discovered device:" << info.name();
@ -255,7 +260,9 @@ void BluetoothDiscovery::start()
} }
qCDebug(dcBluetoothDiscovery) << "Starting discovery."; 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(); emit discoveringChanged();
} }

View File

@ -176,9 +176,8 @@ ios: {
OTHER_FILES += $${OBJECTIVE_SOURCES} OTHER_FILES += $${OBJECTIVE_SOURCES}
LIBS += -framework CoreLocation \ LIBS += -framework CoreLocation \
-framework CoreBluetooth \ -framework CoreBluetooth \
-framework CoreNFC -framework CoreNFC
# Add Firebase SDK # Add Firebase SDK
QMAKE_LFLAGS += -ObjC $(inherited) QMAKE_LFLAGS += -ObjC $(inherited)
@ -186,8 +185,10 @@ ios: {
firebase_files.files += $$files($${IOS_PACKAGE_DIR}/GoogleService-Info.plist) firebase_files.files += $$files($${IOS_PACKAGE_DIR}/GoogleService-Info.plist)
QMAKE_BUNDLE_DATA += firebase_files QMAKE_BUNDLE_DATA += firebase_files
INCLUDEPATH += ../3rdParty/ios/ INCLUDEPATH += ../3rdParty/ios/
LIBS += -F$$PWD/../3rdParty/ios/Firebase/FirebaseAnalytics/ \ LIBS += -F$$PWD/../3rdParty/ios/Firebase/FirebaseAnalytics/ \
-F$$PWD/../3rdParty/ios/Firebase/FirebaseMessaging -F$$PWD/../3rdParty/ios/Firebase/FirebaseMessaging
LIBS += -framework "FirebaseMessaging" \ LIBS += -framework "FirebaseMessaging" \
-framework "GoogleUtilities" \ -framework "GoogleUtilities" \
-framework "Protobuf" \ -framework "Protobuf" \
@ -196,13 +197,13 @@ ios: {
-framework "FirebaseInstallations" \ -framework "FirebaseInstallations" \
-framework "PromisesObjC" \ -framework "PromisesObjC" \
LIBS += -L$$top_builddir/libnymea-app -lnymea-app \ 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 \ 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 # Configure generated xcode project to have our bundle id
QMAKE_TARGET_BUNDLE_PREFIX=$${IOS_BUNDLE_PREFIX} QMAKE_TARGET_BUNDLE_PREFIX=$${IOS_BUNDLE_PREFIX}
@ -220,6 +221,9 @@ ios: {
ios_launch_images.files += $${IOS_PACKAGE_DIR}/NymeaLaunchScreen.storyboard ios_launch_images.files += $${IOS_PACKAGE_DIR}/NymeaLaunchScreen.storyboard
QMAKE_BUNDLE_DATA += ios_launch_images QMAKE_BUNDLE_DATA += ios_launch_images
DEFINES += QT_STATICPLUGIN
QTPLUGIN += qdarwinbluetoothpermission
IOS_DEVELOPMENT_TEAM.name = DEVELOPMENT_TEAM IOS_DEVELOPMENT_TEAM.name = DEVELOPMENT_TEAM
IOS_DEVELOPMENT_TEAM.value = $$IOS_TEAM_ID IOS_DEVELOPMENT_TEAM.value = $$IOS_TEAM_ID
QMAKE_MAC_XCODE_SETTINGS += IOS_DEVELOPMENT_TEAM QMAKE_MAC_XCODE_SETTINGS += IOS_DEVELOPMENT_TEAM

View File

@ -26,6 +26,11 @@
#include <QSettings> #include <QSettings>
#include <QApplication> #include <QApplication>
#include <QPermission>
#include <QBluetoothPermission>
#include "logging.h"
NYMEA_LOGGING_CATEGORY(dcPlatformPermissions, "PlatformPermissions")
PlatformPermissionsIOS *PlatformPermissionsIOS::s_instance = nullptr; PlatformPermissionsIOS *PlatformPermissionsIOS::s_instance = nullptr;
@ -61,13 +66,13 @@ PlatformPermissions::PermissionStatus PlatformPermissionsIOS::checkPermission(Pe
case PermissionBluetooth: case PermissionBluetooth:
return checkBluetoothPermission(); return checkBluetoothPermission();
default: default:
return PermissionStatusGranted; return PermissionStatusGranted;
} }
} }
void PlatformPermissionsIOS::requestPermission(Permission permission) void PlatformPermissionsIOS::requestPermission(Permission platformPermission)
{ {
switch (permission) { switch (platformPermission) {
case PermissionNone: case PermissionNone:
break; break;
case PermissionLocalNetwork: case PermissionLocalNetwork:

View File

@ -32,9 +32,11 @@
#if __OBJC__ #if __OBJC__
@class CLLocationManager; @class CLLocationManager;
@class CBCentralManager; @class CBCentralManager;
@class BluetoothManagerDelegate;
#else #else
typedef void CLLocationManager; typedef void CLLocationManager;
typedef void CBCentralManager; typedef void CBCentralManager;
typedef void BluetoothManagerDelegate;
#endif #endif
class PlatformPermissionsIOS : public PlatformPermissions class PlatformPermissionsIOS : public PlatformPermissions
@ -44,8 +46,8 @@ public:
explicit PlatformPermissionsIOS(QObject *parent = nullptr); explicit PlatformPermissionsIOS(QObject *parent = nullptr);
static PlatformPermissionsIOS *instance(); static PlatformPermissionsIOS *instance();
PermissionStatus checkPermission(Permission permission) const override; PermissionStatus checkPermission(Permission ) const override;
void requestPermission(Permission permission) override; void requestPermission(Permission platformPermission) override;
void openPermissionSettings() override; void openPermissionSettings() override;
private: private:
@ -62,14 +64,15 @@ private:
void requestLocalNetworkPermission(); void requestLocalNetworkPermission();
void requestNotificationPermission(); void requestNotificationPermission();
void requestBluetoothPermission(); void requestBluetoothPermission();
void requestBluetoothPermissionLegacy();
void requestLocationPermission(); void requestLocationPermission();
void requestBackgroundLocationPermission(); void requestBackgroundLocationPermission();
PermissionStatus m_notificationPermissions = PermissionStatusNotDetermined; PermissionStatus m_notificationPermissions = PermissionStatusNotDetermined;
CLLocationManager *m_locationManager = nullptr; CLLocationManager *m_locationManager = nullptr;
CBCentralManager *m_bluetoothManager = nullptr; CBCentralManager *m_bluetoothManager = nullptr;
BluetoothManagerDelegate *m_bluetoothDelegate = nullptr;
}; };
#endif // PLATFORMPERMISSIONSIOS_H #endif // PLATFORMPERMISSIONSIOS_H

View File

@ -1,11 +1,25 @@
#include "platformpermissionsios.h" #include "platformpermissionsios.h"
#include <QApplication>
#include <QBluetoothPermission>
#include <QPermission>
#include <QSharedPointer>
#include <QTimer>
#include <QtPlugin>
#import <UserNotifications/UNUserNotificationCenter.h> #import <UserNotifications/UNUserNotificationCenter.h>
#import <UserNotifications/UNNotificationSettings.h> #import <UserNotifications/UNNotificationSettings.h>
#import <CoreLocation/CoreLocation.h> #import <CoreLocation/CoreLocation.h>
#import <CoreBluetooth/CoreBluetooth.h> #import <CoreBluetooth/CoreBluetooth.h>
#import <UIKit/UIKit.h> #import <UIKit/UIKit.h>
#include "logging.h"
Q_DECLARE_LOGGING_CATEGORY(dcPlatformPermissions)
#ifdef QT_STATICPLUGIN
Q_IMPORT_PLUGIN(QDarwinBluetoothPermissionPlugin)
#endif
@interface LocationManagerPermissionDelegate : NSObject <CLLocationManagerDelegate> @interface LocationManagerPermissionDelegate : NSObject <CLLocationManagerDelegate>
@end @end
@implementation LocationManagerPermissionDelegate @implementation LocationManagerPermissionDelegate
@ -69,28 +83,112 @@ void PlatformPermissionsIOS::requestNotificationPermission()
PlatformPermissions::PermissionStatus PlatformPermissionsIOS::checkBluetoothPermission() const 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, *)) { if (@available(iOS 13.1, *)) {
switch (CBCentralManager.authorization) { switch (CBCentralManager.authorization) {
case CBManagerAuthorizationAllowedAlways: case CBManagerAuthorizationAllowedAlways:
fallbackStatus = PermissionStatusGranted;
break;
case CBManagerAuthorizationRestricted: case CBManagerAuthorizationRestricted:
return PermissionStatusGranted; fallbackStatus = PermissionStatusGranted;
break;
case CBManagerAuthorizationDenied: case CBManagerAuthorizationDenied:
return PermissionStatusDenied; fallbackStatus = PermissionStatusDenied;
break;
case CBManagerAuthorizationNotDetermined: 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() 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<bool>::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) { if (!m_bluetoothManager) {
BluetoothManagerDelegate *delegate = [[BluetoothManagerDelegate alloc] init]; m_bluetoothDelegate = [[BluetoothManagerDelegate alloc] init];
m_bluetoothManager = [[CBCentralManager alloc] initWithDelegate:delegate queue:nil]; m_bluetoothManager = [[CBCentralManager alloc] initWithDelegate:m_bluetoothDelegate queue:nil];
} }
} }

View File

@ -48,6 +48,14 @@
<string>_jsonrpc._tcp</string> <string>_jsonrpc._tcp</string>
<string>_ws._tcp</string> <string>_ws._tcp</string>
</array> </array>
<key>NSAppTransportSecurity</key>
<dict>
<!-- Fallback: allow all loads, including self-signed LAN TLS. -->
<key>NSAllowsArbitraryLoads</key>
<true/>
<key>NSAllowsLocalNetworking</key>
<true/>
</dict>
<key>UIViewControllerBasedStatusBarAppearance</key> <key>UIViewControllerBasedStatusBarAppearance</key>
<false/> <false/>
<key>XSAppIconAssets</key> <key>XSAppIconAssets</key>

View File

@ -1,2 +1,2 @@
1.11.1 1.11.1
687 689