From 5e5f8a8afbdde7637652450ecefd550fc30b9a1e Mon Sep 17 00:00:00 2001 From: Ernest Fan Date: Mon, 14 Nov 2022 20:45:54 -0800 Subject: [PATCH 1/8] Implement NYPLLastListenPositionSynchronizer --- Simplified.xcodeproj/project.pbxproj | 8 ++ .../NYPLLastListenPositionSynchronizer.swift | 87 +++++++++++++++++++ 2 files changed, 95 insertions(+) create mode 100644 Simplified/Audiobooks/NYPLLastListenPositionSynchronizer.swift diff --git a/Simplified.xcodeproj/project.pbxproj b/Simplified.xcodeproj/project.pbxproj index 3356844db..3d94e8891 100644 --- a/Simplified.xcodeproj/project.pbxproj +++ b/Simplified.xcodeproj/project.pbxproj @@ -141,6 +141,9 @@ 17631AEE25E488CD006079C4 /* NYPLAgeCheckViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17631AEC25E488CD006079C4 /* NYPLAgeCheckViewController.swift */; }; 17631AF025E488CD006079C4 /* NYPLAgeCheckViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17631AEC25E488CD006079C4 /* NYPLAgeCheckViewController.swift */; }; 1763C0D624F460FE00A4D0E2 /* NYPLAnnouncementManagerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1763C0D524F460FE00A4D0E2 /* NYPLAnnouncementManagerTests.swift */; }; + 1765A586291DE4470075A09E /* NYPLLastListenPositionSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1765A585291DE4470075A09E /* NYPLLastListenPositionSynchronizer.swift */; }; + 1765A587291DE4470075A09E /* NYPLLastListenPositionSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1765A585291DE4470075A09E /* NYPLLastListenPositionSynchronizer.swift */; }; + 1765A588291DE4470075A09E /* NYPLLastListenPositionSynchronizer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1765A585291DE4470075A09E /* NYPLLastListenPositionSynchronizer.swift */; }; 17681C5127E0314400DF1F8C /* NYPLCatalogUngroupedFeedWithZeroBooks.xml in Resources */ = {isa = PBXBuildFile; fileRef = 17681C5027E0314400DF1F8C /* NYPLCatalogUngroupedFeedWithZeroBooks.xml */; }; 17681C5227E0314400DF1F8C /* NYPLCatalogUngroupedFeedWithZeroBooks.xml in Resources */ = {isa = PBXBuildFile; fileRef = 17681C5027E0314400DF1F8C /* NYPLCatalogUngroupedFeedWithZeroBooks.xml */; }; 177C25FC28B704A600A786F1 /* NYPLUtilities in Frameworks */ = {isa = PBXBuildFile; productRef = 177C25FB28B704A600A786F1 /* NYPLUtilities */; }; @@ -1868,6 +1871,7 @@ 175E480724EF36520066A6CF /* NYPLAnnouncementBusinessLogic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NYPLAnnouncementBusinessLogic.swift; sourceTree = ""; }; 17631AEC25E488CD006079C4 /* NYPLAgeCheckViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NYPLAgeCheckViewController.swift; sourceTree = ""; }; 1763C0D524F460FE00A4D0E2 /* NYPLAnnouncementManagerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NYPLAnnouncementManagerTests.swift; sourceTree = ""; }; + 1765A585291DE4470075A09E /* NYPLLastListenPositionSynchronizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NYPLLastListenPositionSynchronizer.swift; sourceTree = ""; }; 17681C5027E0314400DF1F8C /* NYPLCatalogUngroupedFeedWithZeroBooks.xml */ = {isa = PBXFileReference; lastKnownFileType = text.xml; path = NYPLCatalogUngroupedFeedWithZeroBooks.xml; sourceTree = ""; }; 176F8A802519684D00CE5BFB /* AudioEngine.xcframework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcframework; name = AudioEngine.xcframework; path = NYPLAEToolkit/build/AudioEngine.xcframework; sourceTree = ""; }; 177E04FE28A72BD500DF7587 /* NYPLAudiobookBookmarksBusinessLogic.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NYPLAudiobookBookmarksBusinessLogic.swift; sourceTree = ""; }; @@ -2639,6 +2643,7 @@ 2198F90F250A90EE000D9DAB /* AudioBookVendorsHelper.swift */, 1785228B27F52B58004445ED /* AudiobookManifestAdapter.swift */, 1785229327F7B955004445ED /* NYPLAudiobookDownloader.swift */, + 1765A585291DE4470075A09E /* NYPLLastListenPositionSynchronizer.swift */, ); path = Audiobooks; sourceTree = ""; @@ -4677,6 +4682,7 @@ 739E60D9244A0D8600D00301 /* NYPLKeychain.m in Sources */, 7369A39D264AF9710029D8AB /* NYPLAdobeContentProtectionService.swift in Sources */, 739E60DC244A0D8600D00301 /* LibraryService.swift in Sources */, + 1765A587291DE4470075A09E /* NYPLLastListenPositionSynchronizer.swift in Sources */, 73A172F327ADA6FA005E7BCF /* NYPLAxisXMLCreator.swift in Sources */, 739E60DD244A0D8600D00301 /* NYPLOPDSEntry.m in Sources */, 739E60E0244A0D8600D00301 /* NYPLRootTabBarController+R2.swift in Sources */, @@ -4760,6 +4766,7 @@ 73EB0A7A25821DF4006BC997 /* NYPLMyBooksNavigationController.m in Sources */, 17FFE882278BC65F0084E65D /* NYPLOPDSFeedFetcher.swift in Sources */, 73EB0A7B25821DF4006BC997 /* NYPLSignInBusinessLogicUIDelegate.swift in Sources */, + 1765A588291DE4470075A09E /* NYPLLastListenPositionSynchronizer.swift in Sources */, 73EB0A7C25821DF4006BC997 /* NYPLBookCellCollectionViewController.m in Sources */, 73EB0A7D25821DF4006BC997 /* NYPLKeychainStoredVariable.swift in Sources */, 73EB0A7E25821DF4006BC997 /* NYPLXML.m in Sources */, @@ -5451,6 +5458,7 @@ 113DB8A719C24E54004E1154 /* NYPLIndeterminateProgressView.m in Sources */, 119BEB89198C43A600121439 /* NSString+NYPLStringAdditions.m in Sources */, E6B6E76F1F6859A4007EE361 /* NYPLKeychainManager.swift in Sources */, + 1765A586291DE4470075A09E /* NYPLLastListenPositionSynchronizer.swift in Sources */, 73A172DD27ADA6F9005E7BCF /* NYPLAxisXMLCreator.swift in Sources */, 089E42C6249A823800310360 /* NYPLCookiesWebViewController.swift in Sources */, 0826CD2924AA21B2000F4030 /* NYPLSamlIDPCell.swift in Sources */, diff --git a/Simplified/Audiobooks/NYPLLastListenPositionSynchronizer.swift b/Simplified/Audiobooks/NYPLLastListenPositionSynchronizer.swift new file mode 100644 index 000000000..a4209b9ac --- /dev/null +++ b/Simplified/Audiobooks/NYPLLastListenPositionSynchronizer.swift @@ -0,0 +1,87 @@ +// +// NYPLLastListenPositionSynchronizer.swift +// Simplified +// +// Created by Ernest Fan on 2022-11-10. +// Copyright © 2022 NYPL. All rights reserved. +// + +import Foundation +import NYPLAudiobookToolkit + +// TODO: Update NYPLAnnotations to have generic sync reading position function +// TODO: Implement network executor as property of NYPLLastListenPositionSynchronizer, see NYPLLastReadPositionSynchronizer +// TODO: Ask Risa about sync interval and chapterLocation vs server bookmark + +// Placeholder, move to audiobook toolkit +protocol NYPLLastListenPositionSynchronizing { + func getLastListenPosition(for bookID: String, + completion: @escaping (_ localPosition: NYPLAudiobookBookmark?, _ serverPosition: NYPLAudiobookBookmark?) -> ()) + func postLastListenPosition(_ location: NYPLAudiobookBookmark, for bookID: String) +} + +class NYPLLastListenPositionSynchronizer: NYPLLastListenPositionSynchronizing { + private let bookRegistryProvider: NYPLBookRegistryProvider + private let annotationSynchronizer: NYPLAnnotationSyncing.Type + + private let serialQueue = DispatchQueue(label: "\(Bundle.main.bundleIdentifier!).lastListenedPositionSynchronizer", target: .global(qos: .utility)) + private let renderer: String = "NYPLAudiobookToolkit" + + init(book: NYPLBook, + bookRegistryProvider: NYPLBookRegistryProvider, + annotationSynchronizer: NYPLAnnotationSyncing.Type) { + self.bookRegistryProvider = bookRegistryProvider + self.annotationSynchronizer = annotationSynchronizer + } + + func getLastListenPosition(for bookID: String, + completion: @escaping (_ localPosition: NYPLAudiobookBookmark?, _ serverPosition: NYPLAudiobookBookmark?) -> ()) { + serialQueue.async { [weak self] in + guard let self = self else { return } + + // Retrive local last-listened position + var localPosition: NYPLAudiobookBookmark? + + if let bookLocation = self.bookRegistryProvider.location(forIdentifier: bookID), + bookLocation.renderer == self.renderer { + if let chapterLocation = self.chapterLocation(from: bookLocation.locationString) { + // If the retrieved location is a ChapterLocation object, + // we should create a bookmark from it and return it without server position, + // since we cannot tell which location is the most up-to-date as + // ChapterLocation object has no creation date. + let newLocation = NYPLAudiobookBookmark(chapterLocation: chapterLocation, creationTime: Date()) + completion(newLocation, nil) + return + } + + if let bookmark = self.bookmark(from: bookLocation.locationString) { + localPosition = bookmark + } + } + + // Check if server sync allowed + // Return local last-listened position if not + guard self.annotationSynchronizer.syncIsPossibleAndPermitted() else { + completion(localPosition, nil) + return + } + + // Retrieve last-listened position from server, return both local and server positions + + } + } + + func postLastListenPosition(_ location: NYPLAudiobookBookmark, for bookID: String) { + + } + + // MARK: - Helper + + private func chapterLocation(from locationString: String) -> ChapterLocation? { + return nil + } + + private func bookmark(from locationString: String) -> NYPLAudiobookBookmark? { + return nil + } +} From 2238b85ebcd39a48bb74d8b05a9a7a3fbbaa63ac Mon Sep 17 00:00:00 2001 From: Ernest Fan Date: Mon, 28 Nov 2022 19:58:06 -0800 Subject: [PATCH 2/8] Update NYPLAnnotations last read function with generic type, implement sync and post function in NYPLLastListenPositionSynchronizer --- .../NYPLLastListenPositionSynchronizer.swift | 78 +++++++++++++------ .../Reader2/Networking/NYPLAnnotations.swift | 22 +++--- .../NYPLLastReadPositionSynchronizer.swift | 46 ++++++----- 3 files changed, 90 insertions(+), 56 deletions(-) diff --git a/Simplified/Audiobooks/NYPLLastListenPositionSynchronizer.swift b/Simplified/Audiobooks/NYPLLastListenPositionSynchronizer.swift index a4209b9ac..de722a2db 100644 --- a/Simplified/Audiobooks/NYPLLastListenPositionSynchronizer.swift +++ b/Simplified/Audiobooks/NYPLLastListenPositionSynchronizer.swift @@ -21,15 +21,17 @@ protocol NYPLLastListenPositionSynchronizing { } class NYPLLastListenPositionSynchronizer: NYPLLastListenPositionSynchronizing { + private let book: NYPLBook private let bookRegistryProvider: NYPLBookRegistryProvider - private let annotationSynchronizer: NYPLAnnotationSyncing.Type + private let annotationSynchronizer: NYPLAnnotationSyncing private let serialQueue = DispatchQueue(label: "\(Bundle.main.bundleIdentifier!).lastListenedPositionSynchronizer", target: .global(qos: .utility)) private let renderer: String = "NYPLAudiobookToolkit" init(book: NYPLBook, bookRegistryProvider: NYPLBookRegistryProvider, - annotationSynchronizer: NYPLAnnotationSyncing.Type) { + annotationSynchronizer: NYPLAnnotationSyncing) { + self.book = book self.bookRegistryProvider = bookRegistryProvider self.annotationSynchronizer = annotationSynchronizer } @@ -40,24 +42,7 @@ class NYPLLastListenPositionSynchronizer: NYPLLastListenPositionSynchronizing { guard let self = self else { return } // Retrive local last-listened position - var localPosition: NYPLAudiobookBookmark? - - if let bookLocation = self.bookRegistryProvider.location(forIdentifier: bookID), - bookLocation.renderer == self.renderer { - if let chapterLocation = self.chapterLocation(from: bookLocation.locationString) { - // If the retrieved location is a ChapterLocation object, - // we should create a bookmark from it and return it without server position, - // since we cannot tell which location is the most up-to-date as - // ChapterLocation object has no creation date. - let newLocation = NYPLAudiobookBookmark(chapterLocation: chapterLocation, creationTime: Date()) - completion(newLocation, nil) - return - } - - if let bookmark = self.bookmark(from: bookLocation.locationString) { - localPosition = bookmark - } - } + let localPosition = self.getLocalLastListenPosition(for: bookID) // Check if server sync allowed // Return local last-listened position if not @@ -67,21 +52,70 @@ class NYPLLastListenPositionSynchronizer: NYPLLastListenPositionSynchronizing { } // Retrieve last-listened position from server, return both local and server positions - + self.annotationSynchronizer.syncReadingPosition(of: NYPLAudiobookBookmark.self, + forBook: bookID, + publication: nil, + toURL: self.book.annotationsURL) { serverPosition in + guard let serverPosition = serverPosition else { + Log.info(#function, "No reading position annotation exists on the server for \(self.book.loggableShortString()).") + completion(localPosition, nil) + return + } + + // Pass through returning nil (meaning the server doesn't have a + // last listen position worth restoring) if: + // 1 - The most recent position on the server comes from the same device, or + // 2 - The server and the client have the same position marked + if localPosition?.device == serverPosition.device || + serverPosition.isEqual(localPosition) { + completion(localPosition, nil) + return + } + + completion(localPosition, serverPosition) + } } } func postLastListenPosition(_ location: NYPLAudiobookBookmark, for bookID: String) { - + let selectorValue = NYPLAudiobookBookmarkFactory.makeLocatorString(title: location.title ?? "", + part: location.part, + chapter: location.chapter, + audiobookId: location.audiobookId, + duration: location.duration, + time: location.time) + serialQueue.async { [weak self] in + self?.annotationSynchronizer.postReadingPosition(forBook: bookID, selectorValue: selectorValue) + } } // MARK: - Helper + private func getLocalLastListenPosition(for bookID: String) -> NYPLAudiobookBookmark? { + guard let bookLocation = self.bookRegistryProvider.location(forIdentifier: bookID), + bookLocation.renderer == self.renderer else { + return nil + } + + if let bookmark = self.bookmark(from: bookLocation.locationString) { + return bookmark + } + + if let chapterLocation = self.chapterLocation(from: bookLocation.locationString) { + // If the retrieved location is a ChapterLocation object, + // we should create a bookmark object from it. + return NYPLAudiobookBookmark(chapterLocation: chapterLocation, creationTime: Date()) + } + return nil + } + private func chapterLocation(from locationString: String) -> ChapterLocation? { + // TODO: return nil } private func bookmark(from locationString: String) -> NYPLAudiobookBookmark? { + // TODO: return nil } } diff --git a/Simplified/Reader2/Networking/NYPLAnnotations.swift b/Simplified/Reader2/Networking/NYPLAnnotations.swift index 42b843ad4..6bd02e875 100644 --- a/Simplified/Reader2/Networking/NYPLAnnotations.swift +++ b/Simplified/Reader2/Networking/NYPLAnnotations.swift @@ -25,10 +25,11 @@ protocol NYPLServerSyncChecking: AnyObject { protocol NYPLLastReadPositionSupportAPI: AnyObject { func syncIsPossibleAndPermitted() -> Bool - func syncReadingPosition(ofBook bookID: String?, - publication: Publication?, - toURL url: URL?, - completion: @escaping (_ readPos: NYPLReadiumBookmark?) -> ()) + func syncReadingPosition(of type: T.Type, + forBook bookID: String?, + publication: Publication?, + toURL url: URL?, + completion: @escaping (_ readPos: T?) -> ()) func postReadingPosition(forBook bookID: String, selectorValue: String) @@ -265,10 +266,11 @@ final class NYPLAnnotations: NSObject, NYPLAnnotationSyncing { /// Reads the current reading position from the server, parses the response /// and returns the result to the `completionHandler`. - func syncReadingPosition(ofBook bookID: String?, - publication: Publication?, - toURL url:URL?, - completion: @escaping (_ readPos: NYPLReadiumBookmark?) -> ()) { + func syncReadingPosition(of type: T.Type, + forBook bookID: String?, + publication: Publication?, + toURL url: URL?, + completion: @escaping (_ readPos: T?) -> ()) { guard syncIsPossibleAndPermitted() else { Log.info(#file, "Library account does not support sync or sync is disabled by user.") @@ -284,9 +286,9 @@ final class NYPLAnnotations: NSObject, NYPLAnnotationSyncing { _ = failFastExecutor.GET(url, cachePolicy: .reloadIgnoringLocalCacheData) { data, _, error in - let bookmarks: [NYPLReadiumBookmark]? = NYPLAnnotations + let bookmarks: [T]? = NYPLAnnotations .parseAnnotationsResponse(data, - of: NYPLReadiumBookmark.self, + of: T.self, error: error, motivation: .readingProgress, publication: publication, diff --git a/Simplified/Reader2/Networking/NYPLLastReadPositionSynchronizer.swift b/Simplified/Reader2/Networking/NYPLLastReadPositionSynchronizer.swift index d96b60747..00e38ddeb 100644 --- a/Simplified/Reader2/Networking/NYPLLastReadPositionSynchronizer.swift +++ b/Simplified/Reader2/Networking/NYPLLastReadPositionSynchronizer.swift @@ -107,34 +107,32 @@ class NYPLLastReadPositionSynchronizer: NYPLLastReadPositionSynchronizing { let localLocation = bookRegistry.location(forIdentifier: book.identifier) - synchronizer - .syncReadingPosition(ofBook: book.identifier, publication: publication, toURL: book.annotationsURL) { bookmark in - - guard let bookmark = bookmark else { - Log.info(#function, "No reading position annotation exists on the server for \(book.loggableShortString()).") - completion(nil) - return - } + synchronizer.syncReadingPosition(of: NYPLReadiumBookmark.self, forBook: book.identifier, publication: publication, toURL: book.annotationsURL) { bookmark in + guard let bookmark = bookmark else { + Log.info(#function, "No reading position annotation exists on the server for \(book.loggableShortString()).") + completion(nil) + return + } - let deviceID = bookmark.device ?? "" - let serverLocationString = bookmark.location + let deviceID = bookmark.device ?? "" + let serverLocationString = bookmark.location - // Pass through returning nil (meaning the server doesn't have a - // last read location worth restoring) if: - // 1 - The most recent page on the server comes from the same device, or - // 2 - The server and the client have the same page marked - if deviceID == drmDeviceID - || localLocation?.locationString == serverLocationString { + // Pass through returning nil (meaning the server doesn't have a + // last read location worth restoring) if: + // 1 - The most recent page on the server comes from the same device, or + // 2 - The server and the client have the same page marked + if deviceID == drmDeviceID + || localLocation?.locationString == serverLocationString { - // server location does not differ from or should take no precedence - // over the local position - completion(nil) - return - } + // server location does not differ from or should take no precedence + // over the local position + completion(nil) + return + } - // we got a server location that differs from the local: return that - // so that clients can decide what to do - completion(bookmark.locator(forPublication: publication)) + // we got a server location that differs from the local: return that + // so that clients can decide what to do + completion(bookmark.locator(forPublication: publication)) } } From afbfe9f3316ae7d4bedc3740ff08d2c8379f6ed0 Mon Sep 17 00:00:00 2001 From: Ernest Fan Date: Thu, 1 Dec 2022 21:52:04 -0800 Subject: [PATCH 3/8] Refactor function and timer for syncing audiobook last listen position --- .../NYPLLastListenPositionSynchronizer.swift | 75 +++++++++++-------- ...CellDelegate+AudiobookProgressSaving.swift | 41 ++++++---- .../Book/UI/NYPLBookCellDelegate+Audiobooks.m | 69 +++++------------ 3 files changed, 86 insertions(+), 99 deletions(-) diff --git a/Simplified/Audiobooks/NYPLLastListenPositionSynchronizer.swift b/Simplified/Audiobooks/NYPLLastListenPositionSynchronizer.swift index de722a2db..51d0c816f 100644 --- a/Simplified/Audiobooks/NYPLLastListenPositionSynchronizer.swift +++ b/Simplified/Audiobooks/NYPLLastListenPositionSynchronizer.swift @@ -9,31 +9,22 @@ import Foundation import NYPLAudiobookToolkit -// TODO: Update NYPLAnnotations to have generic sync reading position function -// TODO: Implement network executor as property of NYPLLastListenPositionSynchronizer, see NYPLLastReadPositionSynchronizer // TODO: Ask Risa about sync interval and chapterLocation vs server bookmark -// Placeholder, move to audiobook toolkit -protocol NYPLLastListenPositionSynchronizing { - func getLastListenPosition(for bookID: String, - completion: @escaping (_ localPosition: NYPLAudiobookBookmark?, _ serverPosition: NYPLAudiobookBookmark?) -> ()) - func postLastListenPosition(_ location: NYPLAudiobookBookmark, for bookID: String) -} - class NYPLLastListenPositionSynchronizer: NYPLLastListenPositionSynchronizing { private let book: NYPLBook private let bookRegistryProvider: NYPLBookRegistryProvider - private let annotationSynchronizer: NYPLAnnotationSyncing + private let annotationsSynchronizer: NYPLLastReadPositionSupportAPI private let serialQueue = DispatchQueue(label: "\(Bundle.main.bundleIdentifier!).lastListenedPositionSynchronizer", target: .global(qos: .utility)) private let renderer: String = "NYPLAudiobookToolkit" init(book: NYPLBook, bookRegistryProvider: NYPLBookRegistryProvider, - annotationSynchronizer: NYPLAnnotationSyncing) { + annotationsSynchronizer: NYPLLastReadPositionSupportAPI) { self.book = book self.bookRegistryProvider = bookRegistryProvider - self.annotationSynchronizer = annotationSynchronizer + self.annotationsSynchronizer = annotationsSynchronizer } func getLastListenPosition(for bookID: String, @@ -44,15 +35,8 @@ class NYPLLastListenPositionSynchronizer: NYPLLastListenPositionSynchronizing { // Retrive local last-listened position let localPosition = self.getLocalLastListenPosition(for: bookID) - // Check if server sync allowed - // Return local last-listened position if not - guard self.annotationSynchronizer.syncIsPossibleAndPermitted() else { - completion(localPosition, nil) - return - } - // Retrieve last-listened position from server, return both local and server positions - self.annotationSynchronizer.syncReadingPosition(of: NYPLAudiobookBookmark.self, + self.annotationsSynchronizer.syncReadingPosition(of: NYPLAudiobookBookmark.self, forBook: bookID, publication: nil, toURL: self.book.annotationsURL) { serverPosition in @@ -66,6 +50,8 @@ class NYPLLastListenPositionSynchronizer: NYPLLastListenPositionSynchronizing { // last listen position worth restoring) if: // 1 - The most recent position on the server comes from the same device, or // 2 - The server and the client have the same position marked + + // TODO: Only return server position if server position is further than local position in the book if localPosition?.device == serverPosition.device || serverPosition.isEqual(localPosition) { completion(localPosition, nil) @@ -77,15 +63,40 @@ class NYPLLastListenPositionSynchronizer: NYPLLastListenPositionSynchronizing { } } - func postLastListenPosition(_ location: NYPLAudiobookBookmark, for bookID: String) { + func updateLastListenPositionInMemory(_ location: ChapterLocation) { let selectorValue = NYPLAudiobookBookmarkFactory.makeLocatorString(title: location.title ?? "", part: location.part, - chapter: location.chapter, - audiobookId: location.audiobookId, + chapter: location.number, + audiobookId: location.audiobookID, duration: location.duration, - time: location.time) + time: location.playheadOffset) + + let bookLocation = NYPLBookLocation.init(locationString: selectorValue, renderer: self.renderer) + serialQueue.async { [weak self] in + guard let self = self else { + return + } + self.bookRegistryProvider.setLocation(bookLocation, forIdentifier: self.book.identifier) + } + } + + + func syncLastListenPositionToServer() { + let bookID = self.book.identifier + + guard let localPosition = getLocalLastListenPosition(for: bookID) else { + return + } + + let selectorValue = NYPLAudiobookBookmarkFactory.makeLocatorString(title: localPosition.title ?? "", + part: localPosition.part, + chapter: localPosition.chapter, + audiobookId: localPosition.audiobookId, + duration: localPosition.duration, + time: localPosition.time) serialQueue.async { [weak self] in - self?.annotationSynchronizer.postReadingPosition(forBook: bookID, selectorValue: selectorValue) + self?.annotationsSynchronizer.postReadingPosition(forBook: bookID, + selectorValue: selectorValue) } } @@ -97,7 +108,7 @@ class NYPLLastListenPositionSynchronizer: NYPLLastListenPositionSynchronizing { return nil } - if let bookmark = self.bookmark(from: bookLocation.locationString) { + if let bookmark = NYPLAudiobookBookmark(selectorString: bookLocation.locationString, creationTime: Date()) { return bookmark } @@ -110,12 +121,10 @@ class NYPLLastListenPositionSynchronizer: NYPLLastListenPositionSynchronizing { } private func chapterLocation(from locationString: String) -> ChapterLocation? { - // TODO: - return nil - } - - private func bookmark(from locationString: String) -> NYPLAudiobookBookmark? { - // TODO: - return nil + guard let data = locationString.data(using: .utf8), + let chapterLocation = ChapterLocation.fromData(data) else { + return nil + } + return chapterLocation } } diff --git a/Simplified/Book/UI/NYPLBookCellDelegate+AudiobookProgressSaving.swift b/Simplified/Book/UI/NYPLBookCellDelegate+AudiobookProgressSaving.swift index 31ceb27a1..36d3f368e 100644 --- a/Simplified/Book/UI/NYPLBookCellDelegate+AudiobookProgressSaving.swift +++ b/Simplified/Book/UI/NYPLBookCellDelegate+AudiobookProgressSaving.swift @@ -11,7 +11,7 @@ import NYPLAudiobookToolkit import UIKit import NYPLUtilities -private let NYPLAudiobookProgressSavingInterval: DispatchTimeInterval = .seconds(60) +private let NYPLAudiobookPositionSyncingInterval: DispatchTimeInterval = .seconds(60) @objc extension NYPLBookCellDelegate { @@ -19,19 +19,19 @@ private let NYPLAudiobookProgressSavingInterval: DispatchTimeInterval = .seconds NYPLBookRegistry.shared().save() } - // Create a timer that saves the audiobook progress periodically when the app is inactive. + // Create a timer that saves the audiobook progress to disk and post to server periodically. // We do save the progress when app is being killed and applicationWillTerminate: is called, // but applicationWillTerminate: is not always called when users force quit the app. - // This method is triggered when app resigns active. - @objc(scheduleProgressSavingTimerForAudiobookManager:) - func scheduleProgressSavingTimer(for manager: DefaultAudiobookManager?) { + // We only save to disk when app is in background. + @objc(scheduleLastListenPositionSynchronizingTimerForAudiobookManager:) + func scheduleLastListenPositionSynchronizingTimer(for manager: DefaultAudiobookManager?) { guard let manager = manager else { return } weak var weakManager = manager - let timer = NYPLRepeatingTimer(interval: NYPLAudiobookProgressSavingInterval, + let timer = NYPLRepeatingTimer(interval: NYPLAudiobookPositionSyncingInterval, queue: self.audiobookProgressSavingQueue) { [weak self] in var isActive = false @@ -39,17 +39,28 @@ private let NYPLAudiobookProgressSavingInterval: DispatchTimeInterval = .seconds isActive = UIApplication.shared.applicationState == .active } - if isActive { - // DispatchSourceTimer will automatically cancel the timer if it is released. - weakManager?.cancelProgressSavingTimer() - } else { - if let manager = weakManager, - !manager.progressSavingTimerIsNil() { - self?.savePosition() - } + // Save audiobook progress to disk if app is in background + if !isActive { + self?.savePosition() + } + + // Post audiobook progress to server + if let manager = weakManager { + manager.lastListenPositionSynchronizer?.syncLastListenPositionToServer() } } - manager.setProgressSavingTimer(timer) + manager.setLastListenPositionSyncingTimer(timer) + } + + @objc(setLastListenPositionSynchronizerForBook:AudiobookManager:BookRegistryProvider:) + func setLastListenPositionSynchronizer(for book: NYPLBook, + audiobookManager: DefaultAudiobookManager, + bookRegistryProvider: NYPLBookRegistryProvider) { + let lastListenPosSynchronizer = NYPLLastListenPositionSynchronizer(book: book, + bookRegistryProvider: bookRegistryProvider, + annotationsSynchronizer: NYPLRootTabBarController.shared().annotationsSynchronizer) + + audiobookManager.lastListenPositionSynchronizer = lastListenPosSynchronizer } } #endif diff --git a/Simplified/Book/UI/NYPLBookCellDelegate+Audiobooks.m b/Simplified/Book/UI/NYPLBookCellDelegate+Audiobooks.m index 76bf9319c..0448b692a 100644 --- a/Simplified/Book/UI/NYPLBookCellDelegate+Audiobooks.m +++ b/Simplified/Book/UI/NYPLBookCellDelegate+Audiobooks.m @@ -59,6 +59,11 @@ - (void)openAudiobook:(NYPLBook *)book { - (void)presentAudiobook:(NYPLBook *)book withAudiobookManager:(DefaultAudiobookManager *)audiobookManager { [self setBookmarkBusinessLogicForBook:book AudiobookManager:audiobookManager AudiobookRegistryProvider:[NYPLBookRegistry sharedRegistry]]; + + [self setLastListenPositionSynchronizerForBook:book + AudiobookManager:audiobookManager + BookRegistryProvider:[NYPLBookRegistry sharedRegistry]]; + [NYPLMainThreadRun asyncIfNeeded:^{ AudiobookPlayerViewController *audiobookVC = [self createPlayerVCForAudiobook:audiobookManager.audiobook withBook:book @@ -67,24 +72,20 @@ - (void)presentAudiobook:(NYPLBook *)book withAudiobookManager:(DefaultAudiobook // present audiobook player on screen [[NYPLRootTabBarController sharedController] pushViewController:audiobookVC animated:YES]; - NYPLBookLocation *const bookLocation = - [[NYPLBookRegistry sharedRegistry] locationForIdentifier:book.identifier]; - - // move player to saved position - if (bookLocation) { - NSData *const data = [bookLocation.locationString dataUsingEncoding:NSUTF8StringEncoding]; - ChapterLocation *const chapterLocation = [ChapterLocation fromData:data]; - NYPLLOG_F(@"Returning to Audiobook Location: %@", chapterLocation); - [audiobookManager.audiobook.player movePlayheadToLocation:chapterLocation]; - } - - // poll audiobook player so that we can save the reading position - [self scheduleTimerForAudiobook:book manager:audiobookManager viewController:audiobookVC]; + // TODO: - Get last listen position in audio player +// NYPLBookLocation *const bookLocation = +// [[NYPLBookRegistry sharedRegistry] locationForIdentifier:book.identifier]; +// +// // move player to saved position +// if (bookLocation) { +// NSData *const data = [bookLocation.locationString dataUsingEncoding:NSUTF8StringEncoding]; +// ChapterLocation *const chapterLocation = [ChapterLocation fromData:data]; +// NYPLLOG_F(@"Returning to Audiobook Location: %@", chapterLocation); +// [audiobookManager.audiobook.player movePlayheadToLocation:chapterLocation]; +// } - [[NSNotificationCenter defaultCenter] addObserver:self - selector:@selector(scheduleAudiobookProgressSavingTimer) - name:UIApplicationWillResignActiveNotification - object:nil]; + // Timer for writing last listen position to disk and server + [self scheduleLastListenPositionSynchronizingTimerForAudiobookManager:audiobookManager]; }]; } @@ -194,40 +195,6 @@ - (void)registerCallbackForLogHandler }]; } -// Non-thread safe: currently this is always called on the main thread. -// Even more stricly, since NTPLBookCellDelegate is a singleton (!?), this -// method should be called only when the previous audiobookViewController is -// no longer used. -- (void)scheduleTimerForAudiobook:(NYPLBook *)book - manager:(DefaultAudiobookManager *)manager - viewController:(AudiobookPlayerViewController *)audiobookVC -{ - self.book = book; - self.manager = manager; - - __weak UIViewController *const weakAudiobookVC = audiobookVC; - [NSTimer scheduledTimerWithTimeInterval:1.0 repeats:YES block:^(NSTimer *_Nonnull timer) { - if (weakAudiobookVC == nil) { - [timer invalidate]; - NYPLLOG(@"Invalidating audiobook polling timer and resetting BookCellDelegate state"); - self.book = nil; - self.manager = nil; - return; - } - - NSString *const string = [[NSString alloc] - initWithData:manager.audiobook.player.currentChapterLocation.toData - encoding:NSUTF8StringEncoding]; - [[NYPLBookRegistry sharedRegistry] - setLocation:[[NYPLBookLocation alloc] initWithLocationString:string renderer:@"NYPLAudiobookToolkit"] - forIdentifier:book.identifier]; - }]; -} - -- (void)scheduleAudiobookProgressSavingTimer { - [self scheduleProgressSavingTimerForAudiobookManager:self.manager]; -} - - (void)presentDRMKeyError:(NSError *) error { NSString *title = NSLocalizedString(@"DRM Error", nil); From 98842f0f833345f76b939b95c554a24941e41cb2 Mon Sep 17 00:00:00 2001 From: Ernest Fan Date: Tue, 6 Dec 2022 20:54:19 -0800 Subject: [PATCH 4/8] Implement call of sync, store and restore of audiobook last listen position --- Simplified.xcodeproj/project.pbxproj | 16 +++--- .../NYPLLastListenPositionSynchronizer.swift | 27 +++++----- ...elegate+AudiobookLastListenPosition.swift} | 50 ++++++++++++++++++- .../Book/UI/NYPLBookCellDelegate+Audiobooks.m | 15 ++---- 4 files changed, 74 insertions(+), 34 deletions(-) rename Simplified/Book/UI/{NYPLBookCellDelegate+AudiobookProgressSaving.swift => NYPLBookCellDelegate+AudiobookLastListenPosition.swift} (54%) diff --git a/Simplified.xcodeproj/project.pbxproj b/Simplified.xcodeproj/project.pbxproj index 3d94e8891..ee9cf6b0a 100644 --- a/Simplified.xcodeproj/project.pbxproj +++ b/Simplified.xcodeproj/project.pbxproj @@ -101,9 +101,9 @@ 11F3773319E0876F00487769 /* NYPLCatalogFacet.m in Sources */ = {isa = PBXBuildFile; fileRef = 11F3773219E0876F00487769 /* NYPLCatalogFacet.m */; }; 145798F6215BE9E300F68AFD /* ProblemReportEmail.swift in Sources */ = {isa = PBXBuildFile; fileRef = 145798F5215BE9E300F68AFD /* ProblemReportEmail.swift */; }; 17071065242A923400E2648F /* NYPLSecrets.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17071060242A923400E2648F /* NYPLSecrets.swift */; }; - 17123D4327CEFB5700088193 /* NYPLBookCellDelegate+AudiobookProgressSaving.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17123D4227CEFB5700088193 /* NYPLBookCellDelegate+AudiobookProgressSaving.swift */; }; - 17123D4427CEFB5700088193 /* NYPLBookCellDelegate+AudiobookProgressSaving.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17123D4227CEFB5700088193 /* NYPLBookCellDelegate+AudiobookProgressSaving.swift */; }; - 17123D4527CEFB5700088193 /* NYPLBookCellDelegate+AudiobookProgressSaving.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17123D4227CEFB5700088193 /* NYPLBookCellDelegate+AudiobookProgressSaving.swift */; }; + 17123D4327CEFB5700088193 /* NYPLBookCellDelegate+AudiobookLastListenPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17123D4227CEFB5700088193 /* NYPLBookCellDelegate+AudiobookLastListenPosition.swift */; }; + 17123D4427CEFB5700088193 /* NYPLBookCellDelegate+AudiobookLastListenPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17123D4227CEFB5700088193 /* NYPLBookCellDelegate+AudiobookLastListenPosition.swift */; }; + 17123D4527CEFB5700088193 /* NYPLBookCellDelegate+AudiobookLastListenPosition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 17123D4227CEFB5700088193 /* NYPLBookCellDelegate+AudiobookLastListenPosition.swift */; }; 171966A924170819007BB87E /* NYPLBookState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 171966A824170819007BB87E /* NYPLBookState.swift */; }; 1724CA6A26E00D030015A174 /* NYPLReaderSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1724CA6926E00D030015A174 /* NYPLReaderSettingsView.swift */; }; 1724CA6B26E00D030015A174 /* NYPLReaderSettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1724CA6926E00D030015A174 /* NYPLReaderSettingsView.swift */; }; @@ -1857,7 +1857,7 @@ 11F54C2919423A040086FCAF /* NYPLOPDSLinkTests.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = NYPLOPDSLinkTests.m; sourceTree = ""; }; 145798F5215BE9E300F68AFD /* ProblemReportEmail.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProblemReportEmail.swift; sourceTree = ""; }; 17071060242A923400E2648F /* NYPLSecrets.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NYPLSecrets.swift; sourceTree = ""; }; - 17123D4227CEFB5700088193 /* NYPLBookCellDelegate+AudiobookProgressSaving.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NYPLBookCellDelegate+AudiobookProgressSaving.swift"; sourceTree = ""; }; + 17123D4227CEFB5700088193 /* NYPLBookCellDelegate+AudiobookLastListenPosition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "NYPLBookCellDelegate+AudiobookLastListenPosition.swift"; sourceTree = ""; }; 171966A824170819007BB87E /* NYPLBookState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NYPLBookState.swift; sourceTree = ""; }; 1724CA6926E00D030015A174 /* NYPLReaderSettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NYPLReaderSettingsView.swift; sourceTree = ""; }; 172F40F926F0A0170017476A /* Colors.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Colors.xcassets; sourceTree = ""; }; @@ -3242,7 +3242,7 @@ 085D31D51BE29E38007F7672 /* NYPLProblemReportViewController.h */, 085D31D61BE29E38007F7672 /* NYPLProblemReportViewController.m */, 085D31D81BE29ED4007F7672 /* NYPLProblemReportViewController.xib */, - 17123D4227CEFB5700088193 /* NYPLBookCellDelegate+AudiobookProgressSaving.swift */, + 17123D4227CEFB5700088193 /* NYPLBookCellDelegate+AudiobookLastListenPosition.swift */, 17D8633B29031A750096F11A /* NYPLBookCellDelegate+AudiobookBookmark.swift */, ); path = UI; @@ -4624,7 +4624,7 @@ 739E60B5244A0D8600D00301 /* NYPLMyBooksViewController.m in Sources */, 73B550FA2511723000D05B86 /* NYPLCatalogs+SE.swift in Sources */, 21C7B87F25AE1DBA000E8BF3 /* LibraryServiceError.swift in Sources */, - 17123D4427CEFB5700088193 /* NYPLBookCellDelegate+AudiobookProgressSaving.swift in Sources */, + 17123D4427CEFB5700088193 /* NYPLBookCellDelegate+AudiobookLastListenPosition.swift in Sources */, 739E60B6244A0D8600D00301 /* NYPLBookDetailTableView.swift in Sources */, 73A172F027ADA6FA005E7BCF /* NYPLFullAxisNowResource.swift in Sources */, 739E60B7244A0D8600D00301 /* Account.swift in Sources */, @@ -4922,7 +4922,7 @@ 73EB0AF925821DF4006BC997 /* NYPLRootTabBarController.m in Sources */, 73EB0AFA25821DF4006BC997 /* ExtendedNavBarView.swift in Sources */, 73EB0AFB25821DF4006BC997 /* OPDS2Link.swift in Sources */, - 17123D4527CEFB5700088193 /* NYPLBookCellDelegate+AudiobookProgressSaving.swift in Sources */, + 17123D4527CEFB5700088193 /* NYPLBookCellDelegate+AudiobookLastListenPosition.swift in Sources */, 73EB0AFC25821DF4006BC997 /* NYPLSignInBusinessLogic+BookmarkSyncing.swift in Sources */, 73EB0AFD25821DF4006BC997 /* NYPLBookAuthor.swift in Sources */, 2126FE3A25C0597E0095C45C /* LibraryServiceError.swift in Sources */, @@ -5400,7 +5400,7 @@ 116A5EB3194767DC00491A21 /* NYPLMyBooksViewController.m in Sources */, 21C7B87E25AE1DBA000E8BF3 /* LibraryServiceError.swift in Sources */, E6B3269F1EE066DE00DB877A /* NYPLBookDetailTableView.swift in Sources */, - 17123D4327CEFB5700088193 /* NYPLBookCellDelegate+AudiobookProgressSaving.swift in Sources */, + 17123D4327CEFB5700088193 /* NYPLBookCellDelegate+AudiobookLastListenPosition.swift in Sources */, 734B78992565F7DE006FB8AD /* NYPLReauthenticator.swift in Sources */, 73A172DA27ADA6F9005E7BCF /* NYPLFullAxisNowResource.swift in Sources */, 175E480824EF36520066A6CF /* NYPLAnnouncementBusinessLogic.swift in Sources */, diff --git a/Simplified/Audiobooks/NYPLLastListenPositionSynchronizer.swift b/Simplified/Audiobooks/NYPLLastListenPositionSynchronizer.swift index 51d0c816f..df022e291 100644 --- a/Simplified/Audiobooks/NYPLLastListenPositionSynchronizer.swift +++ b/Simplified/Audiobooks/NYPLLastListenPositionSynchronizer.swift @@ -5,12 +5,10 @@ // Created by Ernest Fan on 2022-11-10. // Copyright © 2022 NYPL. All rights reserved. // - +#if FEATURE_AUDIOBOOKS import Foundation import NYPLAudiobookToolkit -// TODO: Ask Risa about sync interval and chapterLocation vs server bookmark - class NYPLLastListenPositionSynchronizer: NYPLLastListenPositionSynchronizing { private let book: NYPLBook private let bookRegistryProvider: NYPLBookRegistryProvider @@ -27,17 +25,16 @@ class NYPLLastListenPositionSynchronizer: NYPLLastListenPositionSynchronizing { self.annotationsSynchronizer = annotationsSynchronizer } - func getLastListenPosition(for bookID: String, - completion: @escaping (_ localPosition: NYPLAudiobookBookmark?, _ serverPosition: NYPLAudiobookBookmark?) -> ()) { + func getLastListenPosition(completion: @escaping (_ localPosition: NYPLAudiobookBookmark?, _ serverPosition: NYPLAudiobookBookmark?) -> ()) { serialQueue.async { [weak self] in guard let self = self else { return } // Retrive local last-listened position - let localPosition = self.getLocalLastListenPosition(for: bookID) + let localPosition = self.getLocalLastListenPosition(for: self.book.identifier) // Retrieve last-listened position from server, return both local and server positions self.annotationsSynchronizer.syncReadingPosition(of: NYPLAudiobookBookmark.self, - forBook: bookID, + forBook: self.book.identifier, publication: nil, toURL: self.book.annotationsURL) { serverPosition in guard let serverPosition = serverPosition else { @@ -46,14 +43,17 @@ class NYPLLastListenPositionSynchronizer: NYPLLastListenPositionSynchronizing { return } - // Pass through returning nil (meaning the server doesn't have a + guard let localPosition = localPosition else { + completion(nil, serverPosition) + return + } + + // Pass through without server position (meaning the server doesn't have a // last listen position worth restoring) if: // 1 - The most recent position on the server comes from the same device, or - // 2 - The server and the client have the same position marked - - // TODO: Only return server position if server position is further than local position in the book - if localPosition?.device == serverPosition.device || - serverPosition.isEqual(localPosition) { + // 2 - The local position is further in the audiobook than the server position + if serverPosition.device == NYPLUserAccount.sharedAccount().deviceID || + localPosition >= serverPosition { completion(localPosition, nil) return } @@ -128,3 +128,4 @@ class NYPLLastListenPositionSynchronizer: NYPLLastListenPositionSynchronizing { return chapterLocation } } +#endif diff --git a/Simplified/Book/UI/NYPLBookCellDelegate+AudiobookProgressSaving.swift b/Simplified/Book/UI/NYPLBookCellDelegate+AudiobookLastListenPosition.swift similarity index 54% rename from Simplified/Book/UI/NYPLBookCellDelegate+AudiobookProgressSaving.swift rename to Simplified/Book/UI/NYPLBookCellDelegate+AudiobookLastListenPosition.swift index 36d3f368e..f376fb77d 100644 --- a/Simplified/Book/UI/NYPLBookCellDelegate+AudiobookProgressSaving.swift +++ b/Simplified/Book/UI/NYPLBookCellDelegate+AudiobookLastListenPosition.swift @@ -1,5 +1,5 @@ // -// NYPLBookCellDelegate+AudiobookProgressSaving.swift +// NYPLBookCellDelegate+AudiobookLastListenPositio.swift // Simplified // // Created by Ernest Fan on 2022-03-01. @@ -62,5 +62,53 @@ private let NYPLAudiobookPositionSyncingInterval: DispatchTimeInterval = .second audiobookManager.lastListenPositionSynchronizer = lastListenPosSynchronizer } + + @objc(restoreLastListenPositionForAudiobookManager:) + func restoreLastListenPosition(audiobookManager: DefaultAudiobookManager) { + audiobookManager.lastListenPositionSynchronizer?.getLastListenPosition(completion: { [weak self] localPosition, serverPosition in + + guard let self = self else { + return + } + + NYPLMainThreadRun.asyncIfNeeded { + if let serverPosition = serverPosition { + self.presentAlert(serverPosition) { position in + let finalPosition = position != nil ? position : localPosition + guard let finalPosition = finalPosition else { + return + } + + audiobookManager.movePlayhead(to: finalPosition) + } + } else if let localPosition = localPosition { + audiobookManager.movePlayhead(to: localPosition) + } + } + }) + } + + private func presentAlert(_ serverPosition: NYPLAudiobookBookmark, + completion: @escaping (NYPLAudiobookBookmark?) -> ()) { + // TODO: - Update localized strings + let alert = UIAlertController(title: NSLocalizedString("Sync Reading Position", comment: "An alert title notifying the user the reading position has been synced"), + message: NSLocalizedString("Do you want to move to the page on which you left off?", comment: "An alert message asking the user to perform navigation to the synced reading position or not"), + preferredStyle: .alert) + + let stayText = NSLocalizedString("Stay", comment: "Do not perform navigation") + let stayAction = UIAlertAction(title: stayText, style: .cancel) { _ in + completion(nil) + } + + let moveText = NSLocalizedString("Move", comment: "Perform navigation") + let moveAction = UIAlertAction(title: moveText, style: .default) { _ in + completion(serverPosition) + } + + alert.addAction(stayAction) + alert.addAction(moveAction) + + NYPLPresentationUtils.safelyPresent(alert) + } } #endif diff --git a/Simplified/Book/UI/NYPLBookCellDelegate+Audiobooks.m b/Simplified/Book/UI/NYPLBookCellDelegate+Audiobooks.m index 0448b692a..96c83656b 100644 --- a/Simplified/Book/UI/NYPLBookCellDelegate+Audiobooks.m +++ b/Simplified/Book/UI/NYPLBookCellDelegate+Audiobooks.m @@ -72,19 +72,10 @@ - (void)presentAudiobook:(NYPLBook *)book withAudiobookManager:(DefaultAudiobook // present audiobook player on screen [[NYPLRootTabBarController sharedController] pushViewController:audiobookVC animated:YES]; - // TODO: - Get last listen position in audio player -// NYPLBookLocation *const bookLocation = -// [[NYPLBookRegistry sharedRegistry] locationForIdentifier:book.identifier]; -// -// // move player to saved position -// if (bookLocation) { -// NSData *const data = [bookLocation.locationString dataUsingEncoding:NSUTF8StringEncoding]; -// ChapterLocation *const chapterLocation = [ChapterLocation fromData:data]; -// NYPLLOG_F(@"Returning to Audiobook Location: %@", chapterLocation); -// [audiobookManager.audiobook.player movePlayheadToLocation:chapterLocation]; -// } + // Restore last listen position from local storage and server + [self restoreLastListenPositionForAudiobookManager:audiobookManager]; - // Timer for writing last listen position to disk and server + // Set up timer for writing last listen position to local storage and server [self scheduleLastListenPositionSynchronizingTimerForAudiobookManager:audiobookManager]; }]; } From 945874c9a6ef070821ee6623d894b3081f0f1a9f Mon Sep 17 00:00:00 2001 From: Ernest Fan Date: Wed, 7 Dec 2022 23:43:55 -0800 Subject: [PATCH 5/8] Refactor audiobook presentation flow to wait for last listen position sync completion --- ...Delegate+AudiobookLastListenPosition.swift | 15 ++++++++-- .../Book/UI/NYPLBookCellDelegate+Audiobooks.h | 2 +- .../Book/UI/NYPLBookCellDelegate+Audiobooks.m | 29 ++++++++++--------- Simplified/Book/UI/NYPLBookCellDelegate.m | 2 +- 4 files changed, 30 insertions(+), 18 deletions(-) diff --git a/Simplified/Book/UI/NYPLBookCellDelegate+AudiobookLastListenPosition.swift b/Simplified/Book/UI/NYPLBookCellDelegate+AudiobookLastListenPosition.swift index f376fb77d..790c14d2a 100644 --- a/Simplified/Book/UI/NYPLBookCellDelegate+AudiobookLastListenPosition.swift +++ b/Simplified/Book/UI/NYPLBookCellDelegate+AudiobookLastListenPosition.swift @@ -52,7 +52,7 @@ private let NYPLAudiobookPositionSyncingInterval: DispatchTimeInterval = .second manager.setLastListenPositionSyncingTimer(timer) } - @objc(setLastListenPositionSynchronizerForBook:AudiobookManager:BookRegistryProvider:) + @objc(setLastListenPositionSynchronizerForBook:audiobookManager:bookRegistryProvider:) func setLastListenPositionSynchronizer(for book: NYPLBook, audiobookManager: DefaultAudiobookManager, bookRegistryProvider: NYPLBookRegistryProvider) { @@ -63,8 +63,11 @@ private let NYPLAudiobookPositionSyncingInterval: DispatchTimeInterval = .second audiobookManager.lastListenPositionSynchronizer = lastListenPosSynchronizer } - @objc(restoreLastListenPositionForAudiobookManager:) - func restoreLastListenPosition(audiobookManager: DefaultAudiobookManager) { + @objc(restoreLastListenPositionAndPresentAudiobookPlayerVC:audiobookManager:successCompletion:) + func restoreLastListenPositionAndPresent(audiobookPlayerVC: AudiobookPlayerViewController, + audiobookManager: DefaultAudiobookManager, + successCompletion: @escaping () -> ()) { + // Restore last listen position from local storage and server audiobookManager.lastListenPositionSynchronizer?.getLastListenPosition(completion: { [weak self] localPosition, serverPosition in guard let self = self else { @@ -72,6 +75,12 @@ private let NYPLAudiobookPositionSyncingInterval: DispatchTimeInterval = .second } NYPLMainThreadRun.asyncIfNeeded { + // Present audio player + NYPLRootTabBarController.shared().pushViewController(audiobookPlayerVC, animated: true) + // Call completion handler when audiobook has been successfully opened + successCompletion() + + // Present alert for user to decide if they want to move to position found on server if let serverPosition = serverPosition { self.presentAlert(serverPosition) { position in let finalPosition = position != nil ? position : localPosition diff --git a/Simplified/Book/UI/NYPLBookCellDelegate+Audiobooks.h b/Simplified/Book/UI/NYPLBookCellDelegate+Audiobooks.h index a35e11efb..8f130e4da 100644 --- a/Simplified/Book/UI/NYPLBookCellDelegate+Audiobooks.h +++ b/Simplified/Book/UI/NYPLBookCellDelegate+Audiobooks.h @@ -16,7 +16,7 @@ @interface NYPLBookCellDelegate (Audiobooks) -- (void)openAudiobook:(NYPLBook *)book; +- (void)openAudiobook:(NYPLBook *)book successCompletion:(void(^)(void))successCompletion; @end diff --git a/Simplified/Book/UI/NYPLBookCellDelegate+Audiobooks.m b/Simplified/Book/UI/NYPLBookCellDelegate+Audiobooks.m index 96c83656b..d453c1602 100644 --- a/Simplified/Book/UI/NYPLBookCellDelegate+Audiobooks.m +++ b/Simplified/Book/UI/NYPLBookCellDelegate+Audiobooks.m @@ -24,11 +24,11 @@ @implementation NYPLBookCellDelegate (Audiobooks) #pragma mark - Audiobook Methods -- (void)openAudiobook:(NYPLBook *)book { +- (void)openAudiobook:(NYPLBook *)book successCompletion:(void(^)(void))successCompletion { // If an audiobook download is in progress, we should use the existing AudiobookManager. DefaultAudiobookManager *audiobookManager = [[NYPLMyBooksDownloadCenter sharedDownloadCenter] audiobookManagerForBookID:book.identifier]; if (audiobookManager) { - [self presentAudiobook:book withAudiobookManager:audiobookManager]; + [self presentAudiobook:book withAudiobookManager:audiobookManager successCompletion:successCompletion]; return; } @@ -53,34 +53,37 @@ - (void)openAudiobook:(NYPLBook *)book { break; } - [self openAudiobook:book withJSON:json decryptor:decryptor]; + [self openAudiobook:book withJSON:json decryptor:decryptor successCompletion:successCompletion]; }]; } -- (void)presentAudiobook:(NYPLBook *)book withAudiobookManager:(DefaultAudiobookManager *)audiobookManager { +- (void)presentAudiobook:(NYPLBook *)book + withAudiobookManager:(DefaultAudiobookManager *)audiobookManager + successCompletion:(void(^)(void))successCompletion { [self setBookmarkBusinessLogicForBook:book AudiobookManager:audiobookManager AudiobookRegistryProvider:[NYPLBookRegistry sharedRegistry]]; [self setLastListenPositionSynchronizerForBook:book - AudiobookManager:audiobookManager - BookRegistryProvider:[NYPLBookRegistry sharedRegistry]]; + audiobookManager:audiobookManager + bookRegistryProvider:[NYPLBookRegistry sharedRegistry]]; [NYPLMainThreadRun asyncIfNeeded:^{ AudiobookPlayerViewController *audiobookVC = [self createPlayerVCForAudiobook:audiobookManager.audiobook withBook:book configuringAudiobookManager:audiobookManager]; - - // present audiobook player on screen - [[NYPLRootTabBarController sharedController] pushViewController:audiobookVC animated:YES]; - // Restore last listen position from local storage and server - [self restoreLastListenPositionForAudiobookManager:audiobookManager]; + [self restoreLastListenPositionAndPresentAudiobookPlayerVC:audiobookVC + audiobookManager:audiobookManager + successCompletion:successCompletion]; // Set up timer for writing last listen position to local storage and server [self scheduleLastListenPositionSynchronizingTimerForAudiobookManager:audiobookManager]; }]; } -- (void)openAudiobook:(NYPLBook *)book withJSON:(NSDictionary *)json decryptor:(id)audiobookDrmDecryptor { +- (void)openAudiobook:(NYPLBook *)book + withJSON:(NSDictionary *)json + decryptor:(id)audiobookDrmDecryptor + successCompletion:(void(^)(void))successCompletion { [AudioBookVendorsHelper updateVendorKeyWithBook:json completion:^(NSError * _Nullable error) { [NSOperationQueue.mainQueue addOperationWithBlock:^{ id const audiobook = [AudiobookFactory audiobook:json decryptor:audiobookDrmDecryptor]; @@ -109,7 +112,7 @@ - (void)openAudiobook:(NYPLBook *)book withJSON:(NSDictionary *)json decryptor:( ]; } - [self presentAudiobook:book withAudiobookManager:manager]; + [self presentAudiobook:book withAudiobookManager:manager successCompletion:successCompletion]; }]; }]; } diff --git a/Simplified/Book/UI/NYPLBookCellDelegate.m b/Simplified/Book/UI/NYPLBookCellDelegate.m index dd8ef6ce9..9e91003dd 100644 --- a/Simplified/Book/UI/NYPLBookCellDelegate.m +++ b/Simplified/Book/UI/NYPLBookCellDelegate.m @@ -108,7 +108,7 @@ - (void)openBook:(NYPLBook *)book successCompletion:(void(^)(void))successComple break; #if FEATURE_AUDIOBOOKS case NYPLBookContentTypeAudiobook: - [self openAudiobook:book]; + [self openAudiobook:book successCompletion:successCompletion]; break; #endif default: From 962287bdace5d51a9673287ddea023997114535d Mon Sep 17 00:00:00 2001 From: Ernest Fan Date: Thu, 8 Dec 2022 12:46:56 -0800 Subject: [PATCH 6/8] Fix NYPLAnnotationsMock, bump build number and update NYPLAudiobookToolkit ref --- NYPLAudiobookToolkit | 2 +- Simplified.xcodeproj/project.pbxproj | 12 +++++----- .../Mocks/NYPLAnnotationsMock.swift | 24 ++++++++++++------- 3 files changed, 22 insertions(+), 16 deletions(-) diff --git a/NYPLAudiobookToolkit b/NYPLAudiobookToolkit index 75b617e59..1d7d4aa42 160000 --- a/NYPLAudiobookToolkit +++ b/NYPLAudiobookToolkit @@ -1 +1 @@ -Subproject commit 75b617e59954c17608b23530c7c9fbe922756827 +Subproject commit 1d7d4aa4297b0c9cf7d3fe1f3534ffdd112bc8ce diff --git a/Simplified.xcodeproj/project.pbxproj b/Simplified.xcodeproj/project.pbxproj index ee9cf6b0a..245008f17 100644 --- a/Simplified.xcodeproj/project.pbxproj +++ b/Simplified.xcodeproj/project.pbxproj @@ -5722,7 +5722,7 @@ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES_ERROR; CODE_SIGN_ENTITLEMENTS = Simplified/SimplyE.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = 7262U6ST2R; ENABLE_BITCODE = NO; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; @@ -5773,7 +5773,7 @@ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES_ERROR; CODE_SIGN_ENTITLEMENTS = Simplified/SimplyE.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = 7262U6ST2R; ENABLE_BITCODE = NO; GCC_PRECOMPILE_PREFIX_HEADER = YES; @@ -5823,7 +5823,7 @@ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES_ERROR; CODE_SIGN_ENTITLEMENTS = Simplified/SimplyE.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = 7262U6ST2R; ENABLE_BITCODE = NO; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; @@ -5871,7 +5871,7 @@ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES_ERROR; CODE_SIGN_ENTITLEMENTS = Simplified/SimplyE.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = 7262U6ST2R; ENABLE_BITCODE = NO; GCC_PRECOMPILE_PREFIX_HEADER = YES; @@ -6141,7 +6141,7 @@ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES_ERROR; CODE_SIGN_ENTITLEMENTS = Simplified/SimplyE.entitlements; CODE_SIGN_IDENTITY = "iPhone Developer"; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = 7262U6ST2R; ENABLE_BITCODE = NO; "EXCLUDED_ARCHS[sdk=iphonesimulator*]" = arm64; @@ -6192,7 +6192,7 @@ CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES_ERROR; CODE_SIGN_ENTITLEMENTS = Simplified/SimplyE.entitlements; CODE_SIGN_IDENTITY = "iPhone Distribution"; - CURRENT_PROJECT_VERSION = 1; + CURRENT_PROJECT_VERSION = 2; DEVELOPMENT_TEAM = 7262U6ST2R; ENABLE_BITCODE = NO; GCC_PRECOMPILE_PREFIX_HEADER = YES; diff --git a/SimplifiedTests/Mocks/NYPLAnnotationsMock.swift b/SimplifiedTests/Mocks/NYPLAnnotationsMock.swift index 73c064e31..6e2a294fb 100644 --- a/SimplifiedTests/Mocks/NYPLAnnotationsMock.swift +++ b/SimplifiedTests/Mocks/NYPLAnnotationsMock.swift @@ -34,22 +34,28 @@ class NYPLAnnotationsMock: NYPLAnnotationSyncing { // Reading position - func syncReadingPosition(ofBook bookID: String?, - publication: Publication?, - toURL url:URL?, - completion: @escaping (_ readPos: NYPLReadiumBookmark?) -> ()) { + func syncReadingPosition(of type: T.Type, + forBook bookID: String?, + publication: Publication?, + toURL url: URL?, + completion: @escaping (T?) -> ()) where T : NYPLBookmark { guard !failRequest, let id = bookID, let bookmarkSpec = readingPositions[id] else { completion(nil) return } + let bookmarkData = bookmarkSpec.dictionaryForJSONSerialization() - let bookmark = NYPLReadiumBookmarkFactory.make(fromServerAnnotation: bookmarkData, - annotationType: .readingProgress, - bookID: id, - publication: publication) - completion(bookmark) + if type == NYPLReadiumBookmark.self { + let bookmark = NYPLReadiumBookmarkFactory.make(fromServerAnnotation: bookmarkData, + annotationType: .readingProgress, + bookID: id, + publication: publication) + completion(bookmark as? T) + return + } + completion(nil) } func postReadingPosition(forBook bookID: String, selectorValue: String) { From 55a1da899eae5a1c9106d8f2efa1421d1b71cce9 Mon Sep 17 00:00:00 2001 From: Ernest Fan Date: Tue, 13 Dec 2022 13:10:09 -0800 Subject: [PATCH 7/8] Update localized strings for last listened position sync --- .../NYPLBookCellDelegate+AudiobookLastListenPosition.swift | 7 ++++--- Simplified/en.lproj/Localizable.strings | 7 +++++++ Simplified/it.lproj/Localizable.strings | 7 +++++++ 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/Simplified/Book/UI/NYPLBookCellDelegate+AudiobookLastListenPosition.swift b/Simplified/Book/UI/NYPLBookCellDelegate+AudiobookLastListenPosition.swift index 790c14d2a..aae7c5932 100644 --- a/Simplified/Book/UI/NYPLBookCellDelegate+AudiobookLastListenPosition.swift +++ b/Simplified/Book/UI/NYPLBookCellDelegate+AudiobookLastListenPosition.swift @@ -99,9 +99,10 @@ private let NYPLAudiobookPositionSyncingInterval: DispatchTimeInterval = .second private func presentAlert(_ serverPosition: NYPLAudiobookBookmark, completion: @escaping (NYPLAudiobookBookmark?) -> ()) { - // TODO: - Update localized strings - let alert = UIAlertController(title: NSLocalizedString("Sync Reading Position", comment: "An alert title notifying the user the reading position has been synced"), - message: NSLocalizedString("Do you want to move to the page on which you left off?", comment: "An alert message asking the user to perform navigation to the synced reading position or not"), + let alert = UIAlertController(title: NSLocalizedString("Sync Reading Position", + comment: "An alert title notifying the user the listening position has been synced"), + message: NSLocalizedString("Do you want to stay at this location in your audiobook or move to the furthest location you have listened to?", + comment: "An alert message asking the user to perform navigation to the synced listening position or not"), preferredStyle: .alert) let stayText = NSLocalizedString("Stay", comment: "Do not perform navigation") diff --git a/Simplified/en.lproj/Localizable.strings b/Simplified/en.lproj/Localizable.strings index b5e45ed38..72a4d9ed5 100644 --- a/Simplified/en.lproj/Localizable.strings +++ b/Simplified/en.lproj/Localizable.strings @@ -184,6 +184,13 @@ "Error Syncing Bookmarks" = "Error Syncing Bookmarks"; "There was an error syncing bookmarks to the server. Ensure your device is connected to the internet or try again later." = "There was an error syncing bookmarks to the server. Ensure your device is connected to the internet or try again later."; +// MARK: - Reader/Player Positions (Sync) +"Sync Reading Position" = "Sync Reading Position"; +"Do you want to move to the page on which you left off?" = "Do you want to move to the page on which you left off?"; +"Do you want to stay at this location in your audiobook or move to the furthest location you have listened to?" = "Do you want to stay at this location in your audiobook or move to the furthest location you have listened to?"; +"Stay" = "Stay"; +"Move" = "Move"; + // MARK: - Reader error messages (R2) "Content Protection Error" = "Content Protection Error"; "The book you were trying to open is invalid." = "The book you were trying to open is invalid."; diff --git a/Simplified/it.lproj/Localizable.strings b/Simplified/it.lproj/Localizable.strings index 993978397..bf3eedab2 100644 --- a/Simplified/it.lproj/Localizable.strings +++ b/Simplified/it.lproj/Localizable.strings @@ -180,6 +180,13 @@ "Error Syncing Bookmarks" = "Errore nella sincronizzazione dei segnalibri"; "There was an error syncing bookmarks to the server. Ensure your device is connected to the internet or try again later." = "Si è verificato un errore nella sincronizzazione dei segnalibri. Assicurati che il tuo dispositivo sia collegato a internet e riprova."; +// MARK: - Reader/Player Positions (Sync) +"Sync Reading Position" = "Sincronizza punto di lettura"; +"Do you want to move to the page on which you left off?" = "Vuoi spostarti alla pagina dove hai smesso di leggere l'ultima volta?"; +"Do you want to stay at this location in your audiobook or move to the furthest location you have listened to?" = "Vuoi rimanere nella posizione attuale di questo audiolibro o spostarti all'ultimo punto che hai ascoltato?"; +"Stay" = "Rimani"; +"Move" = "Spostati"; + // MARK: - Reader error messages (R2) "Content Protection Error" = "Errore nella protezione del contenuto"; "The book you were trying to open is invalid." = "Il libro che stavi cercando di leggere è in un formato non valido."; From 374ae86ce057fdfe9d888dced8860c13772d6778 Mon Sep 17 00:00:00 2001 From: Ernest Fan Date: Wed, 14 Dec 2022 11:51:37 -0800 Subject: [PATCH 8/8] Update NYPLAEToolkit commit reference and remove serial queue from audiobook position synchronizer --- NYPLAEToolkit | 2 +- .../NYPLLastListenPositionSynchronizer.swift | 80 +++++++++---------- ...Delegate+AudiobookLastListenPosition.swift | 21 ++--- .../Book/UI/NYPLBookCellDelegate+Audiobooks.m | 3 +- 4 files changed, 52 insertions(+), 54 deletions(-) diff --git a/NYPLAEToolkit b/NYPLAEToolkit index b8f91dc45..f82952b12 160000 --- a/NYPLAEToolkit +++ b/NYPLAEToolkit @@ -1 +1 @@ -Subproject commit b8f91dc4536e793660b4be437f7aa1d0f24b8fb4 +Subproject commit f82952b12e0d05d555efd30f919043ba4148fc0b diff --git a/Simplified/Audiobooks/NYPLLastListenPositionSynchronizer.swift b/Simplified/Audiobooks/NYPLLastListenPositionSynchronizer.swift index df022e291..874946947 100644 --- a/Simplified/Audiobooks/NYPLLastListenPositionSynchronizer.swift +++ b/Simplified/Audiobooks/NYPLLastListenPositionSynchronizer.swift @@ -13,53 +13,52 @@ class NYPLLastListenPositionSynchronizer: NYPLLastListenPositionSynchronizing { private let book: NYPLBook private let bookRegistryProvider: NYPLBookRegistryProvider private let annotationsSynchronizer: NYPLLastReadPositionSupportAPI + private let deviceID: String? - private let serialQueue = DispatchQueue(label: "\(Bundle.main.bundleIdentifier!).lastListenedPositionSynchronizer", target: .global(qos: .utility)) private let renderer: String = "NYPLAudiobookToolkit" init(book: NYPLBook, bookRegistryProvider: NYPLBookRegistryProvider, - annotationsSynchronizer: NYPLLastReadPositionSupportAPI) { + annotationsSynchronizer: NYPLLastReadPositionSupportAPI, + deviceID: String?) { self.book = book self.bookRegistryProvider = bookRegistryProvider self.annotationsSynchronizer = annotationsSynchronizer + self.deviceID = deviceID } func getLastListenPosition(completion: @escaping (_ localPosition: NYPLAudiobookBookmark?, _ serverPosition: NYPLAudiobookBookmark?) -> ()) { - serialQueue.async { [weak self] in - guard let self = self else { return } + + // Retrieve local last-listened position + let localPosition = self.getLocalLastListenPosition(for: book.identifier) + + // Retrieve last-listened position from server, return both local and server positions + self.annotationsSynchronizer.syncReadingPosition(of: NYPLAudiobookBookmark.self, + forBook: book.identifier, + publication: nil, + toURL: book.annotationsURL) { serverPosition in + guard let serverPosition = serverPosition else { + Log.info(#function, "No reading position annotation exists on the server for \(self.book.loggableShortString()).") + completion(localPosition, nil) + return + } - // Retrive local last-listened position - let localPosition = self.getLocalLastListenPosition(for: self.book.identifier) + guard let localPosition = localPosition else { + completion(nil, serverPosition) + return + } - // Retrieve last-listened position from server, return both local and server positions - self.annotationsSynchronizer.syncReadingPosition(of: NYPLAudiobookBookmark.self, - forBook: self.book.identifier, - publication: nil, - toURL: self.book.annotationsURL) { serverPosition in - guard let serverPosition = serverPosition else { - Log.info(#function, "No reading position annotation exists on the server for \(self.book.loggableShortString()).") - completion(localPosition, nil) - return - } - - guard let localPosition = localPosition else { - completion(nil, serverPosition) - return - } - - // Pass through without server position (meaning the server doesn't have a - // last listen position worth restoring) if: - // 1 - The most recent position on the server comes from the same device, or - // 2 - The local position is further in the audiobook than the server position - if serverPosition.device == NYPLUserAccount.sharedAccount().deviceID || - localPosition >= serverPosition { - completion(localPosition, nil) - return - } - - completion(localPosition, serverPosition) + // Pass through without server position (meaning the server doesn't have a + // last listen position worth restoring) if: + // 1 - The most recent position on the server comes from the same device, or + // 2 - The local position is further in the audiobook than the server position + if serverPosition.device == self.deviceID || + localPosition >= serverPosition { + completion(localPosition, nil) + return } + + completion(localPosition, serverPosition) } } @@ -72,12 +71,8 @@ class NYPLLastListenPositionSynchronizer: NYPLLastListenPositionSynchronizing { time: location.playheadOffset) let bookLocation = NYPLBookLocation.init(locationString: selectorValue, renderer: self.renderer) - serialQueue.async { [weak self] in - guard let self = self else { - return - } - self.bookRegistryProvider.setLocation(bookLocation, forIdentifier: self.book.identifier) - } + + bookRegistryProvider.setLocation(bookLocation, forIdentifier: self.book.identifier) } @@ -94,10 +89,9 @@ class NYPLLastListenPositionSynchronizer: NYPLLastListenPositionSynchronizing { audiobookId: localPosition.audiobookId, duration: localPosition.duration, time: localPosition.time) - serialQueue.async { [weak self] in - self?.annotationsSynchronizer.postReadingPosition(forBook: bookID, - selectorValue: selectorValue) - } + + annotationsSynchronizer.postReadingPosition(forBook: bookID, + selectorValue: selectorValue) } // MARK: - Helper diff --git a/Simplified/Book/UI/NYPLBookCellDelegate+AudiobookLastListenPosition.swift b/Simplified/Book/UI/NYPLBookCellDelegate+AudiobookLastListenPosition.swift index aae7c5932..b1dc41271 100644 --- a/Simplified/Book/UI/NYPLBookCellDelegate+AudiobookLastListenPosition.swift +++ b/Simplified/Book/UI/NYPLBookCellDelegate+AudiobookLastListenPosition.swift @@ -35,13 +35,13 @@ private let NYPLAudiobookPositionSyncingInterval: DispatchTimeInterval = .second queue: self.audiobookProgressSavingQueue) { [weak self] in var isActive = false - NYPLMainThreadRun.sync { + NYPLMainThreadRun.asyncIfNeeded { isActive = UIApplication.shared.applicationState == .active - } - - // Save audiobook progress to disk if app is in background - if !isActive { - self?.savePosition() + + // Save audiobook progress to disk if app is in background + if !isActive { + self?.savePosition() + } } // Post audiobook progress to server @@ -52,13 +52,16 @@ private let NYPLAudiobookPositionSyncingInterval: DispatchTimeInterval = .second manager.setLastListenPositionSyncingTimer(timer) } - @objc(setLastListenPositionSynchronizerForBook:audiobookManager:bookRegistryProvider:) + /// - Important: Must be called on the main thread since it accesses NYPLRootTabBarController. + @objc(setLastListenPositionSynchronizerForBook:audiobookManager:bookRegistryProvider:deviceID:) func setLastListenPositionSynchronizer(for book: NYPLBook, audiobookManager: DefaultAudiobookManager, - bookRegistryProvider: NYPLBookRegistryProvider) { + bookRegistryProvider: NYPLBookRegistryProvider, + deviceID: String?) { let lastListenPosSynchronizer = NYPLLastListenPositionSynchronizer(book: book, bookRegistryProvider: bookRegistryProvider, - annotationsSynchronizer: NYPLRootTabBarController.shared().annotationsSynchronizer) + annotationsSynchronizer: NYPLRootTabBarController.shared().annotationsSynchronizer, + deviceID: deviceID) audiobookManager.lastListenPositionSynchronizer = lastListenPosSynchronizer } diff --git a/Simplified/Book/UI/NYPLBookCellDelegate+Audiobooks.m b/Simplified/Book/UI/NYPLBookCellDelegate+Audiobooks.m index d453c1602..45c86552f 100644 --- a/Simplified/Book/UI/NYPLBookCellDelegate+Audiobooks.m +++ b/Simplified/Book/UI/NYPLBookCellDelegate+Audiobooks.m @@ -64,7 +64,8 @@ - (void)presentAudiobook:(NYPLBook *)book [self setLastListenPositionSynchronizerForBook:book audiobookManager:audiobookManager - bookRegistryProvider:[NYPLBookRegistry sharedRegistry]]; + bookRegistryProvider:[NYPLBookRegistry sharedRegistry] + deviceID:[[NYPLUserAccount sharedAccount] deviceID]]; [NYPLMainThreadRun asyncIfNeeded:^{ AudiobookPlayerViewController *audiobookVC = [self createPlayerVCForAudiobook:audiobookManager.audiobook