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_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();
}

View File

@ -28,6 +28,8 @@
#include <QTimer>
#include <QBluetoothLocalDevice>
#include <QBluetoothUuid>
#include <QBluetoothPermission>
#include <QCoreApplication>
#include <QLoggingCategory>
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();
}

View File

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

View File

@ -26,6 +26,11 @@
#include <QSettings>
#include <QApplication>
#include <QPermission>
#include <QBluetoothPermission>
#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:

View File

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

View File

@ -1,11 +1,25 @@
#include "platformpermissionsios.h"
#include <QApplication>
#include <QBluetoothPermission>
#include <QPermission>
#include <QSharedPointer>
#include <QTimer>
#include <QtPlugin>
#import <UserNotifications/UNUserNotificationCenter.h>
#import <UserNotifications/UNNotificationSettings.h>
#import <CoreLocation/CoreLocation.h>
#import <CoreBluetooth/CoreBluetooth.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>
@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<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) {
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];
}
}

View File

@ -48,6 +48,14 @@
<string>_jsonrpc._tcp</string>
<string>_ws._tcp</string>
</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>
<false/>
<key>XSAppIconAssets</key>

View File

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