Skip to content

Commit d6885b7

Browse files
authored
Merge pull request #31 from humblehacker/30-fix-stuck-authorization-request
fix stuck authorization request
2 parents 4018f64 + 5d21789 commit d6885b7

File tree

3 files changed

+196
-23
lines changed

3 files changed

+196
-23
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
//
2+
// ApplicationStateMonitor.swift
3+
// AsyncLocationKit
4+
//
5+
// Created by David Whetstone on 11/28/22.
6+
//
7+
8+
import Foundation
9+
import UIKit
10+
11+
@MainActor
12+
class ApplicationStateMonitor {
13+
private(set) var hasResignedActive = false
14+
15+
private var hasResignedActiveTask: Task<Void, Never>?
16+
private var hasBecomeActiveTask: Task<Void, Never>?
17+
18+
func startMonitoringApplicationState() {
19+
guard #available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) else { return }
20+
startMonitoringHasResignedActive()
21+
startMonitoringHasBecomeActive()
22+
}
23+
24+
func stopMonitoringApplicationState() {
25+
guard #available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) else { return }
26+
stopMonitoringHasResignedActive()
27+
stopMonitoringHasBecomeActive()
28+
}
29+
30+
func hasResignedActive() async -> Bool {
31+
guard #available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) else { return false }
32+
var iter = hasResignedActiveSequence.makeAsyncIterator()
33+
return await iter.next() != nil
34+
}
35+
36+
func hasBecomeActive() async -> Bool {
37+
guard #available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) else { return false }
38+
var iter = hasBecomeActiveSequence.makeAsyncIterator()
39+
return await iter.next() != nil
40+
}
41+
42+
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
43+
private func startMonitoringHasResignedActive() {
44+
guard hasResignedActiveTask == nil else { return }
45+
46+
hasResignedActiveTask = Task {
47+
for await _ in self.hasResignedActiveSequence {
48+
if Task.isCancelled { break }
49+
self.hasResignedActive = true
50+
self.stopMonitoringHasResignedActive()
51+
}
52+
}
53+
}
54+
55+
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
56+
private func startMonitoringHasBecomeActive() {
57+
guard hasBecomeActiveTask == nil else { return }
58+
59+
hasBecomeActiveTask = Task {
60+
for await _ in self.hasBecomeActiveSequence {
61+
if Task.isCancelled { break }
62+
self.stopMonitoringHasBecomeActive()
63+
}
64+
}
65+
}
66+
67+
private func stopMonitoringHasResignedActive() {
68+
hasResignedActiveTask?.cancel()
69+
hasResignedActiveTask = nil
70+
}
71+
72+
private func stopMonitoringHasBecomeActive() {
73+
hasBecomeActiveTask?.cancel()
74+
hasBecomeActiveTask = nil
75+
}
76+
77+
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
78+
private var hasResignedActiveSequence: AsyncMapSequence<NotificationCenter.Notifications, Bool> {
79+
_hasResignedActiveSequence as! AsyncMapSequence<NotificationCenter.Notifications, Bool>
80+
}
81+
82+
@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
83+
private var hasBecomeActiveSequence: AsyncMapSequence<NotificationCenter.Notifications, Bool> {
84+
_hasBecomeActiveSequence as! AsyncMapSequence<NotificationCenter.Notifications, Bool>
85+
}
86+
87+
// We unfortunately need these backing variables since properties cannot be declared conditionally available
88+
89+
private var _hasResignedActiveSequence: Any? = {
90+
guard #available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) else { return nil }
91+
return NotificationCenter.default.notifications(named: UIApplication.willResignActiveNotification).map { _ in true }
92+
}()
93+
94+
private var _hasBecomeActiveSequence: Any? = {
95+
guard #available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) else { return nil }
96+
return NotificationCenter.default.notifications(named: UIApplication.didBecomeActiveNotification).map { _ in true }
97+
}()
98+
}

Sources/AsyncLocationKit/AsyncLocationManager.swift

+52-16
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import Foundation
2424
import CoreLocation
2525

2626
public typealias AuthotizationContinuation = CheckedContinuation<CLAuthorizationStatus, Never>
27-
public typealias AccuracyAuthorizationContinuation = CheckedContinuation<CLAccuracyAuthorization?, Never>
27+
public typealias AccuracyAuthorizationContinuation = CheckedContinuation<CLAccuracyAuthorization?, Error>
2828
public typealias LocationOnceContinuation = CheckedContinuation<LocationUpdateEvent?, Error>
2929
public typealias LocationEnabledStream = AsyncStream<LocationEnabledEvent>
3030
public typealias LocationStream = AsyncStream<LocationUpdateEvent>
@@ -59,8 +59,7 @@ public final class AsyncLocationManager {
5959
// Though undocumented, `locationServicesEnabled()` must not be called from the main thread. Otherwise,
6060
// we get a runtime warning "This method can cause UI unresponsiveness if invoked on the main thread"
6161
// Therefore, we use `Task.detached` to ensure we're off the main thread.
62-
// Also, we force `try` as we expect no exceptions to be thrown from `locationServicesEnabled()`
63-
try! await Task.detached { CLLocationManager.locationServicesEnabled() }.value
62+
await Task.detached { CLLocationManager.locationServicesEnabled() }.value
6463
}
6564

