Skip to content

Commit

Permalink
Merge pull request #31 from humblehacker/30-fix-stuck-authorization-r…
Browse files Browse the repository at this point in the history
…equest

fix stuck authorization request
  • Loading branch information
gre4ixin authored Dec 5, 2022
2 parents 4018f64 + 5d21789 commit d6885b7
Show file tree
Hide file tree
Showing 3 changed files with 196 additions and 23 deletions.
98 changes: 98 additions & 0 deletions Sources/AsyncLocationKit/ApplicationStateMonitor.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
//
// ApplicationStateMonitor.swift
// AsyncLocationKit
//
// Created by David Whetstone on 11/28/22.
//

import Foundation
import UIKit

@MainActor
class ApplicationStateMonitor {
private(set) var hasResignedActive = false

private var hasResignedActiveTask: Task<Void, Never>?
private var hasBecomeActiveTask: Task<Void, Never>?

func startMonitoringApplicationState() {
guard #available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) else { return }
startMonitoringHasResignedActive()
startMonitoringHasBecomeActive()
}

func stopMonitoringApplicationState() {
guard #available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) else { return }
stopMonitoringHasResignedActive()
stopMonitoringHasBecomeActive()
}

func hasResignedActive() async -> Bool {
guard #available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) else { return false }
var iter = hasResignedActiveSequence.makeAsyncIterator()
return await iter.next() != nil
}

func hasBecomeActive() async -> Bool {
guard #available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) else { return false }
var iter = hasBecomeActiveSequence.makeAsyncIterator()
return await iter.next() != nil
}

@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
private func startMonitoringHasResignedActive() {
guard hasResignedActiveTask == nil else { return }

hasResignedActiveTask = Task {
for await _ in self.hasResignedActiveSequence {
if Task.isCancelled { break }
self.hasResignedActive = true
self.stopMonitoringHasResignedActive()
}
}
}

@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
private func startMonitoringHasBecomeActive() {
guard hasBecomeActiveTask == nil else { return }

hasBecomeActiveTask = Task {
for await _ in self.hasBecomeActiveSequence {
if Task.isCancelled { break }
self.stopMonitoringHasBecomeActive()
}
}
}

private func stopMonitoringHasResignedActive() {
hasResignedActiveTask?.cancel()
hasResignedActiveTask = nil
}

private func stopMonitoringHasBecomeActive() {
hasBecomeActiveTask?.cancel()
hasBecomeActiveTask = nil
}

@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
private var hasResignedActiveSequence: AsyncMapSequence<NotificationCenter.Notifications, Bool> {
_hasResignedActiveSequence as! AsyncMapSequence<NotificationCenter.Notifications, Bool>
}

@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
private var hasBecomeActiveSequence: AsyncMapSequence<NotificationCenter.Notifications, Bool> {
_hasBecomeActiveSequence as! AsyncMapSequence<NotificationCenter.Notifications, Bool>
}

// We unfortunately need these backing variables since properties cannot be declared conditionally available

private var _hasResignedActiveSequence: Any? = {
guard #available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) else { return nil }
return NotificationCenter.default.notifications(named: UIApplication.willResignActiveNotification).map { _ in true }
}()

private var _hasBecomeActiveSequence: Any? = {
guard #available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) else { return nil }
return NotificationCenter.default.notifications(named: UIApplication.didBecomeActiveNotification).map { _ in true }
}()
}
68 changes: 52 additions & 16 deletions Sources/AsyncLocationKit/AsyncLocationManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import Foundation
import CoreLocation

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

