Skip to content
Open
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
331 changes: 319 additions & 12 deletions LoopFollow.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "1600"
version = "1.7">
<BuildAction
parallelizeBuildables = "YES"
buildImplicitDependencies = "YES">
<BuildActionEntries>
<BuildActionEntry
buildForTesting = "YES"
buildForRunning = "YES"
buildForProfiling = "YES"
buildForArchiving = "YES"
buildForAnalyzing = "YES">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BB01000000000003000000AA"
BuildableName = "LoopFollowWatch.app"
BlueprintName = "LoopFollowWatch"
ReferencedContainer = "container:LoopFollow.xcodeproj">
</BuildableReference>
</BuildActionEntry>
</BuildActionEntries>
</BuildAction>
<TestAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
shouldUseLaunchSchemeArgsEnv = "YES"
shouldAutocreateTestPlan = "YES">
</TestAction>
<LaunchAction
buildConfiguration = "Debug"
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
launchStyle = "0"
useCustomWorkingDirectory = "NO"
ignoresPersistentStateOnLaunch = "NO"
debugDocumentVersioning = "YES"
debugServiceExtension = "internal"
allowLocationSimulation = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BB01000000000003000000AA"
BuildableName = "LoopFollowWatch.app"
BlueprintName = "LoopFollowWatch"
ReferencedContainer = "container:LoopFollow.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</LaunchAction>
<ProfileAction
buildConfiguration = "Release"
shouldUseLaunchSchemeArgsEnv = "YES"
savedToolIdentifier = ""
useCustomWorkingDirectory = "NO"
debugDocumentVersioning = "YES">
<BuildableProductRunnable
runnableDebuggingMode = "0">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "BB01000000000003000000AA"
BuildableName = "LoopFollowWatch.app"
BlueprintName = "LoopFollowWatch"
ReferencedContainer = "container:LoopFollow.xcodeproj">
</BuildableReference>
</BuildableProductRunnable>
</ProfileAction>
<AnalyzeAction
buildConfiguration = "Debug">
</AnalyzeAction>
<ArchiveAction
buildConfiguration = "Release"
revealArchiveInOrganizer = "YES">
</ArchiveAction>
</Scheme>
3 changes: 3 additions & 0 deletions LoopFollow/Application/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import CoreData
import EventKit
import UIKit
import UserNotifications
import WatchConnectivity

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
Expand Down Expand Up @@ -70,6 +71,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
Storage.shared.needsBFUReload = bfu
LogManager.shared.log(category: .general, message: "BFU check: isProtectedDataAvailable=\(!bfu), needsBFUReload=\(bfu)")

PhoneSessionManager.shared.startSession()

return true
}

Expand Down
2 changes: 2 additions & 0 deletions LoopFollow/Controllers/Nightscout/BGData.swift
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@ extension MainViewController {
)
}
Storage.shared.lastBGChecked.value = Date()

