mirror of
https://github.com/element-hq/element-x-ios.git
synced 2025-03-10 21:39:12 +00:00
Show the lock screen placeholder on willResignActive. (#2086)
This commit is contained in:
parent
342feb4113
commit
0b9da83470
@ -40,6 +40,11 @@ class WindowManager {
|
|||||||
[mainWindow, overlayWindow, alternateWindow]
|
[mainWindow, overlayWindow, alternateWindow]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The task used to switch windows, so that we don't get stuck in the wrong state with a quick switch.
|
||||||
|
@CancellableTask private var switchTask: Task<Void, Error>?
|
||||||
|
/// A duration that allows window switching to wait a couple of frames to avoid a transition through black.
|
||||||
|
private let windowHideDelay = Duration.milliseconds(33)
|
||||||
|
|
||||||
/// Configures the window manager to operate on the supplied scene.
|
/// Configures the window manager to operate on the supplied scene.
|
||||||
func configure(with windowScene: UIWindowScene) {
|
func configure(with windowScene: UIWindowScene) {
|
||||||
mainWindow = windowScene.keyWindow
|
mainWindow = windowScene.keyWindow
|
||||||
@ -58,21 +63,33 @@ class WindowManager {
|
|||||||
|
|
||||||
/// Shows the main and overlay window combo, hiding the alternate window.
|
/// Shows the main and overlay window combo, hiding the alternate window.
|
||||||
func switchToMain() {
|
func switchToMain() {
|
||||||
mainWindow.isHidden = false
|
switchTask = Task {
|
||||||
overlayWindow.isHidden = false
|
mainWindow.isHidden = false
|
||||||
alternateWindow.isHidden = true
|
overlayWindow.isHidden = false
|
||||||
|
|
||||||
|
// Delay hiding to make sure the main windows are visible.
|
||||||
|
try await Task.sleep(for: windowHideDelay)
|
||||||
|
|
||||||
|
alternateWindow.isHidden = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Shows the alternate window, hiding the main and overlay combo.
|
/// Shows the alternate window, hiding the main and overlay combo.
|
||||||
func switchToAlternate() {
|
func switchToAlternate() {
|
||||||
alternateWindow.isHidden = false
|
switchTask = Task {
|
||||||
overlayWindow.isHidden = true
|
alternateWindow.isHidden = false
|
||||||
mainWindow.isHidden = true
|
|
||||||
|
// We don't know what route the app will use when returning back
|
||||||
// We don't know what route the app will use when returning back
|
// to the main window, so end any editing operation now to avoid
|
||||||
// to the main window, so end any editing operation now to avoid
|
// e.g. the keyboard being displayed on top of a call sheet.
|
||||||
// e.g. the keyboard being displayed on top of a call sheet.
|
mainWindow.endEditing(true)
|
||||||
mainWindow.endEditing(true)
|
|
||||||
|
// Delay hiding to make sure the alternate window is visible.
|
||||||
|
try await Task.sleep(for: windowHideDelay)
|
||||||
|
|
||||||
|
overlayWindow.isHidden = true
|
||||||
|
mainWindow.isHidden = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,15 +49,21 @@ class AppLockFlowCoordinator: CoordinatorProtocol {
|
|||||||
// Set the initial background state.
|
// Set the initial background state.
|
||||||
showPlaceholder()
|
showPlaceholder()
|
||||||
|
|
||||||
|
notificationCenter.publisher(for: UIApplication.willResignActiveNotification)
|
||||||
|
.sink { [weak self] _ in
|
||||||
|
self?.applicationWillResignActive()
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
notificationCenter.publisher(for: UIApplication.didEnterBackgroundNotification)
|
notificationCenter.publisher(for: UIApplication.didEnterBackgroundNotification)
|
||||||
.sink { [weak self] _ in
|
.sink { [weak self] _ in
|
||||||
self?.applicationDidEnterBackground()
|
self?.applicationDidEnterBackground()
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
|
|
||||||
notificationCenter.publisher(for: UIApplication.willEnterForegroundNotification)
|
notificationCenter.publisher(for: UIApplication.didBecomeActiveNotification)
|
||||||
.sink { [weak self] _ in
|
.sink { [weak self] _ in
|
||||||
self?.applicationWillEnterForeground()
|
self?.applicationDidBecomeActive()
|
||||||
}
|
}
|
||||||
.store(in: &cancellables)
|
.store(in: &cancellables)
|
||||||
}
|
}
|
||||||
@ -68,19 +74,23 @@ class AppLockFlowCoordinator: CoordinatorProtocol {
|
|||||||
|
|
||||||
// MARK: - App unlock
|
// MARK: - App unlock
|
||||||
|
|
||||||
private func applicationDidEnterBackground() {
|
private func applicationWillResignActive() {
|
||||||
unlockTask = nil
|
unlockTask = nil
|
||||||
|
|
||||||
guard appLockService.isEnabled else { return }
|
guard appLockService.isEnabled else { return }
|
||||||
|
|
||||||
appLockService.applicationDidEnterBackground()
|
|
||||||
showPlaceholder()
|
showPlaceholder()
|
||||||
}
|
}
|
||||||
|
|
||||||
private func applicationWillEnterForeground() {
|
private func applicationDidEnterBackground() {
|
||||||
|
guard appLockService.isEnabled else { return }
|
||||||
|
appLockService.applicationDidEnterBackground()
|
||||||
|
showPlaceholder() // Double call but just to be safe
|
||||||
|
}
|
||||||
|
|
||||||
|
private func applicationDidBecomeActive() {
|
||||||
guard appLockService.isEnabled else { return }
|
guard appLockService.isEnabled else { return }
|
||||||
|
|
||||||
guard appLockService.computeNeedsUnlock(willEnterForegroundAt: .now) else {
|
guard appLockService.computeNeedsUnlock(didBecomeActiveAt: .now) else {
|
||||||
// Reveal the app again if within the grace period.
|
// Reveal the app again if within the grace period.
|
||||||
actionsSubject.send(.unlockApp)
|
actionsSubject.send(.unlockApp)
|
||||||
return
|
return
|
||||||
|
@ -246,23 +246,23 @@ class AppLockServiceMock: AppLockServiceProtocol {
|
|||||||
}
|
}
|
||||||
//MARK: - computeNeedsUnlock
|
//MARK: - computeNeedsUnlock
|
||||||
|
|
||||||
var computeNeedsUnlockWillEnterForegroundAtCallsCount = 0
|
var computeNeedsUnlockDidBecomeActiveAtCallsCount = 0
|
||||||
var computeNeedsUnlockWillEnterForegroundAtCalled: Bool {
|
var computeNeedsUnlockDidBecomeActiveAtCalled: Bool {
|
||||||
return computeNeedsUnlockWillEnterForegroundAtCallsCount > 0
|
return computeNeedsUnlockDidBecomeActiveAtCallsCount > 0
|
||||||
}
|
}
|
||||||
var computeNeedsUnlockWillEnterForegroundAtReceivedDate: Date?
|
var computeNeedsUnlockDidBecomeActiveAtReceivedDate: Date?
|
||||||
var computeNeedsUnlockWillEnterForegroundAtReceivedInvocations: [Date] = []
|
var computeNeedsUnlockDidBecomeActiveAtReceivedInvocations: [Date] = []
|
||||||
var computeNeedsUnlockWillEnterForegroundAtReturnValue: Bool!
|
var computeNeedsUnlockDidBecomeActiveAtReturnValue: Bool!
|
||||||
var computeNeedsUnlockWillEnterForegroundAtClosure: ((Date) -> Bool)?
|
var computeNeedsUnlockDidBecomeActiveAtClosure: ((Date) -> Bool)?
|
||||||
|
|
||||||
func computeNeedsUnlock(willEnterForegroundAt date: Date) -> Bool {
|
func computeNeedsUnlock(didBecomeActiveAt date: Date) -> Bool {
|
||||||
computeNeedsUnlockWillEnterForegroundAtCallsCount += 1
|
computeNeedsUnlockDidBecomeActiveAtCallsCount += 1
|
||||||
computeNeedsUnlockWillEnterForegroundAtReceivedDate = date
|
computeNeedsUnlockDidBecomeActiveAtReceivedDate = date
|
||||||
computeNeedsUnlockWillEnterForegroundAtReceivedInvocations.append(date)
|
computeNeedsUnlockDidBecomeActiveAtReceivedInvocations.append(date)
|
||||||
if let computeNeedsUnlockWillEnterForegroundAtClosure = computeNeedsUnlockWillEnterForegroundAtClosure {
|
if let computeNeedsUnlockDidBecomeActiveAtClosure = computeNeedsUnlockDidBecomeActiveAtClosure {
|
||||||
return computeNeedsUnlockWillEnterForegroundAtClosure(date)
|
return computeNeedsUnlockDidBecomeActiveAtClosure(date)
|
||||||
} else {
|
} else {
|
||||||
return computeNeedsUnlockWillEnterForegroundAtReturnValue
|
return computeNeedsUnlockDidBecomeActiveAtReturnValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
//MARK: - unlock
|
//MARK: - unlock
|
||||||
|
@ -111,8 +111,8 @@ class AppLockService: AppLockServiceProtocol {
|
|||||||
timer.applicationDidEnterBackground()
|
timer.applicationDidEnterBackground()
|
||||||
}
|
}
|
||||||
|
|
||||||
func computeNeedsUnlock(willEnterForegroundAt date: Date) -> Bool {
|
func computeNeedsUnlock(didBecomeActiveAt date: Date) -> Bool {
|
||||||
timer.computeLockState(willEnterForegroundAt: date)
|
timer.computeLockState(didBecomeActiveAt: date)
|
||||||
}
|
}
|
||||||
|
|
||||||
func unlock(with pinCode: String) -> Bool {
|
func unlock(with pinCode: String) -> Bool {
|
||||||
|
@ -59,7 +59,7 @@ protocol AppLockServiceProtocol: AnyObject {
|
|||||||
/// Informs the service that the app has entered the background.
|
/// Informs the service that the app has entered the background.
|
||||||
func applicationDidEnterBackground()
|
func applicationDidEnterBackground()
|
||||||
/// Decides whether the app should be unlocked with a PIN code/biometrics on foregrounding.
|
/// Decides whether the app should be unlocked with a PIN code/biometrics on foregrounding.
|
||||||
func computeNeedsUnlock(willEnterForegroundAt date: Date) -> Bool
|
func computeNeedsUnlock(didBecomeActiveAt date: Date) -> Bool
|
||||||
|
|
||||||
/// Attempt to unlock the app with the supplied PIN code.
|
/// Attempt to unlock the app with the supplied PIN code.
|
||||||
func unlock(with pinCode: String) -> Bool
|
func unlock(with pinCode: String) -> Bool
|
||||||
|
@ -21,13 +21,18 @@ class AppLockTimer {
|
|||||||
/// The amount of time the app should remain unlocked for whilst backgrounded.
|
/// The amount of time the app should remain unlocked for whilst backgrounded.
|
||||||
let gracePeriod: TimeInterval
|
let gracePeriod: TimeInterval
|
||||||
|
|
||||||
/// Whether the timer considers the app to be locked or not.
|
/// Whether the timer considers the app to be locked or not. Always starts with a locked app.
|
||||||
///
|
///
|
||||||
/// Internally this value may be incorrect, always call `needsUnlock` to get the correct value.
|
/// Internally this value may be incorrect, always call `needsUnlock` to get the correct value.
|
||||||
private var isLocked = true
|
private var isLocked = true
|
||||||
/// The date when the app was last backgrounded whilst in an unlocked state.
|
/// The date when the app was last backgrounded whilst in an unlocked state.
|
||||||
private var lastUnlockedBackground: Date?
|
private var lastUnlockedBackground: Date?
|
||||||
|
|
||||||
|
/// Whether or not the app has moved to the background.
|
||||||
|
///
|
||||||
|
/// This allows us to distinguish between `willResignActive` and `didEnterBackground`.
|
||||||
|
private var isInBackground = false
|
||||||
|
|
||||||
/// Creates a new timer.
|
/// Creates a new timer.
|
||||||
/// - Parameter gracePeriod: The amount of time the app should remain unlocked for whilst backgrounded.
|
/// - Parameter gracePeriod: The amount of time the app should remain unlocked for whilst backgrounded.
|
||||||
init(gracePeriod: TimeInterval) {
|
init(gracePeriod: TimeInterval) {
|
||||||
@ -36,13 +41,18 @@ class AppLockTimer {
|
|||||||
|
|
||||||
/// Signals to the timer to track how long the app will be backgrounded for.
|
/// Signals to the timer to track how long the app will be backgrounded for.
|
||||||
func applicationDidEnterBackground(date: Date = .now) {
|
func applicationDidEnterBackground(date: Date = .now) {
|
||||||
|
isInBackground = true
|
||||||
// Only update the last background date if the app is currently unlocked.
|
// Only update the last background date if the app is currently unlocked.
|
||||||
guard !isLocked else { return }
|
guard !isLocked else { return }
|
||||||
lastUnlockedBackground = date
|
lastUnlockedBackground = date
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Asks the timer to recompute the lock state on foregrounding.
|
/// Asks the timer to recompute the lock state when active. If ``applicationDidEnterBackground`` hasn't been
|
||||||
func computeLockState(willEnterForegroundAt date: Date) -> Bool {
|
/// called since the last computation, then this method won't recompute it, instead directly returning the previous state.
|
||||||
|
func computeLockState(didBecomeActiveAt date: Date) -> Bool {
|
||||||
|
guard isInBackground else { return isLocked }
|
||||||
|
isInBackground = false
|
||||||
|
|
||||||
guard !isLocked, let lastUnlockedBackground else { return true }
|
guard !isLocked, let lastUnlockedBackground else { return true }
|
||||||
|
|
||||||
isLocked = date.timeIntervalSince(lastUnlockedBackground) >= gracePeriod
|
isLocked = date.timeIntervalSince(lastUnlockedBackground) >= gracePeriod
|
||||||
|
@ -187,7 +187,9 @@ class MockScreen: Identifiable {
|
|||||||
context: context)
|
context: context)
|
||||||
|
|
||||||
if id == .appLockFlowAlternateWindow {
|
if id == .appLockFlowAlternateWindow {
|
||||||
guard case .success = appLockService.setupPINCode("2023") else {
|
let pinCode = "2023"
|
||||||
|
guard case .success = appLockService.setupPINCode(pinCode),
|
||||||
|
appLockService.unlock(with: pinCode) else {
|
||||||
fatalError("Failed to preset the PIN code.")
|
fatalError("Failed to preset the PIN code.")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ class AppLockUITests: XCTestCase {
|
|||||||
try await app.assertScreenshot(.appLockFlow, step: Step.placeholder)
|
try await app.assertScreenshot(.appLockFlow, step: Step.placeholder)
|
||||||
|
|
||||||
// When foregrounding the app.
|
// When foregrounding the app.
|
||||||
try client.send(.notification(name: UIApplication.willEnterForegroundNotification))
|
try client.send(.notification(name: UIApplication.didBecomeActiveNotification))
|
||||||
|
|
||||||
// Then the Lock Screen should be shown to enter a PIN.
|
// Then the Lock Screen should be shown to enter a PIN.
|
||||||
try await app.assertScreenshot(.appLockFlow, step: Step.lockScreen)
|
try await app.assertScreenshot(.appLockFlow, step: Step.lockScreen)
|
||||||
@ -73,7 +73,7 @@ class AppLockUITests: XCTestCase {
|
|||||||
try await app.assertScreenshot(.appLockFlow, step: Step.unlocked)
|
try await app.assertScreenshot(.appLockFlow, step: Step.unlocked)
|
||||||
|
|
||||||
// When foregrounding the app.
|
// When foregrounding the app.
|
||||||
try client.send(.notification(name: UIApplication.willEnterForegroundNotification))
|
try client.send(.notification(name: UIApplication.didBecomeActiveNotification))
|
||||||
|
|
||||||
// Then the app should still remain unlocked.
|
// Then the app should still remain unlocked.
|
||||||
try await app.assertScreenshot(.appLockFlow, step: Step.unlocked)
|
try await app.assertScreenshot(.appLockFlow, step: Step.unlocked)
|
||||||
@ -87,7 +87,7 @@ class AppLockUITests: XCTestCase {
|
|||||||
|
|
||||||
try await app.assertScreenshot(.appLockFlow, step: Step.unlocked)
|
try await app.assertScreenshot(.appLockFlow, step: Step.unlocked)
|
||||||
try client.send(.notification(name: UIApplication.didEnterBackgroundNotification))
|
try client.send(.notification(name: UIApplication.didEnterBackgroundNotification))
|
||||||
try client.send(.notification(name: UIApplication.willEnterForegroundNotification))
|
try client.send(.notification(name: UIApplication.didBecomeActiveNotification))
|
||||||
try await app.assertScreenshot(.appLockFlow, step: Step.lockScreen)
|
try await app.assertScreenshot(.appLockFlow, step: Step.lockScreen)
|
||||||
|
|
||||||
// When entering an incorrect PIN
|
// When entering an incorrect PIN
|
||||||
@ -106,6 +106,28 @@ class AppLockUITests: XCTestCase {
|
|||||||
try await app.assertScreenshot(.appLockFlow, step: Step.forcedLogout)
|
try await app.assertScreenshot(.appLockFlow, step: Step.forcedLogout)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testResignActive() async throws {
|
||||||
|
// Given an app with screen lock enabled.
|
||||||
|
let client = try UITestsSignalling.Client(mode: .tests)
|
||||||
|
app = Application.launch(.appLockFlow)
|
||||||
|
await client.waitForApp()
|
||||||
|
|
||||||
|
// Blank form representing an unlocked app.
|
||||||
|
try await app.assertScreenshot(.appLockFlow, step: Step.unlocked)
|
||||||
|
|
||||||
|
// When the app resigns active but doesn't enter the background.
|
||||||
|
try client.send(.notification(name: UIApplication.willResignActiveNotification))
|
||||||
|
|
||||||
|
// Then the placeholder screen should obscure the content.
|
||||||
|
try await app.assertScreenshot(.appLockFlow, step: Step.placeholder)
|
||||||
|
|
||||||
|
// When the app becomes active again.
|
||||||
|
try client.send(.notification(name: UIApplication.didBecomeActiveNotification))
|
||||||
|
|
||||||
|
// Then the app should not have become unlock.
|
||||||
|
try await app.assertScreenshot(.appLockFlow, step: Step.unlocked)
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Helpers
|
// MARK: - Helpers
|
||||||
|
|
||||||
func enterPIN() {
|
func enterPIN() {
|
||||||
|
@ -34,63 +34,63 @@ class AppLockTimerTests: XCTestCase {
|
|||||||
|
|
||||||
func testTimerLockedOnStartup() {
|
func testTimerLockedOnStartup() {
|
||||||
setupTimer(unlocked: false)
|
setupTimer(unlocked: false)
|
||||||
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now),
|
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now),
|
||||||
"The app should be locked on a fresh launch.")
|
"The app should be locked on a fresh launch.")
|
||||||
|
|
||||||
setupTimer(unlocked: false)
|
setupTimer(unlocked: false)
|
||||||
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now + 1),
|
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + 1),
|
||||||
"The app should be locked after a fresh launch.")
|
"The app should be locked after a fresh launch.")
|
||||||
|
|
||||||
setupTimer(unlocked: false)
|
setupTimer(unlocked: false)
|
||||||
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now + halfGracePeriod),
|
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + halfGracePeriod),
|
||||||
"The app should be locked after a fresh launch.")
|
"The app should be locked after a fresh launch.")
|
||||||
|
|
||||||
setupTimer(unlocked: false)
|
setupTimer(unlocked: false)
|
||||||
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now + gracePeriod),
|
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + gracePeriod),
|
||||||
"The app should be locked after a fresh launch.")
|
"The app should be locked after a fresh launch.")
|
||||||
|
|
||||||
setupTimer(unlocked: false)
|
setupTimer(unlocked: false)
|
||||||
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now + gracePeriodX10),
|
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + gracePeriodX10),
|
||||||
"The app should be locked after a fresh launch.")
|
"The app should be locked after a fresh launch.")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testTimerBeforeFirstUnlock() {
|
func testTimerBeforeFirstUnlock() {
|
||||||
setupTimer(unlocked: false, backgroundedAt: now)
|
setupTimer(unlocked: false, backgroundedAt: now)
|
||||||
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now),
|
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now),
|
||||||
"The app should always remain locked after backgrounding when locked.")
|
"The app should always remain locked after backgrounding when locked.")
|
||||||
|
|
||||||
setupTimer(unlocked: false, backgroundedAt: now)
|
setupTimer(unlocked: false, backgroundedAt: now)
|
||||||
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now + 1),
|
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + 1),
|
||||||
"The app should always remain locked after backgrounding when locked.")
|
"The app should always remain locked after backgrounding when locked.")
|
||||||
|
|
||||||
setupTimer(unlocked: false, backgroundedAt: now)
|
setupTimer(unlocked: false, backgroundedAt: now)
|
||||||
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now + halfGracePeriod),
|
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + halfGracePeriod),
|
||||||
"The app should always remain locked after backgrounding when locked.")
|
"The app should always remain locked after backgrounding when locked.")
|
||||||
|
|
||||||
setupTimer(unlocked: false, backgroundedAt: now)
|
setupTimer(unlocked: false, backgroundedAt: now)
|
||||||
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now + gracePeriod),
|
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + gracePeriod),
|
||||||
"The app should always remain locked after backgrounding when locked.")
|
"The app should always remain locked after backgrounding when locked.")
|
||||||
|
|
||||||
setupTimer(unlocked: false, backgroundedAt: now)
|
setupTimer(unlocked: false, backgroundedAt: now)
|
||||||
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now + gracePeriodX10),
|
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + gracePeriodX10),
|
||||||
"The app should always remain locked after backgrounding when locked.")
|
"The app should always remain locked after backgrounding when locked.")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testTimerWhenUnlocked() {
|
func testTimerWhenUnlocked() {
|
||||||
setupTimer(unlocked: true, backgroundedAt: now)
|
setupTimer(unlocked: true, backgroundedAt: now)
|
||||||
XCTAssertFalse(timer.computeLockState(willEnterForegroundAt: now + 1),
|
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: now + 1),
|
||||||
"The app should remain unlocked when it was unlocked and backgrounded for less then the grace period.")
|
"The app should remain unlocked when it was unlocked and backgrounded for less then the grace period.")
|
||||||
|
|
||||||
setupTimer(unlocked: true, backgroundedAt: now)
|
setupTimer(unlocked: true, backgroundedAt: now)
|
||||||
XCTAssertFalse(timer.computeLockState(willEnterForegroundAt: now + halfGracePeriod),
|
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: now + halfGracePeriod),
|
||||||
"The app should remain unlocked when it was unlocked and backgrounded for less then the grace period.")
|
"The app should remain unlocked when it was unlocked and backgrounded for less then the grace period.")
|
||||||
|
|
||||||
setupTimer(unlocked: true, backgroundedAt: now)
|
setupTimer(unlocked: true, backgroundedAt: now)
|
||||||
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now + gracePeriod),
|
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + gracePeriod),
|
||||||
"The app should become locked when it was unlocked and backgrounded for more than the grace period.")
|
"The app should become locked when it was unlocked and backgrounded for more than the grace period.")
|
||||||
|
|
||||||
setupTimer(unlocked: true, backgroundedAt: now)
|
setupTimer(unlocked: true, backgroundedAt: now)
|
||||||
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now + gracePeriodX10),
|
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + gracePeriodX10),
|
||||||
"The app should become locked when it was unlocked and backgrounded for more than the grace period.")
|
"The app should become locked when it was unlocked and backgrounded for more than the grace period.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,27 +98,27 @@ class AppLockTimerTests: XCTestCase {
|
|||||||
setupTimer(unlocked: true, backgroundedAt: now)
|
setupTimer(unlocked: true, backgroundedAt: now)
|
||||||
|
|
||||||
var nextCheck = now + halfGracePeriod
|
var nextCheck = now + halfGracePeriod
|
||||||
XCTAssertFalse(timer.computeLockState(willEnterForegroundAt: nextCheck),
|
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: nextCheck),
|
||||||
"The app should remain unlocked when it was unlocked and backgrounded for less then the grace period.")
|
"The app should remain unlocked when it was unlocked and backgrounded for less then the grace period.")
|
||||||
timer.applicationDidEnterBackground(date: nextCheck)
|
timer.applicationDidEnterBackground(date: nextCheck)
|
||||||
|
|
||||||
nextCheck = now + gracePeriod
|
nextCheck = now + gracePeriod
|
||||||
XCTAssertFalse(timer.computeLockState(willEnterForegroundAt: nextCheck),
|
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: nextCheck),
|
||||||
"The app should remain unlocked when repeating the backgrounded and foreground within the grace period.")
|
"The app should remain unlocked when repeating the backgrounded and foreground within the grace period.")
|
||||||
timer.applicationDidEnterBackground(date: nextCheck)
|
timer.applicationDidEnterBackground(date: nextCheck)
|
||||||
|
|
||||||
nextCheck = now + gracePeriod + halfGracePeriod
|
nextCheck = now + gracePeriod + halfGracePeriod
|
||||||
XCTAssertFalse(timer.computeLockState(willEnterForegroundAt: nextCheck),
|
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: nextCheck),
|
||||||
"The app should remain unlocked when repeating the backgrounded and foreground within the grace period.")
|
"The app should remain unlocked when repeating the backgrounded and foreground within the grace period.")
|
||||||
timer.applicationDidEnterBackground(date: nextCheck)
|
timer.applicationDidEnterBackground(date: nextCheck)
|
||||||
|
|
||||||
nextCheck = now + gracePeriodX2
|
nextCheck = now + gracePeriodX2
|
||||||
XCTAssertFalse(timer.computeLockState(willEnterForegroundAt: nextCheck),
|
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: nextCheck),
|
||||||
"The app should remain unlocked when repeating the backgrounded and foreground within the grace period.")
|
"The app should remain unlocked when repeating the backgrounded and foreground within the grace period.")
|
||||||
timer.applicationDidEnterBackground(date: nextCheck)
|
timer.applicationDidEnterBackground(date: nextCheck)
|
||||||
|
|
||||||
nextCheck = now + gracePeriodX10
|
nextCheck = now + gracePeriodX10
|
||||||
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: nextCheck),
|
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: nextCheck),
|
||||||
"The app should become locked however when finally staying backgrounded for longer than the grace period.")
|
"The app should become locked however when finally staying backgrounded for longer than the grace period.")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,22 +128,59 @@ class AppLockTimerTests: XCTestCase {
|
|||||||
let backgroundDate = now + gracePeriodX10
|
let backgroundDate = now + gracePeriodX10
|
||||||
timer.applicationDidEnterBackground(date: backgroundDate)
|
timer.applicationDidEnterBackground(date: backgroundDate)
|
||||||
|
|
||||||
XCTAssertFalse(timer.computeLockState(willEnterForegroundAt: backgroundDate + 1),
|
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: backgroundDate + 1),
|
||||||
"The grace period should be measured from the time the app was backgrounded, and not when it was unlocked.")
|
"The grace period should be measured from the time the app was backgrounded, and not when it was unlocked.")
|
||||||
}
|
}
|
||||||
|
|
||||||
func testChangingTimeLocksApp() {
|
func testChangingTimeLocksApp() {
|
||||||
setupTimer(unlocked: true, backgroundedAt: now)
|
setupTimer(unlocked: true, backgroundedAt: now)
|
||||||
XCTAssertTrue(timer.computeLockState(willEnterForegroundAt: now - 1),
|
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now - 1),
|
||||||
"The the device's clock is changed to before the app was backgrounded, the device should remain locked.")
|
"The the device's clock is changed to before the app was backgrounded, the device should remain locked.")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func testNoGracePeriod() {
|
||||||
|
// Given a timer with no grace period that is in the background.
|
||||||
|
setupTimer(gracePeriod: 0, unlocked: true)
|
||||||
|
let backgroundDate = now + 1
|
||||||
|
timer.applicationDidEnterBackground(date: backgroundDate)
|
||||||
|
|
||||||
|
// Then the app should be locked immediately.
|
||||||
|
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: backgroundDate))
|
||||||
|
}
|
||||||
|
|
||||||
|
func testResignActive() {
|
||||||
|
// Given a timer with no grace period.
|
||||||
|
setupTimer(gracePeriod: 0, unlocked: true)
|
||||||
|
|
||||||
|
// When entering the background.
|
||||||
|
timer.applicationDidEnterBackground(date: now)
|
||||||
|
|
||||||
|
// Then the app should be locked.
|
||||||
|
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + 1))
|
||||||
|
|
||||||
|
// When the app resigns active but doesn't enter the background.
|
||||||
|
// (Nothing to do here, we just don't call applicationDidEnterBackground).
|
||||||
|
|
||||||
|
// Then the app should also remain locked.
|
||||||
|
XCTAssertTrue(timer.computeLockState(didBecomeActiveAt: now + 2))
|
||||||
|
|
||||||
|
// When unlocking the app and resigning active (but not entering the background)
|
||||||
|
timer.registerUnlock()
|
||||||
|
// (Again, nothing to do here for resigning active)
|
||||||
|
|
||||||
|
// Then the app should not become locked.
|
||||||
|
XCTAssertFalse(timer.computeLockState(didBecomeActiveAt: now + 3))
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
|
||||||
/// Sets up the timer for testing.
|
/// Sets up the timer for testing.
|
||||||
/// - Parameters:
|
/// - Parameters:
|
||||||
|
/// - gracePeriod: Set up the test with a custom grace period for the timer. Defaults to 3 minutes.
|
||||||
/// - unlocked: Whether the timer should consider itself unlocked or not.
|
/// - unlocked: Whether the timer should consider itself unlocked or not.
|
||||||
/// - backgroundedDate: If not nil, the timer will consider the app to have been backgrounded at the specified date.
|
/// - backgroundedDate: If not nil, the timer will consider the app to have been backgrounded at the specified date.
|
||||||
private func setupTimer(unlocked: Bool, backgroundedAt backgroundedDate: Date? = nil) {
|
private func setupTimer(gracePeriod: TimeInterval = 180, unlocked: Bool, backgroundedAt backgroundedDate: Date? = nil) {
|
||||||
timer = AppLockTimer(gracePeriod: 180)
|
timer = AppLockTimer(gracePeriod: gracePeriod)
|
||||||
if unlocked {
|
if unlocked {
|
||||||
timer.registerUnlock()
|
timer.registerUnlock()
|
||||||
}
|
}
|
||||||
|
1
changelog.d/2026.change
Normal file
1
changelog.d/2026.change
Normal file
@ -0,0 +1 @@
|
|||||||
|
Show the lock screen placeholder whenever the app resigns as active.
|
Loading…
x
Reference in New Issue
Block a user