Skip to content

Commit

Permalink
[LOOP-2546] Display onboarding at startup (#372)
Browse files Browse the repository at this point in the history
- https://tidepool.atlassian.net/browse/LOOP-2546
- https://tidepool.atlassian.net/browse/LOOP-3143
- https://tidepool.atlassian.net/browse/LOOP-3144
- https://tidepool.atlassian.net/browse/LOOP-3145
- Refactor AppDelegate into new LoopAppManager class
- Refactor onboarding into new OnboardingManager class
- Refactor BluetoothStateManager to support deferred authorization and separate state
- Update root view controller dependent classes to use provider
- Update LaunchScreen and Main root view controller to display system background only
- Refactor and remove onboarding related from StatusTableViewController
- Defer StatusTableViewController creation until after onboarding
- Support multiple onboarding plugins
- Use OSLog in PluginManager for debugging
  • Loading branch information
Darin Krauss authored Mar 13, 2021
1 parent a3cc9dd commit 0d98b00
Show file tree
Hide file tree
Showing 26 changed files with 1,277 additions and 749 deletions.
56 changes: 33 additions & 23 deletions Loop.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

241 changes: 35 additions & 206 deletions Loop/AppDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,111 +6,32 @@
// Copyright © 2015 Nathan Racklyeft. All rights reserved.
//

import HealthKit
import Intents
import LoopCore
import LoopKit
import LoopKitUI
import UIKit
import UserNotifications
import LoopKit

@UIApplicationMain
final class AppDelegate: UIResponder, UIApplicationDelegate, DeviceOrientationController {

private lazy var log = DiagnosticLog(category: "AppDelegate")

private lazy var pluginManager = PluginManager()

private var alertManager: AlertManager!
private var deviceDataManager: DeviceDataManager!
private var loopAlertsManager: LoopAlertsManager!
private var bluetoothStateManager: BluetoothStateManager!
private var trustedTimeChecker: TrustedTimeChecker!

final class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?

var launchOptions: [UIApplication.LaunchOptionsKey: Any]?

private var rootViewController: RootNavigationController! {
return window?.rootViewController as? RootNavigationController
}

private func isAfterFirstUnlock() -> Bool {
let fileManager = FileManager.default
do {
let documentDirectory = try fileManager.url(for: .documentDirectory, in: .userDomainMask, appropriateFor: nil, create: false)
let fileURL = documentDirectory.appendingPathComponent("protection.test")
guard fileManager.fileExists(atPath: fileURL.path) else {
let contents = Data("unimportant".utf8)
try? contents.write(to: fileURL, options: .completeFileProtectionUntilFirstUserAuthentication)
// If file doesn't exist, we're at first start, which will be user directed.
return true
}
let contents = try? Data(contentsOf: fileURL)
return contents != nil
} catch {
log.error("Could not create after first unlock test file: %@", String(describing: error))
}
return false
}

private func finishLaunch(application: UIApplication) {
log.default("Finishing launching")
UIDevice.current.isBatteryMonitoringEnabled = true

bluetoothStateManager = BluetoothStateManager()
alertManager = AlertManager(rootViewController: rootViewController, expireAfter: Bundle.main.localCacheDuration)
deviceDataManager = DeviceDataManager(pluginManager: pluginManager, alertManager: alertManager, bluetoothStateManager: bluetoothStateManager, rootViewController: rootViewController)

let statusTableViewController = UIStoryboard(name: "Main", bundle: Bundle(for: AppDelegate.self)).instantiateViewController(withIdentifier: "MainStatusViewController") as! StatusTableViewController

statusTableViewController.deviceManager = deviceDataManager

bluetoothStateManager.addBluetoothStateObserver(statusTableViewController)

loopAlertsManager = LoopAlertsManager(alertManager: alertManager, bluetoothStateManager: bluetoothStateManager)

SharedLogging.instance = deviceDataManager.loggingServicesManager

deviceDataManager?.analyticsServicesManager.application(application, didFinishLaunchingWithOptions: launchOptions)

OrientationLock.deviceOrientationController = self
private let loopAppManager = LoopAppManager()
private let log = DiagnosticLog(category: "AppDelegate")

NotificationManager.authorize(delegate: self)

rootViewController.pushViewController(statusTableViewController, animated: false)

let notificationOption = launchOptions?[.remoteNotification]

if let notification = notificationOption as? [String: AnyObject] {
deviceDataManager?.handleRemoteNotification(notification)
}

scheduleBackgroundTasks()

launchOptions = nil

trustedTimeChecker = TrustedTimeChecker(alertManager)
}
// MARK: - UIApplicationDelegate - Initialization

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {

self.launchOptions = launchOptions

log.default("didFinishLaunchingWithOptions %{public}@", String(describing: launchOptions))

registerBackgroundTasks()
log.default("%{public}@ with launchOptions: %{public}@", #function, String(describing: launchOptions))

guard isAfterFirstUnlock() else {
log.default("Launching before first unlock; pausing launch...")
return false
}
loopAppManager.initialize(with: launchOptions)
loopAppManager.launch(into: window)
return loopAppManager.isLaunchComplete
}

finishLaunch(application: application)
// MARK: - UIApplicationDelegate - Life Cycle

window?.tintColor = .loopAccent
func applicationDidBecomeActive(_ application: UIApplication) {
log.default(#function)

return true
loopAppManager.didBecomeActive()
}

func applicationWillResignActive(_ application: UIApplication) {
Expand All @@ -125,139 +46,47 @@ final class AppDelegate: UIResponder, UIApplicationDelegate, DeviceOrientationCo
log.default(#function)
}

func applicationDidBecomeActive(_ application: UIApplication) {
deviceDataManager?.updatePumpManagerBLEHeartbeatPreference()
}

func applicationWillTerminate(_ application: UIApplication) {
log.default(#function)
}

// MARK: - Continuity

func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
log.default(#function)

if #available(iOS 12.0, *) {
if userActivity.activityType == NewCarbEntryIntent.className {
log.default("Restoring %{public}@ intent", userActivity.activityType)
rootViewController.restoreUserActivityState(.forNewCarbEntry())
return true
}
}

switch userActivity.activityType {
case NSUserActivity.newCarbEntryActivityType,
NSUserActivity.viewLoopStatusActivityType:
log.default("Restoring %{public}@ activity", userActivity.activityType)
restorationHandler([rootViewController])
return true
default:
return false
}
}

// MARK: - Remote notifications
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
let tokenParts = deviceToken.map { data in String(format: "%02.2hhx", data) }
let token = tokenParts.joined()
log.default("RemoteNotifications device token: %{public}@", token)
deviceDataManager?.loopManager.settings.deviceToken = deviceToken
}
// MARK: - UIApplicationDelegate - Environment

func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
log.error("Failed to register: %{public}@", String(describing: error))
}

func application(_ application: UIApplication,
didReceiveRemoteNotification userInfo: [AnyHashable : Any],
fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
guard let notification = userInfo as? [String: AnyObject] else {
completionHandler(.failed)
return
}

deviceDataManager?.handleRemoteNotification(notification)
completionHandler(.noData)
}

func applicationProtectedDataDidBecomeAvailable(_ application: UIApplication) {
log.default("applicationProtectedDataDidBecomeAvailable")

if deviceDataManager == nil {
finishLaunch(application: application)
if !loopAppManager.isLaunchComplete {
loopAppManager.launch(into: window)
}
}

// MARK: - DeviceOrientationController

var supportedInterfaceOrientations = UIInterfaceOrientationMask.allButUpsideDown
// MARK: - UIApplicationDelegate - Remote Notification

func setOriginallySupportedInferfaceOrientations() {
supportedInterfaceOrientations = UIInterfaceOrientationMask.allButUpsideDown
}
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
log.default(#function)

func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
supportedInterfaceOrientations
loopAppManager.setRemoteNotificationsDeviceToken(deviceToken)
}

// MARK: - Background Tasks

private func registerBackgroundTasks() {
if DeviceDataManager.registerCriticalEventLogHistoricalExportBackgroundTask({ self.deviceDataManager.handleCriticalEventLogHistoricalExportBackgroundTask($0) }) {
log.debug("Critical event log export background task registered")
} else {
log.error("Critical event log export background task not registered")
}
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
log.error("%{public}@ with error: %{public}@", #function, String(describing: error))
}

private func scheduleBackgroundTasks() {
deviceDataManager.scheduleCriticalEventLogHistoricalExportBackgroundTask()
}
}
func application(_ application: UIApplication, didReceiveRemoteNotification userInfo: [AnyHashable : Any], fetchCompletionHandler completionHandler: @escaping (UIBackgroundFetchResult) -> Void) {
log.default(#function)

// MARK: UNUserNotificationCenterDelegate implementation
completionHandler(loopAppManager.handleRemoteNotification(userInfo as? [String: AnyObject]) ? .noData : .failed)
}

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[LoopNotificationUserInfoKey.bolusAmount.rawValue] as? Double,
let startDate = response.notification.request.content.userInfo[LoopNotificationUserInfoKey.bolusStartDate.rawValue] as? Date,
startDate.timeIntervalSinceNow >= TimeInterval(minutes: -5)
{
deviceDataManager?.analyticsServicesManager.didRetryBolus()
// MARK: - UIApplicationDelegate - Continuity

deviceDataManager?.enactBolus(units: units, at: startDate) { (_) in
completionHandler()
}
return
}
case NotificationManager.Action.acknowledgeAlert.rawValue:
let userInfo = response.notification.request.content.userInfo
if let alertIdentifier = userInfo[LoopNotificationUserInfoKey.alertTypeID.rawValue] as? Alert.AlertIdentifier,
let managerIdentifier = userInfo[LoopNotificationUserInfoKey.managerIDForAlert.rawValue] as? String {
alertManager.acknowledgeAlert(identifier:
Alert.Identifier(managerIdentifier: managerIdentifier, alertIdentifier: alertIdentifier))
}
default:
break
}
func application(_ application: UIApplication, continue userActivity: NSUserActivity, restorationHandler: @escaping ([UIUserActivityRestoring]?) -> Void) -> Bool {
log.default(#function)

completionHandler()
return loopAppManager.userActivity(userActivity, restorationHandler: restorationHandler)
}

func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
switch notification.request.identifier {
// TODO: Until these notifications are converted to use the new alert system, they shall still show in the foreground
case LoopNotificationCategory.bolusFailure.rawValue,
LoopNotificationCategory.pumpBatteryLow.rawValue,
LoopNotificationCategory.pumpExpired.rawValue,
LoopNotificationCategory.pumpFault.rawValue:
completionHandler([.badge, .sound, .alert])
default:
// All other userNotifications are not to be displayed while in the foreground
completionHandler([])
}
// MARK: - UIApplicationDelegate - Interface

func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
return loopAppManager.supportedInterfaceOrientations
}
}
5 changes: 3 additions & 2 deletions Loop/Base.lproj/LaunchScreen.storyboard
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,14 @@
<!--Navigation Controller-->
<scene sceneID="Ucb-uj-Fzl">
<objects>
<navigationController navigationBarHidden="YES" toolbarHidden="NO" id="c6k-8z-nla" sceneMemberID="viewController">
<navigationController navigationBarHidden="YES" id="c6k-8z-nla" sceneMemberID="viewController">
<tabBarItem key="tabBarItem" enabled="NO" title="" id="nfG-Bf-TrT"/>
<nil key="simulatedTopBarMetrics"/>
<nil key="simulatedBottomBarMetrics"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" id="3rC-hS-Fnr">
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<toolbar key="toolbar" opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" id="4Ow-gU-14I">
<rect key="frame" x="0.0" y="813" width="414" height="49"/>
<autoresizingMask key="autoresizingMask"/>
</toolbar>
<connections>
Expand Down
32 changes: 27 additions & 5 deletions Loop/Base.lproj/Main.storyboard
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17156" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="OJt-dE-GaA">
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="17701" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" colorMatched="YES" initialViewController="OJt-dE-GaA">
<device id="retina4_7" orientation="portrait" appearance="light"/>
<dependencies>
<deployment identifier="iOS"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17126"/>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17703"/>
<capability name="Named colors" minToolsVersion="9.0"/>
<capability name="System colors in document resources" minToolsVersion="11.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
Expand Down Expand Up @@ -673,15 +673,18 @@
<!--Root Navigation Controller-->
<scene sceneID="II9-on-wj3">
<objects>
<navigationController automaticallyAdjustsScrollViewInsets="NO" toolbarHidden="NO" id="OJt-dE-GaA" customClass="RootNavigationController" customModule="Loop" customModuleProvider="target" sceneMemberID="viewController">
<navigationController automaticallyAdjustsScrollViewInsets="NO" navigationBarHidden="YES" id="OJt-dE-GaA" customClass="RootNavigationController" customModule="Loop" customModuleProvider="target" sceneMemberID="viewController">
<nil key="simulatedTopBarMetrics"/>
<nil key="simulatedBottomBarMetrics"/>
<navigationBar key="navigationBar" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="lcU-HN-Qiy">
<rect key="frame" x="0.0" y="0.0" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</navigationBar>
<toolbar key="toolbar" opaque="NO" clearsContextBeforeDrawing="NO" contentMode="scaleToFill" insetsLayoutMarginsFromSafeArea="NO" id="K8q-nd-eVx">
<rect key="frame" x="0.0" y="623" width="375" height="44"/>
<autoresizingMask key="autoresizingMask"/>
</toolbar>
<connections>
<segue destination="csl-Ke-OPG" kind="relationship" relationship="rootViewController" id="2Ti-Uv-cxR"/>
</connections>
</navigationController>
<placeholder placeholderIdentifier="IBFirstResponder" id="Z8K-4u-wX1" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
Expand Down Expand Up @@ -904,6 +907,25 @@
</objects>
<point key="canvasLocation" x="2458" y="733"/>
</scene>
<!--View Controller-->
<scene sceneID="f78-Bb-hbV">
<objects>
<viewController id="csl-Ke-OPG" sceneMemberID="viewController">
<layoutGuides>
<viewControllerLayoutGuide type="top" id="gFu-6V-bzf"/>
<viewControllerLayoutGuide type="bottom" id="iEe-G5-n7b"/>
</layoutGuides>
<view key="view" contentMode="scaleToFill" id="VRE-WG-94h">
<rect key="frame" x="0.0" y="0.0" width="375" height="667"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<color key="backgroundColor" systemColor="systemBackgroundColor"/>
</view>
<navigationItem key="navigationItem" id="HVC-1r-kuZ"/>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="S8O-1K-8tW" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="838" y="323"/>
</scene>
</scenes>
<resources>
<image name="Oval Selection" width="21" height="21"/>
Expand Down
Loading

0 comments on commit 0d98b00

Please sign in to comment.