#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 - (void)locationManagerDidChangeAuthorization:(CLLocationManager *)manager { emit PlatformPermissionsIOS::instance()->locationPermissionChanged(); } @end @interface BluetoothManagerDelegate: NSObject @end @implementation BluetoothManagerDelegate - (void)centralManagerDidUpdateState:(CBCentralManager *)manager { emit PlatformPermissionsIOS::instance()->bluetoothPermissionChanged(); } @end void PlatformPermissionsIOS::initObjC() { m_locationManager = [[CLLocationManager alloc] init]; m_locationManager.delegate = [[LocationManagerPermissionDelegate alloc] init]; // Refresh notification permissions right away as that can be retrieved async only. We wanna be ready when the app requests it. refreshNotificationsPermission(); } void PlatformPermissionsIOS::refreshNotificationsPermission() { // Notification permissions can be retrieved async only. UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter]; [center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings *settings) { PermissionStatus newPermission; switch (settings.authorizationStatus) { case UNAuthorizationStatusNotDetermined: newPermission = PermissionStatusNotDetermined; break; case UNAuthorizationStatusDenied: newPermission = PermissionStatusDenied; break; case UNAuthorizationStatusAuthorized: case UNAuthorizationStatusProvisional: case UNAuthorizationStatusEphemeral: newPermission = PermissionStatusGranted; break; } if (newPermission != m_notificationPermissions) { m_notificationPermissions = newPermission; emit notificationsPermissionChanged(); } }]; } void PlatformPermissionsIOS::requestNotificationPermission() { UNUserNotificationCenter* center = [UNUserNotificationCenter currentNotificationCenter]; [center requestAuthorizationWithOptions:(UNAuthorizationOptionAlert + UNAuthorizationOptionBadge + UNAuthorizationOptionSound) completionHandler:^(BOOL granted, NSError * _Nullable) { m_notificationPermissions = granted ? PermissionStatusGranted : PermissionStatusDenied; emit notificationsPermissionChanged(); }]; } PlatformPermissions::PermissionStatus PlatformPermissionsIOS::checkBluetoothPermission() const { 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: fallbackStatus = PermissionStatusGranted; break; case CBManagerAuthorizationDenied: fallbackStatus = PermissionStatusDenied; break; case CBManagerAuthorizationNotDetermined: fallbackStatus = PermissionStatusNotDetermined; break; } } else { // Before iOS 13, Bluetooth permissions are not required fallbackStatus = 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() { 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) { m_bluetoothDelegate = [[BluetoothManagerDelegate alloc] init]; m_bluetoothManager = [[CBCentralManager alloc] initWithDelegate:m_bluetoothDelegate queue:nil]; } } PlatformPermissions::PermissionStatus PlatformPermissionsIOS::checkLocationPermission() const { switch ([CLLocationManager authorizationStatus]) { case kCLAuthorizationStatusNotDetermined: return PermissionStatusNotDetermined; case kCLAuthorizationStatusAuthorizedAlways: case kCLAuthorizationStatusAuthorizedWhenInUse: return PermissionStatusGranted; case kCLAuthorizationStatusDenied: case kCLAuthorizationStatusRestricted: return PermissionStatusDenied; } return PermissionStatusGranted; } void PlatformPermissionsIOS::requestLocationPermission() { if ([m_locationManager respondsToSelector:@selector(requestWhenInUseAuthorization)]) { [m_locationManager requestWhenInUseAuthorization]; } } PlatformPermissions::PermissionStatus PlatformPermissionsIOS::checkBackgroundLocationPermission() const { switch ([CLLocationManager authorizationStatus]) { case kCLAuthorizationStatusNotDetermined: return PermissionStatusNotDetermined; case kCLAuthorizationStatusAuthorizedAlways: return PermissionStatusGranted; case kCLAuthorizationStatusAuthorizedWhenInUse: case kCLAuthorizationStatusDenied: case kCLAuthorizationStatusRestricted: return PermissionStatusDenied; } return PermissionStatusGranted; } void PlatformPermissionsIOS::requestBackgroundLocationPermission() { if ([m_locationManager respondsToSelector:@selector(requestWhenInUseAuthorization)]) { [m_locationManager requestAlwaysAuthorization]; } } void PlatformPermissionsIOS::openPermissionSettings() { [[UIApplication sharedApplication] openURL:[NSURL URLWithString:UIApplicationOpenSettingsURLString]]; }