diff options
Diffstat (limited to 'src/corelib/platform/darwin/qdarwinpermissionplugin_location.mm')
-rw-r--r-- | src/corelib/platform/darwin/qdarwinpermissionplugin_location.mm | 263 |
1 files changed, 263 insertions, 0 deletions
diff --git a/src/corelib/platform/darwin/qdarwinpermissionplugin_location.mm b/src/corelib/platform/darwin/qdarwinpermissionplugin_location.mm new file mode 100644 index 0000000000..1d32c0fcac --- /dev/null +++ b/src/corelib/platform/darwin/qdarwinpermissionplugin_location.mm @@ -0,0 +1,263 @@ +// 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 <deque> + +#include <CoreLocation/CoreLocation.h> + +@interface QDarwinLocationPermissionHandler () <CLLocationManagerDelegate> +@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<PermissionRequest> 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<QLocationPermission>(); + + 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<QLocationPermission>(); + 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<QLocationPermission>(); + 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" |