summaryrefslogtreecommitdiffstats
path: root/src/corelib/platform/darwin/qdarwinpermissionplugin_location.mm
blob: 1d32c0fcacbbb60acec4023139d268b6f621816f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
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"