Skip to content

Commit

Permalink
background location access
Browse files Browse the repository at this point in the history
  • Loading branch information
JJTech0130 committed Mar 26, 2024
1 parent 7505007 commit 153a7b5
Show file tree
Hide file tree
Showing 5 changed files with 285 additions and 5 deletions.
16 changes: 14 additions & 2 deletions ValidationRelay.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
480989A92BB0C01A00B49AE8 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 480989A82BB0C01A00B49AE8 /* Preview Assets.xcassets */; };
480989B32BB0C19300B49AE8 /* absd.defs in Sources */ = {isa = PBXBuildFile; fileRef = 480989B22BB0C0F800B49AE8 /* absd.defs */; settings = {ATTRIBUTES = (Client, ); }; };
48D289112BB3579000EA9DEC /* libMobileGestalt.tbd in Frameworks */ = {isa = PBXBuildFile; fileRef = 48D289102BB3579000EA9DEC /* libMobileGestalt.tbd */; };
48D289132BB3743F00EA9DEC /* ApplicationMonitor.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48D289122BB3743F00EA9DEC /* ApplicationMonitor.swift */; };
48D289152BB3745000EA9DEC /* LocationManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48D289142BB3745000EA9DEC /* LocationManager.swift */; };
48D8604A2BB0DD110092EF79 /* ValidationData.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48D860492BB0DD110092EF79 /* ValidationData.swift */; };
48D8604D2BB1B8CC0092EF79 /* NWWebSocket in Frameworks */ = {isa = PBXBuildFile; productRef = 48D8604C2BB1B8CC0092EF79 /* NWWebSocket */; };
48D8604F2BB1B8EB0092EF79 /* Relay.swift in Sources */ = {isa = PBXBuildFile; fileRef = 48D8604E2BB1B8EB0092EF79 /* Relay.swift */; };
Expand All @@ -28,6 +30,9 @@
480989B22BB0C0F800B49AE8 /* absd.defs */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.mig; path = absd.defs; sourceTree = "<group>"; };
480989B42BB0D16400B49AE8 /* ValidationRelay.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ValidationRelay.entitlements; sourceTree = "<group>"; };
48D289102BB3579000EA9DEC /* libMobileGestalt.tbd */ = {isa = PBXFileReference; lastKnownFileType = "sourcecode.text-based-dylib-definition"; name = libMobileGestalt.tbd; path = usr/lib/libMobileGestalt.tbd; sourceTree = SDKROOT; };
48D289122BB3743F00EA9DEC /* ApplicationMonitor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApplicationMonitor.swift; sourceTree = "<group>"; };
48D289142BB3745000EA9DEC /* LocationManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationManager.swift; sourceTree = "<group>"; };
48D289162BB374FA00EA9DEC /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
48D860482BB0D7BF0092EF79 /* build.sh */ = {isa = PBXFileReference; lastKnownFileType = text.script.sh; path = build.sh; sourceTree = "<group>"; };
48D860492BB0DD110092EF79 /* ValidationData.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidationData.swift; sourceTree = "<group>"; };
48D8604E2BB1B8EB0092EF79 /* Relay.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Relay.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -67,6 +72,7 @@
480989A02BB0C01900B49AE8 /* ValidationRelay */ = {
isa = PBXGroup;
children = (
48D289162BB374FA00EA9DEC /* Info.plist */,
480989B42BB0D16400B49AE8 /* ValidationRelay.entitlements */,
480989B22BB0C0F800B49AE8 /* absd.defs */,
480989A12BB0C01900B49AE8 /* ValidationRelayApp.swift */,
Expand All @@ -76,6 +82,8 @@
480989AF2BB0C0AF00B49AE8 /* ValidationRelay-Bridging-Header.h */,
48D860492BB0DD110092EF79 /* ValidationData.swift */,
48D8604E2BB1B8EB0092EF79 /* Relay.swift */,
48D289122BB3743F00EA9DEC /* ApplicationMonitor.swift */,
48D289142BB3745000EA9DEC /* LocationManager.swift */,
);
path = ValidationRelay;
sourceTree = "<group>";
Expand Down Expand Up @@ -174,7 +182,9 @@
buildActionMask = 2147483647;
files = (
48D8604A2BB0DD110092EF79 /* ValidationData.swift in Sources */,
48D289132BB3743F00EA9DEC /* ApplicationMonitor.swift in Sources */,
480989B32BB0C19300B49AE8 /* absd.defs in Sources */,
48D289152BB3745000EA9DEC /* LocationManager.swift in Sources */,
480989A42BB0C01900B49AE8 /* ContentView.swift in Sources */,
480989A22BB0C01900B49AE8 /* ValidationRelayApp.swift in Sources */,
48D8604F2BB1B8EB0092EF79 /* Relay.swift in Sources */,
Expand Down Expand Up @@ -315,6 +325,7 @@
DEVELOPMENT_ASSET_PATHS = "\"ValidationRelay/Preview Content\"";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ValidationRelay/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
Expand All @@ -326,7 +337,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.1;
MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = dev.jjtech.experiments.ValidationRelay;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
Expand All @@ -351,6 +362,7 @@
DEVELOPMENT_ASSET_PATHS = "\"ValidationRelay/Preview Content\"";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = ValidationRelay/Info.plist;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.developer-tools";
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
Expand All @@ -362,7 +374,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.1;
MARKETING_VERSION = 1.2;
PRODUCT_BUNDLE_IDENTIFIER = dev.jjtech.experiments.ValidationRelay;
PRODUCT_NAME = "$(TARGET_NAME)";
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
Expand Down
106 changes: 106 additions & 0 deletions ValidationRelay/ApplicationMonitor.swift
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)
}
}
8 changes: 5 additions & 3 deletions ValidationRelay/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ struct ContentView: View {
//private var relayConnectionManager: RelayConnectionManager? = nil
@ObservedObject var relayConnectionManager: RelayConnectionManager

var appMon = ApplicationMonitor()

// init() {
// relayConnectionManager = RelayConnectionManager(registrationCodeBinding: $registrationCode, connectionStatusMessageBinding: $connectionStatusMessage)
// }
Expand All @@ -28,6 +30,7 @@ struct ContentView: View {
if wantRelayConnected {
relayConnectionManager.connect(getCurrentRelayURL())
}
appMon.start()
}

func getCurrentRelayURL() -> URL {
Expand Down Expand Up @@ -66,9 +69,8 @@ struct ContentView: View {
Text(relayConnectionManager.connectionStatusMessage)
}
Section {
// TODO: Actually support running in the background
Toggle("Run in Background", isOn: .constant(false))
.disabled(true)
//Toggle("Run in Background", isOn: .constant(false))
// .disabled(true)
Picker("Relay", selection: $selectedRelay) {
Text("Beeper").tag("Beeper")
//Text("pypush").tag("pypush")
Expand Down
17 changes: 17 additions & 0 deletions ValidationRelay/Info.plist
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>
143 changes: 143 additions & 0 deletions ValidationRelay/LocationManager.swift
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)
}
}

0 comments on commit 153a7b5

Please sign in to comment.