Skip to content

Commit

Permalink
Add unit tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
samsymons committed Feb 22, 2022
1 parent aeb650c commit eb9fc5b
Show file tree
Hide file tree
Showing 6 changed files with 310 additions and 119 deletions.
8 changes: 4 additions & 4 deletions DuckDuckGo.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
4B1E6EF227AB5E5D00F51793 /* PasswordManagementItemList.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B1E6EF027AB5E5D00F51793 /* PasswordManagementItemList.swift */; };
4B2CBF412767EEC1001DF04B /* MacWaitlistStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2CBF402767EEC1001DF04B /* MacWaitlistStoreTests.swift */; };
4B2E7D6326FF9D6500D2DB17 /* PrintingUserScript.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B2E7D6226FF9D6500D2DB17 /* PrintingUserScript.swift */; };
4B379C1527BD91E3008A968E /* QuartzIdleStateDetector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B379C1427BD91E3008A968E /* QuartzIdleStateDetector.swift */; };
4B379C1527BD91E3008A968E /* QuartzIdleStateProvider.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B379C1427BD91E3008A968E /* QuartzIdleStateProvider.swift */; };
4B379C1727BD9D7C008A968E /* LoginsPreferencesTableCellView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B379C1627BD9D7C008A968E /* LoginsPreferencesTableCellView.swift */; };
4B379C1927BD9EAA008A968E /* LoginsPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = 4B379C1827BD9EAA008A968E /* LoginsPreferences.swift */; };
4B379C1B27BD9F88008A968E /* LoginsPreferencesTableCellView.xib in Resources */ = {isa = PBXBuildFile; fileRef = 4B379C1A27BD9F88008A968E /* LoginsPreferencesTableCellView.xib */; };
Expand Down Expand Up @@ -761,7 +761,7 @@
4B1E6EF027AB5E5D00F51793 /* PasswordManagementItemList.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasswordManagementItemList.swift; sourceTree = "<group>"; };
4B2CBF402767EEC1001DF04B /* MacWaitlistStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MacWaitlistStoreTests.swift; sourceTree = "<group>"; };
4B2E7D6226FF9D6500D2DB17 /* PrintingUserScript.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PrintingUserScript.swift; sourceTree = "<group>"; };
4B379C1427BD91E3008A968E /* QuartzIdleStateDetector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuartzIdleStateDetector.swift; sourceTree = "<group>"; };
4B379C1427BD91E3008A968E /* QuartzIdleStateProvider.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuartzIdleStateProvider.swift; sourceTree = "<group>"; };
4B379C1627BD9D7C008A968E /* LoginsPreferencesTableCellView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginsPreferencesTableCellView.swift; sourceTree = "<group>"; };
4B379C1827BD9EAA008A968E /* LoginsPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginsPreferences.swift; sourceTree = "<group>"; };
4B379C1A27BD9F88008A968E /* LoginsPreferencesTableCellView.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = LoginsPreferencesTableCellView.xib; sourceTree = "<group>"; };
Expand Down Expand Up @@ -1590,7 +1590,7 @@
4BBC169F27C4859400E00A38 /* DeviceAuthenticationService.swift */,
4B379C2127BDBA29008A968E /* LocalAuthenticationService.swift */,
4BBC16A127C485BC00E00A38 /* DeviceIdleStateDetector.swift */,
4B379C1427BD91E3008A968E /* QuartzIdleStateDetector.swift */,
4B379C1427BD91E3008A968E /* QuartzIdleStateProvider.swift */,
4B379C1D27BDB7FF008A968E /* DeviceAuthenticator.swift */,
);
path = "Device Authentication";
Expand Down Expand Up @@ -4248,7 +4248,7 @@
B693955426F04BEC0015B914 /* ColorView.swift in Sources */,
B6BBF17427475B15004F850E /* PopupBlockedPopover.swift in Sources */,
8589063A267BCD8E00D23B0D /* SaveCredentialsPopover.swift in Sources */,
4B379C1527BD91E3008A968E /* QuartzIdleStateDetector.swift in Sources */,
4B379C1527BD91E3008A968E /* QuartzIdleStateProvider.swift in Sources */,
AA72D5E325FE977F00C77619 /* AddEditFavoriteViewController.swift in Sources */,
B6C0B22E26E61CE70031CB7F /* DownloadViewModel.swift in Sources */,
B68458B825C7E8B200DC17B6 /* Tab+NSSecureCoding.swift in Sources */,
Expand Down
148 changes: 110 additions & 38 deletions DuckDuckGo/Device Authentication/DeviceAuthenticator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -28,30 +28,72 @@ extension NSNotification.Name {

final class DeviceAuthenticator {

private enum Constants {
static let intervalBetweenIdleChecks: TimeInterval = 1
}

static var deviceSupportsBiometrics: Bool {
let context = LAContext()
return context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: nil)
}

static let shared = DeviceAuthenticator()

// MARK: - Public

private(set) var isAuthenticating: Bool {
get {
return queue.sync {
_isAuthenticating
}
}

private var idleStateDetector: DeviceIdleStateDetector?
set (newState) {
queue.sync {
self._isAuthenticating = newState
}
}
}

// MARK: - Private Dependencies

private var idleStateProvider: DeviceIdleStateProvider
private let authenticationService: DeviceAuthenticationService
private let loginsPreferences: LoginsPreferences

// MARK: - Private State

private let queue = DispatchQueue(label: "Device Authenticator Queue")

private(set) var isAuthenticating: Bool = false
private(set) var deviceIsLocked: Bool {
didSet {
os_log("Device lock state changed: %s", log: .autoLock, deviceIsLocked ? "locked" : "unlocked")
private var timer: Timer?

private var _isAuthenticating: Bool = false
private var _deviceIsLocked: Bool = false

private var deviceIsLocked: Bool {
get {
return queue.sync {
_deviceIsLocked
}
}

set (newState) {
queue.sync {
self._deviceIsLocked = newState
}

if deviceIsLocked {
os_log("Device lock state changed: %s", log: .autoLock, deviceIsLocked ? "locked" : "unlocked")

if newState {
NotificationCenter.default.post(name: .deviceBecameLocked, object: nil)
}
}
}

init(authenticationService: DeviceAuthenticationService = LocalAuthenticationService(),
init(idleStateProvider: DeviceIdleStateProvider = QuartzIdleStateProvider(),
authenticationService: DeviceAuthenticationService = LocalAuthenticationService(),
loginsPreferences: LoginsPreferences = LoginsPreferences()) {
self.idleStateProvider = idleStateProvider
self.authenticationService = authenticationService
self.loginsPreferences = loginsPreferences
self.deviceIsLocked = loginsPreferences.shouldAutoLockLogins
Expand All @@ -71,35 +113,6 @@ final class DeviceAuthenticator {

return deviceIsLocked
}

@objc
private func updateTimerStateBasedOnAutoLockSettings() {
let preferences = LoginsPreferences()

if preferences.shouldAutoLockLogins {
beginCheckingIdleTimer()
} else {
self.idleStateDetector?.cancelIdleCheckTimer()
}
}

func beginCheckingIdleTimer() {
if self.idleStateDetector == nil {
self.idleStateDetector = DeviceIdleStateDetector(idleTimeCallback: self.checkIdleTimeIntervalAndLockIfNecessary(interval:))
}

guard !deviceIsLocked else {
os_log("Tried to start idle timer while device was already locked", log: .autoLock)
return
}

guard loginsPreferences.shouldAutoLockLogins else {
os_log("Tried to start idle timer but device should not auto-lock", log: .autoLock)
return
}

idleStateDetector?.beginIdleCheckTimer()
}

func authenticateUser(result: @escaping (Bool) -> Void) {
guard requiresAuthentication else {
Expand All @@ -119,17 +132,76 @@ final class DeviceAuthenticator {

if authenticated {
// Now that the user has unlocked the device, begin the idle timer again.
self.idleStateDetector?.beginIdleCheckTimer()
self.beginIdleCheckTimer()
}

result(authenticated)
}
}

func authenticateUser() async -> Bool {
await withCheckedContinuation { continuation in
authenticateUser { result in
continuation.resume(returning: result)
}
}
}

// MARK: - Idle Timer Monitoring

private func beginIdleCheckTimer() {
os_log("Beginning idle check timer", log: .autoLock)

self.timer?.invalidate()
self.timer = nil

let timer = Timer(timeInterval: Constants.intervalBetweenIdleChecks, repeats: true) { [weak self] _ in
guard let self = self else {
return
}

self.checkIdleTimeIntervalAndLockIfNecessary(interval: self.idleStateProvider.secondsSinceLastEvent())
}

self.timer = timer
RunLoop.current.add(timer, forMode: .common)
}

private func cancelIdleCheckTimer() {
os_log("Cancelling idle check timer", log: .autoLock)
self.timer?.invalidate()
self.timer = nil
}

@objc
private func updateTimerStateBasedOnAutoLockSettings() {
let preferences = LoginsPreferences()

if preferences.shouldAutoLockLogins {
beginCheckingIdleTimer()
} else {
cancelIdleCheckTimer()
}
}

func beginCheckingIdleTimer() {
guard !deviceIsLocked else {
os_log("Tried to start idle timer while device was already locked", log: .autoLock)
return
}

guard loginsPreferences.shouldAutoLockLogins else {
os_log("Tried to start idle timer but device should not auto-lock", log: .autoLock)
return
}

beginIdleCheckTimer()
}

private func checkIdleTimeIntervalAndLockIfNecessary(interval: TimeInterval) {
if interval >= loginsPreferences.autoLockThreshold.seconds {
self.deviceIsLocked = true
self.idleStateDetector?.cancelIdleCheckTimer()
self.cancelIdleCheckTimer()
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,8 @@

import Foundation

protocol DeviceIdleStateDetector {
protocol DeviceIdleStateProvider {

func beginIdleCheckTimer()

func cancelIdleCheckTimer()
func secondsSinceLastEvent() -> TimeInterval

}
73 changes: 0 additions & 73 deletions DuckDuckGo/Device Authentication/QuartzIdleStateDetector.swift

This file was deleted.

34 changes: 34 additions & 0 deletions DuckDuckGo/Device Authentication/QuartzIdleStateProvider.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// QuartzIdleStateProvider.swift
//
// Copyright © 2022 DuckDuckGo. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

import Foundation
import CoreGraphics
import os.log

final class QuartzIdleStateProvider: DeviceIdleStateProvider {

func secondsSinceLastEvent() -> TimeInterval {
let anyInputEventType = CGEventType(rawValue: ~0)!
let seconds = CGEventSource.secondsSinceLastEventType(.hidSystemState, eventType: anyInputEventType)

os_log("Idle duration since last user input event: %f", log: .autoLock, seconds)

return seconds
}

}
Loading

0 comments on commit eb9fc5b

Please sign in to comment.