-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
7505007
commit 153a7b5
Showing
5 changed files
with
285 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
import UIKit | ||
import AVFoundation | ||
import UserNotifications | ||
import Combine | ||
|
||
private enum UserNotification: String | ||
{ | ||
case appStoppedRunning = "com.rileytestut.Clip.AppStoppedRunning" | ||
} | ||
|
||
private extension CFNotificationName | ||
{ | ||
static let altstoreRequestAppState: CFNotificationName = CFNotificationName("com.altstore.RequestAppState.com.rileytestut.Clip" as CFString) | ||
static let altstoreAppIsRunning: CFNotificationName = CFNotificationName("com.altstore.AppState.Running.com.rileytestut.Clip" as CFString) | ||
} | ||
|
||
private let ReceivedApplicationState: @convention(c) (CFNotificationCenter?, UnsafeMutableRawPointer?, CFNotificationName?, UnsafeRawPointer?, CFDictionary?) -> Void = | ||
{ (center, observer, name, object, userInfo) in | ||
ApplicationMonitor.shared.receivedApplicationStateRequest() | ||
} | ||
|
||
class ApplicationMonitor | ||
{ | ||
static let shared = ApplicationMonitor() | ||
|
||
let locationManager = LocationManager() | ||
|
||
private(set) var isMonitoring = false | ||
|
||
private var backgroundTaskID: UIBackgroundTaskIdentifier? | ||
} | ||
|
||
extension ApplicationMonitor | ||
{ | ||
func start() | ||
{ | ||
guard !self.isMonitoring else { return } | ||
self.isMonitoring = true | ||
print("Monitoring app background") | ||
|
||
self.cancelApplicationQuitNotification() // Cancel any notifications from a previous launch. | ||
self.scheduleApplicationQuitNotification() | ||
|
||
self.locationManager.start() | ||
self.registerForNotifications() | ||
} | ||
|
||
func stop() { | ||
self.cancelApplicationQuitNotification() | ||
} | ||
} | ||
|
||
private extension ApplicationMonitor | ||
{ | ||
func registerForNotifications() | ||
{ | ||
let center = CFNotificationCenterGetDarwinNotifyCenter() | ||
CFNotificationCenterAddObserver(center, nil, ReceivedApplicationState, CFNotificationName.altstoreRequestAppState.rawValue, nil, .deliverImmediately) | ||
} | ||
|
||
func scheduleApplicationQuitNotification() | ||
{ | ||
let delay = 5 as TimeInterval | ||
|
||
let content = UNMutableNotificationContent() | ||
content.title = NSLocalizedString("App Stopped Running", comment: "") | ||
content.body = NSLocalizedString("Tap this notification to resume ValidationRelay", comment: "") | ||
|
||
let trigger = UNTimeIntervalNotificationTrigger(timeInterval: delay + 1, repeats: false) | ||
|
||
let request = UNNotificationRequest(identifier: UserNotification.appStoppedRunning.rawValue, content: content, trigger: trigger) | ||
UNUserNotificationCenter.current().add(request) | ||
|
||
DispatchQueue.global().asyncAfter(deadline: .now() + delay) { | ||
// If app is still running at this point, we schedule another notification with same identifier. | ||
// This prevents the currently scheduled notification from displaying, and starts another countdown timer. | ||
self.scheduleApplicationQuitNotification() | ||
} | ||
} | ||
|
||
func cancelApplicationQuitNotification() | ||
{ | ||
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [UserNotification.appStoppedRunning.rawValue]) | ||
} | ||
|
||
func sendNotification(title: String, message: String) | ||
{ | ||
let content = UNMutableNotificationContent() | ||
content.title = title | ||
content.body = message | ||
|
||
let request = UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil) | ||
UNUserNotificationCenter.current().add(request) | ||
} | ||
} | ||
|
||
private extension ApplicationMonitor | ||
{ | ||
func receivedApplicationStateRequest() | ||
{ | ||
guard UIApplication.shared.applicationState != .background else { return } | ||
|
||
let center = CFNotificationCenterGetDarwinNotifyCenter() | ||
CFNotificationCenterPostNotification(center!, CFNotificationName(CFNotificationName.altstoreAppIsRunning.rawValue), nil, nil, true) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | ||
<plist version="1.0"> | ||
<dict> | ||
<key>NSLocationWhenInUseUsageDescription</key> | ||
<string>Keep ValidationRelay alive in the background</string> | ||
<key>NSLocationAlwaysUsageDescription</key> | ||
<string>Keep ValidationRelay alive in the background</string> | ||
<key>NSLocationAlwaysAndWhenInUseUsageDescription</key> | ||
<string>Keep ValidationRelay alive in the background</string> | ||
<key>UIBackgroundModes</key> | ||
<array> | ||
<string>location</string> | ||
<string>processing</string> | ||
</array> | ||
</dict> | ||
</plist> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
// | ||
// LocationManager.swift | ||
// Clip | ||
// | ||
// Created by Riley Testut on 11/6/20. | ||
// Copyright © 2020 Riley Testut. All rights reserved. | ||
// | ||
|
||
// taken from https://github.com/rileytestut/Clip. many thanks | ||
|
||
import CoreLocation | ||
import Combine | ||
import UIKit | ||
|
||
extension LocationManager | ||
{ | ||
typealias Status = Result<Void, Swift.Error> | ||
|
||
enum Error: LocalizedError, RecoverableError | ||
{ | ||
case requiresAlwaysAuthorization | ||
|
||
var failureReason: String? { | ||
switch self | ||
{ | ||
case .requiresAlwaysAuthorization: return NSLocalizedString("To run in the background, ValidationRelay requires “Always” location permission.", comment: "") | ||
} | ||
} | ||
|
||
var recoverySuggestion: String? { | ||
switch self | ||
{ | ||
case .requiresAlwaysAuthorization: return NSLocalizedString("Please grant ValidationRelay “Always” location permission in Settings so it can run in the background indefinitely.", comment: "") | ||
} | ||
} | ||
|
||
var recoveryOptions: [String] { | ||
switch self | ||
{ | ||
case .requiresAlwaysAuthorization: return [NSLocalizedString("Open Settings", comment: "")] | ||
} | ||
} | ||
|
||
func attemptRecovery(optionIndex recoveryOptionIndex: Int) -> Bool | ||
{ | ||
return false | ||
} | ||
|
||
func attemptRecovery(optionIndex recoveryOptionIndex: Int, resultHandler handler: @escaping (Bool) -> Void) | ||
{ | ||
switch self | ||
{ | ||
case .requiresAlwaysAuthorization: | ||
let openURL = URL(string: UIApplication.openSettingsURLString)! | ||
UIApplication.shared.open(openURL, options: [:], completionHandler: handler) | ||
} | ||
} | ||
} | ||
} | ||
|
||
class LocationManager: NSObject, ObservableObject | ||
{ | ||
var status: Status? = nil | ||
|
||
private let locationManager: CLLocationManager | ||
|
||
override init() | ||
{ | ||
self.locationManager = CLLocationManager() | ||
self.locationManager.distanceFilter = CLLocationDistanceMax | ||
self.locationManager.pausesLocationUpdatesAutomatically = false | ||
self.locationManager.allowsBackgroundLocationUpdates = true | ||
|
||
if #available(iOS 14.0, *) | ||
{ | ||
self.locationManager.desiredAccuracy = kCLLocationAccuracyReduced | ||
} | ||
else | ||
{ | ||
self.locationManager.desiredAccuracy = kCLLocationAccuracyThreeKilometers | ||
} | ||
|
||
super.init() | ||
|
||
self.locationManager.delegate = self | ||
} | ||
|
||
func start() | ||
{ | ||
switch self.status | ||
{ | ||
case .success: return | ||
case .failure, nil: break | ||
} | ||
print("Location permissions: \(locationManager.authorizationStatus)") | ||
if locationManager.authorizationStatus == .notDetermined || locationManager.authorizationStatus == .authorizedWhenInUse | ||
{ | ||
self.locationManager.requestAlwaysAuthorization() | ||
return | ||
} | ||
|
||
self.locationManager.startUpdatingLocation() | ||
} | ||
|
||
func stop() | ||
{ | ||
self.locationManager.stopUpdatingLocation() | ||
self.status = nil | ||
} | ||
} | ||
|
||
|
||
extension LocationManager: CLLocationManagerDelegate | ||
{ | ||
func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) | ||
{ | ||
switch status | ||
{ | ||
case .notDetermined: break | ||
case .restricted, .denied, .authorizedWhenInUse: self.status = .failure(Error.requiresAlwaysAuthorization) | ||
case .authorizedAlways: self.start() | ||
@unknown default: break | ||
} | ||
} | ||
|
||
func locationManager(_ manager: CLLocationManager, didUpdateLocations locations: [CLLocation]) | ||
{ | ||
self.status = .success(()) | ||
} | ||
|
||
func locationManager(_ manager: CLLocationManager, didFailWithError error: Swift.Error) | ||
{ | ||
if let error = error as? CLError | ||
{ | ||
guard error.code != .denied else { | ||
self.status = .failure(Error.requiresAlwaysAuthorization) | ||
return | ||
} | ||
} | ||
|
||
self.status = .failure(error) | ||
} | ||
} |