Skip to content

Commit

Permalink
Merged PR 5227: Add path for switching on Push Notifications
Browse files Browse the repository at this point in the history
- Skipping turning on Push notifications during onboarding
- Denying switching on Push Notifications

Puts the app in an inactive state and routes to the correct handler for either switching on push notifications or routing to settings.

Related work items: #14260
  • Loading branch information
Cameron Mc Gorian committed Jul 7, 2020
2 parents 8fe15dc + 5e59221 commit db55a1d
Show file tree
Hide file tree
Showing 7 changed files with 86 additions and 11 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -669,6 +669,8 @@ private extension ExposureActiveState {
return "Inactive - Bluetooth off"
case .disabled:
return "Inactive - Disabled"
case .pushNotifications:
return "Inactive - Push Notifications"
case .noRecentNotificationUpdates:
return "Inactive - No Recent Updates"
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,13 @@ final class ExposureController: ExposureControlling, Logging {
init(mutableStateStream: MutableExposureStateStreaming,
exposureManager: ExposureManaging,
dataController: ExposureDataControlling,
networkStatusStream: NetworkStatusStreaming) {
networkStatusStream: NetworkStatusStreaming,
userNotificationCenter: UserNotificationCenter) {
self.mutableStateStream = mutableStateStream
self.exposureManager = exposureManager
self.dataController = dataController
self.networkStatusStream = networkStatusStream
self.userNotificationCenter = userNotificationCenter
}

deinit {
Expand All @@ -38,6 +40,8 @@ final class ExposureController: ExposureControlling, Logging {
self.postExposureManagerActivation()
self.updateStatusStream()
}

updatePushNotificationState()
}

func getMinimumiOSVersion(_ completion: @escaping (String?) -> ()) {
Expand Down Expand Up @@ -78,6 +82,7 @@ final class ExposureController: ExposureControlling, Logging {

func refreshStatus() {
updateStatusStream()
updatePushNotificationState()
}

func updateWhenRequired() -> AnyPublisher<(), ExposureDataError> {
Expand Down Expand Up @@ -115,17 +120,13 @@ final class ExposureController: ExposureControlling, Logging {
}

func requestPushNotificationPermission(_ completion: @escaping (() -> ())) {
let uncc = UNUserNotificationCenter.current()

uncc.getNotificationSettings { settings in
if settings.authorizationStatus == .authorized {
DispatchQueue.main.async {
completion()
}
userNotificationCenter.getAuthorizationStatus { authorizationStatus in
if authorizationStatus == .authorized {
completion()
}
}

uncc.requestAuthorization(options: [.alert, .badge, .sound]) { _, _ in
userNotificationCenter.requestAuthorization(options: [.alert, .badge, .sound]) { _, _ in
DispatchQueue.main.async {
completion()
}
Expand Down Expand Up @@ -264,6 +265,8 @@ final class ExposureController: ExposureControlling, Logging {
switch exposureManagerStatus {
case .active where hasBeenTooLongSinceLastUpdate:
activeState = .inactive(.noRecentNotificationUpdates)
case .active where !isPushNotificationsEnabled:
activeState = .inactive(.pushNotifications)
case .active:
activeState = .active
case .inactive(_) where hasBeenTooLongSinceLastUpdate:
Expand All @@ -277,6 +280,8 @@ final class ExposureController: ExposureControlling, Logging {
case let .inactive(error) where error == .unknown || error == .internalTypeMismatch:
// Most likely due to code signing issues
activeState = .inactive(.disabled)
case .inactive where !isPushNotificationsEnabled:
activeState = .inactive(.pushNotifications)
case .inactive:
activeState = .inactive(.disabled)
case .notAuthorized:
Expand Down Expand Up @@ -364,13 +369,22 @@ final class ExposureController: ExposureControlling, Logging {
}
}

private func updatePushNotificationState() {
userNotificationCenter.getAuthorizationStatus { authorizationStatus in
self.isPushNotificationsEnabled = authorizationStatus == .authorized
self.updateStatusStream()
}
}

private let mutableStateStream: MutableExposureStateStreaming
private let exposureManager: ExposureManaging
private let dataController: ExposureDataControlling
private var disposeBag = Set<AnyCancellable>()
private var exposureKeyUpdateStream: AnyPublisher<(), ExposureDataError>?
private let networkStatusStream: NetworkStatusStreaming
private var isActivated = false
private var isPushNotificationsEnabled = false
private let userNotificationCenter: UserNotificationCenter
}

extension LabConfirmationKey: ExposureConfirmationKey {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import Combine
import Foundation
import UserNotifications

/// @mockable
protocol ExposureControlling {
Expand Down Expand Up @@ -128,6 +129,10 @@ private final class ExposureControllerDependencyProvider: DependencyProvider<Exp
fileprivate var dataController: ExposureDataControlling {
return ExposureDataControllerBuilder(dependency: self).build()
}

fileprivate var userNotificationCenter: UserNotificationCenter {
return UNUserNotificationCenter.current()
}
}

final class ExposureControllerBuilder: Builder<ExposureControllerDependency>, ExposureControllerBuildable {
Expand All @@ -137,6 +142,7 @@ final class ExposureControllerBuilder: Builder<ExposureControllerDependency>, Ex
return ExposureController(mutableStateStream: dependencyProvider.dependency.mutableExposureStateStream,
exposureManager: dependencyProvider.exposureManager,
dataController: dependencyProvider.dataController,
networkStatusStream: dependencyProvider.dependency.networkStatusStream)
networkStatusStream: dependencyProvider.dependency.networkStatusStream,
userNotificationCenter: dependencyProvider.userNotificationCenter)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ enum ExposureActiveState: Equatable {
enum ExposureStateInactiveState: Equatable {
case disabled
case bluetoothOff
case pushNotifications
case noRecentNotificationUpdates
}

Expand Down
18 changes: 18 additions & 0 deletions Sources/ENCore/App/Features/Main/MainViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import Combine
import CoreBluetooth
import UIKit
import UserNotifications

/// @mockable
protocol MainRouting: Routing {
Expand Down Expand Up @@ -213,6 +214,12 @@ final class MainViewController: ViewController, MainViewControllable, StatusList
exposureController.requestExposureNotificationPermission()
case let .inactive(reason) where reason == .noRecentNotificationUpdates:
updateWhenRequired()
case let .inactive(reason) where reason == .pushNotifications:
UNUserNotificationCenter.current().getNotificationSettings { settings in
DispatchQueue.main.async {
self.handlePushNotificationSettings(authorizationStatus: settings.authorizationStatus)
}
}
case .inactive:
logError("Unhandled case")
case .active:
Expand All @@ -233,6 +240,17 @@ final class MainViewController: ViewController, MainViewControllable, StatusList
_ = CBCentralManager(delegate: nil, queue: nil, options: [CBCentralManagerOptionShowPowerAlertKey: true])
}

private func handlePushNotificationSettings(authorizationStatus: UNAuthorizationStatus) {
switch authorizationStatus {
case .notDetermined:
self.exposureController.requestPushNotificationPermission {}
case .denied:
self.openAppSettings()
default:
self.logError("Unhandled case")
}
}

private func updateWhenRequired() {
exposureController
.updateWhenRequired()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright (c) 2020 De Staat der Nederlanden, Ministerie van Volksgezondheid, Welzijn en Sport.
* Licensed under the EUROPEAN UNION PUBLIC LICENCE v. 1.2
*
* SPDX-License-Identifier: EUPL-1.2
*/

import Foundation
import NotificationCenter

/// @mockable
protocol UserNotificationCenter {
func getAuthorizationStatus(completionHandler: @escaping (UNAuthorizationStatus) -> ())
func requestAuthorization(options: UNAuthorizationOptions, completionHandler: @escaping (Bool, Error?) -> ())
}

extension UNUserNotificationCenter: UserNotificationCenter {

func getAuthorizationStatus(completionHandler: @escaping (UNAuthorizationStatus) -> ()) {
getNotificationSettings { settings in
DispatchQueue.main.async {
completionHandler(settings.authorizationStatus)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ final class ExposureControllerTests: TestCase {
private let mutableStateStream = MutableExposureStateStreamingMock()
private let exposureManager = ExposureManagingMock()
private let dataController = ExposureDataControllingMock()
private let userNotificationCenter = UserNotificationCenterMock()
private let networkStatusStream = NetworkStatusStreamingMock(networkStatusStream: CurrentValueSubject<Bool, Never>(true).eraseToAnyPublisher())

override func setUp() {
Expand All @@ -24,7 +25,8 @@ final class ExposureControllerTests: TestCase {
controller = ExposureController(mutableStateStream: mutableStateStream,
exposureManager: exposureManager,
dataController: dataController,
networkStatusStream: networkStatusStream)
networkStatusStream: networkStatusStream,
userNotificationCenter: userNotificationCenter)

exposureManager.activateCallCount = 0
mutableStateStream.updateCallCount = 0
Expand All @@ -40,6 +42,10 @@ final class ExposureControllerTests: TestCase {
mutableStateStream.exposureState = stream.eraseToAnyPublisher()

exposureManager.getExposureNotificationStatusHandler = { .active }

userNotificationCenter.getAuthorizationStatusHandler = { handler in
handler(.authorized)
}
}

func test_activate_activesAndUpdatesStream() {
Expand Down Expand Up @@ -339,6 +345,8 @@ final class ExposureControllerTests: TestCase {
}

private func triggerUpdateStream() {
controller.requestPushNotificationPermission {}

// trigger status update by mocking enabling notifications
exposureManager.setExposureNotificationEnabledHandler = { _, completion in completion(.success(())) }

Expand Down

0 comments on commit db55a1d

Please sign in to comment.