Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(location): AWSLocationTracker client #3314

Merged
merged 2 commits into from
Dec 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AWSLocation.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@ Pod::Spec.new do |s|
:tag => s.version}
s.requires_arc = true
s.dependency 'AWSCore', '2.20.0'
s.source_files = 'AWSLocation/*.{h,m}'
s.source_files = 'AWSLocation/*.{h,m}', 'AWSLocation/AWSLocationTracker/**/*.swift'
end
238 changes: 238 additions & 0 deletions AWSLocation/AWSLocationTracker/AWSLocationTracker.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,238 @@
//
// Copyright 2010-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License").
// You may not use this file except in compliance with the License.
// A copy of the License is located at
//
// http://aws.amazon.com/apache2.0
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.
//

import CoreLocation

/// Monitor your device's location and periodically send the tracked coordinates to the Amazon Location Service.
public class AWSLocationTracker {
var backgroundEmitTaskId: String {
get {
return "com.amazonaws.AWSLocationTrackerEmit.\(trackerName)"
}
}
var backgroundRetrieveTaskId: String {
get {
return "com.amazonaws.AWSLocationTrackerRetrieve.\(trackerName)"
}
}

private let lock: NSLock = NSLock()
let operationQueue: OperationQueue

let trackerName: String
let locationService: AWSLocationBehavior
let locationsQueue: AtomicValue<[CLLocation]> = AtomicValue(initialValue: [])
var trackerOptions: TrackerOptions?
var trackingEnabled: AtomicValue<Bool> = AtomicValue(initialValue: false)

weak var delegate: AWSLocationTrackerDelegate?
var listener: ((TrackingListener) -> Void)?
var retrieveLocationsTimer: DispatchSourceTimer?
var emitLocationsTimer: DispatchSourceTimer?

/// Create a new tracker instance to retrieve and send location data to Amazon Location Servies.
/// By calling this method, make sure you call `interceptLocationsRetrieved(locations)` from the periodic
/// locations retrieved back to the tracker.
///
/// - Parameters:
/// - trackerName: The name of your tracker resource
/// - region: The region of your tracker resource
/// - credentialsProvider: Your auth provider - most likely `AWSMobileClient.default()`
public convenience init(trackerName: String,
region: AWSRegionType,
credentialsProvider: AWSCredentialsProvider) {
guard let config = AWSServiceConfiguration(region: region,
credentialsProvider: credentialsProvider) else {
fatalError("Could not create `AWSServiceConfiguration` with \(region) and \(credentialsProvider)")
}

AWSLocation.remove(forKey: trackerName)
AWSLocation.register(with: config, forKey: trackerName)

self.init(trackerName: trackerName,
locationService: AWSLocationAdapter(location: AWSLocation(forKey: trackerName)))
}

/// Internal constructor for testing
init(trackerName: String,
locationService: AWSLocationBehavior) {
self.trackerName = trackerName
self.locationService = locationService
self.operationQueue = OperationQueue()
operationQueue.name = "com.amazonaws.AWSLocationTrackerOperationQueue"
operationQueue.maxConcurrentOperationCount = 1
}

/// Start monitoring your device's location and sending the tracked coordinates to Amazon Location Service.
///
/// - Parameters:
/// - delegate: Configure the delegate for AWSLocationTracker
/// - options: Configure details of how tracking will work.
/// - listener: Contains the various callback methods to listen to the different events which will be emitted.
public func startTracking(delegate: AWSLocationTrackerDelegate,
options: TrackerOptions? = nil,
listener: ((TrackingListener) -> Void)? = nil) -> Result<Void, TrackingError> {
lock.lock()
defer {
lock.unlock()
}
if trackingEnabled.get() {
return .failure(.init(errorType: .trackerAlreadyStarted,
message: "Tracker for \(trackerName) has already started.",
recoverySuggestion: "Call `stopTracking` before calling `startTracking` again."))

}

self.trackerOptions = getDefaultTrackerOptions(options: options)
self.delegate = delegate
self.listener = listener
self.setUpRetrieveLocationsTimer()
self.setUpEmitLocationsTimer()
self.trackingEnabled.set(true)
return .success(())
}

/// Call this method from `locationManager(_:didUpdateLocations)` to pass the location data back to the tracker.
///
/// - Parameters:
/// - locations: List of locations to track
public func interceptLocationsRetrieved(_ locations: [CLLocation]) {
guard trackingEnabled.get() else {
AWSLocationTrackerLogger.info("Locations intercepted, do nothing. Tracking is stopped.")
return
}
AWSLocationTrackerLogger.info("Adding \(locations.count) locations to the queue.")
locationsQueue.append(contentsOf: locations)
}

/// True if this tracker instance is currently monitoring and sending the device's location. False otherwise.
/// - Returns: if tracking instance is currently monitoring and sending the device's location
public func isTracking() -> Bool {
return trackingEnabled.get()
}

/// Stop recording and sending the device's location. You can start tracking again with new options if you want.
public func stopTracking() {
lock.lock()
defer {
lock.unlock()
}

if let listener = listener {
listener(.onStop)
}

reset()
}

// MARK: - Internal methods

func getDefaultTrackerOptions(options: TrackerOptions?,
userDefaults: UserDefaults = UserDefaults.init()) -> TrackerOptions {
guard let trackerOptions = options else {
// No options passed in, create a new one with the default values
return TrackerOptions(
customDeviceId: tryGetDeviceId(for: trackerName, userDefaults: userDefaults),
retrieveLocationFrequency: TrackerOptions.defaultRetrieveLocationFrequency,
emitLocationFrequency: TrackerOptions.defaultEmitLocationFrequency)
}

guard trackerOptions.customDeviceId == nil else {
// `customDeviceId` was passed in, use the passed in options
return trackerOptions
}

// `customDeviceId` is to be generated, reuse existing retrieve and emit location frequenceies
return TrackerOptions(customDeviceId: tryGetDeviceId(for: trackerName,
userDefaults: userDefaults),
retrieveLocationFrequency: trackerOptions.retrieveLocationFrequency,
emitLocationFrequency: trackerOptions.emitLocationFrequency)
}

func tryGetDeviceId(for trackerName: String,
userDefaults: UserDefaults = UserDefaults.init()) -> String {
let key = "com.amazonaws.AWSLocationTrackerKey.\(trackerName)"
guard let deviceId = userDefaults.string(forKey: key) else {
let deviceId = UUID.init().uuidString
AWSLocationTrackerLogger.verbose("Creating new deviceId for \(trackerName): \(deviceId)")
userDefaults.setValue(deviceId, forKey: key)
return deviceId
}
AWSLocationTrackerLogger.verbose("Reusing existing deviceId for \(trackerName): \(deviceId)")
return deviceId
}

func setUpRetrieveLocationsTimer() {
guard let options = trackerOptions else {
fatalError("TrackingOptions is nil")
}
retrieveLocationsTimer = RepeatingTimer.createRepeatingTimer(
timeInterval: options.retrieveLocationFrequency,
eventHandler: { [weak self] in
AWSLocationTrackerLogger.verbose("RetrieveLocationsTimer triggered, call delegate.requestLocation()")
self?.delegate?.requestLocation()
})
retrieveLocationsTimer?.resume()
}

func setUpEmitLocationsTimer() {
guard let options = self.trackerOptions, let deviceId = options.customDeviceId else {
fatalError("TrackingOptions is nil")
}
emitLocationsTimer = RepeatingTimer.createRepeatingTimer(
timeInterval: options.emitLocationFrequency,
eventHandler: { [weak self] in
guard let self = self else {
return
}

AWSLocationTrackerLogger.verbose("EmitLocationsTimer triggered, attempt to send locations to Amazon Location Service")
let operation = EmitLocationsOperation(trackerName: self.trackerName,
deviceId: deviceId,
locationsQueue: self.locationsQueue,
locationService: self.locationService,
listener: self.listener)
self.operationQueue.addOperation(operation)

})
emitLocationsTimer?.resume()
}

private func reset() {
if delegate != nil {
delegate = nil
}
if listener != nil {
listener = nil
}

if retrieveLocationsTimer != nil {
retrieveLocationsTimer?.setEventHandler {}
retrieveLocationsTimer = nil
}

if emitLocationsTimer != nil {
emitLocationsTimer?.setEventHandler {}
emitLocationsTimer = nil
}

locationsQueue.set([])
trackingEnabled.set(false)
}

deinit {
reset()
}
}
21 changes: 21 additions & 0 deletions AWSLocation/AWSLocationTracker/AWSLocationTrackerDelegate.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
//
// Copyright 2010-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License").
// You may not use this file except in compliance with the License.
// A copy of the License is located at
//
// http://aws.amazon.com/apache2.0
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.
//

