Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implemented custom idle timer logic #1028

Merged
merged 4 commits into from
Nov 21, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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()
michalrentka marked this conversation as resolved.
Show resolved Hide resolved
}
}
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 }
michalrentka marked this conversation as resolved.
Show resolved Hide resolved
(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