Skip to content

Commit

Permalink
Implemented custom idle timer logic (#1028)
Browse files Browse the repository at this point in the history
  • Loading branch information
michalrentka authored Nov 21, 2024
1 parent 89f2768 commit f75bf36
Show file tree
Hide file tree
Showing 15 changed files with 144 additions and 97 deletions.
6 changes: 5 additions & 1 deletion Zotero.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,7 @@
B307A2732704A87D005986B3 /* IdleTimerController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B307A2722704A87D005986B3 /* IdleTimerController.swift */; };
B30A44A629B8799600332B4E /* MasterContainerViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B30A44A529B8799600332B4E /* MasterContainerViewController.swift */; };
B30A44AE29B88E7200332B4E /* TagFilterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = B30A44AD29B88E7200332B4E /* TagFilterViewController.swift */; };
B30A6C7E2CEE0AD500E7174B /* EventObservableWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = B30A6C7D2CEE0AD000E7174B /* EventObservableWindow.swift */; };
B30A8C672582690900EC56FB /* HighlightAnnotation.swift in Sources */ = {isa = PBXBuildFile; fileRef = B30A8C662582690900EC56FB /* HighlightAnnotation.swift */; };
B30B405F2490CAFC00FAAF6D /* ItemCell.xib in Resources */ = {isa = PBXBuildFile; fileRef = B30B405E2490CAFC00FAAF6D /* ItemCell.xib */; };
B30B40612490D84300FAAF6D /* ItemCellModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = B30B40602490D84300FAAF6D /* ItemCellModel.swift */; };
Expand Down Expand Up @@ -1487,6 +1488,7 @@
B307E55D22D4A87B00592B3C /* SyncActionsSpec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SyncActionsSpec.swift; sourceTree = "<group>"; };
B30A44A529B8799600332B4E /* MasterContainerViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MasterContainerViewController.swift; sourceTree = "<group>"; };
B30A44AD29B88E7200332B4E /* TagFilterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagFilterViewController.swift; sourceTree = "<group>"; };
B30A6C7D2CEE0AD000E7174B /* EventObservableWindow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EventObservableWindow.swift; sourceTree = "<group>"; };
B30A8C662582690900EC56FB /* HighlightAnnotation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HighlightAnnotation.swift; sourceTree = "<group>"; };
B30B405E2490CAFC00FAAF6D /* ItemCell.xib */ = {isa = PBXFileReference; lastKnownFileType = file.xib; path = ItemCell.xib; sourceTree = "<group>"; };
B30B40602490D84300FAAF6D /* ItemCellModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemCellModel.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -2872,13 +2874,14 @@
B305652B23FC051E003304F2 /* Models */,
B305651323FC051E003304F2 /* Scenes */,
B30D59582206F60400884C4A /* AppDelegate.swift */,
B30A6C7D2CEE0AD000E7174B /* EventObservableWindow.swift */,
B30D59662206F60500884C4A /* Info.plist */,
B30D59632206F60500884C4A /* LaunchScreen.storyboard */,
B39B18E7223947050019F467 /* main.swift */,
618D83E62BAAC88C00E7966B /* PrivacyInfo.xcprivacy */,
B3231333243F0F0000F1905A /* SceneDelegate.swift */,
B39EE32A223A5E3500302E29 /* TestAppDelegate.swift */,
B3F3A6A922F97D4300E3A09C /* Zotero.entitlements */,
618D83E62BAAC88C00E7966B /* PrivacyInfo.xcprivacy */,
61099E6A2C91BAF300EDD92C /* Zotero-Bridging-Header.h */,
);
path = Zotero;
Expand Down Expand Up @@ -5092,6 +5095,7 @@
B37AA28C289960FF00A1C643 /* ItemDetailNoteContentView.swift in Sources */,
B30BE127297AAD9E000AED6A /* AnnotationToolOptionsActionHandler.swift in Sources */,
B338E820273AA9B20003DECD /* DisappearActionHostingController.swift in Sources */,
B30A6C7E2CEE0AD500E7174B /* EventObservableWindow.swift in Sources */,
B305667423FC051F003304F2 /* MD5+Url.swift in Sources */,
B31FACCA25DBC71200DD5F14 /* ConflictReceiverAlertHandler.swift in Sources */,
B398A917270C6A5B00968EE8 /* WebDavSessionStorage.swift in Sources */,
Expand Down
1 change: 0 additions & 1 deletion Zotero/Assets/en.lproj/Localizable.strings
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,6 @@
"pdf.locked.locked" = "Locked";
"pdf.locked.enter_password" = "Please enter the password to open this PDF.";
"pdf.locked.failed" = "Incorrect password. Please try again.";
"pdf.settings.idle_timer_title" = "Allow device to sleep";
"pdf.settings.scroll_direction.title" = "Scroll Direction";
"pdf.settings.scroll_direction.horizontal" = "Horizontal";
"pdf.settings.scroll_direction.vertical" = "Vertical";
Expand Down
31 changes: 20 additions & 11 deletions Zotero/Controllers/Controllers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -416,8 +416,6 @@ final class UserControllers {
itemLocaleController.loadLocale()
autoEmptyController.autoEmptyIfNeeded()

// Enable idleTimerController before syncScheduler inProgress observation starts
idleTimerController.enable()
// Reset Defaults.shared.didPerformFullSyncFix if needed
DDLogInfo("Controllers: performFullSyncGuard: \(Defaults.shared.performFullSyncGuard); currentPerformFullSyncGuard: \(Defaults.currentPerformFullSyncGuard)")
if Defaults.shared.performFullSyncGuard < Defaults.currentPerformFullSyncGuard {
Expand All @@ -432,15 +430,26 @@ final class UserControllers {
syncScheduler.inProgress
.skip(1)
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] inProgress in
if inProgress {
self?.idleTimerController.disable()
} else {
self?.idleTimerController.enable()
if !Defaults.shared.didPerformFullSyncFix {
Defaults.shared.didPerformFullSyncFix = true
DDLogInfo("Controllers: didPerformFullSyncFix: \(Defaults.shared.didPerformFullSyncFix)")
}
.subscribe(onNext: { inProgress in
if !inProgress && !Defaults.shared.didPerformFullSyncFix {
Defaults.shared.didPerformFullSyncFix = true
DDLogInfo("Controllers: didPerformFullSyncFix: \(Defaults.shared.didPerformFullSyncFix)")
}
})
.disposed(by: disposeBag)
syncScheduler.syncController.progressObservable
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] progress in
guard let self else { return }
switch progress {
case .starting:
idleTimerController.startCustomIdleTimer()

case .finished, .aborted:
idleTimerController.stopCustomIdleTimer()

default:
idleTimerController.resetCustomTimer()
}
})
.disposed(by: disposeBag)
Expand Down
105 changes: 91 additions & 14 deletions Zotero/Controllers/IdleTimerController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -8,35 +8,112 @@

