// Copyright (C) 2022 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qdarwinpermissionplugin_p_p.h" #include #include @interface QDarwinLocationPermissionHandler () @property (nonatomic, retain) CLLocationManager *manager; @end Q_LOGGING_CATEGORY(lcLocationPermission, "qt.permissions.location"); void warmUpLocationServices() { // After creating a CLLocationManager the authorizationStatus // will initially be kCLAuthorizationStatusNotDetermined. The // status will then update to an actual status if the app was // previously authorized/denied once the location services // do some initial book-keeping in the background. By kicking // off a CLLocationManager early on here, we ensure that by // the time the user calls checkPermission the authorization // status has been resolved. qCDebug(lcLocationPermission) << "Warming up location services"; [[CLLocationManager new] release]; } Q_CONSTRUCTOR_FUNCTION(warmUpLocationServices); struct PermissionRequest { QPermission permission; PermissionCallback callback; }; @implementation QDarwinLocationPermissionHandler { std::deque m_requests; } - (instancetype)init { if ((self = [super init])) { // The delegate callbacks will come in on the thread that // the CLLocationManager is created on, and we want those // to come in on the main thread, so we defer creation // of the manger until requestPermission, where we know // we are on the main thread. self.manager = nil; } return self; } - (Qt::PermissionStatus)checkPermission:(QPermission)permission { const auto locationPermission = *permission.value(); auto status = [self authorizationStatus:locationPermission]; if (status != Qt::PermissionStatus::Granted) return status; return [self accuracyAuthorization:locationPermission]; } - (Qt::PermissionStatus)authorizationStatus:(QLocationPermission)permission { NSString *bundleIdentifier = NSBundle.mainBundle.bundleIdentifier; if (!bundleIdentifier || !bundleIdentifier.length) { qCWarning(lcLocationPermission) << "Missing bundle identifier" << "in Info.plist. Can not use location permissions."; return Qt::PermissionStatus::Denied; } #if defined(Q_OS_VISIONOS) if (permission.availability() == QLocationPermission::Always) return Qt::PermissionStatus::Denied; #endif auto status = [self authorizationStatus]; switch (status) { case kCLAuthorizationStatusRestricted: case kCLAuthorizationStatusDenied: return Qt::PermissionStatus::Denied; case kCLAuthorizationStatusNotDetermined: return Qt::PermissionStatus::Undetermined; #if !defined(Q_OS_VISIONOS) case kCLAuthorizationStatusAuthorizedAlways: return Qt::PermissionStatus::Granted; #endif #if defined(Q_OS_IOS) || defined(Q_OS_VISIONOS) case kCLAuthorizationStatusAuthorizedWhenInUse: if (permission.availability() == QLocationPermission::Always) return Qt::PermissionStatus::Denied; return Qt::PermissionStatus::Granted; #endif } qCWarning(lcPermissions) << "Unknown permission status" << status << "detected in" << self; return Qt::PermissionStatus::Denied; } - (CLAuthorizationStatus)authorizationStatus { if (self.manager) { if (@available(macOS 11, iOS 14, *)) return self.manager.authorizationStatus; } return QT_IGNORE_DEPRECATIONS(CLLocationManager.authorizationStatus); } - (Qt::PermissionStatus)accuracyAuthorization:(QLocationPermission)permission { auto status = CLAccuracyAuthorizationReducedAccuracy; if (@available(macOS 11, iOS 14, *)) status = self.manager.accuracyAuthorization; switch (status) { case CLAccuracyAuthorizationFullAccuracy: return Qt::PermissionStatus::Granted; case CLAccuracyAuthorizationReducedAccuracy: if (permission.accuracy() == QLocationPermission::Approximate) return Qt::PermissionStatus::Granted; else return Qt::PermissionStatus::Denied; } qCWarning(lcPermissions) << "Unknown accuracy status" << status << "detected in" << self; return Qt::PermissionStatus::Denied; } - (QStringList)usageDescriptionsFor:(QPermission)permission { #if defined(Q_OS_MACOS) return { "NSLocationUsageDescription" }; #else // iOS 11 and above QStringList usageDescriptions = { "NSLocationWhenInUseUsageDescription" }; const auto locationPermission = *permission.value(); if (locationPermission.availability() == QLocationPermission::Always) usageDescriptions << "NSLocationAlwaysAndWhenInUseUsageDescription"; return usageDescriptions; #endif } - (void)requestPermission:(QPermission)permission withCallback:(PermissionCallback)callback { const bool requestAlreadyInFlight = !m_requests.empty(); m_requests.push_back({ permission, callback }); if (requestAlreadyInFlight) { qCDebug(lcLocationPermission).nospace() << "Already processing " << m_requests.front().permission << ". Deferring request"; } else { [self requestQueuedPermission]; } } - (void)requestQueuedPermission { Q_ASSERT(!m_requests.empty()); const auto permission = m_requests.front().permission; qCDebug(lcLocationPermission) << "Requesting" << permission; if (!self.manager) { self.manager = [[CLLocationManager new] autorelease]; self.manager.delegate = self; } const auto locationPermission = *permission.value(); switch (locationPermission.availability()) { case QLocationPermission::WhenInUse: // The documentation specifies that requestWhenInUseAuthorization can // only be called when the current authorization status is undetermined. switch ([self authorizationStatus]) { case kCLAuthorizationStatusNotDetermined: [self.manager requestWhenInUseAuthorization]; break; default: [self deliverResult]; } break; case QLocationPermission::Always: #if defined(Q_OS_VISIONOS) [self deliverResult]; // Not supported #else // The documentation specifies that requestAlwaysAuthorization can only // be called when the current authorization status is either undetermined, // or authorized when in use. switch ([self authorizationStatus]) { case kCLAuthorizationStatusNotDetermined: [self.manager requestAlwaysAuthorization]; break; #ifdef Q_OS_IOS case kCLAuthorizationStatusAuthorizedWhenInUse: // Unfortunately when asking for AlwaysAuthorization when in // the WhenInUse state, to "upgrade" the permission, the iOS // location system will not give us a callback if the user // denies the request, leaving us hanging without a way to // respond to the permission request. qCWarning(lcLocationPermission) << "QLocationPermission::WhenInUse" << "can not be upgraded to QLocationPermission::Always on iOS." << "Please request QLocationPermission::Always directly."; Q_FALLTHROUGH(); #endif default: [self deliverResult]; } #endif break; } } - (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status { qCDebug(lcLocationPermission) << "Processing authorization" << "update with status" << status; if (m_requests.empty()) { qCDebug(lcLocationPermission) << "No requests in flight. Ignoring."; return; } if (status == kCLAuthorizationStatusNotDetermined) { // Initializing a CLLocationManager will result in an initial // callback to the delegate even before we've requested any // location permissions. Normally we would ignore this callback // due to the request queue check above, but if this callback // comes in after the application has requested a permission // we don't want to report the undetermined status, but rather // wait for the actual result to come in. qCDebug(lcLocationPermission) << "Ignoring delegate callback" << "with status kCLAuthorizationStatusNotDetermined"; return; } [self deliverResult]; } - (void)deliverResult { auto request = m_requests.front(); m_requests.pop_front(); auto status = [self checkPermission:request.permission]; qCDebug(lcLocationPermission) << "Result for" << request.permission << "was" << status; request.callback(status); if (!m_requests.empty()) { qCDebug(lcLocationPermission) << "Still have" << m_requests.size() << "deferred request(s)"; [self requestQueuedPermission]; } } @end #include "moc_qdarwinpermissionplugin_p_p.cpp"