Skip to content
This repository has been archived by the owner on Jun 17, 2022. It is now read-only.

Commit

Permalink
Feature/app shut down (#62)
Browse files Browse the repository at this point in the history
- In preparation of the app End of Life, check the configuration response for the EOL status and when it has been reached, clean up all exposure data, reset notifications and turn of the EN API and background checks
- New screen that will be displayed after EOL instead of the normal UI
- Onboarding flow also reflects to Dynamic Type changes in real time

Co-authored-by: Henry Jalonen <henry.jalonen@solita.fi>
  • Loading branch information
tmengesh and Henry Jalonen authored Mar 26, 2022
1 parent c1f953e commit 7072046
Show file tree
Hide file tree
Showing 16 changed files with 245 additions and 32 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased

### Added
- In preparation of the app End of Life, check the configuration response for the EOL status and when it has been reached, clean up all exposure data, reset notifications and turn of the EN API and background checks
- New screen that will be displayed after EOL instead of the normal UI

### Changed
- Onboarding flow also reflects to Dynamic Type changes in real time

## 2.4.3

### Changed
Expand Down
8 changes: 8 additions & 0 deletions Koronavilkku/Koronavilkku.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
objects = {

/* Begin PBXBuildFile section */
112CB85C27E31A0400960B59 /* EndOfLifeViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 112CB85B27E31A0400960B59 /* EndOfLifeViewController.swift */; };
112CB86127E8B3DE00960B59 /* StatisticsCard.swift in Sources */ = {isa = PBXBuildFile; fileRef = 112CB86027E8B3DE00960B59 /* StatisticsCard.swift */; };
327C940224BED1C9003C8429 /* ApiResponses.swift in Sources */ = {isa = PBXBuildFile; fileRef = 327C940124BED1C9003C8429 /* ApiResponses.swift */; };
3288A07724AC593300365B46 /* Backend.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3288A07624AC593300365B46 /* Backend.swift */; };
32899E4924C0680600CFFE27 /* UIViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 32899E4824C0680600CFFE27 /* UIViewController.swift */; };
Expand Down Expand Up @@ -168,6 +170,8 @@
/* End PBXContainerItemProxy section */

/* Begin PBXFileReference section */
112CB85B27E31A0400960B59 /* EndOfLifeViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EndOfLifeViewController.swift; sourceTree = "<group>"; };
112CB86027E8B3DE00960B59 /* StatisticsCard.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatisticsCard.swift; sourceTree = "<group>"; };
327C940124BED1C9003C8429 /* ApiResponses.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ApiResponses.swift; sourceTree = "<group>"; };
3288A07624AC593300365B46 /* Backend.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Backend.swift; sourceTree = "<group>"; };
32899E4824C0680600CFFE27 /* UIViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIViewController.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -451,6 +455,7 @@
EA0EB8CF25ED2D2F00A6D0DC /* NumberView.swift */,
EAF340C42580F1460092ED6E /* RadioButton.swift */,
56C26BAF24AF3A0B002F83C1 /* RoundedButton.swift */,
112CB86027E8B3DE00960B59 /* StatisticsCard.swift */,
);
path = Components;
sourceTree = "<group>";
Expand Down Expand Up @@ -574,6 +579,7 @@
D98EDD2C24CC9C93003E09A1 /* SettingsViewController.swift */,
D98EDD2A24CC7EBA003E09A1 /* SymptomsViewController.swift */,
32B5955D24AA21EC00CC386E /* TestViewController.swift */,
112CB85B27E31A0400960B59 /* EndOfLifeViewController.swift */,
);
path = ViewController;
sourceTree = "<group>";
Expand Down Expand Up @@ -917,6 +923,7 @@
EA8E2251258250FF0008EE92 /* TravelStatusViewController.swift in Sources */,
EA7BEC9225712B56005DDF3D /* BulletItem.swift in Sources */,
561878AE24AD2BDE004B7563 /* PublishTokensViewController.swift in Sources */,
112CB85C27E31A0400960B59 /* EndOfLifeViewController.swift in Sources */,
56E3349624CCC0BB0017CD9A /* MunicipalityContactInformationView.swift in Sources */,
EA4A59432561C25800E65CFB /* Publisher.swift in Sources */,
D98EDD2724CB3E17003E09A1 /* LinkItem.swift in Sources */,
Expand Down Expand Up @@ -1009,6 +1016,7 @@
EAF340C52580F1460092ED6E /* RadioButton.swift in Sources */,
563D44B924AB9CD3008FE2BB /* Dictionary+IndexSubscript.swift in Sources */,
EAC4CD1A25CC21CF001A4648 /* Footer.swift in Sources */,
112CB86127E8B3DE00960B59 /* StatisticsCard.swift in Sources */,
56E3349224CB02C00017CD9A /* ExposureDetectionData.swift in Sources */,
EA71E990251E25E900E66F64 /* ChangeLanguageViewController.swift in Sources */,
EAC4CD0425CBD67C001A4648 /* NoExposuresView.swift in Sources */,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -313,3 +313,7 @@