import Foundation
import UIKit

import CocoaLumberjackSwift
import RxSwift

final class IdleTimerController {
private static let customIdleTimerTimeout: DispatchTimeInterval = .seconds(1200)
private let disposeBag: DisposeBag
/// Processes which require idle timer disabled
private var activeProcesses: Int = 0
private var activeTimer: DispatchSourceTimer?

init() {
disposeBag = DisposeBag()
observeOrientationChange()
observeLowPowerMode()

func observeOrientationChange() {
NotificationCenter.default.rx
.notification(UIDevice.orientationDidChangeNotification)
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] _ in
self?.resetCustomTimer()
})
.disposed(by: disposeBag)
}

func observeLowPowerMode() {
NotificationCenter.default.rx
.notification(.NSProcessInfoPowerStateDidChange)
.observe(on: MainScheduler.instance)
.subscribe(onNext: { [weak self] _ in
guard ProcessInfo.processInfo.isLowPowerModeEnabled, let self else { return }
forceStopIdleTimer()
})
.disposed(by: disposeBag)
}
}

func disable() {
func resetCustomTimer() {
inMainThread { [weak self] in
guard let self = self else { return }
self.activeProcesses += 1
UIApplication.shared.isIdleTimerDisabled = true
DDLogInfo("IdleTimerController: disable idle timer \(self.activeProcesses)")
guard let activeTimer = self?.activeTimer else { return }
DDLogInfo("IdleTimerController: reset idle timer")
activeTimer.suspend()
activeTimer.schedule(deadline: .now() + Self.customIdleTimerTimeout)
activeTimer.resume()
}
}

func enable() {
func startCustomIdleTimer() {
guard !ProcessInfo.processInfo.isLowPowerModeEnabled else { return }
inMainThread { [weak self] in
guard let self = self else { return }
guard self.activeProcesses > 0 else {
guard let self else { return }
activeProcesses += 1
DDLogInfo("IdleTimerController: disable idle timer \(activeProcesses)")
guard activeTimer == nil else { return }
set(disabled: true)
startTimer(controller: self)
UIDevice.current.beginGeneratingDeviceOrientationNotifications()
}

func startTimer(controller: IdleTimerController) {
let timer = DispatchSource.makeTimerSource(flags: [], queue: .main)
timer.schedule(deadline: .now() + Self.customIdleTimerTimeout)
timer.setEventHandler(handler: { [weak controller] in
controller?.forceStopIdleTimer()
})
timer.resume()
controller.activeTimer = timer
}
}

func stopCustomIdleTimer() {
inMainThread { [weak self] in
guard let self else { return }

if activeProcesses > 0 {
activeProcesses -= 1
DDLogInfo("IdleTimerController: enable idle timer \(activeProcesses)")
} else {
DDLogWarn("IdleTimerController: tried to enable idle timer with no active processes")
return
activeProcesses = 0
}

self.activeProcesses -= 1

DDLogInfo("IdleTimerController: enable idle timer \(self.activeProcesses)")
guard activeProcesses == 0 else { return }

guard self.activeProcesses == 0 else { return }
UIApplication.shared.isIdleTimerDisabled = false
set(disabled: false)
activeTimer?.suspend()
activeTimer?.setEventHandler(handler: nil)
activeTimer?.cancel()
/*
If the timer is suspended, calling cancel without resuming
triggers a crash. This is documented here https://forums.developer.apple.com/thread/15902
*/
activeTimer?.resume()
activeTimer = nil
UIDevice.current.endGeneratingDeviceOrientationNotifications()
}
}

private func set(disabled: Bool) {
UIApplication.shared.isIdleTimerDisabled = disabled
}

private func forceStopIdleTimer() {
DDLogInfo("IdleTimerController: force stop timer")
activeProcesses = 1
stopCustomIdleTimer()
}
}
17 changes: 17 additions & 0 deletions Zotero/EventObservableWindow.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
//
// EventObservableWindow.swift
// Zotero
//
// Created by Michal Rentka on 20.11.2024.
// Copyright © 2024 Corporation for Digital Scholarship. All rights reserved.
//

import UIKit

final class EventObservableWindow: UIWindow {
override func sendEvent(_ event: UIEvent) {
super.sendEvent(event)
guard !(event.allTouches ?? []).isEmpty else { return }
(UIApplication.shared.delegate as? AppDelegate)?.controllers.idleTimerController.resetCustomTimer()
}
}
2 changes: 0 additions & 2 deletions Zotero/Extensions/Localizable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1040,8 +1040,6 @@ internal enum L10n {
internal static let title = L10n.tr("Localizable", "pdf.search.title", fallback: "Search in Document")
}
internal enum Settings {
/// Allow device to sleep
internal static let idleTimerTitle = L10n.tr("Localizable", "pdf.settings.idle_timer_title", fallback: "Allow device to sleep")
internal enum Appearance {
/// Automatic
internal static let auto = L10n.tr("Localizable", "pdf.settings.appearance.auto", fallback: "Automatic")
Expand Down
2 changes: 1 addition & 1 deletion Zotero/SceneDelegate.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ final class SceneDelegate: UIResponder, UIWindowSceneDelegate {
// Assign activity counter
activityCounter = delegate
// Create window for scene
let window = UIWindow(frame: frame)
let window = EventObservableWindow(frame: frame)
self.window = window
window.windowScene = windowScene
window.makeKeyAndVisible()
Expand Down
4 changes: 0 additions & 4 deletions Zotero/Scenes/Detail/PDF/Models/PDFSettings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ struct PDFSettings {
var direction: ScrollDirection
var pageFitting: PDFConfiguration.SpreadFitting
var appearanceMode: ReaderSettingsState.Appearance
var idleTimerDisabled: Bool
var isFirstPageAlwaysSingle: Bool

static var `default`: PDFSettings {
Expand All @@ -26,7 +25,6 @@ struct PDFSettings {
direction: .horizontal,
pageFitting: .adaptive,
appearanceMode: .automatic,
idleTimerDisabled: false,
isFirstPageAlwaysSingle: true
)
}
Expand All @@ -52,8 +50,6 @@ extension PDFSettings: Codable {
self.pageMode = PageMode(rawValue: modeRaw) ?? .automatic
self.pageFitting = PDFConfiguration.SpreadFitting(rawValue: fittingRaw) ?? .adaptive
self.isFirstPageAlwaysSingle = isFirstPageAlwaysSingle
// This setting is not persisted, always defaults to false
self.idleTimerDisabled = false
}

func encode(to encoder: Encoder) throws {
Expand Down
4 changes: 2 additions & 2 deletions Zotero/Scenes/Detail/PDF/PDFCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ extension PDFCoordinator: PdfReaderCoordinatorDelegate {

let state = ReaderSettingsState(settings: settings)
let viewModel = ViewModel(initialState: state, handler: ReaderSettingsActionHandler())
let baseController = ReaderSettingsViewController(rows: [.pageTransition, .pageMode, .pageSpreads, .scrollDirection, .pageFitting, .appearance, .sleep], viewModel: viewModel)
let baseController = ReaderSettingsViewController(rows: [.pageTransition, .pageMode, .pageSpreads, .scrollDirection, .pageFitting, .appearance], viewModel: viewModel)

let controller: UIViewController
if UIDevice.current.userInterfaceIdiom == .pad {
Expand All @@ -392,7 +392,7 @@ extension PDFCoordinator: PdfReaderCoordinatorDelegate {

controller.modalPresentationStyle = UIDevice.current.userInterfaceIdiom == .pad ? .popover : .formSheet
controller.popoverPresentationController?.barButtonItem = sender
controller.preferredContentSize = CGSize(width: 480, height: 350)
controller.preferredContentSize = CGSize(width: 480, height: 308)
controller.overrideUserInterfaceStyle = settings.appearanceMode.userInterfaceStyle
self.navigationController?.present(controller, animated: true, completion: nil)

Expand Down
27 changes: 5 additions & 22 deletions Zotero/Scenes/Detail/PDF/ViewModels/PDFReaderActionHandler.swift
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi
update(settings: settings, parentInterfaceStyle: userInterfaceStyle, in: viewModel)

case .changeIdleTimerDisabled(let disabled):
changeIdleTimer(disabled: disabled, in: viewModel)
changeIdleTimer(disabled: disabled)

case .setSidebarEditingEnabled(let enabled):
setSidebar(editing: enabled, in: viewModel)
Expand Down Expand Up @@ -473,32 +473,15 @@ final class PDFReaderActionHandler: ViewModelActionHandler, BackgroundDbProcessi
}
}

private func changeIdleTimer(disabled: Bool, in viewModel: ViewModel<PDFReaderActionHandler>) {
guard viewModel.state.settings.idleTimerDisabled != disabled else { return }
var settings = viewModel.state.settings
settings.idleTimerDisabled = disabled

update(viewModel: viewModel) { state in
state.settings = settings
// Don't need to assign `changes` or update Defaults.shared.pdfSettings, this setting is not stored and doesn't change anything else
}

if settings.idleTimerDisabled {
idleTimerController.disable()
private func changeIdleTimer(disabled: Bool) {
if disabled {
idleTimerController.startCustomIdleTimer()
} else {
idleTimerController.enable()
idleTimerController.stopCustomIdleTimer()
}
}

private func update(settings: PDFSettings, parentInterfaceStyle: UIUserInterfaceStyle, in viewModel: ViewModel<PDFReaderActionHandler>) {
if viewModel.state.settings.idleTimerDisabled != settings.idleTimerDisabled {
if settings.idleTimerDisabled {
idleTimerController.disable()
} else {
idleTimerController.enable()
}
}

// Update local state
update(viewModel: viewModel) { state in
state.settings = settings
Expand Down
4 changes: 2 additions & 2 deletions Zotero/Scenes/Detail/PDF/Views/PDFReaderViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,7 @@ class PDFReaderViewController: UIViewController {
set(userActivity: .contentActivity(with: [openItem], libraryId: viewModel.state.library.identifier, collectionId: Defaults.shared.selectedCollectionId)
.set(title: viewModel.state.title)
)
viewModel.process(action: .changeIdleTimerDisabled(true))
view.backgroundColor = .systemGray6
// Create intraDocumentNavigationHandler before setting up views, as it may be called by a child view controller, before view has finished loading.
intraDocumentNavigationHandler = IntraDocumentNavigationButtonsHandler(
Expand Down Expand Up @@ -381,6 +382,7 @@ class PDFReaderViewController: UIViewController {
}

deinit {
viewModel.process(action: .changeIdleTimerDisabled(false))
DDLogInfo("PDFReaderViewController deinitialized")
}

Expand Down Expand Up @@ -651,7 +653,6 @@ class PDFReaderViewController: UIViewController {
direction: state.scrollDirection,
pageFitting: state.pageFitting,
appearanceMode: state.appearance,
idleTimerDisabled: state.idleTimerDisabled,
isFirstPageAlwaysSingle: state.isFirstPageAlwaysSingle
)
viewModel.process(action: .setSettings(settings: settings, parentUserInterfaceStyle: interfaceStyle))
Expand All @@ -663,7 +664,6 @@ class PDFReaderViewController: UIViewController {
if let page = documentController?.pdfController?.pageIndex {
viewModel.process(action: .submitPendingPage(Int(page)))
}
viewModel.process(action: .changeIdleTimerDisabled(false))
viewModel.process(action: .clearTmpData)
navigationController?.presentingViewController?.dismiss(animated: true, completion: nil)
}
Expand Down
1 change: 0 additions & 1 deletion Zotero/Scenes/General/Models/ReaderSettingsAction.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,4 @@ enum ReaderSettingsAction {
case setPageSpreads(isFirstPageAlwaysSingle: Bool)
// General
case setAppearance(ReaderSettingsState.Appearance)
case setIdleTimerDisabled(Bool)
}
2 changes: 0 additions & 2 deletions Zotero/Scenes/General/Models/ReaderSettingsState.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ struct ReaderSettingsState: ViewModelState {
var pageFitting: PSPDFKitUI.PDFConfiguration.SpreadFitting
var appearance: ReaderSettingsState.Appearance
var isFirstPageAlwaysSingle: Bool
var idleTimerDisabled: Bool

init(settings: PDFSettings) {
transition = settings.transition
Expand All @@ -40,7 +39,6 @@ struct ReaderSettingsState: ViewModelState {
pageFitting = settings.pageFitting
appearance = settings.appearanceMode
isFirstPageAlwaysSingle = settings.isFirstPageAlwaysSingle
idleTimerDisabled = settings.idleTimerDisabled
}

func cleanup() {}
Expand Down
Loading

0 comments on commit f75bf36

Please sign in to comment.