public protocol AWSLocationTrackerDelegate: AnyObject {

/// Invoked periodically on the default or specified location retrieval interval. When using `CLLocationManager`,
/// call `requestLocation()` to request the location from Core Location services.
func requestLocation()
}
31 changes: 31 additions & 0 deletions AWSLocation/AWSLocationTracker/Dependency/AWSLocationAdapter.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
//
// Copyright 2010-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License").
// You may not use this file except in compliance with the License.
// A copy of the License is located at
//
// http://aws.amazon.com/apache2.0
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.
//

class AWSLocationAdapter: AWSLocationBehavior {

let location: AWSLocation

init(location: AWSLocation) {
self.location = location
}

func batchUpdateDevicePosition(_ request: AWSLocationBatchUpdateDevicePositionRequest,
completionHandler: BatchUpdateDevicePositionCompletionHandler?) {
location.batchUpdateDevicePosition(request) { (response, error) in
completionHandler?((response, error))
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
//
// Copyright 2010-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License").
// You may not use this file except in compliance with the License.
// A copy of the License is located at
//
// http://aws.amazon.com/apache2.0
//
// or in the "license" file accompanying this file. This file is distributed
// on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
// express or implied. See the License for the specific language governing
// permissions and limitations under the License.
//

protocol AWSLocationBehavior {

typealias BatchUpdateDevicePositionCompletionEvent = (AWSLocationBatchUpdateDevicePositionResponse?, Error?)
typealias BatchUpdateDevicePositionCompletionHandler = ((BatchUpdateDevicePositionCompletionEvent) -> Void)

func batchUpdateDevicePosition(_ request: AWSLocationBatchUpdateDevicePositionRequest,
completionHandler: BatchUpdateDevicePositionCompletionHandler?)
}
Loading