"ExposureNotificationTitle" = "Potential exposure";
"ExposureNotificationBody" = "You may have been exposed to the coronavirus. Open Koronavilkku and see instructions.";

// MARK: EndOfLifeViewController
"EndOfLifeTitle" = "Koronavilkku is no longer in use";
"EndOfLifeMessage" = "You can now uninstall Koronavilkku from your phone. Thank you for using the app and doing your part in the fight against coronavirus!";
Original file line number Diff line number Diff line change
Expand Up @@ -313,3 +313,7 @@

"ExposureNotificationTitle" = "Mahdollinen altistuminen";
"ExposureNotificationBody" = "Olet saattanut altistua koronavirukselle. Avaa Koronavilkku ja katso toimintaohjeet.";

// MARK: EndOfLifeViewController
"EndOfLifeTitle" = "Koronavilkun toiminta on päättynyt";
"EndOfLifeMessage" = "Voit nyt poistaa Koronavilkun puhelimestasi. Kiitos, että käytit sovellusta ja osallistuit koronaviruksen torjuntaan!";
Original file line number Diff line number Diff line change
Expand Up @@ -313,3 +313,7 @@

"ExposureNotificationTitle" = "Eventuell exponering";
"ExposureNotificationBody" = "Du kan ha exponerats för coronaviruset. Öppna Coronablinkern och se anvisningarna.";

// MARK: EndOfLifeViewController
"EndOfLifeTitle" = "Coronablinkern har lagts ner";
"EndOfLifeMessage" = "Nu kan du avlägsna Coronablinkern från din telefon. Tack för att du använde appen och deltog i bekämpningen av coronaviruset!";
7 changes: 7 additions & 0 deletions Koronavilkku/Koronavilkku/Model/ExposureConfiguration.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ struct ExposureConfiguration: Codable {
let daysSinceExposureThreshold: Int
let minimumWindowScore: Double
let minimumDailyScore: Int

let endOfLifeReached: Bool
let endOfLifeStatistics: [EndOfLifeStatistic]

let daysSinceOnsetToInfectiousness: [String: String]

Expand Down Expand Up @@ -47,6 +50,10 @@ struct ExposureConfiguration: Codable {
}
}

// MARK: - EndOfLifeStatistic
struct EndOfLifeStatistic: Codable {
let value, label: Localized
}