@available(watchOS 6.0, *)
Expand Down Expand Up @@ -133,7 +132,7 @@ public final class AsyncLocationManager {
@available(*, deprecated, message: "Use new function requestPermission(with:)")
@available(watchOS 7.0, *)
public func requestAuthorizationWhenInUse() async -> CLAuthorizationStatus {
let authorizationPerformer = RequestAuthorizationPerformer()
let authorizationPerformer = RequestAuthorizationPerformer(currentStatus: getAuthorizationStatus())
return await withTaskCancellationHandler(operation: {
await withCheckedContinuation { continuation in
let authorizationStatus = getAuthorizationStatus()
Expand All @@ -155,7 +154,7 @@ public final class AsyncLocationManager {
@available(watchOS 7.0, *)
@available(iOS 14, *)
public func requestAuthorizationAlways() async -> CLAuthorizationStatus {
let authorizationPerformer = RequestAuthorizationPerformer()
let authorizationPerformer = RequestAuthorizationPerformer(currentStatus: getAuthorizationStatus())
return await withTaskCancellationHandler(operation: {
await withCheckedContinuation { continuation in
#if os(macOS)
Expand Down Expand Up @@ -197,8 +196,8 @@ public final class AsyncLocationManager {
}

@available(iOS 14, watchOS 7, *)
public func requestTemporaryFullAccuracyAuthorization(purposeKey: String) async -> CLAccuracyAuthorization? {
await locationPermissionTemporaryFullAccuracy(purposeKey: purposeKey)
public func requestTemporaryFullAccuracyAuthorization(purposeKey: String) async throws -> CLAccuracyAuthorization? {
try await locationPermissionTemporaryFullAccuracy(purposeKey: purposeKey)
}

public func startUpdatingLocation() async -> LocationStream {
Expand Down Expand Up @@ -316,7 +315,7 @@ public final class AsyncLocationManager {

extension AsyncLocationManager {
private func locationPermissionWhenInUse() async -> CLAuthorizationStatus {
let authorizationPerformer = RequestAuthorizationPerformer()
let authorizationPerformer = RequestAuthorizationPerformer(currentStatus: getAuthorizationStatus())
return await withTaskCancellationHandler(operation: {
await withCheckedContinuation { continuation in
let authorizationStatus = getAuthorizationStatus()
Expand All @@ -334,7 +333,7 @@ extension AsyncLocationManager {
}

private func locationPermissionAlways() async -> CLAuthorizationStatus {
let authorizationPerformer = RequestAuthorizationPerformer()
let authorizationPerformer = RequestAuthorizationPerformer(currentStatus: getAuthorizationStatus())
return await withTaskCancellationHandler(operation: {
await withCheckedContinuation { continuation in
#if os(macOS)
Expand All @@ -361,24 +360,61 @@ extension AsyncLocationManager {
}

@available(iOS 14, watchOS 7, *)
private func locationPermissionTemporaryFullAccuracy(purposeKey: String) async -> CLAccuracyAuthorization? {
private func locationPermissionTemporaryFullAccuracy(purposeKey: String) async throws -> CLAccuracyAuthorization? {
let authorizationPerformer = RequestAccuracyAuthorizationPerformer()
return await withTaskCancellationHandler(operation: {
await withCheckedContinuation { continuation in
if locationManager.authorizationStatus != .notDetermined && locationManager.accuracyAuthorization == .fullAccuracy {
continuation.resume(with: .success(locationManager.accuracyAuthorization))
} else if locationManager.authorizationStatus == .notDetermined {
return try await withTaskCancellationHandler(operation: {
try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<CLAccuracyAuthorization?, Error>) in
if locationManager.authorizationStatus == .notDetermined {
continuation.resume(with: .success(nil))
} else if locationManager.accuracyAuthorization == .fullAccuracy {
continuation.resume(with: .success(locationManager.accuracyAuthorization))
} else if !CLLocationManager.locationServicesEnabled() {
continuation.resume(with: .success(nil))
} else {
authorizationPerformer.linkContinuation(continuation)
proxyDelegate.addPerformer(authorizationPerformer)
locationManager.requestTemporaryFullAccuracyAuthorization(withPurposeKey: purposeKey)
locationManager.requestTemporaryFullAccuracyAuthorization(withPurposeKey: purposeKey) { error in
if let error {
continuation.resume(with: .failure(error))
return
}

// If the user chooses reduced accuracy, the didChangeAuthorization delegate method
// will not called. So we must emulate that here.
if self.locationManager.accuracyAuthorization == .reducedAccuracy {
self.proxyDelegate.eventForMethodInvoked(
.didChangeAccuracyAuthorization(authorization: self.locationManager.accuracyAuthorization)
)
}
}
}
}
}, onCancel: {
proxyDelegate.cancel(for: authorizationPerformer.uniqueIdentifier)
})
}
}

extension CLAuthorizationStatus: CustomStringConvertible {
public var description: String {
switch self {
case .notDetermined: return ".notDetermined"
case .restricted: return ".restricted"
case .denied: return ".denied"
case .authorizedWhenInUse: return ".authorizedWhenInUse"
case .authorizedAlways: return ".authorisedAlways"
@unknown default: return "unknown \(rawValue)"
}
}
}

extension CLAccuracyAuthorization: CustomStringConvertible {
public var description: String {
switch self {
case .fullAccuracy: return ".fullAccuracy"
case .reducedAccuracy: return ".reducedAccuracy"
@unknown default: return "unknown \(rawValue)"
}
}
}

53 changes: 46 additions & 7 deletions Sources/AsyncLocationKit/Performers/AuthorizationPerformer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,13 @@ import Foundation
import CoreLocation.CLLocation

class RequestAuthorizationPerformer: AnyLocationPerformer {
private let currentStatus: CLAuthorizationStatus
private var applicationStateMonitor: ApplicationStateMonitor!

init(currentStatus: CLAuthorizationStatus) {
self.currentStatus = currentStatus
}

var typeIdentifier: ObjectIdentifier {
return ObjectIdentifier(Self.self)
}
Expand All @@ -38,8 +45,29 @@ class RequestAuthorizationPerformer: AnyLocationPerformer {

func linkContinuation(_ continuation: AuthotizationContinuation) {
self.continuation = continuation
Task { await start() }
}


func start() async {
applicationStateMonitor = await ApplicationStateMonitor()
await applicationStateMonitor.startMonitoringApplicationState()

// Wait a brief amount of time for the permission dialog to appear.
Task { [applicationStateMonitor, currentStatus] in
guard let applicationStateMonitor else { return }
try await Task.sleep(nanoseconds: UInt64(Double(NSEC_PER_SEC) * 0.3))

if await !applicationStateMonitor.hasResignedActive {
// We timed out waiting for the dialog to appear, so we can assume that the permission request
// silently failed. We then emit the `currentStatus` to be returned to the caller.
await applicationStateMonitor.stopMonitoringApplicationState()
await MainActor.run {
self.invokedMethod(event:.didChangeAuthorization(status: currentStatus))
}
}
}
}

func eventSupported(_ event: CoreLocationDelegateEvent) -> Bool {
return eventsSupport.contains(event.rawEvent())
}
Expand All @@ -48,15 +76,26 @@ class RequestAuthorizationPerformer: AnyLocationPerformer {
switch event {
case .didChangeAuthorization(let status):
if status != .notDetermined {
guard let continuation = continuation else { cancellabel?.cancel(for: self); return }
continuation.resume(returning: status)
self.continuation = nil
cancellabel?.cancel(for: self)
Task {
if await applicationStateMonitor.hasResignedActive {
_ = await applicationStateMonitor.hasBecomeActive()
}

guard let continuation = continuation else { cancellabel?.cancel(for: self); return }
continuation.resume(returning: status)
self.continuation = nil
cancellabel?.cancel(for: self)
}
}
default:
fatalError("Method can't be execute by this performer: \(String(describing: self)) for event: \(type(of: event))")
}
}

func cancelation() { }

func cancelation() {
Task {
await applicationStateMonitor.stopMonitoringApplicationState()
}
}
}

0 comments on commit d6885b7

Please sign in to comment.