summaryrefslogtreecommitdiffstats
path: root/src/corelib/platform/darwin/qdarwinpermissionplugin_location.mm
diff options
context:
space:
mode:
Diffstat (limited to 'src/corelib/platform/darwin/qdarwinpermissionplugin_location.mm')
-rw-r--r--src/corelib/platform/darwin/qdarwinpermissionplugin_location.mm263
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"