extension ENExposureConfiguration {
convenience init(from: ExposureConfiguration) {
Expand Down
31 changes: 30 additions & 1 deletion Koronavilkku/Koronavilkku/Repository/ExposureRepository.swift
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ protocol ExposureRepository {
func deleteBatchFiles()
func removeExpiredExposures()
func showExposureNotification(delay: TimeInterval?)

var isEndOfLife: Bool { get }
var onEndOfLife: AnyPublisher<Void, Never> { get }
}

struct ExposureRepositoryImpl : ExposureRepository {
Expand Down Expand Up @@ -105,9 +108,35 @@ struct ExposureRepositoryImpl : ExposureRepository {
}

func getConfiguration() -> AnyPublisher<ExposureConfiguration, Error> {
return backend.getConfiguration()
return backend.getConfiguration().receive(on: RunLoop.main).map { config in
if config.endOfLifeReached {
LocalStore.shared.endOfLifeStatisticsData = config.endOfLifeStatistics
notificationService.updateBadgeNumber(nil)
LocalStore.shared.exposures.removeAll()
LocalStore.shared.countExposureNotifications.removeAll()
LocalStore.shared.daysExposureNotifications.removeAll()

if exposureManager.exposureNotificationStatus != .unauthorized {
setStatus(enabled: false)
}
}

return config
}
.eraseToAnyPublisher()
}

var isEndOfLife: Bool {
!LocalStore.shared.endOfLifeStatisticsData.isEmpty
}

var onEndOfLife: AnyPublisher<Void, Never> {
LocalStore.shared.$endOfLifeStatisticsData.$wrappedValue
.filter { !$0.isEmpty }
.map { _ in }
.eraseToAnyPublisher()
}

func detectExposures(ids: [String], config: ExposureConfiguration) -> AnyPublisher<Bool, Error> {

let urls = ids.map { self.storage.getFileUrls(forBatchId: $0) }.flatMap { $0 }
Expand Down
3 changes: 3 additions & 0 deletions Koronavilkku/Koronavilkku/Repository/LocalStore.swift
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,9 @@ class LocalStore : BatchIdCache {
@Persisted(userDefaultsKey: "detectionData", notificationName: .init("LocalStoreDetectionData"), defaultValue: [])
var detectionData: [ExposureDetectionData]

@Persisted(userDefaultsKey: "endOfLifeStatisticsData", notificationName: .init("EndOfLifeStatisticsDataDidChange"), defaultValue: [])
var endOfLifeStatisticsData: [EndOfLifeStatistic]

func updateDateLastPerformedExposureDetection() {
dateLastPerformedExposureDetection = Date()
}
Expand Down
59 changes: 43 additions & 16 deletions Koronavilkku/Koronavilkku/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4,33 +4,43 @@ import UIKit
class SceneDelegate: UIResponder, UIWindowSceneDelegate, UNUserNotificationCenterDelegate {
var window: UIWindow?
var activationTask: AnyCancellable?
var configurationTask: AnyCancellable?
var endOfLifeTask: AnyCancellable?

override init() {
super.init()
UNUserNotificationCenter.current().delegate = self
}

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
// Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`.
// If using a storyboard, the `window` property will automatically be initialized and attached to the scene.
// This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead).
if let windowScene = scene as? UIWindowScene {
let window = UIWindow(windowScene: windowScene)

// Check onboarding status
if !LocalStore.shared.isOnboarded {
// Set every UILabel automatically respond to Dynamic Type changes
UILabel.appearance().adjustsFontForContentSizeCategory = true

switch true {
case Environment.default.exposureRepository.isEndOfLife:
window.rootViewController = EndOfLifeViewController()

case !LocalStore.shared.isOnboarded:
window.rootViewController = OnboardingViewController()
} else {

default:
window.rootViewController = RootViewController()
}

self.window = window
window.makeKeyAndVisible()

// Check if user activity contains browsing action
if let activity = connectionOptions.userActivities
.filter({ $0.activityType == NSUserActivityTypeBrowsingWeb })
.first,
let code = extractCode(userActivity: activity) {
let code = extractCode(userActivity: activity) {
openCodeView(using: code)
}
}
Expand All @@ -52,10 +62,10 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UNUserNotificationCente

func extractCode(userActivity: NSUserActivity) -> String? {
guard userActivity.activityType == NSUserActivityTypeBrowsingWeb,
let url = userActivity.webpageURL,
let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true),
let code = components.query,
let _ = UInt64(code) else { return nil }
let url = userActivity.webpageURL,
let components = NSURLComponents(url: url, resolvingAgainstBaseURL: true),
let code = components.query,
let _ = UInt64(code) else { return nil }
return code
}

Expand All @@ -65,25 +75,42 @@ class SceneDelegate: UIResponder, UIWindowSceneDelegate, UNUserNotificationCente
// Release any resources associated with this scene that can be re-created the next time the scene connects.
// The scene may re-connect later, as its session was not neccessarily discarded (see `application:didDiscardSceneSessions` instead).
}

func sceneDidBecomeActive(_ scene: UIScene) {
let exposureRepository = Environment.default.exposureRepository

guard !exposureRepository.isEndOfLife else { return }

// Detect when the end of life has been reached an update the UI accordingly
endOfLifeTask = exposureRepository.onEndOfLife
.receive(on: RunLoop.main)
.sink { _ in
self.window?.rootViewController = EndOfLifeViewController()
}

// Fetch configuration to refresh the EFGS country list and end of life data
configurationTask = exposureRepository.getConfiguration()
.sink { _ in } receiveValue: { config in
Environment.default.efgsRepository.updateCountryList(from: config)
}

activationTask = ExposureManagerProvider.shared.activated.sink { activated in
Environment.default.exposureRepository.refreshStatus()
exposureRepository.refreshStatus()
}

Environment.default.exposureRepository.removeExpiredExposures()
}

exposureRepository.removeExpiredExposures()
}

func sceneWillResignActive(_ scene: UIScene) {
// Called when the scene will move from an active state to an inactive state.
// This may occur due to temporary interruptions (ex. an incoming phone call).
}

func sceneWillEnterForeground(_ scene: UIScene) {
// Called as the scene transitions from the background to the foreground.
// Use this method to undo the changes made on entering the background.
}

func sceneDidEnterBackground(_ scene: UIScene) {
// Called as the scene transitions from the foreground to the background.
// Use this method to save data, release shared resources, and store enough scene-specific state information
Expand Down
10 changes: 9 additions & 1 deletion Koronavilkku/Koronavilkku/Tools/BackgroundTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,11 @@ final class BackgroundTaskForNotifications: BackgroundTask {
Log.d("Register background task for notifications")
BGTaskScheduler.shared.register(forTaskWithIdentifier: identifier, using: .main) { task in
Log.d("Run background task for notifications")


guard !Environment.default.exposureRepository.isEndOfLife else {
return task.setTaskCompleted(success: true)
}

// reschedule first to prevent unexpected errors from breaking the chain
self.schedule()

Expand Down Expand Up @@ -207,6 +211,10 @@ fileprivate final class BackgroundTaskForDummyPosting: BackgroundTask {
BGTaskScheduler.shared.register(forTaskWithIdentifier: identifier, using: .main) { task in
Log.d("Run background task for dummy posting")

guard !Environment.default.exposureRepository.isEndOfLife else {
return task.setTaskCompleted(success: true)
}

// reschedule first to prevent unexpected errors from breaking the chain
self.schedule()

Expand Down
3 changes: 3 additions & 0 deletions Koronavilkku/Koronavilkku/Tools/LivePreview.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ extension Environment {
}

struct PreviewExposureRepository: ExposureRepository {
var isEndOfLife = false
var onEndOfLife = Empty<Void, Never>().eraseToAnyPublisher()

let state: PreviewState

func getExposureNotifications() -> AnyPublisher<[ExposureNotification], Never> {
Expand Down
3 changes: 3 additions & 0 deletions Koronavilkku/Koronavilkku/Tools/Translation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,7 @@ enum Translation: String, Localizable {

case ExposureNotificationTitle
case ExposureNotificationBody

case EndOfLifeTitle
case EndOfLifeMessage
}
37 changes: 37 additions & 0 deletions Koronavilkku/Koronavilkku/UI/Components/StatisticsCard.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import UIKit

final class StatisticsCard: CardElement {
init(title: String, body: String) {
super.init()

let titleView = UILabel(label: title, font: .heading2, color: .Primary.blue)
titleView.numberOfLines = 0

let bodyView = UILabel(label: body, font: .bodySmall, color: .Greyscale.black)
bodyView.numberOfLines = 0

if #available(iOS 14.0, *) {
titleView.lineBreakStrategy = .hangulWordPriority
bodyView.lineBreakStrategy = .hangulWordPriority
}

addSubview(titleView)
addSubview(bodyView)

titleView.snp.makeConstraints { make in
make.top.left.right.equalToSuperview().inset(20)
}

bodyView.snp.makeConstraints { make in
make.top.equalTo(titleView.snp.bottom)
make.bottom.left.right.equalToSuperview().inset(20)
}

isAccessibilityElement = true
accessibilityLabel = "\(title) \(body)"
}

required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
}
Loading

0 comments on commit 7072046

Please sign in to comment.