Skip to content
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 Loop.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -1089,7 +1089,7 @@
4F70C1E71DE8DCA7006380B7 /* PBXTargetDependency */,
);
name = Loop;
productName = Naterade;
productName = Loop;
productReference = 43776F8C1B8022E90074EA36 /* Loop.app */;
productType = "com.apple.product-type.application";
};
Expand Down
12 changes: 4 additions & 8 deletions Loop/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -70,18 +70,14 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
extension AppDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
switch response.actionIdentifier {
case NotificationManager.Action.RetryBolus.rawValue:
if let units = response.notification.request.content.userInfo[NotificationManager.UserInfoKey.BolusAmount.rawValue] as? Double,
let startDate = response.notification.request.content.userInfo[NotificationManager.UserInfoKey.BolusStartDate.rawValue] as? Date,
case NotificationManager.Action.retryBolus.rawValue:
if let units = response.notification.request.content.userInfo[NotificationManager.UserInfoKey.bolusAmount.rawValue] as? Double,
let startDate = response.notification.request.content.userInfo[NotificationManager.UserInfoKey.bolusStartDate.rawValue] as? Date,
startDate.timeIntervalSinceNow >= TimeInterval(minutes: -5)
{
AnalyticsManager.sharedManager.didRetryBolus()

deviceManager.enactBolus(units: units) { (error) in
if error != nil {
NotificationManager.sendBolusFailureNotificationForAmount(units, atStartDate: startDate)
}

deviceManager.enactBolus(units: units, at: startDate) { (_) in
completionHandler()
}
return
Expand Down
4 changes: 2 additions & 2 deletions Loop/Managers/AnalyticsManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -118,15 +118,15 @@ final class AnalyticsManager {
// MARK: - Loop Events

func didAddCarbsFromWatch(_ carbs: Double) {
logEvent("Carb entry created", withProperties: ["source" : "Watch", "value": carbs], outOfSession: true)
logEvent("Carb entry created", withProperties: ["source" : "Watch"], outOfSession: true)
}

func didRetryBolus() {
logEvent("Bolus Retry", outOfSession: true)
}

func didSetBolusFromWatch(_ units: Double) {
logEvent("Bolus set", withProperties: ["source" : "Watch", "value": units], outOfSession: true)
logEvent("Bolus set", withProperties: ["source" : "Watch"], outOfSession: true)
}

func loopDidSucceed() {
Expand Down
29 changes: 21 additions & 8 deletions Loop/Managers/DeviceDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -361,30 +361,38 @@ final class DeviceDataManager {
/// - parameter units: The number of units to deliver
/// - parameter completion: A clsure called after the command is complete. This closure takes a single argument:
/// - error: An error describing why the command failed
func enactBolus(units: Double, completion: @escaping (_ error: Error?) -> Void) {
func enactBolus(units: Double, at startDate: Date = Date(), completion: @escaping (_ error: Error?) -> Void) {
let notify = { (error: Error?) -> Void in
if let error = error {
NotificationManager.sendBolusFailureNotification(for: error, units: units, at: startDate)
}

completion(error)
}

guard units > 0 else {
completion(nil)
notify(nil)
return
}

guard let device = rileyLinkManager.firstConnectedDevice else {
completion(LoopError.connectionError)
notify(LoopError.connectionError)
return
}

guard let ops = device.ops else {
completion(LoopError.configurationError("PumpOps"))
notify(LoopError.configurationError("PumpOps"))
return
}

let setBolus = {
ops.setNormalBolus(units: units) { (error) in
if let error = error {
self.logger.addError(error, fromSource: "Bolus")
completion(LoopError.communicationError)
notify(error)
} else {
self.loopManager.addExpectedBolus(units, at: Date())
completion(nil)
notify(nil)
}
}
}
Expand All @@ -401,13 +409,18 @@ final class DeviceDataManager {
switch result {
case .failure(let error):
self.logger.addError(error, fromSource: "Bolus")
completion(error)
notify(error)
case .success:
setBolus()
}
}
case .failure(let error):
completion(error)
switch error {
case let error as PumpCommsError:
notify(SetBolusError.certain(error))
default:
notify(error)
}
}
}
} else {
Expand Down
62 changes: 37 additions & 25 deletions Loop/Managers/NotificationManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,36 +9,38 @@
import UIKit
import UserNotifications

import RileyLinkKit


struct NotificationManager {
enum Category: String {
case BolusFailure
case LoopNotRunning
case PumpBatteryLow
case PumpReservoirEmpty
case PumpReservoirLow
case bolusFailure
case loopNotRunning
case pumpBatteryLow
case pumpReservoirEmpty
case pumpReservoirLow
}

enum Action: String {
case RetryBolus
case retryBolus
}

enum UserInfoKey: String {
case BolusAmount
case BolusStartDate
case bolusAmount
case bolusStartDate
}

private static var notificationCategories: Set<UNNotificationCategory> {
var categories = [UNNotificationCategory]()

let retryBolusAction = UNNotificationAction(
identifier: Action.RetryBolus.rawValue,
identifier: Action.retryBolus.rawValue,
title: NSLocalizedString("Retry", comment: "The title of the notification action to retry a bolus command"),
options: []
)

categories.append(UNNotificationCategory(
identifier: Category.BolusFailure.rawValue,
identifier: Category.bolusFailure.rawValue,
actions: [retryBolusAction],
intentIdentifiers: [],
options: []
Expand All @@ -57,25 +59,35 @@ struct NotificationManager {

// MARK: - Notifications

static func sendBolusFailureNotificationForAmount(_ units: Double, atStartDate startDate: Date) {
static func sendBolusFailureNotification(for error: Error, units: Double, at startDate: Date) {
let notification = UNMutableNotificationContent()

notification.title = NSLocalizedString("Bolus", comment: "The notification title for a bolus failure")
notification.body = String(format: NSLocalizedString("%@ U bolus may have failed.", comment: "The notification alert describing a possible bolus failure. The substitution parameter is the size of the bolus in units."), NumberFormatter.localizedString(from: NSNumber(value: units), number: .decimal))

switch error {
case let error as RileyLinkKit.SetBolusError:
notification.subtitle = error.errorDescriptionWithUnits(units)
notification.body = String(format: "%@ %@", error.failureReason!, error.recoverySuggestion!)
case let error as LocalizedError:
notification.body = error.errorDescription ?? error.localizedDescription
default:
notification.body = error.localizedDescription
}

notification.sound = UNNotificationSound.default()

if startDate.timeIntervalSinceNow >= TimeInterval(minutes: -5) {
notification.categoryIdentifier = Category.BolusFailure.rawValue
notification.categoryIdentifier = Category.bolusFailure.rawValue
}

notification.userInfo = [
UserInfoKey.BolusAmount.rawValue: units,
UserInfoKey.BolusStartDate.rawValue: startDate
UserInfoKey.bolusAmount.rawValue: units,
UserInfoKey.bolusStartDate.rawValue: startDate
]

let request = UNNotificationRequest(
// Only support 1 bolus notification at once
identifier: Category.BolusFailure.rawValue,
identifier: Category.bolusFailure.rawValue,
content: notification,
trigger: nil
)
Expand Down Expand Up @@ -107,11 +119,11 @@ struct NotificationManager {

notification.title = NSLocalizedString("Loop Failure", comment: "The notification title for a loop failure")
notification.sound = UNNotificationSound.default()
notification.categoryIdentifier = Category.LoopNotRunning.rawValue
notification.threadIdentifier = Category.LoopNotRunning.rawValue
notification.categoryIdentifier = Category.loopNotRunning.rawValue
notification.threadIdentifier = Category.loopNotRunning.rawValue

let request = UNNotificationRequest(
identifier: "\(Category.LoopNotRunning.rawValue)\(failureInterval)",
identifier: "\(Category.loopNotRunning.rawValue)\(failureInterval)",
content: notification,
trigger: UNTimeIntervalNotificationTrigger(
timeInterval: failureInterval + gracePeriod,
Expand All @@ -129,10 +141,10 @@ struct NotificationManager {
notification.title = NSLocalizedString("Pump Battery Low", comment: "The notification title for a low pump battery")
notification.body = NSLocalizedString("Change the pump battery immediately", comment: "The notification alert describing a low pump battery")
notification.sound = UNNotificationSound.default()
notification.categoryIdentifier = Category.PumpBatteryLow.rawValue
notification.categoryIdentifier = Category.pumpBatteryLow.rawValue

let request = UNNotificationRequest(
identifier: Category.PumpBatteryLow.rawValue,
identifier: Category.pumpBatteryLow.rawValue,
content: notification,
trigger: nil
)
Expand All @@ -146,11 +158,11 @@ struct NotificationManager {
notification.title = NSLocalizedString("Pump Reservoir Empty", comment: "The notification title for an empty pump reservoir")
notification.body = NSLocalizedString("Change the pump reservoir now", comment: "The notification alert describing an empty pump reservoir")
notification.sound = UNNotificationSound.default()
notification.categoryIdentifier = Category.PumpReservoirEmpty.rawValue
notification.categoryIdentifier = Category.pumpReservoirEmpty.rawValue

let request = UNNotificationRequest(
// Not a typo: this should replace any pump reservoir low notifications
identifier: Category.PumpReservoirLow.rawValue,
identifier: Category.pumpReservoirLow.rawValue,
content: notification,
trigger: nil
)
Expand Down Expand Up @@ -179,10 +191,10 @@ struct NotificationManager {
}

notification.sound = UNNotificationSound.default()
notification.categoryIdentifier = Category.PumpReservoirLow.rawValue
notification.categoryIdentifier = Category.pumpReservoirLow.rawValue

let request = UNNotificationRequest(
identifier: Category.PumpReservoirLow.rawValue,
identifier: Category.pumpReservoirLow.rawValue,
content: notification,
trigger: nil
)
Expand Down
6 changes: 2 additions & 4 deletions Loop/Managers/WatchDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -154,10 +154,8 @@ final class WatchDataManager: NSObject, WCSessionDelegate {
}
case SetBolusUserInfo.name?:
if let bolus = SetBolusUserInfo(rawValue: message as SetBolusUserInfo.RawValue) {
self.deviceDataManager.enactBolus(units: bolus.value) { (error) in
if error != nil {
NotificationManager.sendBolusFailureNotificationForAmount(bolus.value, atStartDate: bolus.startDate)
} else {
self.deviceDataManager.enactBolus(units: bolus.value, at: bolus.startDate) { (error) in
if error == nil {
AnalyticsManager.sharedManager.didSetBolusFromWatch(bolus.value)
}

Expand Down
13 changes: 8 additions & 5 deletions Loop/Models/LoopError.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@
//

import Foundation
import RileyLinkKit


enum LoopError: Error {
// Failure during device communication
case communicationError
// A bolus failed to start
case bolusCommand(SetBolusError)

// Missing or unexpected configuration values
case configurationError(String)
Expand All @@ -34,6 +36,7 @@ enum LoopError: Error {
case invalidData(details: String)
}


extension LoopError: LocalizedError {

public var recoverySuggestion: String? {
Expand All @@ -46,14 +49,13 @@ extension LoopError: LocalizedError {
}

public var errorDescription: String? {

let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.minute]
formatter.unitsStyle = .full

switch self {
case .communicationError:
return NSLocalizedString("Communication Error", comment: "The error message displayed after a communication error.")
case .bolusCommand(let error):
return error.errorDescription
case .configurationError(let details):
return String(format: NSLocalizedString("Configuration Error: %1$@", comment: "The error message displayed for configuration errors. (1: configuration error details)"), details)
case .connectionError:
Expand All @@ -76,3 +78,4 @@ extension LoopError: LocalizedError {
}
}


6 changes: 1 addition & 5 deletions Loop/View Controllers/StatusTableViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -798,12 +798,8 @@ final class StatusTableViewController: UITableViewController, UIGestureRecognize
if let bolusViewController = segue.source as? BolusViewController {
if let bolus = bolusViewController.bolus, bolus > 0 {
self.bolusState = .enacting
let startDate = Date()
dataManager.enactBolus(units: bolus) { (error) in
dataManager.enactBolus(units: bolus) { (_) in
self.bolusState = nil
if error != nil {
NotificationManager.sendBolusFailureNotificationForAmount(bolus, atStartDate: startDate)
}
}
} else {
self.bolusState = nil
Expand Down
9 changes: 9 additions & 0 deletions WatchApp Extension/ExtensionDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import WatchConnectivity
import WatchKit
import os
import UserNotifications


final class ExtensionDelegate: NSObject, WKExtensionDelegate {
Expand Down Expand Up @@ -40,6 +41,7 @@ final class ExtensionDelegate: NSObject, WKExtensionDelegate {

func applicationDidFinishLaunching() {
// Perform any final initialization of your application.
UNUserNotificationCenter.current().delegate = self
}

func applicationDidBecomeActive() {
Expand Down Expand Up @@ -170,6 +172,13 @@ extension ExtensionDelegate: WCSessionDelegate {
}


extension ExtensionDelegate: UNUserNotificationCenterDelegate {
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
completionHandler([.badge, .sound, .alert])
}
}


extension ExtensionDelegate {

/// Global shortcut to present an alert for a specific error out-of-context with a specific interface controller.
Expand Down
11 changes: 6 additions & 5 deletions WatchApp Extension/PushNotificationPayload.apns
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
{
"aps": {
"alert": {
"body": "Test message",
"title": "Optional title"
"body": "RileyLink timed out. Check your pump before retrying.",
"title": "Bolus",
"subtitle": "3.5 U bolus failed"
},
"category": "myCategory"
"category": "bolusFailure"
},

"WatchKit Simulator Actions": [
{
"title": "First Button",
"identifier": "firstButtonAction"
"title": "Retry",
"identifier": "retryBolus"
}
],

Expand Down
Loading