PhoneSessionManager.shared.sendConfig()
}
}
}
2 changes: 2 additions & 0 deletions LoopFollow/Nightscout/NightscoutSettingsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ class NightscoutSettingsViewModel: ObservableObject {
if newValue != nightscoutURL {
Storage.shared.url.value = newValue
triggerCheckStatus()
PhoneSessionManager.shared.sendConfig()
}
}
}
Expand All @@ -34,6 +35,7 @@ class NightscoutSettingsViewModel: ObservableObject {
if newValue != nightscoutToken {
Storage.shared.token.value = newValue
triggerCheckStatus()
PhoneSessionManager.shared.sendConfig()
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions LoopFollow/Settings/DexcomSettingsViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ class DexcomSettingsViewModel: ObservableObject {
willSet {
if newValue != userName {
Storage.shared.shareUserName.value = newValue
PhoneSessionManager.shared.sendConfig()
}
}
}
Expand All @@ -20,6 +21,7 @@ class DexcomSettingsViewModel: ObservableObject {
willSet {
if newValue != password {
Storage.shared.sharePassword.value = newValue
PhoneSessionManager.shared.sendConfig()
}
}
}
Expand All @@ -28,6 +30,7 @@ class DexcomSettingsViewModel: ObservableObject {
willSet {
if newValue != server {
Storage.shared.shareServer.value = newValue
PhoneSessionManager.shared.sendConfig()
}
}
}
Expand Down
3 changes: 3 additions & 0 deletions LoopFollow/Settings/UnitsConfigurationView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ struct UnitsConfigurationView: View {
.pickerStyle(.segmented)
.onChange(of: glucoseUnit) { newValue in
UnitSettingsStore.shared.glucoseUnit = newValue
PhoneSessionManager.shared.sendConfig()
}
}

Expand All @@ -46,6 +47,7 @@ struct UnitsConfigurationView: View {
.onChange(of: lowValue) { newValue in
Storage.shared.lowLine.value = newValue
Observable.shared.chartSettingsChanged.value = true
PhoneSessionManager.shared.sendConfig()
}
BGPicker(
title: "High",
Expand All @@ -56,6 +58,7 @@ struct UnitsConfigurationView: View {
.onChange(of: highValue) { newValue in
Storage.shared.highLine.value = newValue
Observable.shared.chartSettingsChanged.value = true
PhoneSessionManager.shared.sendConfig()
}
}
}
Expand Down
124 changes: 124 additions & 0 deletions LoopFollow/Watch/PhoneSessionManager.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
// LoopFollow
// PhoneSessionManager.swift

import Foundation
import HealthKit
import WatchConnectivity

class PhoneSessionManager: NSObject, WCSessionDelegate {
static let shared = PhoneSessionManager()

private override init() {
super.init()
}

func startSession() {
guard WCSession.isSupported() else { return }
WCSession.default.delegate = self
WCSession.default.activate()
}

private func buildConfig() -> [String: Any] {
// LF (LoopFollow's own) APNS credentials — used by the watch to
// populate `return_notification` so Trio acks land here on the
// iPhone (which can then forward to the watch via WCSession).
// Mirrors PushNotificationManager.createReturnNotificationInfo().
let lfDeviceToken = Observable.shared.loopFollowDeviceToken.value
let lfTeamId = BuildDetails.default.teamID ?? ""
let lfBundleId = Bundle.main.bundleIdentifier ?? ""
let lfProductionEnv = BuildDetails.default.isTestFlightBuild()

return [
"nsURL": Storage.shared.url.value,
"nsToken": Storage.shared.token.value,
"dexUsername": Storage.shared.shareUserName.value,
"dexPassword": Storage.shared.sharePassword.value,
"dexServer": Storage.shared.shareServer.value,
"units": Storage.shared.units.value,
"lowLine": Storage.shared.lowLine.value,
"highLine": Storage.shared.highLine.value,
"remoteType": Storage.shared.remoteType.value.rawValue,
"maxBolus": Storage.shared.maxBolus.value.doubleValue(for: .internationalUnit()),
"maxCarbs": Storage.shared.maxCarbs.value.doubleValue(for: .gram()),
"trcDeviceToken": Storage.shared.deviceToken.value,
"trcSharedSecret": Storage.shared.sharedSecret.value,
"trcApnsKey": Storage.shared.remoteApnsKey.value,
"trcKeyId": Storage.shared.remoteKeyId.value,
"trcTeamId": Storage.shared.teamId.value ?? "",
"trcBundleId": Storage.shared.bundleId.value,
"trcProductionEnv": Storage.shared.productionEnvironment.value,
"trcUser": Storage.shared.user.value,
"nsWriteAuth": Storage.shared.nsWriteAuth.value,
"lfDeviceToken": lfDeviceToken,
"lfApnsKey": Storage.shared.lfApnsKey.value,
"lfKeyId": Storage.shared.lfKeyId.value,
"lfTeamId": lfTeamId,
"lfBundleId": lfBundleId,
"lfProductionEnv": lfProductionEnv,
]
}

func sendConfig() {
guard WCSession.default.activationState == .activated else { return }
let config = buildConfig()
try? WCSession.default.updateApplicationContext(config)

// Also send via message for immediate delivery if Watch is reachable
if WCSession.default.isReachable {
WCSession.default.sendMessage(config, replyHandler: nil, errorHandler: nil)
}
}

// MARK: - WCSessionDelegate

func session(_ session: WCSession, activationDidCompleteWith activationState: WCSessionActivationState, error: Error?) {
if activationState == .activated {
sendConfig()
}
}

func sessionDidBecomeInactive(_ session: WCSession) {}

func sessionDidDeactivate(_ session: WCSession) {
WCSession.default.activate()
}

// Re-send config when Watch becomes reachable (handles fresh install)
func sessionReachabilityDidChange(_ session: WCSession) {
if session.isReachable {
sendConfig()
}
}

// Handle Watch requesting config via applicationContext
func session(_ session: WCSession, didReceiveApplicationContext applicationContext: [String: Any]) {
if applicationContext["requestConfig"] != nil {
sendConfig()
}
}

// Handle Watch requesting config via sendMessage (with reply)
func session(_ session: WCSession, didReceiveMessage message: [String: Any], replyHandler: @escaping ([String: Any]) -> Void) {
if message["requestConfig"] != nil {
let config = buildConfig()
replyHandler(config)
// Also update application context so it's cached
try? WCSession.default.updateApplicationContext(config)
} else {
replyHandler([:])
}
}

func session(_ session: WCSession, didReceiveMessage message: [String: Any]) {
if message["requestConfig"] != nil {
sendConfig()
}
}

// Handle Watch requesting config via transferUserInfo
func session(_ session: WCSession, didReceiveUserInfo userInfo: [String: Any] = [:]) {
if userInfo["requestConfig"] != nil {
sendConfig()
}
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 14 additions & 0 deletions LoopFollowWatch/Assets.xcassets/AppIcon.appiconset/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"images" : [
{
"filename" : "1024.png",
"idiom" : "universal",
"platform" : "watchos",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}
6 changes: 6 additions & 0 deletions LoopFollowWatch/Assets.xcassets/Contents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}
Loading
Loading