6665
@available(watchOS 6.0, *)
@@ -133,7 +132,7 @@ public final class AsyncLocationManager {
133132
@available(*, deprecated, message: "Use new function requestPermission(with:)")
134133
@available(watchOS 7.0, *)
135134
public func requestAuthorizationWhenInUse() async -> CLAuthorizationStatus {
136-
let authorizationPerformer = RequestAuthorizationPerformer()
135+
let authorizationPerformer = RequestAuthorizationPerformer(currentStatus: getAuthorizationStatus())
137136
return await withTaskCancellationHandler(operation: {
138137
await withCheckedContinuation { continuation in
139138
let authorizationStatus = getAuthorizationStatus()
@@ -155,7 +154,7 @@ public final class AsyncLocationManager {
155154
@available(watchOS 7.0, *)
156155
@available(iOS 14, *)
157156
public func requestAuthorizationAlways() async -> CLAuthorizationStatus {
158-
let authorizationPerformer = RequestAuthorizationPerformer()
157+
let authorizationPerformer = RequestAuthorizationPerformer(currentStatus: getAuthorizationStatus())
159158
return await withTaskCancellationHandler(operation: {
160159
await withCheckedContinuation { continuation in
161160
#if os(macOS)
@@ -197,8 +196,8 @@ public final class AsyncLocationManager {
197196
}
198197

199198
@available(iOS 14, watchOS 7, *)
200-
public func requestTemporaryFullAccuracyAuthorization(purposeKey: String) async -> CLAccuracyAuthorization? {
201-
await locationPermissionTemporaryFullAccuracy(purposeKey: purposeKey)
199+
public func requestTemporaryFullAccuracyAuthorization(purposeKey: String) async throws -> CLAccuracyAuthorization? {
200+
try await locationPermissionTemporaryFullAccuracy(purposeKey: purposeKey)
202201
}
203202

204203
public func startUpdatingLocation() async -> LocationStream {
@@ -316,7 +315,7 @@ public final class AsyncLocationManager {
316315

317316
extension AsyncLocationManager {
318317
private func locationPermissionWhenInUse() async -> CLAuthorizationStatus {
319-
let authorizationPerformer = RequestAuthorizationPerformer()
318+
let authorizationPerformer = RequestAuthorizationPerformer(currentStatus: getAuthorizationStatus())
320319
return await withTaskCancellationHandler(operation: {
321320
await withCheckedContinuation { continuation in
322321
let authorizationStatus = getAuthorizationStatus()
@@ -334,7 +333,7 @@ extension AsyncLocationManager {
334333
}
335334

336335
private func locationPermissionAlways() async -> CLAuthorizationStatus {
337-
let authorizationPerformer = RequestAuthorizationPerformer()
336+
let authorizationPerformer = RequestAuthorizationPerformer(currentStatus: getAuthorizationStatus())
338337
return await withTaskCancellationHandler(operation: {
339338
await withCheckedContinuation { continuation in
340339
#if os(macOS)
@@ -361,24 +360,61 @@ extension AsyncLocationManager {
361360
}
362361

363362
@available(iOS 14, watchOS 7, *)
364-
private func locationPermissionTemporaryFullAccuracy(purposeKey: String) async -> CLAccuracyAuthorization? {
363+
private func locationPermissionTemporaryFullAccuracy(purposeKey: String) async throws -> CLAccuracyAuthorization? {
365364
let authorizationPerformer = RequestAccuracyAuthorizationPerformer()
366-
return await withTaskCancellationHandler(operation: {
367-
await withCheckedContinuation { continuation in
368-
if locationManager.authorizationStatus != .notDetermined && locationManager.accuracyAuthorization == .fullAccuracy {
369-
continuation.resume(with: .success(locationManager.accuracyAuthorization))
370-
} else if locationManager.authorizationStatus == .notDetermined {
365+
return try await withTaskCancellationHandler(operation: {
366+
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<CLAccuracyAuthorization?, Error>) in
367+
if locationManager.authorizationStatus == .notDetermined {
371368
continuation.resume(with: .success(nil))
369+
} else if locationManager.accuracyAuthorization == .fullAccuracy {
370+
continuation.resume(with: .success(locationManager.accuracyAuthorization))
372371
} else if !CLLocationManager.locationServicesEnabled() {
373372
continuation.resume(with: .success(nil))
374373
} else {
375374
authorizationPerformer.linkContinuation(continuation)
376375
proxyDelegate.addPerformer(authorizationPerformer)
377-
locationManager.requestTemporaryFullAccuracyAuthorization(withPurposeKey: purposeKey)
376+
locationManager.requestTemporaryFullAccuracyAuthorization(withPurposeKey: purposeKey) { error in
377+
if let error {
378+
continuation.resume(with: .failure(error))
379+
return
380+
}
381+
382+
// If the user chooses reduced accuracy, the didChangeAuthorization delegate method
383+
// will not called. So we must emulate that here.
384+
if self.locationManager.accuracyAuthorization == .reducedAccuracy {
385+
self.proxyDelegate.eventForMethodInvoked(
386+
.didChangeAccuracyAuthorization(authorization: self.locationManager.accuracyAuthorization)
387+
)
388+
}
389+
}
378390
}
379391
}
380392
}, onCancel: {
381393
proxyDelegate.cancel(for: authorizationPerformer.uniqueIdentifier)
382394
})
383395
}
384396
}
397+
398+
extension CLAuthorizationStatus: CustomStringConvertible {
399+
public var description: String {
400+
switch self {
401+
case .notDetermined: return ".notDetermined"
402+
case .restricted: return ".restricted"
403+
case .denied: return ".denied"
404+
case .authorizedWhenInUse: return ".authorizedWhenInUse"
405+
case .authorizedAlways: return ".authorisedAlways"
406+
@unknown default: return "unknown \(rawValue)"
407+
}
408+
}
409+
}
410+
411+
extension CLAccuracyAuthorization: CustomStringConvertible {
412+
public var description: String {
413+
switch self {
414+
case .fullAccuracy: return ".fullAccuracy"
415+
case .reducedAccuracy: return ".reducedAccuracy"
416+
@unknown default: return "unknown \(rawValue)"
417+
}
418+
}
419+
}
420+

Sources/AsyncLocationKit/Performers/AuthorizationPerformer.swift

+46-7
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,13 @@ import Foundation
2424
import CoreLocation.CLLocation
2525

2626
class RequestAuthorizationPerformer: AnyLocationPerformer {
27+
private let currentStatus: CLAuthorizationStatus
28+
private var applicationStateMonitor: ApplicationStateMonitor!
29+
30+
init(currentStatus: CLAuthorizationStatus) {
31+
self.currentStatus = currentStatus
32+
}
33+
2734
var typeIdentifier: ObjectIdentifier {
2835
return ObjectIdentifier(Self.self)
2936
}
@@ -38,8 +45,29 @@ class RequestAuthorizationPerformer: AnyLocationPerformer {
3845

3946
func linkContinuation(_ continuation: AuthotizationContinuation) {
4047
self.continuation = continuation
48+
Task { await start() }
4149
}
42-
50+
51+
func start() async {
52+
applicationStateMonitor = await ApplicationStateMonitor()
53+
await applicationStateMonitor.startMonitoringApplicationState()
54+
55+
// Wait a brief amount of time for the permission dialog to appear.
56+
Task { [applicationStateMonitor, currentStatus] in
57+
guard let applicationStateMonitor else { return }
58+
try await Task.sleep(nanoseconds: UInt64(Double(NSEC_PER_SEC) * 0.3))
59+
60+
if await !applicationStateMonitor.hasResignedActive {
61+
// We timed out waiting for the dialog to appear, so we can assume that the permission request
62+
// silently failed. We then emit the `currentStatus` to be returned to the caller.
63+
await applicationStateMonitor.stopMonitoringApplicationState()
64+
await MainActor.run {
65+
self.invokedMethod(event:.didChangeAuthorization(status: currentStatus))
66+
}
67+
}
68+
}
69+
}
70+
4371
func eventSupported(_ event: CoreLocationDelegateEvent) -> Bool {
4472
return eventsSupport.contains(event.rawEvent())
4573
}
@@ -48,15 +76,26 @@ class RequestAuthorizationPerformer: AnyLocationPerformer {
4876
switch event {
4977
case .didChangeAuthorization(let status):
5078
if status != .notDetermined {
51-
guard let continuation = continuation else { cancellabel?.cancel(for: self); return }
52-
continuation.resume(returning: status)
53-
self.continuation = nil
54-
cancellabel?.cancel(for: self)
79+
Task {
80+
if await applicationStateMonitor.hasResignedActive {
81+
_ = await applicationStateMonitor.hasBecomeActive()
82+
}
83+
84+
guard let continuation = continuation else { cancellabel?.cancel(for: self); return }
85+
continuation.resume(returning: status)
86+
self.continuation = nil
87+
cancellabel?.cancel(for: self)
88+
}
5589
}
5690
default:
5791
fatalError("Method can't be execute by this performer: \(String(describing: self)) for event: \(type(of: event))")
5892
}
5993
}
60-
61-
func cancelation() { }
94+
95+
func cancelation() {
96+
Task {
97+
await applicationStateMonitor.stopMonitoringApplicationState()
98+
}
99+
}
62100
}
101+

0 commit comments

Comments
 (0)