From fa30309e57a81029b6dc55eabeead6339f0e10b1 Mon Sep 17 00:00:00 2001 From: Mark Raynsford Date: Tue, 30 Apr 2024 12:13:54 +0000 Subject: [PATCH 01/11] Initial refactoring for new audiobook player Affects: https://ebce-lyrasis.atlassian.net/browse/PP-1074 Affects: https://ebce-lyrasis.atlassian.net/browse/PP-1075 Affects: https://ebce-lyrasis.atlassian.net/browse/PP-1076 Affects: https://ebce-lyrasis.atlassian.net/browse/PP-1082 --- .../AudioBookLoadingFragment.kt | 0 .../AudioBookLoadingFragmentListenerType.kt | 0 .../AudioBookLoadingFragmentParameters.kt | 0 .../AudioBookPlayerActivity.kt | 8 +- org.thepalaceproject.android.platform | 2 +- .../accounts/api/AccountDescription.java | 2 - .../api/AccountUnknownProviderException.kt | 2 - .../AccountUnresolvableProviderException.kt | 2 - .../accounts/json/AccountPreferencesJSON.kt | 1 - .../accounts/json/AccountProvidersJSON.kt | 8 +- ...countAuthenticationCredentialsAdobeJSON.kt | 6 +- .../adobe/extensions/AdobeDRMExtensions.kt | 6 +- .../adobe/extensions/AdobeDRMServices.java | 8 +- simplified-app-palace/build.gradle.kts | 11 +- .../palace/PalaceBuildConfigurationService.kt | 2 +- .../bookmarks/api/BookmarkAnnotations.kt | 236 +---- .../bookmarks/api/BookmarkAnnotationsJSON.kt | 220 +--- .../simplified/bookmarks/api/BookmarkEvent.kt | 4 +- .../api/BookmarkServiceUsableType.kt | 42 +- .../simplified/bookmarks/api/Bookmarks.kt | 18 - .../bookmarks/api/BookmarksForBook.kt | 37 + .../simplified/bookmarks/internal/BService.kt | 248 +++-- .../bookmarks/internal/BServiceBookmarks.kt | 89 -- .../internal/BServiceOpCreateBookmark.kt | 12 +- .../internal/BServiceOpCreateLocalBookmark.kt | 109 +- .../BServiceOpCreateRemoteBookmark.kt | 60 +- .../internal/BServiceOpDeleteBookmark.kt | 60 +- .../internal/BServiceOpLoadBookmarks.kt | 23 +- .../internal/BServiceOpSyncAllAccounts.kt | 3 +- .../internal/BServiceOpSyncOneAccount.kt | 218 +--- simplified-books-api/build.gradle.kts | 1 + .../books/api/BookChapterProgress.kt | 22 - .../books/api/BookContentProtections.kt | 2 +- .../nypl/simplified/books/api/BookFormat.kt | 27 +- .../nypl/simplified/books/api/BookLocation.kt | 50 - .../simplified/books/api/bookmark/Bookmark.kt | 395 -------- .../books/api/bookmark/BookmarkDigests.kt | 27 + .../books/api/bookmark/BookmarkID.kt | 5 +- .../books/api/bookmark/BookmarkJSON.kt | 669 ------------- .../books/api/bookmark/BookmarkMetadata.kt | 43 + .../books/api/bookmark/SerializedBookmark.kt | 125 +++ .../bookmark/SerializedBookmark20210317.kt | 87 ++ .../bookmark/SerializedBookmark20210828.kt | 87 ++ .../bookmark/SerializedBookmark20240424.kt | 95 ++ .../api/bookmark/SerializedBookmarkLegacy.kt | 111 +++ .../books/api/bookmark/SerializedBookmarks.kt | 238 +++++ .../books/api/bookmark/SerializedLocator.kt | 44 + .../SerializedLocatorAudioBookTime1.kt | 61 ++ .../SerializedLocatorAudioBookTime2.kt | 43 + ...erializedLocatorHrefProgression20210317.kt | 51 + .../bookmark/SerializedLocatorLegacyCFI.kt | 47 + .../api/bookmark/SerializedLocatorPage1.kt | 43 + .../books/api/bookmark/SerializedLocators.kt | 169 ++++ .../books/api/helper/AudiobookLocationJSON.kt | 102 -- .../books/api/helper/ReaderLocationJSON.kt | 224 ----- simplified-books-audio/build.gradle.kts | 2 +- .../AbstractAudioBookManifestStrategy.kt | 6 +- .../audio/AudioBookManifestStrategyType.kt | 2 +- .../UnpackagedAudioBookManifestStrategy.kt | 2 +- simplified-books-borrowing/build.gradle.kts | 3 +- .../books/borrowing/internal/BorrowACSM.kt | 2 +- .../borrowing/internal/BorrowAudioBook.kt | 2 +- .../books/controller/BookSyncTask.kt | 1 - .../simplified/books/controller/Controller.kt | 6 +- .../books/controller/ProfileFeedTask.kt | 1 - .../api/BookDRMInformationHandle.kt | 2 +- .../api/BookDatabaseEntryType.kt | 97 +- .../DatabaseFormatHandleAudioBook.kt | 122 ++- .../book_database/DatabaseFormatHandleEPUB.kt | 94 +- .../book_database/DatabaseFormatHandlePDF.kt | 100 +- .../book_database/NullDownloadProvider.kt | 9 +- .../book_registry/BookRegistryReadableType.kt | 3 - .../time/tracking/TimeTrackingService.kt | 31 +- .../org/nypl/simplified/feeds/api/Feed.kt | 2 - .../nypl/simplified/files/FileLocking.java | 1 + .../json/core/JSONParserUtilities.java | 938 ------------------ .../json/core/JSONParserUtilities.kt | 864 ++++++++++++++++ .../json/core/JSONSerializerUtilities.java | 66 -- .../json/core/JSONSerializerUtilities.kt | 76 ++ .../lcp/LCPContentProtectionProvider.kt | 2 +- .../main/MainAdobeWarnings.kt | 2 +- .../main/MainFragmentListenerDelegate.kt | 2 +- .../AuthenticationDocumentParser.kt | 2 +- .../core/OPDSAcquisitionFeedEntryParser.java | 38 +- .../nypl/simplified/opds/core/OPDSAtom.java | 1 - .../simplified/opds/core/OPDSFeedParser.java | 14 +- .../opds/core/OPDSSearchParser.java | 8 +- .../nypl/simplified/opds/core/OPDSXML.java | 1 - .../opds2/irradia/OPDS2ParsersIrradia.kt | 2 +- .../nypl/simplified/parser/api/ParseError.kt | 1 - .../simplified/parser/api/ParseWarning.kt | 1 - .../profiles/ProfileDescriptionJSON.kt | 37 +- .../reader/api/ReaderPreferencesJSON.java | 10 +- .../tenprint/TenPrintGenerator.java | 1 + simplified-tests/build.gradle.kts | 10 +- ...teAudiobookBookmarkAnnotationsJSONTest.kt} | 71 +- .../PDFBookmarkAnnotationsJSONTest.kt | 41 +- .../ReaderBookmarkAnnotationsJSONTest.kt | 80 +- .../bookmarks/AudiobookBookmarkJSONTest.kt | 107 -- .../tests/bookmarks/ReaderBookmarkJSONTest.kt | 346 ------- .../tests/books/AccountBug0613d7f6.kt | 1 - .../accounts/AccountsDatabaseContract.kt | 1 - .../books/accounts/AccountsDatabaseTest.java | 1 - .../audio/AudioBookManifestStrategyTest.kt | 2 +- .../books/audio/AudioBookSucceedingParsers.kt | 9 +- .../BookDatabaseAudioBookContract.kt | 1 - .../BookDatabaseAudioBookTest.kt | 1 - .../book_database/BookDatabaseContract.kt | 60 +- .../book_database/BookDatabaseEPUBContract.kt | 82 +- .../book_database/BookDatabaseEPUBTest.java | 1 - .../book_database/BookDatabasePDFContract.kt | 43 +- .../book_database/BookDatabasePDFTest.java | 1 - .../books/book_database/BookDatabaseTest.java | 1 - .../tests/books/bookmarks/BHTTPCallsTest.kt | 9 +- .../bookmarks/BookmarkServiceContract.kt | 64 +- .../books/bookmarks/BookmarkServiceTest.kt | 2 +- .../bookmarks/BookmarksSerializationTest.kt | 320 ++++++ .../tests/books/borrowing/BorrowACSMTest.kt | 1 - .../books/borrowing/BorrowAudioBookTest.kt | 1 - .../books/borrowing/BorrowAxisNowTest.kt | 1 - .../tests/books/borrowing/BorrowCopyTest.kt | 1 - .../borrowing/BorrowDirectDownloadTest.kt | 1 - .../books/borrowing/BorrowLimitLoanTest.kt | 1 - .../books/borrowing/BorrowLoanCreateTest.kt | 1 - .../books/borrowing/BorrowSAMLDownloadTest.kt | 1 - .../tests/books/borrowing/BorrowTaskTest.kt | 1 - .../controller/BookRevokeTaskAdobeDRMTest.kt | 5 - .../books/controller/BookRevokeTaskTest.kt | 3 - .../controller/BooksControllerContract.kt | 2 - .../books/controller/BooksControllerTest.java | 1 - .../ProfileAccountCreateCustomOPDSTest.kt | 2 - .../controller/ProfilesControllerContract.kt | 12 +- .../controller/ProfilesControllerTest.java | 1 - .../ProfileAccountLoginTaskContract.kt | 2 - .../ProfileAccountLogoutTaskContract.kt | 2 - .../profiles/ProfileDescriptionJSONTest.kt | 2 - .../profiles/ProfilesDatabaseContract.kt | 1 - .../books/profiles/ProfilesDatabaseTest.java | 1 - .../simplified/tests/bugs/Simply3635Test.kt | 1 - .../bookmarks/BookmarkRefreshTokenTest.kt | 1 - .../borrow/BorrowBookRefreshTokenTest.kt | 1 - .../lcp/LCPContentProtectionProviderTest.kt | 1 - .../tests/mocking/MockAccountProviders.kt | 3 - .../mocking/MockAudioBookManifestStrategy.kt | 4 +- ...kBookDatabaseEntryFormatHandleAudioBook.kt | 21 +- .../MockBookDatabaseEntryFormatHandleEPUB.kt | 19 +- .../MockBookDatabaseEntryFormatHandlePDF.kt | 19 +- .../tests/pdf/PdfViewerProviderTest.kt | 2 +- .../controller/testBooksRevokeEmptyFeed.xml | 2 +- .../nypl/simplified/tests/books/groups.xml | 3 +- .../org/nypl/simplified/tests/books/loans.xml | 10 +- .../books/revoke-error-empty-feed-revoke.xml | 8 +- .../tests/opds/acquisition-categories-0.xml | 9 +- .../tests/opds/acquisition-facets-0.xml | 9 +- .../tests/opds/acquisition-facets-1.xml | 9 +- .../tests/opds/acquisition-fiction-0.xml | 4 +- .../tests/opds/acquisition-groups-0.xml | 5 +- .../tests/opds/acquisition-paginated-0.xml | 5 +- .../tests/opds/analytics-20190509.xml | 4 +- .../simplified/tests/opds/bad-uri-syntax.xml | 9 +- .../simplified/tests/opds/dpla-test-feed.xml | 5 +- .../nypl/simplified/tests/opds/empty-0.xml | 9 +- .../nypl/simplified/tests/opds/entry-0.xml | 10 +- .../org/nypl/simplified/tests/opds/loans.xml | 3 +- .../tests/opds/minotaur-20231113.xml | 5 +- .../simplified/tests/opds/navigation-0.xml | 2 +- ...n-bad-entry-featured-link-without-href.xml | 2 +- ...navigation-bad-entry-link-without-href.xml | 2 +- .../opds/navigation-bad-entry-no-links.xml | 2 +- ...bad-entry-subsection-link-without-href.xml | 2 +- .../ui/accounts/AccountCardCreatorFragment.kt | 2 +- .../ui/accounts/AccountDetailFragment.kt | 6 +- .../ui/accounts/AccountListFragment.kt | 4 +- .../accounts/AccountListRegistryFragment.kt | 4 +- .../accounts/AccountPickerDialogFragment.kt | 2 +- .../accounts/FilterableAccountListAdapter.kt | 2 +- .../accounts/saml20/AccountSAML20Fragment.kt | 4 +- .../accounts/saml20/AccountSAML20ViewModel.kt | 2 +- .../view_bindings/ViewsForBasicToken.kt | 2 +- .../view_bindings/ViewsForCOPPAAgeGate.kt | 2 +- .../accounts/view_bindings/ViewsForSAML20.kt | 2 +- .../main/res/layout/account_list_item_old.xml | 1 - .../ui/catalog/AgeGateDialog.kt | 2 +- .../ui/catalog/CatalogBookDetailFragment.kt | 2 +- .../ui/catalog/CatalogFeedFragment.kt | 2 +- .../ui/catalog/CatalogPagedViewHolder.kt | 2 +- .../src/main/res/layout/book_saml20.xml | 1 - .../ui/navigation/tabs/BottomNavigators.kt | 8 +- .../OnboardingDefaultViewModelFactory.kt | 2 +- .../ui/onboarding/OnboardingFragment.kt | 2 +- .../res/layout/onboarding_start_screen.xml | 1 - .../ui/settings/SettingsCustomOPDSFragment.kt | 3 +- .../ui/settings/SettingsDebugFragment.kt | 2 +- .../ui/settings/SettingsDebugViewModel.kt | 2 +- .../SettingsDocumentViewerFragment.kt | 2 +- .../ui/settings/SettingsMainFragment.kt | 3 +- .../src/main/res/layout/settings_debug.xml | 8 +- .../ui/splash/BootViewModel.kt | 2 +- .../ui/splash/MailtoWebViewClient.kt | 4 +- .../ui/splash/MigrationReportEmail.kt | 2 +- .../src/main/res/layout/splash_selection.xml | 2 - .../org/nypl/simplified/viewer/api/Viewers.kt | 6 +- simplified-viewer-audiobook/build.gradle.kts | 7 +- .../viewer/audiobook/AudioBookHelpers.kt | 123 --- .../audiobook/AudioBookLoadingFragment2.kt | 5 + .../audiobook/AudioBookPlayerActivity2.kt | 73 ++ .../viewer/audiobook/AudioBookViewer.kt | 7 +- .../viewer/epub/readium2/Reader2Activity.kt | 4 +- .../viewer/epub/readium2/Reader2Bookmarks.kt | 121 +-- .../viewer/epub/readium2/ReaderViewerR2.kt | 6 +- .../viewer/pdf/pdfjs/PdfBookmark.kt | 9 + .../viewer/pdf/pdfjs/PdfBookmarkKind.kt | 6 + .../pdfjs/{factory => }/PdfDocumentFactory.kt | 2 +- .../viewer/pdf/pdfjs/PdfReaderActivity.kt | 198 ++-- .../viewer/pdf/pdfjs/PdfReaderBookmarks.kt | 89 +- .../pdfjs/{factory => }/PdfReaderDocument.kt | 2 +- .../viewer/pdf/pdfjs/PdfServer.kt | 1 - .../viewer/pdf/pdfjs/PdfViewerProvider.kt | 6 +- .../viewer/spi/ViewerProviderType.kt | 4 +- .../simplified/webview/WebViewUtilities.kt | 2 +- 220 files changed, 3933 insertions(+), 5070 deletions(-) rename {simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook => junk}/AudioBookLoadingFragment.kt (100%) rename {simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook => junk}/AudioBookLoadingFragmentListenerType.kt (100%) rename {simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook => junk}/AudioBookLoadingFragmentParameters.kt (100%) rename {simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook => junk}/AudioBookPlayerActivity.kt (99%) delete mode 100644 simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/Bookmarks.kt create mode 100644 simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarksForBook.kt delete mode 100644 simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceBookmarks.kt delete mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookChapterProgress.kt delete mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookLocation.kt delete mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/Bookmark.kt create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkDigests.kt delete mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkJSON.kt create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkMetadata.kt create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark.kt create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark20210317.kt create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark20210828.kt create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark20240424.kt create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmarkLegacy.kt create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmarks.kt create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocator.kt create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorAudioBookTime1.kt create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorAudioBookTime2.kt create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorHrefProgression20210317.kt create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorLegacyCFI.kt create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorPage1.kt create mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocators.kt delete mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/helper/AudiobookLocationJSON.kt delete mode 100644 simplified-books-api/src/main/java/org/nypl/simplified/books/api/helper/ReaderLocationJSON.kt delete mode 100644 simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONParserUtilities.java create mode 100644 simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONParserUtilities.kt delete mode 100644 simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONSerializerUtilities.java create mode 100644 simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONSerializerUtilities.kt rename simplified-tests/src/test/java/org/nypl/simplified/tests/bookmark_annotations/{AudiobookBookmarkAnnotationsJSONTest.kt => ObsoleteAudiobookBookmarkAnnotationsJSONTest.kt} (86%) delete mode 100644 simplified-tests/src/test/java/org/nypl/simplified/tests/bookmarks/AudiobookBookmarkJSONTest.kt delete mode 100644 simplified-tests/src/test/java/org/nypl/simplified/tests/bookmarks/ReaderBookmarkJSONTest.kt create mode 100644 simplified-tests/src/test/java/org/nypl/simplified/tests/books/bookmarks/BookmarksSerializationTest.kt delete mode 100644 simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookHelpers.kt create mode 100644 simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookLoadingFragment2.kt create mode 100644 simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookPlayerActivity2.kt create mode 100644 simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfBookmark.kt create mode 100644 simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfBookmarkKind.kt rename simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/{factory => }/PdfDocumentFactory.kt (95%) rename simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/{factory => }/PdfReaderDocument.kt (97%) diff --git a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookLoadingFragment.kt b/junk/AudioBookLoadingFragment.kt similarity index 100% rename from simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookLoadingFragment.kt rename to junk/AudioBookLoadingFragment.kt diff --git a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookLoadingFragmentListenerType.kt b/junk/AudioBookLoadingFragmentListenerType.kt similarity index 100% rename from simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookLoadingFragmentListenerType.kt rename to junk/AudioBookLoadingFragmentListenerType.kt diff --git a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookLoadingFragmentParameters.kt b/junk/AudioBookLoadingFragmentParameters.kt similarity index 100% rename from simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookLoadingFragmentParameters.kt rename to junk/AudioBookLoadingFragmentParameters.kt diff --git a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookPlayerActivity.kt b/junk/AudioBookPlayerActivity.kt similarity index 99% rename from simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookPlayerActivity.kt rename to junk/AudioBookPlayerActivity.kt index 2bb9970df..3f1642d95 100644 --- a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookPlayerActivity.kt +++ b/junk/AudioBookPlayerActivity.kt @@ -167,7 +167,7 @@ class AudioBookPlayerActivity : private val reloadingManifest = AtomicBoolean(false) private val currentBookmarks = - Collections.synchronizedList(arrayListOf()) + Collections.synchronizedList(arrayListOf()) @Volatile private var destroying: Boolean = false @@ -353,7 +353,7 @@ class AudioBookPlayerActivity : private fun savePlayerPosition(event: PlayerEventCreateBookmark) { try { - val bookmark = Bookmark.AudiobookBookmark.create( + val bookmark = Bookmark.ObsoleteAudiobookBookmark.create( opdsId = this.parameters.opdsEntry.id, location = PlayerPosition( title = event.spineElement.position.title, @@ -690,7 +690,7 @@ class AudioBookPlayerActivity : try { val audiobookBookmarks = bookmarks - .filterIsInstance() + .filterIsInstance() val bookMarkLastReadPosition = audiobookBookmarks.find { bookmark -> bookmark.kind == BookmarkKind.BookmarkLastReadLocation @@ -1058,7 +1058,7 @@ class AudioBookPlayerActivity : bookmark = bookmark, ignoreRemoteFailures = true ).map { savedBookmark -> - this.currentBookmarks.add(savedBookmark as Bookmark.AudiobookBookmark) + this.currentBookmarks.add(savedBookmark as Bookmark.ObsoleteAudiobookBookmark) this.showToastMessage(R.string.audio_book_player_bookmark_added) }.onAnyError { /* Otherwise, something in the chain failed. */ diff --git a/org.thepalaceproject.android.platform b/org.thepalaceproject.android.platform index acdff6b8c..b90bb370b 160000 --- a/org.thepalaceproject.android.platform +++ b/org.thepalaceproject.android.platform @@ -1 +1 @@ -Subproject commit acdff6b8ce245eae9b08c45da34bd8fe82f1b460 +Subproject commit b90bb370b7045d54ed5143cb2e03c6f2ba57ebae diff --git a/simplified-accounts-api/src/main/java/org/nypl/simplified/accounts/api/AccountDescription.java b/simplified-accounts-api/src/main/java/org/nypl/simplified/accounts/api/AccountDescription.java index 2d4efc321..b3956bf2d 100644 --- a/simplified-accounts-api/src/main/java/org/nypl/simplified/accounts/api/AccountDescription.java +++ b/simplified-accounts-api/src/main/java/org/nypl/simplified/accounts/api/AccountDescription.java @@ -2,8 +2,6 @@ import com.google.auto.value.AutoValue; -import java.net.URI; - /** * A description of an account. */ diff --git a/simplified-accounts-api/src/main/java/org/nypl/simplified/accounts/api/AccountUnknownProviderException.kt b/simplified-accounts-api/src/main/java/org/nypl/simplified/accounts/api/AccountUnknownProviderException.kt index df643613f..360dd27c6 100644 --- a/simplified-accounts-api/src/main/java/org/nypl/simplified/accounts/api/AccountUnknownProviderException.kt +++ b/simplified-accounts-api/src/main/java/org/nypl/simplified/accounts/api/AccountUnknownProviderException.kt @@ -1,7 +1,5 @@ package org.nypl.simplified.accounts.api -import java.lang.Exception - /** * An unrecognized provider was specified when trying to create an account. */ diff --git a/simplified-accounts-api/src/main/java/org/nypl/simplified/accounts/api/AccountUnresolvableProviderException.kt b/simplified-accounts-api/src/main/java/org/nypl/simplified/accounts/api/AccountUnresolvableProviderException.kt index aae0c40a4..c19bdc61c 100644 --- a/simplified-accounts-api/src/main/java/org/nypl/simplified/accounts/api/AccountUnresolvableProviderException.kt +++ b/simplified-accounts-api/src/main/java/org/nypl/simplified/accounts/api/AccountUnresolvableProviderException.kt @@ -1,7 +1,5 @@ package org.nypl.simplified.accounts.api -import java.lang.Exception - /** * An unresolvable provider was specified when trying to create an account. */ diff --git a/simplified-accounts-json/src/main/java/org/nypl/simplified/accounts/json/AccountPreferencesJSON.kt b/simplified-accounts-json/src/main/java/org/nypl/simplified/accounts/json/AccountPreferencesJSON.kt index 15a638b88..828bf52b2 100644 --- a/simplified-accounts-json/src/main/java/org/nypl/simplified/accounts/json/AccountPreferencesJSON.kt +++ b/simplified-accounts-json/src/main/java/org/nypl/simplified/accounts/json/AccountPreferencesJSON.kt @@ -8,7 +8,6 @@ import org.nypl.simplified.accounts.api.AccountPreferences import org.nypl.simplified.json.core.JSONParseException import org.nypl.simplified.json.core.JSONParserUtilities import org.slf4j.LoggerFactory -import java.lang.Exception import java.util.UUID /** diff --git a/simplified-accounts-json/src/main/java/org/nypl/simplified/accounts/json/AccountProvidersJSON.kt b/simplified-accounts-json/src/main/java/org/nypl/simplified/accounts/json/AccountProvidersJSON.kt index 0f8ae32d3..e05517c54 100644 --- a/simplified-accounts-json/src/main/java/org/nypl/simplified/accounts/json/AccountProvidersJSON.kt +++ b/simplified-accounts-json/src/main/java/org/nypl/simplified/accounts/json/AccountProvidersJSON.kt @@ -17,8 +17,8 @@ import org.nypl.simplified.accounts.api.AccountProviderAuthenticationDescription import org.nypl.simplified.accounts.api.AccountProviderAuthenticationDescription.BasicToken import org.nypl.simplified.accounts.api.AccountProviderAuthenticationDescription.COPPAAgeGate import org.nypl.simplified.accounts.api.AccountProviderAuthenticationDescription.Companion.ANONYMOUS_TYPE -import org.nypl.simplified.accounts.api.AccountProviderAuthenticationDescription.Companion.BASIC_TYPE import org.nypl.simplified.accounts.api.AccountProviderAuthenticationDescription.Companion.BASIC_TOKEN_TYPE +import org.nypl.simplified.accounts.api.AccountProviderAuthenticationDescription.Companion.BASIC_TYPE import org.nypl.simplified.accounts.api.AccountProviderAuthenticationDescription.Companion.COPPA_TYPE import org.nypl.simplified.accounts.api.AccountProviderAuthenticationDescription.Companion.OAUTH_INTERMEDIARY_TYPE import org.nypl.simplified.accounts.api.AccountProviderAuthenticationDescription.Companion.SAML_2_0_TYPE @@ -486,7 +486,7 @@ object AccountProvidersJSON { val logoURI = JSONParserUtilities.getURIOrNull(container, "logo") val authenticationURI = - JSONParserUtilities.getURIOrNull(container, "authenticationURI") + JSONParserUtilities.getURI(container, "authenticationURI") BasicToken( authenticationURI = authenticationURI, @@ -503,9 +503,9 @@ object AccountProvidersJSON { COPPA_TYPE -> { COPPAAgeGate( greaterEqual13 = - JSONParserUtilities.getURIOrNull(container, "greaterEqual13"), + JSONParserUtilities.getURI(container, "greaterEqual13"), under13 = - JSONParserUtilities.getURIOrNull(container, "under13") + JSONParserUtilities.getURI(container, "under13") ) } else -> { diff --git a/simplified-accounts-json/src/main/java/org/nypl/simplified/accounts/json/internal/AccountAuthenticationCredentialsAdobeJSON.kt b/simplified-accounts-json/src/main/java/org/nypl/simplified/accounts/json/internal/AccountAuthenticationCredentialsAdobeJSON.kt index a2632f9d9..313389281 100644 --- a/simplified-accounts-json/src/main/java/org/nypl/simplified/accounts/json/internal/AccountAuthenticationCredentialsAdobeJSON.kt +++ b/simplified-accounts-json/src/main/java/org/nypl/simplified/accounts/json/internal/AccountAuthenticationCredentialsAdobeJSON.kt @@ -2,13 +2,13 @@ package org.nypl.simplified.accounts.json.internal import com.fasterxml.jackson.databind.node.ObjectNode import com.io7m.jfunctional.Some +import org.nypl.drm.core.AdobeDeviceID +import org.nypl.drm.core.AdobeUserID +import org.nypl.drm.core.AdobeVendorID import org.nypl.simplified.accounts.api.AccountAuthenticationAdobeClientToken import org.nypl.simplified.accounts.api.AccountAuthenticationAdobePostActivationCredentials import org.nypl.simplified.accounts.api.AccountAuthenticationAdobePreActivationCredentials import org.nypl.simplified.json.core.JSONParserUtilities -import org.nypl.drm.core.AdobeDeviceID -import org.nypl.drm.core.AdobeUserID -import org.nypl.drm.core.AdobeVendorID object AccountAuthenticationCredentialsAdobeJSON { diff --git a/simplified-adobe-extensions/src/main/java/org/nypl/simplified/adobe/extensions/AdobeDRMExtensions.kt b/simplified-adobe-extensions/src/main/java/org/nypl/simplified/adobe/extensions/AdobeDRMExtensions.kt index 21f6952d4..07c499774 100644 --- a/simplified-adobe-extensions/src/main/java/org/nypl/simplified/adobe/extensions/AdobeDRMExtensions.kt +++ b/simplified-adobe-extensions/src/main/java/org/nypl/simplified/adobe/extensions/AdobeDRMExtensions.kt @@ -2,9 +2,6 @@ package org.nypl.simplified.adobe.extensions import com.google.common.util.concurrent.ListenableFuture import com.google.common.util.concurrent.SettableFuture -import org.nypl.simplified.accounts.api.AccountAuthenticationAdobeClientToken -import org.nypl.simplified.accounts.api.AccountAuthenticationAdobePostActivationCredentials -import org.nypl.simplified.files.FileUtilities import org.nypl.drm.core.AdobeAdeptActivationReceiverType import org.nypl.drm.core.AdobeAdeptConnectorType import org.nypl.drm.core.AdobeAdeptDeactivationReceiverType @@ -15,6 +12,9 @@ import org.nypl.drm.core.AdobeAdeptLoanReturnListenerType import org.nypl.drm.core.AdobeDeviceID import org.nypl.drm.core.AdobeUserID import org.nypl.drm.core.AdobeVendorID +import org.nypl.simplified.accounts.api.AccountAuthenticationAdobeClientToken +import org.nypl.simplified.accounts.api.AccountAuthenticationAdobePostActivationCredentials +import org.nypl.simplified.files.FileUtilities import java.io.File import java.util.concurrent.CancellationException diff --git a/simplified-adobe-extensions/src/main/java/org/nypl/simplified/adobe/extensions/AdobeDRMServices.java b/simplified-adobe-extensions/src/main/java/org/nypl/simplified/adobe/extensions/AdobeDRMServices.java index 1779ec0cd..36fb07fa0 100644 --- a/simplified-adobe-extensions/src/main/java/org/nypl/simplified/adobe/extensions/AdobeDRMServices.java +++ b/simplified-adobe-extensions/src/main/java/org/nypl/simplified/adobe/extensions/AdobeDRMServices.java @@ -20,10 +20,6 @@ import org.joda.time.Instant; import org.librarysimplified.adobe.extensions.BuildConfig; -import org.nypl.simplified.files.DirectoryUtilities; -import org.nypl.simplified.json.core.JSONParserUtilities; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import org.nypl.drm.core.AdobeAdeptConnectorFactory; import org.nypl.drm.core.AdobeAdeptConnectorFactoryType; import org.nypl.drm.core.AdobeAdeptConnectorParameters; @@ -38,6 +34,10 @@ import org.nypl.drm.core.AdobeAdeptResourceProviderType; import org.nypl.drm.core.DRMException; import org.nypl.drm.core.DRMUnsupportedException; +import org.nypl.simplified.files.DirectoryUtilities; +import org.nypl.simplified.json.core.JSONParserUtilities; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.ByteArrayOutputStream; import java.io.File; diff --git a/simplified-app-palace/build.gradle.kts b/simplified-app-palace/build.gradle.kts index e4e701f07..68e1aad82 100644 --- a/simplified-app-palace/build.gradle.kts +++ b/simplified-app-palace/build.gradle.kts @@ -356,6 +356,9 @@ dependencies { */ if (findawayDRM) { + implementation(libs.palace.audiobook.audioengine) + + // Findaway transitive dependencies. implementation(libs.dagger) implementation(libs.exoplayer2.core) implementation(libs.findaway) @@ -373,12 +376,12 @@ dependencies { implementation(libs.moshi.kotlin) implementation(libs.okhttp3) implementation(libs.okhttp3.logging.interceptor) - implementation(libs.palace.findaway) implementation(libs.retrofit2) implementation(libs.retrofit2.adapter.rxjava) implementation(libs.retrofit2.converter.gson) implementation(libs.retrofit2.converter.moshi) implementation(libs.rxandroid) + implementation(libs.rxjava) implementation(libs.rxrelay) implementation(libs.sqlbrite) implementation(libs.stately.common) @@ -492,6 +495,7 @@ dependencies { implementation(libs.androidx.viewpager) implementation(libs.androidx.viewpager2) implementation(libs.androidx.webkit) + implementation(libs.azam.ulidj) implementation(libs.commons.compress) implementation(libs.firebase.analytics) @@ -538,6 +542,7 @@ dependencies { implementation(libs.javax.inject) implementation(libs.joda.time) implementation(libs.jsoup) + implementation(libs.kabstand) implementation(libs.koi.core) implementation(libs.kotlin.reflect) implementation(libs.kotlin.stdlib) @@ -557,7 +562,6 @@ dependencies { implementation(libs.palace.audiobook.http) implementation(libs.palace.audiobook.json.canon) implementation(libs.palace.audiobook.json.web.token) - implementation(libs.palace.audiobook.lcp) implementation(libs.palace.audiobook.lcp.license.status) implementation(libs.palace.audiobook.license.check.api) implementation(libs.palace.audiobook.license.check.spi) @@ -569,9 +573,8 @@ dependencies { implementation(libs.palace.audiobook.manifest.parser.api) implementation(libs.palace.audiobook.manifest.parser.extension.spi) implementation(libs.palace.audiobook.manifest.parser.webpub) - implementation(libs.palace.audiobook.open.access) + implementation(libs.palace.audiobook.media3) implementation(libs.palace.audiobook.parser.api) - implementation(libs.palace.audiobook.rbdigital) implementation(libs.palace.audiobook.views) implementation(libs.palace.drm.core) implementation(libs.palace.http.api) diff --git a/simplified-app-palace/src/main/java/org/thepalaceproject/palace/PalaceBuildConfigurationService.kt b/simplified-app-palace/src/main/java/org/thepalaceproject/palace/PalaceBuildConfigurationService.kt index 8cd0369c4..e1112e703 100644 --- a/simplified-app-palace/src/main/java/org/thepalaceproject/palace/PalaceBuildConfigurationService.kt +++ b/simplified-app-palace/src/main/java/org/thepalaceproject/palace/PalaceBuildConfigurationService.kt @@ -1,9 +1,9 @@ package org.thepalaceproject.palace +import org.librarysimplified.main.BuildConfig import org.nypl.simplified.buildconfig.api.BuildConfigOAuthScheme import org.nypl.simplified.buildconfig.api.BuildConfigurationAccountsRegistryURIs import org.nypl.simplified.buildconfig.api.BuildConfigurationServiceType -import org.librarysimplified.main.BuildConfig import java.net.URI class PalaceBuildConfigurationService : BuildConfigurationServiceType { diff --git a/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkAnnotations.kt b/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkAnnotations.kt index 6ef22e51c..7da051d8a 100644 --- a/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkAnnotations.kt +++ b/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkAnnotations.kt @@ -1,12 +1,12 @@ package org.nypl.simplified.bookmarks.api import com.fasterxml.jackson.databind.ObjectMapper -import org.joda.time.DateTime -import org.joda.time.DateTimeZone import org.joda.time.format.DateTimeFormat import org.joda.time.format.ISODateTimeFormat -import org.nypl.simplified.books.api.bookmark.Bookmark import org.nypl.simplified.books.api.bookmark.BookmarkKind +import org.nypl.simplified.books.api.bookmark.SerializedBookmark +import org.nypl.simplified.books.api.bookmark.SerializedBookmark20210828 +import org.nypl.simplified.books.api.bookmark.SerializedLocators import java.net.URI data class BookmarkAnnotationSelectorNode( @@ -22,8 +22,8 @@ data class BookmarkAnnotationTargetNode( data class BookmarkAnnotationBodyNode( val timestamp: String, val device: String, - val chapterTitle: String?, - val bookProgress: Float? + val chapterTitle: String = "", + val bookProgress: Float = 0.0f ) data class BookmarkAnnotation( @@ -67,236 +67,58 @@ object BookmarkAnnotations { private val dateFormatter = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss'Z'") - fun fromReaderBookmark( + fun fromSerializedBookmark( objectMapper: ObjectMapper, - bookmark: Bookmark.ReaderBookmark + serializedBookmark: SerializedBookmark ): BookmarkAnnotation { - /* - * Check for some values that were likely added by [toBookmark]. Write special values here - * to ensure that [fromBookmark] is the exact inverse of [toBookmark]. - */ - - val chapterTitle = - if (bookmark.chapterTitle == "") { - null - } else { - bookmark.chapterTitle - } - - val bookProgress = - if (bookmark.bookProgress == 0.0) { - null - } else { - bookmark.bookProgress?.toFloat() - } - val timestamp = - dateFormatter.print(bookmark.time) + this.dateFormatter.print(serializedBookmark.time) val bodyAnnotation = BookmarkAnnotationBodyNode( timestamp = timestamp, - device = bookmark.deviceID, - chapterTitle = chapterTitle, - bookProgress = bookProgress + device = serializedBookmark.deviceID, + chapterTitle = serializedBookmark.bookChapterTitle, + bookProgress = serializedBookmark.bookProgress.toFloat() ) val locationJSON = - BookmarkAnnotationsJSON.serializeBookmarkLocation( - objectMapper = objectMapper, - bookmark = bookmark - ) + serializedBookmark.location.toJSONString(objectMapper) val target = BookmarkAnnotationTargetNode( - bookmark.opdsId, + serializedBookmark.opdsId, BookmarkAnnotationSelectorNode("oa:FragmentSelector", locationJSON) ) return BookmarkAnnotation( context = "http://www.w3.org/ns/anno.jsonld", body = bodyAnnotation, - id = bookmark.uri?.toString(), + id = serializedBookmark.uri?.toString(), type = "Annotation", - motivation = bookmark.kind.motivationURI, + motivation = serializedBookmark.kind.motivationURI, target = target ) } - fun toReaderBookmark( + fun toSerializedBookmark( objectMapper: ObjectMapper, annotation: BookmarkAnnotation - ): Bookmark.ReaderBookmark { - val locationJSON = - BookmarkAnnotationsJSON.deserializeReaderLocation( - objectMapper = objectMapper, - value = annotation.target.selector.value - ) - - val time = - if (annotation.body.timestamp != null) { - dateParser.parseDateTime(annotation.body.timestamp) - } else { - DateTime.now(DateTimeZone.UTC) - } - - return Bookmark.ReaderBookmark.create( - opdsId = annotation.target.source, - location = locationJSON, - kind = BookmarkKind.ofMotivation(annotation.motivation), - time = time, - chapterTitle = annotation.body.chapterTitle ?: "", - bookProgress = annotation.body.bookProgress?.toDouble(), - uri = if (annotation.id != null) URI.create(annotation.id) else null, - deviceID = annotation.body.device - ) - } - - fun fromAudiobookBookmark( - objectMapper: ObjectMapper, - bookmark: Bookmark.AudiobookBookmark - ): BookmarkAnnotation { - /* - * Check for some values that were likely added by [toBookmark]. Write special values here - * to ensure that [fromBookmark] is the exact inverse of [toBookmark]. - */ - - val timestamp = - this.dateFormatter.print(bookmark.time) - - val bodyAnnotation = - BookmarkAnnotationBodyNode( - timestamp = timestamp, - device = bookmark.deviceID, - chapterTitle = bookmark.location.title.orEmpty(), - bookProgress = null - ) - - val locationJSON = - BookmarkAnnotationsJSON.serializeBookmarkLocation( - objectMapper = objectMapper, - bookmark = bookmark - ) - - val target = - BookmarkAnnotationTargetNode( - bookmark.opdsId, - BookmarkAnnotationSelectorNode("oa:FragmentSelector", locationJSON) - ) - - return BookmarkAnnotation( - context = "http://www.w3.org/ns/anno.jsonld", - body = bodyAnnotation, - id = bookmark.uri?.toString(), - type = "Annotation", - motivation = bookmark.kind.motivationURI, - target = target - ) - } - - fun toAudiobookBookmark( - objectMapper: ObjectMapper, - annotation: BookmarkAnnotation - ): Bookmark.AudiobookBookmark { - val locationJSON = - BookmarkAnnotationsJSON.deserializeAudiobookLocation( - objectMapper = objectMapper, - value = annotation.target.selector.value - ) - - val duration = - BookmarkAnnotationsJSON.deserializeAudiobookDuration( - objectMapper = objectMapper, - value = annotation.target.selector.value - ) - - val time = - if (annotation.body.timestamp != null) { - this.dateParser.parseDateTime(annotation.body.timestamp) - } else { - DateTime.now(DateTimeZone.UTC) - } - - return Bookmark.AudiobookBookmark.create( - opdsId = annotation.target.source, - location = locationJSON, - duration = duration, - kind = BookmarkKind.ofMotivation(annotation.motivation), - time = time, - uri = if (annotation.id != null) URI.create(annotation.id) else null, - deviceID = annotation.body.device - ) - } - - fun fromPdfBookmark( - objectMapper: ObjectMapper, - bookmark: Bookmark.PDFBookmark - ): BookmarkAnnotation { - /* - * Check for some values that were likely added by [toBookmark]. Write special values here - * to ensure that [fromBookmark] is the exact inverse of [toBookmark]. - */ - - val chapterTitle = null - val bookProgress = null - - val timestamp = - dateFormatter.print(bookmark.time) - - val bodyAnnotation = - BookmarkAnnotationBodyNode( - timestamp = timestamp, - device = bookmark.deviceID, - chapterTitle = chapterTitle, - bookProgress = bookProgress - ) - - val locationJSON = - BookmarkAnnotationsJSON.serializeBookmarkLocation( - objectMapper = objectMapper, - bookmark = bookmark - ) - - val target = - BookmarkAnnotationTargetNode( - bookmark.opdsId, - BookmarkAnnotationSelectorNode("oa:FragmentSelector", locationJSON) - ) - - return BookmarkAnnotation( - context = "http://www.w3.org/ns/anno.jsonld", - body = bodyAnnotation, - id = bookmark.uri?.toString(), - type = "Annotation", - motivation = bookmark.kind.motivationURI, - target = target - ) - } - - fun toPdfBookmark( - objectMapper: ObjectMapper, - annotation: BookmarkAnnotation - ): Bookmark.PDFBookmark { - val locationJSON = - BookmarkAnnotationsJSON.deserializePdfLocation( - objectMapper = objectMapper, - value = annotation.target.selector.value - ) - - val time = - if (annotation.body.timestamp != null) { - dateParser.parseDateTime(annotation.body.timestamp) - } else { - DateTime.now(DateTimeZone.UTC) - } - - return Bookmark.PDFBookmark.create( + ): SerializedBookmark { + val location = + SerializedLocators.parseLocator(objectMapper.readTree(annotation.target.selector.value)) + + return SerializedBookmark20210828( + deviceID = annotation.body.device, + kind = annotation.kind, + location = location, opdsId = annotation.target.source, - kind = BookmarkKind.ofMotivation(annotation.motivation), - time = time, - pageNumber = locationJSON, + time = this.dateParser.parseDateTime(annotation.body.timestamp), uri = if (annotation.id != null) URI.create(annotation.id) else null, - deviceID = annotation.body.device + bookProgress = annotation.body.bookProgress.toDouble(), + bookChapterProgress = 0.0, + bookChapterTitle = annotation.body.chapterTitle, + bookTitle = "" ) } } diff --git a/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkAnnotationsJSON.kt b/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkAnnotationsJSON.kt index 3f34d659d..38a4f8ddf 100644 --- a/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkAnnotationsJSON.kt +++ b/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkAnnotationsJSON.kt @@ -8,10 +8,7 @@ import com.fasterxml.jackson.databind.node.ArrayNode import com.fasterxml.jackson.databind.node.ObjectNode import com.io7m.jfunctional.OptionType import com.io7m.jfunctional.Some -import org.librarysimplified.audiobook.api.PlayerPosition -import org.nypl.simplified.books.api.BookChapterProgress -import org.nypl.simplified.books.api.BookLocation -import org.nypl.simplified.books.api.bookmark.Bookmark +import org.nypl.simplified.books.api.bookmark.SerializedLocators import org.nypl.simplified.json.core.JSONParseException import org.nypl.simplified.json.core.JSONParserUtilities @@ -33,22 +30,8 @@ object BookmarkAnnotationsJSON { */ try { - val selectorNode = - objectMapper.readTree(value) - val selectorObj = - JSONParserUtilities.checkObject(null, selectorNode) - - when (JSONParserUtilities.getStringOrNull(selectorObj, "@type")) { - "LocatorAudioBookTime" -> { - deserializeAudiobookLocation(objectMapper, value) - } - "LocatorPage" -> { - deserializePdfLocation(objectMapper, value) - } - else -> { - deserializeReaderLocation(objectMapper, value) - } - } + val selectorNode = objectMapper.readTree(value) + SerializedLocators.parseLocator(selectorNode) } catch (e: Exception) { throw JSONParseException(e) } @@ -137,14 +120,16 @@ object BookmarkAnnotationsJSON { device = JSONParserUtilities.getString(node, "http://librarysimplified.org/terms/device"), chapterTitle = - JSONParserUtilities.getStringOrNull(node, "http://librarysimplified.org/terms/chapter"), - bookProgress = mapOptionNull( - JSONParserUtilities.getDoubleOptional( - node, - "http://librarysimplified.org/terms/progressWithinBook" - ) - .map { x -> x.toFloat() } - ) + JSONParserUtilities.getStringDefault( + node, + "http://librarysimplified.org/terms/chapter", + "" + ), + bookProgress = JSONParserUtilities.getDoubleDefault( + node, + "http://librarysimplified.org/terms/progressWithinBook", + 0.0 + ).toFloat() ) } @@ -309,183 +294,4 @@ object BookmarkAnnotationsJSON { node = JSONParserUtilities.checkObject(null, node), ) } - - @Throws(JSONParseException::class) - fun serializeBookmarkLocation( - objectMapper: ObjectMapper, - bookmark: Bookmark - ): String { - objectMapper.configure(SerializationFeature.INDENT_OUTPUT, false) - objectMapper.configure(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, true) - return when (bookmark) { - is Bookmark.AudiobookBookmark -> - objectMapper.writeValueAsString(serializeLocationToNode(objectMapper, bookmark)) - is Bookmark.ReaderBookmark -> - objectMapper.writeValueAsString( - serializeLocationToNode( - objectMapper, - bookmark.location - ) - ) - is Bookmark.PDFBookmark -> { - objectMapper.writeValueAsString(serializeIndexToNode(objectMapper, bookmark.pageNumber)) - } - else -> - throw IllegalArgumentException("Unsupported bookmark type: $bookmark") - } - } - - @Throws(JSONParseException::class) - private fun serializeIndexToNode( - objectMapper: ObjectMapper, - pageIndex: Int - ): ObjectNode { - val objectNode = objectMapper.createObjectNode() - objectNode.put("@type", "LocatorPage") - objectNode.put("page", pageIndex) - return objectNode - } - - @Throws(JSONParseException::class) - private fun serializeLocationToNode( - objectMapper: ObjectMapper, - location: BookLocation - ): ObjectNode { - val objectNode = objectMapper.createObjectNode() - return when (location) { - is BookLocation.BookLocationR2 -> { - objectNode.put("@type", "LocatorHrefProgression") - objectNode.put("href", location.progress.chapterHref) - objectNode.put("progressWithinChapter", location.progress.chapterProgress) - objectNode - } - is BookLocation.BookLocationR1 -> { - objectNode.put("@type", "LocatorLegacyCFI") - location.idRef?.let { - objectNode.put("idref", it) - } - location.contentCFI?.let { - objectNode.put("contentCFI", it) - } - objectNode.put("progressWithinChapter", location.progress ?: 0.0) - objectNode - } - } - } - - @Throws(JSONParseException::class) - private fun serializeLocationToNode( - objectMapper: ObjectMapper, - bookmark: Bookmark.AudiobookBookmark - ): ObjectNode { - val objectNode = objectMapper.createObjectNode() - objectNode.put("@type", "LocatorAudioBookTime") - objectNode.put("chapter", bookmark.location.chapter) - objectNode.put("startOffset", bookmark.location.startOffset) - objectNode.put( - "time", - bookmark.location.startOffset + bookmark.location.currentOffset - ) - objectNode.put("part", bookmark.location.part) - objectNode.put("title", bookmark.location.title.orEmpty()) - - // these fields are required by the iOS app, so we're sending them but since we don't need them - // in the Android app, there's no need to parsing them back - objectNode.put("audiobookID", bookmark.opdsId) - objectNode.put("duration", bookmark.duration) - - return objectNode - } - - @Throws(JSONParseException::class) - fun deserializeAudiobookLocation( - objectMapper: ObjectMapper, - value: String - ): PlayerPosition { - val node = - objectMapper.readTree(value) - val obj = - JSONParserUtilities.checkObject(null, node) - - val startOffset = JSONParserUtilities.getIntegerDefault(obj, "startOffset", 0).toLong() - - return PlayerPosition( - chapter = JSONParserUtilities.getIntegerDefault(obj, "chapter", 0), - startOffset = startOffset, - currentOffset = JSONParserUtilities.getInteger(obj, "time").toLong() - startOffset, - part = JSONParserUtilities.getIntegerDefault(obj, "part", 0), - title = JSONParserUtilities.getStringOrNull(obj, "title") - ) - } - - @Throws(JSONParseException::class) - fun deserializeAudiobookDuration( - objectMapper: ObjectMapper, - value: String - ): Long { - val node = - objectMapper.readTree(value) - val obj = - JSONParserUtilities.checkObject(null, node) - - return JSONParserUtilities.getInteger(obj, "duration").toLong() - } - - @Throws(JSONParseException::class) - fun deserializePdfLocation( - objectMapper: ObjectMapper, - value: String - ): Int { - val node = - objectMapper.readTree(value) - val obj = - JSONParserUtilities.checkObject(null, node) - - return JSONParserUtilities.getInteger(obj, "page") - } - - @Throws(JSONParseException::class) - fun deserializeReaderLocation( - objectMapper: ObjectMapper, - value: String - ): BookLocation { - val node = - objectMapper.readTree(value) - val obj = - JSONParserUtilities.checkObject(null, node) - val type = - JSONParserUtilities.getStringOrNull(obj, "@type") - - return when (type) { - "LocatorHrefProgression" -> - deserializeLocationR2(obj) - "LocatorLegacyCFI" -> - deserializeLocationLegacyCFI(obj) - null -> - deserializeLocationLegacyCFI(obj) - else -> - throw JSONParseException("Unsupported locator type: $type") - } - } - - private fun deserializeLocationLegacyCFI( - obj: ObjectNode - ): BookLocation.BookLocationR1 { - return BookLocation.BookLocationR1( - progress = JSONParserUtilities.getDoubleDefault(obj, "progressWithinChapter", 0.0), - contentCFI = JSONParserUtilities.getStringOrNull(obj, "contentCFI"), - idRef = JSONParserUtilities.getStringOrNull(obj, "idref"), - ) - } - - private fun deserializeLocationR2( - obj: ObjectNode - ): BookLocation.BookLocationR2 { - val progress = - BookChapterProgress( - chapterHref = JSONParserUtilities.getString(obj, "href"), - chapterProgress = JSONParserUtilities.getDouble(obj, "progressWithinChapter") - ) - return BookLocation.BookLocationR2(progress) - } } diff --git a/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkEvent.kt b/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkEvent.kt index 43376e096..ddcf7d49c 100644 --- a/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkEvent.kt +++ b/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkEvent.kt @@ -1,7 +1,7 @@ package org.nypl.simplified.bookmarks.api import org.nypl.simplified.accounts.api.AccountID -import org.nypl.simplified.books.api.bookmark.Bookmark +import org.nypl.simplified.books.api.bookmark.SerializedBookmark /** * The type of events published by the bookmark controller. @@ -31,6 +31,6 @@ sealed class BookmarkEvent { data class BookmarkSaved( val accountID: AccountID, - val bookmark: Bookmark + val bookmark: SerializedBookmark ) : BookmarkEvent() } diff --git a/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkServiceUsableType.kt b/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkServiceUsableType.kt index 7e99decc1..162552eb4 100644 --- a/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkServiceUsableType.kt +++ b/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarkServiceUsableType.kt @@ -1,10 +1,10 @@ package org.nypl.simplified.bookmarks.api -import com.google.common.util.concurrent.FluentFuture import io.reactivex.Observable import org.nypl.simplified.accounts.api.AccountID import org.nypl.simplified.books.api.BookID -import org.nypl.simplified.books.api.bookmark.Bookmark +import org.nypl.simplified.books.api.bookmark.SerializedBookmark +import java.util.concurrent.CompletableFuture /** * The "usable" bookmark service interface. Usable, in this sense, refers to the @@ -19,21 +19,20 @@ interface BookmarkServiceUsableType { val bookmarkEvents: Observable - /** - * Sync the bookmarks for the given account. - */ - fun bookmarkSyncAccount( - accountID: AccountID, - bookID: BookID - ): FluentFuture - /** * Sync the bookmarks for the given account, and load bookmarks for the given book. */ fun bookmarkSyncAndLoad( accountID: AccountID, book: BookID - ): FluentFuture + ): CompletableFuture + + /** + * Sync the bookmarks for the given account. + */ + fun bookmarkSyncAccount( + accountID: AccountID + ): CompletableFuture> /** * The user wants their current bookmarks. @@ -41,9 +40,8 @@ interface BookmarkServiceUsableType { fun bookmarkLoad( accountID: AccountID, - book: BookID, - lastReadBookmarkServer: Bookmark? - ): FluentFuture + book: BookID + ): CompletableFuture /** * Create a local bookmark. @@ -51,8 +49,8 @@ interface BookmarkServiceUsableType { fun bookmarkCreateLocal( accountID: AccountID, - bookmark: Bookmark - ): FluentFuture + bookmark: SerializedBookmark + ): CompletableFuture /** * Create a remote bookmark. @@ -60,8 +58,8 @@ interface BookmarkServiceUsableType { fun bookmarkCreateRemote( accountID: AccountID, - bookmark: Bookmark - ): FluentFuture + bookmark: SerializedBookmark + ): CompletableFuture /** * Create a local bookmark, and then create a remote bookmark if necessary. @@ -69,9 +67,9 @@ interface BookmarkServiceUsableType { fun bookmarkCreate( accountID: AccountID, - bookmark: Bookmark, + bookmark: SerializedBookmark, ignoreRemoteFailures: Boolean - ): FluentFuture + ): CompletableFuture /** * The user has requested that a bookmark be deleted. @@ -79,7 +77,7 @@ interface BookmarkServiceUsableType { fun bookmarkDelete( accountID: AccountID, - bookmark: Bookmark, + bookmark: SerializedBookmark, ignoreRemoteFailures: Boolean - ): FluentFuture + ): CompletableFuture } diff --git a/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/Bookmarks.kt b/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/Bookmarks.kt deleted file mode 100644 index ef6d631c2..000000000 --- a/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/Bookmarks.kt +++ /dev/null @@ -1,18 +0,0 @@ -package org.nypl.simplified.bookmarks.api - -import org.nypl.simplified.books.api.bookmark.Bookmark -import java.io.Serializable - -/** - * A set of bookmarks. - * - *

Note: The type is {@link Serializable} purely because the Android API requires this - * in order pass values of this type between activities. We make absolutely no guarantees - * that serialized values of this class will be compatible with future releases.

- */ - -data class Bookmarks( - val lastReadLocal: Bookmark?, - val lastReadServer: Bookmark?, - val bookmarks: List -) : Serializable diff --git a/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarksForBook.kt b/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarksForBook.kt new file mode 100644 index 000000000..bf44ddf88 --- /dev/null +++ b/simplified-bookmarks-api/src/main/java/org/nypl/simplified/bookmarks/api/BookmarksForBook.kt @@ -0,0 +1,37 @@ +package org.nypl.simplified.bookmarks.api + +import org.nypl.simplified.books.api.BookID +import org.nypl.simplified.books.api.bookmark.BookmarkKind +import org.nypl.simplified.books.api.bookmark.SerializedBookmark +import java.io.Serializable + +/** + * A set of bookmarks for a specific book. + * + *

Note: The type is {@link Serializable} purely because the Android API requires this + * in order pass values of this type between activities. We make absolutely no guarantees + * that serialized values of this class will be compatible with future releases.

+ */ + +data class BookmarksForBook( + val bookId: BookID, + val lastRead: SerializedBookmark?, + val bookmarks: List +) : Serializable { + init { + check(this.bookmarks.all { bookmark -> bookmark.kind == BookmarkKind.BookmarkExplicit }) { + "All bookmarks must be explicit bookmarks." + } + check(this.bookmarks.all { bookmark -> bookmark.book == this.bookId }) { + "All bookmarks must be for book ${this.bookId}." + } + if (this.lastRead != null) { + check(this.lastRead.kind == BookmarkKind.BookmarkLastReadLocation) { + "Last-read bookmark must be of a last-read kind" + } + check(this.lastRead.book == this.bookId) { + "All bookmarks must be for book ${this.bookId}." + } + } + } +} diff --git a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BService.kt b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BService.kt index ccb03edbe..f6d63f8f4 100644 --- a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BService.kt +++ b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BService.kt @@ -1,11 +1,6 @@ package org.nypl.simplified.bookmarks.internal import com.fasterxml.jackson.databind.ObjectMapper -import com.google.common.util.concurrent.FluentFuture -import com.google.common.util.concurrent.Futures -import com.google.common.util.concurrent.ListenableFuture -import com.google.common.util.concurrent.ListeningScheduledExecutorService -import com.google.common.util.concurrent.MoreExecutors import io.reactivex.Observable import io.reactivex.disposables.CompositeDisposable import io.reactivex.subjects.Subject @@ -14,23 +9,25 @@ import org.nypl.simplified.accounts.api.AccountEventLoginStateChanged import org.nypl.simplified.accounts.api.AccountID import org.nypl.simplified.accounts.api.AccountLoginState.AccountLoggedIn import org.nypl.simplified.accounts.api.AccountLoginState.AccountLoggingIn -import org.nypl.simplified.accounts.api.AccountLoginState.AccountLoginFailed import org.nypl.simplified.accounts.api.AccountLoginState.AccountLoggingInWaitingForExternalAuthentication import org.nypl.simplified.accounts.api.AccountLoginState.AccountLoggingOut +import org.nypl.simplified.accounts.api.AccountLoginState.AccountLoginFailed import org.nypl.simplified.accounts.api.AccountLoginState.AccountLogoutFailed import org.nypl.simplified.accounts.api.AccountLoginState.AccountNotLoggedIn import org.nypl.simplified.bookmarks.api.BookmarkEvent import org.nypl.simplified.bookmarks.api.BookmarkHTTPCallsType -import org.nypl.simplified.bookmarks.api.Bookmarks import org.nypl.simplified.bookmarks.api.BookmarkServiceType +import org.nypl.simplified.bookmarks.api.BookmarksForBook import org.nypl.simplified.books.api.BookID -import org.nypl.simplified.books.api.bookmark.Bookmark +import org.nypl.simplified.books.api.bookmark.SerializedBookmark import org.nypl.simplified.profiles.api.ProfileEvent import org.nypl.simplified.profiles.api.ProfileNoneCurrentException import org.nypl.simplified.profiles.api.ProfileSelection import org.nypl.simplified.profiles.controller.api.ProfilesControllerType import org.slf4j.LoggerFactory +import java.util.concurrent.CompletableFuture import java.util.concurrent.Executors +import java.util.concurrent.ScheduledExecutorService import java.util.concurrent.TimeUnit class BService( @@ -43,12 +40,10 @@ class BService( private val disposables = CompositeDisposable() - private val executor: ListeningScheduledExecutorService = - MoreExecutors.listeningDecorator( - Executors.newScheduledThreadPool(1) { runnable -> - BServiceThread(this.threads.invoke(runnable)) - } - ) + private val executor: ScheduledExecutorService = + Executors.newScheduledThreadPool(1) { runnable -> + BServiceThread(this.threads.invoke(runnable)) + } private val logger = LoggerFactory.getLogger(BService::class.java) @@ -69,7 +64,12 @@ class BService( * Sync bookmarks hourly. */ - this.executor.scheduleAtFixedRate({ this.onRegularSyncTimeElapsed() }, 0L, 1L, TimeUnit.HOURS) + this.executor.scheduleAtFixedRate( + { this.onRegularSyncTimeElapsed() }, + 0L, + 1L, + TimeUnit.HOURS + ) } private fun onProfileEvent(event: ProfileEvent) { @@ -101,13 +101,27 @@ class BService( } } + private fun submitOp( + op: BServiceOp + ): CompletableFuture { + val f = CompletableFuture() + this.executor.execute { + try { + f.complete(op.runActual()) + } catch (e: Throwable) { + f.completeExceptionally(e) + } + } + return f + } + /** * Asynchronously send and receive all bookmarks. */ - private fun sync(): ListenableFuture<*> { + private fun sync(): CompletableFuture<*> { return try { - this.executor.submit( + this.submitOp( BServiceOpSyncAllAccounts( this.logger, this.httpCalls, @@ -116,12 +130,20 @@ class BService( this.profilesController.profileCurrent() ) ) - } catch (e: Exception) { + } catch (e: Throwable) { this.logger.error("sync: unable to sync profile: ", e) - FluentFuture.from(Futures.immediateFailedFuture(e)) + this.failedFuture(e) } } + private fun failedFuture( + e: Throwable + ): CompletableFuture { + val f = CompletableFuture() + f.completeExceptionally(e) + return f + } + private fun onAccountLoggedIn() { this.sync() } @@ -143,165 +165,137 @@ class BService( get() = this.bookmarkEventsOut override fun bookmarkSyncAccount( - accountID: AccountID, - bookID: BookID - ): FluentFuture { + accountID: AccountID + ): CompletableFuture> { return try { - FluentFuture.from( - this.executor.submit( - BServiceOpSyncOneAccount( - this.logger, - this.httpCalls, - this.bookmarkEventsOut, - this.objectMapper, - this.profilesController.profileCurrent(), - accountID, - bookID - ) + this.submitOp( + BServiceOpSyncOneAccount( + this.logger, + this.httpCalls, + this.bookmarkEventsOut, + this.objectMapper, + this.profilesController.profileCurrent(), + accountID ) ) - } catch (e: Exception) { + } catch (e: Throwable) { this.logger.error("sync: unable to sync account: ", e) - FluentFuture.from(Futures.immediateFailedFuture(e)) + this.failedFuture(e) } } override fun bookmarkSyncAndLoad( accountID: AccountID, book: BookID - ): FluentFuture { - return this.bookmarkSyncAccount(accountID, book) - .transformAsync( - { lastReadServer -> - this.bookmarkLoad(accountID, book, lastReadServer) - }, - this.executor - ) - .catchingAsync( - Exception::class.java, - { - // If sync fails, continue to load the local bookmarks. - this.bookmarkLoad(accountID, book, null) - }, - this.executor - ) + ): CompletableFuture { + return this.bookmarkSyncAccount(accountID) + .exceptionally { listOf() } + .thenCompose { this.bookmarkLoad(accountID, book) } } override fun bookmarkLoad( accountID: AccountID, - book: BookID, - lastReadBookmarkServer: Bookmark? - ): FluentFuture { + book: BookID + ): CompletableFuture { return try { - FluentFuture.from( - this.executor.submit( - BServiceOpLoadBookmarks( - logger = this.logger, - accountID = accountID, - profile = profilesController.profileCurrent(), - book = book, - lastReadBookmarkServer = lastReadBookmarkServer - ) + this.submitOp( + BServiceOpLoadBookmarks( + logger = this.logger, + accountID = accountID, + profile = this.profilesController.profileCurrent(), + book = book ) ) - } catch (e: ProfileNoneCurrentException) { - this.logger.error("bookmarkLoad: no profile is current: ", e) - FluentFuture.from(Futures.immediateFailedFuture(e)) + } catch (e: Throwable) { + this.logger.error("bookmarkLoad: ", e) + this.failedFuture(e) } } override fun bookmarkCreateLocal( accountID: AccountID, - bookmark: Bookmark - ): FluentFuture { + bookmark: SerializedBookmark + ): CompletableFuture { return try { - FluentFuture.from( - this.executor.submit( - BServiceOpCreateLocalBookmark( - logger = this.logger, - bookmarkEventsOut = this.bookmarkEventsOut, - profile = profilesController.profileCurrent(), - accountID = accountID, - bookmark = bookmark - ) + this.submitOp( + BServiceOpCreateLocalBookmark( + logger = this.logger, + bookmarkEventsOut = this.bookmarkEventsOut, + profile = this.profilesController.profileCurrent(), + accountID = accountID, + bookmark = bookmark ) ) - } catch (e: ProfileNoneCurrentException) { - this.logger.error("bookmarkCreateLocal: no profile is current: ", e) - FluentFuture.from(Futures.immediateFailedFuture(e)) + } catch (e: Throwable) { + this.logger.error("bookmarkCreateLocal: ", e) + this.failedFuture(e) } } override fun bookmarkCreateRemote( accountID: AccountID, - bookmark: Bookmark - ): FluentFuture { + bookmark: SerializedBookmark + ): CompletableFuture { return try { - FluentFuture.from( - this.executor.submit( - BServiceOpCreateRemoteBookmark( - logger = this.logger, - objectMapper = this.objectMapper, - httpCalls = this.httpCalls, - profile = profilesController.profileCurrent(), - accountID = accountID, - bookmark = bookmark - ) + this.submitOp( + BServiceOpCreateRemoteBookmark( + logger = this.logger, + objectMapper = this.objectMapper, + httpCalls = this.httpCalls, + profile = this.profilesController.profileCurrent(), + accountID = accountID, + bookmark = bookmark ) ) - } catch (e: ProfileNoneCurrentException) { - this.logger.error("bookmarkCreateRemote: no profile is current: ", e) - FluentFuture.from(Futures.immediateFailedFuture(e)) + } catch (e: Throwable) { + this.logger.error("bookmarkCreateRemote: ", e) + this.failedFuture(e) } } override fun bookmarkCreate( accountID: AccountID, - bookmark: Bookmark, + bookmark: SerializedBookmark, ignoreRemoteFailures: Boolean - ): FluentFuture { + ): CompletableFuture { return try { - FluentFuture.from( - this.executor.submit( - BServiceOpCreateBookmark( - logger = this.logger, - objectMapper = this.objectMapper, - httpCalls = this.httpCalls, - profile = profilesController.profileCurrent(), - accountID = accountID, - bookmarkEventsOut = this.bookmarkEventsOut, - bookmark = bookmark, - ignoreRemoteFailures = ignoreRemoteFailures - ) + this.submitOp( + BServiceOpCreateBookmark( + logger = this.logger, + objectMapper = this.objectMapper, + httpCalls = this.httpCalls, + profile = this.profilesController.profileCurrent(), + accountID = accountID, + bookmarkEventsOut = this.bookmarkEventsOut, + bookmark = bookmark, + ignoreRemoteFailures = ignoreRemoteFailures ) ) - } catch (e: ProfileNoneCurrentException) { - this.logger.error("bookmarkCreate: no profile is current: ", e) - FluentFuture.from(Futures.immediateFailedFuture(e)) + } catch (e: Throwable) { + this.logger.error("bookmarkCreate: ", e) + this.failedFuture(e) } } override fun bookmarkDelete( accountID: AccountID, - bookmark: Bookmark, + bookmark: SerializedBookmark, ignoreRemoteFailures: Boolean - ): FluentFuture { + ): CompletableFuture { return try { - FluentFuture.from( - this.executor.submit( - BServiceOpDeleteBookmark( - logger = this.logger, - httpCalls = this.httpCalls, - profile = profilesController.profileCurrent(), - accountID = accountID, - bookmark = bookmark, - ignoreRemoteFailures = ignoreRemoteFailures - ) + this.submitOp( + BServiceOpDeleteBookmark( + logger = this.logger, + httpCalls = this.httpCalls, + profile = this.profilesController.profileCurrent(), + accountID = accountID, + bookmark = bookmark, + ignoreRemoteFailures = ignoreRemoteFailures ) ) - } catch (e: ProfileNoneCurrentException) { - this.logger.error("bookmarkLoad: no profile is current: ", e) - FluentFuture.from(Futures.immediateFailedFuture(e)) + } catch (e: Throwable) { + this.logger.error("bookmarkLoad: ", e) + this.failedFuture(e) } } } diff --git a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceBookmarks.kt b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceBookmarks.kt deleted file mode 100644 index 4b0c8fcef..000000000 --- a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceBookmarks.kt +++ /dev/null @@ -1,89 +0,0 @@ -package org.nypl.simplified.bookmarks.internal - -import org.nypl.simplified.books.api.bookmark.Bookmark -import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle -import org.nypl.simplified.profiles.api.ProfileID -import org.slf4j.Logger - -internal object BServiceBookmarks { - - /** - * Normalize the set of bookmarks in the book database. This is necessary because bookmarks - * do not really have identities and we have to manually deduplicate them if we happen to - * notice that two bookmarks are the same. Bookmarks have a manually calculated "identity" - * represented by the [Bookmark.bookmarkId] property, and we can use this to effectively - * deduplicate bookmarks. - */ - - fun normalizeBookmarks( - logger: Logger, - profileId: ProfileID, - handle: BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleEPUB, - bookmark: Bookmark.ReaderBookmark - ): List { - val originalBookmarks = - handle.format.bookmarks - val bookmarksById = - originalBookmarks.associateBy { mark -> mark.bookmarkId } - .toMutableMap() - - bookmarksById[bookmark.bookmarkId] = bookmark - - logger.debug( - "[{}]: normalized {} -> {} bookmarks", - profileId.uuid, - originalBookmarks.size, - bookmarksById.size - ) - - return bookmarksById.values.toList() - } - - fun normalizeBookmarks( - logger: Logger, - profileId: ProfileID, - handle: BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleAudioBook, - bookmark: Bookmark.AudiobookBookmark - ): List { - val originalBookmarks = - handle.format.bookmarks - val bookmarksById = - originalBookmarks.associateBy { mark -> mark.bookmarkId } - .toMutableMap() - - bookmarksById[bookmark.bookmarkId] = bookmark - - logger.debug( - "[{}]: normalized {} -> {} bookmarks", - profileId.uuid, - originalBookmarks.size, - bookmarksById.size - ) - - return bookmarksById.values.toList() - } - - fun normalizeBookmarks( - logger: Logger, - profileId: ProfileID, - handle: BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandlePDF, - bookmark: Bookmark.PDFBookmark - ): List { - val originalBookmarks = - handle.format.bookmarks - val bookmarksById = - originalBookmarks.associateBy { mark -> mark.bookmarkId } - .toMutableMap() - - bookmarksById[bookmark.bookmarkId] = bookmark - - logger.debug( - "[{}]: normalized {} -> {} bookmarks", - profileId.uuid, - originalBookmarks.size, - bookmarksById.size - ) - - return bookmarksById.values.toList() - } -} diff --git a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpCreateBookmark.kt b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpCreateBookmark.kt index 240a844d2..43053159d 100644 --- a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpCreateBookmark.kt +++ b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpCreateBookmark.kt @@ -5,7 +5,7 @@ import io.reactivex.subjects.Subject import org.nypl.simplified.accounts.api.AccountID import org.nypl.simplified.bookmarks.api.BookmarkEvent import org.nypl.simplified.bookmarks.api.BookmarkHTTPCallsType -import org.nypl.simplified.books.api.bookmark.Bookmark +import org.nypl.simplified.books.api.bookmark.SerializedBookmark import org.nypl.simplified.profiles.api.ProfileReadableType import org.slf4j.Logger @@ -22,11 +22,11 @@ internal class BServiceOpCreateBookmark( private val httpCalls: BookmarkHTTPCallsType, private val profile: ProfileReadableType, private val accountID: AccountID, - private val bookmark: Bookmark, + private val bookmark: SerializedBookmark, private val ignoreRemoteFailures: Boolean -) : BServiceOp(logger) { +) : BServiceOp(logger) { - override fun runActual(): Bookmark { + override fun runActual(): SerializedBookmark { return try { this.createLocalBookmarkFrom(this.bookmark) @@ -50,7 +50,9 @@ internal class BServiceOpCreateBookmark( } } - private fun createLocalBookmarkFrom(bookmark: Bookmark): Bookmark { + private fun createLocalBookmarkFrom( + bookmark: SerializedBookmark + ): SerializedBookmark { return BServiceOpCreateLocalBookmark( this.logger, this.bookmarkEventsOut, diff --git a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpCreateLocalBookmark.kt b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpCreateLocalBookmark.kt index a229182ef..4c2ebe904 100644 --- a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpCreateLocalBookmark.kt +++ b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpCreateLocalBookmark.kt @@ -3,10 +3,7 @@ package org.nypl.simplified.bookmarks.internal import io.reactivex.subjects.Subject import org.nypl.simplified.accounts.api.AccountID import org.nypl.simplified.bookmarks.api.BookmarkEvent -import org.nypl.simplified.books.api.bookmark.Bookmark -import org.nypl.simplified.books.api.bookmark.BookmarkKind -import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle -import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleEPUB +import org.nypl.simplified.books.api.bookmark.SerializedBookmark import org.nypl.simplified.profiles.api.ProfileReadableType import org.slf4j.Logger @@ -19,14 +16,14 @@ internal class BServiceOpCreateLocalBookmark( private val bookmarkEventsOut: Subject, private val profile: ProfileReadableType, private val accountID: AccountID, - private val bookmark: Bookmark -) : BServiceOp(logger) { + private val bookmark: SerializedBookmark +) : BServiceOp(logger) { - override fun runActual(): Bookmark { + override fun runActual(): SerializedBookmark { return this.locallySaveBookmark() } - private fun locallySaveBookmark(): Bookmark { + private fun locallySaveBookmark(): SerializedBookmark { return try { this.logger.debug( "[{}]: locally saving bookmark {}", @@ -34,98 +31,26 @@ internal class BServiceOpCreateLocalBookmark( this.bookmark.bookmarkId.value ) - val account = this.profile.account(this.accountID) - val books = account.bookDatabase - val entry = books.entry(this.bookmark.book) + val account = + this.profile.account(this.accountID) + val books = + account.bookDatabase + val entry = + books.entry(this.bookmark.book) - when (bookmark) { - is Bookmark.ReaderBookmark -> { - val handle = - entry.findFormatHandle(BookDatabaseEntryFormatHandleEPUB::class.java) - ?: throw this.errorNoFormatHandle() - - when (this.bookmark.kind) { - BookmarkKind.BookmarkLastReadLocation -> - handle.setLastReadLocation(this.bookmark) - - BookmarkKind.BookmarkExplicit -> - handle.setBookmarks( - BServiceBookmarks.normalizeBookmarks( - logger = this.logger, - profileId = this.profile.id, - handle = handle, - bookmark = bookmark - ) - ) - } - - this.publishSavedEvent(this.bookmark) - } - - is Bookmark.PDFBookmark -> { - val handle = - entry.findFormatHandle(BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandlePDF::class.java) - ?: throw this.errorNoFormatHandle() - - when (this.bookmark.kind) { - BookmarkKind.BookmarkLastReadLocation -> - handle.setLastReadLocation(this.bookmark) - - BookmarkKind.BookmarkExplicit -> { - handle.setBookmarks( - BServiceBookmarks.normalizeBookmarks( - logger = this.logger, - profileId = this.profile.id, - handle = handle, - bookmark = bookmark - ) - ) - } - } - - this.publishSavedEvent(this.bookmark) - } - - is Bookmark.AudiobookBookmark -> { - val handle = - entry.findFormatHandle( - BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleAudioBook::class.java - ) ?: throw this.errorNoFormatHandle() - - val updatedBookmark = bookmark.copy( - location = bookmark.location.copy( - currentOffset = bookmark.location.startOffset + bookmark.location.currentOffset - ) - ) - - when (this.bookmark.kind) { - BookmarkKind.BookmarkLastReadLocation -> - handle.setLastReadLocation(updatedBookmark) - - BookmarkKind.BookmarkExplicit -> - handle.setBookmarks( - BServiceBookmarks.normalizeBookmarks( - logger = this.logger, - profileId = this.profile.id, - handle = handle, - bookmark = updatedBookmark - ) - ) - } - - this.publishSavedEvent(updatedBookmark) - } - - else -> - throw IllegalStateException("Unsupported bookmark type: $bookmark") + for (handle in entry.formatHandles) { + handle.addBookmark(this.bookmark) + this.publishSavedEvent(this.bookmark) } + + this.bookmark } catch (e: Exception) { this.logger.error("error saving bookmark locally: ", e) throw e } } - private fun publishSavedEvent(updatedBookmark: Bookmark): Bookmark { + private fun publishSavedEvent(updatedBookmark: SerializedBookmark): SerializedBookmark { this.bookmarkEventsOut.onNext(BookmarkEvent.BookmarkSaved(this.accountID, updatedBookmark)) return updatedBookmark } diff --git a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpCreateRemoteBookmark.kt b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpCreateRemoteBookmark.kt index 633b85b43..b07508f87 100644 --- a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpCreateRemoteBookmark.kt +++ b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpCreateRemoteBookmark.kt @@ -4,7 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import org.nypl.simplified.accounts.api.AccountID import org.nypl.simplified.bookmarks.api.BookmarkAnnotations import org.nypl.simplified.bookmarks.api.BookmarkHTTPCallsType -import org.nypl.simplified.books.api.bookmark.Bookmark +import org.nypl.simplified.books.api.bookmark.SerializedBookmark import org.nypl.simplified.profiles.api.ProfileReadableType import org.slf4j.Logger @@ -18,14 +18,14 @@ internal class BServiceOpCreateRemoteBookmark( private val httpCalls: BookmarkHTTPCallsType, private val profile: ProfileReadableType, private val accountID: AccountID, - private val bookmark: Bookmark -) : BServiceOp(logger) { + private val bookmark: SerializedBookmark +) : BServiceOp(logger) { - override fun runActual(): Bookmark { + override fun runActual(): SerializedBookmark { return this.remotelySendBookmark() } - private fun remotelySendBookmark(): Bookmark { + private fun remotelySendBookmark(): SerializedBookmark { return try { this.logger.debug( "[{}]: remote sending bookmark {}", @@ -44,48 +44,18 @@ internal class BServiceOpCreateRemoteBookmark( return this.bookmark } - val bookmarkAnnotation = when (this.bookmark) { - is Bookmark.ReaderBookmark -> { - BookmarkAnnotations.fromReaderBookmark(this.objectMapper, this.bookmark) - } - is Bookmark.AudiobookBookmark -> { - BookmarkAnnotations.fromAudiobookBookmark(this.objectMapper, this.bookmark) - } - is Bookmark.PDFBookmark -> { - BookmarkAnnotations.fromPdfBookmark(this.objectMapper, this.bookmark) - } - else -> { - throw IllegalStateException("Unsupported bookmark type: $bookmark") - } - } + val bookmarkAnnotation = + BookmarkAnnotations.fromSerializedBookmark(this.objectMapper, this.bookmark) - val bookmarkUri = this.httpCalls.bookmarkAdd( - account = account, - annotationsURI = syncInfo.annotationsURI, - credentials = syncInfo.credentials, - bookmark = bookmarkAnnotation - ) ?: throw IllegalStateException("Server HTTP call failed") + val bookmarkUri = + this.httpCalls.bookmarkAdd( + account = account, + annotationsURI = syncInfo.annotationsURI, + credentials = syncInfo.credentials, + bookmark = bookmarkAnnotation + ) ?: throw IllegalStateException("Server HTTP call failed") - when (this.bookmark) { - is Bookmark.ReaderBookmark -> { - bookmark.copy( - uri = bookmarkUri - ) - } - is Bookmark.AudiobookBookmark -> { - bookmark.copy( - uri = bookmarkUri - ) - } - is Bookmark.PDFBookmark -> { - bookmark.copy( - uri = bookmarkUri - ) - } - else -> { - throw IllegalStateException("Unsupported bookmark type: $bookmark") - } - } + return this.bookmark.withURI(bookmarkUri) } catch (e: Exception) { this.logger.error("error sending bookmark: ", e) throw e diff --git a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpDeleteBookmark.kt b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpDeleteBookmark.kt index ccf71800f..0199cba4f 100644 --- a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpDeleteBookmark.kt +++ b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpDeleteBookmark.kt @@ -2,9 +2,8 @@ package org.nypl.simplified.bookmarks.internal import org.nypl.simplified.accounts.api.AccountID import org.nypl.simplified.bookmarks.api.BookmarkHTTPCallsType -import org.nypl.simplified.books.api.bookmark.Bookmark import org.nypl.simplified.books.api.bookmark.BookmarkKind -import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle +import org.nypl.simplified.books.api.bookmark.SerializedBookmark import org.nypl.simplified.profiles.api.ProfileReadableType import org.slf4j.Logger @@ -17,7 +16,7 @@ internal class BServiceOpDeleteBookmark( private val httpCalls: BookmarkHTTPCallsType, private val profile: ProfileReadableType, private val accountID: AccountID, - private val bookmark: Bookmark, + private val bookmark: SerializedBookmark, private val ignoreRemoteFailures: Boolean ) : BServiceOp(logger) { @@ -92,58 +91,17 @@ internal class BServiceOpDeleteBookmark( val books = account.bookDatabase val entry = books.entry(this.bookmark.book) - when (this.bookmark) { - is Bookmark.ReaderBookmark -> { - val handle = - entry.findFormatHandle(BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleEPUB::class.java) - ?: throw this.errorNoFormatHandle() - - when (this.bookmark.kind) { - BookmarkKind.BookmarkLastReadLocation -> - handle.setLastReadLocation(null) - BookmarkKind.BookmarkExplicit -> - handle.setBookmarks(handle.format.bookmarks.minus(this.bookmark)) - } - } - is Bookmark.PDFBookmark -> { - val handle = - entry.findFormatHandle(BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandlePDF::class.java) - ?: throw this.errorNoFormatHandle() - - when (this.bookmark.kind) { - BookmarkKind.BookmarkLastReadLocation -> - handle.setLastReadLocation(null) - BookmarkKind.BookmarkExplicit -> { - handle.setBookmarks(handle.format.bookmarks.minus(this.bookmark)) - } - } + for (handle in entry.formatHandles) { + when (this.bookmark.kind) { + BookmarkKind.BookmarkLastReadLocation -> + handle.setLastReadLocation(null) + BookmarkKind.BookmarkExplicit -> + handle.deleteBookmark(this.bookmark.bookmarkId) } - is Bookmark.AudiobookBookmark -> { - val handle = - entry.findFormatHandle(BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleAudioBook::class.java) - ?: throw this.errorNoFormatHandle() - - when (this.bookmark.kind) { - BookmarkKind.BookmarkLastReadLocation -> - handle.setLastReadLocation(null) - BookmarkKind.BookmarkExplicit -> - handle.setBookmarks(handle.format.bookmarks.minus(this.bookmark)) - } - } - else -> - throw IllegalStateException("Unsupported bookmark type: ${this.bookmark}") } } catch (e: Exception) { - this.logger.error("[{}]: error deleting bookmark locally: ", this.profile.id.uuid, e) + this.logger.error("[{}]: Error deleting bookmark locally: ", this.profile.id.uuid, e) throw e } } - - private fun errorNoFormatHandle(): IllegalStateException { - this.logger.debug( - "[{}]: unable to delete bookmark; no format handle", - this.profile.id.uuid - ) - return IllegalStateException("No format handle") - } } diff --git a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpLoadBookmarks.kt b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpLoadBookmarks.kt index e9571e583..5997e5f5f 100644 --- a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpLoadBookmarks.kt +++ b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpLoadBookmarks.kt @@ -1,10 +1,10 @@ package org.nypl.simplified.bookmarks.internal import org.nypl.simplified.accounts.api.AccountID -import org.nypl.simplified.bookmarks.api.Bookmarks +import org.nypl.simplified.bookmarks.api.BookmarksForBook import org.nypl.simplified.books.api.BookFormat import org.nypl.simplified.books.api.BookID -import org.nypl.simplified.books.api.bookmark.Bookmark +import org.nypl.simplified.books.api.bookmark.SerializedBookmark import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleAudioBook import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleEPUB import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandlePDF @@ -19,11 +19,10 @@ internal class BServiceOpLoadBookmarks( logger: Logger, private val profile: ProfileReadableType, private val accountID: AccountID, - private val book: BookID, - private val lastReadBookmarkServer: Bookmark? -) : BServiceOp(logger) { + private val book: BookID +) : BServiceOp(logger) { - override fun runActual(): Bookmarks { + override fun runActual(): BookmarksForBook { try { this.logger.debug("[{}]: loading bookmarks for book {}", this.profile.id.uuid, this.book.brief()) @@ -35,8 +34,8 @@ internal class BServiceOpLoadBookmarks( ?: entry.findFormatHandle(BookDatabaseEntryFormatHandlePDF::class.java) if (handle != null) { - val bookmarks: List - val lastReadLocation: Bookmark? + val bookmarks: List + val lastReadLocation: SerializedBookmark? when (handle.format) { is BookFormat.BookFormatEPUB -> { @@ -66,9 +65,9 @@ internal class BServiceOpLoadBookmarks( bookmarks.size ) - return Bookmarks( - lastReadLocal = lastReadLocation, - lastReadServer = lastReadBookmarkServer, + return BookmarksForBook( + bookId = this.book, + lastRead = lastReadLocation, bookmarks = bookmarks ) } @@ -77,6 +76,6 @@ internal class BServiceOpLoadBookmarks( } this.logger.debug("[{}]: returning empty bookmarks", this.profile.id.uuid) - return Bookmarks(null, null, listOf()) + return BookmarksForBook(this.book, null, listOf()) } } diff --git a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpSyncAllAccounts.kt b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpSyncAllAccounts.kt index b3bd8e193..c5fdca6a7 100644 --- a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpSyncAllAccounts.kt +++ b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpSyncAllAccounts.kt @@ -29,8 +29,7 @@ internal class BServiceOpSyncAllAccounts( this.bookmarkEventsOut, this.objectMapper, this.profile, - account, - bookID = null + account ).runActual() } catch (e: Exception) { this.logger.debug("failed to sync account {}: ", account.uuid, e) diff --git a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpSyncOneAccount.kt b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpSyncOneAccount.kt index aad634a0f..657d02382 100644 --- a/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpSyncOneAccount.kt +++ b/simplified-bookmarks/src/main/java/org/nypl/simplified/bookmarks/internal/BServiceOpSyncOneAccount.kt @@ -7,11 +7,8 @@ import org.nypl.simplified.bookmarks.api.BookmarkAnnotation import org.nypl.simplified.bookmarks.api.BookmarkAnnotations import org.nypl.simplified.bookmarks.api.BookmarkEvent import org.nypl.simplified.bookmarks.api.BookmarkHTTPCallsType -import org.nypl.simplified.bookmarks.api.Bookmarks -import org.nypl.simplified.books.api.BookID -import org.nypl.simplified.books.api.bookmark.Bookmark import org.nypl.simplified.books.api.bookmark.BookmarkKind -import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle +import org.nypl.simplified.books.api.bookmark.SerializedBookmark import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleEPUB import org.nypl.simplified.profiles.api.ProfileReadableType import org.slf4j.Logger @@ -26,11 +23,10 @@ internal class BServiceOpSyncOneAccount( private val bookmarkEventsOut: Subject, private val objectMapper: ObjectMapper, private val profile: ProfileReadableType, - private val accountID: AccountID, - private val bookID: BookID? -) : BServiceOp(logger) { + private val accountID: AccountID +) : BServiceOp>(logger) { - override fun runActual(): Bookmark? { + override fun runActual(): List { this.logger.debug( "[{}]: syncing account {}", this.profile.id.uuid, @@ -42,30 +38,29 @@ internal class BServiceOpSyncOneAccount( if (syncable == null) { this.logger.error("[{}]: account no longer syncable", this.accountID.uuid) - return null + return listOf() } if (!syncable.account.preferences.bookmarkSyncingPermitted) { this.logger.debug("[{}]: syncing not permitted", this.accountID.uuid) - return null + return listOf() } - val received = this.readBookmarksFromServer(syncable, bookID) - this.sendBookmarksToServer(syncable, received.bookmarks) + val received = this.readBookmarksFromServer(syncable) + this.sendBookmarksToServer(syncable, received) this.bookmarkEventsOut.onNext(BookmarkEvent.BookmarkSyncFinished(syncable.account.id)) - - return received.lastReadServer + return received } private fun sendBookmarksToServer( syncable: BSyncableAccount, - received: List + received: List ) { val localExtras = this.determineExtraLocalBookmarks(received, syncable) this.logger.debug( - "[{}]: we have {} bookmarks the server did not have", + "[{}]: We have {} bookmarks the server did not have", this.accountID.uuid, localExtras.size ) @@ -73,25 +68,13 @@ internal class BServiceOpSyncOneAccount( for (bookmark in localExtras) { try { this.logger.debug( - "[{}]: sending bookmark {}", + "[{}]: Sending bookmark {}", this.accountID.uuid, bookmark.bookmarkId.value ) - val bookmarkAnnotation = when (bookmark) { - is Bookmark.ReaderBookmark -> { - BookmarkAnnotations.fromReaderBookmark(this.objectMapper, bookmark) - } - is Bookmark.AudiobookBookmark -> { - BookmarkAnnotations.fromAudiobookBookmark(this.objectMapper, bookmark) - } - is Bookmark.PDFBookmark -> { - BookmarkAnnotations.fromPdfBookmark(this.objectMapper, bookmark) - } - else -> { - throw IllegalStateException("Unsupported bookmark type: $bookmark") - } - } + val bookmarkAnnotation = + BookmarkAnnotations.fromSerializedBookmark(this.objectMapper, bookmark) this.httpCalls.bookmarkAdd( account = syncable.account, @@ -100,7 +83,7 @@ internal class BServiceOpSyncOneAccount( bookmark = bookmarkAnnotation ) } catch (e: Exception) { - this.logger.error("[{}]: error sending bookmark: ", this.accountID.uuid, e) + this.logger.error("[{}]: Error sending bookmark: ", this.accountID.uuid, e) } } } @@ -111,9 +94,9 @@ internal class BServiceOpSyncOneAccount( */ private fun determineExtraLocalBookmarks( - received: List, + received: List, syncable: BSyncableAccount - ): Set { + ): Set { return syncable.account.bookDatabase.books() .map { id -> syncable.account.bookDatabase.entry(id) } .mapNotNull { entry -> entry.findFormatHandle(BookDatabaseEntryFormatHandleEPUB::class.java) } @@ -124,22 +107,20 @@ internal class BServiceOpSyncOneAccount( } private fun readBookmarksFromServer( - syncable: BSyncableAccount, - bookID: BookID? - ): Bookmarks { + syncable: BSyncableAccount + ): List { this.bookmarkEventsOut.onNext(BookmarkEvent.BookmarkSyncStarted(syncable.account.id)) - val bookmarks: List = + val bookmarkAnnotations: List = try { - val annotations = this.httpCalls.bookmarksGet( + this.httpCalls.bookmarksGet( account = syncable.account, annotationsURI = syncable.annotationsURI, credentials = syncable.credentials ) - annotations.mapNotNull(this::parseBookmarkOrNull) } catch (e: Exception) { this.logger.error( - "[{}]: could not receive bookmarks for account {}: ", + "[{}]: Could not receive bookmarks for account {}: ", this.profile.id.uuid, syncable.account.id, e @@ -147,21 +128,28 @@ internal class BServiceOpSyncOneAccount( listOf() } - this.logger.debug("[{}]: received {} bookmarks", this.profile.id.uuid, bookmarks.size) + this.logger.debug( + "[{}]: Received {} bookmarks", + this.profile.id.uuid, + bookmarkAnnotations.size + ) - var serverLastReadBookmark: Bookmark? = null + val results = arrayListOf() - for (bookmark in bookmarks) { + for (bookmarkAnnotation in bookmarkAnnotations) { try { + val bookmark = + BookmarkAnnotations.toSerializedBookmark(this.objectMapper, bookmarkAnnotation) + this.logger.debug( - "[{}]: received bookmark {}", + "[{}]: Received bookmark {}", this.profile.id.uuid, bookmark.bookmarkId.value ) if (!syncable.account.bookDatabase.books().contains(bookmark.book)) { this.logger.debug( - "[{}]: we no longer have book {}", + "[{}]: We no longer have book {}", this.profile.id.uuid, bookmark.book.value() ) @@ -169,106 +157,17 @@ internal class BServiceOpSyncOneAccount( } val entry = syncable.account.bookDatabase.entry(bookmark.book) - - when (bookmark) { - is Bookmark.ReaderBookmark -> { - val handle = entry.findFormatHandle(BookDatabaseEntryFormatHandleEPUB::class.java) - if (handle != null) { - when (bookmark.kind) { - BookmarkKind.BookmarkLastReadLocation -> { - // check if it's the last read bookmark for the given book ID - if (bookID != null && bookmark.book == bookID) { - serverLastReadBookmark = bookmark - } else { - handle.setLastReadLocation(bookmark) - } - } - BookmarkKind.BookmarkExplicit -> { - handle.setBookmarks( - BServiceBookmarks.normalizeBookmarks( - logger = this.logger, - profileId = this.profile.id, - handle = handle, - bookmark = bookmark - ) - ) - } - } - - this.bookmarkEventsOut.onNext( - BookmarkEvent.BookmarkSaved( - syncable.account.id, - bookmark - ) - ) - } - } - is Bookmark.PDFBookmark -> { - val handle = entry.findFormatHandle( - BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandlePDF::class.java + for (handle in entry.formatHandles) { + handle.addBookmark(bookmark) + this.bookmarkEventsOut.onNext( + BookmarkEvent.BookmarkSaved( + syncable.account.id, + bookmark ) - if (handle != null) { - when (bookmark.kind) { - BookmarkKind.BookmarkLastReadLocation -> { - // check if it's the last read bookmark for the given book ID - if (bookID != null && bookmark.book == bookID) { - serverLastReadBookmark = bookmark - } else { - handle.setLastReadLocation(bookmark) - } - } - BookmarkKind.BookmarkExplicit -> { - handle.setBookmarks( - BServiceBookmarks.normalizeBookmarks( - logger = this.logger, - profileId = this.profile.id, - handle = handle, - bookmark = bookmark - ) - ) - } - } - - this.bookmarkEventsOut.onNext( - BookmarkEvent.BookmarkSaved( - syncable.account.id, - bookmark - ) - ) - } - } - is Bookmark.AudiobookBookmark -> { - val handle = - entry.findFormatHandle( - BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleAudioBook::class.java - ) - if (handle != null) { - when (bookmark.kind) { - BookmarkKind.BookmarkLastReadLocation -> { - handle.setLastReadLocation(bookmark) - } - BookmarkKind.BookmarkExplicit -> { - handle.setBookmarks( - BServiceBookmarks.normalizeBookmarks( - logger = this.logger, - profileId = this.profile.id, - handle = handle, - bookmark = bookmark - ) - ) - } - } - this.bookmarkEventsOut.onNext( - BookmarkEvent.BookmarkSaved( - syncable.account.id, - bookmark - ) - ) - } - } - else -> - throw IllegalStateException("Unsupported bookmark type: $bookmark") + ) } + + results.add(bookmark) } catch (e: Exception) { this.logger.error( "[{}]: could not store bookmark for account {}: ", @@ -279,35 +178,6 @@ internal class BServiceOpSyncOneAccount( } } - return Bookmarks( - lastReadLocal = null, - lastReadServer = serverLastReadBookmark, - bookmarks = bookmarks - ) - } - - private fun parseBookmarkOrNull( - annotation: BookmarkAnnotation - ): Bookmark? { - return try { - val bookmark = BookmarkAnnotations.toAudiobookBookmark(this.objectMapper, annotation) - this.logger.debug("Audiobook bookmark successfully parsed") - bookmark - } catch (e: Exception) { - try { - val bookmark = BookmarkAnnotations.toReaderBookmark(this.objectMapper, annotation) - this.logger.debug("Reader bookmark successfully parsed") - bookmark - } catch (e: Exception) { - try { - val bookmark = BookmarkAnnotations.toPdfBookmark(this.objectMapper, annotation) - this.logger.debug("PDF bookmark successfully parsed") - bookmark - } catch (e: Exception) { - this.logger.error("unable to parse bookmark: ", e) - null - } - } - } + return results.toList() } } diff --git a/simplified-books-api/build.gradle.kts b/simplified-books-api/build.gradle.kts index f20d9a10d..4bb204683 100644 --- a/simplified-books-api/build.gradle.kts +++ b/simplified-books-api/build.gradle.kts @@ -15,6 +15,7 @@ dependencies { implementation(libs.kotlin.reflect) implementation(libs.kotlin.stdlib) implementation(libs.palace.audiobook.api) + implementation(libs.palace.audiobook.manifest.api) implementation(libs.palace.drm.core) implementation(libs.palace.readium2.api) implementation(libs.r2.shared) diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookChapterProgress.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookChapterProgress.kt deleted file mode 100644 index 96dec87a0..000000000 --- a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookChapterProgress.kt +++ /dev/null @@ -1,22 +0,0 @@ -package org.nypl.simplified.books.api - -import java.io.Serializable - -/** - * Progress through a specific chapter. - */ - -data class BookChapterProgress( - - /** - * The href of the chapter. - */ - - val chapterHref: String, - - /** - * The progress through the chapter. - */ - - val chapterProgress: Double -) : Serializable diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookContentProtections.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookContentProtections.kt index 5138b0e1f..44cc400ea 100644 --- a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookContentProtections.kt +++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookContentProtections.kt @@ -1,10 +1,10 @@ package org.nypl.simplified.books.api import android.app.Application +import org.nypl.drm.core.ContentProtectionProvider import org.nypl.simplified.lcp.LCPContentProtectionProvider import org.readium.r2.shared.publication.protection.ContentProtection import org.slf4j.LoggerFactory -import org.nypl.drm.core.ContentProtectionProvider object BookContentProtections { diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookFormat.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookFormat.kt index 25eae2d1f..b94a851f0 100644 --- a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookFormat.kt +++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookFormat.kt @@ -1,7 +1,7 @@ package org.nypl.simplified.books.api import one.irradia.mime.api.MIMEType -import org.nypl.simplified.books.api.bookmark.Bookmark +import org.nypl.simplified.books.api.bookmark.SerializedBookmark import java.io.File import java.net.URI @@ -30,6 +30,18 @@ sealed class BookFormat { abstract val isDownloaded: Boolean + /** + * The last read location of the book, if any. + */ + + abstract val lastReadLocation: SerializedBookmark? + + /** + * The list of bookmarks. + */ + + abstract val bookmarks: List + /** * An EPUB format. */ @@ -47,13 +59,13 @@ sealed class BookFormat { * The last read location of the book, if any. */ - val lastReadLocation: Bookmark.ReaderBookmark?, + override val lastReadLocation: SerializedBookmark?, /** * The list of bookmarks. */ - val bookmarks: List, + override val bookmarks: List, override val contentType: MIMEType ) : BookFormat() { @@ -105,12 +117,12 @@ sealed class BookFormat { /** * The last read location of the audiobook, if any. */ - val lastReadLocation: Bookmark.AudiobookBookmark?, + override val lastReadLocation: SerializedBookmark?, /** * The list of bookmarks. */ - val bookmarks: List, + override val bookmarks: List, override val contentType: MIMEType ) : BookFormat() { @@ -134,12 +146,13 @@ sealed class BookFormat { * The last read location of the PDF book, if any. */ - val lastReadLocation: Bookmark.PDFBookmark?, + override val lastReadLocation: SerializedBookmark?, /** * The list of bookmarks. */ - val bookmarks: List, + + override val bookmarks: List, /** * The PDF file on disk, if one has been downloaded. diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookLocation.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookLocation.kt deleted file mode 100644 index 4a5a98d36..000000000 --- a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/BookLocation.kt +++ /dev/null @@ -1,50 +0,0 @@ -package org.nypl.simplified.books.api - -import java.io.Serializable - -/** - * The current page. A specific location in an EPUB is identified by a chapter index, or an - * *idref* and a *content CFI*. In some cases, the *content CFI* - * may not be present. - * - * Note: The type is [Serializable] purely because the Android API requires this - * in order pass values of this type between activities. We make absolutely no guarantees - * that serialized values of this class will be compatible with future releases. - */ - -sealed class BookLocation : Serializable { - - /** - * R2-specific book locations. - */ - - data class BookLocationR2( - val progress: BookChapterProgress - ) : BookLocation() - - /** - * R1-specific book locations. - */ - - @Deprecated("Use R2") - data class BookLocationR1( - - /** - * The chapter progress. - */ - - val progress: Double?, - - /** - * @return The content CFI, if any - */ - - val contentCFI: String?, - - /** - * @return The IDRef - */ - - val idRef: String? - ) : BookLocation() -} diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/Bookmark.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/Bookmark.kt deleted file mode 100644 index ae7e375b8..000000000 --- a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/Bookmark.kt +++ /dev/null @@ -1,395 +0,0 @@ -package org.nypl.simplified.books.api.bookmark - -import org.joda.time.DateTime -import org.joda.time.DateTimeZone -import org.librarysimplified.audiobook.api.PlayerPosition -import org.nypl.simplified.books.api.BookID -import org.nypl.simplified.books.api.BookIDs -import org.nypl.simplified.books.api.BookLocation -import java.io.Serializable -import java.net.URI -import java.nio.charset.Charset -import java.security.MessageDigest -import java.security.NoSuchAlgorithmException - -/** - * The saved data for a bookmark. - */ - -sealed class Bookmark { - - /** - * @return The identifier of the book taken from the OPDS entry that provided it. - */ - - abstract val opdsId: String - - /** - * @return The kind of bookmark. - */ - - abstract val kind: BookmarkKind - - /** - * @return The time the bookmark was created. - */ - - abstract val time: DateTime - - /** - * @return The identifier of the device that created the bookmark, if one is available. - */ - - abstract val deviceID: String - - /** - * @return The URI of this bookmark, if the bookmark exists on a remote server. - */ - - abstract val uri: URI? - - /** - * The ID of the book to which the bookmark belongs. - */ - - abstract val book: BookID - - /** - * The unique ID of the bookmark. - */ - - abstract val bookmarkId: BookmarkID - - /** - * Convenience function to convert a bookmark to a last-read-location kind. - */ - abstract fun toLastReadLocation(): Bookmark - - /** - * Convenience function to convert a bookmark to an explicit kind. - */ - - abstract fun toExplicit(): Bookmark - - /** - * Class for bookmarks of reader type. - * - *

Note: The type is {@link Serializable} purely because the Android API requires this - * in order pass values of this type between activities. We make absolutely no guarantees - * that serialized values of this class will be compatible with future releases.

- */ - - data class ReaderBookmark( - override val opdsId: String, - override val time: DateTime, - override val deviceID: String, - override val kind: BookmarkKind, - override val uri: URI?, - - /** - * The title of the chapter. - */ - - val chapterTitle: String, - - /** - * The location of the bookmark. - */ - - val location: BookLocation, - - /** - * An estimate of the current book progress, in the range [0, 1] - */ - - @Deprecated("Use progress information from the BookLocation") - val bookProgress: Double? - - ) : Bookmark(), Serializable { - - override val book: BookID = BookIDs.newFromText(this.opdsId) - - override val bookmarkId: BookmarkID = createBookmarkID(this.book, this.location, this.kind) - - override fun toLastReadLocation(): Bookmark { - return this.copy(kind = BookmarkKind.BookmarkLastReadLocation) - } - - override fun toExplicit(): Bookmark { - return this.copy(kind = BookmarkKind.BookmarkExplicit) - } - - init { - check(this.time.zone == DateTimeZone.UTC) { - "Bookmark time zones must be UTC" - } - } - - /** - * An estimate of the current chapter progress, in the range [0, 1] - */ - - val chapterProgress: Double = - when (this.location) { - is BookLocation.BookLocationR2 -> this.location.progress.chapterProgress - is BookLocation.BookLocationR1 -> this.location.progress ?: 0.0 - } - - private fun createBookmarkID( - book: BookID, - location: BookLocation, - kind: BookmarkKind - ): BookmarkID { - try { - val messageDigest = MessageDigest.getInstance("SHA-256") - val utf8 = Charset.forName("UTF-8") - messageDigest.update(book.value().toByteArray(utf8)) - - when (location) { - is BookLocation.BookLocationR2 -> { - val chapterProgress = location.progress - messageDigest.update(chapterProgress.chapterHref.toByteArray(utf8)) - val truncatedProgress = String.format("%.6f", chapterProgress.chapterProgress) - messageDigest.update(truncatedProgress.toByteArray(utf8)) - } - is BookLocation.BookLocationR1 -> { - val cfi = location.contentCFI - if (cfi != null) { - messageDigest.update(cfi.toByteArray(utf8)) - } - val idRef = location.idRef - if (idRef != null) { - messageDigest.update(idRef.toByteArray(utf8)) - } - } - } - - messageDigest.update(kind.motivationURI.toByteArray(utf8)) - - val digestResult = messageDigest.digest() - val builder = StringBuilder(64) - for (index in digestResult.indices) { - val bb = digestResult[index] - builder.append(String.format("%02x", bb)) - } - - return BookmarkID(builder.toString()) - } catch (e: NoSuchAlgorithmException) { - throw IllegalStateException(e) - } - } - - companion object { - - fun create( - opdsId: String, - location: BookLocation, - kind: BookmarkKind, - time: DateTime, - chapterTitle: String, - bookProgress: Double?, - deviceID: String, - uri: URI? - ): ReaderBookmark { - return ReaderBookmark( - opdsId = opdsId, - location = location, - kind = kind, - time = time.toDateTime(DateTimeZone.UTC), - chapterTitle = chapterTitle, - bookProgress = bookProgress, - deviceID = deviceID, - uri = uri - ) - } - } - } - - /** - * Class for bookmarks of PDF type. - * - *

Note: The type is {@link Serializable} purely because the Android API requires this - * in order pass values of this type between activities. We make absolutely no guarantees - * that serialized values of this class will be compatible with future releases.

- */ - - data class PDFBookmark( - override val opdsId: String, - override val time: DateTime, - override val deviceID: String, - override val kind: BookmarkKind, - override val uri: URI?, - val pageNumber: Int - ) : Bookmark(), Serializable { - - override val book: BookID = BookIDs.newFromText(this.opdsId) - - override val bookmarkId: BookmarkID = createBookmarkID(this.book, this.kind, this.pageNumber) - - override fun toLastReadLocation(): Bookmark { - return this.copy(kind = BookmarkKind.BookmarkLastReadLocation) - } - - override fun toExplicit(): Bookmark { - return this.copy(kind = BookmarkKind.BookmarkExplicit) - } - - init { - check(this.time.zone == DateTimeZone.UTC) { - "Bookmark time zones must be UTC" - } - } - - /** - * Create a bookmark ID from the given book ID, kind and page number. - */ - - private fun createBookmarkID( - book: BookID, - kind: BookmarkKind, - pageNumber: Int - ): BookmarkID { - try { - val messageDigest = MessageDigest.getInstance("SHA-256") - val utf8 = Charset.forName("UTF-8") - messageDigest.update(book.value().toByteArray(utf8)) - messageDigest.update(kind.motivationURI.toByteArray(utf8)) - messageDigest.update(pageNumber.toString().toByteArray(utf8)) - - val digestResult = messageDigest.digest() - val builder = StringBuilder(64) - for (index in digestResult.indices) { - val bb = digestResult[index] - builder.append(String.format("%02x", bb)) - } - - return BookmarkID(builder.toString()) - } catch (e: NoSuchAlgorithmException) { - throw IllegalStateException(e) - } - } - - companion object { - - fun create( - opdsId: String, - kind: BookmarkKind, - time: DateTime, - pageNumber: Int, - deviceID: String, - uri: URI? - ): PDFBookmark { - return PDFBookmark( - opdsId = opdsId, - pageNumber = pageNumber, - kind = kind, - time = time.toDateTime(DateTimeZone.UTC), - deviceID = deviceID, - uri = uri - ) - } - } - } - - /** - * Class for bookmarks of audiobook type. - * - *

Note: The type is {@link Serializable} purely because the Android API requires this - * in order pass values of this type between activities. We make absolutely no guarantees - * that serialized values of this class will be compatible with future releases.

- */ - - data class AudiobookBookmark( - override val opdsId: String, - override val time: DateTime, - override val deviceID: String, - override val kind: BookmarkKind, - override val uri: URI?, - - /** - * The location of the bookmark. - */ - - val location: PlayerPosition, - - /** - * The duration of the bookmark's chapter. - */ - val duration: Long - - ) : Bookmark() { - - override val book: BookID = BookIDs.newFromText(this.opdsId) - - override val bookmarkId: BookmarkID = createBookmarkID(this.book, this.kind, this.location) - - override fun toLastReadLocation(): Bookmark { - return this.copy(kind = BookmarkKind.BookmarkLastReadLocation) - } - - override fun toExplicit(): Bookmark { - return this.copy(kind = BookmarkKind.BookmarkExplicit) - } - - init { - check(this.time.zone == DateTimeZone.UTC) { - "Bookmark time zones must be UTC" - } - } - - /** - * Create a bookmark ID from the given book ID, kind and location. - */ - - private fun createBookmarkID( - book: BookID, - kind: BookmarkKind, - location: PlayerPosition - ): BookmarkID { - try { - val messageDigest = MessageDigest.getInstance("SHA-256") - val utf8 = Charset.forName("UTF-8") - messageDigest.update(book.value().toByteArray(utf8)) - messageDigest.update(kind.motivationURI.toByteArray(utf8)) - messageDigest.update(location.chapter.toString().toByteArray(utf8)) - messageDigest.update(location.part.toString().toByteArray(utf8)) - messageDigest.update(location.startOffset.toString().toByteArray(utf8)) - messageDigest.update(location.currentOffset.toString().toByteArray(utf8)) - - val digestResult = messageDigest.digest() - val builder = StringBuilder(64) - for (index in digestResult.indices) { - val bb = digestResult[index] - builder.append(String.format("%02x", bb)) - } - - return BookmarkID(builder.toString()) - } catch (e: NoSuchAlgorithmException) { - throw IllegalStateException(e) - } - } - - companion object { - - fun create( - opdsId: String, - location: PlayerPosition, - kind: BookmarkKind, - time: DateTime, - deviceID: String, - duration: Long, - uri: URI? - ): AudiobookBookmark { - return AudiobookBookmark( - opdsId = opdsId, - location = location, - kind = kind, - time = time.toDateTime(DateTimeZone.UTC), - deviceID = deviceID, - duration = duration, - uri = uri - ) - } - } - } -} diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkDigests.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkDigests.kt new file mode 100644 index 000000000..cdbb03517 --- /dev/null +++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkDigests.kt @@ -0,0 +1,27 @@ +package org.nypl.simplified.books.api.bookmark + +import java.nio.charset.StandardCharsets.UTF_8 +import java.security.MessageDigest + +object BookmarkDigests { + fun addToDigest( + digest: MessageDigest, + text: String + ) { + digest.update(text.toByteArray(UTF_8)) + } + + fun addToDigest( + digest: MessageDigest, + number: Int + ) { + digest.update(number.toString().toByteArray(UTF_8)) + } + + fun addToDigest( + digest: MessageDigest, + number: Double + ) { + addToDigest(digest, String.format("%.6f", number)) + } +} diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkID.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkID.kt index 1910c086f..d96205083 100644 --- a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkID.kt +++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkID.kt @@ -1,6 +1,5 @@ package org.nypl.simplified.books.api.bookmark -import java.io.Serializable import java.util.regex.Pattern /** @@ -11,7 +10,9 @@ import java.util.regex.Pattern * that serialized values of this class will be compatible with future releases.

*/ -data class BookmarkID(val value: String) : Serializable { +data class BookmarkID( + val value: String +) { init { if (!VALID_BOOKMARK_ID.matcher(value).matches()) { diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkJSON.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkJSON.kt deleted file mode 100644 index 8eb0080d2..000000000 --- a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkJSON.kt +++ /dev/null @@ -1,669 +0,0 @@ -package org.nypl.simplified.books.api.bookmark - -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.node.ArrayNode -import com.fasterxml.jackson.databind.node.ObjectNode -import com.io7m.jfunctional.OptionType -import com.io7m.jfunctional.Some -import org.joda.time.DateTime -import org.joda.time.DateTimeZone -import org.joda.time.format.ISODateTimeFormat -import org.librarysimplified.audiobook.api.PlayerPosition -import org.librarysimplified.audiobook.api.PlayerPositions -import org.librarysimplified.audiobook.api.PlayerResult -import org.nypl.simplified.books.api.BookLocation -import org.nypl.simplified.books.api.helper.ReaderLocationJSON -import org.nypl.simplified.json.core.JSONParseException -import org.nypl.simplified.json.core.JSONParserUtilities -import org.nypl.simplified.json.core.JSONSerializerUtilities -import org.nypl.simplified.opds.core.getOrNull -import java.io.ByteArrayOutputStream -import java.io.IOException - -/** - * Functions to serialize bookmarks to/from JSON. - */ - -object BookmarkJSON { - - private val dateFormatter = - ISODateTimeFormat.dateTime() - .withZoneUTC() - - private val dateParserWithTimezone = - ISODateTimeFormat.dateTimeParser() - .withOffsetParsed() - - private val dateParserWithUTC = - ISODateTimeFormat.dateTimeParser() - .withZoneUTC() - - /** - * Deserialize bookmarks from the given JSON node. - * - * @param kind The kind of bookmark - * @param node A JSON node - * @return A reader bookmark - * @throws JSONParseException On parse errors - */ - - @JvmStatic - @Throws(JSONParseException::class) - fun deserializeReaderBookmarkFromJSON( - kind: BookmarkKind, - node: JsonNode - ): Bookmark.ReaderBookmark { - return deserializeReaderBookmarkFromJSON( - kind = kind, - node = JSONParserUtilities.checkObject(null, node) - ) - } - - /** - * Deserialize bookmarks from the given JSON node. - * - * @param kind The kind of bookmark - * @param node A JSON node - * @return A pdf bookmark - * @throws JSONParseException On parse errors - */ - - @JvmStatic - @Throws(JSONParseException::class) - fun deserializePdfBookmarkFromJSON( - kind: BookmarkKind, - node: JsonNode - ): Bookmark.PDFBookmark { - return deserializePdfBookmarkFromJSON( - kind = kind, - node = JSONParserUtilities.checkObject(null, node) - ) - } - - /** - * Deserialize bookmarks from the given JSON node. - * - * @param kind The kind of bookmark - * @param node A JSON node - * @return A reader bookmark - * @throws JSONParseException On parse errors - */ - - @JvmStatic - @Throws(JSONParseException::class) - fun deserializeReaderBookmarkFromJSON( - kind: BookmarkKind, - node: ObjectNode - ): Bookmark.ReaderBookmark { - return when (val version = JSONParserUtilities.getIntegerOrNull(node, "@version")) { - 20210828 -> - deserializeReaderBookmarkFromJSON20210828(kind, node) - 20210317 -> - deserializeReaderBookmarkFromJSON20210828(kind, node) - null -> - deserializeReaderBookmarkFromJSONOld(kind, node) - else -> - throw JSONParseException("Unsupported bookmark version: $version") - } - } - - /** - * Deserialize bookmarks from the given JSON node. - * - * @param kind The kind of bookmark - * @param node A JSON node - * @return A pdf bookmark - * @throws JSONParseException On parse errors - */ - - @JvmStatic - @Throws(JSONParseException::class) - fun deserializePdfBookmarkFromJSON( - kind: BookmarkKind, - node: ObjectNode - ): Bookmark.PDFBookmark { - return when (val version = JSONParserUtilities.getIntegerOrNull(node, "@version")) { - 1 -> - deserializePdfBookmarkFromJSONVersion1(kind, node) - else -> - throw JSONParseException("Unsupported bookmark version: $version") - } - } - - /** - * Deserialize bookmarks from the given JSON node. - * - * @param objectMapper A JSON object mapper - * @param kind The kind of bookmark - * @param node A JSON node - * @return An audiobook bookmark - * @throws JSONParseException On parse errors - */ - - @JvmStatic - @Throws(JSONParseException::class) - fun deserializeFromJSON( - objectMapper: ObjectMapper, - kind: BookmarkKind, - node: JsonNode - ): Bookmark.AudiobookBookmark { - return deserializeFromJSON( - objectMapper = objectMapper, - kind = kind, - node = JSONParserUtilities.checkObject(null, node) - ) - } - - /** - * Deserialize bookmarks from the given JSON node. - * - * @param kind The kind of bookmark - * @param node A JSON node - * @return An audiobook bookmark - * @throws JSONParseException On parse errors - */ - - @JvmStatic - @Throws(JSONParseException::class) - fun deserializeAudiobookBookmarkFromJSON( - kind: BookmarkKind, - node: ObjectNode - ): Bookmark.AudiobookBookmark { - val locationResult = PlayerPositions.parseFromObjectNode(node) - val location: PlayerPosition - - when (locationResult) { - is PlayerResult.Success -> { - location = locationResult.result - } - is PlayerResult.Failure -> throw locationResult.failure - } - - val parsedTime = parseTime( - JSONParserUtilities.getStringDefault(node, "time", dateFormatter.print(DateTime.now())) - ) - - return Bookmark.AudiobookBookmark.create( - opdsId = JSONParserUtilities.getStringDefault(node, "opdsId", ""), - kind = kind, - location = location, - duration = 0L, - time = parsedTime, - uri = toNullable(JSONParserUtilities.getURIOptional(node, "uri")), - deviceID = JSONParserUtilities.getStringDefault(node, "deviceID", "") - ) - } - - private fun deserializePdfBookmarkFromJSONVersion1( - kind: BookmarkKind, - node: ObjectNode - ): Bookmark.PDFBookmark { - val locationObj = JSONParserUtilities.getObject(node, "location") - val pageIndex = - JSONParserUtilities.getIntegerOrNull(locationObj, "page") ?: 1 - - val timeParsed = - parseTime(JSONParserUtilities.getString(node, "time")) - - return Bookmark.PDFBookmark.create( - opdsId = JSONParserUtilities.getString(node, "opdsId"), - kind = kind, - pageNumber = pageIndex, - time = timeParsed, - uri = toNullable(JSONParserUtilities.getURIOptional(node, "uri")), - deviceID = JSONParserUtilities.getStringDefault(node, "deviceID", null) - ) - } - - private fun deserializeReaderBookmarkFromJSON20210828( - kind: BookmarkKind, - node: ObjectNode - ): Bookmark.ReaderBookmark { - val location = - ReaderLocationJSON.deserializeFromJSON( - JSONParserUtilities.getObject(node, "location") - ) - - val timeParsed = - parseTime(JSONParserUtilities.getString(node, "time")) - - return Bookmark.ReaderBookmark.create( - opdsId = JSONParserUtilities.getString(node, "opdsId"), - kind = kind, - location = location, - time = timeParsed, - chapterTitle = JSONParserUtilities.getString(node, "chapterTitle"), - bookProgress = JSONParserUtilities.getDoubleDefault(node, "bookProgress", 0.0), - uri = toNullable(JSONParserUtilities.getURIOptional(node, "uri")), - deviceID = JSONParserUtilities.getStringDefault(node, "deviceID", null) - ) - } - - private fun deserializeReaderBookmarkFromJSONOld( - kind: BookmarkKind, - node: ObjectNode - ): Bookmark.ReaderBookmark { - val location = - ReaderLocationJSON.deserializeFromJSON( - JSONParserUtilities.getObject(node, "location") - ) - - /* - * Old bookmarks have a top-level chapterProgress value. We've moved to having this - * stored explicitly in book locations for modern bookmarks. We pick whichever is - * the greater of the two possible values, because we default to 0.0 for missing - * values. - */ - - val chapterProgress = - JSONParserUtilities.getDoubleDefault(node, "chapterProgress", 0.0) - - val locationMax = - when (location) { - is BookLocation.BookLocationR2 -> - location - is BookLocation.BookLocationR1 -> - location.copy(progress = Math.max(location.progress ?: 0.0, chapterProgress)) - } - - return Bookmark.ReaderBookmark( - opdsId = JSONParserUtilities.getString(node, "opdsId"), - kind = kind, - location = locationMax, - time = parseTime(JSONParserUtilities.getString(node, "time")), - chapterTitle = JSONParserUtilities.getString(node, "chapterTitle"), - bookProgress = JSONParserUtilities.getDoubleOptional(node, "bookProgress").getOrNull(), - uri = toNullable(JSONParserUtilities.getURIOptional(node, "uri")), - deviceID = JSONParserUtilities.getStringDefault(node, "deviceID", null) - ) - } - - /** - * Correctly parse a date/time value. - * - * This slightly odd function first attempts to parse the incoming string as if it was - * a date/time string with an included time zone. If the time string turned out not to - * include a time zone, Joda Time will parse it using the system's default timezone. We - * then detect that this has happened and, if the current system's timezone isn't UTC, - * we parse the string *again* but this time assuming a UTC timezone. - */ - - private fun parseTime( - timeText: String - ): DateTime { - val defaultZone = DateTimeZone.getDefault() - val timeParsedWithZone = dateParserWithTimezone.parseDateTime(timeText) - if (timeParsedWithZone.zone == defaultZone && defaultZone != DateTimeZone.UTC) { - return dateParserWithUTC.parseDateTime(timeText) - } - return timeParsedWithZone.toDateTime(DateTimeZone.UTC) - } - - private fun toNullable(option: OptionType): T? { - return if (option is Some) { - option.get() - } else { - null - } - } - - /** - * Serialize a bookmark to JSON. - * - * @param objectMapper A JSON object mapper - * @param bookmark A reader bookmark - * @return A serialized object - */ - - @JvmStatic - fun serializeReaderBookmarkToJSON( - objectMapper: ObjectMapper, - bookmark: Bookmark.ReaderBookmark - ): ObjectNode { - val node = objectMapper.createObjectNode() - node.put("@version", 20210828) - node.put("opdsId", bookmark.opdsId) - val location = ReaderLocationJSON.serializeToJSON(objectMapper, bookmark.location) - node.set("location", location) - node.put("time", dateFormatter.print(bookmark.time)) - node.put("chapterTitle", bookmark.chapterTitle) - bookmark.bookProgress?.let { node.put("bookProgress", it) } - bookmark.deviceID.let { device -> node.put("deviceID", device) } - return node - } - - /** - * Serialize a bookmark to JSON. - * - * @param objectMapper A JSON object mapper - * @param bookmark A pdf bookmark - * @return A serialized object - */ - - @JvmStatic - fun serializePdfBookmarkToJSON( - objectMapper: ObjectMapper, - bookmark: Bookmark.PDFBookmark - ): ObjectNode { - val node = objectMapper.createObjectNode() - node.put("@version", 1) - node.put("opdsId", bookmark.opdsId) - - val locationObject = objectMapper.createObjectNode() - locationObject.put("page", bookmark.pageNumber) - node.set("location", locationObject) - node.put("time", dateFormatter.print(bookmark.time)) - bookmark.deviceID.let { device -> node.put("deviceID", device) } - return node - } - - /** - * Serialize a bookmark to JSON. - * - * @param objectMapper A JSON object mapper - * @param bookmarks A list of reader bookmarks - * @return A serialized object - */ - - @JvmStatic - fun serializeReaderBookmarksToJSON( - objectMapper: ObjectMapper, - bookmarks: List - ): ArrayNode { - val node = objectMapper.createArrayNode() - bookmarks.forEach { bookmark -> - node.add( - serializeReaderBookmarkToJSON( - objectMapper, - bookmark - ) - ) - } - return node - } - - /** - * Serialize a bookmark to JSON. - * - * @param objectMapper A JSON object mapper - * @param bookmarks A list of pdf bookmarks - * @return A serialized object - */ - - @JvmStatic - fun serializePdfBookmarksToJSON( - objectMapper: ObjectMapper, - bookmarks: List - ): ArrayNode { - val node = objectMapper.createArrayNode() - bookmarks.forEach { bookmark -> - node.add( - serializePdfBookmarkToJSON( - objectMapper, - bookmark - ) - ) - } - return node - } - - /** - * Serialize a bookmark to a JSON string. - * - * @param objectMapper A JSON object mapper - * @param bookmark A reader bookmark - * @return A JSON string - * @throws IOException On serialization errors - */ - - @JvmStatic - @Throws(IOException::class) - fun serializeReaderBookmarkToString( - objectMapper: ObjectMapper, - bookmark: Bookmark.ReaderBookmark - ): String { - val json = serializeReaderBookmarkToJSON(objectMapper, bookmark) - val output = ByteArrayOutputStream(1024) - JSONSerializerUtilities.serialize(json, output) - return output.toString("UTF-8") - } - - /** - * Serialize a bookmark to a JSON string. - * - * @param objectMapper A JSON object mapper - * @param bookmark A pdf bookmark - * @return A JSON string - * @throws IOException On serialization errors - */ - - @JvmStatic - @Throws(IOException::class) - fun serializePdfBookmarkToString( - objectMapper: ObjectMapper, - bookmark: Bookmark.PDFBookmark - ): String { - val json = serializePdfBookmarkToJSON(objectMapper, bookmark) - val output = ByteArrayOutputStream(1024) - JSONSerializerUtilities.serialize(json, output) - return output.toString("UTF-8") - } - - /** - * Serialize a bookmark to a JSON string. - * - * @param objectMapper A JSON object mapper - * @param bookmarks A list of reader bookmarks - * @return A JSON string - * @throws IOException On serialization errors - */ - - @JvmStatic - @Throws(IOException::class) - fun serializeReaderBookmarksToString( - objectMapper: ObjectMapper, - bookmarks: List - ): String { - val json = serializeReaderBookmarksToJSON(objectMapper, bookmarks) - val output = ByteArrayOutputStream(1024) - val writer = objectMapper.writerWithDefaultPrettyPrinter() - writer.writeValue(output, json) - return output.toString("UTF-8") - } - - /** - * Serialize a bookmark to a JSON string. - * - * @param objectMapper A JSON object mapper - * @param bookmarks A list of pdf bookmarks - * @return A JSON string - * @throws IOException On serialization errors - */ - - @JvmStatic - @Throws(IOException::class) - fun serializePdfBookmarksToString( - objectMapper: ObjectMapper, - bookmarks: List - ): String { - val json = serializePdfBookmarksToJSON(objectMapper, bookmarks) - val output = ByteArrayOutputStream(1024) - val writer = objectMapper.writerWithDefaultPrettyPrinter() - writer.writeValue(output, json) - return output.toString("UTF-8") - } - - /** - * Deserialize a bookmark from the given string. - * - * @param objectMapper A JSON object mapper - * @param kind The kind of bookmark - * @param serialized A serialized JSON string - * @return A reader bookmark - * @throws IOException On I/O or parser errors - */ - - @JvmStatic - @Throws(IOException::class) - fun deserializeReaderBookmarkFromString( - objectMapper: ObjectMapper, - kind: BookmarkKind, - serialized: String - ): Bookmark.ReaderBookmark { - return deserializeReaderBookmarkFromJSON( - kind = kind, - node = objectMapper.readTree(serialized) - ) - } - - /** - * Deserialize a bookmark from the given string. - * - * @param objectMapper A JSON object mapper - * @param kind The kind of bookmark - * @param serialized A serialized JSON string - * @return A pdf bookmark - * @throws IOException On I/O or parser errors - */ - - @JvmStatic - @Throws(IOException::class) - fun deserializePdfBookmarkFromString( - objectMapper: ObjectMapper, - kind: BookmarkKind, - serialized: String - ): Bookmark.PDFBookmark { - return deserializePdfBookmarkFromJSON( - kind = kind, - node = objectMapper.readTree(serialized) - ) - } - - /** - * Serialize a bookmark to JSON. - * - * @param bookmark An audiobook bookmark - * @return A serialized object - */ - - @JvmStatic - fun serializeAudiobookBookmarkToJSON( - bookmark: Bookmark.AudiobookBookmark - ): ObjectNode { - val node = PlayerPositions.serializeToObjectNode(bookmark.location) - node.put("opdsId", bookmark.opdsId) - node.put("time", dateFormatter.print(bookmark.time)) - bookmark.deviceID.let { device -> node.put("deviceID", device) } - return node - } - - /** - * Serialize a bookmark to JSON. - * - * @param objectMapper A JSON object mapper - * @param bookmarks A list of audiobook bookmarks object mapper - * @return A serialized object - */ - - @JvmStatic - fun serializeAudiobookBookmarksToJSON( - objectMapper: ObjectMapper, - bookmarks: List - ): ArrayNode { - val node = objectMapper.createArrayNode() - bookmarks.forEach { bookmark -> - node.add( - serializeAudiobookBookmarkToJSON( - bookmark - ) - ) - } - return node - } - - /** - * Serialize a bookmark to a JSON string. - * - * @param bookmark An audiobook bookmark - * @return A JSON string - * @throws IOException On serialization errors - */ - - @JvmStatic - @Throws(IOException::class) - fun serializeAudiobookBookmarkToString( - bookmark: Bookmark.AudiobookBookmark - ): String { - val json = serializeAudiobookBookmarkToJSON(bookmark) - val output = ByteArrayOutputStream(1024) - JSONSerializerUtilities.serialize(json, output) - return output.toString("UTF-8") - } - - /** - * Serialize a bookmark to a JSON string. - * - * @param objectMapper A JSON object mapper - * @param bookmarks A list of audiobook bookmarks - * @return A JSON string - * @throws IOException On serialization errors - */ - - @JvmStatic - @Throws(IOException::class) - fun serializeAudiobookBookmarksToString( - objectMapper: ObjectMapper, - bookmarks: List - ): String { - val json = serializeAudiobookBookmarksToJSON(objectMapper, bookmarks) - val output = ByteArrayOutputStream(1024) - val writer = objectMapper.writerWithDefaultPrettyPrinter() - writer.writeValue(output, json) - return output.toString("UTF-8") - } - - /** - * Deserialize a bookmark from the given string. - * - * @param objectMapper A JSON object mapper - * @param kind The kind of bookmark - * @param serialized A serialized JSON string - * @return A parsed location - * @throws IOException On I/O or parser errors - */ - - @JvmStatic - @Throws(IOException::class) - fun deserializeAudiobookBookmarkFromString( - objectMapper: ObjectMapper, - kind: BookmarkKind, - serialized: String - ): Bookmark.AudiobookBookmark { - return deserializeAudiobookBookmarkFromJSON( - kind = kind, - node = objectMapper.readTree(serialized) - ) - } - - /** - * Deserialize bookmarks from the given JSON node. - * - * @param kind The kind of bookmark - * @param node A JSON node - * @return A parsed description - * @throws JSONParseException On parse errors - */ - - @JvmStatic - @Throws(JSONParseException::class) - fun deserializeAudiobookBookmarkFromJSON( - kind: BookmarkKind, - node: JsonNode - ): Bookmark.AudiobookBookmark { - return deserializeAudiobookBookmarkFromJSON( - kind = kind, - node = JSONParserUtilities.checkObject(null, node) - ) - } -} diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkMetadata.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkMetadata.kt new file mode 100644 index 000000000..81575c041 --- /dev/null +++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/BookmarkMetadata.kt @@ -0,0 +1,43 @@ +package org.nypl.simplified.books.api.bookmark + +import org.joda.time.DateTime +import org.joda.time.format.ISODateTimeFormat +import java.net.URI +import java.security.MessageDigest + +/** + * The non-critical metadata for a bookmark. + */ + +data class BookmarkMetadata( + val bookChapterProgress: Double, + val bookChapterTitle: String, + val bookOpdsId: String, + val bookProgress: Double, + val bookTitle: String, + val deviceID: String, + val time: DateTime, + val uri: URI?, +) { + + private val dateFormatter = + ISODateTimeFormat.dateTime() + .withZoneUTC() + + /** + * Add the fields of this object to the given message digest + */ + + fun addToDigest( + digest: MessageDigest + ) { + BookmarkDigests.addToDigest(digest, this.bookChapterProgress) + BookmarkDigests.addToDigest(digest, this.bookChapterTitle) + BookmarkDigests.addToDigest(digest, this.bookOpdsId) + BookmarkDigests.addToDigest(digest, this.bookProgress) + BookmarkDigests.addToDigest(digest, this.bookTitle) + BookmarkDigests.addToDigest(digest, this.deviceID) + BookmarkDigests.addToDigest(digest, this.dateFormatter.print(this.time)) + this.uri.let { x -> BookmarkDigests.addToDigest(digest, x.toString()) } + } +} diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark.kt new file mode 100644 index 000000000..b60aeee44 --- /dev/null +++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark.kt @@ -0,0 +1,125 @@ +package org.nypl.simplified.books.api.bookmark + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ObjectNode +import org.joda.time.DateTime +import org.nypl.simplified.books.api.BookID +import java.net.URI +import java.security.MessageDigest + +/** + * The type of serialized bookmarks. + */ + +sealed class SerializedBookmark { + + /** + * The type name (such as "Bookmark") + */ + + abstract val typeName: String + + /** + * The type version (such as 20210317) + */ + + abstract val typeVersion: Int + + /** + * @return The identifier of the book taken from the OPDS entry that provided it. + */ + + abstract val opdsId: String + + /** + * @return The kind of bookmark. + */ + + abstract val kind: BookmarkKind + + /** + * @return The time the bookmark was created. + */ + + abstract val time: DateTime + + /** + * @return The identifier of the device that created the bookmark, if one is available. + */ + + abstract val deviceID: String + + /** + * @return The URI of this bookmark, if the bookmark exists on a remote server. + */ + + abstract val uri: URI? + + /** + * The ID of the book to which the bookmark belongs. + */ + + abstract val book: BookID + + /** + * The unique ID of the bookmark. + */ + + abstract val bookmarkId: BookmarkID + + /** + * The bookmark location + */ + + abstract val location: SerializedLocator + + /** + * The progress of the bookmark throughout the entire book + */ + + abstract val bookProgress: Double + + /** + * The progress of the bookmark throughout the chapter + */ + + abstract val bookChapterProgress: Double + + /** + * The book title + */ + + abstract val bookTitle: String + + /** + * The book chapter title + */ + + abstract val bookChapterTitle: String + + /** + * Serialize this bookmark to JSON. + */ + + abstract fun toJSON( + objectMapper: ObjectMapper + ): ObjectNode + + /** + * Add the fields of this object to the given message digest + */ + + abstract fun addToDigest(digest: MessageDigest) + + /** + * @return This bookmark with the given URI + */ + + abstract fun withURI(uri: URI): SerializedBookmark + + /** + * @return This bookmark without the URI + */ + + abstract fun withoutURI(): SerializedBookmark +} diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark20210317.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark20210317.kt new file mode 100644 index 000000000..2ca53b9ac --- /dev/null +++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark20210317.kt @@ -0,0 +1,87 @@ +package org.nypl.simplified.books.api.bookmark + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ObjectNode +import org.joda.time.DateTime +import org.joda.time.format.ISODateTimeFormat +import org.nypl.simplified.books.api.BookID +import org.nypl.simplified.books.api.BookIDs +import java.net.URI +import java.nio.charset.StandardCharsets.UTF_8 +import java.security.MessageDigest + +data class SerializedBookmark20210317( + override val bookChapterProgress: Double, + override val bookChapterTitle: String, + override val bookProgress: Double, + override val bookTitle: String, + override val deviceID: String, + override val kind: BookmarkKind, + override val location: SerializedLocator, + override val opdsId: String, + override val time: DateTime, + override val uri: URI?, +) : SerializedBookmark() { + + private val dateFormatter = + ISODateTimeFormat.dateTime() + .withZoneUTC() + + override val typeName: String + get() = "Bookmark" + + override val typeVersion: Int + get() = 20210317 + + override val book: BookID + get() = BookIDs.newFromText(this.opdsId) + + override val bookmarkId: BookmarkID + get() = this.createBookmarkId() + + private fun createBookmarkId(): BookmarkID { + val digest = MessageDigest.getInstance("SHA-256") + this.addToDigest(digest) + + val digestResult = digest.digest() + val builder = StringBuilder(64) + for (index in digestResult.indices) { + val bb = digestResult[index] + builder.append(String.format("%02x", bb)) + } + return BookmarkID(builder.toString()) + } + + override fun toJSON( + objectMapper: ObjectMapper + ): ObjectNode { + val node = objectMapper.createObjectNode() + node.put("@type", this.typeName) + node.put("@version", this.typeVersion) + + val location = this.location.toJSON(objectMapper) + node.set("location", location) + node.put("time", this.dateFormatter.print(this.time)) + node.put("kind", this.kind.toString()) + node.put("opdsId", this.opdsId) + this.deviceID.let { device -> node.put("deviceID", device) } + this.uri.let { u -> node.put("uri", u.toString()) } + return node + } + + override fun addToDigest( + digest: MessageDigest + ) { + BookmarkDigests.addToDigest(digest, this.opdsId) + this.location.addToDigest(digest) + digest.update(this.dateFormatter.print(this.time).toByteArray(UTF_8)) + } + + override fun withURI(uri: URI): SerializedBookmark { + return this.copy(uri = uri) + } + + override fun withoutURI(): SerializedBookmark { + return this.copy(uri = null) + } +} diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark20210828.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark20210828.kt new file mode 100644 index 000000000..50e953ac8 --- /dev/null +++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark20210828.kt @@ -0,0 +1,87 @@ +package org.nypl.simplified.books.api.bookmark + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ObjectNode +import org.joda.time.DateTime +import org.joda.time.format.ISODateTimeFormat +import org.nypl.simplified.books.api.BookID +import org.nypl.simplified.books.api.BookIDs +import java.net.URI +import java.nio.charset.StandardCharsets.UTF_8 +import java.security.MessageDigest + +data class SerializedBookmark20210828( + override val bookChapterProgress: Double, + override val bookChapterTitle: String, + override val bookProgress: Double, + override val bookTitle: String, + override val deviceID: String, + override val kind: BookmarkKind, + override val location: SerializedLocator, + override val opdsId: String, + override val time: DateTime, + override val uri: URI?, +) : SerializedBookmark() { + + private val dateFormatter = + ISODateTimeFormat.dateTime() + .withZoneUTC() + + override val typeName: String + get() = "Bookmark" + + override val typeVersion: Int + get() = 20210828 + + override val book: BookID + get() = BookIDs.newFromText(this.opdsId) + + override val bookmarkId: BookmarkID + get() = this.createBookmarkId() + + private fun createBookmarkId(): BookmarkID { + val digest = MessageDigest.getInstance("SHA-256") + this.addToDigest(digest) + + val digestResult = digest.digest() + val builder = StringBuilder(64) + for (index in digestResult.indices) { + val bb = digestResult[index] + builder.append(String.format("%02x", bb)) + } + return BookmarkID(builder.toString()) + } + + override fun toJSON( + objectMapper: ObjectMapper + ): ObjectNode { + val node = objectMapper.createObjectNode() + node.put("@type", this.typeName) + node.put("@version", this.typeVersion) + + val location = this.location.toJSON(objectMapper) + node.set("location", location) + node.put("time", dateFormatter.print(this.time)) + node.put("kind", this.kind.toString()) + node.put("opdsId", this.opdsId) + this.deviceID.let { device -> node.put("deviceID", device) } + this.uri.let { u -> node.put("uri", u.toString()) } + return node + } + + override fun addToDigest( + digest: MessageDigest + ) { + BookmarkDigests.addToDigest(digest, this.opdsId) + this.location.addToDigest(digest) + digest.update(this.dateFormatter.print(this.time).toByteArray(UTF_8)) + } + + override fun withURI(uri: URI): SerializedBookmark { + return this.copy(uri = uri) + } + + override fun withoutURI(): SerializedBookmark { + return this.copy(uri = null) + } +} diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark20240424.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark20240424.kt new file mode 100644 index 000000000..1a5707854 --- /dev/null +++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmark20240424.kt @@ -0,0 +1,95 @@ +package org.nypl.simplified.books.api.bookmark + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ObjectNode +import org.joda.time.DateTime +import org.joda.time.format.ISODateTimeFormat +import org.nypl.simplified.books.api.BookID +import org.nypl.simplified.books.api.BookIDs +import java.net.URI +import java.nio.charset.StandardCharsets.UTF_8 +import java.security.MessageDigest + +data class SerializedBookmark20240424( + override val bookChapterProgress: Double, + override val bookChapterTitle: String, + override val bookProgress: Double, + override val bookTitle: String, + override val deviceID: String, + override val kind: BookmarkKind, + override val location: SerializedLocator, + override val opdsId: String, + override val time: DateTime, + override val uri: URI?, +) : SerializedBookmark() { + + private val dateFormatter = + ISODateTimeFormat.dateTime() + .withZoneUTC() + + override val typeName: String + get() = "Bookmark" + + override val typeVersion: Int + get() = 20240424 + + override val book: BookID + get() = BookIDs.newFromText(this.opdsId) + + override val bookmarkId: BookmarkID + get() = this.createBookmarkId() + + private fun createBookmarkId(): BookmarkID { + val digest = MessageDigest.getInstance("SHA-256") + this.addToDigest(digest) + + val digestResult = digest.digest() + val builder = StringBuilder(64) + for (index in digestResult.indices) { + val bb = digestResult[index] + builder.append(String.format("%02x", bb)) + } + return BookmarkID(builder.toString()) + } + + override fun toJSON( + objectMapper: ObjectMapper + ): ObjectNode { + val node = objectMapper.createObjectNode() + node.put("@type", this.typeName) + node.put("@version", this.typeVersion) + + val location = this.location.toJSON(objectMapper) + node.set("location", location) + + val metadata = objectMapper.createObjectNode() + metadata.put("bookChapterProgress", this.bookChapterProgress) + metadata.put("bookChapterTitle", this.bookChapterTitle) + metadata.put("bookProgress", this.bookProgress) + metadata.put("bookTitle", this.bookTitle) + metadata.put("deviceID", this.deviceID) + metadata.put("kind", this.kind.motivationURI) + metadata.put("opdsId", this.opdsId) + metadata.put("time", this.dateFormatter.print(this.time)) + this.uri.let { x -> metadata.put("uri", x.toString()) } + + node.set("metadata", metadata) + return node + } + + override fun addToDigest( + digest: MessageDigest + ) { + BookmarkDigests.addToDigest(digest, this.opdsId) + this.location.addToDigest(digest) + digest.update(this.dateFormatter.print(this.time).toByteArray(UTF_8)) + } + + override fun withURI(uri: URI): SerializedBookmark { + return this.copy(uri = uri) + } + + override fun withoutURI(): SerializedBookmark { + return this.copy(uri = null) + } +} diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmarkLegacy.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmarkLegacy.kt new file mode 100644 index 000000000..1662ea596 --- /dev/null +++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmarkLegacy.kt @@ -0,0 +1,111 @@ +package org.nypl.simplified.books.api.bookmark + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ObjectNode +import org.joda.time.DateTime +import org.joda.time.format.ISODateTimeFormat +import org.nypl.simplified.books.api.BookID +import org.nypl.simplified.books.api.BookIDs +import java.net.URI +import java.nio.charset.StandardCharsets.UTF_8 +import java.security.MessageDigest + +data class SerializedBookmarkLegacy( + override val bookChapterProgress: Double, + override val bookChapterTitle: String, + override val bookTitle: String, + override val deviceID: String, + override val kind: BookmarkKind, + override val location: SerializedLocator, + override val opdsId: String, + override val time: DateTime, + override val uri: URI?, + override val bookProgress: Double +) : SerializedBookmark() { + + private val dateFormatter = + ISODateTimeFormat.dateTime() + .withZoneUTC() + + override val typeName: String + get() = "Bookmark" + + override val typeVersion: Int + get() = 20210316 + + override val book: BookID + get() = BookIDs.newFromText(this.opdsId) + + override val bookmarkId: BookmarkID + get() = this.createBookmarkId() + + private fun createBookmarkId(): BookmarkID { + val digest = MessageDigest.getInstance("SHA-256") + this.addToDigest(digest) + + val digestResult = digest.digest() + val builder = StringBuilder(64) + for (index in digestResult.indices) { + val bb = digestResult[index] + builder.append(String.format("%02x", bb)) + } + return BookmarkID(builder.toString()) + } + + override fun toJSON( + objectMapper: ObjectMapper + ): ObjectNode { + val node = objectMapper.createObjectNode() + node.put("@type", this.typeName) + node.put("@version", this.typeVersion) + + // Legacy bookmarks had these at the top level + node.put("bookProgress", this.bookProgress) + node.put("chapterTitle", this.bookChapterTitle) + + when (this.location) { + is SerializedLocatorAudioBookTime1 -> { + node.put( + "chapterProgress", + this.location.timeMilliseconds.toDouble() / this.location.duration.toDouble() + ) + } + is SerializedLocatorHrefProgression20210317 -> { + node.put("chapterProgress", this.location.chapterProgress) + } + is SerializedLocatorLegacyCFI -> { + node.put("chapterProgress", this.location.chapterProgression) + } + is SerializedLocatorPage1 -> { + // Nothing + } + is SerializedLocatorAudioBookTime2 -> { + // Nothing + } + } + + val location = this.location.toJSON(objectMapper) + node.set("location", location) + node.put("time", dateFormatter.print(this.time)) + node.put("kind", this.kind.toString()) + this.deviceID.let { device -> node.put("deviceID", device) } + this.uri.let { u -> node.put("uri", u.toString()) } + return node + } + + override fun addToDigest( + digest: MessageDigest + ) { + BookmarkDigests.addToDigest(digest, this.opdsId) + this.location.addToDigest(digest) + digest.update(this.dateFormatter.print(this.time).toByteArray(UTF_8)) + } + + override fun withURI(uri: URI): SerializedBookmark { + return this.copy(uri = uri) + } + + override fun withoutURI(): SerializedBookmark { + return this.copy(uri = null) + } +} diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmarks.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmarks.kt new file mode 100644 index 000000000..76d51d10c --- /dev/null +++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedBookmarks.kt @@ -0,0 +1,238 @@ +package org.nypl.simplified.books.api.bookmark + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ObjectNode +import org.joda.time.DateTime +import org.nypl.simplified.json.core.JSONParseException +import org.nypl.simplified.json.core.JSONParserUtilities +import java.net.URI + +object SerializedBookmarks { + + private val objectMapper: ObjectMapper = + ObjectMapper() + + @JvmStatic + @Throws(JSONParseException::class) + fun parseBookmarkFromString( + text: String + ): SerializedBookmark { + return this.parseBookmark(this.objectMapper.readTree(text)) + } + + @JvmStatic + @Throws(JSONParseException::class) + fun parseBookmark( + node: JsonNode + ): SerializedBookmark { + val type = node["@type"] + return if (type != null) { + when (type.asText()) { + "Bookmark" -> { + this.parseBookmarkGuess(node) + } + else -> { + this.parseBookmarkGuess(node) + } + } + } else { + this.parseBookmarkGuess(node) + } + } + + @JvmStatic + @Throws(JSONParseException::class) + private fun parseBookmarkGuess( + node: JsonNode + ): SerializedBookmark { + val version = node["@version"] + return if (version != null) { + when (version.asText()) { + "20210317" -> { + this.parseBookmark20210317(node) + } + "20210828" -> { + this.parseBookmark20210828(node) + } + "20240424" -> { + this.parseBookmark20240424(node) + } + else -> { + this.parseBookmarkLegacy(node) + } + } + } else { + this.parseBookmarkLegacy(node) + } + } + + @JvmStatic + @Throws(JSONParseException::class) + private fun parseBookmark20240424( + node: JsonNode + ): SerializedBookmark { + return when (node) { + is ObjectNode -> { + val metadata = + JSONParserUtilities.checkObject("metadata", node.get("metadata")) + val opdsId = + JSONParserUtilities.getString(metadata, "opdsId") + val time = + JSONParserUtilities.getTimestamp(metadata, "time") + val deviceId = + JSONParserUtilities.getStringDefault(metadata, "deviceID", "null") + val uri = + JSONParserUtilities.getURIOrNull(metadata, "uri") + val bookChapterTitle = + JSONParserUtilities.getStringDefault(metadata, "bookChapterTitle", "") + val bookTitle = + JSONParserUtilities.getStringDefault(metadata, "bookTitle", "") + val bookProgress = + JSONParserUtilities.getDoubleDefault(metadata, "bookProgress", 0.0) + val bookChapterProgress = + JSONParserUtilities.getDoubleDefault(metadata, "bookChapterProgress", 0.0) + + SerializedBookmark20240424( + bookChapterProgress = bookChapterProgress, + bookChapterTitle = bookChapterTitle, + bookProgress = bookProgress, + bookTitle = bookTitle, + deviceID = deviceId, + kind = BookmarkKind.BookmarkExplicit, + location = SerializedLocators.parseLocator(node.get("location")), + opdsId = opdsId, + time = time, + uri = uri, + ) + } + else -> { + throw JSONParseException( + String.format( + "Bookmarks can only be parsed from JSON object nodes (received %s)", + node.nodeType + ) + ) + } + } + } + + @JvmStatic + @Throws(JSONParseException::class) + private fun parseBookmarkLegacy( + node: JsonNode + ): SerializedBookmarkLegacy { + return when (node) { + is ObjectNode -> { + SerializedBookmarkLegacy( + bookChapterProgress = 0.0, + bookChapterTitle = JSONParserUtilities.getStringDefault(node, "chapterTitle", ""), + bookProgress = JSONParserUtilities.getDoubleDefault(node, "bookProgress", 0.0), + bookTitle = "", + deviceID = JSONParserUtilities.getStringDefault(node, "deviceID", "null"), + kind = BookmarkKind.BookmarkExplicit, + location = SerializedLocators.parseLocator(node.get("location")), + opdsId = JSONParserUtilities.getString(node, "opdsId"), + time = JSONParserUtilities.getTimestamp(node, "time"), + uri = JSONParserUtilities.getURIOrNull(node, "uri"), + ) + } + else -> { + throw JSONParseException( + String.format( + "Bookmarks can only be parsed from JSON object nodes (received %s)", + node.nodeType + ) + ) + } + } + } + + @JvmStatic + @Throws(JSONParseException::class) + private fun parseBookmark20210317( + node: JsonNode + ): SerializedBookmark20210317 { + return when (node) { + is ObjectNode -> { + SerializedBookmark20210317( + bookChapterProgress = 0.0, + bookChapterTitle = JSONParserUtilities.getStringDefault(node, "chapterTitle", ""), + bookProgress = JSONParserUtilities.getDoubleDefault(node, "bookProgress", 0.0), + bookTitle = "", + deviceID = JSONParserUtilities.getStringDefault(node, "deviceID", "null"), + kind = BookmarkKind.BookmarkExplicit, + location = SerializedLocators.parseLocator(node.get("location")), + opdsId = JSONParserUtilities.getString(node, "opdsId"), + time = JSONParserUtilities.getTimestamp(node, "time"), + uri = JSONParserUtilities.getURIOrNull(node, "uri"), + ) + } + else -> { + throw JSONParseException( + String.format( + "Bookmarks can only be parsed from JSON object nodes (received %s)", + node.nodeType + ) + ) + } + } + } + + @JvmStatic + @Throws(JSONParseException::class) + private fun parseBookmark20210828( + node: JsonNode + ): SerializedBookmark20210828 { + return when (node) { + is ObjectNode -> { + SerializedBookmark20210828( + bookChapterProgress = 0.0, + bookChapterTitle = JSONParserUtilities.getStringDefault(node, "chapterTitle", ""), + bookProgress = JSONParserUtilities.getDoubleDefault(node, "bookProgress", 0.0), + bookTitle = "", + deviceID = JSONParserUtilities.getStringDefault(node, "deviceID", "null"), + kind = BookmarkKind.BookmarkExplicit, + location = SerializedLocators.parseLocator(node.get("location")), + opdsId = JSONParserUtilities.getString(node, "opdsId"), + time = JSONParserUtilities.getTimestamp(node, "time"), + uri = JSONParserUtilities.getURIOrNull(node, "uri"), + ) + } + else -> { + throw JSONParseException( + String.format( + "Bookmarks can only be parsed from JSON object nodes (received %s)", + node.nodeType + ) + ) + } + } + } + + fun createWithCurrentFormat( + bookChapterProgress: Double, + bookChapterTitle: String, + bookProgress: Double, + bookTitle: String, + deviceID: String, + kind: BookmarkKind, + location: SerializedLocator, + opdsId: String, + time: DateTime, + uri: URI? + ): SerializedBookmark { + return SerializedBookmark20240424( + bookChapterProgress = bookChapterProgress, + bookChapterTitle = bookChapterTitle, + bookProgress = bookProgress, + bookTitle = bookTitle, + deviceID = deviceID, + kind = kind, + location = location, + opdsId = opdsId, + time = time, + uri = uri + ) + } +} diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocator.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocator.kt new file mode 100644 index 000000000..124f1dc73 --- /dev/null +++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocator.kt @@ -0,0 +1,44 @@ +package org.nypl.simplified.books.api.bookmark + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ObjectNode +import java.security.MessageDigest + +/** + * @see "https://github.com/ThePalaceProject/mobile-specs/tree/main/bookmarks#locators" + */ + +sealed class SerializedLocator { + + /** + * The type name (such as "BookLocationR2") + */ + + abstract val typeName: String + + /** + * The type version (such as 20210317) + */ + + abstract val typeVersion: Int + + /** + * Serialize this locator to JSON. + */ + + abstract fun toJSON(objectMapper: ObjectMapper): ObjectNode + + /** + * Serialize this locator to a JSON string. + */ + + fun toJSONString(objectMapper: ObjectMapper): String { + return objectMapper.writeValueAsString(toJSON(objectMapper)) + } + + /** + * Add the fields of this object to the given message digest + */ + + abstract fun addToDigest(digest: MessageDigest) +} diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorAudioBookTime1.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorAudioBookTime1.kt new file mode 100644 index 000000000..f86d66b8c --- /dev/null +++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorAudioBookTime1.kt @@ -0,0 +1,61 @@ +package org.nypl.simplified.books.api.bookmark + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ObjectNode +import java.nio.charset.StandardCharsets.UTF_8 +import java.security.MessageDigest + +/** + * The version 1 standardization of "audio book time" locators. + * + * @see "https://github.com/ThePalaceProject/mobile-specs/tree/main/bookmarks#locatoraudiobooktime" + */ + +data class SerializedLocatorAudioBookTime1( + val audioBookId: String, + val chapter: Int, + val duration: Long, + val part: Int, + val startOffsetMilliseconds: Long, + val timeMilliseconds: Long, + val title: String, +) : SerializedLocator() { + + val timeWithoutOffset: Long + get() = this.timeMilliseconds - this.startOffsetMilliseconds + + override val typeName: String + get() = "LocatorAudioBookTime" + + override val typeVersion: Int + get() = 1 + + override fun toJSON( + objectMapper: ObjectMapper + ): ObjectNode { + val root = objectMapper.createObjectNode() + root.put("@type", this.typeName) + root.put("@version", this.typeVersion) + + root.put("audiobookID", this.audioBookId) + root.put("chapter", this.chapter) + root.put("duration", this.duration) + root.put("part", this.part) + root.put("startOffset", this.startOffsetMilliseconds) + root.put("time", this.timeMilliseconds) + root.put("title", this.title) + return root + } + + override fun addToDigest( + digest: MessageDigest + ) { + digest.update(this.audioBookId.toByteArray(UTF_8)) + digest.update(this.chapter.toString().toByteArray(UTF_8)) + digest.update(this.duration.toString().toByteArray(UTF_8)) + digest.update(this.part.toString().toByteArray(UTF_8)) + digest.update(this.startOffsetMilliseconds.toString().toByteArray(UTF_8)) + digest.update(this.timeMilliseconds.toString().toByteArray(UTF_8)) + digest.update(this.title.toByteArray(UTF_8)) + } +} diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorAudioBookTime2.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorAudioBookTime2.kt new file mode 100644 index 000000000..3ad559f33 --- /dev/null +++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorAudioBookTime2.kt @@ -0,0 +1,43 @@ +package org.nypl.simplified.books.api.bookmark + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ObjectNode +import java.nio.charset.StandardCharsets.UTF_8 +import java.security.MessageDigest + +/** + * The version 2 standardization of "audio book time" locators. + * + * @see "https://github.com/ThePalaceProject/mobile-specs/tree/main/bookmarks#locatoraudiobooktime" + */ + +data class SerializedLocatorAudioBookTime2( + val chapterHref: String, + val chapterOffsetMilliseconds: Long +) : SerializedLocator() { + + override val typeName: String + get() = "LocatorAudioBookTime" + + override val typeVersion: Int + get() = 2 + + override fun toJSON( + objectMapper: ObjectMapper + ): ObjectNode { + val root = objectMapper.createObjectNode() + root.put("@type", this.typeName) + root.put("@version", this.typeVersion) + + root.put("chapterHref", this.chapterHref) + root.put("chapterOffsetMilliseconds", this.chapterOffsetMilliseconds) + return root + } + + override fun addToDigest( + digest: MessageDigest + ) { + digest.update(this.chapterHref.toByteArray(UTF_8)) + digest.update(this.chapterOffsetMilliseconds.toString().toByteArray(UTF_8)) + } +} diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorHrefProgression20210317.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorHrefProgression20210317.kt new file mode 100644 index 000000000..15ede57e6 --- /dev/null +++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorHrefProgression20210317.kt @@ -0,0 +1,51 @@ +package org.nypl.simplified.books.api.bookmark + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ObjectNode +import java.nio.charset.StandardCharsets.UTF_8 +import java.security.MessageDigest + +/** + * The 20210317 standardization of R2 "href/progression" locators. + * + * @see "https://github.com/ThePalaceProject/mobile-specs/tree/main/bookmarks#locatorhrefprogression" + */ + +data class SerializedLocatorHrefProgression20210317( + /** + * The href of the chapter. + */ + + val chapterHref: String, + + /** + * The progress through the chapter. + */ + + val chapterProgress: Double, +) : SerializedLocator() { + + override val typeName: String + get() = "LocatorHrefProgression" + + override val typeVersion: Int + get() = 20210317 + + override fun toJSON( + objectMapper: ObjectMapper + ): ObjectNode { + val root = objectMapper.createObjectNode() + root.put("@type", this.typeName) + root.put("@version", this.typeVersion) + root.put("href", this.chapterHref) + root.put("progressWithinChapter", this.chapterProgress) + return root + } + + override fun addToDigest( + digest: MessageDigest + ) { + digest.update(this.chapterHref.toByteArray(UTF_8)) + digest.update(String.format("%.6f", this.chapterProgress).toByteArray(UTF_8)) + } +} diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorLegacyCFI.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorLegacyCFI.kt new file mode 100644 index 000000000..4fcc4ba52 --- /dev/null +++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorLegacyCFI.kt @@ -0,0 +1,47 @@ +package org.nypl.simplified.books.api.bookmark + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ObjectNode +import java.nio.charset.StandardCharsets.UTF_8 +import java.security.MessageDigest + +/** + * The version 1 standardization of legacy "CFI" locators. + * + * @see "https://github.com/ThePalaceProject/mobile-specs/tree/main/bookmarks#locatorlegacycfi" + */ + +data class SerializedLocatorLegacyCFI( + val idRef: String?, + val contentCFI: String?, + val chapterProgression: Double +) : SerializedLocator() { + + override val typeName: String + get() = "LocatorLegacyCFI" + + override val typeVersion: Int + get() = 1 + + override fun toJSON( + objectMapper: ObjectMapper + ): ObjectNode { + val root = objectMapper.createObjectNode() + root.put("@type", this.typeName) + root.put("@version", this.typeVersion) + + this.idRef?.let { x -> root.put("idref", x) } + this.contentCFI?.let { x -> root.put("contentCFI", x) } + + root.put("progressWithinChapter", this.chapterProgression) + return root + } + + override fun addToDigest( + digest: MessageDigest + ) { + this.idRef?.let { x -> digest.update(x.toByteArray(UTF_8)) } + this.contentCFI?.let { x -> digest.update(x.toByteArray(UTF_8)) } + digest.update(String.format("%.6f", this.chapterProgression).toByteArray(UTF_8)) + } +} diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorPage1.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorPage1.kt new file mode 100644 index 000000000..339f82f63 --- /dev/null +++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocatorPage1.kt @@ -0,0 +1,43 @@ +package org.nypl.simplified.books.api.bookmark + +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ObjectNode +import java.nio.charset.StandardCharsets.UTF_8 +import java.security.MessageDigest + +/** + * The version 1 standardization of "page" locators. + * + * @see "https://github.com/ThePalaceProject/mobile-specs/tree/main/bookmarks#locatorpage" + */ + +data class SerializedLocatorPage1( + /** + * The page number. + */ + + val page: Int, +) : SerializedLocator() { + + override val typeName: String + get() = "LocatorPage" + + override val typeVersion: Int + get() = 1 + + override fun toJSON( + objectMapper: ObjectMapper + ): ObjectNode { + val root = objectMapper.createObjectNode() + root.put("@type", this.typeName) + root.put("@version", this.typeVersion) + root.put("page", this.page) + return root + } + + override fun addToDigest( + digest: MessageDigest + ) { + digest.update(this.page.toString().toByteArray(UTF_8)) + } +} diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocators.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocators.kt new file mode 100644 index 000000000..51d72bb42 --- /dev/null +++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocators.kt @@ -0,0 +1,169 @@ +package org.nypl.simplified.books.api.bookmark + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ObjectNode +import org.nypl.simplified.json.core.JSONParseException +import org.nypl.simplified.json.core.JSONParserUtilities +import java.math.BigInteger + +object SerializedLocators { + + private val objectMapper: ObjectMapper = + ObjectMapper() + + @JvmStatic + @Throws(JSONParseException::class) + fun parseLocatorFromString( + text: String + ): SerializedLocator { + return this.parseLocator(this.objectMapper.readTree(text)) + } + + @JvmStatic + @Throws(JSONParseException::class) + fun parseLocator( + node: JsonNode + ): SerializedLocator { + return when (node) { + is ObjectNode -> { + this.parseLocator(node) + } + + else -> { + throw JSONParseException( + String.format( + "Locators can only be parsed from JSON object nodes (received %s)", + node.nodeType + ) + ) + } + } + } + + @JvmStatic + @Throws(JSONParseException::class) + fun parseLocator( + node: ObjectNode + ): SerializedLocator { + val type = node["@type"] + return if (type != null) { + when (type.asText()) { + "BookLocationR1", "LocatorLegacyCFI" -> { + this.parseLocatorLegacyCFI(node) + } + + "LocatorAudioBookTime" -> { + this.parseLocatorAudioBookTime(node) + } + + "BookLocationR2", "LocatorHrefProgression" -> { + this.parseLocatorHrefProgression(node) + } + + "LocatorPage" -> { + this.parseLocatorPage(node) + } + + else -> { + this.parseLocatorGuess(node) + } + } + } else { + this.parseLocatorGuess(node) + } + } + + @JvmStatic + @Throws(JSONParseException::class) + private fun parseLocatorLegacyCFI( + node: ObjectNode + ): SerializedLocatorLegacyCFI { + return SerializedLocatorLegacyCFI( + idRef = JSONParserUtilities.getStringOrNull(node, "idref"), + contentCFI = JSONParserUtilities.getStringOrNull(node, "contentCFI"), + chapterProgression = JSONParserUtilities.getDoubleDefault(node, "progressWithinChapter", 0.0) + ) + } + + @JvmStatic + @Throws(JSONParseException::class) + private fun parseLocatorGuess( + node: ObjectNode + ): SerializedLocator { + return this.parseLocatorLegacyCFI(node) + } + + @JvmStatic + @Throws(JSONParseException::class) + private fun parseLocatorPage( + node: ObjectNode + ): SerializedLocatorPage1 { + return SerializedLocatorPage1( + page = JSONParserUtilities.getInteger(node, "page"), + ) + } + + @JvmStatic + @Throws(JSONParseException::class) + private fun parseLocatorHrefProgression( + node: ObjectNode + ): SerializedLocatorHrefProgression20210317 { + return SerializedLocatorHrefProgression20210317( + chapterHref = JSONParserUtilities.getString(node, "href"), + chapterProgress = JSONParserUtilities.getDouble(node, "progressWithinChapter") + ) + } + + @JvmStatic + @Throws(JSONParseException::class) + private fun parseLocatorAudioBookTime( + node: ObjectNode + ): SerializedLocator { + return when (val version = JSONParserUtilities.getIntegerDefault(node, "@version", 1)) { + 1 -> this.parseLocatorAudioBookTime1(node) + 2 -> this.parseLocatorAudioBookTime2(node) + + else -> { + throw JSONParseException( + String.format( + "Unsupported audio book locator version (received %s)", + version + ) + ) + } + } + } + + private fun parseLocatorAudioBookTime2( + node: ObjectNode + ): SerializedLocatorAudioBookTime2 { + return SerializedLocatorAudioBookTime2( + chapterHref = + JSONParserUtilities.getString(node, "chapterHref"), + chapterOffsetMilliseconds = + JSONParserUtilities.getBigInteger(node, "chapterOffsetMilliseconds").toLong() + ) + } + + private fun parseLocatorAudioBookTime1( + node: ObjectNode + ): SerializedLocatorAudioBookTime1 { + val startOffset = + JSONParserUtilities.getBigIntegerDefault(node, "startOffset", BigInteger.ZERO) + .toLong() + val timeMillisecondsRaw = + JSONParserUtilities.getBigInteger(node, "time") + .toLong() + + return SerializedLocatorAudioBookTime1( + part = JSONParserUtilities.getInteger(node, "part"), + chapter = JSONParserUtilities.getInteger(node, "chapter"), + title = JSONParserUtilities.getString(node, "title"), + audioBookId = JSONParserUtilities.getString(node, "audiobookID"), + duration = JSONParserUtilities.getBigInteger(node, "duration").toLong(), + startOffsetMilliseconds = startOffset, + timeMilliseconds = timeMillisecondsRaw, + ) + } +} diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/helper/AudiobookLocationJSON.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/helper/AudiobookLocationJSON.kt deleted file mode 100644 index 7f6960bb7..000000000 --- a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/helper/AudiobookLocationJSON.kt +++ /dev/null @@ -1,102 +0,0 @@ -package org.nypl.simplified.books.api.helper - -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.node.ObjectNode -import org.librarysimplified.audiobook.api.PlayerPosition -import org.nypl.simplified.json.core.JSONParseException -import org.nypl.simplified.json.core.JSONParserUtilities -import org.nypl.simplified.json.core.JSONSerializerUtilities -import java.io.ByteArrayOutputStream -import java.io.IOException - -/** - * Functions to serialize audiobook locations to/from JSON. - */ - -object AudiobookLocationJSON { - - /** - * Deserialize audiobook player positions from the given JSON node. - * - * @param node A JSON node - * @return A parsed player position - * @throws JSONParseException On parse errors - */ - - @Throws(JSONParseException::class) - fun deserializeFromJSON( - node: JsonNode - ): PlayerPosition { - val obj = - JSONParserUtilities.checkObject(null, node) - return PlayerPosition( - title = JSONParserUtilities.getStringOrNull(obj, "title"), - part = JSONParserUtilities.getIntegerDefault(obj, "part", 0), - chapter = JSONParserUtilities.getIntegerDefault(obj, "chapter", 0), - startOffset = JSONParserUtilities.getIntegerDefault(obj, "startOffset", 0).toLong(), - currentOffset = JSONParserUtilities.getIntegerDefault(obj, "time", 0).toLong() - ) - } - - /** - * Serialize reader book locations to JSON. - * - * @param objectMapper A JSON object mapper - * @param position The position of the audiobook - * @return A serialized object - */ - - fun serializeToJSON( - objectMapper: ObjectMapper, - position: PlayerPosition - ): ObjectNode { - val root = objectMapper.createObjectNode() - root.put("chapter", position.chapter) - root.put("startOffset", position.startOffset) - root.put("time", position.currentOffset) - root.put("part", position.part) - root.put("title", position.title) - return root - } - - /** - * Serialize reader book locations to a JSON string. - * - * @param objectMapper A JSON object mapper - * @param position The position in the audiobook - * @return A JSON string - * @throws IOException On serialization errors - */ - - @Throws(IOException::class) - fun serializeToString( - objectMapper: ObjectMapper, - position: PlayerPosition - ): String { - val jo = serializeToJSON(objectMapper, position) - val bao = ByteArrayOutputStream(1024) - JSONSerializerUtilities.serialize(jo, bao) - return bao.toString("UTF-8") - } - - /** - * Deserialize a reader book location from the given string. - * - * @param objectMapper A JSON object mapper - * @param text The text to map - * @return A parsed player position - * @throws IOException On I/O or parser errors - */ - - @Throws(IOException::class) - fun deserializeFromString( - objectMapper: ObjectMapper, - text: String - ): PlayerPosition { - val node = objectMapper.readTree(text) - return deserializeFromJSON( - node = JSONParserUtilities.checkObject(null, node) - ) - } -} diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/helper/ReaderLocationJSON.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/helper/ReaderLocationJSON.kt deleted file mode 100644 index f6c6ab0c6..000000000 --- a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/helper/ReaderLocationJSON.kt +++ /dev/null @@ -1,224 +0,0 @@ -package org.nypl.simplified.books.api.helper - -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.node.ObjectNode -import org.nypl.simplified.books.api.BookChapterProgress -import org.nypl.simplified.books.api.BookLocation -import org.nypl.simplified.books.api.BookLocation.BookLocationR1 -import org.nypl.simplified.books.api.BookLocation.BookLocationR2 -import org.nypl.simplified.json.core.JSONParseException -import org.nypl.simplified.json.core.JSONParserUtilities -import org.nypl.simplified.json.core.JSONSerializerUtilities -import java.io.ByteArrayOutputStream -import java.io.IOException - -/** - * Functions to serialize and reader book locations to/from JSON. - */ - -object ReaderLocationJSON { - - /** - * Deserialize chapter progress from the given JSON node. - * - * @param node A JSON node - * @return A parsed description - * @throws JSONParseException On parse errors - */ - - @Throws(JSONParseException::class) - fun deserializeProgressFromJSON( - node: JsonNode - ): BookChapterProgress { - val obj = JSONParserUtilities.checkObject(null, node) - return BookChapterProgress( - chapterHref = JSONParserUtilities.getString(obj, "chapterHref"), - chapterProgress = JSONParserUtilities.getDouble(obj, "chapterProgress") - ) - } - - /** - * Deserialize reader book locations from the given JSON node. - * - * @param node A JSON node - * @return A book location - * @throws JSONParseException On parse errors - */ - - @Throws(JSONParseException::class) - fun deserializeFromJSON( - node: JsonNode - ): BookLocation { - val obj = - JSONParserUtilities.checkObject(null, node) - val type = - JSONParserUtilities.getStringOrNull(obj, "@type") - ?: return deserializeFromJSONR1Old(obj) - return when (type) { - "BookLocationR2" -> - deserializeFromJSONR2(obj) - "BookLocationR1" -> - deserializeFromJSONR1(obj) - else -> - throw JSONParseException("Unsupported location type: $type") - } - } - - private fun deserializeFromJSONR1( - obj: ObjectNode - ): BookLocationR1 { - val version = - JSONParserUtilities.getIntegerOrNull(obj, "@version") - ?: return deserializeFromJSONR1Old(obj) - return when (version) { - 20210317 -> - deserializeFromJSONR1_20210317(obj) - else -> - throw JSONParseException("Unsupported book location format version: $version") - } - } - - private fun deserializeFromJSONR1Old( - obj: ObjectNode - ): BookLocationR1 { - return BookLocationR1( - progress = JSONParserUtilities.getDoubleDefault(obj, "chapterProgress", 0.0), - contentCFI = JSONParserUtilities.getStringOrNull(obj, "contentCFI"), - idRef = JSONParserUtilities.getStringOrNull(obj, "idref") - ) - } - - private fun deserializeFromJSONR1_20210317( - obj: ObjectNode - ): BookLocationR1 { - return BookLocationR1( - progress = JSONParserUtilities.getDouble(obj, "chapterProgress"), - contentCFI = JSONParserUtilities.getStringOrNull(obj, "contentCFI"), - idRef = JSONParserUtilities.getStringOrNull(obj, "idref") - ) - } - - @Throws(JSONParseException::class) - private fun deserializeFromJSONR2( - obj: ObjectNode - ): BookLocation { - val version = - JSONParserUtilities.getIntegerOrNull(obj, "@version") - ?: return deserializeFromJSONR2Old() - return when (version) { - 20210317 -> - deserializeFromJSONR2_20210317(obj) - else -> - throw JSONParseException("Unsupported book location format version: $version") - } - } - - @Throws(JSONParseException::class) - private fun deserializeFromJSONR2_20210317( - obj: ObjectNode - ): BookLocationR2 { - val progress = JSONParserUtilities.getObject(obj, "progress") - return BookLocationR2(deserializeProgressFromJSON(progress)) - } - - @Throws(JSONParseException::class) - private fun deserializeFromJSONR2Old(): BookLocation { - throw JSONParseException("Unsupported book location format version: (unspecified)") - } - - /** - * Serialize reader book locations to JSON. - * - * @param objectMapper A JSON object mapper - * @return A serialized object - */ - - fun serializeToJSON( - objectMapper: ObjectMapper, - description: BookLocation - ): ObjectNode { - return when (description) { - is BookLocationR2 -> - serializeToJSONR2(objectMapper, description) - is BookLocationR1 -> - serializeToJSONR1(objectMapper, description) - } - } - - private fun serializeToJSONR2( - objectMapper: ObjectMapper, - description: BookLocationR2 - ): ObjectNode { - val root = objectMapper.createObjectNode() - root.put("@type", "BookLocationR2") - root.put("@version", 20210317) - - val progress = objectMapper.createObjectNode() - progress.put("chapterHref", description.progress.chapterHref) - progress.put("chapterProgress", description.progress.chapterProgress) - - root.set("progress", progress) - return root - } - - private fun serializeToJSONR1( - objectMapper: ObjectMapper, - description: BookLocationR1 - ): ObjectNode { - val root = objectMapper.createObjectNode() - - root.put("@type", "BookLocationR1") - root.put("@version", 20210317) - - val contentCFI = description.contentCFI - if (contentCFI != null) { - root.put("contentCFI", contentCFI) - } - val idRef = description.idRef - if (idRef != null) { - root.put("idref", idRef) - } - - root.put("chapterProgress", description.progress ?: 0.0) - return root - } - - /** - * Serialize reader book locations to a JSON string. - * - * @param objectMapper A JSON object mapper - * @return A JSON string - * @throws IOException On serialization errors - */ - - @Throws(IOException::class) - fun serializeToString( - objectMapper: ObjectMapper, - description: BookLocation - ): String { - val jo = serializeToJSON(objectMapper, description) - val bao = ByteArrayOutputStream(1024) - JSONSerializerUtilities.serialize(jo, bao) - return bao.toString("UTF-8") - } - - /** - * Deserialize a reader book location from the given string. - * - * @param objectMapper A JSON object mapper - * @return A parsed location - * @throws IOException On I/O or parser errors - */ - - @Throws(IOException::class) - fun deserializeFromString( - objectMapper: ObjectMapper, - text: String - ): BookLocation { - val node = objectMapper.readTree(text) - return deserializeFromJSON( - node = JSONParserUtilities.checkObject(null, node) - ) - } -} diff --git a/simplified-books-audio/build.gradle.kts b/simplified-books-audio/build.gradle.kts index ea39e15ad..5ce2944ce 100644 --- a/simplified-books-audio/build.gradle.kts +++ b/simplified-books-audio/build.gradle.kts @@ -26,6 +26,6 @@ dependencies { implementation(libs.palace.audiobook.parser.api) implementation(libs.palace.http.api) implementation(libs.r2.shared) - implementation(libs.rxjava) + implementation(libs.rxjava2) implementation(libs.slf4j) } diff --git a/simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/AbstractAudioBookManifestStrategy.kt b/simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/AbstractAudioBookManifestStrategy.kt index 855f6d3e1..3b2a6be6c 100644 --- a/simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/AbstractAudioBookManifestStrategy.kt +++ b/simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/AbstractAudioBookManifestStrategy.kt @@ -1,6 +1,8 @@ package org.nypl.simplified.books.audio import android.app.Application +import io.reactivex.Observable +import io.reactivex.subjects.PublishSubject import one.irradia.mime.api.MIMEType import org.librarysimplified.audiobook.api.PlayerResult import org.librarysimplified.audiobook.license_check.api.LicenseCheckParameters @@ -19,8 +21,6 @@ import org.nypl.simplified.taskrecorder.api.TaskRecorder import org.nypl.simplified.taskrecorder.api.TaskRecorderType import org.nypl.simplified.taskrecorder.api.TaskResult import org.slf4j.LoggerFactory -import rx.Observable -import rx.subjects.PublishSubject import java.net.URI /** @@ -271,7 +271,7 @@ abstract class AbstractAudioBookManifestStrategy( try { return check.execute() } finally { - checkSubscription.unsubscribe() + checkSubscription.dispose() } } diff --git a/simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/AudioBookManifestStrategyType.kt b/simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/AudioBookManifestStrategyType.kt index 2d1f11a12..5e6b37ca2 100644 --- a/simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/AudioBookManifestStrategyType.kt +++ b/simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/AudioBookManifestStrategyType.kt @@ -1,7 +1,7 @@ package org.nypl.simplified.books.audio +import io.reactivex.Observable import org.nypl.simplified.taskrecorder.api.TaskResult -import rx.Observable /** * A strategy for obtaining, parsing, and license-checking an audio book manifest. A given diff --git a/simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/UnpackagedAudioBookManifestStrategy.kt b/simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/UnpackagedAudioBookManifestStrategy.kt index 351b35f6b..2b9b6961e 100644 --- a/simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/UnpackagedAudioBookManifestStrategy.kt +++ b/simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/UnpackagedAudioBookManifestStrategy.kt @@ -91,7 +91,7 @@ class UnpackagedAudioBookManifestStrategy( try { return strategy.execute() } finally { - fulfillSubscription.unsubscribe() + fulfillSubscription.dispose() } } diff --git a/simplified-books-borrowing/build.gradle.kts b/simplified-books-borrowing/build.gradle.kts index 277b1d89e..304e7aeb1 100644 --- a/simplified-books-borrowing/build.gradle.kts +++ b/simplified-books-borrowing/build.gradle.kts @@ -24,9 +24,9 @@ dependencies { implementation(libs.io7m.junreachable) implementation(libs.irradia.mime.api) implementation(libs.joda.time) - implementation(libs.kotlinx.coroutines) implementation(libs.kotlin.reflect) implementation(libs.kotlin.stdlib) + implementation(libs.kotlinx.coroutines) implementation(libs.palace.audiobook.api) implementation(libs.palace.audiobook.manifest.fulfill.api) implementation(libs.palace.audiobook.manifest.fulfill.spi) @@ -37,6 +37,7 @@ dependencies { implementation(libs.r2.lcp) implementation(libs.r2.shared) implementation(libs.rxjava) + implementation(libs.rxjava2) implementation(libs.service.wight.core) implementation(libs.slf4j) implementation(libs.truecommons.key.disable) diff --git a/simplified-books-borrowing/src/main/java/org/nypl/simplified/books/borrowing/internal/BorrowACSM.kt b/simplified-books-borrowing/src/main/java/org/nypl/simplified/books/borrowing/internal/BorrowACSM.kt index f966ef026..3f31cc9c4 100644 --- a/simplified-books-borrowing/src/main/java/org/nypl/simplified/books/borrowing/internal/BorrowACSM.kt +++ b/simplified-books-borrowing/src/main/java/org/nypl/simplified/books/borrowing/internal/BorrowACSM.kt @@ -18,8 +18,8 @@ import org.nypl.simplified.adobe.extensions.AdobeDRMExtensions import org.nypl.simplified.adobe.extensions.AdobeDRMExtensions.AdobeDRMFulfillmentException import org.nypl.simplified.books.api.BookDRMKind.ACS import org.nypl.simplified.books.book_database.api.BookDRMInformationHandle.ACSHandle -import org.nypl.simplified.books.book_database.api.BookDRMInformationHandle.LCPHandle import org.nypl.simplified.books.book_database.api.BookDRMInformationHandle.AxisHandle +import org.nypl.simplified.books.book_database.api.BookDRMInformationHandle.LCPHandle import org.nypl.simplified.books.book_database.api.BookDRMInformationHandle.NoneHandle import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleAudioBook diff --git a/simplified-books-borrowing/src/main/java/org/nypl/simplified/books/borrowing/internal/BorrowAudioBook.kt b/simplified-books-borrowing/src/main/java/org/nypl/simplified/books/borrowing/internal/BorrowAudioBook.kt index bdab7fa12..4ba7cbefb 100644 --- a/simplified-books-borrowing/src/main/java/org/nypl/simplified/books/borrowing/internal/BorrowAudioBook.kt +++ b/simplified-books-borrowing/src/main/java/org/nypl/simplified/books/borrowing/internal/BorrowAudioBook.kt @@ -172,7 +172,7 @@ class BorrowAudioBook private constructor() : BorrowSubtaskType { } } } finally { - subscription.unsubscribe() + subscription.dispose() } } diff --git a/simplified-books-controller/src/main/java/org/nypl/simplified/books/controller/BookSyncTask.kt b/simplified-books-controller/src/main/java/org/nypl/simplified/books/controller/BookSyncTask.kt index dfd02c8b6..b4d8f5835 100644 --- a/simplified-books-controller/src/main/java/org/nypl/simplified/books/controller/BookSyncTask.kt +++ b/simplified-books-controller/src/main/java/org/nypl/simplified/books/controller/BookSyncTask.kt @@ -39,7 +39,6 @@ import java.io.ByteArrayInputStream import java.io.IOException import java.io.InputStream import java.net.URI -import java.util.HashSet import java.util.concurrent.TimeUnit class BookSyncTask( diff --git a/simplified-books-controller/src/main/java/org/nypl/simplified/books/controller/Controller.kt b/simplified-books-controller/src/main/java/org/nypl/simplified/books/controller/Controller.kt index daf28fc79..32f974285 100644 --- a/simplified-books-controller/src/main/java/org/nypl/simplified/books/controller/Controller.kt +++ b/simplified-books-controller/src/main/java/org/nypl/simplified/books/controller/Controller.kt @@ -24,9 +24,6 @@ import org.nypl.simplified.accounts.api.AccountLogoutStringResourcesType import org.nypl.simplified.accounts.api.AccountProviderType import org.nypl.simplified.accounts.database.api.AccountType import org.nypl.simplified.accounts.database.api.AccountsDatabaseNonexistentException -import org.nypl.simplified.deeplinks.controller.api.DeepLinkEvent -import org.nypl.simplified.deeplinks.controller.api.DeepLinksControllerType -import org.nypl.simplified.deeplinks.controller.api.ScreenID import org.nypl.simplified.accounts.registry.api.AccountProviderRegistryEvent import org.nypl.simplified.accounts.registry.api.AccountProviderRegistryType import org.nypl.simplified.analytics.api.AnalyticsType @@ -47,6 +44,9 @@ import org.nypl.simplified.books.formats.api.BookFormatSupportType import org.nypl.simplified.books.preview.BookPreviewRequirements import org.nypl.simplified.books.preview.BookPreviewTask import org.nypl.simplified.crashlytics.api.CrashlyticsServiceType +import org.nypl.simplified.deeplinks.controller.api.DeepLinkEvent +import org.nypl.simplified.deeplinks.controller.api.DeepLinksControllerType +import org.nypl.simplified.deeplinks.controller.api.ScreenID import org.nypl.simplified.feeds.api.Feed import org.nypl.simplified.feeds.api.FeedEntry import org.nypl.simplified.feeds.api.FeedLoaderType diff --git a/simplified-books-controller/src/main/java/org/nypl/simplified/books/controller/ProfileFeedTask.kt b/simplified-books-controller/src/main/java/org/nypl/simplified/books/controller/ProfileFeedTask.kt index 2f3cef231..f87b963f6 100644 --- a/simplified-books-controller/src/main/java/org/nypl/simplified/books/controller/ProfileFeedTask.kt +++ b/simplified-books-controller/src/main/java/org/nypl/simplified/books/controller/ProfileFeedTask.kt @@ -17,7 +17,6 @@ import org.nypl.simplified.feeds.api.FeedSearch import org.nypl.simplified.profiles.controller.api.ProfileFeedRequest import org.nypl.simplified.profiles.controller.api.ProfilesControllerType import org.slf4j.LoggerFactory -import java.util.ArrayList import java.util.Collections import java.util.Locale import java.util.concurrent.Callable diff --git a/simplified-books-database-api/src/main/java/org/nypl/simplified/books/book_database/api/BookDRMInformationHandle.kt b/simplified-books-database-api/src/main/java/org/nypl/simplified/books/book_database/api/BookDRMInformationHandle.kt index 292e512ef..94fef2e8f 100644 --- a/simplified-books-database-api/src/main/java/org/nypl/simplified/books/book_database/api/BookDRMInformationHandle.kt +++ b/simplified-books-database-api/src/main/java/org/nypl/simplified/books/book_database/api/BookDRMInformationHandle.kt @@ -1,8 +1,8 @@ package org.nypl.simplified.books.book_database.api import net.jcip.annotations.ThreadSafe -import org.nypl.simplified.books.api.BookDRMInformation import org.nypl.drm.core.AdobeAdeptLoan +import org.nypl.simplified.books.api.BookDRMInformation import java.io.File import java.io.IOException diff --git a/simplified-books-database-api/src/main/java/org/nypl/simplified/books/book_database/api/BookDatabaseEntryType.kt b/simplified-books-database-api/src/main/java/org/nypl/simplified/books/book_database/api/BookDatabaseEntryType.kt index f6e176b92..b2e16924f 100644 --- a/simplified-books-database-api/src/main/java/org/nypl/simplified/books/book_database/api/BookDatabaseEntryType.kt +++ b/simplified-books-database-api/src/main/java/org/nypl/simplified/books/book_database/api/BookDatabaseEntryType.kt @@ -8,7 +8,8 @@ import org.nypl.simplified.books.api.BookFormat import org.nypl.simplified.books.api.BookFormat.BookFormatAudioBook import org.nypl.simplified.books.api.BookFormat.BookFormatEPUB import org.nypl.simplified.books.api.BookFormat.BookFormatPDF -import org.nypl.simplified.books.api.bookmark.Bookmark +import org.nypl.simplified.books.api.bookmark.BookmarkID +import org.nypl.simplified.books.api.bookmark.SerializedBookmark import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleAudioBook import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleEPUB import org.nypl.simplified.books.book_database.api.BookFormats.BookFormatDefinition.BOOK_FORMAT_AUDIO @@ -168,6 +169,35 @@ sealed class BookDatabaseEntryFormatHandle { @Throws(IOException::class) abstract fun deleteBookData() + /** + * Delete the bookmark with the given ID. + */ + + @Throws(IOException::class) + abstract fun deleteBookmark(bookmarkId: BookmarkID) + + /** + * Set the last read location for the book. + * + * @param bookmark The location + * + * @throws IOException On I/O errors + */ + + @Throws(IOException::class) + abstract fun setLastReadLocation(bookmark: SerializedBookmark?) + + /** + * Add a bookmark to the book, replacing any existing bookmark with the same ID. + * + * @param bookmark The location + * + * @throws IOException On I/O errors + */ + + @Throws(IOException::class) + abstract fun addBookmark(bookmark: SerializedBookmark) + /** * The interface exposed by the EPUB format in database entries. */ @@ -189,28 +219,6 @@ sealed class BookDatabaseEntryFormatHandle { @Throws(IOException::class) abstract fun copyInBook(file: File) - - /** - * Set the last read location for the book. - * - * @param bookmark The location - * - * @throws IOException On I/O errors - */ - - @Throws(IOException::class) - abstract fun setLastReadLocation(bookmark: Bookmark.ReaderBookmark?) - - /** - * Set the bookmarks for the book. - * - * @param bookmarks The list of bookmarks - * - * @throws IOException On I/O errors - */ - - @Throws(IOException::class) - abstract fun setBookmarks(bookmarks: List) } /** @@ -234,27 +242,6 @@ sealed class BookDatabaseEntryFormatHandle { @Throws(IOException::class) abstract fun copyInBook(file: File) - - /** - * Set the last read location for the PDF book. - * - * @param bookmark The bookmark of the PDF book - * - * @throws IOException On I/O errors - */ - @Throws(IOException::class) - abstract fun setLastReadLocation(bookmark: Bookmark.PDFBookmark?) - - /** - * Set the bookmarks for the book. - * - * @param bookmarks The list of bookmarks - * - * @throws IOException On I/O errors - */ - - @Throws(IOException::class) - abstract fun setBookmarks(bookmarks: List) } /** @@ -302,27 +289,5 @@ sealed class BookDatabaseEntryFormatHandle { @Throws(IOException::class) abstract fun moveInBook(file: File) - - /** - * Set the last read location for the book. - * - * @param bookmark The location - * - * @throws IOException On I/O errors - */ - - @Throws(IOException::class) - abstract fun setLastReadLocation(bookmark: Bookmark.AudiobookBookmark?) - - /** - * Set the bookmarks for the book. - * - * @param bookmarks The list of bookmarks - * - * @throws IOException On I/O errors - */ - - @Throws(IOException::class) - abstract fun setBookmarks(bookmarks: List) } } diff --git a/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/DatabaseFormatHandleAudioBook.kt b/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/DatabaseFormatHandleAudioBook.kt index 83c64c24b..9a5f48c4b 100644 --- a/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/DatabaseFormatHandleAudioBook.kt +++ b/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/DatabaseFormatHandleAudioBook.kt @@ -6,18 +6,18 @@ import net.jcip.annotations.GuardedBy import one.irradia.mime.api.MIMEType import org.librarysimplified.audiobook.api.PlayerAudioEngineRequest import org.librarysimplified.audiobook.api.PlayerAudioEngines -import org.librarysimplified.audiobook.api.PlayerPositions import org.librarysimplified.audiobook.api.PlayerResult import org.librarysimplified.audiobook.api.PlayerUserAgent import org.librarysimplified.audiobook.manifest.api.PlayerManifest import org.librarysimplified.audiobook.manifest_parser.api.ManifestParsers import org.librarysimplified.audiobook.parser.api.ParseResult -import org.nypl.simplified.books.api.BookDRMKind import org.nypl.simplified.books.api.BookDRMInformation +import org.nypl.simplified.books.api.BookDRMKind import org.nypl.simplified.books.api.BookFormat -import org.nypl.simplified.books.api.bookmark.Bookmark -import org.nypl.simplified.books.api.bookmark.BookmarkJSON +import org.nypl.simplified.books.api.bookmark.BookmarkID import org.nypl.simplified.books.api.bookmark.BookmarkKind +import org.nypl.simplified.books.api.bookmark.SerializedBookmark +import org.nypl.simplified.books.api.bookmark.SerializedBookmarks import org.nypl.simplified.books.book_database.api.BookDRMInformationHandle import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleAudioBook import org.nypl.simplified.files.DirectoryUtilities @@ -29,7 +29,6 @@ import org.slf4j.LoggerFactory import java.io.File import java.io.FileInputStream import java.io.IOException -import java.lang.IllegalStateException import java.net.URI /** @@ -238,6 +237,26 @@ internal class DatabaseFormatHandleAudioBook internal constructor( this.parameters.onUpdated.invoke(newFormat) } + override fun deleteBookmark(bookmarkId: BookmarkID) { + val newFormat = synchronized(this.dataLock) { + val serialized = this.formatRef.bookmarks.filter { bookmark -> + bookmark.bookmarkId != bookmarkId + } + + FileUtilities.fileWriteUTF8Atomically( + this.fileBookmarks, + this.fileBookmarksTmp, + JSONSerializerUtilities.serializeToString( + serialized.map { x -> x.toJSON(this.parameters.objectMapper) } + ) + ) + this.formatRef = this.formatRef.copy(bookmarks = serialized) + this.formatRef + } + + this.parameters.onUpdated.invoke(newFormat) + } + override fun copyInManifestAndURI( data: ByteArray, manifestURI: URI @@ -301,21 +320,7 @@ internal class DatabaseFormatHandleAudioBook internal constructor( this.parameters.onUpdated.invoke(newFormat) } - override fun setBookmarks(bookmarks: List) { - val newFormat = synchronized(this.dataLock) { - FileUtilities.fileWriteUTF8Atomically( - this.fileBookmarks, - this.fileBookmarksTmp, - BookmarkJSON.serializeAudiobookBookmarksToString(this.parameters.objectMapper, bookmarks) - ) - this.formatRef = this.formatRef.copy(bookmarks = bookmarks) - this.formatRef - } - - this.parameters.onUpdated.invoke(newFormat) - } - - override fun setLastReadLocation(bookmark: Bookmark.AudiobookBookmark?) { + override fun setLastReadLocation(bookmark: SerializedBookmark?) { val newFormat = synchronized(this.dataLock) { if (bookmark != null) { Preconditions.checkArgument( @@ -326,9 +331,7 @@ internal class DatabaseFormatHandleAudioBook internal constructor( FileUtilities.fileWriteUTF8Atomically( this.filePosition, this.filePositionTmp, - JSONSerializerUtilities.serializeToString( - PlayerPositions.serializeToObjectNode(bookmark.location) - ) + JSONSerializerUtilities.serializeToString(bookmark.toJSON(this.parameters.objectMapper)) ) } else { FileUtilities.fileDelete(this.filePosition) @@ -341,6 +344,29 @@ internal class DatabaseFormatHandleAudioBook internal constructor( this.parameters.onUpdated.invoke(newFormat) } + override fun addBookmark( + bookmark: SerializedBookmark + ) { + val newFormat = synchronized(this.dataLock) { + val newBookmarks = arrayListOf() + newBookmarks.addAll(this.formatRef.bookmarks) + newBookmarks.removeIf { b -> b.bookmarkId == bookmark.bookmarkId } + newBookmarks.add(bookmark) + + FileUtilities.fileWriteUTF8Atomically( + this.fileBookmarks, + this.fileBookmarksTmp, + JSONSerializerUtilities.serializeToString( + newBookmarks.map { x -> x.toJSON(this.parameters.objectMapper) } + ) + ) + this.formatRef = this.formatRef.copy(bookmarks = newBookmarks) + this.formatRef + } + + this.parameters.onUpdated.invoke(newFormat) + } + companion object { private val logger = @@ -361,8 +387,8 @@ internal class DatabaseFormatHandleAudioBook internal constructor( manifest = this.loadManifestIfNecessary(fileManifest, fileManifestURI), contentType = contentType, drmInformation = drmInfo, - bookmarks = loadBookmarksIfPresent(objectMapper, fileBookmarks), - lastReadLocation = loadLastReadLocationIfPresent(objectMapper, filePosition), + bookmarks = this.loadBookmarksIfPresent(objectMapper, fileBookmarks), + lastReadLocation = this.loadLastReadLocationIfPresent(filePosition), ) } @@ -370,9 +396,9 @@ internal class DatabaseFormatHandleAudioBook internal constructor( private fun loadBookmarksIfPresent( objectMapper: ObjectMapper, fileBookmarks: File - ): List { + ): List { return if (fileBookmarks.isFile) { - loadBookmarks( + this.loadBookmarks( objectMapper = objectMapper, fileBookmarks = fileBookmarks ) @@ -384,41 +410,33 @@ internal class DatabaseFormatHandleAudioBook internal constructor( private fun loadBookmarks( objectMapper: ObjectMapper, fileBookmarks: File - ): List { - val tree = objectMapper.readTree(fileBookmarks) - val array = JSONParserUtilities.checkArray(null, tree) - - val bookmarks = arrayListOf() + ): List { + val tree = + objectMapper.readTree(fileBookmarks) + val array = + JSONParserUtilities.checkArray(null, tree) + val bookmarks = + arrayListOf() array.forEach { node -> try { - val bookmark = BookmarkJSON.deserializeAudiobookBookmarkFromJSON( - kind = BookmarkKind.BookmarkExplicit, - node = node - ) - - bookmarks.add(bookmark) + bookmarks.add(SerializedBookmarks.parseBookmark(node)) } catch (exception: JSONParseException) { - this.logger.debug("Failed to parse an audiobook bookmark from the bookmarks file") + this.logger.debug("Failed to parse bookmark: ", exception) } } - return bookmarks } @Throws(IOException::class) private fun loadLastReadLocationIfPresent( - objectMapper: ObjectMapper, fileLastRead: File - ): Bookmark.AudiobookBookmark? { + ): SerializedBookmark? { return if (fileLastRead.isFile) { try { - loadLastReadLocation( - objectMapper = objectMapper, - fileLastRead = fileLastRead - ) + this.loadLastReadLocation(fileLastRead = fileLastRead) } catch (e: Exception) { - logger.error("failed to read the last-read location: ", e) + this.logger.error("Failed to read the last-read location: ", e) null } } else { @@ -428,15 +446,9 @@ internal class DatabaseFormatHandleAudioBook internal constructor( @Throws(IOException::class) private fun loadLastReadLocation( - objectMapper: ObjectMapper, fileLastRead: File - ): Bookmark.AudiobookBookmark { - val serialized = FileUtilities.fileReadUTF8(fileLastRead) - return BookmarkJSON.deserializeAudiobookBookmarkFromString( - objectMapper = objectMapper, - kind = BookmarkKind.BookmarkLastReadLocation, - serialized = serialized - ) + ): SerializedBookmark { + return SerializedBookmarks.parseBookmarkFromString(FileUtilities.fileReadUTF8(fileLastRead)) } private fun loadManifestIfNecessary( diff --git a/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/DatabaseFormatHandleEPUB.kt b/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/DatabaseFormatHandleEPUB.kt index b4316e78b..7356d8a16 100644 --- a/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/DatabaseFormatHandleEPUB.kt +++ b/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/DatabaseFormatHandleEPUB.kt @@ -7,15 +7,17 @@ import one.irradia.mime.api.MIMEType import org.nypl.simplified.books.api.BookDRMInformation import org.nypl.simplified.books.api.BookDRMKind import org.nypl.simplified.books.api.BookFormat -import org.nypl.simplified.books.api.bookmark.Bookmark -import org.nypl.simplified.books.api.bookmark.BookmarkJSON +import org.nypl.simplified.books.api.bookmark.BookmarkID import org.nypl.simplified.books.api.bookmark.BookmarkKind +import org.nypl.simplified.books.api.bookmark.SerializedBookmark +import org.nypl.simplified.books.api.bookmark.SerializedBookmarks import org.nypl.simplified.books.book_database.api.BookDRMInformationHandle import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleEPUB import org.nypl.simplified.files.DirectoryUtilities import org.nypl.simplified.files.FileUtilities import org.nypl.simplified.json.core.JSONParseException import org.nypl.simplified.json.core.JSONParserUtilities +import org.nypl.simplified.json.core.JSONSerializerUtilities import org.slf4j.LoggerFactory import java.io.File import java.io.IOException @@ -136,6 +138,26 @@ internal class DatabaseFormatHandleEPUB internal constructor( this.parameters.onUpdated.invoke(newFormat) } + override fun deleteBookmark(bookmarkId: BookmarkID) { + val newFormat = synchronized(this.dataLock) { + val serialized = this.formatRef.bookmarks.filter { bookmark -> + bookmark.bookmarkId != bookmarkId + } + + FileUtilities.fileWriteUTF8Atomically( + this.fileBookmarks, + this.fileBookmarksTmp, + JSONSerializerUtilities.serializeToString( + serialized.map { x -> x.toJSON(this.parameters.objectMapper) } + ) + ) + this.formatRef = this.formatRef.copy(bookmarks = serialized) + this.formatRef + } + + this.parameters.onUpdated.invoke(newFormat) + } + override fun copyInBook(file: File) { val newFormat = synchronized(this.dataLock) { if (file.isDirectory) { @@ -151,7 +173,7 @@ internal class DatabaseFormatHandleEPUB internal constructor( this.parameters.onUpdated.invoke(newFormat) } - override fun setLastReadLocation(bookmark: Bookmark.ReaderBookmark?) { + override fun setLastReadLocation(bookmark: SerializedBookmark?) { val newFormat = synchronized(this.dataLock) { if (bookmark != null) { Preconditions.checkArgument( @@ -162,7 +184,7 @@ internal class DatabaseFormatHandleEPUB internal constructor( FileUtilities.fileWriteUTF8Atomically( this.fileLastRead, this.fileLastReadTmp, - BookmarkJSON.serializeReaderBookmarkToString(this.parameters.objectMapper, bookmark) + JSONSerializerUtilities.serializeToString(bookmark.toJSON(this.parameters.objectMapper)) ) } else { FileUtilities.fileDelete(this.fileLastRead) @@ -175,14 +197,23 @@ internal class DatabaseFormatHandleEPUB internal constructor( this.parameters.onUpdated.invoke(newFormat) } - override fun setBookmarks(bookmarks: List) { + override fun addBookmark( + bookmark: SerializedBookmark + ) { val newFormat = synchronized(this.dataLock) { + val newBookmarks = arrayListOf() + newBookmarks.addAll(this.formatRef.bookmarks) + newBookmarks.removeIf { b -> b.bookmarkId == bookmark.bookmarkId } + newBookmarks.add(bookmark) + FileUtilities.fileWriteUTF8Atomically( this.fileBookmarks, this.fileBookmarksTmp, - BookmarkJSON.serializeReaderBookmarksToString(this.parameters.objectMapper, bookmarks) + JSONSerializerUtilities.serializeToString( + newBookmarks.map { x -> x.toJSON(this.parameters.objectMapper) } + ) ) - this.formatRef = this.formatRef.copy(bookmarks = bookmarks) + this.formatRef = this.formatRef.copy(bookmarks = newBookmarks) this.formatRef } @@ -204,9 +235,9 @@ internal class DatabaseFormatHandleEPUB internal constructor( drmInfo: BookDRMInformation ): BookFormat.BookFormatEPUB { return BookFormat.BookFormatEPUB( - bookmarks = loadBookmarksIfPresent(objectMapper, fileBookmarks), + bookmarks = this.loadBookmarksIfPresent(objectMapper, fileBookmarks), file = if (fileBook.exists()) fileBook else null, - lastReadLocation = loadLastReadLocationIfPresent(objectMapper, fileLastRead), + lastReadLocation = this.loadLastReadLocationIfPresent(fileLastRead), contentType = contentType, drmInformation = drmInfo ) @@ -216,9 +247,9 @@ internal class DatabaseFormatHandleEPUB internal constructor( private fun loadBookmarksIfPresent( objectMapper: ObjectMapper, fileBookmarks: File - ): List { + ): List { return if (fileBookmarks.isFile) { - loadBookmarks( + this.loadBookmarks( objectMapper = objectMapper, fileBookmarks = fileBookmarks ) @@ -230,40 +261,33 @@ internal class DatabaseFormatHandleEPUB internal constructor( private fun loadBookmarks( objectMapper: ObjectMapper, fileBookmarks: File - ): List { - val tree = objectMapper.readTree(fileBookmarks) - val array = JSONParserUtilities.checkArray(null, tree) - - val bookmarks = arrayListOf() + ): List { + val tree = + objectMapper.readTree(fileBookmarks) + val array = + JSONParserUtilities.checkArray(null, tree) + val bookmarks = + arrayListOf() array.forEach { node -> try { - val bookmark = BookmarkJSON.deserializeReaderBookmarkFromJSON( - kind = BookmarkKind.BookmarkExplicit, - node = node - ) - bookmarks.add(bookmark) + bookmarks.add(SerializedBookmarks.parseBookmark(node)) } catch (exception: JSONParseException) { - this.logger.debug("There was an error parsing the reader bookmark from bookmarks file") + this.logger.debug("Failed to parse bookmark: ", exception) } } - return bookmarks } @Throws(IOException::class) private fun loadLastReadLocationIfPresent( - objectMapper: ObjectMapper, fileLastRead: File - ): Bookmark.ReaderBookmark? { + ): SerializedBookmark? { return if (fileLastRead.isFile) { try { - loadLastReadLocation( - objectMapper = objectMapper, - fileLastRead = fileLastRead - ) + this.loadLastReadLocation(fileLastRead = fileLastRead) } catch (e: Exception) { - logger.error("failed to read the last-read location: ", e) + this.logger.error("Failed to read the last-read location: ", e) null } } else { @@ -273,15 +297,9 @@ internal class DatabaseFormatHandleEPUB internal constructor( @Throws(IOException::class) private fun loadLastReadLocation( - objectMapper: ObjectMapper, fileLastRead: File - ): Bookmark.ReaderBookmark { - val serialized = FileUtilities.fileReadUTF8(fileLastRead) - return BookmarkJSON.deserializeReaderBookmarkFromString( - objectMapper = objectMapper, - kind = BookmarkKind.BookmarkLastReadLocation, - serialized = serialized - ) + ): SerializedBookmark { + return SerializedBookmarks.parseBookmarkFromString(FileUtilities.fileReadUTF8(fileLastRead)) } } } diff --git a/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/DatabaseFormatHandlePDF.kt b/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/DatabaseFormatHandlePDF.kt index c42226ea6..8b40ba3fd 100644 --- a/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/DatabaseFormatHandlePDF.kt +++ b/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/DatabaseFormatHandlePDF.kt @@ -3,23 +3,21 @@ package org.nypl.simplified.books.book_database import com.fasterxml.jackson.databind.ObjectMapper import net.jcip.annotations.GuardedBy import one.irradia.mime.api.MIMEType -import org.joda.time.DateTime import org.nypl.simplified.books.api.BookDRMInformation import org.nypl.simplified.books.api.BookDRMKind import org.nypl.simplified.books.api.BookFormat -import org.nypl.simplified.books.api.bookmark.Bookmark -import org.nypl.simplified.books.api.bookmark.BookmarkJSON -import org.nypl.simplified.books.api.bookmark.BookmarkKind +import org.nypl.simplified.books.api.bookmark.BookmarkID +import org.nypl.simplified.books.api.bookmark.SerializedBookmark +import org.nypl.simplified.books.api.bookmark.SerializedBookmarks import org.nypl.simplified.books.book_database.api.BookDRMInformationHandle import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandlePDF import org.nypl.simplified.files.FileUtilities import org.nypl.simplified.json.core.JSONParseException import org.nypl.simplified.json.core.JSONParserUtilities +import org.nypl.simplified.json.core.JSONSerializerUtilities import org.slf4j.LoggerFactory import java.io.File import java.io.IOException -import java.lang.IllegalStateException -import java.lang.NumberFormatException /** * Operations on PDF formats in database entries. @@ -129,6 +127,26 @@ internal class DatabaseFormatHandlePDF internal constructor( this.parameters.onUpdated.invoke(newFormat) } + override fun deleteBookmark(bookmarkId: BookmarkID) { + val newFormat = synchronized(this.dataLock) { + val serialized = this.formatRef.bookmarks.filter { + bookmark -> bookmark.bookmarkId != bookmarkId + } + + FileUtilities.fileWriteUTF8Atomically( + this.fileBookmarks, + this.fileBookmarksTmp, + JSONSerializerUtilities.serializeToString( + serialized.map { x -> x.toJSON(this.parameters.objectMapper) } + ) + ) + this.formatRef = this.formatRef.copy(bookmarks = serialized) + this.formatRef + } + + this.parameters.onUpdated.invoke(newFormat) + } + override fun copyInBook(file: File) { val newFormat = synchronized(this.dataLock) { FileUtilities.fileCopy(file, this.fileBook) @@ -139,13 +157,13 @@ internal class DatabaseFormatHandlePDF internal constructor( this.parameters.onUpdated.invoke(newFormat) } - override fun setLastReadLocation(bookmark: Bookmark.PDFBookmark?) { + override fun setLastReadLocation(bookmark: SerializedBookmark?) { val newFormat = synchronized(this.dataLock) { if (bookmark != null) { FileUtilities.fileWriteUTF8Atomically( this.fileLastRead, this.fileLastReadTmp, - BookmarkJSON.serializePdfBookmarkToString(this.parameters.objectMapper, bookmark) + JSONSerializerUtilities.serializeToString(bookmark.toJSON(this.parameters.objectMapper)) ) } else { FileUtilities.fileDelete(this.fileLastRead) @@ -158,14 +176,23 @@ internal class DatabaseFormatHandlePDF internal constructor( this.parameters.onUpdated.invoke(newFormat) } - override fun setBookmarks(bookmarks: List) { + override fun addBookmark( + bookmark: SerializedBookmark + ) { val newFormat = synchronized(this.dataLock) { + val newBookmarks = arrayListOf() + newBookmarks.addAll(this.formatRef.bookmarks) + newBookmarks.removeIf { b -> b.bookmarkId == bookmark.bookmarkId } + newBookmarks.add(bookmark) + FileUtilities.fileWriteUTF8Atomically( this.fileBookmarks, this.fileBookmarksTmp, - BookmarkJSON.serializePdfBookmarksToString(this.parameters.objectMapper, bookmarks) + JSONSerializerUtilities.serializeToString( + newBookmarks.map { x -> x.toJSON(this.parameters.objectMapper) } + ) ) - this.formatRef = this.formatRef.copy(bookmarks = bookmarks) + this.formatRef = this.formatRef.copy(bookmarks = newBookmarks) this.formatRef } @@ -189,7 +216,7 @@ internal class DatabaseFormatHandlePDF internal constructor( return BookFormat.BookFormatPDF( bookmarks = loadBookmarksIfPresent(objectMapper, fileBookmarks), file = if (fileBook.isFile) fileBook else null, - lastReadLocation = loadLastReadLocationIfPresent(objectMapper, fileLastRead), + lastReadLocation = loadLastReadLocationIfPresent(fileLastRead), contentType = contentType, drmInformation = drmInfo ) @@ -199,7 +226,7 @@ internal class DatabaseFormatHandlePDF internal constructor( private fun loadBookmarksIfPresent( objectMapper: ObjectMapper, fileBookmarks: File - ): List { + ): List { return if (fileBookmarks.isFile) { loadBookmarks( objectMapper = objectMapper, @@ -213,59 +240,42 @@ internal class DatabaseFormatHandlePDF internal constructor( private fun loadBookmarks( objectMapper: ObjectMapper, fileBookmarks: File - ): List { - val tree = objectMapper.readTree(fileBookmarks) - val array = JSONParserUtilities.checkArray(null, tree) - - val bookmarks = arrayListOf() + ): List { + val tree = + objectMapper.readTree(fileBookmarks) + val array = + JSONParserUtilities.checkArray(null, tree) + val bookmarks = + arrayListOf() array.forEach { node -> try { - val bookmark = BookmarkJSON.deserializePdfBookmarkFromJSON( - kind = BookmarkKind.BookmarkExplicit, - node = node - ) - bookmarks.add(bookmark) - } catch (exception: JSONParseException) { - this.logger.debug("There was an error parsing the pdf bookmark from bookmarks file") + bookmarks.add(SerializedBookmarks.parseBookmark(node)) + } catch (e: JSONParseException) { + this.logger.debug("Failed to parse bookmark: ", e) } } - return bookmarks } @Throws(IOException::class) private fun loadLastReadLocation( - objectMapper: ObjectMapper, fileLastRead: File - ): Bookmark.PDFBookmark { + ): SerializedBookmark? { val serialized = FileUtilities.fileReadUTF8(fileLastRead) return try { - Bookmark.PDFBookmark.create( - opdsId = "", - kind = BookmarkKind.BookmarkLastReadLocation, - time = DateTime.now(), - pageNumber = serialized.toInt(), - deviceID = "", - uri = null - ) + SerializedBookmarks.parseBookmarkFromString(serialized) } catch (exception: NumberFormatException) { - this.logger.debug("The stored bookmark is not from the older version") - BookmarkJSON.deserializePdfBookmarkFromString( - objectMapper = objectMapper, - kind = BookmarkKind.BookmarkLastReadLocation, - serialized = serialized - ) + null } } @Throws(IOException::class) private fun loadLastReadLocationIfPresent( - objectMapper: ObjectMapper, fileLastRead: File - ): Bookmark.PDFBookmark? { + ): SerializedBookmark? { return if (fileLastRead.isFile) { - loadLastReadLocation(objectMapper, fileLastRead) + loadLastReadLocation(fileLastRead) } else { null } diff --git a/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/NullDownloadProvider.kt b/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/NullDownloadProvider.kt index 392f54fc1..85b3568cd 100644 --- a/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/NullDownloadProvider.kt +++ b/simplified-books-database/src/main/java/org/nypl/simplified/books/book_database/NullDownloadProvider.kt @@ -1,16 +1,17 @@ package org.nypl.simplified.books.book_database -import com.google.common.util.concurrent.Futures -import com.google.common.util.concurrent.ListenableFuture import org.librarysimplified.audiobook.api.PlayerDownloadProviderType import org.librarysimplified.audiobook.api.PlayerDownloadRequest +import java.util.concurrent.CompletableFuture /** * A download provider that does nothing. */ internal class NullDownloadProvider : PlayerDownloadProviderType { - override fun download(request: PlayerDownloadRequest): ListenableFuture { - return Futures.immediateFailedFuture(UnsupportedOperationException()) + override fun download(request: PlayerDownloadRequest): CompletableFuture { + val future = CompletableFuture() + future.completeExceptionally(UnsupportedOperationException()) + return future } } diff --git a/simplified-books-registry-api/src/main/java/org/nypl/simplified/books/book_registry/BookRegistryReadableType.kt b/simplified-books-registry-api/src/main/java/org/nypl/simplified/books/book_registry/BookRegistryReadableType.kt index 87bc10b05..5621e4fb0 100644 --- a/simplified-books-registry-api/src/main/java/org/nypl/simplified/books/book_registry/BookRegistryReadableType.kt +++ b/simplified-books-registry-api/src/main/java/org/nypl/simplified/books/book_registry/BookRegistryReadableType.kt @@ -5,10 +5,7 @@ import com.io7m.jfunctional.OptionType import com.io7m.jfunctional.OptionVisitorType import com.io7m.jfunctional.Some import io.reactivex.Observable - import org.nypl.simplified.books.api.BookID - -import java.util.NoSuchElementException import java.util.SortedMap /** diff --git a/simplified-books-time-tracking/src/main/java/org/nypl/simplified/books/time/tracking/TimeTrackingService.kt b/simplified-books-time-tracking/src/main/java/org/nypl/simplified/books/time/tracking/TimeTrackingService.kt index ee4ca93cf..632ccce16 100644 --- a/simplified-books-time-tracking/src/main/java/org/nypl/simplified/books/time/tracking/TimeTrackingService.kt +++ b/simplified-books-time-tracking/src/main/java/org/nypl/simplified/books/time/tracking/TimeTrackingService.kt @@ -14,8 +14,8 @@ import org.nypl.simplified.profiles.controller.api.ProfilesControllerType import org.slf4j.LoggerFactory import java.io.File import java.net.URI -import java.util.concurrent.TimeUnit import java.util.UUID +import java.util.concurrent.TimeUnit import kotlin.math.min class TimeTrackingService( @@ -136,8 +136,8 @@ class TimeTrackingService( } when (playerEvent) { - is PlayerEvent.PlayerEventWithSpineElement.PlayerEventPlaybackProgressUpdate, - is PlayerEvent.PlayerEventWithSpineElement.PlayerEventPlaybackStarted -> { + is PlayerEvent.PlayerEventWithPosition.PlayerEventPlaybackProgressUpdate, + is PlayerEvent.PlayerEventWithPosition.PlayerEventPlaybackStarted -> { isPlaying = true if (audiobookPlayingDisposable == null) { createTimeTrackingEntry() @@ -145,20 +145,31 @@ class TimeTrackingService( } } - is PlayerEvent.PlayerEventWithSpineElement.PlayerEventPlaybackBuffering, - is PlayerEvent.PlayerEventWithSpineElement.PlayerEventPlaybackWaitingForAction, - is PlayerEvent.PlayerEventWithSpineElement.PlayerEventChapterWaiting, - is PlayerEvent.PlayerEventWithSpineElement.PlayerEventPlaybackPaused, - is PlayerEvent.PlayerEventWithSpineElement.PlayerEventPlaybackStopped, - is PlayerEvent.PlayerEventWithSpineElement.PlayerEventChapterCompleted -> { + is PlayerEvent.PlayerEventWithPosition.PlayerEventPlaybackBuffering, + is PlayerEvent.PlayerEventWithPosition.PlayerEventPlaybackWaitingForAction, + is PlayerEvent.PlayerEventWithPosition.PlayerEventChapterWaiting, + is PlayerEvent.PlayerEventWithPosition.PlayerEventPlaybackPaused, + is PlayerEvent.PlayerEventWithPosition.PlayerEventPlaybackStopped, + is PlayerEvent.PlayerEventWithPosition.PlayerEventChapterCompleted -> { isPlaying = false } - is PlayerEvent.PlayerEventWithSpineElement.PlayerEventCreateBookmark, + is PlayerEvent.PlayerEventWithPosition.PlayerEventCreateBookmark, is PlayerEvent.PlayerEventPlaybackRateChanged, is PlayerEvent.PlayerEventError, PlayerEvent.PlayerEventManifestUpdated -> { // do nothing } + + is PlayerEvent.PlayerAccessibilityEvent.PlayerAccessibilityChapterSelected, + is PlayerEvent.PlayerAccessibilityEvent.PlayerAccessibilityErrorOccurred, + is PlayerEvent.PlayerAccessibilityEvent.PlayerAccessibilityIsBuffering, + is PlayerEvent.PlayerAccessibilityEvent.PlayerAccessibilityIsWaitingForChapter, + is PlayerEvent.PlayerAccessibilityEvent.PlayerAccessibilityPlaybackRateChanged, + is PlayerEvent.PlayerAccessibilityEvent.PlayerAccessibilitySleepTimerSettingChanged, + is PlayerEvent.PlayerEventDeleteBookmark, + is PlayerEvent.PlayerEventWithPosition.PlayerEventPlaybackPreparing -> { + // do nothing + } } } diff --git a/simplified-feeds-api/src/main/java/org/nypl/simplified/feeds/api/Feed.kt b/simplified-feeds-api/src/main/java/org/nypl/simplified/feeds/api/Feed.kt index 422113139..d2e7768c6 100644 --- a/simplified-feeds-api/src/main/java/org/nypl/simplified/feeds/api/Feed.kt +++ b/simplified-feeds-api/src/main/java/org/nypl/simplified/feeds/api/Feed.kt @@ -18,9 +18,7 @@ import org.nypl.simplified.opds.core.OPDSAcquisitionFeed import org.nypl.simplified.opds.core.OPDSAcquisitionFeedEntry import org.nypl.simplified.opds.core.OPDSOpenSearch1_1 import java.net.URI -import java.util.ArrayList import java.util.Collections -import java.util.HashMap /** * The type of mutable feeds. diff --git a/simplified-files/src/main/java/org/nypl/simplified/files/FileLocking.java b/simplified-files/src/main/java/org/nypl/simplified/files/FileLocking.java index 7bc6f6814..295958adc 100644 --- a/simplified-files/src/main/java/org/nypl/simplified/files/FileLocking.java +++ b/simplified-files/src/main/java/org/nypl/simplified/files/FileLocking.java @@ -4,6 +4,7 @@ import com.io7m.jfunctional.Unit; import com.io7m.jnull.NullCheck; import com.io7m.junreachable.UnreachableCodeException; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; diff --git a/simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONParserUtilities.java b/simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONParserUtilities.java deleted file mode 100644 index 8ca58893b..000000000 --- a/simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONParserUtilities.java +++ /dev/null @@ -1,938 +0,0 @@ -package org.nypl.simplified.json.core; - -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.io7m.jfunctional.None; -import com.io7m.jfunctional.Option; -import com.io7m.jfunctional.OptionType; -import com.io7m.jfunctional.OptionVisitorType; -import com.io7m.jfunctional.Some; -import com.io7m.jnull.NullCheck; -import com.io7m.jnull.Nullable; -import com.io7m.junreachable.UnreachableCodeException; - -import org.joda.time.DateTime; -import org.joda.time.format.ISODateTimeFormat; - -import java.math.BigInteger; -import java.net.URI; -import java.net.URISyntaxException; -import java.util.Objects; - -/** - *

Utility functions for deserializing elements from JSON.

- *

- *

The functions take a strict approach: Types are checked upon key retrieval - * and exceptions are raised if the type is not exactly as expected.

- */ - -public final class JSONParserUtilities { - private JSONParserUtilities() { - throw new UnreachableCodeException(); - } - - /** - * Check that {@code n} is an object. - * - * @param key An optional advisory key to be used in error messages - * @param n A node - * @return {@code n} as an {@link ObjectNode} - * @throws JSONParseException On type errors - */ - - public static ObjectNode checkObject( - final @Nullable String key, - final JsonNode n) - throws JSONParseException { - - NullCheck.notNull(n); - - switch (n.getNodeType()) { - case ARRAY: - case BINARY: - case BOOLEAN: - case MISSING: - case NULL: - case NUMBER: - case POJO: - case STRING: { - final StringBuilder sb = new StringBuilder(128); - if (key != null) { - sb.append("Expected: A key '"); - sb.append(key); - sb.append("' with a value of type Object\n"); - sb.append("Got: A value of type "); - sb.append(n.getNodeType()); - sb.append("\n"); - } else { - sb.append("Expected: A value of type Object\n"); - sb.append("Got: A value of type "); - sb.append(n.getNodeType()); - sb.append("\n"); - } - - final String m = NullCheck.notNull(sb.toString()); - throw new JSONParseException(m); - } - case OBJECT: { - return (ObjectNode) n; - } - } - - throw new UnreachableCodeException(); - } - - /** - * Check that {@code n} is an array. - * - * @param key An optional advisory key to be used in error messages - * @param n A node - * @return {@code n} as an {@link ObjectNode} - * @throws JSONParseException On type errors - */ - - public static ArrayNode checkArray( - final @Nullable String key, - final JsonNode n) - throws JSONParseException { - - NullCheck.notNull(n); - - switch (n.getNodeType()) { - case ARRAY: { - return (ArrayNode) n; - } - case BINARY: - case BOOLEAN: - case MISSING: - case NULL: - case NUMBER: - case POJO: - case OBJECT: - case STRING: { - final StringBuilder sb = new StringBuilder(128); - if (key != null) { - sb.append("Expected: A key '"); - sb.append(key); - sb.append("' with a value of type Object\n"); - sb.append("Got: A value of type "); - sb.append(n.getNodeType()); - sb.append("\n"); - } else { - sb.append("Expected: A value of type Object\n"); - sb.append("Got: A value of type "); - sb.append(n.getNodeType()); - sb.append("\n"); - } - - final String m = NullCheck.notNull(sb.toString()); - throw new JSONParseException(m); - } - } - - throw new UnreachableCodeException(); - } - - /** - * Check that {@code n} is a string. - * - * @param n A node - * @return {@code n} as a String - * @throws JSONParseException On type errors - */ - - public static String checkString( - final JsonNode n) - throws JSONParseException { - - NullCheck.notNull(n); - - switch (n.getNodeType()) { - case STRING: { - return n.asText(); - } - case ARRAY: - case BINARY: - case BOOLEAN: - case MISSING: - case NULL: - case NUMBER: - case POJO: - case OBJECT: { - final StringBuilder sb = new StringBuilder(128); - sb.append("Expected: A value of type String\n"); - sb.append("Got: A value of type "); - sb.append(n.getNodeType()); - sb.append("\n"); - throw new JSONParseException(NullCheck.notNull(sb.toString())); - } - } - - throw new UnreachableCodeException(); - } - - /** - * @param key A key assumed to be holding a value - * @param s A node - * @return An array from key {@code key} - * @throws JSONParseException On type errors - */ - - public static ArrayNode getArray( - final ObjectNode s, - final String key) - throws JSONParseException { - - NullCheck.notNull(s); - NullCheck.notNull(key); - - final JsonNode n = JSONParserUtilities.getNode(s, key); - switch (n.getNodeType()) { - case ARRAY: { - return (ArrayNode) n; - } - case BINARY: - case BOOLEAN: - case MISSING: - case NULL: - case NUMBER: - case POJO: - case STRING: - case OBJECT: { - final StringBuilder sb = new StringBuilder(128); - sb.append("Expected: A key '"); - sb.append(key); - sb.append("' with a value of type Array\n"); - sb.append("Got: A value of type "); - sb.append(n.getNodeType()); - sb.append("\n"); - final String m = NullCheck.notNull(sb.toString()); - throw new JSONParseException(m); - } - } - - throw new UnreachableCodeException(); - } - - /** - * @param key A key assumed to be holding a value - * @param s A node - * @return An array from key {@code key}, or null if the key is not present - * @throws JSONParseException On type errors - */ - - public static ArrayNode getArrayOrNull( - final ObjectNode s, - final String key) - throws JSONParseException { - - NullCheck.notNull(s); - NullCheck.notNull(key); - - if (s.has(key)) { - return getArray(s, key); - } - return null; - } - - /** - * @param key A key assumed to be holding a value - * @param o A node - * @return A boolean value from key {@code key} - * @throws JSONParseException On type errors - */ - - public static boolean getBoolean( - final ObjectNode o, - final String key) - throws JSONParseException { - - NullCheck.notNull(o); - NullCheck.notNull(key); - - final JsonNode v = JSONParserUtilities.getNode(o, key); - switch (v.getNodeType()) { - case ARRAY: - case BINARY: - case MISSING: - case NULL: - case OBJECT: - case POJO: - case STRING: - case NUMBER: { - final StringBuilder sb = new StringBuilder(128); - sb.append("Expected: A key '"); - sb.append(key); - sb.append("' with a value of type Boolean\n"); - sb.append("Got: A value of type "); - sb.append(v.getNodeType()); - sb.append("\n"); - final String m = NullCheck.notNull(sb.toString()); - throw new JSONParseException(m); - } - case BOOLEAN: { - return v.asBoolean(); - } - } - - throw new UnreachableCodeException(); - } - - /** - * @param key A key assumed to be holding a value - * @param n A node - * @return An integer value from key {@code key} - * @throws JSONParseException On type errors - */ - - public static int getInteger( - final ObjectNode n, - final String key) - throws JSONParseException { - - NullCheck.notNull(n); - NullCheck.notNull(key); - - final JsonNode v = JSONParserUtilities.getNode(n, key); - switch (v.getNodeType()) { - case ARRAY: - case BINARY: - case BOOLEAN: - case MISSING: - case NULL: - case OBJECT: - case POJO: - case STRING: { - final StringBuilder sb = new StringBuilder(128); - sb.append("Expected: A key '"); - sb.append(key); - sb.append("' with a value of type Integer\n"); - sb.append("Got: A value of type "); - sb.append(v.getNodeType()); - sb.append("\n"); - final String m = NullCheck.notNull(sb.toString()); - throw new JSONParseException(m); - } - case NUMBER: { - return v.asInt(); - } - } - - throw new UnreachableCodeException(); - } - - /** - * @param key A key assumed to be holding a value - * @param n A node - * @return A double value from key {@code key} - * @throws JSONParseException On type errors - */ - - public static double getDouble( - final ObjectNode n, - final String key) - throws JSONParseException { - - NullCheck.notNull(n); - NullCheck.notNull(key); - - final JsonNode v = JSONParserUtilities.getNode(n, key); - switch (v.getNodeType()) { - case ARRAY: - case BINARY: - case BOOLEAN: - case MISSING: - case NULL: - case OBJECT: - case POJO: - case STRING: { - final StringBuilder sb = new StringBuilder(128); - sb.append("Expected: A key '"); - sb.append(key); - sb.append("' with a value of type Double\n"); - sb.append("Got: A value of type "); - sb.append(v.getNodeType()); - sb.append("\n"); - final String m = NullCheck.notNull(sb.toString()); - throw new JSONParseException(m); - } - case NUMBER: { - return v.asDouble(); - } - } - - throw new UnreachableCodeException(); - } - - /** - * @param key A key assumed to be holding a value - * @param s A node - * @return An arbitrary json node from key {@code key} - * @throws JSONParseException On type errors - */ - - public static JsonNode getNode( - final ObjectNode s, - final String key) - throws JSONParseException { - - NullCheck.notNull(s); - NullCheck.notNull(key); - - if (s.has(key)) { - return NullCheck.notNull(s.get(key)); - } - - final StringBuilder sb = new StringBuilder(128); - sb.append("Expected: A key '"); - sb.append(key); - sb.append("'\n"); - sb.append("Got: nothing\n"); - final String m = NullCheck.notNull(sb.toString()); - throw new JSONParseException(m); - } - - /** - * @param key A key assumed to be holding a value - * @param s A node - * @return An object value from key {@code key} - * @throws JSONParseException On type errors - */ - - public static ObjectNode getObject( - final ObjectNode s, - final String key) - throws JSONParseException { - - NullCheck.notNull(s); - NullCheck.notNull(key); - - final JsonNode n = JSONParserUtilities.getNode(s, key); - return JSONParserUtilities.checkObject(key, n); - } - - /** - * @param key A key assumed to be holding a value - * @param s A node - * @return An object value from key {@code key}, if the key exists - * @throws JSONParseException On type errors - */ - - public static OptionType getObjectOptional( - final ObjectNode s, - final String key) - throws JSONParseException { - - NullCheck.notNull(s); - NullCheck.notNull(key); - - if (s.has(key)) { - return Option.some(JSONParserUtilities.getObject(s, key)); - } - return Option.none(); - } - - /** - * @param key A key assumed to be holding a value - * @param s A node - * @return An object value from key {@code key}, if the key exists - * @throws JSONParseException On type errors - */ - - public static ObjectNode getObjectOrNull( - final ObjectNode s, - final String key) - throws JSONParseException { - - NullCheck.notNull(s); - NullCheck.notNull(key); - - if (s.has(key)) { - return JSONParserUtilities.getObject(s, key); - } - return null; - } - - /** - * @param key A key assumed to be holding a value - * @param s A node - * @return A string value from key {@code key} - * @throws JSONParseException On type errors - */ - - public static String getString( - final ObjectNode s, - final String key) - throws JSONParseException { - - NullCheck.notNull(s); - NullCheck.notNull(key); - - final JsonNode v = JSONParserUtilities.getNode(s, key); - switch (v.getNodeType()) { - case ARRAY: - case BINARY: - case BOOLEAN: - case MISSING: - case NULL: - case NUMBER: - case OBJECT: - case POJO: { - final StringBuilder sb = new StringBuilder(128); - sb.append("Expected: A key '"); - sb.append(key); - sb.append("' with a value of type String\n"); - sb.append("Got: A value of type "); - sb.append(v.getNodeType()); - sb.append("\n"); - final String m = NullCheck.notNull(sb.toString()); - throw new JSONParseException(m); - } - case STRING: { - return NullCheck.notNull(v.asText()); - } - } - - throw new UnreachableCodeException(); - } - - /** - * @param key A key assumed to be holding a value - * @param n A node - * @return An integer value from key {@code key}, if the key exists - * @throws JSONParseException On type errors - */ - - public static OptionType getIntegerOptional( - final ObjectNode n, - final String key) - throws JSONParseException { - - NullCheck.notNull(n); - NullCheck.notNull(key); - - if (n.has(key)) { - return Option.some(JSONParserUtilities.getInteger(n, key)); - } - return Option.none(); - } - - /** - * @param key A key assumed to be holding a value - * @param n A node - * @return An integer value from key {@code key}, if the key exists - * @throws JSONParseException On type errors - */ - - public static Integer getIntegerOrNull( - final ObjectNode n, - final String key) - throws JSONParseException { - - NullCheck.notNull(n); - NullCheck.notNull(key); - - if (n.has(key)) { - return JSONParserUtilities.getInteger(n, key); - } - return null; - } - - /** - * @param key A key assumed to be holding a value - * @param n A node - * @return A string value from key {@code key}, if the key exists, or {@code default_value} otherwise. - * @throws JSONParseException On type errors - */ - - public static int getIntegerDefault( - final ObjectNode n, - final String key, - final int default_value) - throws JSONParseException { - - NullCheck.notNull(n); - NullCheck.notNull(key); - - return getIntegerOptional(n, key).accept( - new OptionVisitorType() { - @Override - public Integer none(final None none) { - return default_value; - } - - @Override - public Integer some(final Some some) { - return some.get(); - } - }).intValue(); - } - - /** - * @param key A key assumed to be holding a value - * @param n A node - * @return An double value from key {@code key}, if the key exists - * @throws JSONParseException On type errors - */ - - public static OptionType getDoubleOptional( - final ObjectNode n, - final String key) - throws JSONParseException { - - NullCheck.notNull(n); - NullCheck.notNull(key); - - if (n.has(key)) { - return Option.some(JSONParserUtilities.getDouble(n, key)); - } - return Option.none(); - } - - /** - * @param key A key assumed to be holding a value - * @param n A node - * @return An double value from key {@code key}, if the key exists, or {@code default_value} otherwise. - * @throws JSONParseException On type errors - */ - - public static double getDoubleDefault( - final ObjectNode n, - final String key, - final double default_value) - throws JSONParseException { - - NullCheck.notNull(n); - NullCheck.notNull(key); - - return getDoubleOptional(n, key).accept( - new OptionVisitorType() { - @Override - public Double none(final None none) { - return default_value; - } - - @Override - public Double some(final Some some) { - return some.get(); - } - }); - } - - /** - * @param key A key assumed to be holding a value - * @param n A node - * @return A string value from key {@code key}, if the key exists - * @throws JSONParseException On type errors - */ - - public static OptionType getStringOptional( - final ObjectNode n, - final String key) - throws JSONParseException { - - NullCheck.notNull(n); - NullCheck.notNull(key); - - if (n.has(key)) { - if (n.get(key).isNull()) { - return Option.none(); - } - return Option.some(JSONParserUtilities.getString(n, key)); - } - return Option.none(); - } - - /** - * @param key A key assumed to be holding a value - * @param n A node - * @return A string value from key {@code key}, if the key exists - * @throws JSONParseException On type errors - */ - - public static String getStringOrNull( - final ObjectNode n, - final String key) - throws JSONParseException { - - NullCheck.notNull(n); - NullCheck.notNull(key); - - if (n.has(key)) { - if (n.get(key).isNull()) { - return null; - } - return JSONParserUtilities.getString(n, key); - } - return null; - } - - /** - * @param key A key assumed to be holding a value - * @param n A node - * @return A string value from key {@code key}, if the key exists, or {@code default_value} otherwise. - * @throws JSONParseException On type errors - */ - - public static String getStringDefault( - final ObjectNode n, - final String key, - final String default_value) - throws JSONParseException { - - NullCheck.notNull(n); - NullCheck.notNull(key); - - return getStringOptional(n, key).accept( - new OptionVisitorType() { - @Override - public String none(final None none) { - return default_value; - } - - @Override - public String some(final Some some) { - return some.get(); - } - }); - } - - /** - * @param s A node - * @param key A key assumed to be holding a value - * @return A timestamp value from key {@code key} - * @throws JSONParseException On type errors - */ - - public static DateTime getTimestamp( - final ObjectNode s, - final String key) - throws JSONParseException { - - NullCheck.notNull(s); - NullCheck.notNull(key); - - try { - return ISODateTimeFormat.dateTimeParser() - .withZoneUTC() - .parseDateTime(JSONParserUtilities.getString(s, key)); - } catch (final IllegalArgumentException e) { - throw new JSONParseException( - String.format("Could not parse RFC3999 date for key '%s'", key), e); - } - } - - /** - * @param key A key assumed to be holding a value - * @param n A node - * @return A timestamp value from key {@code key}, if the key exists - * @throws JSONParseException On type errors - */ - - public static OptionType getTimestampOptional( - final ObjectNode n, - final String key) - throws JSONParseException { - - NullCheck.notNull(n); - NullCheck.notNull(key); - - if (n.has(key)) { - return Option.some(JSONParserUtilities.getTimestamp(n, key)); - } - return Option.none(); - } - - /** - * @param key A key assumed to be holding a value - * @param n A node - * @return A URI value from key {@code key}, if the key exists - * @throws JSONParseException On type errors - */ - - public static OptionType getURIOptional( - final ObjectNode n, - final String key) - throws JSONParseException { - - NullCheck.notNull(n); - NullCheck.notNull(key); - - return JSONParserUtilities.getStringOptional(n, key).mapPartial( - x -> { - try { - return new URI(x); - } catch (final URISyntaxException e) { - throw new JSONParseException(e); - } - }); - } - - /** - * @param key A key assumed to be holding a value - * @param n A node - * @return A URI value from key {@code key}, if the key exists - * @throws JSONParseException On type errors - */ - - public static URI getURIOrNull( - final ObjectNode n, - final String key) - throws JSONParseException { - - NullCheck.notNull(n); - NullCheck.notNull(key); - - OptionType opt = getURIOptional(n, key); - if (opt.isSome()) { - return ((Some) opt).get(); - } else { - return null; - } - } - - /** - * @param key A key assumed to be holding a value - * @param n A node - * @return A URI value from key {@code key} - * @throws JSONParseException On type errors - */ - - public static URI getURI( - final ObjectNode n, - final String key) - throws JSONParseException { - - NullCheck.notNull(n); - NullCheck.notNull(key); - - try { - return new URI(JSONParserUtilities.getString(n, key).trim()); - } catch (final URISyntaxException e) { - throw new JSONParseException(e); - } - } - - /** - * @param key A key assumed to be holding a value - * @param n A node - * @return A URI value from key {@code key} - * @throws JSONParseException On type errors - */ - - public static URI getURIDefault( - final ObjectNode n, - final String key, - final URI default_value) - throws JSONParseException { - - NullCheck.notNull(n); - NullCheck.notNull(key); - Objects.requireNonNull(default_value, "Default"); - - return getURIOptional(n, key).accept(new OptionVisitorType() { - @Override - public URI none(None n) { - return default_value; - } - - @Override - public URI some(Some s) { - return s.get(); - } - }); - } - - /** - * @param key A key assumed to be holding a value - * @param n A node - * @param v A default value - * @return A boolean from key {@code key}, or {@code v} if the key does not - * exist - * @throws JSONParseException On type errors - */ - - public static boolean getBooleanDefault( - final ObjectNode n, - final String key, - final boolean v) - throws JSONParseException { - - NullCheck.notNull(n); - NullCheck.notNull(key); - - if (n.has(key)) { - return JSONParserUtilities.getBoolean(n, key); - } - return v; - } - - /** - * @param key A key assumed to be holding a value - * @param n A node - * @return A big integer value from key {@code key}, if the key exists - * @throws JSONParseException On type errors - */ - - public static OptionType getBigIntegerOptional( - final ObjectNode n, - final String key) - throws JSONParseException { - - NullCheck.notNull(n); - NullCheck.notNull(key); - - if (n.has(key)) { - return Option.some(JSONParserUtilities.getBigInteger(n, key)); - } - return Option.none(); - } - - /** - * @param key A key assumed to be holding a value - * @param n A node - * @return A big integer value from key {@code key} - * @throws JSONParseException On type errors - */ - - public static BigInteger getBigInteger( - final ObjectNode n, - final String key) - throws JSONParseException { - - NullCheck.notNull(n); - NullCheck.notNull(key); - - final JsonNode v = JSONParserUtilities.getNode(n, key); - switch (v.getNodeType()) { - case ARRAY: - case BINARY: - case BOOLEAN: - case MISSING: - case NULL: - case OBJECT: - case POJO: - case STRING: { - final StringBuilder sb = new StringBuilder(128); - sb.append("Expected: A key '"); - sb.append(key); - sb.append("' with a value of type Integer\n"); - sb.append("Got: A value of type "); - sb.append(v.getNodeType()); - sb.append("\n"); - final String m = NullCheck.notNull(sb.toString()); - throw new JSONParseException(m); - } - case NUMBER: { - try { - return new BigInteger(v.asText()); - } catch (final NumberFormatException e) { - throw new JSONParseException(e); - } - } - } - - throw new UnreachableCodeException(); - } -} diff --git a/simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONParserUtilities.kt b/simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONParserUtilities.kt new file mode 100644 index 000000000..68733e854 --- /dev/null +++ b/simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONParserUtilities.kt @@ -0,0 +1,864 @@ +package org.nypl.simplified.json.core + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.node.ArrayNode +import com.fasterxml.jackson.databind.node.JsonNodeType +import com.fasterxml.jackson.databind.node.ObjectNode +import com.io7m.jfunctional.None +import com.io7m.jfunctional.Option +import com.io7m.jfunctional.OptionType +import com.io7m.jfunctional.OptionVisitorType +import com.io7m.jfunctional.Some +import org.joda.time.DateTime +import org.joda.time.format.ISODateTimeFormat +import java.math.BigInteger +import java.net.URI +import java.net.URISyntaxException +import java.util.Objects + +/** + * Utility functions for deserializing elements from JSON. + * + * The functions take a strict approach: Types are checked upon key retrieval + * and exceptions are raised if the type is not exactly as expected. + */ +object JSONParserUtilities { + + /** + * Check that `n` is an object. + * + * @param key An optional advisory key to be used in error messages + * @param n A node + * @return `n` as an [ObjectNode] + * @throws JSONParseException On type errors + */ + + @JvmStatic + @Throws(JSONParseException::class) + fun checkObject( + key: String?, + n: JsonNode + ): ObjectNode { + return when (n.nodeType) { + JsonNodeType.ARRAY, + JsonNodeType.BINARY, + JsonNodeType.BOOLEAN, + JsonNodeType.MISSING, + JsonNodeType.NULL, + JsonNodeType.NUMBER, + JsonNodeType.POJO, + JsonNodeType.STRING -> { + val sb = StringBuilder(128) + if (key != null) { + sb.append("Expected: A key '") + sb.append(key) + sb.append("' with a value of type Object\n") + sb.append("Got: A value of type ") + sb.append(n.nodeType) + sb.append("\n") + } else { + sb.append("Expected: A value of type Object\n") + sb.append("Got: A value of type ") + sb.append(n.nodeType) + sb.append("\n") + } + throw JSONParseException(sb.toString()) + } + + JsonNodeType.OBJECT -> { + n as ObjectNode + } + } + } + + /** + * Check that `n` is an array. + * + * @param key An optional advisory key to be used in error messages + * @param n A node + * @return `n` as an [ObjectNode] + * @throws JSONParseException On type errors + */ + + @JvmStatic + @Throws(JSONParseException::class) + fun checkArray( + key: String?, + n: JsonNode + ): ArrayNode { + return when (n.nodeType) { + JsonNodeType.ARRAY -> { + n as ArrayNode + } + + JsonNodeType.BINARY, + JsonNodeType.BOOLEAN, + JsonNodeType.MISSING, + JsonNodeType.NULL, + JsonNodeType.NUMBER, + JsonNodeType.POJO, + JsonNodeType.OBJECT, + JsonNodeType.STRING -> { + val sb = StringBuilder(128) + if (key != null) { + sb.append("Expected: A key '") + sb.append(key) + sb.append("' with a value of type Object\n") + sb.append("Got: A value of type ") + sb.append(n.nodeType) + sb.append("\n") + } else { + sb.append("Expected: A value of type Object\n") + sb.append("Got: A value of type ") + sb.append(n.nodeType) + sb.append("\n") + } + throw JSONParseException(sb.toString()) + } + } + } + + /** + * Check that `n` is a string. + * + * @param n A node + * @return `n` as a String + * @throws JSONParseException On type errors + */ + + @JvmStatic + @Throws(JSONParseException::class) + fun checkString( + n: JsonNode + ): String { + return when (n.nodeType) { + JsonNodeType.STRING -> { + n.asText() + } + + JsonNodeType.ARRAY, + JsonNodeType.BINARY, + JsonNodeType.BOOLEAN, + JsonNodeType.MISSING, + JsonNodeType.NULL, + JsonNodeType.NUMBER, + JsonNodeType.POJO, + JsonNodeType.OBJECT -> { + val sb = StringBuilder(128) + sb.append("Expected: A value of type String\n") + sb.append("Got: A value of type ") + sb.append(n.nodeType) + sb.append("\n") + throw JSONParseException(sb.toString()) + } + } + } + + /** + * @param key A key assumed to be holding a value + * @param s A node + * @return An array from key `key` + * @throws JSONParseException On type errors + */ + + @JvmStatic + @Throws(JSONParseException::class) + fun getArray( + s: ObjectNode, + key: String + ): ArrayNode { + val n = getNode(s, key) + return when (n.nodeType) { + JsonNodeType.ARRAY -> { + n as ArrayNode + } + + JsonNodeType.BINARY, + JsonNodeType.BOOLEAN, + JsonNodeType.MISSING, + JsonNodeType.NULL, + JsonNodeType.NUMBER, + JsonNodeType.POJO, + JsonNodeType.STRING, + JsonNodeType.OBJECT -> { + val sb = StringBuilder(128) + sb.append("Expected: A key '") + sb.append(key) + sb.append("' with a value of type Array\n") + sb.append("Got: A value of type ") + sb.append(n.nodeType) + sb.append("\n") + throw JSONParseException(sb.toString()) + } + } + } + + /** + * @param key A key assumed to be holding a value + * @param s A node + * @return An array from key `key`, or null if the key is not present + * @throws JSONParseException On type errors + */ + + @JvmStatic + @Throws(JSONParseException::class) + fun getArrayOrNull( + s: ObjectNode, + key: String + ): ArrayNode? { + return if (s.has(key)) { + getArray(s, key) + } else { + null + } + } + + /** + * @param key A key assumed to be holding a value + * @param o A node + * @return A boolean value from key `key` + * @throws JSONParseException On type errors + */ + + @JvmStatic + @Throws(JSONParseException::class) + fun getBoolean( + o: ObjectNode, + key: String + ): Boolean { + val v = getNode(o, key) + return when (v.nodeType) { + JsonNodeType.ARRAY, + JsonNodeType.BINARY, + JsonNodeType.MISSING, + JsonNodeType.NULL, + JsonNodeType.OBJECT, + JsonNodeType.POJO, + JsonNodeType.STRING, + JsonNodeType.NUMBER -> { + val sb = StringBuilder(128) + sb.append("Expected: A key '") + sb.append(key) + sb.append("' with a value of type Boolean\n") + sb.append("Got: A value of type ") + sb.append(v.nodeType) + sb.append("\n") + throw JSONParseException(sb.toString()) + } + + JsonNodeType.BOOLEAN -> { + v.asBoolean() + } + } + } + + /** + * @param key A key assumed to be holding a value + * @param n A node + * @return An integer value from key `key` + * @throws JSONParseException On type errors + */ + + @JvmStatic + @Throws(JSONParseException::class) + fun getInteger( + n: ObjectNode, + key: String + ): Int { + val v = getNode(n, key) + return when (v.nodeType) { + JsonNodeType.ARRAY, + JsonNodeType.BINARY, + JsonNodeType.BOOLEAN, + JsonNodeType.MISSING, + JsonNodeType.NULL, + JsonNodeType.OBJECT, + JsonNodeType.POJO, + JsonNodeType.STRING -> { + val sb = StringBuilder(128) + sb.append("Expected: A key '") + sb.append(key) + sb.append("' with a value of type Integer\n") + sb.append("Got: A value of type ") + sb.append(v.nodeType) + sb.append("\n") + throw JSONParseException(sb.toString()) + } + + JsonNodeType.NUMBER -> { + v.asInt() + } + } + } + + /** + * @param key A key assumed to be holding a value + * @param n A node + * @return A double value from key `key` + * @throws JSONParseException On type errors + */ + + @JvmStatic + @Throws(JSONParseException::class) + fun getDouble( + n: ObjectNode, + key: String + ): Double { + val v = getNode(n, key) + return when (v.nodeType) { + JsonNodeType.ARRAY, + JsonNodeType.BINARY, + JsonNodeType.BOOLEAN, + JsonNodeType.MISSING, + JsonNodeType.NULL, + JsonNodeType.OBJECT, + JsonNodeType.POJO, + JsonNodeType.STRING -> { + val sb = StringBuilder(128) + sb.append("Expected: A key '") + sb.append(key) + sb.append("' with a value of type Double\n") + sb.append("Got: A value of type ") + sb.append(v.nodeType) + sb.append("\n") + throw JSONParseException(sb.toString()) + } + + JsonNodeType.NUMBER -> { + v.asDouble() + } + } + } + + /** + * @param key A key assumed to be holding a value + * @param s A node + * @return An arbitrary json node from key `key` + * @throws JSONParseException On type errors + */ + + @JvmStatic + @Throws(JSONParseException::class) + fun getNode( + s: ObjectNode, + key: String + ): JsonNode { + if (s.has(key)) { + return s[key] + } + val sb = StringBuilder(128) + sb.append("Expected: A key '") + sb.append(key) + sb.append("'\n") + sb.append("Got: nothing\n") + throw JSONParseException(sb.toString()) + } + + /** + * @param key A key assumed to be holding a value + * @param s A node + * @return An object value from key `key` + * @throws JSONParseException On type errors + */ + + @JvmStatic + @Throws(JSONParseException::class) + fun getObject( + s: ObjectNode, + key: String + ): ObjectNode { + val n = getNode(s, key) + return checkObject(key, n) + } + + /** + * @param key A key assumed to be holding a value + * @param s A node + * @return An object value from key `key`, if the key exists + * @throws JSONParseException On type errors + */ + + @JvmStatic + @Throws(JSONParseException::class) + fun getObjectOptional( + s: ObjectNode, + key: String + ): OptionType { + return if (s.has(key)) { + Option.some( + getObject(s, key) + ) + } else { + Option.none() + } + } + + /** + * @param key A key assumed to be holding a value + * @param s A node + * @return An object value from key `key`, if the key exists + * @throws JSONParseException On type errors + */ + + @JvmStatic + @Throws(JSONParseException::class) + fun getObjectOrNull( + s: ObjectNode, + key: String + ): ObjectNode? { + return if (s.has(key)) { + getObject(s, key) + } else { + null + } + } + + /** + * @param key A key assumed to be holding a value + * @param s A node + * @return A string value from key `key` + * @throws JSONParseException On type errors + */ + @JvmStatic + @Throws(JSONParseException::class) + fun getString( + s: ObjectNode, + key: String + ): String { + val v = getNode(s, key) + return when (v.nodeType) { + JsonNodeType.ARRAY, + JsonNodeType.BINARY, + JsonNodeType.BOOLEAN, + JsonNodeType.MISSING, + JsonNodeType.NULL, + JsonNodeType.NUMBER, + JsonNodeType.OBJECT, + JsonNodeType.POJO -> { + val sb = StringBuilder(128) + sb.append("Expected: A key '") + sb.append(key) + sb.append("' with a value of type String\n") + sb.append("Got: A value of type ") + sb.append(v.nodeType) + sb.append("\n") + throw JSONParseException(sb.toString()) + } + + JsonNodeType.STRING -> { + v.asText() + } + } + } + + /** + * @param key A key assumed to be holding a value + * @param n A node + * @return An integer value from key `key`, if the key exists + * @throws JSONParseException On type errors + */ + @JvmStatic + @Throws(JSONParseException::class) + fun getIntegerOptional( + n: ObjectNode, + key: String + ): OptionType { + return if (n.has(key)) { + Option.some(getInteger(n, key)) + } else { + Option.none() + } + } + + /** + * @param key A key assumed to be holding a value + * @param n A node + * @return An integer value from key `key`, if the key exists + * @throws JSONParseException On type errors + */ + @JvmStatic + @Throws(JSONParseException::class) + fun getIntegerOrNull( + n: ObjectNode, + key: String + ): Int? { + return if (n.has(key)) { + getInteger(n, key) + } else { + null + } + } + + /** + * @param key A key assumed to be holding a value + * @param n A node + * @return A string value from key `key`, if the key exists, or `default_value` otherwise. + * @throws JSONParseException On type errors + */ + + @JvmStatic + @Throws(JSONParseException::class) + fun getIntegerDefault( + n: ObjectNode, + key: String, + default_value: Int + ): Int { + return getIntegerOptional(n, key).accept( + object : OptionVisitorType { + override fun none(n: None?): Int? { + return default_value + } + + override fun some(s: Some?): Int? { + return s!!.get() + } + })!! + } + + /** + * @param key A key assumed to be holding a value + * @param n A node + * @return An double value from key `key`, if the key exists + * @throws JSONParseException On type errors + */ + + @JvmStatic + @Throws(JSONParseException::class) + fun getDoubleOptional( + n: ObjectNode, + key: String + ): OptionType { + return if (n.has(key)) { + Option.some(getDouble(n, key)) + } else { + Option.none() + } + } + + /** + * @param key A key assumed to be holding a value + * @param n A node + * @return An double value from key `key`, if the key exists, or `default_value` otherwise. + * @throws JSONParseException On type errors + */ + @JvmStatic + @Throws(JSONParseException::class) + fun getDoubleDefault( + n: ObjectNode, + key: String, + default_value: Double + ): Double { + return getDoubleOptional(n, key).accept( + object : OptionVisitorType { + override fun none(n: None?): Double? { + return default_value + } + + override fun some(s: Some?): Double? { + return s!!.get() + } + })!! + } + + /** + * @param key A key assumed to be holding a value + * @param n A node + * @return A string value from key `key`, if the key exists + * @throws JSONParseException On type errors + */ + @JvmStatic + @Throws(JSONParseException::class) + fun getStringOptional( + n: ObjectNode, + key: String + ): OptionType { + return if (n.has(key)) { + if (n[key].isNull) { + Option.none() + } else { + Option.some(getString(n, key)) + } + } else { + Option.none() + } + } + + /** + * @param key A key assumed to be holding a value + * @param n A node + * @return A string value from key `key`, if the key exists + * @throws JSONParseException On type errors + */ + + @JvmStatic + @Throws(JSONParseException::class) + fun getStringOrNull( + n: ObjectNode, + key: String + ): String? { + return if (n.has(key)) { + if (n[key].isNull) { + null + } else { + getString(n, key) + } + } else { + null + } + } + + /** + * @param key A key assumed to be holding a value + * @param n A node + * @return A string value from key `key`, if the key exists, or `default_value` otherwise. + * @throws JSONParseException On type errors + */ + + @JvmStatic + @Throws(JSONParseException::class) + fun getStringDefault( + n: ObjectNode, + key: String, + default_value: String + ): String { + return getStringOptional(n, key).accept( + object : OptionVisitorType { + override fun none(n: None?): String? { + return default_value + } + + override fun some(s: Some?): String? { + return s!!.get() + } + })!! + } + + /** + * @param s A node + * @param key A key assumed to be holding a value + * @return A timestamp value from key `key` + * @throws JSONParseException On type errors + */ + + @JvmStatic + @Throws(JSONParseException::class) + fun getTimestamp( + s: ObjectNode, + key: String + ): DateTime { + return try { + ISODateTimeFormat.dateTimeParser() + .withZoneUTC() + .parseDateTime(getString(s, key)) + } catch (e: IllegalArgumentException) { + throw JSONParseException( + String.format( + "Could not parse RFC3999 date for key '%s'", + key + ), e + ) + } + } + + /** + * @param key A key assumed to be holding a value + * @param n A node + * @return A timestamp value from key `key`, if the key exists + * @throws JSONParseException On type errors + */ + + @JvmStatic + @Throws(JSONParseException::class) + fun getTimestampOptional( + n: ObjectNode, + key: String + ): OptionType { + return if (n.has(key)) { + Option.some( + getTimestamp( + n, + key + ) + ) + } else { + Option.none() + } + } + + /** + * @param key A key assumed to be holding a value + * @param n A node + * @return A URI value from key `key`, if the key exists + * @throws JSONParseException On type errors + */ + + @JvmStatic + @Throws(JSONParseException::class) + fun getURIOptional( + n: ObjectNode, + key: String + ): OptionType { + return getStringOptional(n, key).mapPartial { x: String? -> + try { + return@mapPartial URI(x) + } catch (e: URISyntaxException) { + throw JSONParseException(e) + } + } + } + + /** + * @param key A key assumed to be holding a value + * @param n A node + * @return A URI value from key `key`, if the key exists + * @throws JSONParseException On type errors + */ + + @JvmStatic + @Throws(JSONParseException::class) + fun getURIOrNull( + n: ObjectNode, + key: String + ): URI? { + val opt = getURIOptional(n, key) + return if (opt.isSome) { + (opt as Some).get() + } else { + null + } + } + + /** + * @param key A key assumed to be holding a value + * @param n A node + * @return A URI value from key `key` + * @throws JSONParseException On type errors + */ + + @JvmStatic + @Throws(JSONParseException::class) + fun getURI( + n: ObjectNode, + key: String + ): URI { + return try { + URI(getString(n, key).trim { it <= ' ' }) + } catch (e: URISyntaxException) { + throw JSONParseException(e) + } + } + + /** + * @param key A key assumed to be holding a value + * @param n A node + * @return A URI value from key `key` + * @throws JSONParseException On type errors + */ + + @JvmStatic + @Throws(JSONParseException::class) + fun getURIDefault( + n: ObjectNode, + key: String, + default_value: URI + ): URI { + Objects.requireNonNull(default_value, "Default") + return getURIOptional(n, key).accept(object : OptionVisitorType { + override fun none(n: None?): URI? { + return default_value + } + + override fun some(s: Some?): URI? { + return s!!.get() + } + })!! + } + + /** + * @param key A key assumed to be holding a value + * @param n A node + * @param v A default value + * @return A boolean from key `key`, or `v` if the key does not + * exist + * @throws JSONParseException On type errors + */ + + @JvmStatic + @Throws(JSONParseException::class) + fun getBooleanDefault( + n: ObjectNode, + key: String, + v: Boolean + ): Boolean { + return if (n.has(key)) { + getBoolean(n, key) + } else { + v + } + } + + /** + * @param key A key assumed to be holding a value + * @param n A node + * @return A big integer value from key `key` + * @throws JSONParseException On type errors + */ + + @JvmStatic + @Throws(JSONParseException::class) + fun getBigInteger( + n: ObjectNode, + key: String + ): BigInteger { + val v = getNode(n, key) + return when (v.nodeType) { + JsonNodeType.ARRAY, + JsonNodeType.BINARY, + JsonNodeType.BOOLEAN, + JsonNodeType.MISSING, + JsonNodeType.NULL, + JsonNodeType.OBJECT, + JsonNodeType.POJO, + JsonNodeType.STRING -> { + val sb = StringBuilder(128) + sb.append("Expected: A key '") + sb.append(key) + sb.append("' with a value of type Integer\n") + sb.append("Got: A value of type ") + sb.append(v.nodeType) + sb.append("\n") + throw JSONParseException(sb.toString()) + } + + JsonNodeType.NUMBER -> { + try { + BigInteger(v.asText()) + } catch (e: NumberFormatException) { + throw JSONParseException(e) + } + } + } + } + + /** + * @param key A key assumed to be holding a value + * @param node A node + * @param defaultValue The default value + * @return A big integer value from key `key` or `defaultValue` + * @throws JSONParseException On type errors + */ + + @JvmStatic + @Throws(JSONParseException::class) + fun getBigIntegerDefault( + node: ObjectNode, + key: String, + defaultValue: BigInteger + ): BigInteger { + return if (node.has(key)) { + getBigInteger(node, key) + } else { + defaultValue + } + } +} diff --git a/simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONSerializerUtilities.java b/simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONSerializerUtilities.java deleted file mode 100644 index add93985f..000000000 --- a/simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONSerializerUtilities.java +++ /dev/null @@ -1,66 +0,0 @@ -package org.nypl.simplified.json.core; - -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectWriter; -import com.fasterxml.jackson.databind.node.ArrayNode; -import com.fasterxml.jackson.databind.node.ObjectNode; -import com.io7m.jnull.NullCheck; -import com.io7m.junreachable.UnreachableCodeException; - -import java.io.IOException; -import java.io.OutputStream; - -/** - * Utilities for implementing JSON serializers. - */ - -public final class JSONSerializerUtilities -{ - private JSONSerializerUtilities() - { - throw new UnreachableCodeException(); - } - - /** - * Serialize the given object node to the given stream. - * - * @param d The node - * @param os The output stream - * - * @throws IOException On I/O errors - */ - - public static void serialize( - final ObjectNode d, - final OutputStream os) - throws IOException - { - NullCheck.notNull(d); - NullCheck.notNull(os); - - final ObjectMapper jom = new ObjectMapper(); - final ObjectWriter jw = jom.writerWithDefaultPrettyPrinter(); - jw.writeValue(os, d); - } - - /** - * Serialize the given object node to a string. - * - * @param d The node - * - * @return Pretty-printed JSON - * - * @throws IOException On I/O errors - */ - - public static String serializeToString( - final ObjectNode d) - throws IOException - { - NullCheck.notNull(d); - - final ObjectMapper jom = new ObjectMapper(); - final ObjectWriter jw = jom.writerWithDefaultPrettyPrinter(); - return jw.writeValueAsString(d); - } -} diff --git a/simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONSerializerUtilities.kt b/simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONSerializerUtilities.kt new file mode 100644 index 000000000..20ec6fffd --- /dev/null +++ b/simplified-json-core/src/main/java/org/nypl/simplified/json/core/JSONSerializerUtilities.kt @@ -0,0 +1,76 @@ +package org.nypl.simplified.json.core + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ObjectNode +import java.io.IOException +import java.io.OutputStream + +/** + * Utilities for implementing JSON serializers. + */ +object JSONSerializerUtilities { + + private val objectMapper: ObjectMapper = ObjectMapper() + + /** + * Serialize the given object node to the given stream. + * + * @param d The node + * @param os The output stream + * + * @throws IOException On I/O errors + */ + + @JvmStatic + @Throws(IOException::class) + fun serialize( + d: ObjectNode, + os: OutputStream + ) { + this.objectMapper.writerWithDefaultPrettyPrinter() + .writeValue(os, d) + } + + /** + * Serialize the given object node to a string. + * + * @param d The node + * + * @return Pretty-printed JSON + * + * @throws IOException On I/O errors + */ + + @JvmStatic + @Throws(IOException::class) + fun serializeToString( + d: ObjectNode + ): String { + return this.objectMapper.writerWithDefaultPrettyPrinter() + .writeValueAsString(d) + } + + /** + * Serialize the given object node to a string. + * + * @param d The node + * + * @return Pretty-printed JSON + * + * @throws IOException On I/O errors + */ + + @JvmStatic + @Throws(IOException::class) + fun serializeToString( + d: List + ): String { + val a = this.objectMapper.createArrayNode() + for (o in d) { + a.add(o) + } + return this.objectMapper.writerWithDefaultPrettyPrinter() + .writeValueAsString(a) + } +} diff --git a/simplified-lcp/src/main/java/org/nypl/simplified/lcp/LCPContentProtectionProvider.kt b/simplified-lcp/src/main/java/org/nypl/simplified/lcp/LCPContentProtectionProvider.kt index 7ecd2f3da..eaa05b468 100644 --- a/simplified-lcp/src/main/java/org/nypl/simplified/lcp/LCPContentProtectionProvider.kt +++ b/simplified-lcp/src/main/java/org/nypl/simplified/lcp/LCPContentProtectionProvider.kt @@ -8,6 +8,7 @@ import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.librarysimplified.lcp.R +import org.nypl.drm.core.ContentProtectionProvider import org.readium.r2.lcp.LcpAuthenticating import org.readium.r2.lcp.LcpService import org.readium.r2.shared.publication.protection.ContentProtection @@ -15,7 +16,6 @@ import org.readium.r2.shared.util.asset.AssetRetriever import org.readium.r2.shared.util.downloads.foreground.ForegroundDownloadManager import org.readium.r2.shared.util.http.DefaultHttpClient import org.slf4j.LoggerFactory -import org.nypl.drm.core.ContentProtectionProvider import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine diff --git a/simplified-main/src/main/java/org/librarysimplified/main/MainAdobeWarnings.kt b/simplified-main/src/main/java/org/librarysimplified/main/MainAdobeWarnings.kt index 5b9645e31..e9b270014 100644 --- a/simplified-main/src/main/java/org/librarysimplified/main/MainAdobeWarnings.kt +++ b/simplified-main/src/main/java/org/librarysimplified/main/MainAdobeWarnings.kt @@ -4,8 +4,8 @@ import android.content.Context import androidx.annotation.UiThread import com.google.android.material.dialog.MaterialAlertDialogBuilder import org.librarysimplified.services.api.ServiceDirectoryType -import org.nypl.simplified.adobe.extensions.AdobeDRMServices import org.nypl.drm.core.AdobeAdeptExecutorType +import org.nypl.simplified.adobe.extensions.AdobeDRMServices /** * Functions to display Adobe DRM related warnings. diff --git a/simplified-main/src/main/java/org/librarysimplified/main/MainFragmentListenerDelegate.kt b/simplified-main/src/main/java/org/librarysimplified/main/MainFragmentListenerDelegate.kt index e669835f7..64ad1c241 100644 --- a/simplified-main/src/main/java/org/librarysimplified/main/MainFragmentListenerDelegate.kt +++ b/simplified-main/src/main/java/org/librarysimplified/main/MainFragmentListenerDelegate.kt @@ -692,7 +692,7 @@ internal class MainFragmentListenerDelegate( ) Viewers.openViewer( - activity = this.fragment.requireActivity(), + context = MainApplication.application, preferences = viewerPreferences, book = book, format = format diff --git a/simplified-opds-auth-document/src/main/java/org/nypl/simplified/opds/auth_document/AuthenticationDocumentParser.kt b/simplified-opds-auth-document/src/main/java/org/nypl/simplified/opds/auth_document/AuthenticationDocumentParser.kt index 67011c732..00ede1bbc 100644 --- a/simplified-opds-auth-document/src/main/java/org/nypl/simplified/opds/auth_document/AuthenticationDocumentParser.kt +++ b/simplified-opds-auth-document/src/main/java/org/nypl/simplified/opds/auth_document/AuthenticationDocumentParser.kt @@ -270,7 +270,7 @@ internal class AuthenticationDocumentParser( private fun parseInput( fieldName: String, - root: ObjectNode? + root: ObjectNode ): AuthenticationObjectNYPLInput? { return try { AuthenticationObjectNYPLInput( diff --git a/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSAcquisitionFeedEntryParser.java b/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSAcquisitionFeedEntryParser.java index d1d8cc417..755d27404 100644 --- a/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSAcquisitionFeedEntryParser.java +++ b/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSAcquisitionFeedEntryParser.java @@ -1,5 +1,24 @@ package org.nypl.simplified.opds.core; +import static org.nypl.simplified.opds.core.OPDSFeedConstants.ACQUISITION_URI_PREFIX_TEXT; +import static org.nypl.simplified.opds.core.OPDSFeedConstants.ALTERNATE_REL_TEXT; +import static org.nypl.simplified.opds.core.OPDSFeedConstants.ANNOTATION_URI_TEXT; +import static org.nypl.simplified.opds.core.OPDSFeedConstants.ATOM_URI; +import static org.nypl.simplified.opds.core.OPDSFeedConstants.BIBFRAME_URI; +import static org.nypl.simplified.opds.core.OPDSFeedConstants.CIRCULATION_ANALYTICS_OPEN_BOOK_REL_TEXT; +import static org.nypl.simplified.opds.core.OPDSFeedConstants.DUBLIN_CORE_TERMS_URI; +import static org.nypl.simplified.opds.core.OPDSFeedConstants.GROUP_REL_TEXT; +import static org.nypl.simplified.opds.core.OPDSFeedConstants.IMAGE_URI_TEXT; +import static org.nypl.simplified.opds.core.OPDSFeedConstants.ISSUES_REL_TEXT; +import static org.nypl.simplified.opds.core.OPDSFeedConstants.LCP_URI; +import static org.nypl.simplified.opds.core.OPDSFeedConstants.OPDS_URI; +import static org.nypl.simplified.opds.core.OPDSFeedConstants.PREVIEW_TEXT; +import static org.nypl.simplified.opds.core.OPDSFeedConstants.RELATED_REL_TEXT; +import static org.nypl.simplified.opds.core.OPDSFeedConstants.REVOKE_URI_TEXT; +import static org.nypl.simplified.opds.core.OPDSFeedConstants.SAMPLE_TEXT; +import static org.nypl.simplified.opds.core.OPDSFeedConstants.THUMBNAIL_URI_TEXT; +import static org.nypl.simplified.opds.core.OPDSFeedConstants.TIME_TRACKING_URI_TEXT; + import com.io7m.jfunctional.Option; import com.io7m.jfunctional.OptionType; import com.io7m.jfunctional.Some; @@ -34,25 +53,6 @@ import one.irradia.mime.api.MIMEType; import one.irradia.mime.vanilla.MIMEParser; -import static org.nypl.simplified.opds.core.OPDSFeedConstants.ACQUISITION_URI_PREFIX_TEXT; -import static org.nypl.simplified.opds.core.OPDSFeedConstants.ALTERNATE_REL_TEXT; -import static org.nypl.simplified.opds.core.OPDSFeedConstants.ANNOTATION_URI_TEXT; -import static org.nypl.simplified.opds.core.OPDSFeedConstants.ATOM_URI; -import static org.nypl.simplified.opds.core.OPDSFeedConstants.BIBFRAME_URI; -import static org.nypl.simplified.opds.core.OPDSFeedConstants.CIRCULATION_ANALYTICS_OPEN_BOOK_REL_TEXT; -import static org.nypl.simplified.opds.core.OPDSFeedConstants.DUBLIN_CORE_TERMS_URI; -import static org.nypl.simplified.opds.core.OPDSFeedConstants.GROUP_REL_TEXT; -import static org.nypl.simplified.opds.core.OPDSFeedConstants.IMAGE_URI_TEXT; -import static org.nypl.simplified.opds.core.OPDSFeedConstants.ISSUES_REL_TEXT; -import static org.nypl.simplified.opds.core.OPDSFeedConstants.LCP_URI; -import static org.nypl.simplified.opds.core.OPDSFeedConstants.OPDS_URI; -import static org.nypl.simplified.opds.core.OPDSFeedConstants.PREVIEW_TEXT; -import static org.nypl.simplified.opds.core.OPDSFeedConstants.RELATED_REL_TEXT; -import static org.nypl.simplified.opds.core.OPDSFeedConstants.REVOKE_URI_TEXT; -import static org.nypl.simplified.opds.core.OPDSFeedConstants.SAMPLE_TEXT; -import static org.nypl.simplified.opds.core.OPDSFeedConstants.THUMBNAIL_URI_TEXT; -import static org.nypl.simplified.opds.core.OPDSFeedConstants.TIME_TRACKING_URI_TEXT; - /** * The default implementation of the {@link OPDSAcquisitionFeedEntryParserType} * type. diff --git a/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSAtom.java b/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSAtom.java index 64c85d676..0c8391690 100644 --- a/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSAtom.java +++ b/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSAtom.java @@ -5,7 +5,6 @@ import com.io7m.junreachable.UnreachableCodeException; import org.joda.time.DateTime; -import org.joda.time.format.ISODateTimeFormat; import org.w3c.dom.DOMException; import org.w3c.dom.Element; diff --git a/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSFeedParser.java b/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSFeedParser.java index af6d5be23..74f9d2830 100644 --- a/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSFeedParser.java +++ b/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSFeedParser.java @@ -1,5 +1,12 @@ package org.nypl.simplified.opds.core; +import static org.nypl.simplified.opds.core.OPDSFeedConstants.ATOM_URI; +import static org.nypl.simplified.opds.core.OPDSFeedConstants.AUTHENTICATION_DOCUMENT_RELATION_URI_TEXT; +import static org.nypl.simplified.opds.core.OPDSFeedConstants.DRM_URI; +import static org.nypl.simplified.opds.core.OPDSFeedConstants.FACET_URI_TEXT; +import static org.nypl.simplified.opds.core.OPDSFeedConstants.OPDS_URI_TEXT; +import static org.nypl.simplified.opds.core.OPDSFeedConstants.SIMPLIFIED_URI_TEXT; + import com.google.common.base.Preconditions; import com.io7m.jfunctional.Option; import com.io7m.jfunctional.OptionType; @@ -29,13 +36,6 @@ import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; -import static org.nypl.simplified.opds.core.OPDSFeedConstants.ATOM_URI; -import static org.nypl.simplified.opds.core.OPDSFeedConstants.AUTHENTICATION_DOCUMENT_RELATION_URI_TEXT; -import static org.nypl.simplified.opds.core.OPDSFeedConstants.DRM_URI; -import static org.nypl.simplified.opds.core.OPDSFeedConstants.FACET_URI_TEXT; -import static org.nypl.simplified.opds.core.OPDSFeedConstants.OPDS_URI_TEXT; -import static org.nypl.simplified.opds.core.OPDSFeedConstants.SIMPLIFIED_URI_TEXT; - /** *

The default implementation of the {@link OPDSFeedParserType}.

* diff --git a/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSSearchParser.java b/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSSearchParser.java index 3654d119b..5ebafc86f 100644 --- a/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSSearchParser.java +++ b/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSSearchParser.java @@ -1,6 +1,7 @@ package org.nypl.simplified.opds.core; import com.io7m.jnull.NullCheck; + import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.w3c.dom.DOMException; @@ -9,14 +10,15 @@ import org.w3c.dom.Node; import org.xml.sax.SAXException; -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; import java.io.IOException; import java.io.InputStream; import java.net.URI; import java.util.concurrent.TimeUnit; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; + /** *

The default implementation of the {@link OPDSSearchParserType}.

*/ diff --git a/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSXML.java b/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSXML.java index 86227a9b1..99de23083 100644 --- a/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSXML.java +++ b/simplified-opds-core/src/main/java/org/nypl/simplified/opds/core/OPDSXML.java @@ -6,7 +6,6 @@ import com.io7m.junreachable.UnreachableCodeException; import org.joda.time.DateTime; -import org.joda.time.format.ISODateTimeFormat; import org.w3c.dom.Document; import org.w3c.dom.Element; import org.w3c.dom.Node; diff --git a/simplified-opds2-irradia/src/main/java/org/nypl/simplified/opds2/irradia/OPDS2ParsersIrradia.kt b/simplified-opds2-irradia/src/main/java/org/nypl/simplified/opds2/irradia/OPDS2ParsersIrradia.kt index caa5ccb0c..7b222c13d 100644 --- a/simplified-opds2-irradia/src/main/java/org/nypl/simplified/opds2/irradia/OPDS2ParsersIrradia.kt +++ b/simplified-opds2-irradia/src/main/java/org/nypl/simplified/opds2/irradia/OPDS2ParsersIrradia.kt @@ -1,9 +1,9 @@ package org.nypl.simplified.opds2.irradia import one.irradia.opds2_0.parser.extension.library_simplified.OPDS20CatalogExtension +import one.irradia.opds2_0.parser.extension.spi.OPDS20ExtensionType import one.irradia.opds2_0.parser.vanilla.OPDS20FeedParsers import org.nypl.simplified.opds2.OPDS2Feed -import one.irradia.opds2_0.parser.extension.spi.OPDS20ExtensionType import org.nypl.simplified.opds2.irradia.internal.OPDS2ParserIrradia import org.nypl.simplified.opds2.parser.api.OPDS2ParsersType import org.nypl.simplified.parser.api.ParserType diff --git a/simplified-parser-api/src/main/java/org/nypl/simplified/parser/api/ParseError.kt b/simplified-parser-api/src/main/java/org/nypl/simplified/parser/api/ParseError.kt index 0221e293d..c31e069a9 100644 --- a/simplified-parser-api/src/main/java/org/nypl/simplified/parser/api/ParseError.kt +++ b/simplified-parser-api/src/main/java/org/nypl/simplified/parser/api/ParseError.kt @@ -1,7 +1,6 @@ package org.nypl.simplified.parser.api import java.io.Serializable -import java.lang.Exception import java.net.URI /** diff --git a/simplified-parser-api/src/main/java/org/nypl/simplified/parser/api/ParseWarning.kt b/simplified-parser-api/src/main/java/org/nypl/simplified/parser/api/ParseWarning.kt index b83a53b30..2265ed56b 100644 --- a/simplified-parser-api/src/main/java/org/nypl/simplified/parser/api/ParseWarning.kt +++ b/simplified-parser-api/src/main/java/org/nypl/simplified/parser/api/ParseWarning.kt @@ -1,7 +1,6 @@ package org.nypl.simplified.parser.api import java.io.Serializable -import java.lang.Exception import java.net.URI /** diff --git a/simplified-profiles/src/main/java/org/nypl/simplified/profiles/ProfileDescriptionJSON.kt b/simplified-profiles/src/main/java/org/nypl/simplified/profiles/ProfileDescriptionJSON.kt index 2aadf7b9a..379503be9 100644 --- a/simplified-profiles/src/main/java/org/nypl/simplified/profiles/ProfileDescriptionJSON.kt +++ b/simplified-profiles/src/main/java/org/nypl/simplified/profiles/ProfileDescriptionJSON.kt @@ -348,7 +348,7 @@ object ProfileDescriptionJSON { private fun deserializeReaderPreferences( objectMapper: ObjectMapper, - node: ObjectNode? + node: ObjectNode ): ReaderPreferences { return JSONParserUtilities.getObjectOptional(node, "readerPreferences") .mapPartial { prefsNode -> @@ -367,11 +367,10 @@ object ProfileDescriptionJSON { private fun deserializePlaybackRates( objectMapper: ObjectMapper, - node: ObjectNode? + node: ObjectNode ): Map { - val str = JSONParserUtilities.getObjectOrNull( - node, "playbackRates" - ) ?: return hashMapOf() + val str = JSONParserUtilities.getObjectOrNull(node, "playbackRates") + ?: return hashMapOf() val map = objectMapper.readValue( str.toString(), @@ -385,34 +384,6 @@ object ProfileDescriptionJSON { } } - private fun deserializeSleepTimers( - objectMapper: ObjectMapper, - node: ObjectNode? - ): Map { - val str = JSONParserUtilities.getObjectOrNull( - node, "sleepTimers" - ) ?: return hashMapOf() - - val map = objectMapper.readValue( - str.toString(), - object : TypeReference>() { - // Do nothing - } - ) - - return map.mapValues { entry -> - if (entry.value != null) { - try { - entry.value?.toLong() - } catch (exception: NumberFormatException) { - 0L - } - } else { - null - } - } - } - private fun someOrNull(opt: OptionType?): T? { return if (opt is Some) { opt.get() diff --git a/simplified-reader-api/src/main/java/org/nypl/simplified/reader/api/ReaderPreferencesJSON.java b/simplified-reader-api/src/main/java/org/nypl/simplified/reader/api/ReaderPreferencesJSON.java index a59c20a17..c81b15e52 100644 --- a/simplified-reader-api/src/main/java/org/nypl/simplified/reader/api/ReaderPreferencesJSON.java +++ b/simplified-reader-api/src/main/java/org/nypl/simplified/reader/api/ReaderPreferencesJSON.java @@ -1,14 +1,13 @@ package org.nypl.simplified.reader.api; +import static org.nypl.simplified.reader.api.ReaderColorScheme.SCHEME_BLACK_ON_WHITE; +import static org.nypl.simplified.reader.api.ReaderFontSelection.READER_FONT_SANS_SERIF; + import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.node.ObjectNode; -import com.io7m.jnull.NullCheck; import com.io7m.junreachable.UnreachableCodeException; -import org.nypl.simplified.reader.api.ReaderColorScheme; -import org.nypl.simplified.reader.api.ReaderFontSelection; -import org.nypl.simplified.reader.api.ReaderPreferences; import org.nypl.simplified.json.core.JSONParseException; import org.nypl.simplified.json.core.JSONParserUtilities; import org.nypl.simplified.json.core.JSONSerializerUtilities; @@ -17,9 +16,6 @@ import java.io.IOException; import java.util.Objects; -import static org.nypl.simplified.reader.api.ReaderColorScheme.SCHEME_BLACK_ON_WHITE; -import static org.nypl.simplified.reader.api.ReaderFontSelection.READER_FONT_SANS_SERIF; - /** * Functions to serialize and reader preferences to/from JSON. */ diff --git a/simplified-tenprint/src/main/java/org/nypl/simplified/tenprint/TenPrintGenerator.java b/simplified-tenprint/src/main/java/org/nypl/simplified/tenprint/TenPrintGenerator.java index ab21d1416..2525065e8 100644 --- a/simplified-tenprint/src/main/java/org/nypl/simplified/tenprint/TenPrintGenerator.java +++ b/simplified-tenprint/src/main/java/org/nypl/simplified/tenprint/TenPrintGenerator.java @@ -14,6 +14,7 @@ import android.text.Layout; import android.text.StaticLayout; import android.text.TextPaint; + import com.io7m.jnull.NullCheck; import java.util.ArrayList; diff --git a/simplified-tests/build.gradle.kts b/simplified-tests/build.gradle.kts index e990a117f..7fa5a32a5 100644 --- a/simplified-tests/build.gradle.kts +++ b/simplified-tests/build.gradle.kts @@ -141,6 +141,11 @@ val dependencyObjects = listOf( libs.androidx.lifecycle.viewmodel, libs.androidx.lifecycle.viewmodel.savedstate, libs.androidx.media, + libs.media3.common, + libs.media3.datasource, + libs.media3.exoplayer, + libs.media3.extractor, + libs.media3.session, libs.androidx.paging.common, libs.androidx.paging.common.ktx, libs.androidx.paging.runtime, @@ -171,6 +176,7 @@ val dependencyObjects = listOf( libs.io7m.jfunctional, libs.io7m.jnull, libs.io7m.junreachable, + libs.kabstand, libs.irradia.fieldrush.api, libs.irradia.fieldrush.vanilla, libs.irradia.mime.api, @@ -219,7 +225,6 @@ val dependencyObjects = listOf( libs.palace.audiobook.downloads, libs.palace.audiobook.feedbooks, libs.palace.audiobook.http, - libs.palace.audiobook.lcp, libs.palace.audiobook.lcp.license.status, libs.palace.audiobook.license.check.api, libs.palace.audiobook.license.check.spi, @@ -231,9 +236,8 @@ val dependencyObjects = listOf( libs.palace.audiobook.manifest.parser.api, libs.palace.audiobook.manifest.parser.extension.spi, libs.palace.audiobook.manifest.parser.webpub, - libs.palace.audiobook.open.access, + libs.palace.audiobook.media3, libs.palace.audiobook.parser.api, - libs.palace.audiobook.rbdigital, libs.palace.audiobook.views, libs.palace.drm.core, libs.palace.http.api, diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/bookmark_annotations/AudiobookBookmarkAnnotationsJSONTest.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/bookmark_annotations/ObsoleteAudiobookBookmarkAnnotationsJSONTest.kt similarity index 86% rename from simplified-tests/src/test/java/org/nypl/simplified/tests/bookmark_annotations/AudiobookBookmarkAnnotationsJSONTest.kt rename to simplified-tests/src/test/java/org/nypl/simplified/tests/bookmark_annotations/ObsoleteAudiobookBookmarkAnnotationsJSONTest.kt index 978082a43..280bc2685 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/bookmark_annotations/AudiobookBookmarkAnnotationsJSONTest.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/bookmark_annotations/ObsoleteAudiobookBookmarkAnnotationsJSONTest.kt @@ -9,18 +9,21 @@ import org.nypl.simplified.bookmarks.api.BookmarkAnnotationBodyNode import org.nypl.simplified.bookmarks.api.BookmarkAnnotationFirstNode import org.nypl.simplified.bookmarks.api.BookmarkAnnotationResponse import org.nypl.simplified.bookmarks.api.BookmarkAnnotationSelectorNode +import org.nypl.simplified.bookmarks.api.BookmarkAnnotationTargetNode import org.nypl.simplified.bookmarks.api.BookmarkAnnotations import org.nypl.simplified.bookmarks.api.BookmarkAnnotationsJSON -import org.nypl.simplified.bookmarks.api.BookmarkAnnotationTargetNode import org.nypl.simplified.books.api.bookmark.BookmarkKind +import org.nypl.simplified.books.api.bookmark.SerializedLocatorAudioBookTime1 +import org.nypl.simplified.books.api.bookmark.SerializedLocatorAudioBookTime2 +import org.nypl.simplified.books.api.bookmark.SerializedLocators import org.slf4j.LoggerFactory import java.io.FileNotFoundException import java.io.InputStream -class AudiobookBookmarkAnnotationsJSONTest { +class ObsoleteAudiobookBookmarkAnnotationsJSONTest { private val logger = - LoggerFactory.getLogger(AudiobookBookmarkAnnotationsJSONTest::class.java) + LoggerFactory.getLogger(ObsoleteAudiobookBookmarkAnnotationsJSONTest::class.java) private val objectMapper: ObjectMapper = ObjectMapper() private val targetValue0 = @@ -38,7 +41,7 @@ class AudiobookBookmarkAnnotationsJSONTest { timestamp = "2022-06-27T12:39:37+0000", device = "cca80416-3168-4e58-b621-7964b9265ac9", chapterTitle = "A Title", - bookProgress = null + bookProgress = 0.0f ) private val bookmarkBody1 = @@ -46,7 +49,7 @@ class AudiobookBookmarkAnnotationsJSONTest { timestamp = "2022-06-27T12:39:37+0000", device = "cca80416-3168-4e58-b621-7964b9265ac9", chapterTitle = "A Title", - bookProgress = null + bookProgress = 0.0f ) private val bookmarkBody2 = @@ -54,7 +57,7 @@ class AudiobookBookmarkAnnotationsJSONTest { timestamp = "2022-06-27T12:39:37+0000", device = "cca80416-3168-4e58-b621-7964b9265ac9", chapterTitle = "A Title", - bookProgress = null + bookProgress = 0.0f ) private val bookmarkBodyBadDate = @@ -62,7 +65,7 @@ class AudiobookBookmarkAnnotationsJSONTest { timestamp = "2022-06-27T20:00:37Z", device = "cca80416-3168-4e58-b621-7964b9265ac9", chapterTitle = "A Title", - bookProgress = null + bookProgress = 0.0f ) private val bookmark0 = @@ -177,31 +180,27 @@ class AudiobookBookmarkAnnotationsJSONTest { @Test fun testSpecValidLocator() { val location = - BookmarkAnnotationsJSON.deserializeAudiobookLocation( - objectMapper = this.objectMapper, - value = this.resourceText("valid-locator-3.json") - ) + SerializedLocators.parseLocatorFromString(this.resourceText("valid-locator-3.json")) + as SerializedLocatorAudioBookTime1 assertEquals(32, location.chapter) assertEquals(3, location.part) assertEquals("Chapter title", location.title) - assertEquals(0, location.startOffset) - assertEquals(78000, location.currentOffset) + assertEquals(0, location.startOffsetMilliseconds) + assertEquals(78000, location.timeWithoutOffset) } @Test fun testSpecValidLocatorTwoOffsets() { val location = - BookmarkAnnotationsJSON.deserializeAudiobookLocation( - objectMapper = this.objectMapper, - value = this.resourceText("valid-locator-4.json") - ) + SerializedLocators.parseLocatorFromString(this.resourceText("valid-locator-4.json")) + as SerializedLocatorAudioBookTime1 assertEquals(32, location.chapter) assertEquals(3, location.part) assertEquals("Chapter title", location.title) - assertEquals(15000, location.startOffset) - assertEquals(63000, location.currentOffset) + assertEquals(15000, location.startOffsetMilliseconds) + assertEquals(63000, location.timeWithoutOffset) } @Test @@ -303,18 +302,18 @@ class AudiobookBookmarkAnnotationsJSONTest { node = this.resourceNode("valid-bookmark-4.json") ) - val bookmark = BookmarkAnnotations.toAudiobookBookmark(this.objectMapper, annotation) + val bookmark = BookmarkAnnotations.toSerializedBookmark(this.objectMapper, annotation) assertEquals("urn:uuid:1daa8de6-94e8-4711-b7d1-e43b572aa6e0", bookmark.opdsId) assertEquals("urn:uuid:c83db5b1-9130-4b86-93ea-634b00235c7c", bookmark.deviceID) assertEquals(BookmarkKind.BookmarkLastReadLocation, bookmark.kind) assertEquals("2022-06-27T12:47:49.000Z", bookmark.time.toString()) - val location = bookmark.location + val location = bookmark.location as SerializedLocatorAudioBookTime1 assertEquals("Chapter title", location.title) assertEquals(32, location.chapter) assertEquals(3, location.part) - assertEquals(0, location.startOffset) - assertEquals(78000, location.currentOffset) + assertEquals(0, location.startOffsetMilliseconds) + assertEquals(78000, location.timeMilliseconds) this.checkRoundTrip(annotation) } @@ -327,18 +326,18 @@ class AudiobookBookmarkAnnotationsJSONTest { node = this.resourceNode("valid-bookmark-6.json") ) - val bookmark = BookmarkAnnotations.toAudiobookBookmark(this.objectMapper, annotation) + val bookmark = BookmarkAnnotations.toSerializedBookmark(this.objectMapper, annotation) assertEquals("urn:uuid:1daa8de6-94e8-4711-b7d1-e43b572aa6e0", bookmark.opdsId) assertEquals("urn:uuid:c83db5b1-9130-4b86-93ea-634b00235c7c", bookmark.deviceID) assertEquals(BookmarkKind.BookmarkLastReadLocation, bookmark.kind) assertEquals("2022-06-27T12:47:49.000Z", bookmark.time.toString()) - val location = bookmark.location + val location = bookmark.location as SerializedLocatorAudioBookTime1 assertEquals("Chapter title", location.title) assertEquals(32, location.chapter) assertEquals(3, location.part) - assertEquals(15000, location.startOffset) - assertEquals(63000, location.currentOffset) + assertEquals(15000, location.startOffsetMilliseconds) + assertEquals(63000, location.timeWithoutOffset) this.checkRoundTrip(annotation) } @@ -375,11 +374,11 @@ class AudiobookBookmarkAnnotationsJSONTest { this.compareAnnotations(bookmarkAnnotation, deserialized) val toBookmark = - BookmarkAnnotations.toAudiobookBookmark(this.objectMapper, deserialized) + BookmarkAnnotations.toSerializedBookmark(this.objectMapper, deserialized) val fromBookmark = - BookmarkAnnotations.fromAudiobookBookmark(this.objectMapper, toBookmark) + BookmarkAnnotations.fromSerializedBookmark(this.objectMapper, toBookmark) val toBookmarkAgain = - BookmarkAnnotations.toAudiobookBookmark(this.objectMapper, fromBookmark) + BookmarkAnnotations.toSerializedBookmark(this.objectMapper, fromBookmark) this.compareAnnotations(bookmarkAnnotation, deserialized) this.compareAnnotations(bookmarkAnnotation, fromBookmark) @@ -404,15 +403,9 @@ class AudiobookBookmarkAnnotationsJSONTest { assertEquals(x.target.selector.type, y.target.selector.type) val xSelectorValue = - BookmarkAnnotationsJSON.deserializeAudiobookLocation( - this.objectMapper, - x.target.selector.value - ) + SerializedLocators.parseLocatorFromString(x.target.selector.value) val ySelectorValue = - BookmarkAnnotationsJSON.deserializeAudiobookLocation( - this.objectMapper, - y.target.selector.value - ) + SerializedLocators.parseLocatorFromString(y.target.selector.value) assertEquals(xSelectorValue, ySelectorValue) assertEquals(x.target.source, y.target.source) @@ -425,7 +418,7 @@ class AudiobookBookmarkAnnotationsJSONTest { val fileName = "/org/nypl/simplified/tests/bookmark_annotations/spec/bookmarks/$name" val url = - AudiobookBookmarkAnnotationsJSONTest::class.java.getResource(fileName) + ObsoleteAudiobookBookmarkAnnotationsJSONTest::class.java.getResource(fileName) ?: throw FileNotFoundException("No such resource: $fileName") return url.openStream() } diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/bookmark_annotations/PDFBookmarkAnnotationsJSONTest.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/bookmark_annotations/PDFBookmarkAnnotationsJSONTest.kt index ce6549cd3..7faf5ffe2 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/bookmark_annotations/PDFBookmarkAnnotationsJSONTest.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/bookmark_annotations/PDFBookmarkAnnotationsJSONTest.kt @@ -10,11 +10,13 @@ import org.nypl.simplified.bookmarks.api.BookmarkAnnotation import org.nypl.simplified.bookmarks.api.BookmarkAnnotationBodyNode import org.nypl.simplified.bookmarks.api.BookmarkAnnotationFirstNode import org.nypl.simplified.bookmarks.api.BookmarkAnnotationResponse -import org.nypl.simplified.bookmarks.api.BookmarkAnnotations import org.nypl.simplified.bookmarks.api.BookmarkAnnotationSelectorNode -import org.nypl.simplified.bookmarks.api.BookmarkAnnotationsJSON import org.nypl.simplified.bookmarks.api.BookmarkAnnotationTargetNode +import org.nypl.simplified.bookmarks.api.BookmarkAnnotations +import org.nypl.simplified.bookmarks.api.BookmarkAnnotationsJSON import org.nypl.simplified.books.api.bookmark.BookmarkKind +import org.nypl.simplified.books.api.bookmark.SerializedLocatorPage1 +import org.nypl.simplified.books.api.bookmark.SerializedLocators import org.nypl.simplified.json.core.JSONParseException import org.slf4j.LoggerFactory import java.io.FileNotFoundException @@ -38,24 +40,24 @@ class PDFBookmarkAnnotationsJSONTest { BookmarkAnnotationBodyNode( timestamp = "2022-08-05T18:35:37+0000", device = "cca80416-3168-4e58-b621-7964b9265ac9", - chapterTitle = null, - bookProgress = null + chapterTitle = "", + bookProgress = 0.0f ) private val bookmarkBody1 = BookmarkAnnotationBodyNode( timestamp = "2022-08-05T18:36:37+0000", device = "cca80416-3168-4e58-b621-7964b9265ac9", - chapterTitle = null, - bookProgress = null + chapterTitle = "", + bookProgress = 0.0f ) private val bookmarkBody2 = BookmarkAnnotationBodyNode( timestamp = "2022-08-05T18:37:37+0000", device = "cca80416-3168-4e58-b621-7964b9265ac9", - chapterTitle = null, - bookProgress = null + chapterTitle = "", + bookProgress = 0.0f ) private val bookmarkBodyBadDate = @@ -63,7 +65,7 @@ class PDFBookmarkAnnotationsJSONTest { timestamp = "2022-08-05T18:00:37Z", device = "cca80416-3168-4e58-b621-7964b9265ac9", chapterTitle = "A Title", - bookProgress = null + bookProgress = 0.0f ) private val bookmark0 = @@ -246,12 +248,12 @@ class PDFBookmarkAnnotationsJSONTest { node = this.resourceNode("valid-bookmark-5.json") ) - val bookmark = BookmarkAnnotations.toPdfBookmark(this.objectMapper, annotation) + val bookmark = BookmarkAnnotations.toSerializedBookmark(this.objectMapper, annotation) assertEquals("urn:uuid:1daa8de6-94e8-4711-b7d1-e43b572aa6e0", bookmark.opdsId) assertEquals("urn:uuid:c83db5b1-9130-4b86-93ea-634b00235c7c", bookmark.deviceID) assertEquals(BookmarkKind.BookmarkLastReadLocation, bookmark.kind) assertEquals("2022-08-05T16:32:49.000Z", bookmark.time.toString()) - assertEquals(2, bookmark.pageNumber) + assertEquals(2, ((bookmark.location) as SerializedLocatorPage1).page) this.checkRoundTrip(annotation) } @@ -259,12 +261,9 @@ class PDFBookmarkAnnotationsJSONTest { @Test fun testSpecValidLocator() { val location = - BookmarkAnnotationsJSON.deserializePdfLocation( - objectMapper = this.objectMapper, - value = this.resourceText("valid-locator-2.json") - ) + SerializedLocators.parseLocatorFromString(this.resourceText("valid-locator-2.json")) - assertEquals(23, location) + assertEquals(SerializedLocatorPage1(23), location) } @Test @@ -307,11 +306,11 @@ class PDFBookmarkAnnotationsJSONTest { this.compareAnnotations(bookmarkAnnotation, deserialized) val toBookmark = - BookmarkAnnotations.toPdfBookmark(this.objectMapper, deserialized) + BookmarkAnnotations.toSerializedBookmark(this.objectMapper, deserialized) val fromBookmark = - BookmarkAnnotations.fromPdfBookmark(this.objectMapper, toBookmark) + BookmarkAnnotations.fromSerializedBookmark(this.objectMapper, toBookmark) val toBookmarkAgain = - BookmarkAnnotations.toPdfBookmark(this.objectMapper, fromBookmark) + BookmarkAnnotations.toSerializedBookmark(this.objectMapper, fromBookmark) this.compareAnnotations(bookmarkAnnotation, deserialized) this.compareAnnotations(bookmarkAnnotation, fromBookmark) @@ -336,9 +335,9 @@ class PDFBookmarkAnnotationsJSONTest { assertEquals(x.target.selector.type, y.target.selector.type) val xSelectorValue = - BookmarkAnnotationsJSON.deserializePdfLocation(this.objectMapper, x.target.selector.value) + SerializedLocators.parseLocatorFromString(x.target.selector.value) val ySelectorValue = - BookmarkAnnotationsJSON.deserializePdfLocation(this.objectMapper, y.target.selector.value) + SerializedLocators.parseLocatorFromString(y.target.selector.value) assertEquals(xSelectorValue, ySelectorValue) assertEquals(x.target.source, y.target.source) diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/bookmark_annotations/ReaderBookmarkAnnotationsJSONTest.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/bookmark_annotations/ReaderBookmarkAnnotationsJSONTest.kt index b01db6a98..b694132b9 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/bookmark_annotations/ReaderBookmarkAnnotationsJSONTest.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/bookmark_annotations/ReaderBookmarkAnnotationsJSONTest.kt @@ -12,12 +12,14 @@ import org.nypl.simplified.bookmarks.api.BookmarkAnnotation import org.nypl.simplified.bookmarks.api.BookmarkAnnotationBodyNode import org.nypl.simplified.bookmarks.api.BookmarkAnnotationFirstNode import org.nypl.simplified.bookmarks.api.BookmarkAnnotationResponse -import org.nypl.simplified.bookmarks.api.BookmarkAnnotations import org.nypl.simplified.bookmarks.api.BookmarkAnnotationSelectorNode -import org.nypl.simplified.bookmarks.api.BookmarkAnnotationsJSON import org.nypl.simplified.bookmarks.api.BookmarkAnnotationTargetNode -import org.nypl.simplified.books.api.BookLocation +import org.nypl.simplified.bookmarks.api.BookmarkAnnotations +import org.nypl.simplified.bookmarks.api.BookmarkAnnotationsJSON import org.nypl.simplified.books.api.bookmark.BookmarkKind +import org.nypl.simplified.books.api.bookmark.SerializedLocatorHrefProgression20210317 +import org.nypl.simplified.books.api.bookmark.SerializedLocatorLegacyCFI +import org.nypl.simplified.books.api.bookmark.SerializedLocators import org.nypl.simplified.json.core.JSONParseException import org.slf4j.LoggerFactory import java.io.FileNotFoundException @@ -66,7 +68,7 @@ class ReaderBookmarkAnnotationsJSONTest { timestamp = "2019-01-25T20:00:37Z", device = "cca80416-3168-4e58-b621-7964b9265ac9", chapterTitle = "A Title", - bookProgress = null + bookProgress = 0.0f ) private val bookmark0 = @@ -262,16 +264,16 @@ class ReaderBookmarkAnnotationsJSONTest { node = this.resourceNode("valid-bookmark-0.json") ) - val bookmark = BookmarkAnnotations.toReaderBookmark(this.objectMapper, annotation) + val bookmark = BookmarkAnnotations.toSerializedBookmark(this.objectMapper, annotation) assertEquals("urn:uuid:1daa8de6-94e8-4711-b7d1-e43b572aa6e0", bookmark.opdsId) assertEquals("urn:uuid:c83db5b1-9130-4b86-93ea-634b00235c7c", bookmark.deviceID) assertEquals(BookmarkKind.BookmarkLastReadLocation, bookmark.kind) assertEquals("2021-03-12T16:32:49.000Z", bookmark.time.toString()) - assertEquals("", bookmark.chapterTitle) + assertEquals("", bookmark.bookChapterTitle) - val location = bookmark.location as BookLocation.BookLocationR2 - assertEquals(0.666, location.progress.chapterProgress, 0.0) - assertEquals("/xyz.html", location.progress.chapterHref) + val location = bookmark.location as SerializedLocatorHrefProgression20210317 + assertEquals(0.666, location.chapterProgress, 0.0) + assertEquals("/xyz.html", location.chapterHref) this.checkRoundTrip(annotation) } @@ -286,16 +288,16 @@ class ReaderBookmarkAnnotationsJSONTest { DateTimeUtils.setCurrentMillisFixed(0L) - val bookmark = BookmarkAnnotations.toReaderBookmark(this.objectMapper, annotation) + val bookmark = BookmarkAnnotations.toSerializedBookmark(this.objectMapper, annotation) assertEquals("urn:uuid:1daa8de6-94e8-4711-b7d1-e43b572aa6e0", bookmark.opdsId) assertEquals("urn:uuid:c83db5b1-9130-4b86-93ea-634b00235c7c", bookmark.deviceID) assertEquals(BookmarkKind.BookmarkLastReadLocation, bookmark.kind) assertEquals("2021-03-12T16:32:49.000Z", bookmark.time.toString()) - assertEquals("", bookmark.chapterTitle) + assertEquals("", bookmark.bookChapterTitle) - val location = bookmark.location as BookLocation.BookLocationR2 - assertEquals(0.666, location.progress.chapterProgress, 0.0) - assertEquals("/xyz.html", location.progress.chapterHref) + val location = bookmark.location as SerializedLocatorHrefProgression20210317 + assertEquals(0.666, location.chapterProgress, 0.0) + assertEquals("/xyz.html", location.chapterHref) this.checkRoundTrip(annotation) } @@ -310,16 +312,16 @@ class ReaderBookmarkAnnotationsJSONTest { DateTimeUtils.setCurrentMillisFixed(0L) - val bookmark = BookmarkAnnotations.toReaderBookmark(this.objectMapper, annotation) + val bookmark = BookmarkAnnotations.toSerializedBookmark(this.objectMapper, annotation) assertEquals("urn:uuid:1daa8de6-94e8-4711-b7d1-e43b572aa6e0", bookmark.opdsId) assertEquals("urn:uuid:c83db5b1-9130-4b86-93ea-634b00235c7c", bookmark.deviceID) assertEquals(BookmarkKind.BookmarkExplicit, bookmark.kind) assertEquals("2021-03-12T16:32:49.000Z", bookmark.time.toString()) - assertEquals("", bookmark.chapterTitle) + assertEquals("", bookmark.bookChapterTitle) - val location = bookmark.location as BookLocation.BookLocationR2 - assertEquals(0.666, location.progress.chapterProgress, 0.0) - assertEquals("/xyz.html", location.progress.chapterHref) + val location = bookmark.location as SerializedLocatorHrefProgression20210317 + assertEquals(0.666, location.chapterProgress, 0.0) + assertEquals("/xyz.html", location.chapterHref) this.checkRoundTrip(annotation) } @@ -334,16 +336,16 @@ class ReaderBookmarkAnnotationsJSONTest { DateTimeUtils.setCurrentMillisFixed(0L) - val bookmark = BookmarkAnnotations.toReaderBookmark(this.objectMapper, annotation) + val bookmark = BookmarkAnnotations.toSerializedBookmark(this.objectMapper, annotation) assertEquals("urn:uuid:1daa8de6-94e8-4711-b7d1-e43b572aa6e0", bookmark.opdsId) assertEquals("urn:uuid:c83db5b1-9130-4b86-93ea-634b00235c7c", bookmark.deviceID) assertEquals(BookmarkKind.BookmarkExplicit, bookmark.kind) assertEquals("2021-03-12T16:32:49.000Z", bookmark.time.toString()) - assertEquals("", bookmark.chapterTitle) + assertEquals("", bookmark.bookChapterTitle) - val location = bookmark.location as BookLocation.BookLocationR2 - assertEquals(0.666, location.progress.chapterProgress, 0.0) - assertEquals("/xyz.html", location.progress.chapterHref) + val location = bookmark.location as SerializedLocatorHrefProgression20210317 + assertEquals(0.666, location.chapterProgress, 0.0) + assertEquals("/xyz.html", location.chapterHref) this.checkRoundTrip(annotation) } @@ -351,26 +353,20 @@ class ReaderBookmarkAnnotationsJSONTest { @Test fun testSpecValidLocator0() { val location = - BookmarkAnnotationsJSON.deserializeReaderLocation( - objectMapper = this.objectMapper, - value = this.resourceText("valid-locator-0.json") - ) + SerializedLocators.parseLocatorFromString(this.resourceText("valid-locator-0.json")) - val locationHP = location as BookLocation.BookLocationR2 - assertEquals(0.666, locationHP.progress.chapterProgress, 0.0) - assertEquals("/xyz.html", locationHP.progress.chapterHref) + val locationHP = location as SerializedLocatorHrefProgression20210317 + assertEquals(0.666, locationHP.chapterProgress, 0.0) + assertEquals("/xyz.html", locationHP.chapterHref) } @Test fun testSpecValidLocator1() { val location = - BookmarkAnnotationsJSON.deserializeReaderLocation( - objectMapper = this.objectMapper, - value = this.resourceText("valid-locator-1.json") - ) + SerializedLocators.parseLocatorFromString(this.resourceText("valid-locator-1.json")) - val locationR1 = location as BookLocation.BookLocationR1 - assertEquals(0.25, locationR1.progress!!, 0.0) + val locationR1 = location as SerializedLocatorLegacyCFI + assertEquals(0.25, locationR1.chapterProgression, 0.0) assertEquals("xyz-html", locationR1.idRef) assertEquals("/4/2/2/2", locationR1.contentCFI) } @@ -481,11 +477,11 @@ class ReaderBookmarkAnnotationsJSONTest { this.compareAnnotations(bookmarkAnnotation, deserialized) val toBookmark = - BookmarkAnnotations.toReaderBookmark(this.objectMapper, deserialized) + BookmarkAnnotations.toSerializedBookmark(this.objectMapper, deserialized) val fromBookmark = - BookmarkAnnotations.fromReaderBookmark(this.objectMapper, toBookmark) + BookmarkAnnotations.fromSerializedBookmark(this.objectMapper, toBookmark) val toBookmarkAgain = - BookmarkAnnotations.toReaderBookmark(this.objectMapper, fromBookmark) + BookmarkAnnotations.toSerializedBookmark(this.objectMapper, fromBookmark) this.compareAnnotations(bookmarkAnnotation, deserialized) this.compareAnnotations(bookmarkAnnotation, fromBookmark) @@ -510,9 +506,9 @@ class ReaderBookmarkAnnotationsJSONTest { assertEquals(x.target.selector.type, y.target.selector.type) val xSelectorValue = - BookmarkAnnotationsJSON.deserializeReaderLocation(this.objectMapper, x.target.selector.value) + SerializedLocators.parseLocatorFromString(x.target.selector.value) val ySelectorValue = - BookmarkAnnotationsJSON.deserializeReaderLocation(this.objectMapper, y.target.selector.value) + SerializedLocators.parseLocatorFromString(y.target.selector.value) assertEquals(xSelectorValue, ySelectorValue) assertEquals(x.target.source, y.target.source) diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/bookmarks/AudiobookBookmarkJSONTest.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/bookmarks/AudiobookBookmarkJSONTest.kt deleted file mode 100644 index 2d3410c25..000000000 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/bookmarks/AudiobookBookmarkJSONTest.kt +++ /dev/null @@ -1,107 +0,0 @@ -package org.nypl.simplified.tests.bookmarks - -import com.fasterxml.jackson.databind.ObjectMapper -import org.joda.time.DateTimeZone -import org.joda.time.format.DateTimeFormatter -import org.joda.time.format.ISODateTimeFormat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.nypl.simplified.books.api.bookmark.BookmarkJSON -import org.nypl.simplified.books.api.bookmark.BookmarkKind - -class AudiobookBookmarkJSONTest { - - private lateinit var objectMapper: ObjectMapper - private lateinit var formatter: DateTimeFormatter - - @BeforeEach - fun testSetup() { - this.objectMapper = ObjectMapper() - this.formatter = ISODateTimeFormat.dateTime().withZoneUTC() - } - - @AfterEach - fun tearDown() { - DateTimeZone.setDefault(DateTimeZone.getDefault()) - } - - @Test - fun testDeserializeJSON() { - val bookmark = BookmarkJSON.deserializeAudiobookBookmarkFromString( - objectMapper = this.objectMapper, - kind = BookmarkKind.BookmarkLastReadLocation, - serialized = """ - { - "@version" : 2, - "opdsId" : "urn:isbn:9781683609438", - "location" : { - "chapter" : 1, - "part" : 2, - "title" : "Is That You, Walt Whitman?", - "time" : 100000 - }, - "time" : "2022-06-27T14:51:46.238", - "chapterTitle" : "Is That You, Walt Whitman?", - "deviceID" : "null" - } - """ - ) - - assertEquals(100000, bookmark.location.startOffset) - assertEquals(1, bookmark.location.chapter) - assertEquals(2, bookmark.location.part) - assertEquals("Is That You, Walt Whitman?", bookmark.location.title) - - val serializedText = - BookmarkJSON.serializeAudiobookBookmarkToString(bookmark) - val serialized = - BookmarkJSON.deserializeAudiobookBookmarkFromString( - objectMapper = this.objectMapper, - kind = bookmark.kind, - serialized = serializedText - ) - assertEquals(bookmark, serialized) - } - - @Test - fun testDeserializeJSONV3() { - val bookmark = BookmarkJSON.deserializeAudiobookBookmarkFromString( - objectMapper = this.objectMapper, - kind = BookmarkKind.BookmarkLastReadLocation, - serialized = """ - { - "@version" : 3, - "opdsId" : "urn:isbn:9781683609438", - "location" : { - "chapter" : 1, - "part" : 2, - "title" : "Is That You, Walt Whitman?", - "startOffset" : 28000, - "time" : 100000 - }, - "time" : "2022-06-27T14:51:46.238", - "chapterTitle" : "Is That You, Walt Whitman?", - "deviceID" : "null" - } - """ - ) - - assertEquals(28000, bookmark.location.startOffset) - assertEquals(100000, bookmark.location.currentOffset) - assertEquals(1, bookmark.location.chapter) - assertEquals(2, bookmark.location.part) - assertEquals("Is That You, Walt Whitman?", bookmark.location.title) - - val serializedText = - BookmarkJSON.serializeAudiobookBookmarkToString(bookmark) - val serialized = - BookmarkJSON.deserializeAudiobookBookmarkFromString( - objectMapper = this.objectMapper, - kind = bookmark.kind, - serialized = serializedText - ) - assertEquals(bookmark, serialized) - } -} diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/bookmarks/ReaderBookmarkJSONTest.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/bookmarks/ReaderBookmarkJSONTest.kt deleted file mode 100644 index cebb6ee41..000000000 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/bookmarks/ReaderBookmarkJSONTest.kt +++ /dev/null @@ -1,346 +0,0 @@ -package org.nypl.simplified.tests.bookmarks - -import com.fasterxml.jackson.databind.ObjectMapper -import org.joda.time.DateTimeZone -import org.joda.time.format.DateTimeFormatter -import org.joda.time.format.ISODateTimeFormat -import org.junit.jupiter.api.AfterEach -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertThrows -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.BeforeEach -import org.junit.jupiter.api.Test -import org.nypl.simplified.books.api.BookLocation -import org.nypl.simplified.books.api.bookmark.Bookmark -import org.nypl.simplified.books.api.bookmark.BookmarkJSON -import org.nypl.simplified.books.api.bookmark.BookmarkKind -import org.nypl.simplified.json.core.JSONParseException -import org.nypl.simplified.tests.bookmark_annotations.ReaderBookmarkAnnotationsJSONTest -import java.io.FileNotFoundException -import java.io.InputStream - -class ReaderBookmarkJSONTest { - - private lateinit var objectMapper: ObjectMapper - private lateinit var formatter: DateTimeFormatter - - @BeforeEach - fun testSetup() { - this.objectMapper = ObjectMapper() - this.formatter = ISODateTimeFormat.dateTime().withZoneUTC() - } - - @AfterEach - fun tearDown() { - DateTimeZone.setDefault(DateTimeZone.getDefault()) - } - - /** - * Deserialize JSON representing a bookmark with a top-level chapterProgress property. Older - * bookmarks had this structure. The top-level chapterProgress should be deserialized into - * location.progress.chapterProgress. - */ - - @Test - fun testDeserializeJSONWithTopLevelChapterProgress() { - val bookmark = BookmarkJSON.deserializeReaderBookmarkFromString( - objectMapper = this.objectMapper, - kind = BookmarkKind.BookmarkExplicit, - serialized = """ - { - "opdsId" : "urn:isbn:9781683609438", - "location" : { - "contentCFI" : "/4/2[is-that-you-walt-whitman]/4[is-that-you-walt-whitman-text]/78/1:287", - "idref" : "is-that-you-walt-whitman-xhtml" - }, - "time" : "2020-09-16T14:51:46.238", - "chapterTitle" : "Is That You, Walt Whitman?", - "chapterProgress" : 0.4736842215061188, - "bookProgress" : 0.49, - "deviceID" : "null" - } - """ - ) - - assertEquals(0.4736842215061188, bookmark.chapterProgress, .0001) - - this.checkRoundTrip(bookmark) - } - - /** - * Deserialize JSON representing a bookmark with chapterProgress nested in location.progress. - */ - - @Test - fun testDeserializeJSONWithNestedChapterProgress() { - val bookmark = BookmarkJSON.deserializeReaderBookmarkFromString( - objectMapper = this.objectMapper, - kind = BookmarkKind.BookmarkExplicit, - serialized = """ - { - "opdsId" : "urn:isbn:9781683601111", - "location" : { - "contentCFI" : "/4/2[the-end-of-coney-island-avenue]/4[the-end-of-coney-island-avenue-text]/84/1:325", - "idref" : "the-end-of-coney-island-avenue-xhtml", - "progress" : { - "chapterIndex" : 9, - "chapterProgress" : 0.4285714328289032 - } - }, - "time" : "2020-09-16T19:07:21.455", - "chapterTitle" : "The End of Coney Island Avenue", - "chapterProgress" : 0.4285714328289032, - "bookProgress" : 0.34, - "deviceID" : "null" - } - """ - ) - - assertEquals(0.4285714328289032, bookmark.chapterProgress, .0001) - - this.checkRoundTrip(bookmark) - } - - @Test - fun testBookmark20210317_r1_0() { - DateTimeZone.setDefault(DateTimeZone.getDefault()) - - val text = this.resourceText("bookmark-20210317-r1-0.json") - val bookmark = BookmarkJSON.deserializeReaderBookmarkFromString( - objectMapper = this.objectMapper, - kind = BookmarkKind.BookmarkExplicit, - serialized = text - ) - - assertEquals("2021-01-21T19:16:54.066Z", this.formatter.print(bookmark.time)) - assertEquals("urn:isbn:9781683607144", bookmark.opdsId) - assertEquals("A title!", bookmark.chapterTitle) - assertEquals("fc4f5d19-43a2-4181-99a0-7579e0a4935b", bookmark.deviceID) - assertEquals(BookmarkKind.BookmarkExplicit, bookmark.kind) - - val location = bookmark.location as BookLocation.BookLocationR1 - assertEquals("/4/2[title-page]/2/2/1:0", location.contentCFI) - assertEquals("title-page-xhtml", location.idRef) - assertEquals(0.25, location.progress) - - this.checkRoundTrip(bookmark) - } - - @Test - fun testBookmark20210317_r2_0() { - DateTimeZone.setDefault(DateTimeZone.getDefault()) - - val text = this.resourceText("bookmark-20210317-r2-0.json") - val bookmark = BookmarkJSON.deserializeReaderBookmarkFromString( - objectMapper = this.objectMapper, - kind = BookmarkKind.BookmarkExplicit, - serialized = text - ) - - assertEquals("2021-01-21T19:16:54.066Z", this.formatter.print(bookmark.time)) - assertEquals("urn:isbn:9781683607144", bookmark.opdsId) - assertEquals("Another title", bookmark.chapterTitle) - assertEquals("null", bookmark.deviceID) - assertEquals(BookmarkKind.BookmarkExplicit, bookmark.kind) - - val location = bookmark.location as BookLocation.BookLocationR2 - assertEquals(0.25, location.progress.chapterProgress, 0.0) - assertEquals("/title-page.xhtml", location.progress.chapterHref) - - this.checkRoundTrip(bookmark) - } - - @Test - fun testBookmarkLegacyR1_0() { - DateTimeZone.setDefault(DateTimeZone.getDefault()) - - val text = this.resourceText("bookmark-legacy-r1-0.json") - val bookmark = BookmarkJSON.deserializeReaderBookmarkFromString( - objectMapper = this.objectMapper, - kind = BookmarkKind.BookmarkExplicit, - serialized = text - ) - - assertEquals("2021-01-21T19:16:54.066Z", this.formatter.print(bookmark.time)) - assertEquals("urn:isbn:9781683607144", bookmark.opdsId) - assertEquals("Some title", bookmark.chapterTitle) - assertEquals("70c47074-c048-48c0-8eae-286b9738c108", bookmark.deviceID) - assertEquals(BookmarkKind.BookmarkExplicit, bookmark.kind) - - val location = bookmark.location as BookLocation.BookLocationR1 - assertEquals("/4/2[title-page]/2/2/1:0", location.contentCFI) - assertEquals("title-page-xhtml", location.idRef) - assertEquals(0.30, location.progress) - - this.checkRoundTrip(bookmark) - } - - @Test - fun testBookmarkLegacyR1_1() { - DateTimeZone.setDefault(DateTimeZone.getDefault()) - - val text = this.resourceText("bookmark-legacy-r1-1.json") - val bookmark = BookmarkJSON.deserializeReaderBookmarkFromString( - objectMapper = this.objectMapper, - kind = BookmarkKind.BookmarkExplicit, - serialized = text - ) - - assertEquals("2021-03-17T15:19:56.465Z", this.formatter.print(bookmark.time)) - assertEquals("urn:isbn:9781683606123", bookmark.opdsId) - assertEquals("Unknown", bookmark.chapterTitle) - assertEquals("null", bookmark.deviceID) - assertEquals(BookmarkKind.BookmarkExplicit, bookmark.kind) - - val location = bookmark.location as BookLocation.BookLocationR1 - assertEquals("/4/2[cover-image]/2", location.contentCFI) - assertEquals("Cover", location.idRef) - assertEquals(0.3, location.progress) - - this.checkRoundTrip(bookmark) - } - - private fun checkRoundTrip(bookmark: Bookmark.ReaderBookmark) { - val serializedText = - BookmarkJSON.serializeReaderBookmarkToString(this.objectMapper, bookmark) - val serialized = - BookmarkJSON.deserializeReaderBookmarkFromString( - objectMapper = this.objectMapper, - kind = bookmark.kind, - serialized = serializedText - ) - assertEquals(bookmark, serialized) - } - - @Test - fun testBookmarkLegacyR2_0() { - DateTimeZone.setDefault(DateTimeZone.getDefault()) - - val text = this.resourceText("bookmark-legacy-r2-0.json") - - val ex = assertThrows(JSONParseException::class.java) { - BookmarkJSON.deserializeReaderBookmarkFromString( - objectMapper = this.objectMapper, - kind = BookmarkKind.BookmarkExplicit, - serialized = text - ) - } - assertTrue(ex.message!!.contains("Unsupported book location format version: (unspecified)")) - } - - private fun resourceText( - name: String - ): String { - return this.resource(name).readBytes().decodeToString() - } - - private fun resource( - name: String - ): InputStream { - val fileName = - "/org/nypl/simplified/tests/bookmarks/$name" - val url = - ReaderBookmarkAnnotationsJSONTest::class.java.getResource(fileName) - ?: throw FileNotFoundException("No such resource: $fileName") - return url.openStream() - } - - @Test - fun testBookmark20210317_r1_0_UTC() { - DateTimeZone.setDefault(DateTimeZone.UTC) - - val text = this.resourceText("bookmark-20210317-r1-0.json") - val bookmark = BookmarkJSON.deserializeReaderBookmarkFromString( - objectMapper = this.objectMapper, - kind = BookmarkKind.BookmarkExplicit, - serialized = text - ) - - assertEquals("2021-01-21T19:16:54.066Z", this.formatter.print(bookmark.time)) - assertEquals("urn:isbn:9781683607144", bookmark.opdsId) - assertEquals("A title!", bookmark.chapterTitle) - assertEquals("fc4f5d19-43a2-4181-99a0-7579e0a4935b", bookmark.deviceID) - assertEquals(BookmarkKind.BookmarkExplicit, bookmark.kind) - - val location = bookmark.location as BookLocation.BookLocationR1 - assertEquals("/4/2[title-page]/2/2/1:0", location.contentCFI) - assertEquals("title-page-xhtml", location.idRef) - assertEquals(0.25, location.progress) - - this.checkRoundTrip(bookmark) - } - - @Test - fun testBookmark20210317_r2_0_UTC() { - DateTimeZone.setDefault(DateTimeZone.UTC) - - val text = this.resourceText("bookmark-20210317-r2-0.json") - val bookmark = BookmarkJSON.deserializeReaderBookmarkFromString( - objectMapper = this.objectMapper, - kind = BookmarkKind.BookmarkExplicit, - serialized = text - ) - - assertEquals("2021-01-21T19:16:54.066Z", this.formatter.print(bookmark.time)) - assertEquals("urn:isbn:9781683607144", bookmark.opdsId) - assertEquals("Another title", bookmark.chapterTitle) - assertEquals("null", bookmark.deviceID) - assertEquals(BookmarkKind.BookmarkExplicit, bookmark.kind) - - val location = bookmark.location as BookLocation.BookLocationR2 - assertEquals(0.25, location.progress.chapterProgress, 0.0) - assertEquals("/title-page.xhtml", location.progress.chapterHref) - - this.checkRoundTrip(bookmark) - } - - @Test - fun testBookmarkLegacyR1_0_UTC() { - DateTimeZone.setDefault(DateTimeZone.UTC) - - val text = this.resourceText("bookmark-legacy-r1-0.json") - val bookmark = BookmarkJSON.deserializeReaderBookmarkFromString( - objectMapper = this.objectMapper, - kind = BookmarkKind.BookmarkExplicit, - serialized = text - ) - - assertEquals("2021-01-21T19:16:54.066Z", this.formatter.print(bookmark.time)) - assertEquals("urn:isbn:9781683607144", bookmark.opdsId) - assertEquals("Some title", bookmark.chapterTitle) - assertEquals("70c47074-c048-48c0-8eae-286b9738c108", bookmark.deviceID) - assertEquals(BookmarkKind.BookmarkExplicit, bookmark.kind) - - val location = bookmark.location as BookLocation.BookLocationR1 - assertEquals("/4/2[title-page]/2/2/1:0", location.contentCFI) - assertEquals("title-page-xhtml", location.idRef) - assertEquals(0.30, location.progress) - - this.checkRoundTrip(bookmark) - } - - @Test - fun testBookmarkLegacyR1_1_UTC() { - DateTimeZone.setDefault(DateTimeZone.UTC) - - val text = this.resourceText("bookmark-legacy-r1-1.json") - val bookmark = BookmarkJSON.deserializeReaderBookmarkFromString( - objectMapper = this.objectMapper, - kind = BookmarkKind.BookmarkExplicit, - serialized = text - ) - - assertEquals("2021-03-17T15:19:56.465Z", this.formatter.print(bookmark.time)) - assertEquals("urn:isbn:9781683606123", bookmark.opdsId) - assertEquals("Unknown", bookmark.chapterTitle) - assertEquals("null", bookmark.deviceID) - assertEquals(BookmarkKind.BookmarkExplicit, bookmark.kind) - - val location = bookmark.location as BookLocation.BookLocationR1 - assertEquals("/4/2[cover-image]/2", location.contentCFI) - assertEquals("Cover", location.idRef) - assertEquals(0.3, location.progress) - - this.checkRoundTrip(bookmark) - } -} diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/AccountBug0613d7f6.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/AccountBug0613d7f6.kt index ba1491610..4bf91d18e 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/AccountBug0613d7f6.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/AccountBug0613d7f6.kt @@ -1,7 +1,6 @@ package org.nypl.simplified.tests.books import android.app.Application -import android.content.Context import io.reactivex.subjects.PublishSubject import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/accounts/AccountsDatabaseContract.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/accounts/AccountsDatabaseContract.kt index 8a0cbd297..82dbe1875 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/accounts/AccountsDatabaseContract.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/accounts/AccountsDatabaseContract.kt @@ -1,7 +1,6 @@ package org.nypl.simplified.tests.books.accounts import android.app.Application -import android.content.Context import io.reactivex.subjects.PublishSubject import org.hamcrest.BaseMatcher import org.hamcrest.Description diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/accounts/AccountsDatabaseTest.java b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/accounts/AccountsDatabaseTest.java index d735f35d8..8fd29febc 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/accounts/AccountsDatabaseTest.java +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/accounts/AccountsDatabaseTest.java @@ -1,7 +1,6 @@ package org.nypl.simplified.tests.books.accounts; import android.app.Application; -import android.content.Context; import org.mockito.Mockito; diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/audio/AudioBookManifestStrategyTest.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/audio/AudioBookManifestStrategyTest.kt index c546c5f57..b6de13de2 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/audio/AudioBookManifestStrategyTest.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/audio/AudioBookManifestStrategyTest.kt @@ -1,6 +1,7 @@ package org.nypl.simplified.tests.books.audio import android.app.Application +import io.reactivex.Observable import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test @@ -22,7 +23,6 @@ import org.nypl.simplified.taskrecorder.api.TaskResult import org.nypl.simplified.tests.MutableServiceDirectory import org.nypl.simplified.tests.TestDirectories import org.slf4j.LoggerFactory -import rx.Observable import java.io.File import java.net.URI diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/audio/AudioBookSucceedingParsers.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/audio/AudioBookSucceedingParsers.kt index b24fcabed..66a9ef8f2 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/audio/AudioBookSucceedingParsers.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/audio/AudioBookSucceedingParsers.kt @@ -3,6 +3,8 @@ package org.nypl.simplified.tests.books.audio import org.librarysimplified.audiobook.manifest.api.PlayerManifest import org.librarysimplified.audiobook.manifest.api.PlayerManifestLink import org.librarysimplified.audiobook.manifest.api.PlayerManifestMetadata +import org.librarysimplified.audiobook.manifest.api.PlayerManifestReadingOrderID +import org.librarysimplified.audiobook.manifest.api.PlayerManifestReadingOrderItem import org.librarysimplified.audiobook.manifest_parser.api.ManifestParsersType import org.librarysimplified.audiobook.manifest_parser.extension_spi.ManifestParserExtensionType import org.librarysimplified.audiobook.parser.api.ParseResult @@ -13,7 +15,12 @@ object AudioBookSucceedingParsers : ManifestParsersType { val playerManifest = PlayerManifest( originalBytes = ByteArray(23), - readingOrder = listOf(PlayerManifestLink.LinkBasic(URI.create("http://www.example.com"))), + readingOrder = listOf( + PlayerManifestReadingOrderItem( + PlayerManifestReadingOrderID("urn:0"), + PlayerManifestLink.LinkBasic(URI.create("http://www.example.com")) + ) + ), metadata = PlayerManifestMetadata( title = "A book", identifier = "c925eb26-ab0c-44e2-9bec-ca4c38c0b6c8", diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabaseAudioBookContract.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabaseAudioBookContract.kt index 2faf1c663..eb8a48de9 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabaseAudioBookContract.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabaseAudioBookContract.kt @@ -1,7 +1,6 @@ package org.nypl.simplified.tests.books.book_database import android.app.Application -import android.content.Context import com.io7m.jfunctional.Option import org.joda.time.DateTime import org.junit.jupiter.api.Test diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabaseAudioBookTest.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabaseAudioBookTest.kt index ccaf01f66..84a7857fd 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabaseAudioBookTest.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabaseAudioBookTest.kt @@ -1,7 +1,6 @@ package org.nypl.simplified.tests.books.book_database import android.app.Application -import android.content.Context import org.mockito.Mockito class BookDatabaseAudioBookTest : BookDatabaseAudioBookContract() { diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabaseContract.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabaseContract.kt index 364b8a4e1..ac02b53b9 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabaseContract.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabaseContract.kt @@ -1,7 +1,6 @@ package org.nypl.simplified.tests.books.book_database import android.app.Application -import android.content.Context import com.io7m.jfunctional.Option import one.irradia.mime.api.MIMEType import one.irradia.mime.vanilla.MIMEParser @@ -17,8 +16,9 @@ import org.nypl.simplified.books.api.BookFormat.BookFormatAudioBook import org.nypl.simplified.books.api.BookFormat.BookFormatEPUB import org.nypl.simplified.books.api.BookFormat.BookFormatPDF import org.nypl.simplified.books.api.BookID -import org.nypl.simplified.books.api.bookmark.Bookmark import org.nypl.simplified.books.api.bookmark.BookmarkKind +import org.nypl.simplified.books.api.bookmark.SerializedBookmark20210828 +import org.nypl.simplified.books.api.bookmark.SerializedLocatorAudioBookTime1 import org.nypl.simplified.books.book_database.BookDRMInformationHandleACS import org.nypl.simplified.books.book_database.BookDRMInformationHandleNone import org.nypl.simplified.books.book_database.BookDatabase @@ -632,14 +632,25 @@ abstract class BookDatabaseContract { val format = databaseEntry.findFormatHandle(BookDatabaseEntryFormatHandleAudioBook::class.java) format!! format.setLastReadLocation( - Bookmark.AudiobookBookmark.create( + SerializedBookmark20210828( opdsId = feedEntry.id, kind = BookmarkKind.BookmarkLastReadLocation, - location = PlayerPosition(title = "Title", part = 0, chapter = 1, startOffset = 1000L, currentOffset = 230000L), + location = SerializedLocatorAudioBookTime1( + audioBookId = "X", + chapter = 1, + part = 0, + startOffsetMilliseconds = 1000L, + timeMilliseconds = 230000L, + duration = 500000L, + title = "Title" + ), deviceID = "", time = DateTime.now(), uri = null, - duration = 500000L + bookChapterProgress = 0.5, + bookChapterTitle = "Chapter", + bookTitle = "Title", + bookProgress = 0.25 ) ) @@ -647,33 +658,46 @@ abstract class BookDatabaseContract { val book = databaseEntry.book val bookFormat = book.findFormat(BookFormatAudioBook::class.java) val lastReadLocation = bookFormat!!.lastReadLocation!! - Assertions.assertEquals("Title", lastReadLocation.location.title) - Assertions.assertEquals(0, lastReadLocation.location.part) - Assertions.assertEquals(1, lastReadLocation.location.chapter) - Assertions.assertEquals(1000, lastReadLocation.location.startOffset) - Assertions.assertEquals(230000, lastReadLocation.location.currentOffset) + val location = lastReadLocation.location as SerializedLocatorAudioBookTime1 + Assertions.assertEquals("Title", location.title) + Assertions.assertEquals(0, location.part) + Assertions.assertEquals(1, location.chapter) + Assertions.assertEquals(1000, location.startOffsetMilliseconds) + Assertions.assertEquals(230000, location.timeMilliseconds) } format.setLastReadLocation( - Bookmark.AudiobookBookmark.create( + SerializedBookmark20210828( opdsId = feedEntry.id, kind = BookmarkKind.BookmarkLastReadLocation, - location = PlayerPosition(title = "Title 2", part = 2, chapter = 3, currentOffset = 46000, startOffset = 0), + location = SerializedLocatorAudioBookTime1( + audioBookId = "X", + chapter = 3, + part = 2, + title = "Title 2", + timeMilliseconds = 46000L, + startOffsetMilliseconds = 0L, + duration = 80000L + ), deviceID = "", time = DateTime.now(), uri = null, - duration = 80000 + bookChapterProgress = 0.5, + bookChapterTitle = "Chapter", + bookTitle = "Title", + bookProgress = 0.25 ) ) run { val book = databaseEntry.book val bookFormat = book.findFormat(BookFormatAudioBook::class.java) val lastReadLocation = bookFormat!!.lastReadLocation!! - Assertions.assertEquals("Title 2", lastReadLocation.location.title) - Assertions.assertEquals(2, lastReadLocation.location.part) - Assertions.assertEquals(3, lastReadLocation.location.chapter) - Assertions.assertEquals(0, lastReadLocation.location.startOffset) - Assertions.assertEquals(46000, lastReadLocation.location.currentOffset) + val location = lastReadLocation.location as SerializedLocatorAudioBookTime1 + Assertions.assertEquals("Title 2", location.title) + Assertions.assertEquals(2, location.part) + Assertions.assertEquals(3, location.chapter) + Assertions.assertEquals(0, location.startOffsetMilliseconds) + Assertions.assertEquals(46000, location.timeMilliseconds) } format.setLastReadLocation(null) diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabaseEPUBContract.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabaseEPUBContract.kt index 6aa839501..e67d23fe4 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabaseEPUBContract.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabaseEPUBContract.kt @@ -1,20 +1,22 @@ package org.nypl.simplified.tests.books.book_database import android.app.Application -import android.content.Context import com.io7m.jfunctional.Option import one.irradia.mime.vanilla.MIMEParser import org.joda.time.DateTime import org.joda.time.DateTimeZone -import org.junit.jupiter.api.Assertions +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.Test import org.nypl.simplified.accounts.api.AccountID import org.nypl.simplified.books.api.BookDRMInformation import org.nypl.simplified.books.api.BookDRMKind import org.nypl.simplified.books.api.BookIDs -import org.nypl.simplified.books.api.BookLocation -import org.nypl.simplified.books.api.bookmark.Bookmark import org.nypl.simplified.books.api.bookmark.BookmarkKind +import org.nypl.simplified.books.api.bookmark.SerializedBookmark +import org.nypl.simplified.books.api.bookmark.SerializedBookmark20210828 +import org.nypl.simplified.books.api.bookmark.SerializedBookmarks +import org.nypl.simplified.books.api.bookmark.SerializedLocatorLegacyCFI import org.nypl.simplified.books.book_database.BookDRMInformationHandleACS import org.nypl.simplified.books.book_database.BookDRMInformationHandleLCP import org.nypl.simplified.books.book_database.BookDRMInformationHandleNone @@ -62,20 +64,22 @@ abstract class BookDatabaseEPUBContract { val formatHandle = databaseEntry0.findFormatHandle(BookDatabaseEntryFormatHandleEPUB::class.java) - Assertions.assertTrue(formatHandle != null, "Format is present") + assertTrue(formatHandle != null, "Format is present") formatHandle!! - Assertions.assertEquals(null, formatHandle.format.lastReadLocation) + assertEquals(null, formatHandle.format.lastReadLocation) val bookmark = - Bookmark.ReaderBookmark.create( + SerializedBookmark20210828( + bookTitle = "Title", opdsId = "abcd", - location = BookLocation.BookLocationR1( - progress = 0.5, + location = SerializedLocatorLegacyCFI( + chapterProgression = 0.5, contentCFI = "xyz", idRef = "abc" ), time = DateTime.now(DateTimeZone.UTC), - chapterTitle = "A title", + bookChapterProgress = 0.5, + bookChapterTitle = "A title", kind = BookmarkKind.BookmarkLastReadLocation, bookProgress = 0.25, uri = null, @@ -83,10 +87,10 @@ abstract class BookDatabaseEPUBContract { ) formatHandle.setLastReadLocation(bookmark) - Assertions.assertEquals(bookmark, formatHandle.format.lastReadLocation) + assertEquals(bookmark, formatHandle.format.lastReadLocation) formatHandle.setLastReadLocation(null) - Assertions.assertEquals(null, formatHandle.format.lastReadLocation) + assertEquals(null, formatHandle.format.lastReadLocation) } } @@ -108,49 +112,55 @@ abstract class BookDatabaseEPUBContract { val databaseEntry0 = database0.createOrUpdate(bookID, feedEntry) val bookmark0 = - Bookmark.ReaderBookmark.create( + SerializedBookmarks.createWithCurrentFormat( + bookTitle = "Title", opdsId = "abcd", - location = BookLocation.BookLocationR1( - progress = 0.5, + location = SerializedLocatorLegacyCFI( + chapterProgression = 0.5, contentCFI = "xyz", idRef = "abc" ), time = DateTime.now(DateTimeZone.UTC), kind = BookmarkKind.BookmarkExplicit, - chapterTitle = "A title", + bookChapterTitle = "A title", bookProgress = 0.25, + bookChapterProgress = 0.5, uri = null, deviceID = "3475fa24-25ca-4ddb-9d7b-762358d5f83a" ) val bookmark1 = - Bookmark.ReaderBookmark.create( + SerializedBookmarks.createWithCurrentFormat( + bookTitle = "Title", opdsId = "abcd", - location = BookLocation.BookLocationR1( - progress = 0.6, + location = SerializedLocatorLegacyCFI( + chapterProgression = 0.6, contentCFI = "xyz", idRef = "abc" ), time = DateTime.now(DateTimeZone.UTC), kind = BookmarkKind.BookmarkExplicit, - chapterTitle = "A title", + bookChapterTitle = "A title", bookProgress = 0.25, + bookChapterProgress = 0.6, uri = null, deviceID = "3475fa24-25ca-4ddb-9d7b-762358d5f83a" ) val bookmark2 = - Bookmark.ReaderBookmark.create( + SerializedBookmarks.createWithCurrentFormat( + bookTitle = "Title", opdsId = "abcd", - location = BookLocation.BookLocationR1( - progress = 0.7, + location = SerializedLocatorLegacyCFI( + chapterProgression = 0.7, contentCFI = "xyz", idRef = "abc" ), time = DateTime.now(DateTimeZone.UTC), kind = BookmarkKind.BookmarkExplicit, - chapterTitle = "A title", + bookChapterTitle = "A title", bookProgress = 0.25, + bookChapterProgress = 0.7, uri = null, deviceID = "3475fa24-25ca-4ddb-9d7b-762358d5f83a" ) @@ -162,15 +172,16 @@ abstract class BookDatabaseEPUBContract { val formatHandle = databaseEntry0.findFormatHandle(BookDatabaseEntryFormatHandleEPUB::class.java) - Assertions.assertTrue(formatHandle != null, "Format is present") + assertTrue(formatHandle != null, "Format is present") formatHandle!! - Assertions.assertEquals(listOf(), formatHandle.format.bookmarks) + assertEquals(listOf(), formatHandle.format.bookmarks) - formatHandle.setBookmarks(bookmarks0) - Assertions.assertEquals(bookmarks0, formatHandle.format.bookmarks) + formatHandle.addBookmark(bookmark0) + assertEquals(bookmarks0, formatHandle.format.bookmarks) - formatHandle.setBookmarks(bookmarks1) - Assertions.assertEquals(bookmarks1, formatHandle.format.bookmarks) + formatHandle.addBookmark(bookmark1) + formatHandle.addBookmark(bookmark2) + assertEquals(bookmarks1, formatHandle.format.bookmarks) } val database1 = @@ -182,10 +193,17 @@ abstract class BookDatabaseEPUBContract { val formatHandle = databaseEntry1.findFormatHandle(BookDatabaseEntryFormatHandleEPUB::class.java) - Assertions.assertTrue(formatHandle != null, "Format is present") + assertTrue(formatHandle != null, "Format is present") formatHandle!! - Assertions.assertEquals(bookmarks1, formatHandle.format.bookmarks) + + val expectedBookmarks = bookmarks1.sortedBy { b -> b.bookmarkId.value } + val receivedBookmarks = formatHandle.format.bookmarks.sortedBy { b -> b.bookmarkId.value } + + assertEquals(expectedBookmarks[0], receivedBookmarks[0].withoutURI()) + assertEquals(expectedBookmarks[1], receivedBookmarks[1].withoutURI()) + assertEquals(expectedBookmarks[2], receivedBookmarks[2].withoutURI()) + assertEquals(expectedBookmarks.size, receivedBookmarks.size) } } diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabaseEPUBTest.java b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabaseEPUBTest.java index 3aa3de440..e561e3724 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabaseEPUBTest.java +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabaseEPUBTest.java @@ -1,7 +1,6 @@ package org.nypl.simplified.tests.books.book_database; import android.app.Application; -import android.content.Context; import org.mockito.Mockito; diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabasePDFContract.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabasePDFContract.kt index 251a3b325..03771a8c5 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabasePDFContract.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabasePDFContract.kt @@ -1,7 +1,6 @@ package org.nypl.simplified.tests.books.book_database import android.app.Application -import android.content.Context import com.io7m.jfunctional.Option import org.joda.time.DateTime import org.junit.jupiter.api.Assertions @@ -10,8 +9,9 @@ import org.nypl.simplified.accounts.api.AccountID import org.nypl.simplified.books.api.BookDRMInformation import org.nypl.simplified.books.api.BookDRMKind import org.nypl.simplified.books.api.BookIDs -import org.nypl.simplified.books.api.bookmark.Bookmark import org.nypl.simplified.books.api.bookmark.BookmarkKind +import org.nypl.simplified.books.api.bookmark.SerializedBookmark20210828 +import org.nypl.simplified.books.api.bookmark.SerializedLocatorPage1 import org.nypl.simplified.books.book_database.BookDRMInformationHandleACS import org.nypl.simplified.books.book_database.BookDRMInformationHandleLCP import org.nypl.simplified.books.book_database.BookDRMInformationHandleNone @@ -52,7 +52,14 @@ abstract class BookDatabasePDFContract { val parser = OPDSJSONParser.newParser() val serializer = OPDSJSONSerializer.newSerializer() val directory = DirectoryUtilities.directoryCreateTemporary() - val bookDatabase = BookDatabase.open(context(), parser, serializer, BookFormatsTesting.supportsEverything, accountID, directory) + val bookDatabase = BookDatabase.open( + context(), + parser, + serializer, + BookFormatsTesting.supportsEverything, + accountID, + directory + ) val feedEntry: OPDSAcquisitionFeedEntry = this.acquisitionFeedEntryWithPDF() val bookID = BookIDs.newFromText("abcd") @@ -68,14 +75,19 @@ abstract class BookDatabasePDFContract { formatHandle!! Assertions.assertEquals(null, formatHandle.format.lastReadLocation) - val bookmark = Bookmark.PDFBookmark.create( - opdsId = "", - kind = BookmarkKind.BookmarkLastReadLocation, - time = DateTime.now(), - pageNumber = 25, - deviceID = "", - uri = null - ) + val bookmark = + SerializedBookmark20210828( + opdsId = "", + kind = BookmarkKind.BookmarkLastReadLocation, + time = DateTime.now(), + bookChapterProgress = 0.0, + bookProgress = 0.0, + bookChapterTitle = "C", + bookTitle = "A", + location = SerializedLocatorPage1(25), + deviceID = "", + uri = null + ) formatHandle.setLastReadLocation(bookmark) Assertions.assertEquals(bookmark, formatHandle.format.lastReadLocation) @@ -96,7 +108,14 @@ abstract class BookDatabasePDFContract { val parser = OPDSJSONParser.newParser() val serializer = OPDSJSONSerializer.newSerializer() val directory = DirectoryUtilities.directoryCreateTemporary() - val database0 = BookDatabase.open(context(), parser, serializer, BookFormatsTesting.supportsEverything, accountID, directory) + val database0 = BookDatabase.open( + context(), + parser, + serializer, + BookFormatsTesting.supportsEverything, + accountID, + directory + ) val feedEntry: OPDSAcquisitionFeedEntry = this.acquisitionFeedEntryWithPDF() val bookID = BookIDs.newFromText("abcd") diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabasePDFTest.java b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabasePDFTest.java index f22f65208..5078ec0ce 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabasePDFTest.java +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabasePDFTest.java @@ -1,7 +1,6 @@ package org.nypl.simplified.tests.books.book_database; import android.app.Application; -import android.content.Context; import org.mockito.Mockito; diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabaseTest.java b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabaseTest.java index b34fba899..2f3349a92 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabaseTest.java +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/book_database/BookDatabaseTest.java @@ -1,7 +1,6 @@ package org.nypl.simplified.tests.books.book_database; import android.app.Application; -import android.content.Context; import org.mockito.Mockito; diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/bookmarks/BHTTPCallsTest.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/bookmarks/BHTTPCallsTest.kt index 7c4f9cdcc..cfd437ad0 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/bookmarks/BHTTPCallsTest.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/bookmarks/BHTTPCallsTest.kt @@ -15,7 +15,6 @@ import org.librarysimplified.http.vanilla.LSHTTPClients import org.mockito.Mock import org.mockito.Mockito import org.nypl.simplified.accounts.api.AccountAuthenticationCredentials -import org.nypl.simplified.accounts.api.AccountAuthenticationTokenInfo import org.nypl.simplified.accounts.api.AccountPassword import org.nypl.simplified.accounts.api.AccountUsername import org.nypl.simplified.accounts.database.api.AccountType @@ -116,8 +115,8 @@ class BHTTPCallsTest { BookmarkAnnotationBodyNode( timestamp = "2019-02-08T15:37:46+0000", device = "urn:uuid:d8c5a487-646b-4c75-a83f-80599e8cf9d1", - chapterTitle = null, - bookProgress = null + chapterTitle = "", + bookProgress = 0.0f ), id = "https://example.com/annotations/book0", type = "Annotation", @@ -138,8 +137,8 @@ class BHTTPCallsTest { BookmarkAnnotationBodyNode( timestamp = "2019-02-08T15:37:47+0000", device = "urn:uuid:d8c5a487-646b-4c75-a83f-80599e8cf9d1", - chapterTitle = null, - bookProgress = null + chapterTitle = "", + bookProgress = 0.0f ), id = "https://example.com/annotations/book0", type = "Annotation", diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/bookmarks/BookmarkServiceContract.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/bookmarks/BookmarkServiceContract.kt index 5a36bae75..59f19e71c 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/bookmarks/BookmarkServiceContract.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/bookmarks/BookmarkServiceContract.kt @@ -1,7 +1,9 @@ package org.nypl.simplified.tests.books.bookmarks +import android.app.Application import android.content.Context import com.fasterxml.jackson.databind.ObjectMapper +import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.Subject import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer @@ -17,6 +19,7 @@ import org.librarysimplified.http.api.LSHTTPClientConfiguration import org.librarysimplified.http.api.LSHTTPClientType import org.librarysimplified.http.vanilla.LSHTTPClients import org.mockito.Mockito +import org.mockito.kotlin.any import org.nypl.simplified.accounts.api.AccountAuthenticationCredentials import org.nypl.simplified.accounts.api.AccountEvent import org.nypl.simplified.accounts.api.AccountID @@ -26,24 +29,26 @@ import org.nypl.simplified.accounts.api.AccountPreferences import org.nypl.simplified.accounts.api.AccountProviderType import org.nypl.simplified.accounts.api.AccountUsername import org.nypl.simplified.accounts.database.api.AccountType +import org.nypl.simplified.bookmarks.api.BookmarkEvent +import org.nypl.simplified.bookmarks.api.BookmarkHTTPCallsType +import org.nypl.simplified.bookmarks.api.BookmarkServiceType +import org.nypl.simplified.bookmarks.internal.BHTTPCalls import org.nypl.simplified.books.api.BookDRMInformation import org.nypl.simplified.books.api.BookFormat import org.nypl.simplified.books.api.BookID -import org.nypl.simplified.books.api.BookLocation -import org.nypl.simplified.books.api.bookmark.Bookmark import org.nypl.simplified.books.api.bookmark.BookmarkKind +import org.nypl.simplified.books.api.bookmark.SerializedBookmark +import org.nypl.simplified.books.api.bookmark.SerializedBookmark20210828 +import org.nypl.simplified.books.api.bookmark.SerializedLocatorLegacyCFI import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleEPUB import org.nypl.simplified.books.book_database.api.BookDatabaseEntryType import org.nypl.simplified.books.book_database.api.BookDatabaseType import org.nypl.simplified.books.book_database.api.BookFormats +import org.nypl.simplified.books.controller.Controller import org.nypl.simplified.profiles.api.ProfileEvent import org.nypl.simplified.profiles.api.ProfileID import org.nypl.simplified.profiles.api.ProfileType import org.nypl.simplified.profiles.controller.api.ProfilesControllerType -import org.nypl.simplified.bookmarks.api.BookmarkEvent -import org.nypl.simplified.bookmarks.api.BookmarkHTTPCallsType -import org.nypl.simplified.bookmarks.api.BookmarkServiceType -import org.nypl.simplified.bookmarks.internal.BHTTPCalls import org.nypl.simplified.tests.EventAssertions import org.nypl.simplified.tests.EventLogging import org.nypl.simplified.tests.mocking.MockProfilesController @@ -51,6 +56,8 @@ import org.slf4j.Logger import java.net.InetAddress import java.net.URI import java.util.UUID +import java.util.concurrent.ExecutorService +import java.util.concurrent.Executors import java.util.concurrent.TimeUnit abstract class BookmarkServiceContract { @@ -349,7 +356,7 @@ abstract class BookmarkServiceContract { BookID.create("fab6e4ebeb3240676b3f7585f8ee4faecccbe1f9243a652153f3071e90599325") val receivedBookmarks = - mutableListOf() + mutableListOf() val format = BookFormat.BookFormatEPUB( @@ -366,18 +373,17 @@ abstract class BookmarkServiceContract { Mockito.`when`(formatHandle.format) .thenReturn(format) - Mockito.`when`(formatHandle.setBookmarks(Mockito.anyList())) + Mockito.`when`(formatHandle.addBookmark(any())) .then { input -> - val bookmarks: List = input.arguments[0] as List - receivedBookmarks.addAll(bookmarks) + receivedBookmarks.add(input.arguments[0] as SerializedBookmark) Unit } val bookEntry = Mockito.mock(BookDatabaseEntryType::class.java) - Mockito.`when`(bookEntry.findFormatHandle(BookDatabaseEntryFormatHandleEPUB::class.java)) - .thenReturn(formatHandle) + Mockito.`when`(bookEntry.formatHandles) + .thenReturn(listOf(formatHandle)) val books = Mockito.mock(BookDatabaseType::class.java) @@ -486,7 +492,7 @@ abstract class BookmarkServiceContract { */ @Test - @Timeout(value = 5L, unit = TimeUnit.SECONDS) + // @Timeout(value = 5L, unit = TimeUnit.SECONDS) fun testInitializeSendBookmarks() { this.addResponse("/patron", this.patronSettingsWithAnnotationsEnabled) @@ -542,18 +548,24 @@ abstract class BookmarkServiceContract { BookID.create("fab6e4ebeb3240676b3f7585f8ee4faecccbe1f9243a652153f3071e90599325") val receivedBookmarks = - mutableListOf() + mutableListOf() val startingBookmarks = listOf( - Bookmark.ReaderBookmark.create( + SerializedBookmark20210828( opdsId = "urn:example.com/terms/id/c083c0a6-54c6-4cc5-9d3a-425317da662a", - location = BookLocation.BookLocationR1(0.5, null, bookID.value()), + location = SerializedLocatorLegacyCFI( + chapterProgression = 0.5, + idRef = null, + contentCFI = bookID.value() + ), kind = BookmarkKind.BookmarkLastReadLocation, time = DateTime.now(DateTimeZone.UTC), - chapterTitle = "A Title", + bookChapterTitle = "A Title", bookProgress = 0.5, deviceID = "urn:uuid:253c7cbc-4fdf-430e-81b9-18bea90b6026", + bookTitle = "A book", + bookChapterProgress = 0.5, uri = null ) ) @@ -573,18 +585,17 @@ abstract class BookmarkServiceContract { Mockito.`when`(formatHandle.format) .thenReturn(format) - Mockito.`when`(formatHandle.setBookmarks(Mockito.anyList())) + Mockito.`when`(formatHandle.addBookmark(any())) .then { input -> - val bookmarks: List = input.arguments[0] as List - receivedBookmarks.addAll(bookmarks) + receivedBookmarks.add(input.arguments[0] as SerializedBookmark) Unit } val bookEntry = Mockito.mock(BookDatabaseEntryType::class.java) - Mockito.`when`(bookEntry.findFormatHandle(BookDatabaseEntryFormatHandleEPUB::class.java)) - .thenReturn(formatHandle) + Mockito.`when`(bookEntry.formatHandles) + .thenReturn(listOf(formatHandle)) val books = Mockito.mock(BookDatabaseType::class.java) @@ -674,15 +685,11 @@ abstract class BookmarkServiceContract { { event -> Assertions.assertEquals(this.fakeAccountID, event.accountID) } ) - Assertions.assertEquals(2, receivedBookmarks.size) + Assertions.assertEquals(1, receivedBookmarks.size) Assertions.assertEquals( "urn:example.com/terms/id/c083c0a6-54c6-4cc5-9d3a-425317da662a", receivedBookmarks[0].opdsId ) - Assertions.assertEquals( - "urn:example.com/terms/id/c083c0a6-54c6-4cc5-9d3a-425317da662a", - receivedBookmarks[1].opdsId - ) val allRequests = this.takeAllRequests() Assertions.assertTrue( @@ -708,8 +715,7 @@ abstract class BookmarkServiceContract { try { service.bookmarkLoad( accountID = profiles.profileList[0].accountList[0].id, - book = BookID.create("x"), - lastReadBookmarkServer = null + book = BookID.create("x") ).get(3L, TimeUnit.SECONDS) } catch (e: Exception) { // Not a problem diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/bookmarks/BookmarkServiceTest.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/bookmarks/BookmarkServiceTest.kt index 0b1d982d6..494cbaf83 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/bookmarks/BookmarkServiceTest.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/bookmarks/BookmarkServiceTest.kt @@ -2,11 +2,11 @@ package org.nypl.simplified.tests.books.bookmarks import io.reactivex.subjects.Subject import org.nypl.simplified.bookmarks.BookmarkService -import org.nypl.simplified.profiles.controller.api.ProfilesControllerType import org.nypl.simplified.bookmarks.api.BookmarkEvent import org.nypl.simplified.bookmarks.api.BookmarkHTTPCallsType import org.nypl.simplified.bookmarks.api.BookmarkServiceProviderType import org.nypl.simplified.bookmarks.api.BookmarkServiceType +import org.nypl.simplified.profiles.controller.api.ProfilesControllerType import org.slf4j.Logger import org.slf4j.LoggerFactory diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/bookmarks/BookmarksSerializationTest.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/bookmarks/BookmarksSerializationTest.kt new file mode 100644 index 000000000..e4bf1fdcc --- /dev/null +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/bookmarks/BookmarksSerializationTest.kt @@ -0,0 +1,320 @@ +package org.nypl.simplified.tests.books.bookmarks + +import com.fasterxml.jackson.databind.ObjectMapper +import org.joda.time.DateTime +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.nypl.simplified.books.api.bookmark.BookmarkKind +import org.nypl.simplified.books.api.bookmark.SerializedBookmark20210317 +import org.nypl.simplified.books.api.bookmark.SerializedBookmark20210828 +import org.nypl.simplified.books.api.bookmark.SerializedBookmark20240424 +import org.nypl.simplified.books.api.bookmark.SerializedBookmarks +import org.nypl.simplified.books.api.bookmark.SerializedLocatorAudioBookTime1 +import org.nypl.simplified.books.api.bookmark.SerializedLocatorAudioBookTime2 +import org.nypl.simplified.books.api.bookmark.SerializedLocatorHrefProgression20210317 +import org.nypl.simplified.books.api.bookmark.SerializedLocatorLegacyCFI +import org.nypl.simplified.books.api.bookmark.SerializedLocatorPage1 +import org.nypl.simplified.books.api.bookmark.SerializedLocators +import org.slf4j.LoggerFactory +import java.net.URI + +class BookmarksSerializationTest { + + private val objectMapper = + ObjectMapper() + private val logger = + LoggerFactory.getLogger(BookmarksSerializationTest::class.java) + + @Test + fun testFormat20210317() { + val bookmarkOut = + SerializedBookmark20210317( + bookChapterProgress = 0.25, + bookChapterTitle = "Chapter Title", + bookProgress = 0.1, + bookTitle = "Book Title", + deviceID = "7a68e229-abbd-4bec-99e2-44c23be152d5", + kind = BookmarkKind.BookmarkExplicit, + location = SerializedLocatorLegacyCFI( + idRef = "idRef", + contentCFI = "/content/cfi", + chapterProgression = 0.3 + ), + opdsId = "opdsId", + time = DateTime.parse("2001-01-01T00:00:00+00:00"), + uri = URI.create("https://www.example.com/") + ) + + /* + * The 20210317 format doesn't support many of the fields we use. The parser will return + * default values. + */ + + val bookmarkLimited = + bookmarkOut.copy( + bookChapterProgress = 0.0, + bookChapterTitle = "", + bookProgress = 0.0, + bookTitle = "", + ) + + val bookmarkText = + bookmarkOut.toJSON(this.objectMapper).toPrettyString() + + this.logger.debug("{}", bookmarkText) + + val bookmarkIn = + SerializedBookmarks.parseBookmarkFromString(bookmarkText) + as SerializedBookmark20210317 + + assertEquals(bookmarkLimited, bookmarkIn) + } + + @Test + fun testFormat20210828() { + val bookmarkOut = + SerializedBookmark20210828( + bookChapterProgress = 0.25, + bookChapterTitle = "Chapter Title", + bookProgress = 0.1, + bookTitle = "Book Title", + deviceID = "7a68e229-abbd-4bec-99e2-44c23be152d5", + kind = BookmarkKind.BookmarkExplicit, + location = SerializedLocatorLegacyCFI( + idRef = "idRef", + contentCFI = "/content/cfi", + chapterProgression = 0.3 + ), + opdsId = "opdsId", + time = DateTime.parse("2001-01-01T00:00:00+00:00"), + uri = URI.create("https://www.example.com/") + ) + + /* + * The 20210828 format doesn't support many of the fields we use. The parser will return + * default values. + */ + + val bookmarkLimited = + bookmarkOut.copy( + bookChapterProgress = 0.0, + bookChapterTitle = "", + bookProgress = 0.0, + bookTitle = "", + ) + + val bookmarkText = + bookmarkOut.toJSON(this.objectMapper).toPrettyString() + + this.logger.debug("{}", bookmarkText) + + val bookmarkIn = + SerializedBookmarks.parseBookmarkFromString(bookmarkText) + as SerializedBookmark20210828 + + assertEquals(bookmarkLimited, bookmarkIn) + } + + @Test + fun testFormat20240424() { + val bookmarkOut = + SerializedBookmark20240424( + bookChapterProgress = 0.25, + bookChapterTitle = "Chapter Title", + bookProgress = 0.1, + bookTitle = "Book Title", + deviceID = "7a68e229-abbd-4bec-99e2-44c23be152d5", + kind = BookmarkKind.BookmarkExplicit, + location = SerializedLocatorLegacyCFI( + idRef = "idRef", + contentCFI = "/content/cfi", + chapterProgression = 0.3 + ), + opdsId = "opdsId", + time = DateTime.parse("2001-01-01T00:00:00+00:00"), + uri = URI.create("https://www.example.com/") + ) + + val bookmarkLimited = + bookmarkOut.copy() + + val bookmarkText = + bookmarkOut.toJSON(this.objectMapper).toPrettyString() + + this.logger.debug("{}", bookmarkText) + + val bookmarkIn = + SerializedBookmarks.parseBookmarkFromString(bookmarkText) + as SerializedBookmark20240424 + + assertEquals(bookmarkLimited, bookmarkIn) + } + + @Test + fun testLocatorAudioBookTime1() { + val locatorOut = + SerializedLocatorAudioBookTime1( + audioBookId = "x", + chapter = 1, + duration = 8640000L, + part = 2, + startOffsetMilliseconds = 1000L, + timeMilliseconds = 3000L, + title = "Title", + ) + + val locatorText = + locatorOut.toJSON(this.objectMapper).toPrettyString() + + this.logger.debug("{}", locatorText) + + val locatorIn = + SerializedLocators.parseLocatorFromString(locatorText) + as SerializedLocatorAudioBookTime1 + + assertEquals(locatorOut, locatorIn) + } + + @Test + fun testLocatorAudioBookTime2() { + val locatorOut = + SerializedLocatorAudioBookTime2( + chapterHref = "urn:org.thepalaceproject:readingOrderItem:23", + chapterOffsetMilliseconds = 25000L + ) + + val locatorText = + locatorOut.toJSON(this.objectMapper).toPrettyString() + + this.logger.debug("{}", locatorText) + + val locatorIn = + SerializedLocators.parseLocatorFromString(locatorText) + as SerializedLocatorAudioBookTime2 + + assertEquals(locatorOut, locatorIn) + } + + @Test + fun testLocatorLegacyCFI0() { + val locatorOut = + SerializedLocatorLegacyCFI( + idRef = "/a", + contentCFI = "/b", + chapterProgression = 0.5 + ) + + val locatorText = + locatorOut.toJSON(this.objectMapper).toPrettyString() + + this.logger.debug("{}", locatorText) + + val locatorIn = + SerializedLocators.parseLocatorFromString(locatorText) + as SerializedLocatorLegacyCFI + + assertEquals(locatorOut, locatorIn) + } + + @Test + fun testLocatorLegacyCFI1() { + val locatorOut = + SerializedLocatorLegacyCFI( + idRef = "/a", + contentCFI = null, + chapterProgression = 0.5 + ) + + val locatorText = + locatorOut.toJSON(this.objectMapper).toPrettyString() + + this.logger.debug("{}", locatorText) + + val locatorIn = + SerializedLocators.parseLocatorFromString(locatorText) + as SerializedLocatorLegacyCFI + + assertEquals(locatorOut, locatorIn) + } + + @Test + fun testLocatorLegacyCFI2() { + val locatorOut = + SerializedLocatorLegacyCFI( + idRef = null, + contentCFI = "/b", + chapterProgression = 0.5 + ) + + val locatorText = + locatorOut.toJSON(this.objectMapper).toPrettyString() + + this.logger.debug("{}", locatorText) + + val locatorIn = + SerializedLocators.parseLocatorFromString(locatorText) + as SerializedLocatorLegacyCFI + + assertEquals(locatorOut, locatorIn) + } + + @Test + fun testLocatorLegacyCFI3() { + val locatorOut = + SerializedLocatorLegacyCFI( + idRef = null, + contentCFI = null, + chapterProgression = 0.5 + ) + + val locatorText = + locatorOut.toJSON(this.objectMapper).toPrettyString() + + this.logger.debug("{}", locatorText) + + val locatorIn = + SerializedLocators.parseLocatorFromString(locatorText) + as SerializedLocatorLegacyCFI + + assertEquals(locatorOut, locatorIn) + } + + @Test + fun testLocatorPage1() { + val locatorOut = + SerializedLocatorPage1( + page = 23 + ) + + val locatorText = + locatorOut.toJSON(this.objectMapper).toPrettyString() + + this.logger.debug("{}", locatorText) + + val locatorIn = + SerializedLocators.parseLocatorFromString(locatorText) + as SerializedLocatorPage1 + + assertEquals(locatorOut, locatorIn) + } + + @Test + fun testLocatorHrefProgression20210317() { + val locatorOut = + SerializedLocatorHrefProgression20210317( + chapterHref = "/x/y/z", + chapterProgress = 0.5 + ) + + val locatorText = + locatorOut.toJSON(this.objectMapper).toPrettyString() + + this.logger.debug("{}", locatorText) + + val locatorIn = + SerializedLocators.parseLocatorFromString(locatorText) + as SerializedLocatorHrefProgression20210317 + + assertEquals(locatorOut, locatorIn) + } +} diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowACSMTest.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowACSMTest.kt index 4cfe8a48a..074ac7e85 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowACSMTest.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowACSMTest.kt @@ -1,7 +1,6 @@ package org.nypl.simplified.tests.books.borrowing import android.app.Application -import android.content.Context import io.reactivex.disposables.Disposable import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowAudioBookTest.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowAudioBookTest.kt index 8df6d451e..67fc0b50c 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowAudioBookTest.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowAudioBookTest.kt @@ -1,7 +1,6 @@ package org.nypl.simplified.tests.books.borrowing import android.app.Application -import android.content.Context import io.reactivex.disposables.Disposable import org.joda.time.Instant import org.junit.jupiter.api.Assertions diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowAxisNowTest.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowAxisNowTest.kt index 316798cc9..459da3dfb 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowAxisNowTest.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowAxisNowTest.kt @@ -1,7 +1,6 @@ package org.nypl.simplified.tests.books.borrowing import android.app.Application -import android.content.Context import io.reactivex.disposables.Disposable import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowCopyTest.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowCopyTest.kt index e4a7d94ed..69cead61f 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowCopyTest.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowCopyTest.kt @@ -1,7 +1,6 @@ package org.nypl.simplified.tests.books.borrowing import android.app.Application -import android.content.Context import io.reactivex.disposables.Disposable import org.joda.time.Instant import org.junit.jupiter.api.Assertions diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowDirectDownloadTest.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowDirectDownloadTest.kt index b900c937d..3b738f4bf 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowDirectDownloadTest.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowDirectDownloadTest.kt @@ -1,7 +1,6 @@ package org.nypl.simplified.tests.books.borrowing import android.app.Application -import android.content.Context import io.reactivex.disposables.Disposable import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowLimitLoanTest.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowLimitLoanTest.kt index e70ba3b51..cab3595fd 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowLimitLoanTest.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowLimitLoanTest.kt @@ -1,7 +1,6 @@ package org.nypl.simplified.tests.books.borrowing import android.app.Application -import android.content.Context import io.reactivex.disposables.Disposable import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowLoanCreateTest.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowLoanCreateTest.kt index 7d3ecb1e4..d11d343f9 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowLoanCreateTest.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowLoanCreateTest.kt @@ -1,7 +1,6 @@ package org.nypl.simplified.tests.books.borrowing import android.app.Application -import android.content.Context import io.reactivex.disposables.Disposable import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowSAMLDownloadTest.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowSAMLDownloadTest.kt index 74c59a0ef..14ff1f922 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowSAMLDownloadTest.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowSAMLDownloadTest.kt @@ -1,7 +1,6 @@ package org.nypl.simplified.tests.books.borrowing import android.app.Application -import android.content.Context import io.reactivex.disposables.Disposable import okhttp3.mockwebserver.Dispatcher import okhttp3.mockwebserver.MockResponse diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowTaskTest.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowTaskTest.kt index db859dac0..0d488b888 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowTaskTest.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/borrowing/BorrowTaskTest.kt @@ -2,7 +2,6 @@ package org.nypl.simplified.tests.books.borrowing import android.app.Application import android.content.ContentResolver -import android.content.Context import com.io7m.jfunctional.Option import io.reactivex.disposables.Disposable import okhttp3.mockwebserver.MockResponse diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/controller/BookRevokeTaskAdobeDRMTest.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/controller/BookRevokeTaskAdobeDRMTest.kt index 86e2a9f40..ffe4bab4b 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/controller/BookRevokeTaskAdobeDRMTest.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/controller/BookRevokeTaskAdobeDRMTest.kt @@ -4,8 +4,6 @@ import android.content.Context import com.google.common.util.concurrent.ListeningExecutorService import com.google.common.util.concurrent.MoreExecutors import com.io7m.jfunctional.Option -import com.io7m.jfunctional.OptionType -import com.io7m.jfunctional.Some import one.irradia.mime.api.MIMEType import one.irradia.mime.vanilla.MIMEParser import org.joda.time.DateTime @@ -35,9 +33,7 @@ import org.nypl.simplified.accounts.api.AccountAuthenticationCredentials import org.nypl.simplified.accounts.api.AccountID import org.nypl.simplified.accounts.api.AccountLoginState import org.nypl.simplified.accounts.api.AccountPassword -import org.nypl.simplified.accounts.api.AccountProviderType import org.nypl.simplified.accounts.api.AccountUsername -import org.nypl.simplified.accounts.database.api.AccountType import org.nypl.simplified.books.api.Book import org.nypl.simplified.books.api.BookDRMInformation import org.nypl.simplified.books.api.BookEvent @@ -78,7 +74,6 @@ import java.io.FileNotFoundException import java.io.IOException import java.io.InputStream import java.net.URI -import java.util.ArrayList import java.util.Collections import java.util.UUID import java.util.concurrent.ConcurrentSkipListMap diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/controller/BookRevokeTaskTest.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/controller/BookRevokeTaskTest.kt index 8f87a7806..ac19d8ed3 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/controller/BookRevokeTaskTest.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/controller/BookRevokeTaskTest.kt @@ -28,8 +28,6 @@ import org.librarysimplified.http.vanilla.LSHTTPClients import org.mockito.Mockito import org.mockito.internal.verification.Times import org.nypl.simplified.accounts.api.AccountID -import org.nypl.simplified.accounts.api.AccountProviderType -import org.nypl.simplified.accounts.database.api.AccountType import org.nypl.simplified.books.api.Book import org.nypl.simplified.books.api.BookDRMInformation import org.nypl.simplified.books.api.BookEvent @@ -81,7 +79,6 @@ import java.io.File import java.io.FileNotFoundException import java.io.IOException import java.net.URI -import java.util.ArrayList import java.util.Collections import java.util.UUID import java.util.concurrent.ConcurrentSkipListMap diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/controller/BooksControllerContract.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/controller/BooksControllerContract.kt index f46257897..1388eadab 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/controller/BooksControllerContract.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/controller/BooksControllerContract.kt @@ -99,9 +99,7 @@ import java.io.FileNotFoundException import java.io.IOException import java.io.InputStream import java.net.URI -import java.util.ArrayList import java.util.Collections -import java.util.NoSuchElementException import java.util.concurrent.ExecutorService import java.util.concurrent.Executors import java.util.concurrent.TimeUnit diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/controller/BooksControllerTest.java b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/controller/BooksControllerTest.java index 91147d1c1..1a2ddf647 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/controller/BooksControllerTest.java +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/controller/BooksControllerTest.java @@ -1,7 +1,6 @@ package org.nypl.simplified.tests.books.controller; import android.app.Application; -import android.content.Context; import org.mockito.Mockito; diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/controller/ProfileAccountCreateCustomOPDSTest.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/controller/ProfileAccountCreateCustomOPDSTest.kt index 93fc22da8..c50b3dcc9 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/controller/ProfileAccountCreateCustomOPDSTest.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/controller/ProfileAccountCreateCustomOPDSTest.kt @@ -19,7 +19,6 @@ import org.nypl.simplified.accounts.api.AccountEvent import org.nypl.simplified.accounts.api.AccountID import org.nypl.simplified.accounts.api.AccountPreferences import org.nypl.simplified.accounts.api.AccountProvider -import org.nypl.simplified.accounts.database.api.AccountType import org.nypl.simplified.accounts.database.api.AccountsDatabaseIOException import org.nypl.simplified.accounts.registry.AccountProviderRegistry import org.nypl.simplified.accounts.registry.api.AccountProviderRegistryType @@ -54,7 +53,6 @@ import java.io.File import java.io.FileNotFoundException import java.io.IOException import java.io.InputStream -import java.util.ArrayList import java.util.Collections import java.util.UUID import java.util.concurrent.Executors diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/controller/ProfilesControllerContract.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/controller/ProfilesControllerContract.kt index b3d64333f..398cbd46b 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/controller/ProfilesControllerContract.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/controller/ProfilesControllerContract.kt @@ -1,7 +1,6 @@ package org.nypl.simplified.tests.books.controller import android.app.Application -import android.content.Context import com.google.common.util.concurrent.ListeningExecutorService import com.google.common.util.concurrent.MoreExecutors import io.reactivex.subjects.PublishSubject @@ -22,7 +21,10 @@ import org.nypl.simplified.accounts.database.AccountBundledCredentialsEmpty import org.nypl.simplified.accounts.database.AccountsDatabases import org.nypl.simplified.accounts.registry.api.AccountProviderRegistryType import org.nypl.simplified.analytics.api.AnalyticsType +import org.nypl.simplified.bookmarks.api.BookmarkEvent import org.nypl.simplified.books.audio.AudioBookManifestStrategiesType +import org.nypl.simplified.books.book_registry.BookPreviewRegistry +import org.nypl.simplified.books.book_registry.BookPreviewRegistryType import org.nypl.simplified.books.book_registry.BookRegistry import org.nypl.simplified.books.book_registry.BookRegistryType import org.nypl.simplified.books.borrowing.BorrowSubtasks @@ -37,6 +39,8 @@ import org.nypl.simplified.feeds.api.FeedHTTPTransport import org.nypl.simplified.feeds.api.FeedLoader import org.nypl.simplified.feeds.api.FeedLoaderType import org.nypl.simplified.files.DirectoryUtilities +import org.nypl.simplified.notifications.NotificationTokenHTTPCalls +import org.nypl.simplified.notifications.NotificationTokenHTTPCallsType import org.nypl.simplified.opds.auth_document.api.AuthenticationDocumentParsersType import org.nypl.simplified.opds.core.OPDSAcquisitionFeedEntryParser import org.nypl.simplified.opds.core.OPDSFeedParser @@ -62,11 +66,6 @@ import org.nypl.simplified.profiles.controller.api.ProfilesControllerType import org.nypl.simplified.reader.api.ReaderColorScheme import org.nypl.simplified.reader.api.ReaderFontSelection import org.nypl.simplified.reader.api.ReaderPreferences -import org.nypl.simplified.bookmarks.api.BookmarkEvent -import org.nypl.simplified.books.book_registry.BookPreviewRegistry -import org.nypl.simplified.books.book_registry.BookPreviewRegistryType -import org.nypl.simplified.notifications.NotificationTokenHTTPCalls -import org.nypl.simplified.notifications.NotificationTokenHTTPCallsType import org.nypl.simplified.tests.EventAssertions import org.nypl.simplified.tests.MutableServiceDirectory import org.nypl.simplified.tests.books.BookFormatsTesting @@ -84,7 +83,6 @@ import org.slf4j.Logger import java.io.File import java.io.FileNotFoundException import java.net.URI -import java.util.ArrayList import java.util.Collections import java.util.concurrent.ExecutorService import java.util.concurrent.Executors diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/controller/ProfilesControllerTest.java b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/controller/ProfilesControllerTest.java index b22739b4c..43e575ef2 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/controller/ProfilesControllerTest.java +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/controller/ProfilesControllerTest.java @@ -1,7 +1,6 @@ package org.nypl.simplified.tests.books.controller; import android.app.Application; -import android.content.Context; import org.mockito.Mockito; import org.slf4j.Logger; diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/profiles/ProfileAccountLoginTaskContract.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/profiles/ProfileAccountLoginTaskContract.kt index fd139efaf..0ac963ca6 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/profiles/ProfileAccountLoginTaskContract.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/profiles/ProfileAccountLoginTaskContract.kt @@ -36,7 +36,6 @@ import org.nypl.simplified.accounts.api.AccountLoginStringResourcesType import org.nypl.simplified.accounts.api.AccountPassword import org.nypl.simplified.accounts.api.AccountProviderAuthenticationDescription import org.nypl.simplified.accounts.api.AccountProviderAuthenticationDescription.KeyboardInput -import org.nypl.simplified.accounts.api.AccountProviderType import org.nypl.simplified.accounts.api.AccountUsername import org.nypl.simplified.accounts.database.api.AccountType import org.nypl.simplified.books.controller.ProfileAccountLoginTask @@ -44,7 +43,6 @@ import org.nypl.simplified.notifications.NotificationTokenHTTPCalls import org.nypl.simplified.notifications.NotificationTokenHTTPCallsType import org.nypl.simplified.patron.PatronUserProfileParsers import org.nypl.simplified.profiles.api.ProfileID -import org.nypl.simplified.profiles.api.ProfilePreferences import org.nypl.simplified.profiles.api.ProfileReadableType import org.nypl.simplified.profiles.controller.api.ProfileAccountLoginRequest import org.nypl.simplified.taskrecorder.api.TaskResult diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/profiles/ProfileAccountLogoutTaskContract.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/profiles/ProfileAccountLogoutTaskContract.kt index eef4167c4..6d1ed3928 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/profiles/ProfileAccountLogoutTaskContract.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/profiles/ProfileAccountLogoutTaskContract.kt @@ -33,7 +33,6 @@ import org.nypl.simplified.accounts.api.AccountLoginState.AccountLogoutFailed import org.nypl.simplified.accounts.api.AccountLoginState.AccountNotLoggedIn import org.nypl.simplified.accounts.api.AccountLogoutStringResourcesType import org.nypl.simplified.accounts.api.AccountPassword -import org.nypl.simplified.accounts.api.AccountProviderType import org.nypl.simplified.accounts.api.AccountUsername import org.nypl.simplified.accounts.database.api.AccountType import org.nypl.simplified.books.api.BookID @@ -50,7 +49,6 @@ import org.nypl.simplified.opds.core.OPDSAvailabilityOpenAccess import org.nypl.simplified.patron.PatronUserProfileParsers import org.nypl.simplified.patron.api.PatronUserProfileParsersType import org.nypl.simplified.profiles.api.ProfileID -import org.nypl.simplified.profiles.api.ProfilePreferences import org.nypl.simplified.profiles.api.ProfileReadableType import org.nypl.simplified.tests.books.controller.FakeAccounts.fakeAccountProvider import org.nypl.simplified.tests.mocking.MockAccountLogoutStringResources diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/profiles/ProfileDescriptionJSONTest.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/profiles/ProfileDescriptionJSONTest.kt index 5e6215464..b9420fac9 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/profiles/ProfileDescriptionJSONTest.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/profiles/ProfileDescriptionJSONTest.kt @@ -4,13 +4,11 @@ import com.fasterxml.jackson.databind.ObjectMapper import org.joda.time.DateTime import org.joda.time.DateTimeUtils import org.joda.time.DateTimeZone -import org.joda.time.Duration import org.junit.jupiter.api.AfterEach import org.junit.jupiter.api.Assertions.assertEquals import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.nypl.simplified.accounts.api.AccountID - import org.nypl.simplified.profiles.ProfileDescriptionJSON import org.nypl.simplified.profiles.api.ProfileAttributes import org.nypl.simplified.profiles.api.ProfileDateOfBirth diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/profiles/ProfilesDatabaseContract.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/profiles/ProfilesDatabaseContract.kt index 866f4dd41..eb52ff673 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/profiles/ProfilesDatabaseContract.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/profiles/ProfilesDatabaseContract.kt @@ -1,7 +1,6 @@ package org.nypl.simplified.tests.books.profiles import android.app.Application -import android.content.Context import com.io7m.jfunctional.Option import io.reactivex.subjects.PublishSubject import org.joda.time.DateTime diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/profiles/ProfilesDatabaseTest.java b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/profiles/ProfilesDatabaseTest.java index ad7ba8ee6..7cb763c0f 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/profiles/ProfilesDatabaseTest.java +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/profiles/ProfilesDatabaseTest.java @@ -1,7 +1,6 @@ package org.nypl.simplified.tests.books.profiles; import android.app.Application; -import android.content.Context; import org.mockito.Mockito; diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/bugs/Simply3635Test.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/bugs/Simply3635Test.kt index f7ce970be..7768a93d8 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/bugs/Simply3635Test.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/bugs/Simply3635Test.kt @@ -2,7 +2,6 @@ package org.nypl.simplified.tests.bugs import android.app.Application import android.content.ContentResolver -import android.content.Context import com.google.common.util.concurrent.ListeningExecutorService import com.google.common.util.concurrent.MoreExecutors import com.io7m.jfunctional.Option diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/http/refresh_token/bookmarks/BookmarkRefreshTokenTest.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/http/refresh_token/bookmarks/BookmarkRefreshTokenTest.kt index 214686726..f0a9b14a0 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/http/refresh_token/bookmarks/BookmarkRefreshTokenTest.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/http/refresh_token/bookmarks/BookmarkRefreshTokenTest.kt @@ -1,7 +1,6 @@ package org.nypl.simplified.tests.http.refresh_token.bookmarks import android.app.Application -import android.content.Context import com.fasterxml.jackson.databind.ObjectMapper import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/http/refresh_token/borrow/BorrowBookRefreshTokenTest.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/http/refresh_token/borrow/BorrowBookRefreshTokenTest.kt index b6654c023..2c07b0338 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/http/refresh_token/borrow/BorrowBookRefreshTokenTest.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/http/refresh_token/borrow/BorrowBookRefreshTokenTest.kt @@ -2,7 +2,6 @@ package org.nypl.simplified.tests.http.refresh_token.borrow import android.app.Application import android.content.ContentResolver -import android.content.Context import okhttp3.mockwebserver.MockResponse import okhttp3.mockwebserver.MockWebServer import okio.Buffer diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/lcp/LCPContentProtectionProviderTest.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/lcp/LCPContentProtectionProviderTest.kt index f9a1a82ba..c9c9b09dc 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/lcp/LCPContentProtectionProviderTest.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/lcp/LCPContentProtectionProviderTest.kt @@ -3,7 +3,6 @@ package org.nypl.simplified.tests.lcp import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.nypl.simplified.lcp.LCPContentProtectionProvider -import java.lang.IllegalStateException class LCPContentProtectionProviderTest { diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/mocking/MockAccountProviders.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/mocking/MockAccountProviders.kt index 1cc93b4d2..544e6898f 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/mocking/MockAccountProviders.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/mocking/MockAccountProviders.kt @@ -1,7 +1,6 @@ package org.nypl.simplified.tests.mocking import android.content.Context -import com.google.common.base.Preconditions import org.joda.time.DateTime import org.mockito.Mockito import org.nypl.simplified.accounts.api.AccountProvider @@ -9,8 +8,6 @@ import org.nypl.simplified.accounts.api.AccountProviderAuthenticationDescription import org.nypl.simplified.accounts.api.AccountProviderType import org.nypl.simplified.accounts.registry.AccountProviderRegistry import org.nypl.simplified.accounts.registry.api.AccountProviderRegistryType -import org.nypl.simplified.taskrecorder.api.TaskResult -import org.slf4j.LoggerFactory import java.net.URI import java.util.TreeMap diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/mocking/MockAudioBookManifestStrategy.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/mocking/MockAudioBookManifestStrategy.kt index 4697226f5..e311f2bae 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/mocking/MockAudioBookManifestStrategy.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/mocking/MockAudioBookManifestStrategy.kt @@ -1,10 +1,10 @@ package org.nypl.simplified.tests.mocking +import io.reactivex.Observable +import io.reactivex.subjects.PublishSubject import org.nypl.simplified.books.audio.AudioBookManifestData import org.nypl.simplified.books.audio.AudioBookManifestStrategyType import org.nypl.simplified.taskrecorder.api.TaskResult -import rx.Observable -import rx.subjects.PublishSubject class MockAudioBookManifestStrategy : AudioBookManifestStrategyType { diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/mocking/MockBookDatabaseEntryFormatHandleAudioBook.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/mocking/MockBookDatabaseEntryFormatHandleAudioBook.kt index 4dcfef7b6..03cf01f3f 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/mocking/MockBookDatabaseEntryFormatHandleAudioBook.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/mocking/MockBookDatabaseEntryFormatHandleAudioBook.kt @@ -4,7 +4,8 @@ import org.nypl.simplified.books.api.BookDRMInformation import org.nypl.simplified.books.api.BookDRMKind import org.nypl.simplified.books.api.BookFormat import org.nypl.simplified.books.api.BookID -import org.nypl.simplified.books.api.bookmark.Bookmark +import org.nypl.simplified.books.api.bookmark.BookmarkID +import org.nypl.simplified.books.api.bookmark.SerializedBookmark import org.nypl.simplified.books.book_database.api.BookDRMInformationHandle import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle import org.nypl.simplified.books.formats.api.StandardFormatNames @@ -69,16 +70,24 @@ class MockBookDatabaseEntryFormatHandleAudioBook( check(this.formatField.isDownloaded) } - override fun setLastReadLocation(bookmark: Bookmark.AudiobookBookmark?) { + override fun setLastReadLocation(bookmark: SerializedBookmark?) { this.formatField = this.formatField.copy( lastReadLocation = bookmark ) } - override fun setBookmarks(bookmarks: List) { - this.formatField = this.formatField.copy( - bookmarks = bookmarks - ) + override fun addBookmark(bookmark: SerializedBookmark) { + val newList = arrayListOf() + newList.addAll(this.formatField.bookmarks) + newList.add(bookmark) + this.formatField = this.formatField.copy(bookmarks = newList.toList()) + } + + override fun deleteBookmark(bookmarkId: BookmarkID) { + val newList = arrayListOf() + newList.addAll(this.formatField.bookmarks) + newList.removeIf { b -> b.bookmarkId == bookmarkId } + this.formatField = this.formatField.copy(bookmarks = newList.toList()) } override val drmInformationHandle: BookDRMInformationHandle diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/mocking/MockBookDatabaseEntryFormatHandleEPUB.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/mocking/MockBookDatabaseEntryFormatHandleEPUB.kt index 775c27137..19240a969 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/mocking/MockBookDatabaseEntryFormatHandleEPUB.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/mocking/MockBookDatabaseEntryFormatHandleEPUB.kt @@ -4,7 +4,8 @@ import org.nypl.simplified.books.api.BookDRMInformation import org.nypl.simplified.books.api.BookDRMKind import org.nypl.simplified.books.api.BookFormat import org.nypl.simplified.books.api.BookID -import org.nypl.simplified.books.api.bookmark.Bookmark +import org.nypl.simplified.books.api.bookmark.BookmarkID +import org.nypl.simplified.books.api.bookmark.SerializedBookmark import org.nypl.simplified.books.book_database.api.BookDRMInformationHandle import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandleEPUB import org.nypl.simplified.books.formats.api.StandardFormatNames @@ -48,12 +49,22 @@ class MockBookDatabaseEntryFormatHandleEPUB( check(this.formatField.isDownloaded) } - override fun setLastReadLocation(bookmark: Bookmark.ReaderBookmark?) { + override fun setLastReadLocation(bookmark: SerializedBookmark?) { this.formatField = this.formatField.copy(lastReadLocation = bookmark) } - override fun setBookmarks(bookmarks: List) { - this.formatField = this.formatField.copy(bookmarks = bookmarks) + override fun addBookmark(bookmark: SerializedBookmark) { + val newList = arrayListOf() + newList.addAll(this.formatField.bookmarks) + newList.add(bookmark) + this.formatField = this.formatField.copy(bookmarks = newList.toList()) + } + + override fun deleteBookmark(bookmarkId: BookmarkID) { + val newList = arrayListOf() + newList.addAll(this.formatField.bookmarks) + newList.removeIf { b -> b.bookmarkId == bookmarkId } + this.formatField = this.formatField.copy(bookmarks = newList.toList()) } override val drmInformationHandle: BookDRMInformationHandle diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/mocking/MockBookDatabaseEntryFormatHandlePDF.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/mocking/MockBookDatabaseEntryFormatHandlePDF.kt index edab0ecb6..3a96a800f 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/mocking/MockBookDatabaseEntryFormatHandlePDF.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/mocking/MockBookDatabaseEntryFormatHandlePDF.kt @@ -5,7 +5,8 @@ import org.nypl.simplified.books.api.BookDRMInformation import org.nypl.simplified.books.api.BookDRMKind import org.nypl.simplified.books.api.BookFormat import org.nypl.simplified.books.api.BookID -import org.nypl.simplified.books.api.bookmark.Bookmark +import org.nypl.simplified.books.api.bookmark.BookmarkID +import org.nypl.simplified.books.api.bookmark.SerializedBookmark import org.nypl.simplified.books.book_database.api.BookDRMInformationHandle import org.nypl.simplified.books.book_database.api.BookDatabaseEntryFormatHandle.BookDatabaseEntryFormatHandlePDF import org.nypl.simplified.books.formats.api.StandardFormatNames @@ -43,12 +44,22 @@ class MockBookDatabaseEntryFormatHandlePDF( check(this.formatField.isDownloaded) } - override fun setLastReadLocation(bookmark: Bookmark.PDFBookmark?) { + override fun setLastReadLocation(bookmark: SerializedBookmark?) { this.formatField = this.formatField.copy(lastReadLocation = bookmark) } - override fun setBookmarks(bookmarks: List) { - this.formatField = this.formatField.copy(bookmarks = bookmarks) + override fun addBookmark(bookmark: SerializedBookmark) { + val newList = arrayListOf() + newList.addAll(this.formatField.bookmarks) + newList.add(bookmark) + this.formatField = this.formatField.copy(bookmarks = newList.toList()) + } + + override fun deleteBookmark(bookmarkId: BookmarkID) { + val newList = arrayListOf() + newList.addAll(this.formatField.bookmarks) + newList.removeIf { b -> b.bookmarkId == bookmarkId } + this.formatField = this.formatField.copy(bookmarks = newList.toList()) } override val drmInformationHandle: BookDRMInformationHandle diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/pdf/PdfViewerProviderTest.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/pdf/PdfViewerProviderTest.kt index 8a3692a86..e6d29edbd 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/pdf/PdfViewerProviderTest.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/pdf/PdfViewerProviderTest.kt @@ -3,10 +3,10 @@ package org.nypl.simplified.tests.pdf import one.irradia.mime.vanilla.MIMEParser import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test +import org.librarysimplified.viewer.pdf.pdfjs.PdfViewerProvider import org.mockito.Mockito import org.nypl.simplified.books.api.Book import org.nypl.simplified.books.api.BookFormat -import org.librarysimplified.viewer.pdf.pdfjs.PdfViewerProvider import org.nypl.simplified.viewer.spi.ViewerPreferences class PdfViewerProviderTest { diff --git a/simplified-tests/src/test/resources/org/nypl/simplified/tests/books/controller/testBooksRevokeEmptyFeed.xml b/simplified-tests/src/test/resources/org/nypl/simplified/tests/books/controller/testBooksRevokeEmptyFeed.xml index d3f59a924..c8e223a55 100644 --- a/simplified-tests/src/test/resources/org/nypl/simplified/tests/books/controller/testBooksRevokeEmptyFeed.xml +++ b/simplified-tests/src/test/resources/org/nypl/simplified/tests/books/controller/testBooksRevokeEmptyFeed.xml @@ -1,4 +1,4 @@ - + urn:feed:0 Feed diff --git a/simplified-tests/src/test/resources/org/nypl/simplified/tests/books/groups.xml b/simplified-tests/src/test/resources/org/nypl/simplified/tests/books/groups.xml index 8f9f34b0a..36da348d4 100644 --- a/simplified-tests/src/test/resources/org/nypl/simplified/tests/books/groups.xml +++ b/simplified-tests/src/test/resources/org/nypl/simplified/tests/books/groups.xml @@ -1,4 +1,5 @@ - + https://d3frcbs0h4wafj.cloudfront.net/groups/ Featured 2015-05-19T18:22:36Z diff --git a/simplified-tests/src/test/resources/org/nypl/simplified/tests/books/loans.xml b/simplified-tests/src/test/resources/org/nypl/simplified/tests/books/loans.xml index 2b25267e4..f6b06f77d 100644 --- a/simplified-tests/src/test/resources/org/nypl/simplified/tests/books/loans.xml +++ b/simplified-tests/src/test/resources/org/nypl/simplified/tests/books/loans.xml @@ -1,11 +1,5 @@ - + http://circulation.alpha.librarysimplified.org/loans/ Active loans diff --git a/simplified-tests/src/test/resources/org/nypl/simplified/tests/books/revoke-error-empty-feed-revoke.xml b/simplified-tests/src/test/resources/org/nypl/simplified/tests/books/revoke-error-empty-feed-revoke.xml index 418721688..531eb5537 100644 --- a/simplified-tests/src/test/resources/org/nypl/simplified/tests/books/revoke-error-empty-feed-revoke.xml +++ b/simplified-tests/src/test/resources/org/nypl/simplified/tests/books/revoke-error-empty-feed-revoke.xml @@ -1,10 +1,4 @@ - + http://circulation.alpha.librarysimplified.org/loans/ Active loans diff --git a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/acquisition-categories-0.xml b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/acquisition-categories-0.xml index 374d09807..6c7a4b1cb 100644 --- a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/acquisition-categories-0.xml +++ b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/acquisition-categories-0.xml @@ -1,10 +1,5 @@ - + http://circulation.alpha.librarysimplified.org/feed/Picture%20Books Picture Books: By author diff --git a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/acquisition-facets-0.xml b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/acquisition-facets-0.xml index 374d09807..6c7a4b1cb 100644 --- a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/acquisition-facets-0.xml +++ b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/acquisition-facets-0.xml @@ -1,10 +1,5 @@ - + http://circulation.alpha.librarysimplified.org/feed/Picture%20Books Picture Books: By author diff --git a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/acquisition-facets-1.xml b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/acquisition-facets-1.xml index 0de977396..326be742b 100644 --- a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/acquisition-facets-1.xml +++ b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/acquisition-facets-1.xml @@ -1,10 +1,5 @@ - + http://circulation.alpha.librarysimplified.org/feed/Picture%20Books Picture Books: By author diff --git a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/acquisition-fiction-0.xml b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/acquisition-fiction-0.xml index a39a25acb..ebd155ae9 100644 --- a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/acquisition-fiction-0.xml +++ b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/acquisition-fiction-0.xml @@ -1,4 +1,6 @@ - + https://d5v0j5lesri7q.cloudfront.net/NYBKLYN/groups/ All Books 2017-12-18T20:00:27Z diff --git a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/acquisition-groups-0.xml b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/acquisition-groups-0.xml index bc189c85e..2fec2b3fb 100644 --- a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/acquisition-groups-0.xml +++ b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/acquisition-groups-0.xml @@ -1,4 +1,7 @@ - + /NYNYPL/groups/ The New York Public Library 2020-09-02T13:10:18Z diff --git a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/acquisition-paginated-0.xml b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/acquisition-paginated-0.xml index d542ec191..792ff12e4 100644 --- a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/acquisition-paginated-0.xml +++ b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/acquisition-paginated-0.xml @@ -1,4 +1,7 @@ - + /NYNYPL/feed/13 Historical Fiction 2020-09-02T13:22:05Z diff --git a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/analytics-20190509.xml b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/analytics-20190509.xml index b7c18de32..d7337a2d4 100644 --- a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/analytics-20190509.xml +++ b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/analytics-20190509.xml @@ -1,4 +1,6 @@ - + https://circulation.librarysimplified.org/CLASSICS/search/1831?q=corner&entrypoint=Book Search 2019-05-09T17:14:04Z diff --git a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/bad-uri-syntax.xml b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/bad-uri-syntax.xml index 670ba73e1..647cdbb9e 100644 --- a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/bad-uri-syntax.xml +++ b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/bad-uri-syntax.xml @@ -1,10 +1,5 @@ - + http://circulation.alpha.librarysimplified.org/feed/Picture%20Books Picture Books: By author diff --git a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/dpla-test-feed.xml b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/dpla-test-feed.xml index 1a38f6e60..24a56fbd4 100644 --- a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/dpla-test-feed.xml +++ b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/dpla-test-feed.xml @@ -1,4 +1,7 @@ - + http://qa.circulation.librarysimplified.org/DEX/groups/?entrypoint=Audio DPLA Exchange Audiobook Test 2020-03-12T17:32:32Z diff --git a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/empty-0.xml b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/empty-0.xml index 3ce865ecc..9be440a0b 100644 --- a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/empty-0.xml +++ b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/empty-0.xml @@ -1,11 +1,4 @@ - + http://library-simplified.herokuapp.com/feed/Fiction Fiction: featured diff --git a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/entry-0.xml b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/entry-0.xml index c6daf8952..ab9c7b538 100644 --- a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/entry-0.xml +++ b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/entry-0.xml @@ -1,11 +1,5 @@ - + http://circulation.alpha.librarysimplified.org/works/?urn=urn%3Alibrarysimplified.org%2Fterms%2Fid%2FOverdrive%2520ID%2F64647fbc-2dd2-45ba-9080-a5820afd0e9a 50 Favourite Nursery Rhymes diff --git a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/loans.xml b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/loans.xml index e94e701d8..3d4b9eb95 100644 --- a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/loans.xml +++ b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/loans.xml @@ -1,4 +1,5 @@ - + http://circulation.alpha.librarysimplified.org/loans/ Active loans and holds 2015-06-26T18:48:49Z diff --git a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/minotaur-20231113.xml b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/minotaur-20231113.xml index ded222a74..f61f92edc 100644 --- a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/minotaur-20231113.xml +++ b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/minotaur-20231113.xml @@ -1,4 +1,7 @@ - + Minotaur Test Library https://minotaur.dev.palaceproject.io/minotaur-test-library/groups/ 2023-11-13T13:10:28+00:00 diff --git a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/navigation-0.xml b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/navigation-0.xml index 88f295191..3dcfbb91c 100644 --- a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/navigation-0.xml +++ b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/navigation-0.xml @@ -1,4 +1,4 @@ - + http://library-simplified.herokuapp.com/lanes/ Navigation feed 2015-02-03T13:39:40Z diff --git a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/navigation-bad-entry-featured-link-without-href.xml b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/navigation-bad-entry-featured-link-without-href.xml index 3131ecf74..6f5ace2af 100644 --- a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/navigation-bad-entry-featured-link-without-href.xml +++ b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/navigation-bad-entry-featured-link-without-href.xml @@ -1,4 +1,4 @@ - + http://library-simplified.herokuapp.com/lanes/ Navigation feed 2015-02-03T13:39:40Z diff --git a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/navigation-bad-entry-link-without-href.xml b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/navigation-bad-entry-link-without-href.xml index 0714b6563..92c74cf61 100644 --- a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/navigation-bad-entry-link-without-href.xml +++ b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/navigation-bad-entry-link-without-href.xml @@ -1,4 +1,4 @@ - + http://library-simplified.herokuapp.com/lanes/ Navigation feed 2015-02-03T13:39:40Z diff --git a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/navigation-bad-entry-no-links.xml b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/navigation-bad-entry-no-links.xml index 3e724f5a4..73f4327a3 100644 --- a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/navigation-bad-entry-no-links.xml +++ b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/navigation-bad-entry-no-links.xml @@ -1,4 +1,4 @@ - + http://library-simplified.herokuapp.com/lanes/ Navigation feed 2015-02-03T13:39:40Z diff --git a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/navigation-bad-entry-subsection-link-without-href.xml b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/navigation-bad-entry-subsection-link-without-href.xml index f94ee0606..82c0683c6 100644 --- a/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/navigation-bad-entry-subsection-link-without-href.xml +++ b/simplified-tests/src/test/resources/org/nypl/simplified/tests/opds/navigation-bad-entry-subsection-link-without-href.xml @@ -1,4 +1,4 @@ - + http://library-simplified.herokuapp.com/lanes/ Navigation feed 2015-02-03T13:39:40Z diff --git a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountCardCreatorFragment.kt b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountCardCreatorFragment.kt index 0dbe21d94..55c59cc85 100644 --- a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountCardCreatorFragment.kt +++ b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountCardCreatorFragment.kt @@ -8,11 +8,11 @@ import android.webkit.WebView import android.webkit.WebViewClient import androidx.core.os.bundleOf import androidx.fragment.app.Fragment +import org.librarysimplified.ui.accounts.R import org.nypl.simplified.android.ktx.supportActionBar import org.nypl.simplified.listeners.api.FragmentListenerType import org.nypl.simplified.listeners.api.fragmentListeners import org.nypl.simplified.webview.WebViewUtilities -import org.librarysimplified.ui.accounts.R import org.thepalaceproject.theme.core.PalaceToolbar /** diff --git a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountDetailFragment.kt b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountDetailFragment.kt index 7a8e593a9..1eb8b4b2d 100644 --- a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountDetailFragment.kt +++ b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountDetailFragment.kt @@ -14,7 +14,6 @@ import android.widget.ImageView import android.widget.ProgressBar import android.widget.TextView import androidx.activity.result.contract.ActivityResultContracts -import com.google.android.material.dialog.MaterialAlertDialogBuilder import androidx.appcompat.widget.SwitchCompat import androidx.constraintlayout.widget.ConstraintLayout import androidx.core.app.ActivityCompat @@ -23,10 +22,12 @@ import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import com.google.android.gms.location.LocationServices +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.io7m.junreachable.UnimplementedCodeException import com.io7m.junreachable.UnreachableCodeException import io.reactivex.disposables.CompositeDisposable import org.librarysimplified.services.api.Services +import org.librarysimplified.ui.accounts.R import org.nypl.simplified.accounts.api.AccountAuthenticationCredentials import org.nypl.simplified.accounts.api.AccountLoginState import org.nypl.simplified.accounts.api.AccountLoginState.AccountLoggedIn @@ -57,9 +58,8 @@ import org.nypl.simplified.ui.accounts.AccountLoginButtonStatus.AsLogoutButtonEn import org.nypl.simplified.ui.images.ImageAccountIcons import org.nypl.simplified.ui.images.ImageLoaderType import org.slf4j.LoggerFactory -import java.net.URI -import org.librarysimplified.ui.accounts.R import org.thepalaceproject.theme.core.PalaceToolbar +import java.net.URI /** * A fragment that shows settings for a single account. diff --git a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountListFragment.kt b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountListFragment.kt index 227f84af3..209c101c4 100644 --- a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountListFragment.kt +++ b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountListFragment.kt @@ -6,15 +6,16 @@ import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View -import com.google.android.material.dialog.MaterialAlertDialogBuilder import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.SimpleItemAnimator +import com.google.android.material.dialog.MaterialAlertDialogBuilder import io.reactivex.disposables.CompositeDisposable import org.librarysimplified.services.api.Services +import org.librarysimplified.ui.accounts.R import org.nypl.simplified.accounts.api.AccountEvent import org.nypl.simplified.accounts.api.AccountEventCreation import org.nypl.simplified.accounts.api.AccountEventDeletion @@ -26,7 +27,6 @@ import org.nypl.simplified.listeners.api.FragmentListenerType import org.nypl.simplified.listeners.api.fragmentListeners import org.nypl.simplified.ui.errorpage.ErrorPageParameters import org.nypl.simplified.ui.images.ImageLoaderType -import org.librarysimplified.ui.accounts.R import org.thepalaceproject.theme.core.PalaceToolbar /** diff --git a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountListRegistryFragment.kt b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountListRegistryFragment.kt index 4b93665cf..1742669a5 100644 --- a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountListRegistryFragment.kt +++ b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountListRegistryFragment.kt @@ -1,6 +1,5 @@ package org.nypl.simplified.ui.accounts -import com.google.android.material.dialog.MaterialAlertDialogBuilder import android.annotation.SuppressLint import android.content.Context import android.location.LocationManager @@ -21,8 +20,10 @@ import androidx.core.widget.ContentLoadingProgressBar import androidx.fragment.app.Fragment import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.google.android.material.dialog.MaterialAlertDialogBuilder import io.reactivex.disposables.CompositeDisposable import org.librarysimplified.services.api.Services +import org.librarysimplified.ui.accounts.R import org.nypl.simplified.accounts.api.AccountEvent import org.nypl.simplified.accounts.api.AccountEventCreation import org.nypl.simplified.accounts.api.AccountProviderDescription @@ -33,7 +34,6 @@ import org.nypl.simplified.listeners.api.fragmentListeners import org.nypl.simplified.ui.errorpage.ErrorPageParameters import org.nypl.simplified.ui.images.ImageLoaderType import org.slf4j.LoggerFactory -import org.librarysimplified.ui.accounts.R import org.thepalaceproject.theme.core.PalaceToolbar /** diff --git a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountPickerDialogFragment.kt b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountPickerDialogFragment.kt index 10947ff0d..d9650a9e3 100644 --- a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountPickerDialogFragment.kt +++ b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountPickerDialogFragment.kt @@ -14,6 +14,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetDialogFragment import org.librarysimplified.services.api.Services +import org.librarysimplified.ui.accounts.R import org.nypl.simplified.accounts.api.AccountID import org.nypl.simplified.accounts.database.api.AccountType import org.nypl.simplified.listeners.api.FragmentListenerType @@ -23,7 +24,6 @@ import org.nypl.simplified.ui.accounts.AccountPickerAdapter.OnAccountClickListen import org.nypl.simplified.ui.images.ImageAccountIcons import org.nypl.simplified.ui.images.ImageLoaderType import org.slf4j.LoggerFactory -import org.librarysimplified.ui.accounts.R /** * Present a dialog that shows a list of all active accounts for the current profile. diff --git a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/FilterableAccountListAdapter.kt b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/FilterableAccountListAdapter.kt index 847edbff3..6e4f457b5 100644 --- a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/FilterableAccountListAdapter.kt +++ b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/FilterableAccountListAdapter.kt @@ -8,11 +8,11 @@ import android.widget.TextView import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ListAdapter import androidx.recyclerview.widget.RecyclerView +import org.librarysimplified.ui.accounts.R import org.nypl.simplified.accounts.api.AccountProviderDescription import org.nypl.simplified.ui.images.ImageAccountIcons import org.nypl.simplified.ui.images.ImageLoaderType import org.slf4j.LoggerFactory -import org.librarysimplified.ui.accounts.R /** * Adapter for showing a list of `AccountProviderDescription` items. diff --git a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/saml20/AccountSAML20Fragment.kt b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/saml20/AccountSAML20Fragment.kt index 209ce721d..22ccbf926 100644 --- a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/saml20/AccountSAML20Fragment.kt +++ b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/saml20/AccountSAML20Fragment.kt @@ -4,11 +4,12 @@ import android.os.Bundle import android.view.View import android.webkit.WebView import android.widget.ProgressBar -import com.google.android.material.dialog.MaterialAlertDialogBuilder import androidx.core.os.bundleOf import androidx.fragment.app.Fragment import androidx.fragment.app.viewModels +import com.google.android.material.dialog.MaterialAlertDialogBuilder import io.reactivex.disposables.CompositeDisposable +import org.librarysimplified.ui.accounts.R import org.nypl.simplified.accounts.database.api.AccountType import org.nypl.simplified.listeners.api.FragmentListenerType import org.nypl.simplified.listeners.api.fragmentListeners @@ -16,7 +17,6 @@ import org.nypl.simplified.taskrecorder.api.TaskRecorder import org.nypl.simplified.taskrecorder.api.TaskStep import org.nypl.simplified.ui.errorpage.ErrorPageParameters import org.nypl.simplified.webview.WebViewUtilities -import org.librarysimplified.ui.accounts.R /** * A fragment that performs the SAML 2.0 login workflow. diff --git a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/saml20/AccountSAML20ViewModel.kt b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/saml20/AccountSAML20ViewModel.kt index 040a210fd..e0d81f92c 100644 --- a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/saml20/AccountSAML20ViewModel.kt +++ b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/saml20/AccountSAML20ViewModel.kt @@ -14,6 +14,7 @@ import io.reactivex.android.schedulers.AndroidSchedulers import io.reactivex.disposables.CompositeDisposable import io.reactivex.subjects.PublishSubject import org.librarysimplified.services.api.Services +import org.librarysimplified.ui.accounts.R import org.nypl.simplified.accounts.api.AccountCookie import org.nypl.simplified.accounts.api.AccountID import org.nypl.simplified.accounts.api.AccountProviderAuthenticationDescription @@ -25,7 +26,6 @@ import org.slf4j.Logger import org.slf4j.LoggerFactory import java.io.File import java.util.concurrent.atomic.AtomicReference -import org.librarysimplified.ui.accounts.R /** * View state for the SAML 2.0 fragment. diff --git a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/view_bindings/ViewsForBasicToken.kt b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/view_bindings/ViewsForBasicToken.kt index e0dc596dd..4763f0f31 100644 --- a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/view_bindings/ViewsForBasicToken.kt +++ b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/view_bindings/ViewsForBasicToken.kt @@ -9,12 +9,12 @@ import android.widget.TextView import androidx.core.view.isVisible import com.google.android.material.textfield.TextInputEditText import com.google.android.material.textfield.TextInputLayout +import org.librarysimplified.ui.accounts.R import org.nypl.simplified.accounts.api.AccountPassword import org.nypl.simplified.accounts.api.AccountProviderAuthenticationDescription import org.nypl.simplified.accounts.api.AccountUsername import org.nypl.simplified.ui.accounts.AccountLoginButtonStatus import org.nypl.simplified.ui.accounts.OnTextChangeListener -import org.librarysimplified.ui.accounts.R import org.slf4j.LoggerFactory class ViewsForBasicToken( diff --git a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/view_bindings/ViewsForCOPPAAgeGate.kt b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/view_bindings/ViewsForCOPPAAgeGate.kt index 5605ac3f0..400082a20 100644 --- a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/view_bindings/ViewsForCOPPAAgeGate.kt +++ b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/view_bindings/ViewsForCOPPAAgeGate.kt @@ -3,8 +3,8 @@ package org.nypl.simplified.ui.accounts.view_bindings import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.SwitchCompat -import org.nypl.simplified.ui.accounts.AccountLoginButtonStatus import org.librarysimplified.ui.accounts.R +import org.nypl.simplified.ui.accounts.AccountLoginButtonStatus class ViewsForCOPPAAgeGate( override val viewGroup: ViewGroup, diff --git a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/view_bindings/ViewsForSAML20.kt b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/view_bindings/ViewsForSAML20.kt index 74075a860..39854c09b 100644 --- a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/view_bindings/ViewsForSAML20.kt +++ b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/view_bindings/ViewsForSAML20.kt @@ -4,9 +4,9 @@ import android.view.ViewGroup import android.widget.Button import android.widget.TextView import androidx.core.view.isVisible +import org.librarysimplified.ui.accounts.R import org.nypl.simplified.accounts.api.AccountProviderAuthenticationDescription import org.nypl.simplified.ui.accounts.AccountLoginButtonStatus -import org.librarysimplified.ui.accounts.R class ViewsForSAML20( override val viewGroup: ViewGroup, diff --git a/simplified-ui-accounts/src/main/res/layout/account_list_item_old.xml b/simplified-ui-accounts/src/main/res/layout/account_list_item_old.xml index 85a1cf8f1..72f8036d3 100644 --- a/simplified-ui-accounts/src/main/res/layout/account_list_item_old.xml +++ b/simplified-ui-accounts/src/main/res/layout/account_list_item_old.xml @@ -1,6 +1,5 @@ diff --git a/simplified-ui-navigation-tabs/src/main/java/org/librarysimplified/ui/navigation/tabs/BottomNavigators.kt b/simplified-ui-navigation-tabs/src/main/java/org/librarysimplified/ui/navigation/tabs/BottomNavigators.kt index 79e41623f..e41276c30 100644 --- a/simplified-ui-navigation-tabs/src/main/java/org/librarysimplified/ui/navigation/tabs/BottomNavigators.kt +++ b/simplified-ui-navigation-tabs/src/main/java/org/librarysimplified/ui/navigation/tabs/BottomNavigators.kt @@ -8,6 +8,10 @@ import com.google.android.material.bottomnavigation.LabelVisibilityMode import com.io7m.junreachable.UnreachableCodeException import com.pandora.bottomnavigator.BottomNavigator import org.joda.time.DateTime +import org.librarysimplified.ui.catalog.CatalogFeedArguments +import org.librarysimplified.ui.catalog.CatalogFeedFragment +import org.librarysimplified.ui.catalog.CatalogFeedOwnership +import org.librarysimplified.ui.tabs.R import org.nypl.simplified.accounts.api.AccountProviderType import org.nypl.simplified.accounts.database.api.AccountType import org.nypl.simplified.accounts.registry.api.AccountProviderRegistryType @@ -15,10 +19,6 @@ import org.nypl.simplified.buildconfig.api.BuildConfigurationServiceType import org.nypl.simplified.feeds.api.FeedBooksSelection import org.nypl.simplified.feeds.api.FeedFacet import org.nypl.simplified.profiles.controller.api.ProfilesControllerType -import org.librarysimplified.ui.catalog.CatalogFeedArguments -import org.librarysimplified.ui.catalog.CatalogFeedFragment -import org.librarysimplified.ui.catalog.CatalogFeedOwnership -import org.librarysimplified.ui.tabs.R import org.nypl.simplified.ui.settings.SettingsMainFragment import org.slf4j.LoggerFactory diff --git a/simplified-ui-onboarding/src/main/java/org/librarysimplified/ui/onboarding/OnboardingDefaultViewModelFactory.kt b/simplified-ui-onboarding/src/main/java/org/librarysimplified/ui/onboarding/OnboardingDefaultViewModelFactory.kt index b237c015d..a07e713b7 100644 --- a/simplified-ui-onboarding/src/main/java/org/librarysimplified/ui/onboarding/OnboardingDefaultViewModelFactory.kt +++ b/simplified-ui-onboarding/src/main/java/org/librarysimplified/ui/onboarding/OnboardingDefaultViewModelFactory.kt @@ -1,8 +1,8 @@ package org.librarysimplified.ui.onboarding import androidx.lifecycle.ViewModelProvider -import org.nypl.simplified.listeners.api.ListenerRepositoryFactory import org.nypl.simplified.listeners.api.ListenerRepository +import org.nypl.simplified.listeners.api.ListenerRepositoryFactory import org.nypl.simplified.ui.accounts.AccountListRegistryEvent class OnboardingDefaultViewModelFactory(fallbackFactory: ViewModelProvider.Factory) : diff --git a/simplified-ui-onboarding/src/main/java/org/librarysimplified/ui/onboarding/OnboardingFragment.kt b/simplified-ui-onboarding/src/main/java/org/librarysimplified/ui/onboarding/OnboardingFragment.kt index fe96b7151..75a760118 100644 --- a/simplified-ui-onboarding/src/main/java/org/librarysimplified/ui/onboarding/OnboardingFragment.kt +++ b/simplified-ui-onboarding/src/main/java/org/librarysimplified/ui/onboarding/OnboardingFragment.kt @@ -17,8 +17,8 @@ import org.nypl.simplified.listeners.api.ListenerRepository import org.nypl.simplified.listeners.api.fragmentListeners import org.nypl.simplified.listeners.api.listenerRepositories import org.nypl.simplified.profiles.controller.api.ProfilesControllerType -import org.nypl.simplified.ui.accounts.AccountListRegistryFragment import org.nypl.simplified.ui.accounts.AccountListRegistryEvent +import org.nypl.simplified.ui.accounts.AccountListRegistryFragment import org.nypl.simplified.ui.errorpage.ErrorPageFragment import org.nypl.simplified.ui.errorpage.ErrorPageParameters import org.slf4j.Logger diff --git a/simplified-ui-onboarding/src/main/res/layout/onboarding_start_screen.xml b/simplified-ui-onboarding/src/main/res/layout/onboarding_start_screen.xml index 20e24e24c..ae0c06fdf 100644 --- a/simplified-ui-onboarding/src/main/res/layout/onboarding_start_screen.xml +++ b/simplified-ui-onboarding/src/main/res/layout/onboarding_start_screen.xml @@ -1,6 +1,5 @@ + android:layout_width="match_parent" + android:layout_height="match_parent" + android:scrollbarAlwaysDrawHorizontalTrack="true"> { - val rawBookmarks = - loadRawBookmarks( - bookmarkService = bookmarkService, - accountID = accountID, - bookID = bookID - ) - - // for now we'll keep the existing behavior and ignore the "lastReadServer" field that is always - // null for the audiobook bookmarks - val lastRead = rawBookmarks.lastReadLocal - val explicits = rawBookmarks.bookmarks - - val results = mutableListOf() - lastRead?.let(results::add) - results.addAll(explicits) - return results.toList() - } - - fun toPlayerBookmark(bookmark: Bookmark.AudiobookBookmark): PlayerBookmark { - return PlayerBookmark( - date = bookmark.time, - position = bookmark.location, - duration = bookmark.duration, - uri = bookmark.uri - ) - } - - fun fromPlayerBookmark( - opdsId: String, - deviceID: String, - playerBookmark: PlayerBookmark - ): Bookmark.AudiobookBookmark { - return Bookmark.AudiobookBookmark( - opdsId = opdsId, - deviceID = deviceID, - time = playerBookmark.date, - kind = BookmarkKind.BookmarkExplicit, - uri = playerBookmark.uri, - location = playerBookmark.position, - duration = playerBookmark.duration - ) - } -} diff --git a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookLoadingFragment2.kt b/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookLoadingFragment2.kt new file mode 100644 index 000000000..69efa59d3 --- /dev/null +++ b/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookLoadingFragment2.kt @@ -0,0 +1,5 @@ +package org.librarysimplified.viewer.audiobook + +import androidx.fragment.app.Fragment + +class AudioBookLoadingFragment2 : Fragment(R.layout.audio_book_player_loading) diff --git a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookPlayerActivity2.kt b/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookPlayerActivity2.kt new file mode 100644 index 000000000..5ff546007 --- /dev/null +++ b/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookPlayerActivity2.kt @@ -0,0 +1,73 @@ +package org.librarysimplified.viewer.audiobook + +import androidx.annotation.UiThread +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.app.AppCompatDelegate +import androidx.appcompat.app.TxContextWrappingDelegate2 +import androidx.fragment.app.Fragment +import io.reactivex.disposables.CompositeDisposable +import org.librarysimplified.audiobook.api.PlayerEvent +import org.librarysimplified.audiobook.api.PlayerUIThread +import org.librarysimplified.audiobook.api.extensions.PlayerExtensionType +import org.librarysimplified.audiobook.views.PlayerModel +import org.librarysimplified.audiobook.views.PlayerModelState +import org.librarysimplified.audiobook.views.PlayerViewCommand + +class AudioBookPlayerActivity2 : AppCompatActivity(R.layout.audio_book_player_base) { + + private val playerExtensions: List = listOf() + private var fragmentNow: Fragment = AudioBookLoadingFragment2() + private var subscriptions: CompositeDisposable = CompositeDisposable() + + private val appCompatDelegate: TxContextWrappingDelegate2 by lazy { + TxContextWrappingDelegate2(super.getDelegate()) + } + + override fun getDelegate(): AppCompatDelegate { + return this.appCompatDelegate + } + + override fun onStart() { + super.onStart() + + this.subscriptions = CompositeDisposable() + this.subscriptions.add(PlayerModel.stateEvents.subscribe(this::onModelStateEvent)) + this.subscriptions.add(PlayerModel.viewCommands.subscribe(this::onPlayerViewCommand)) + this.subscriptions.add(PlayerModel.playerEvents.subscribe(this::onPlayerEvent)) + } + + override fun onStop() { + super.onStop() + this.subscriptions.dispose() + } + + @UiThread + private fun onPlayerEvent( + event: PlayerEvent + ) { + PlayerUIThread.checkIsUIThread() + } + + @UiThread + private fun onModelStateEvent( + state: PlayerModelState + ) { + PlayerUIThread.checkIsUIThread() + } + + @UiThread + private fun onPlayerViewCommand( + command: PlayerViewCommand + ) { + PlayerUIThread.checkIsUIThread() + } + + private fun switchFragment( + fragment: Fragment + ) { + this.fragmentNow = fragment + this.supportFragmentManager.beginTransaction() + .replace(R.id.audio_book_player_fragment_holder, fragment) + .commit() + } +} diff --git a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookViewer.kt b/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookViewer.kt index c713abe39..f3b48eeae 100644 --- a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookViewer.kt +++ b/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookViewer.kt @@ -1,6 +1,7 @@ package org.librarysimplified.viewer.audiobook -import android.app.Activity +import android.app.Application +import android.content.Intent import one.irradia.mime.api.MIMEType import org.librarysimplified.http.api.LSHTTPClientType import org.librarysimplified.services.api.Services @@ -45,7 +46,7 @@ class AudioBookViewer : ViewerProviderType { } override fun open( - activity: Activity, + context: Application, preferences: ViewerPreferences, book: Book, format: BookFormat, @@ -78,6 +79,6 @@ class AudioBookViewer : ViewerProviderType { null } - AudioBookPlayerActivity.startActivity(activity, params) + context.startActivity(Intent(context, AudioBookPlayerActivity2::class.java)) } } diff --git a/simplified-viewer-epub-readium2/src/main/java/org/librarysimplified/viewer/epub/readium2/Reader2Activity.kt b/simplified-viewer-epub-readium2/src/main/java/org/librarysimplified/viewer/epub/readium2/Reader2Activity.kt index b5bd5be1f..d2421c365 100644 --- a/simplified-viewer-epub-readium2/src/main/java/org/librarysimplified/viewer/epub/readium2/Reader2Activity.kt +++ b/simplified-viewer-epub-readium2/src/main/java/org/librarysimplified/viewer/epub/readium2/Reader2Activity.kt @@ -1,6 +1,6 @@ package org.librarysimplified.viewer.epub.readium2 -import android.app.Activity +import android.app.Application import android.content.Intent import android.content.pm.ApplicationInfo import android.os.Bundle @@ -79,7 +79,7 @@ class Reader2Activity : AppCompatActivity(R.layout.reader2) { */ fun startActivity( - context: Activity, + context: Application, parameters: Reader2ActivityParameters ) { val intent = Intent(context, Reader2Activity::class.java) diff --git a/simplified-viewer-epub-readium2/src/main/java/org/librarysimplified/viewer/epub/readium2/Reader2Bookmarks.kt b/simplified-viewer-epub-readium2/src/main/java/org/librarysimplified/viewer/epub/readium2/Reader2Bookmarks.kt index 53dbc0aae..743c65de4 100644 --- a/simplified-viewer-epub-readium2/src/main/java/org/librarysimplified/viewer/epub/readium2/Reader2Bookmarks.kt +++ b/simplified-viewer-epub-readium2/src/main/java/org/librarysimplified/viewer/epub/readium2/Reader2Bookmarks.kt @@ -4,12 +4,16 @@ import org.librarysimplified.r2.api.SR2Bookmark import org.librarysimplified.r2.api.SR2Locator import org.nypl.simplified.accounts.api.AccountID import org.nypl.simplified.bookmarks.api.BookmarkServiceUsableType -import org.nypl.simplified.bookmarks.api.Bookmarks -import org.nypl.simplified.books.api.BookChapterProgress +import org.nypl.simplified.bookmarks.api.BookmarksForBook import org.nypl.simplified.books.api.BookID -import org.nypl.simplified.books.api.BookLocation -import org.nypl.simplified.books.api.bookmark.Bookmark import org.nypl.simplified.books.api.bookmark.BookmarkKind +import org.nypl.simplified.books.api.bookmark.SerializedBookmark +import org.nypl.simplified.books.api.bookmark.SerializedBookmarks +import org.nypl.simplified.books.api.bookmark.SerializedLocatorAudioBookTime1 +import org.nypl.simplified.books.api.bookmark.SerializedLocatorAudioBookTime2 +import org.nypl.simplified.books.api.bookmark.SerializedLocatorHrefProgression20210317 +import org.nypl.simplified.books.api.bookmark.SerializedLocatorLegacyCFI +import org.nypl.simplified.books.api.bookmark.SerializedLocatorPage1 import org.nypl.simplified.feeds.api.FeedEntry import org.readium.r2.shared.publication.Href import org.slf4j.LoggerFactory @@ -28,14 +32,14 @@ object Reader2Bookmarks { bookmarkService: BookmarkServiceUsableType, accountID: AccountID, bookID: BookID - ): Bookmarks { + ): BookmarksForBook { return try { bookmarkService .bookmarkSyncAndLoad(accountID, bookID) .get(15L, TimeUnit.SECONDS) } catch (e: Exception) { - logger.error("could not load bookmarks: ", e) - Bookmarks(null, null, emptyList()) + this.logger.error("could not load bookmarks: ", e) + BookmarksForBook(bookID, null, emptyList()) } } @@ -49,21 +53,19 @@ object Reader2Bookmarks { bookID: BookID ): List { val rawBookmarks = - loadRawBookmarks( + this.loadRawBookmarks( bookmarkService = bookmarkService, accountID = accountID, bookID = bookID ) + val lastReadLocal = - rawBookmarks.lastReadLocal?.let { toSR2Bookmark(it) } - val lastReadServer = - rawBookmarks.lastReadServer?.let { toSR2Bookmark(it) } + rawBookmarks.lastRead?.let { this.toSR2Bookmark(it) } val explicits = - rawBookmarks.bookmarks.mapNotNull { toSR2Bookmark(it) } + rawBookmarks.bookmarks.mapNotNull { this.toSR2Bookmark(it) } val results = mutableListOf() lastReadLocal?.let(results::add) - lastReadServer?.let(results::add) results.addAll(explicits) return results.toList() } @@ -76,34 +78,37 @@ object Reader2Bookmarks { bookEntry: FeedEntry.FeedEntryOPDS, deviceId: String, source: SR2Bookmark - ): Bookmark.ReaderBookmark { - val progress = BookChapterProgress( - chapterHref = source.locator.chapterHref.toString(), - chapterProgress = when (val locator = source.locator) { + ): SerializedBookmark { + val chapterHref = + source.locator.chapterHref.toString() + val chapterProgress = + when (val locator = source.locator) { is SR2Locator.SR2LocatorPercent -> locator.chapterProgress is SR2Locator.SR2LocatorChapterEnd -> 1.0 } - ) val location = - BookLocation.BookLocationR2(progress) + SerializedLocatorHrefProgression20210317(chapterHref, chapterProgress) val kind = when (source.type) { SR2Bookmark.Type.EXPLICIT -> BookmarkKind.BookmarkExplicit + SR2Bookmark.Type.LAST_READ -> BookmarkKind.BookmarkLastReadLocation } - return Bookmark.ReaderBookmark.create( - opdsId = bookEntry.feedEntry.id, + return SerializedBookmarks.createWithCurrentFormat( + bookChapterProgress = chapterProgress, + bookChapterTitle = source.title, + bookProgress = source.bookProgress ?: 0.0, + bookTitle = bookEntry.feedEntry.title, + deviceID = deviceId, + kind = kind, location = location, + opdsId = bookEntry.feedEntry.id, time = source.date, - kind = kind, - chapterTitle = source.title, - bookProgress = source.bookProgress, - deviceID = deviceId, - uri = source.uri + uri = source.uri, ) } @@ -112,39 +117,41 @@ object Reader2Bookmarks { */ fun toSR2Bookmark( - source: Bookmark + source: SerializedBookmark ): SR2Bookmark? { - if (source !is Bookmark.ReaderBookmark) { - throw IllegalStateException("Unsupported type of bookmark: $source") - } return when (val location = source.location) { - is BookLocation.BookLocationR2 -> - r2ToSR2Bookmark(source, location) - is BookLocation.BookLocationR1 -> - null // R1 bookmarks are not supported any more. - } - } + is SerializedLocatorAudioBookTime1, + is SerializedLocatorAudioBookTime2, + is SerializedLocatorLegacyCFI, + is SerializedLocatorPage1 -> { + // None of these locator formats are suitable for EPUBs in R2 + null + } - private fun r2ToSR2Bookmark( - source: Bookmark.ReaderBookmark, - location: BookLocation.BookLocationR2 - ): SR2Bookmark { - return SR2Bookmark( - date = source.time.toDateTime(), - type = when (source.kind) { - BookmarkKind.BookmarkLastReadLocation -> - SR2Bookmark.Type.LAST_READ - - BookmarkKind.BookmarkExplicit -> - SR2Bookmark.Type.EXPLICIT - }, - title = source.chapterTitle, - locator = SR2Locator.SR2LocatorPercent( - chapterHref = Href(location.progress.chapterHref)!!, - chapterProgress = location.progress.chapterProgress - ), - bookProgress = source.bookProgress, - uri = source.uri - ) + is SerializedLocatorHrefProgression20210317 -> { + val href = Href(location.chapterHref) + if (href != null) { + SR2Bookmark( + date = source.time.toDateTime(), + type = when (source.kind) { + BookmarkKind.BookmarkLastReadLocation -> + SR2Bookmark.Type.LAST_READ + + BookmarkKind.BookmarkExplicit -> + SR2Bookmark.Type.EXPLICIT + }, + title = source.bookChapterTitle, + locator = SR2Locator.SR2LocatorPercent( + chapterHref = href, + chapterProgress = location.chapterProgress + ), + bookProgress = source.bookProgress, + uri = source.uri + ) + } else { + null + } + } + } } } diff --git a/simplified-viewer-epub-readium2/src/main/java/org/librarysimplified/viewer/epub/readium2/ReaderViewerR2.kt b/simplified-viewer-epub-readium2/src/main/java/org/librarysimplified/viewer/epub/readium2/ReaderViewerR2.kt index e94834657..87b198a40 100644 --- a/simplified-viewer-epub-readium2/src/main/java/org/librarysimplified/viewer/epub/readium2/ReaderViewerR2.kt +++ b/simplified-viewer-epub-readium2/src/main/java/org/librarysimplified/viewer/epub/readium2/ReaderViewerR2.kt @@ -1,6 +1,6 @@ package org.librarysimplified.viewer.epub.readium2 -import android.app.Activity +import android.app.Application import one.irradia.mime.api.MIMEType import org.nypl.simplified.books.api.Book import org.nypl.simplified.books.api.BookFormat @@ -33,7 +33,7 @@ class ReaderViewerR2 : ViewerProviderType { } override fun open( - activity: Activity, + context: Application, preferences: ViewerPreferences, book: Book, format: BookFormat, @@ -56,7 +56,7 @@ class ReaderViewerR2 : ViewerProviderType { ) Reader2Activity.startActivity( - context = activity, + context = context, parameters = parameters ) } diff --git a/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfBookmark.kt b/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfBookmark.kt new file mode 100644 index 000000000..55ad0c7cd --- /dev/null +++ b/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfBookmark.kt @@ -0,0 +1,9 @@ +package org.librarysimplified.viewer.pdf.pdfjs + +import org.joda.time.DateTime + +data class PdfBookmark( + val kind: PdfBookmarkKind, + val pageNumber: Int, + val time: DateTime, +) diff --git a/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfBookmarkKind.kt b/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfBookmarkKind.kt new file mode 100644 index 000000000..1518bd6e8 --- /dev/null +++ b/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfBookmarkKind.kt @@ -0,0 +1,6 @@ +package org.librarysimplified.viewer.pdf.pdfjs + +enum class PdfBookmarkKind { + EXPLICIT, + LAST_READ +} diff --git a/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/factory/PdfDocumentFactory.kt b/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfDocumentFactory.kt similarity index 95% rename from simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/factory/PdfDocumentFactory.kt rename to simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfDocumentFactory.kt index fcb4d10c5..c24ca2498 100644 --- a/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/factory/PdfDocumentFactory.kt +++ b/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfDocumentFactory.kt @@ -1,4 +1,4 @@ -package org.librarysimplified.viewer.pdf.pdfjs.factory +package org.librarysimplified.viewer.pdf.pdfjs import android.app.Application import com.shockwave.pdfium.PdfiumCore diff --git a/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfReaderActivity.kt b/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfReaderActivity.kt index 64cc71349..ef771cb8d 100644 --- a/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfReaderActivity.kt +++ b/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfReaderActivity.kt @@ -1,6 +1,6 @@ package org.librarysimplified.viewer.pdf.pdfjs -import android.app.Activity +import android.app.Application import android.content.Context import android.content.Intent import android.os.Bundle @@ -27,8 +27,9 @@ import org.nypl.simplified.bookmarks.api.BookmarkServiceType import org.nypl.simplified.books.api.BookContentProtections import org.nypl.simplified.books.api.BookDRMInformation import org.nypl.simplified.books.api.BookID -import org.nypl.simplified.books.api.bookmark.Bookmark import org.nypl.simplified.books.api.bookmark.BookmarkKind +import org.nypl.simplified.books.api.bookmark.SerializedBookmarks +import org.nypl.simplified.books.api.bookmark.SerializedLocatorPage1 import org.nypl.simplified.feeds.api.FeedEntry import org.nypl.simplified.profiles.controller.api.ProfilesControllerType import org.nypl.simplified.ui.thread.api.UIThreadServiceType @@ -50,18 +51,18 @@ class PdfReaderActivity : AppCompatActivity() { * Factory method to start a [PdfReaderActivity] */ fun startActivity( - from: Activity, + context: Application, parameters: PdfReaderParameters ) { val bundle = Bundle().apply { - putSerializable(PARAMS_ID, parameters) + this.putSerializable(this@Companion.PARAMS_ID, parameters) } - val intent = Intent(from, PdfReaderActivity::class.java).apply { - putExtras(bundle) + val intent = Intent(context, PdfReaderActivity::class.java).apply { + this.putExtras(bundle) } - from.startActivity(intent) + context.startActivity(intent) } } @@ -70,9 +71,9 @@ class PdfReaderActivity : AppCompatActivity() { private val services = Services.serviceDirectory() private val bookmarkService = - services.requireService(BookmarkServiceType::class.java) + this.services.requireService(BookmarkServiceType::class.java) private val profilesController = - services.requireService(ProfilesControllerType::class.java) + this.services.requireService(ProfilesControllerType::class.java) private lateinit var uiThread: UIThreadServiceType private lateinit var pdfReaderContainer: FrameLayout @@ -89,12 +90,12 @@ class PdfReaderActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val params = intent?.getSerializableExtra(PARAMS_ID) as PdfReaderParameters + val params = this.intent?.getSerializableExtra(PARAMS_ID) as PdfReaderParameters - setContentView(R.layout.pdfjs_reader) - createToolbar(params.documentTitle) + this.setContentView(R.layout.pdfjs_reader) + this.createToolbar(params.documentTitle) - this.loadingBar = findViewById(R.id.pdf_loading_progress) + this.loadingBar = this.findViewById(R.id.pdf_loading_progress) this.accountId = params.accountId this.feedEntry = params.entry this.bookID = params.id @@ -108,28 +109,34 @@ class PdfReaderActivity : AppCompatActivity() { MDC.remove(MDCKeys.BOOK_DRM) this.uiThread = - services.requireService(UIThreadServiceType::class.java) + this.services.requireService(UIThreadServiceType::class.java) val backgroundThread = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(1)) backgroundThread.execute { - restoreSavedPosition( + this.restoreSavedPosition( params = params, isSavedInstanceStateNull = savedInstanceState == null ) } } - private fun completeReaderSetup(params: PdfReaderParameters, isSavedInstanceStateNull: Boolean) { + private fun completeReaderSetup( + params: PdfReaderParameters, + isSavedInstanceStateNull: Boolean + ) { this.loadingBar.visibility = View.GONE if (isSavedInstanceStateNull) { - createWebView() - createPdfServer(params.drmInfo, params.pdfFile) + this.createWebView() + this.createPdfServer(params.drmInfo, params.pdfFile) } } - private fun restoreSavedPosition(params: PdfReaderParameters, isSavedInstanceStateNull: Boolean) { + private fun restoreSavedPosition( + params: PdfReaderParameters, + isSavedInstanceStateNull: Boolean + ) { val bookmarks = PdfReaderBookmarks.loadBookmarks( bookmarkService = this.bookmarkService, @@ -137,51 +144,21 @@ class PdfReaderActivity : AppCompatActivity() { bookID = this.bookID ) - val lastReadBookmarks = bookmarks - .filterIsInstance() - .filter { bookmark -> - bookmark.kind == BookmarkKind.BookmarkLastReadLocation - } + val lastReadBookmark = + bookmarks.filter { bookmark -> bookmark.kind == PdfBookmarkKind.LAST_READ } + .firstOrNull() this.uiThread.runOnUIThread { try { - // if there's more than one last read bookmark, we'll need to compare their dates - if (lastReadBookmarks.size > 1) { - val localLastReadBookmark = lastReadBookmarks.first() - val serverLastReadBookmark = lastReadBookmarks.last() - - if (serverLastReadBookmark.time.isAfter(localLastReadBookmark.time) && - localLastReadBookmark.pageNumber != serverLastReadBookmark.pageNumber - ) { - showBookmarkPrompt( - localLastReadBookmark = localLastReadBookmark, - serverLastReadBookmark = serverLastReadBookmark, - params = params, - isSavedInstanceStateNull = isSavedInstanceStateNull - ) - } else { - this.documentPageIndex = lastReadBookmarks.first().pageNumber - completeReaderSetup( - params = params, - isSavedInstanceStateNull = isSavedInstanceStateNull - ) - } - } else if (lastReadBookmarks.isNotEmpty()) { - this.documentPageIndex = lastReadBookmarks.first().pageNumber - - completeReaderSetup( - params = params, - isSavedInstanceStateNull = isSavedInstanceStateNull - ) + if (lastReadBookmark != null) { + this.documentPageIndex = lastReadBookmark.pageNumber } else { - completeReaderSetup( - params = params, - isSavedInstanceStateNull = isSavedInstanceStateNull - ) + this.documentPageIndex = 1 } } catch (e: Exception) { - log.error("Could not get lastReadLocation, defaulting to the 1st page", e) - completeReaderSetup( + this.log.error("Could not get lastReadLocation, defaulting to the 1st page", e) + } finally { + this.completeReaderSetup( params = params, isSavedInstanceStateNull = isSavedInstanceStateNull ) @@ -189,41 +166,6 @@ class PdfReaderActivity : AppCompatActivity() { } } - private fun showBookmarkPrompt( - localLastReadBookmark: Bookmark.PDFBookmark, - serverLastReadBookmark: Bookmark.PDFBookmark, - params: PdfReaderParameters, - isSavedInstanceStateNull: Boolean - ) { - MaterialAlertDialogBuilder(this) - .setTitle(R.string.viewer_position_title) - .setMessage(R.string.viewer_position_message) - .setNegativeButton(R.string.viewer_position_move) { dialog, _ -> - this.documentPageIndex = serverLastReadBookmark.pageNumber - dialog.dismiss() - createLocalBookmarkFromPromptAction( - bookmark = serverLastReadBookmark - ) - completeReaderSetup( - params = params, - isSavedInstanceStateNull = isSavedInstanceStateNull - ) - } - .setPositiveButton(R.string.viewer_position_stay) { dialog, _ -> - this.documentPageIndex = localLastReadBookmark.pageNumber - dialog.dismiss() - createLocalBookmarkFromPromptAction( - bookmark = localLastReadBookmark - ) - completeReaderSetup( - params = params, - isSavedInstanceStateNull = isSavedInstanceStateNull - ) - } - .create() - .show() - } - private fun createToolbar(title: String) { val toolbar = this.findViewById(R.id.pdf_toolbar) as PalaceToolbar toolbar.setLogoOnClickListener { @@ -242,7 +184,7 @@ class PdfReaderActivity : AppCompatActivity() { WebView.setWebContentsDebuggingEnabled(true) this.webView = WebView(this).apply { - settings.javaScriptEnabled = true + this.settings.javaScriptEnabled = true } this.webView.addJavascriptInterface( @@ -260,28 +202,29 @@ class PdfReaderActivity : AppCompatActivity() { "PDFListener" ) - this.pdfReaderContainer = findViewById(R.id.pdf_reader_container) + this.pdfReaderContainer = this.findViewById(R.id.pdf_reader_container) this.pdfReaderContainer.addView(this.webView) } private fun createPdfServer(drmInfo: BookDRMInformation, pdfFile: File) { val contentProtections = BookContentProtections.create( context = this.application, - contentProtectionProviders = ServiceLoader.load(ContentProtectionProvider::class.java).toList(), + contentProtectionProviders = ServiceLoader.load(ContentProtectionProvider::class.java) + .toList(), drmInfo = drmInfo, isManualPassphraseEnabled = - profilesController.profileCurrent().preferences().isManualLCPPassphraseEnabled, + this.profilesController.profileCurrent().preferences().isManualLCPPassphraseEnabled, onLCPDialogDismissed = { - finish() + this.finish() } ) // Create an immediately-closed socket to get a free port number. - val ephemeralSocket = ServerSocket(0).apply { close() } + val ephemeralSocket = ServerSocket(0).apply { this.close() } val createPdfOperation = GlobalScope.async { try { - pdfServer = PdfServer.create( + this@PdfReaderActivity.pdfServer = PdfServer.create( contentProtections = contentProtections, context = this@PdfReaderActivity.application, drmInfo = drmInfo, @@ -289,7 +232,7 @@ class PdfReaderActivity : AppCompatActivity() { port = ephemeralSocket.localPort ) } catch (exception: Exception) { - showErrorWithRunnable( + this@PdfReaderActivity.showErrorWithRunnable( context = this@PdfReaderActivity, title = exception.message ?: "", failure = exception, @@ -303,18 +246,18 @@ class PdfReaderActivity : AppCompatActivity() { GlobalScope.launch(Dispatchers.Main) { createPdfOperation.await() - pdfServer?.let { + this@PdfReaderActivity.pdfServer?.let { it.start() - webView.loadUrl( - "http://localhost:${it.port}/assets/pdf-viewer/viewer.html?file=%2Fbook.pdf#page=$documentPageIndex" + this@PdfReaderActivity.webView.loadUrl( + "http://localhost:${it.port}/assets/pdf-viewer/viewer.html?file=%2Fbook.pdf#page=${this@PdfReaderActivity.documentPageIndex}" ) } } } override fun onCreateOptionsMenu(menu: Menu?): Boolean { - menuInflater.inflate(R.menu.pdf_reader_menu, menu) + this.menuInflater.inflate(R.menu.pdf_reader_menu, menu) menu?.findItem(R.id.readerMenuTOC)?.setOnMenuItemClickListener { this.onReaderMenuTOCSelected() @@ -366,7 +309,7 @@ class PdfReaderActivity : AppCompatActivity() { override fun onOptionsItemSelected(item: MenuItem): Boolean { if (item.itemId == android.R.id.home) { - onBackPressed() + this.onBackPressed() } return super.onOptionsItemSelected(item) @@ -383,37 +326,28 @@ class PdfReaderActivity : AppCompatActivity() { } private fun onReaderPageChanged(pageIndex: Int) { - log.debug("onReaderPageChanged {}", pageIndex) + this.log.debug("onReaderPageChanged {}", pageIndex) this.documentPageIndex = pageIndex - val bookmark = Bookmark.PDFBookmark.create( - opdsId = this.feedEntry.feedEntry.id, - time = DateTime.now(), - kind = BookmarkKind.BookmarkLastReadLocation, - pageNumber = pageIndex, - deviceID = PdfReaderDevices.deviceId(this.profilesController, this.bookID), - uri = null - ) - - this.bookmarkService.bookmarkCreateLocal( - accountID = this.accountId, - bookmark = bookmark - ) - - this.bookmarkService.bookmarkCreateRemote( - accountID = this.accountId, - bookmark = bookmark - ) - } + val bookmark = + SerializedBookmarks.createWithCurrentFormat( + bookChapterProgress = 0.0, + bookChapterTitle = "", + bookProgress = 0.0, + bookTitle = this.feedEntry.feedEntry.title, + deviceID = PdfReaderDevices.deviceId(this.profilesController, this.bookID), + kind = BookmarkKind.BookmarkLastReadLocation, + location = SerializedLocatorPage1(pageIndex), + opdsId = this.feedEntry.feedEntry.id, + time = DateTime.now(), + uri = null + ) - private fun createLocalBookmarkFromPromptAction(bookmark: Bookmark.PDFBookmark) { - // we need to create a local bookmark after choosing an option from the prompt because the local - // bookmark is no longer created when syncing from the server returns a last read location - // bookmark - this.bookmarkService.bookmarkCreateLocal( + this.bookmarkService.bookmarkCreate( accountID = this.accountId, - bookmark = bookmark + bookmark = bookmark, + ignoreRemoteFailures = true ) } diff --git a/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfReaderBookmarks.kt b/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfReaderBookmarks.kt index 62fcce606..d12903d76 100644 --- a/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfReaderBookmarks.kt +++ b/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfReaderBookmarks.kt @@ -2,9 +2,18 @@ package org.librarysimplified.viewer.pdf.pdfjs import org.nypl.simplified.accounts.api.AccountID import org.nypl.simplified.bookmarks.api.BookmarkServiceUsableType -import org.nypl.simplified.bookmarks.api.Bookmarks +import org.nypl.simplified.bookmarks.api.BookmarksForBook import org.nypl.simplified.books.api.BookID -import org.nypl.simplified.books.api.bookmark.Bookmark +import org.nypl.simplified.books.api.bookmark.BookmarkKind.BookmarkExplicit +import org.nypl.simplified.books.api.bookmark.BookmarkKind.BookmarkLastReadLocation +import org.nypl.simplified.books.api.bookmark.SerializedBookmark +import org.nypl.simplified.books.api.bookmark.SerializedBookmarks +import org.nypl.simplified.books.api.bookmark.SerializedLocatorAudioBookTime1 +import org.nypl.simplified.books.api.bookmark.SerializedLocatorAudioBookTime2 +import org.nypl.simplified.books.api.bookmark.SerializedLocatorHrefProgression20210317 +import org.nypl.simplified.books.api.bookmark.SerializedLocatorLegacyCFI +import org.nypl.simplified.books.api.bookmark.SerializedLocatorPage1 +import org.nypl.simplified.feeds.api.FeedEntry import org.slf4j.LoggerFactory import java.util.concurrent.TimeUnit @@ -17,14 +26,14 @@ internal object PdfReaderBookmarks { bookmarkService: BookmarkServiceUsableType, accountID: AccountID, bookID: BookID - ): Bookmarks { + ): BookmarksForBook { return try { bookmarkService .bookmarkSyncAndLoad(accountID, bookID) .get(15L, TimeUnit.SECONDS) } catch (e: Exception) { - logger.error("could not load bookmarks: ", e) - Bookmarks(null, null, emptyList()) + this.logger.error("Could not load bookmarks: ", e) + BookmarksForBook(bookID, null, emptyList()) } } @@ -36,21 +45,77 @@ internal object PdfReaderBookmarks { bookmarkService: BookmarkServiceUsableType, accountID: AccountID, bookID: BookID - ): List { + ): List { val rawBookmarks = - loadRawBookmarks( + this.loadRawBookmarks( bookmarkService = bookmarkService, accountID = accountID, bookID = bookID ) - val lastReadLocal = rawBookmarks.lastReadLocal - val lastReadServer = rawBookmarks.lastReadServer - val explicits = rawBookmarks.bookmarks - val results = mutableListOf() + val lastReadLocal = + rawBookmarks.lastRead?.let { this.toPdfBookmark(it) } + val explicits = + rawBookmarks.bookmarks.mapNotNull { this.toPdfBookmark(it) } + + val results = mutableListOf() lastReadLocal?.let(results::add) - lastReadServer?.let(results::add) results.addAll(explicits) return results.toList() } + + /** + * Convert a SimplyE bookmark to an SR2 bookmark. + */ + + fun toPdfBookmark( + source: SerializedBookmark + ): PdfBookmark? { + return when (val location = source.location) { + is SerializedLocatorAudioBookTime1, + is SerializedLocatorAudioBookTime2, + is SerializedLocatorLegacyCFI, + is SerializedLocatorHrefProgression20210317 -> { + // None of these locator formats are suitable for PDFs + null + } + + is SerializedLocatorPage1 -> { + PdfBookmark( + kind = when (source.kind) { + BookmarkExplicit -> PdfBookmarkKind.EXPLICIT + BookmarkLastReadLocation -> PdfBookmarkKind.LAST_READ + }, + pageNumber = location.page, + time = source.time + ) + } + } + } + + /** + * Convert a PDF bookmark to a SimplyE bookmark. + */ + + fun fromPdfBookmark( + bookEntry: FeedEntry.FeedEntryOPDS, + deviceId: String, + source: PdfBookmark + ): SerializedBookmark { + return SerializedBookmarks.createWithCurrentFormat( + bookChapterProgress = 0.0, + bookChapterTitle = "", + bookProgress = 0.0, + bookTitle = bookEntry.feedEntry.title, + deviceID = deviceId, + kind = when (source.kind) { + PdfBookmarkKind.EXPLICIT -> BookmarkExplicit + PdfBookmarkKind.LAST_READ -> BookmarkLastReadLocation + }, + location = SerializedLocatorPage1(source.pageNumber), + opdsId = bookEntry.feedEntry.id, + time = source.time, + uri = null, + ) + } } diff --git a/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/factory/PdfReaderDocument.kt b/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfReaderDocument.kt similarity index 97% rename from simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/factory/PdfReaderDocument.kt rename to simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfReaderDocument.kt index a27ce07f6..14365b4dc 100644 --- a/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/factory/PdfReaderDocument.kt +++ b/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfReaderDocument.kt @@ -1,4 +1,4 @@ -package org.librarysimplified.viewer.pdf.pdfjs.factory +package org.librarysimplified.viewer.pdf.pdfjs import android.content.Context import android.graphics.Bitmap diff --git a/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfServer.kt b/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfServer.kt index 3e5336264..69ddb7937 100644 --- a/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfServer.kt +++ b/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfServer.kt @@ -4,7 +4,6 @@ import android.app.Application import android.content.Context import android.net.Uri import kotlinx.coroutines.runBlocking -import org.librarysimplified.viewer.pdf.pdfjs.factory.PdfDocumentFactory import org.nanohttpd.protocols.http.IHTTPSession import org.nanohttpd.protocols.http.response.Response import org.nanohttpd.protocols.http.response.Status diff --git a/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfViewerProvider.kt b/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfViewerProvider.kt index 081281f17..1c75db650 100644 --- a/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfViewerProvider.kt +++ b/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfViewerProvider.kt @@ -1,6 +1,6 @@ package org.librarysimplified.viewer.pdf.pdfjs -import android.app.Activity +import android.app.Application import one.irradia.mime.api.MIMEType import org.nypl.simplified.books.api.Book import org.nypl.simplified.books.api.BookFormat @@ -41,7 +41,7 @@ class PdfViewerProvider : ViewerProviderType { } override fun open( - activity: Activity, + context: Application, preferences: ViewerPreferences, book: Book, format: BookFormat, @@ -52,7 +52,7 @@ class PdfViewerProvider : ViewerProviderType { FeedEntry.FeedEntryOPDS(book.account, book.entry) PdfReaderActivity.startActivity( - from = activity, + context = context, parameters = PdfReaderParameters( accountId = book.account, documentTitle = book.entry.title, diff --git a/simplified-viewer-spi/src/main/java/org/nypl/simplified/viewer/spi/ViewerProviderType.kt b/simplified-viewer-spi/src/main/java/org/nypl/simplified/viewer/spi/ViewerProviderType.kt index 9c0a0f67f..777f78fb4 100644 --- a/simplified-viewer-spi/src/main/java/org/nypl/simplified/viewer/spi/ViewerProviderType.kt +++ b/simplified-viewer-spi/src/main/java/org/nypl/simplified/viewer/spi/ViewerProviderType.kt @@ -1,6 +1,6 @@ package org.nypl.simplified.viewer.spi -import android.app.Activity +import android.app.Application import one.irradia.mime.api.MIMEType import org.nypl.simplified.books.api.Book import org.nypl.simplified.books.api.BookFormat @@ -57,7 +57,7 @@ interface ViewerProviderType { */ fun open( - activity: Activity, + context: Application, preferences: ViewerPreferences, book: Book, format: BookFormat, diff --git a/simplified-webview/src/main/java/org/nypl/simplified/webview/WebViewUtilities.kt b/simplified-webview/src/main/java/org/nypl/simplified/webview/WebViewUtilities.kt index 7c72804e4..ea70085f1 100644 --- a/simplified-webview/src/main/java/org/nypl/simplified/webview/WebViewUtilities.kt +++ b/simplified-webview/src/main/java/org/nypl/simplified/webview/WebViewUtilities.kt @@ -4,8 +4,8 @@ import android.content.res.Configuration import android.webkit.CookieManager import android.webkit.WebSettings import androidx.webkit.WebSettingsCompat -import androidx.webkit.WebSettingsCompat.FORCE_DARK_ON import androidx.webkit.WebSettingsCompat.FORCE_DARK_OFF +import androidx.webkit.WebSettingsCompat.FORCE_DARK_ON import androidx.webkit.WebViewFeature import org.nypl.simplified.accounts.api.AccountCookie import org.nypl.simplified.android.ktx.isNightModeYes From 93dfcb6c590d64e6fe110f63212c2eab330a711b Mon Sep 17 00:00:00 2001 From: Mark Raynsford Date: Wed, 1 May 2024 13:47:10 +0000 Subject: [PATCH 02/11] Continued refactoring for new audiobook player Affects: https://ebce-lyrasis.atlassian.net/browse/PP-1074 Affects: https://ebce-lyrasis.atlassian.net/browse/PP-1075 Affects: https://ebce-lyrasis.atlassian.net/browse/PP-1076 Affects: https://ebce-lyrasis.atlassian.net/browse/PP-1082 --- .../src/main/AndroidManifest.xml | 2 +- .../books/audio/AudioBookManifestData.kt | 2 +- .../AudioBookManifestFulfillmentAdapter.kt | 47 ++++ .../AudioBookManifestFulfillmentError.kt | 15 + .../main/MainFragmentListenerDelegate.kt | 2 +- .../org/nypl/simplified/viewer/api/Viewers.kt | 4 +- simplified-viewer-audiobook/build.gradle.kts | 9 + .../viewer/audiobook/AudioBookBookmarks.kt | 147 ++++++++++ .../viewer/audiobook/AudioBookDevices.kt | 23 -- .../audiobook/AudioBookLoadingFragment2.kt | 58 +++- .../audiobook/AudioBookPlayerActivity2.kt | 263 ++++++++++++++++++ .../viewer/audiobook/AudioBookViewer.kt | 151 ++++++++-- .../viewer/audiobook/AudioBookViewerModel.kt | 7 + .../res/layout/audio_book_player_loading.xml | 48 +++- .../viewer/epub/readium2/Reader2Activity.kt | 8 +- .../viewer/epub/readium2/ReaderViewerR2.kt | 6 +- .../viewer/pdf/pdfjs/PdfReaderActivity.kt | 4 +- .../viewer/pdf/pdfjs/PdfViewerProvider.kt | 9 +- .../viewer/spi/ViewerProviderType.kt | 4 +- 19 files changed, 732 insertions(+), 77 deletions(-) create mode 100644 simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/AudioBookManifestFulfillmentAdapter.kt create mode 100644 simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/AudioBookManifestFulfillmentError.kt create mode 100644 simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookBookmarks.kt delete mode 100644 simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookDevices.kt create mode 100644 simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookViewerModel.kt diff --git a/simplified-app-palace/src/main/AndroidManifest.xml b/simplified-app-palace/src/main/AndroidManifest.xml index 115e13059..3b4835fd1 100644 --- a/simplified-app-palace/src/main/AndroidManifest.xml +++ b/simplified-app-palace/src/main/AndroidManifest.xml @@ -88,7 +88,7 @@ android:label="@string/app_name" /> = + TaskResult.fail( + "Not yet executed.", + "Not yet executed.", + "error-not-executed" + ) + + val result: TaskResult + get() = this.resultField + + override val events: Observable + get() = this.strategy.events.map(::ManifestFulfillmentEvent) + + override fun close() { + // Nothing to close + } + + override fun execute(): PlayerResult { + this.resultField = this.strategy.execute() + + return when (val result = this.strategy.execute()) { + is TaskResult.Failure -> { + PlayerResult.Failure( + AudioBookManifestFulfillmentError(result) + ) + } + + is TaskResult.Success -> { + PlayerResult.Success(result.result.fulfilled) + } + } + } +} diff --git a/simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/AudioBookManifestFulfillmentError.kt b/simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/AudioBookManifestFulfillmentError.kt new file mode 100644 index 000000000..f7a17a71e --- /dev/null +++ b/simplified-books-audio/src/main/java/org/nypl/simplified/books/audio/AudioBookManifestFulfillmentError.kt @@ -0,0 +1,15 @@ +package org.nypl.simplified.books.audio + +import org.librarysimplified.audiobook.manifest_fulfill.spi.ManifestFulfillmentErrorType +import org.nypl.simplified.taskrecorder.api.TaskResult + +class AudioBookManifestFulfillmentError( + val taskFailure: TaskResult.Failure +) : ManifestFulfillmentErrorType { + + override val message: String + get() = taskFailure.message + + override val serverData: ManifestFulfillmentErrorType.ServerData? + get() = null +} diff --git a/simplified-main/src/main/java/org/librarysimplified/main/MainFragmentListenerDelegate.kt b/simplified-main/src/main/java/org/librarysimplified/main/MainFragmentListenerDelegate.kt index 64ad1c241..a8c1b88c0 100644 --- a/simplified-main/src/main/java/org/librarysimplified/main/MainFragmentListenerDelegate.kt +++ b/simplified-main/src/main/java/org/librarysimplified/main/MainFragmentListenerDelegate.kt @@ -692,7 +692,7 @@ internal class MainFragmentListenerDelegate( ) Viewers.openViewer( - context = MainApplication.application, + context = this.fragment.requireActivity(), preferences = viewerPreferences, book = book, format = format diff --git a/simplified-viewer-api/src/main/java/org/nypl/simplified/viewer/api/Viewers.kt b/simplified-viewer-api/src/main/java/org/nypl/simplified/viewer/api/Viewers.kt index db534267d..6a974d967 100644 --- a/simplified-viewer-api/src/main/java/org/nypl/simplified/viewer/api/Viewers.kt +++ b/simplified-viewer-api/src/main/java/org/nypl/simplified/viewer/api/Viewers.kt @@ -1,6 +1,6 @@ package org.nypl.simplified.viewer.api -import android.app.Application +import android.app.Activity import org.joda.time.LocalDateTime import org.librarysimplified.mdc.MDCKeys import org.librarysimplified.services.api.Services @@ -41,7 +41,7 @@ object Viewers { */ fun openViewer( - context: Application, + context: Activity, preferences: ViewerPreferences, book: Book, format: BookFormat diff --git a/simplified-viewer-audiobook/build.gradle.kts b/simplified-viewer-audiobook/build.gradle.kts index afaec6b68..ecfe0e60c 100644 --- a/simplified-viewer-audiobook/build.gradle.kts +++ b/simplified-viewer-audiobook/build.gradle.kts @@ -30,10 +30,17 @@ dependencies { implementation(libs.androidx.activity.ktx) implementation(libs.androidx.annotation) implementation(libs.androidx.appcompat) + implementation(libs.androidx.appcompat.resources) implementation(libs.androidx.cardview) + implementation(libs.androidx.collection) implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.constraintlayout.core) + implementation(libs.androidx.constraintlayout.solver) implementation(libs.androidx.coordinatorlayout) implementation(libs.androidx.core) + implementation(libs.androidx.emoji2) + implementation(libs.androidx.emoji2.views) + implementation(libs.androidx.emoji2.views.helper) implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.splashscreen) implementation(libs.androidx.customview) @@ -64,11 +71,13 @@ dependencies { implementation(libs.palace.audiobook.downloads) implementation(libs.palace.audiobook.feedbooks) implementation(libs.palace.audiobook.license.check.api) + implementation(libs.palace.audiobook.license.check.spi) implementation(libs.palace.audiobook.manifest.api) implementation(libs.palace.audiobook.manifest.fulfill.api) implementation(libs.palace.audiobook.manifest.fulfill.basic) implementation(libs.palace.audiobook.manifest.fulfill.spi) implementation(libs.palace.audiobook.manifest.parser.api) + implementation(libs.palace.audiobook.manifest.parser.extension.spi) implementation(libs.palace.audiobook.manifest.parser.webpub) implementation(libs.palace.audiobook.media3) implementation(libs.palace.audiobook.views) diff --git a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookBookmarks.kt b/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookBookmarks.kt new file mode 100644 index 000000000..185dcae08 --- /dev/null +++ b/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookBookmarks.kt @@ -0,0 +1,147 @@ +package org.librarysimplified.viewer.audiobook + +import org.joda.time.Duration +import org.librarysimplified.audiobook.api.PlayerBookmark +import org.librarysimplified.audiobook.api.PlayerBookmarkKind +import org.librarysimplified.audiobook.api.PlayerBookmarkMetadata +import org.librarysimplified.audiobook.manifest.api.PlayerManifestReadingOrderID +import org.nypl.simplified.accounts.api.AccountID +import org.nypl.simplified.bookmarks.api.BookmarkServiceUsableType +import org.nypl.simplified.bookmarks.api.BookmarksForBook +import org.nypl.simplified.books.api.BookID +import org.nypl.simplified.books.api.bookmark.BookmarkKind +import org.nypl.simplified.books.api.bookmark.SerializedBookmark +import org.nypl.simplified.books.api.bookmark.SerializedBookmarks +import org.nypl.simplified.books.api.bookmark.SerializedLocatorAudioBookTime1 +import org.nypl.simplified.books.api.bookmark.SerializedLocatorAudioBookTime2 +import org.nypl.simplified.books.api.bookmark.SerializedLocatorHrefProgression20210317 +import org.nypl.simplified.books.api.bookmark.SerializedLocatorLegacyCFI +import org.nypl.simplified.books.api.bookmark.SerializedLocatorPage1 +import org.nypl.simplified.opds.core.OPDSAcquisitionFeedEntry +import org.slf4j.LoggerFactory +import java.util.concurrent.TimeUnit + +/** + * Functions to convert between SimplyE and SR2 bookmarks. + */ + +object AudioBookBookmarks { + + private val logger = + LoggerFactory.getLogger(AudioBookBookmarks::class.java) + + private fun loadRawBookmarks( + bookmarkService: BookmarkServiceUsableType, + accountID: AccountID, + bookID: BookID + ): BookmarksForBook { + return try { + bookmarkService + .bookmarkSyncAndLoad(accountID, bookID) + .get(15L, TimeUnit.SECONDS) + } catch (e: Exception) { + this.logger.error("could not load bookmarks: ", e) + BookmarksForBook(bookID, null, emptyList()) + } + } + + /** + * Load bookmarks from the given bookmark service. + */ + + fun loadBookmarks( + bookmarkService: BookmarkServiceUsableType, + accountID: AccountID, + bookID: BookID + ): List { + val rawBookmarks = + this.loadRawBookmarks( + bookmarkService = bookmarkService, + accountID = accountID, + bookID = bookID + ) + + val lastReadLocal = + rawBookmarks.lastRead?.let { this.toPlayerBookmark(it) } + val explicits = + rawBookmarks.bookmarks.mapNotNull { this.toPlayerBookmark(it) } + + val results = mutableListOf() + lastReadLocal?.let(results::add) + results.addAll(explicits) + return results.toList() + } + + /** + * Convert a Player bookmark to a SimplyE bookmark. + */ + + fun fromPlayerBookmark( + feedEntry: OPDSAcquisitionFeedEntry, + deviceId: String, + source: PlayerBookmark + ): SerializedBookmark { + val kind = + when (source.kind) { + PlayerBookmarkKind.EXPLICIT -> BookmarkKind.BookmarkExplicit + PlayerBookmarkKind.LAST_READ -> BookmarkKind.BookmarkLastReadLocation + } + + return SerializedBookmarks.createWithCurrentFormat( + bookTitle = feedEntry.title, + deviceID = deviceId, + kind = kind, + opdsId = feedEntry.id, + time = source.metadata.creationTime, + bookChapterProgress = source.metadata.chapterProgressEstimate, + bookChapterTitle = source.metadata.chapterTitle, + bookProgress = source.metadata.bookProgressEstimate, + location = SerializedLocatorAudioBookTime2( + chapterHref = source.readingOrderID.text, + chapterOffsetMilliseconds = source.offsetMilliseconds + ), + uri = null + ) + } + + /** + * Convert a SimplyE bookmark to a Player bookmark. + */ + + fun toPlayerBookmark( + source: SerializedBookmark + ): PlayerBookmark? { + return when (val location = source.location) { + is SerializedLocatorAudioBookTime1 -> { + null + } + + is SerializedLocatorAudioBookTime2 -> { + val kind: PlayerBookmarkKind = + when (source.kind) { + BookmarkKind.BookmarkExplicit -> PlayerBookmarkKind.EXPLICIT + BookmarkKind.BookmarkLastReadLocation -> PlayerBookmarkKind.LAST_READ + } + PlayerBookmark( + kind = kind, + readingOrderID = PlayerManifestReadingOrderID(location.chapterHref), + offsetMilliseconds = location.chapterOffsetMilliseconds, + metadata = PlayerBookmarkMetadata( + creationTime = source.time, + chapterTitle = source.bookChapterTitle, + totalRemainingBookTime = Duration.ZERO, + chapterProgressEstimate = source.bookChapterProgress, + bookProgressEstimate = source.bookProgress + ) + ) + } + + is SerializedLocatorLegacyCFI, + is SerializedLocatorPage1, + is SerializedLocatorHrefProgression20210317 -> { + // None of these locator formats are suitable for audio books. + null + } + } + } +} diff --git a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookDevices.kt b/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookDevices.kt deleted file mode 100644 index b71544b39..000000000 --- a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookDevices.kt +++ /dev/null @@ -1,23 +0,0 @@ -package org.librarysimplified.viewer.audiobook - -import org.nypl.simplified.books.api.BookID -import org.nypl.simplified.profiles.controller.api.ProfilesControllerType - -object AudioBookDevices { - - /** - * Return the device ID for the account that owns `bookID`. - */ - - fun deviceId( - profilesController: ProfilesControllerType, - bookID: BookID - ): String { - val account = profilesController.profileAccountForBook(bookID) - val state = account.loginState - val credentials = state.credentials - - // Yes, really return a string that says "null" - return credentials?.adobeCredentials?.postActivationCredentials?.deviceID?.value ?: "null" - } -} diff --git a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookLoadingFragment2.kt b/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookLoadingFragment2.kt index 69efa59d3..797ae3fb9 100644 --- a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookLoadingFragment2.kt +++ b/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookLoadingFragment2.kt @@ -1,5 +1,61 @@ package org.librarysimplified.viewer.audiobook +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ProgressBar +import android.widget.TextView import androidx.fragment.app.Fragment +import io.reactivex.disposables.CompositeDisposable +import org.librarysimplified.audiobook.license_check.spi.SingleLicenseCheckStatus +import org.librarysimplified.audiobook.manifest_fulfill.spi.ManifestFulfillmentEvent +import org.librarysimplified.audiobook.views.PlayerModel -class AudioBookLoadingFragment2 : Fragment(R.layout.audio_book_player_loading) +class AudioBookLoadingFragment2 : Fragment(R.layout.audio_book_player_loading) { + + private lateinit var progressText: TextView + private lateinit var progressBar: ProgressBar + private var subscriptions: CompositeDisposable = CompositeDisposable() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val layout = + inflater.inflate(R.layout.audio_book_player_loading, container, false) + + this.progressText = + layout.findViewById(R.id.progressText) + this.progressBar = + layout.findViewById(R.id.progressBar) + + this.progressBar.isIndeterminate = true + return layout + } + + override fun onStart() { + super.onStart() + + this.subscriptions.add(PlayerModel.manifestDownloadEvents.subscribe(this::onManifestEvent)) + this.subscriptions.add(PlayerModel.singleLicenseCheckEvents.subscribe(this::onLicenseEvent)) + } + + override fun onStop() { + super.onStop() + this.subscriptions.dispose() + } + + private fun onLicenseEvent( + event: SingleLicenseCheckStatus + ) { + this.progressText.text = event.message + } + + private fun onManifestEvent( + event: ManifestFulfillmentEvent + ) { + this.progressText.text = event.message + } +} diff --git a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookPlayerActivity2.kt b/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookPlayerActivity2.kt index 5ff546007..1847ffec3 100644 --- a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookPlayerActivity2.kt +++ b/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookPlayerActivity2.kt @@ -1,20 +1,58 @@ package org.librarysimplified.viewer.audiobook +import android.os.Bundle import androidx.annotation.UiThread import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.TxContextWrappingDelegate2 +import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment import io.reactivex.disposables.CompositeDisposable +import org.librarysimplified.audiobook.api.PlayerBookmark import org.librarysimplified.audiobook.api.PlayerEvent +import org.librarysimplified.audiobook.api.PlayerEvent.PlayerAccessibilityEvent.PlayerAccessibilityChapterSelected +import org.librarysimplified.audiobook.api.PlayerEvent.PlayerAccessibilityEvent.PlayerAccessibilityErrorOccurred +import org.librarysimplified.audiobook.api.PlayerEvent.PlayerAccessibilityEvent.PlayerAccessibilityIsBuffering +import org.librarysimplified.audiobook.api.PlayerEvent.PlayerAccessibilityEvent.PlayerAccessibilityIsWaitingForChapter +import org.librarysimplified.audiobook.api.PlayerEvent.PlayerAccessibilityEvent.PlayerAccessibilityPlaybackRateChanged +import org.librarysimplified.audiobook.api.PlayerEvent.PlayerAccessibilityEvent.PlayerAccessibilitySleepTimerSettingChanged +import org.librarysimplified.audiobook.api.PlayerEvent.PlayerEventDeleteBookmark +import org.librarysimplified.audiobook.api.PlayerEvent.PlayerEventError +import org.librarysimplified.audiobook.api.PlayerEvent.PlayerEventManifestUpdated +import org.librarysimplified.audiobook.api.PlayerEvent.PlayerEventPlaybackRateChanged +import org.librarysimplified.audiobook.api.PlayerEvent.PlayerEventWithPosition.PlayerEventChapterCompleted +import org.librarysimplified.audiobook.api.PlayerEvent.PlayerEventWithPosition.PlayerEventChapterWaiting +import org.librarysimplified.audiobook.api.PlayerEvent.PlayerEventWithPosition.PlayerEventCreateBookmark +import org.librarysimplified.audiobook.api.PlayerEvent.PlayerEventWithPosition.PlayerEventPlaybackBuffering +import org.librarysimplified.audiobook.api.PlayerEvent.PlayerEventWithPosition.PlayerEventPlaybackPaused +import org.librarysimplified.audiobook.api.PlayerEvent.PlayerEventWithPosition.PlayerEventPlaybackPreparing +import org.librarysimplified.audiobook.api.PlayerEvent.PlayerEventWithPosition.PlayerEventPlaybackProgressUpdate +import org.librarysimplified.audiobook.api.PlayerEvent.PlayerEventWithPosition.PlayerEventPlaybackStarted +import org.librarysimplified.audiobook.api.PlayerEvent.PlayerEventWithPosition.PlayerEventPlaybackStopped +import org.librarysimplified.audiobook.api.PlayerEvent.PlayerEventWithPosition.PlayerEventPlaybackWaitingForAction import org.librarysimplified.audiobook.api.PlayerUIThread +import org.librarysimplified.audiobook.api.PlayerUserAgent import org.librarysimplified.audiobook.api.extensions.PlayerExtensionType +import org.librarysimplified.audiobook.views.PlayerBaseFragment +import org.librarysimplified.audiobook.views.PlayerBookmarkModel +import org.librarysimplified.audiobook.views.PlayerFragment import org.librarysimplified.audiobook.views.PlayerModel import org.librarysimplified.audiobook.views.PlayerModelState +import org.librarysimplified.audiobook.views.PlayerPlaybackRateFragment +import org.librarysimplified.audiobook.views.PlayerSleepTimerFragment +import org.librarysimplified.audiobook.views.PlayerTOCFragment import org.librarysimplified.audiobook.views.PlayerViewCommand +import org.librarysimplified.services.api.Services +import org.nypl.simplified.bookmarks.api.BookmarkServiceType +import org.slf4j.LoggerFactory class AudioBookPlayerActivity2 : AppCompatActivity(R.layout.audio_book_player_base) { + private val logger = + LoggerFactory.getLogger(AudioBookPlayerActivity2::class.java) + + private lateinit var bookmarkService: BookmarkServiceType + private val playerExtensions: List = listOf() private var fragmentNow: Fragment = AudioBookLoadingFragment2() private var subscriptions: CompositeDisposable = CompositeDisposable() @@ -27,6 +65,18 @@ class AudioBookPlayerActivity2 : AppCompatActivity(R.layout.audio_book_player_ba return this.appCompatDelegate } + override fun onCreate( + savedInstanceState: Bundle? + ) { + super.onCreate(savedInstanceState) + + val services = + Services.serviceDirectory() + + this.bookmarkService = + services.requireService(BookmarkServiceType::class.java) + } + override fun onStart() { super.onStart() @@ -41,11 +91,111 @@ class AudioBookPlayerActivity2 : AppCompatActivity(R.layout.audio_book_player_ba this.subscriptions.dispose() } + @Deprecated("Deprecated in Java") + override fun onBackPressed() { + return when (val f = this.fragmentNow) { + is PlayerBaseFragment -> { + when (f) { + is PlayerFragment -> { + PlayerModel.closeBookOrDismissError() + Unit + } + + is PlayerTOCFragment -> { + this.switchFragment(PlayerFragment()) + } + } + } + + is AudioBookLoadingFragment2 -> { + PlayerModel.closeBookOrDismissError() + super.onBackPressed() + } + + null -> { + PlayerModel.closeBookOrDismissError() + super.onBackPressed() + } + + else -> { + throw IllegalStateException("Unrecognized fragment: $f") + } + } + } + @UiThread private fun onPlayerEvent( event: PlayerEvent ) { PlayerUIThread.checkIsUIThread() + + when (event) { + is PlayerAccessibilityChapterSelected, + is PlayerAccessibilityErrorOccurred, + is PlayerAccessibilityIsBuffering, + is PlayerAccessibilityIsWaitingForChapter, + is PlayerAccessibilityPlaybackRateChanged, + is PlayerAccessibilitySleepTimerSettingChanged, + is PlayerEventPlaybackRateChanged, + is PlayerEventChapterCompleted, + is PlayerEventChapterWaiting, + is PlayerEventPlaybackBuffering, + is PlayerEventPlaybackPaused, + is PlayerEventPlaybackPreparing, + is PlayerEventPlaybackProgressUpdate, + is PlayerEventPlaybackStarted, + is PlayerEventPlaybackStopped, + is PlayerEventPlaybackWaitingForAction -> { + // Nothing to do + } + + is PlayerEventCreateBookmark -> { + val parameters = + AudioBookViewerModel.parameters ?: return + + val playerBookmark = + PlayerBookmark( + kind = event.kind, + readingOrderID = event.readingOrderItem.id, + offsetMilliseconds = event.offsetMilliseconds, + metadata = event.bookmarkMetadata + ) + + this.bookmarkService.bookmarkCreate( + accountID = parameters.accountID, + bookmark = AudioBookBookmarks.fromPlayerBookmark( + feedEntry = parameters.opdsEntry, + deviceId = "null", + source = playerBookmark + ), + ignoreRemoteFailures = true, + ) + } + + is PlayerEventDeleteBookmark -> { + val parameters = + AudioBookViewerModel.parameters ?: return + + val playerBookmark = event.bookmark + this.bookmarkService.bookmarkDelete( + accountID = parameters.accountID, + bookmark = AudioBookBookmarks.fromPlayerBookmark( + feedEntry = parameters.opdsEntry, + deviceId = "null", + source = playerBookmark + ), + ignoreRemoteFailures = true, + ) + } + + is PlayerEventError -> { + // Nothing yet... + } + + PlayerEventManifestUpdated -> { + // Nothing yet... + } + } } @UiThread @@ -53,6 +203,93 @@ class AudioBookPlayerActivity2 : AppCompatActivity(R.layout.audio_book_player_ba state: PlayerModelState ) { PlayerUIThread.checkIsUIThread() + + val bookParameters = + AudioBookViewerModel.parameters ?: return + + when (state) { + is PlayerModelState.PlayerBookOpenFailed -> { + this.onBookOpenFailed(state) + } + + PlayerModelState.PlayerClosed -> { + this.switchFragment(AudioBookLoadingFragment2()) + } + + is PlayerModelState.PlayerManifestDownloadFailed -> { + this.onManifestDownloadFailed(state) + } + + is PlayerModelState.PlayerManifestLicenseChecksFailed -> { + this.onManifestLicenseChecksFailed(state) + } + + is PlayerModelState.PlayerManifestOK -> { + PlayerModel.openPlayerForManifest( + context = this.application, + userAgent = PlayerUserAgent(bookParameters.userAgent), + extensions = this.playerExtensions, + manifest = state.manifest + ) + } + + is PlayerModelState.PlayerManifestParseFailed -> { + this.onManifestParseFailed(state) + } + + is PlayerModelState.PlayerOpen -> { + /* + * XXX: This shouldn't really be a blocking call to get() + * The bookmarks service should expose an always-up-to-date readable set of bookmarks. + */ + + val bookmarks = + this.bookmarkService.bookmarkLoad( + accountID = bookParameters.accountID, + book = bookParameters.bookID + ).get() + + val bookmarksConverted = + bookmarks.bookmarks.mapNotNull(AudioBookBookmarks::toPlayerBookmark) + val bookmarkLastRead = + bookmarks.lastRead?.let { b -> AudioBookBookmarks.toPlayerBookmark(b) } + + PlayerBookmarkModel.setBookmarks(bookmarksConverted) + if (bookmarkLastRead != null) { + PlayerModel.movePlayheadTo(bookmarkLastRead.position) + } + + this.switchFragment(PlayerFragment()) + } + + PlayerModelState.PlayerManifestInProgress -> { + this.switchFragment(AudioBookLoadingFragment2()) + } + } + } + + private fun onManifestParseFailed( + state: PlayerModelState.PlayerManifestParseFailed + ) { + this.logger.error("onManifestParseFailed: {}", state) + } + + private fun onBookOpenFailed( + state: PlayerModelState.PlayerBookOpenFailed + ) { + this.logger.error("onBookOpenFailed: {}", state) + } + + private fun onManifestDownloadFailed( + state: PlayerModelState.PlayerManifestDownloadFailed + ) { + this.logger.error("onManifestDownloadFailed: {}", state) + } + + private fun onManifestLicenseChecksFailed( + state: PlayerModelState.PlayerManifestLicenseChecksFailed + ) { + this.logger.error("onManifestLicenseChecksFailed: {}", state) } @UiThread @@ -60,6 +297,28 @@ class AudioBookPlayerActivity2 : AppCompatActivity(R.layout.audio_book_player_ba command: PlayerViewCommand ) { PlayerUIThread.checkIsUIThread() + + when (command) { + PlayerViewCommand.PlayerViewCoverImageChanged -> { + // Nothing to do + } + + PlayerViewCommand.PlayerViewNavigationPlaybackRateMenuOpen -> { + this.popupFragment(PlayerPlaybackRateFragment()) + } + + PlayerViewCommand.PlayerViewNavigationSleepMenuOpen -> { + this.popupFragment(PlayerSleepTimerFragment()) + } + + PlayerViewCommand.PlayerViewNavigationTOCClose -> { + this.switchFragment(PlayerFragment()) + } + + PlayerViewCommand.PlayerViewNavigationTOCOpen -> { + this.switchFragment(PlayerTOCFragment()) + } + } } private fun switchFragment( @@ -70,4 +329,8 @@ class AudioBookPlayerActivity2 : AppCompatActivity(R.layout.audio_book_player_ba .replace(R.id.audio_book_player_fragment_holder, fragment) .commit() } + + private fun popupFragment(fragment: DialogFragment) { + fragment.show(this.supportFragmentManager, fragment.tag) + } } diff --git a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookViewer.kt b/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookViewer.kt index f3b48eeae..f41987936 100644 --- a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookViewer.kt +++ b/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookViewer.kt @@ -1,17 +1,32 @@ package org.librarysimplified.viewer.audiobook -import android.app.Application +import android.app.Activity import android.content.Intent import one.irradia.mime.api.MIMEType +import org.librarysimplified.audiobook.api.PlayerUserAgent +import org.librarysimplified.audiobook.api.extensions.PlayerExtensionType +import org.librarysimplified.audiobook.feedbooks.FeedbooksPlayerExtension +import org.librarysimplified.audiobook.license_check.spi.SingleLicenseCheckProviderType +import org.librarysimplified.audiobook.manifest_parser.extension_spi.ManifestParserExtensionType +import org.librarysimplified.audiobook.media3.BearerTokenExtension +import org.librarysimplified.audiobook.views.PlayerModel +import org.librarysimplified.http.api.LSHTTPAuthorizationType import org.librarysimplified.http.api.LSHTTPClientType +import org.librarysimplified.services.api.ServiceDirectoryType import org.librarysimplified.services.api.Services import org.nypl.simplified.books.api.Book import org.nypl.simplified.books.api.BookFormat +import org.nypl.simplified.books.audio.AudioBookFeedbooksSecretServiceType +import org.nypl.simplified.books.audio.AudioBookManifestFulfillmentAdapter +import org.nypl.simplified.books.audio.AudioBookManifestStrategiesType import org.nypl.simplified.books.formats.api.StandardFormatNames +import org.nypl.simplified.networkconnectivity.api.NetworkConnectivityType +import org.nypl.simplified.profiles.controller.api.ProfilesControllerType import org.nypl.simplified.viewer.spi.ViewerPreferences import org.nypl.simplified.viewer.spi.ViewerProviderType import org.slf4j.LoggerFactory import java.net.URI +import java.util.ServiceLoader /** * An audio book viewer service. @@ -36,6 +51,7 @@ class AudioBookViewer : ViewerProviderType { this.logger.debug("audio book viewer can only view audio books") false } + is BookFormat.BookFormatAudioBook -> true } @@ -45,40 +61,135 @@ class AudioBookViewer : ViewerProviderType { return StandardFormatNames.allAudioBooks.contains(type) } + private fun loadAndConfigureExtensions( + authorization: LSHTTPAuthorizationType? + ): List { + val extensions = + ServiceLoader.load(PlayerExtensionType::class.java) + .toList() + + val services = Services.serviceDirectory() + this.loadAndConfigureBearerToken(extensions, authorization) + this.loadAndConfigureFeedbooks(services, extensions) + return extensions + } + + private fun loadAndConfigureFeedbooks( + services: ServiceDirectoryType, + extensions: List + ) { + val feedbooksConfigService = + services.optionalService(AudioBookFeedbooksSecretServiceType::class.java) + + if (feedbooksConfigService != null) { + this.logger.debug("Feedbooks configuration service is available; configuring extension") + val extension = + extensions.filterIsInstance() + .firstOrNull() + if (extension != null) { + this.logger.debug("Feedbooks extension is available") + extension.configuration = feedbooksConfigService.configuration + } else { + this.logger.debug("Feedbooks extension is not available") + } + } + } + + private fun loadAndConfigureBearerToken( + extensions: List, + authorization: LSHTTPAuthorizationType? + ) { + this.logger.debug( + "Configuring bearer token extension with authorization: {}", + authorization?.toHeaderValue() + ) + val extension = + extensions.filterIsInstance() + .firstOrNull() + if (extension != null) { + this.logger.debug("Bearer token extension is available") + extension.authorization = authorization + } else { + this.logger.debug("Bearer token extension is not available") + } + } + override fun open( - context: Application, + activity: Activity, preferences: ViewerPreferences, book: Book, format: BookFormat, accountProviderId: URI ) { + val services = + Services.serviceDirectory() + val httpClient = + services.requireService(LSHTTPClientType::class.java) + val networkConnectivity = + services.requireService(NetworkConnectivityType::class.java) + val strategies = + services.requireService(AudioBookManifestStrategiesType::class.java) + val profiles = + services.requireService(ProfilesControllerType::class.java) + val parserExtensions = + ServiceLoader.load(ManifestParserExtensionType::class.java).toList() + val licenseChecks = + ServiceLoader.load(SingleLicenseCheckProviderType::class.java).toList() + val formatAudio = format as BookFormat.BookFormatAudioBook val file = formatAudio.file val manifest = formatAudio.manifest - val httpClient = - Services.serviceDirectory() - .requireService(LSHTTPClientType::class.java) - - val params = if (manifest != null) { - AudioBookPlayerParameters( - accountID = book.account, - accountProviderID = accountProviderId, - bookID = book.id, - file = file, - drmInfo = formatAudio.drmInformation, - manifestContentType = format.contentType.fullType, - manifestFile = manifest.manifestFile, - manifestURI = manifest.manifestURI, - opdsEntry = book.entry, - userAgent = httpClient.userAgent() + + if (manifest != null) { + val parameters = + AudioBookPlayerParameters( + accountID = book.account, + accountProviderID = accountProviderId, + bookID = book.id, + file = file, + drmInfo = formatAudio.drmInformation, + manifestContentType = format.contentType.fullType, + manifestFile = manifest.manifestFile, + manifestURI = manifest.manifestURI, + opdsEntry = book.entry, + userAgent = httpClient.userAgent() + ) + + AudioBookViewerModel.parameters = parameters + + val accountCredentials = + profiles.profileCurrent() + .account(book.account) + .loginState + .credentials + + val strategy = + parameters.toManifestStrategy( + application = activity.application, + strategies = strategies, + isNetworkAvailable = { networkConnectivity.isNetworkAvailable }, + credentials = accountCredentials, + cacheDirectory = activity.cacheDir + ) + + val strategyAdapted = + AudioBookManifestFulfillmentAdapter(strategy) + + PlayerModel.downloadParseAndCheckManifest( + sourceURI = manifest.manifestURI, + userAgent = PlayerUserAgent(httpClient.userAgent()), + cacheDir = activity.cacheDir, + licenseChecks = licenseChecks, + parserExtensions = parserExtensions, + strategy = strategyAdapted ) } else { - null + AudioBookViewerModel.parameters = null } - context.startActivity(Intent(context, AudioBookPlayerActivity2::class.java)) + activity.startActivity(Intent(activity, AudioBookPlayerActivity2::class.java)) } } diff --git a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookViewerModel.kt b/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookViewerModel.kt new file mode 100644 index 000000000..371e0aec2 --- /dev/null +++ b/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookViewerModel.kt @@ -0,0 +1,7 @@ +package org.librarysimplified.viewer.audiobook + +internal object AudioBookViewerModel { + + @Volatile + internal var parameters: AudioBookPlayerParameters? = null +} diff --git a/simplified-viewer-audiobook/src/main/res/layout/audio_book_player_loading.xml b/simplified-viewer-audiobook/src/main/res/layout/audio_book_player_loading.xml index 9d29881e5..792d436eb 100644 --- a/simplified-viewer-audiobook/src/main/res/layout/audio_book_player_loading.xml +++ b/simplified-viewer-audiobook/src/main/res/layout/audio_book_player_loading.xml @@ -1,15 +1,37 @@ - - - - - + + + + + + + diff --git a/simplified-viewer-epub-readium2/src/main/java/org/librarysimplified/viewer/epub/readium2/Reader2Activity.kt b/simplified-viewer-epub-readium2/src/main/java/org/librarysimplified/viewer/epub/readium2/Reader2Activity.kt index d2421c365..7e17cb9ce 100644 --- a/simplified-viewer-epub-readium2/src/main/java/org/librarysimplified/viewer/epub/readium2/Reader2Activity.kt +++ b/simplified-viewer-epub-readium2/src/main/java/org/librarysimplified/viewer/epub/readium2/Reader2Activity.kt @@ -1,6 +1,6 @@ package org.librarysimplified.viewer.epub.readium2 -import android.app.Application +import android.app.Activity import android.content.Intent import android.content.pm.ApplicationInfo import android.os.Bundle @@ -79,15 +79,15 @@ class Reader2Activity : AppCompatActivity(R.layout.reader2) { */ fun startActivity( - context: Application, + activity: Activity, parameters: Reader2ActivityParameters ) { - val intent = Intent(context, Reader2Activity::class.java) + val intent = Intent(activity, Reader2Activity::class.java) val bundle = Bundle().apply { this.putSerializable(this@Companion.ARG_PARAMETERS, parameters) } intent.putExtras(bundle) - context.startActivity(intent) + activity.startActivity(intent) } } diff --git a/simplified-viewer-epub-readium2/src/main/java/org/librarysimplified/viewer/epub/readium2/ReaderViewerR2.kt b/simplified-viewer-epub-readium2/src/main/java/org/librarysimplified/viewer/epub/readium2/ReaderViewerR2.kt index 87b198a40..5cfdb2315 100644 --- a/simplified-viewer-epub-readium2/src/main/java/org/librarysimplified/viewer/epub/readium2/ReaderViewerR2.kt +++ b/simplified-viewer-epub-readium2/src/main/java/org/librarysimplified/viewer/epub/readium2/ReaderViewerR2.kt @@ -1,6 +1,6 @@ package org.librarysimplified.viewer.epub.readium2 -import android.app.Application +import android.app.Activity import one.irradia.mime.api.MIMEType import org.nypl.simplified.books.api.Book import org.nypl.simplified.books.api.BookFormat @@ -33,7 +33,7 @@ class ReaderViewerR2 : ViewerProviderType { } override fun open( - context: Application, + activity: Activity, preferences: ViewerPreferences, book: Book, format: BookFormat, @@ -56,7 +56,7 @@ class ReaderViewerR2 : ViewerProviderType { ) Reader2Activity.startActivity( - context = context, + activity = activity, parameters = parameters ) } diff --git a/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfReaderActivity.kt b/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfReaderActivity.kt index ef771cb8d..c805ac5bf 100644 --- a/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfReaderActivity.kt +++ b/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfReaderActivity.kt @@ -1,6 +1,6 @@ package org.librarysimplified.viewer.pdf.pdfjs -import android.app.Application +import android.app.Activity import android.content.Context import android.content.Intent import android.os.Bundle @@ -51,7 +51,7 @@ class PdfReaderActivity : AppCompatActivity() { * Factory method to start a [PdfReaderActivity] */ fun startActivity( - context: Application, + context: Activity, parameters: PdfReaderParameters ) { val bundle = Bundle().apply { diff --git a/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfViewerProvider.kt b/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfViewerProvider.kt index 1c75db650..2377625d9 100644 --- a/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfViewerProvider.kt +++ b/simplified-viewer-pdf-pdfjs/src/main/java/org/librarysimplified/viewer/pdf/pdfjs/PdfViewerProvider.kt @@ -1,6 +1,6 @@ package org.librarysimplified.viewer.pdf.pdfjs -import android.app.Application +import android.app.Activity import one.irradia.mime.api.MIMEType import org.nypl.simplified.books.api.Book import org.nypl.simplified.books.api.BookFormat @@ -41,18 +41,19 @@ class PdfViewerProvider : ViewerProviderType { } override fun open( - context: Application, + activity: Activity, preferences: ViewerPreferences, book: Book, format: BookFormat, accountProviderId: URI ) { - val formatPDF = format as BookFormat.BookFormatPDF + val formatPDF = + format as BookFormat.BookFormatPDF val entry = FeedEntry.FeedEntryOPDS(book.account, book.entry) PdfReaderActivity.startActivity( - context = context, + context = activity, parameters = PdfReaderParameters( accountId = book.account, documentTitle = book.entry.title, diff --git a/simplified-viewer-spi/src/main/java/org/nypl/simplified/viewer/spi/ViewerProviderType.kt b/simplified-viewer-spi/src/main/java/org/nypl/simplified/viewer/spi/ViewerProviderType.kt index 777f78fb4..9c0a0f67f 100644 --- a/simplified-viewer-spi/src/main/java/org/nypl/simplified/viewer/spi/ViewerProviderType.kt +++ b/simplified-viewer-spi/src/main/java/org/nypl/simplified/viewer/spi/ViewerProviderType.kt @@ -1,6 +1,6 @@ package org.nypl.simplified.viewer.spi -import android.app.Application +import android.app.Activity import one.irradia.mime.api.MIMEType import org.nypl.simplified.books.api.Book import org.nypl.simplified.books.api.BookFormat @@ -57,7 +57,7 @@ interface ViewerProviderType { */ fun open( - context: Application, + activity: Activity, preferences: ViewerPreferences, book: Book, format: BookFormat, From dc5d3b6c1dab5ef3005706e4ccbcde48b5832ca0 Mon Sep 17 00:00:00 2001 From: Mark Raynsford Date: Wed, 1 May 2024 17:19:19 +0000 Subject: [PATCH 03/11] Fix numerous UX issues. Affects: https://ebce-lyrasis.atlassian.net/browse/PP-1074 Affects: https://ebce-lyrasis.atlassian.net/browse/PP-1075 Affects: https://ebce-lyrasis.atlassian.net/browse/PP-1076 Affects: https://ebce-lyrasis.atlassian.net/browse/PP-1082 --- .../books/api/bookmark/SerializedLocators.kt | 18 +++++- .../books/covers/BookCoverProvider.kt | 12 ++++ .../books/covers/BookCoverProviderType.kt | 16 +++++ .../bookmarks/BookmarksSerializationTest.kt | 60 ++++++++++++++++++ .../bookmarks/bookmark-20210317-r2-1.json | 16 +++++ .../audiobook/AudioBookPlayerActivity2.kt | 37 ++++++++++- .../viewer/audiobook/AudioBookViewer.kt | 3 + .../src/main/res/drawable/empty.png | Bin 0 -> 68 bytes .../res/layout/audio_book_player_loading.xml | 2 +- 9 files changed, 161 insertions(+), 3 deletions(-) create mode 100644 simplified-tests/src/test/resources/org/nypl/simplified/tests/bookmarks/bookmark-20210317-r2-1.json create mode 100644 simplified-viewer-audiobook/src/main/res/drawable/empty.png diff --git a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocators.kt b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocators.kt index 51d72bb42..e1b7bed18 100644 --- a/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocators.kt +++ b/simplified-books-api/src/main/java/org/nypl/simplified/books/api/bookmark/SerializedLocators.kt @@ -57,7 +57,11 @@ object SerializedLocators { this.parseLocatorAudioBookTime(node) } - "BookLocationR2", "LocatorHrefProgression" -> { + "BookLocationR2" -> { + this.parseLocatorLegacyR2(node) + } + + "LocatorHrefProgression" -> { this.parseLocatorHrefProgression(node) } @@ -74,6 +78,18 @@ object SerializedLocators { } } + @JvmStatic + @Throws(JSONParseException::class) + private fun parseLocatorLegacyR2( + node: ObjectNode + ): SerializedLocator { + val progress = JSONParserUtilities.getObject(node, "progress") + return SerializedLocatorHrefProgression20210317( + chapterHref = JSONParserUtilities.getString(progress, "chapterHref"), + chapterProgress = JSONParserUtilities.getDouble(progress, "chapterProgress") + ) + } + @JvmStatic @Throws(JSONParseException::class) private fun parseLocatorLegacyCFI( diff --git a/simplified-books-covers/src/main/java/org/nypl/simplified/books/covers/BookCoverProvider.kt b/simplified-books-covers/src/main/java/org/nypl/simplified/books/covers/BookCoverProvider.kt index 9ed62b2a0..137cb626b 100644 --- a/simplified-books-covers/src/main/java/org/nypl/simplified/books/covers/BookCoverProvider.kt +++ b/simplified-books-covers/src/main/java/org/nypl/simplified/books/covers/BookCoverProvider.kt @@ -236,6 +236,18 @@ class BookCoverProvider private constructor( ) } + override fun loadCoverAsBitmap( + source: URI, + onBitmapLoaded: (Bitmap) -> Unit, + defaultResource: Int + ) { + doLoadCoverAsBitmap( + coverURI = source, + onBitmapLoaded = onBitmapLoaded, + defaultResource = defaultResource + ) + } + private fun mapOptionToNull(option: OptionType): T? { if (option is Some) { return option.get() diff --git a/simplified-books-covers/src/main/java/org/nypl/simplified/books/covers/BookCoverProviderType.kt b/simplified-books-covers/src/main/java/org/nypl/simplified/books/covers/BookCoverProviderType.kt index 775c710ec..751d91285 100644 --- a/simplified-books-covers/src/main/java/org/nypl/simplified/books/covers/BookCoverProviderType.kt +++ b/simplified-books-covers/src/main/java/org/nypl/simplified/books/covers/BookCoverProviderType.kt @@ -4,6 +4,7 @@ import android.graphics.Bitmap import android.widget.ImageView import com.google.common.util.concurrent.FluentFuture import org.nypl.simplified.feeds.api.FeedEntry +import java.net.URI /** * The type of cover providers. @@ -78,4 +79,19 @@ interface BookCoverProviderType { onBitmapLoaded: (Bitmap) -> Unit, defaultResource: Int ) + + /** + * Load the cover based on `entry` as bitmap to be used as the argument of the callback + * + * @param entry The feed entry + * @param onBitmapLoaded The callback to call when the image is loaded + * @param defaultResource The id for the default resource if something goes wrong while loading + * the bitmapUse 0 as desired dimension to resize keeping aspect ratio. + **/ + + fun loadCoverAsBitmap( + source: URI, + onBitmapLoaded: (Bitmap) -> Unit, + defaultResource: Int + ) } diff --git a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/bookmarks/BookmarksSerializationTest.kt b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/bookmarks/BookmarksSerializationTest.kt index e4bf1fdcc..c50005718 100644 --- a/simplified-tests/src/test/java/org/nypl/simplified/tests/books/bookmarks/BookmarksSerializationTest.kt +++ b/simplified-tests/src/test/java/org/nypl/simplified/tests/books/bookmarks/BookmarksSerializationTest.kt @@ -17,6 +17,7 @@ import org.nypl.simplified.books.api.bookmark.SerializedLocatorPage1 import org.nypl.simplified.books.api.bookmark.SerializedLocators import org.slf4j.LoggerFactory import java.net.URI +import java.nio.charset.StandardCharsets class BookmarksSerializationTest { @@ -317,4 +318,63 @@ class BookmarksSerializationTest { assertEquals(locatorOut, locatorIn) } + + @Test + fun testParseLegacyBookmarks0() { + val bookmark = + SerializedBookmarks.parseBookmarkFromString( + textOf("bookmark-20210317-r1-0.json") + ) + + assertEquals("urn:isbn:9781683607144", bookmark.opdsId) + } + + @Test + fun testParseLegacyBookmarks1() { + val bookmark = + SerializedBookmarks.parseBookmarkFromString( + textOf("bookmark-20210317-r2-0.json") + ) + + assertEquals("urn:isbn:9781683607144", bookmark.opdsId) + } + + @Test + fun testParseLegacyBookmarks2() { + val bookmark = + SerializedBookmarks.parseBookmarkFromString( + textOf("bookmark-20210317-r2-1.json") + ) + + assertEquals("urn:uuid:808b2d99-c286-499a-91a1-580afc4c563f", bookmark.opdsId) + } + + @Test + fun testParseLegacyBookmarks3() { + val bookmark = + SerializedBookmarks.parseBookmarkFromString( + textOf("bookmark-legacy-r1-0.json") + ) + + assertEquals("urn:isbn:9781683607144", bookmark.opdsId) + } + + @Test + fun testParseLegacyBookmarks4() { + val bookmark = + SerializedBookmarks.parseBookmarkFromString( + textOf("bookmark-legacy-r1-1.json") + ) + + assertEquals("urn:isbn:9781683606123", bookmark.opdsId) + } + + private fun textOf( + name: String + ): String { + return BookmarksSerializationTest::class.java.getResourceAsStream( + "/org/nypl/simplified/tests/bookmarks/$name" + ).readAllBytes() + .toString(StandardCharsets.UTF_8) + } } diff --git a/simplified-tests/src/test/resources/org/nypl/simplified/tests/bookmarks/bookmark-20210317-r2-1.json b/simplified-tests/src/test/resources/org/nypl/simplified/tests/bookmarks/bookmark-20210317-r2-1.json new file mode 100644 index 000000000..04b43d19d --- /dev/null +++ b/simplified-tests/src/test/resources/org/nypl/simplified/tests/bookmarks/bookmark-20210317-r2-1.json @@ -0,0 +1,16 @@ +{ + "@version" : 20210828, + "opdsId" : "urn:uuid:808b2d99-c286-499a-91a1-580afc4c563f", + "location" : { + "@type" : "BookLocationR2", + "@version" : 20210317, + "progress" : { + "chapterHref" : "63389/@tmp@gitberg-share-books@Homestead-Ranch_63389@63389-h@63389-h-0.htm.html", + "chapterProgress" : 0.2653061894548496 + } + }, + "time" : "2024-04-05T11:12:43.032Z", + "chapterTitle" : "", + "bookProgress" : 0.2517006877172055, + "deviceID" : "null" +} \ No newline at end of file diff --git a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookPlayerActivity2.kt b/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookPlayerActivity2.kt index 1847ffec3..ac94a96b8 100644 --- a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookPlayerActivity2.kt +++ b/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookPlayerActivity2.kt @@ -7,6 +7,7 @@ import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.TxContextWrappingDelegate2 import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment +import com.io7m.jfunctional.Some import io.reactivex.disposables.CompositeDisposable import org.librarysimplified.audiobook.api.PlayerBookmark import org.librarysimplified.audiobook.api.PlayerEvent @@ -44,7 +45,9 @@ import org.librarysimplified.audiobook.views.PlayerTOCFragment import org.librarysimplified.audiobook.views.PlayerViewCommand import org.librarysimplified.services.api.Services import org.nypl.simplified.bookmarks.api.BookmarkServiceType +import org.nypl.simplified.books.covers.BookCoverProviderType import org.slf4j.LoggerFactory +import java.net.URI class AudioBookPlayerActivity2 : AppCompatActivity(R.layout.audio_book_player_base) { @@ -52,6 +55,7 @@ class AudioBookPlayerActivity2 : AppCompatActivity(R.layout.audio_book_player_ba LoggerFactory.getLogger(AudioBookPlayerActivity2::class.java) private lateinit var bookmarkService: BookmarkServiceType + private lateinit var coverService: BookCoverProviderType private val playerExtensions: List = listOf() private var fragmentNow: Fragment = AudioBookLoadingFragment2() @@ -75,6 +79,8 @@ class AudioBookPlayerActivity2 : AppCompatActivity(R.layout.audio_book_player_ba this.bookmarkService = services.requireService(BookmarkServiceType::class.java) + this.coverService = + services.requireService(BookCoverProviderType::class.java) } override fun onStart() { @@ -98,7 +104,7 @@ class AudioBookPlayerActivity2 : AppCompatActivity(R.layout.audio_book_player_ba when (f) { is PlayerFragment -> { PlayerModel.closeBookOrDismissError() - Unit + super.onBackPressed() } is PlayerTOCFragment -> { @@ -238,6 +244,21 @@ class AudioBookPlayerActivity2 : AppCompatActivity(R.layout.audio_book_player_ba } is PlayerModelState.PlayerOpen -> { + val parameters = AudioBookViewerModel.parameters ?: return + + /* + * Set the cover image (loading it asynchronously). + */ + + val coverURI = parameters.opdsEntry.cover + if (coverURI is Some) { + this.coverService.loadCoverAsBitmap( + source = coverURI.get(), + onBitmapLoaded = PlayerModel::setCoverImage, + defaultResource = R.drawable.empty + ) + } + /* * XXX: This shouldn't really be a blocking call to get() * The bookmarks service should expose an always-up-to-date readable set of bookmarks. @@ -260,6 +281,20 @@ class AudioBookPlayerActivity2 : AppCompatActivity(R.layout.audio_book_player_ba } this.switchFragment(PlayerFragment()) + + /* + * Tell the downloader to download every chapter. + * + * XXX: This is probably something we don't want to do unconditionally anymore. It's + * here for backwards compatibility until we support streaming and have a refurbished + * TOC that allows for good control over downloads. + */ + + PlayerUIThread.runOnUIThreadDelayed({ + PlayerModel.book() + .wholeBookDownloadTask + .fetch() + }, 2000L) } PlayerModelState.PlayerManifestInProgress -> { diff --git a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookViewer.kt b/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookViewer.kt index f41987936..65bee35f3 100644 --- a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookViewer.kt +++ b/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookViewer.kt @@ -143,6 +143,9 @@ class AudioBookViewer : ViewerProviderType { val manifest = formatAudio.manifest + PlayerModel.bookAuthor = book.entry.authorsCommaSeparated + PlayerModel.bookTitle = book.entry.title + if (manifest != null) { val parameters = AudioBookPlayerParameters( diff --git a/simplified-viewer-audiobook/src/main/res/drawable/empty.png b/simplified-viewer-audiobook/src/main/res/drawable/empty.png new file mode 100644 index 0000000000000000000000000000000000000000..3639dc75ab9fe6ff6d118c8ac2440aed5bc8eec9 GIT binary patch literal 68 zcmeAS@N?(olHy`uVBq!ia0vp^Od!m`1|*BN@u~nRZci7-5RU7~2@dQG3_=Wy?>j@5 Q0EHPmUHx3vIVCg!0BRBp)Bpeg literal 0 HcmV?d00001 diff --git a/simplified-viewer-audiobook/src/main/res/layout/audio_book_player_loading.xml b/simplified-viewer-audiobook/src/main/res/layout/audio_book_player_loading.xml index 792d436eb..06f00c777 100644 --- a/simplified-viewer-audiobook/src/main/res/layout/audio_book_player_loading.xml +++ b/simplified-viewer-audiobook/src/main/res/layout/audio_book_player_loading.xml @@ -10,7 +10,7 @@ android:id="@+id/progressBar" style="?android:attr/progressBarStyleHorizontal" android:layout_width="256dp" - android:layout_height="wrap_content" + android:layout_height="16dp" android:layout_marginStart="16dp" android:layout_marginTop="16dp" android:layout_marginEnd="16dp" From d6a401620c84a41fd303cb60327d77d672338e64 Mon Sep 17 00:00:00 2001 From: Mark Raynsford Date: Thu, 2 May 2024 16:45:13 +0000 Subject: [PATCH 04/11] Adjust progress bar, use new audiobook snapshot. --- .../src/main/res/layout/audio_book_player_loading.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/simplified-viewer-audiobook/src/main/res/layout/audio_book_player_loading.xml b/simplified-viewer-audiobook/src/main/res/layout/audio_book_player_loading.xml index 06f00c777..f5cb9bd2a 100644 --- a/simplified-viewer-audiobook/src/main/res/layout/audio_book_player_loading.xml +++ b/simplified-viewer-audiobook/src/main/res/layout/audio_book_player_loading.xml @@ -10,7 +10,7 @@ android:id="@+id/progressBar" style="?android:attr/progressBarStyleHorizontal" android:layout_width="256dp" - android:layout_height="16dp" + android:layout_height="32dp" android:layout_marginStart="16dp" android:layout_marginTop="16dp" android:layout_marginEnd="16dp" From c3fc048c87ea754c06d677d1e1c4c30947a7935d Mon Sep 17 00:00:00 2001 From: Mark Raynsford Date: Fri, 3 May 2024 11:04:30 +0000 Subject: [PATCH 05/11] Improve error handling for audiobooks. --- .../ui/accounts/AccountDetailViewModel.kt | 2 +- .../ui/accounts/AccountListFragment.kt | 2 +- .../accounts/AccountListRegistryFragment.kt | 2 +- .../accounts/saml20/AccountSAML20Fragment.kt | 2 +- .../catalog/saml20/CatalogSAML20ViewModel.kt | 2 +- ...geBaseActivity.kt => ErrorPageActivity.kt} | 2 +- .../ui/settings/SettingsDebugFragment.kt | 2 +- simplified-viewer-audiobook/build.gradle.kts | 9 +- .../audiobook/AudioBookPlayerActivity2.kt | 111 ++++++++++++++++-- .../src/main/res/values/strings.xml | 5 + 10 files changed, 119 insertions(+), 20 deletions(-) rename simplified-ui-errorpage/src/main/java/org/nypl/simplified/ui/errorpage/{ErrorPageBaseActivity.kt => ErrorPageActivity.kt} (94%) diff --git a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountDetailViewModel.kt b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountDetailViewModel.kt index 454e71e6a..1cd5bfd35 100644 --- a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountDetailViewModel.kt +++ b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountDetailViewModel.kt @@ -198,7 +198,7 @@ class AccountDetailViewModel( ErrorPageParameters( emailAddress = this.buildConfig.supportErrorReportEmailAddress, body = "", - subject = "[simplye-error-report]", + subject = "[palace-error-report]", attributes = sortedMapOf(), taskSteps = taskSteps ) diff --git a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountListFragment.kt b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountListFragment.kt index 209c101c4..8d40b5010 100644 --- a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountListFragment.kt +++ b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountListFragment.kt @@ -224,7 +224,7 @@ class AccountListFragment : Fragment(R.layout.account_list) { ErrorPageParameters( emailAddress = this.viewModel.supportEmailAddress, body = "", - subject = "[simplye-error-report]", + subject = "[palace-error-report]", attributes = accountEvent.attributes.toSortedMap(), taskSteps = accountEvent.taskResult.steps ) diff --git a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountListRegistryFragment.kt b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountListRegistryFragment.kt index 1742669a5..9407adf81 100644 --- a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountListRegistryFragment.kt +++ b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/AccountListRegistryFragment.kt @@ -294,7 +294,7 @@ class AccountListRegistryFragment : Fragment(R.layout.account_list_registry) { ErrorPageParameters( emailAddress = this.viewModel.supportEmailAddress, body = "", - subject = "[simplye-error-report]", + subject = "[palace-error-report]", attributes = accountEvent.attributes.toSortedMap(), taskSteps = accountEvent.taskResult.steps ) diff --git a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/saml20/AccountSAML20Fragment.kt b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/saml20/AccountSAML20Fragment.kt index 22ccbf926..2628d556e 100644 --- a/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/saml20/AccountSAML20Fragment.kt +++ b/simplified-ui-accounts/src/main/java/org/nypl/simplified/ui/accounts/saml20/AccountSAML20Fragment.kt @@ -146,7 +146,7 @@ class AccountSAML20Fragment : Fragment(R.layout.account_saml20) { ErrorPageParameters( emailAddress = this.viewModel.supportEmailAddress, body = "", - subject = "[simplye-error-report]", + subject = "[palace-error-report]", attributes = sortedMapOf(), taskSteps = taskSteps ) diff --git a/simplified-ui-catalog/src/main/java/org/librarysimplified/ui/catalog/saml20/CatalogSAML20ViewModel.kt b/simplified-ui-catalog/src/main/java/org/librarysimplified/ui/catalog/saml20/CatalogSAML20ViewModel.kt index bf0ca4939..ab67ebd4f 100644 --- a/simplified-ui-catalog/src/main/java/org/librarysimplified/ui/catalog/saml20/CatalogSAML20ViewModel.kt +++ b/simplified-ui-catalog/src/main/java/org/librarysimplified/ui/catalog/saml20/CatalogSAML20ViewModel.kt @@ -149,7 +149,7 @@ class CatalogSAML20ViewModel( ErrorPageParameters( emailAddress = this.buildConfig.supportErrorReportEmailAddress, body = "", - subject = "[simplye-error-report]", + subject = "[palace-error-report]", attributes = sortedMapOf(), taskSteps = taskSteps ) diff --git a/simplified-ui-errorpage/src/main/java/org/nypl/simplified/ui/errorpage/ErrorPageBaseActivity.kt b/simplified-ui-errorpage/src/main/java/org/nypl/simplified/ui/errorpage/ErrorPageActivity.kt similarity index 94% rename from simplified-ui-errorpage/src/main/java/org/nypl/simplified/ui/errorpage/ErrorPageBaseActivity.kt rename to simplified-ui-errorpage/src/main/java/org/nypl/simplified/ui/errorpage/ErrorPageActivity.kt index cf8152a32..44af76eab 100644 --- a/simplified-ui-errorpage/src/main/java/org/nypl/simplified/ui/errorpage/ErrorPageBaseActivity.kt +++ b/simplified-ui-errorpage/src/main/java/org/nypl/simplified/ui/errorpage/ErrorPageActivity.kt @@ -8,7 +8,7 @@ import org.librarysimplified.ui.errorpage.R * A convenient base activity used to show error pages. */ -abstract class ErrorPageBaseActivity : AppCompatActivity(R.layout.error_host) { +class ErrorPageActivity : AppCompatActivity(R.layout.error_host) { companion object { const val PARAMETER_ID = diff --git a/simplified-ui-settings/src/main/java/org/nypl/simplified/ui/settings/SettingsDebugFragment.kt b/simplified-ui-settings/src/main/java/org/nypl/simplified/ui/settings/SettingsDebugFragment.kt index 0259b5b23..d543a9b80 100644 --- a/simplified-ui-settings/src/main/java/org/nypl/simplified/ui/settings/SettingsDebugFragment.kt +++ b/simplified-ui-settings/src/main/java/org/nypl/simplified/ui/settings/SettingsDebugFragment.kt @@ -350,7 +350,7 @@ class SettingsDebugFragment : Fragment(R.layout.settings_debug) { ErrorPageParameters( emailAddress = this.viewModel.supportEmailAddress, body = "", - subject = "[simplye-error-report] ${this.viewModel.appVersion}", + subject = "[palace-error-report] ${this.viewModel.appVersion}", attributes = attributes, taskSteps = taskSteps ) diff --git a/simplified-viewer-audiobook/build.gradle.kts b/simplified-viewer-audiobook/build.gradle.kts index ecfe0e60c..0bacfe677 100644 --- a/simplified-viewer-audiobook/build.gradle.kts +++ b/simplified-viewer-audiobook/build.gradle.kts @@ -9,6 +9,7 @@ dependencies { implementation(project(":simplified-books-database-api")) implementation(project(":simplified-books-formats-api")) implementation(project(":simplified-books-time-tracking")) + implementation(project(":simplified-buildconfig-api")) implementation(project(":simplified-feeds-api")) implementation(project(":simplified-files")) implementation(project(":simplified-futures")) @@ -22,6 +23,7 @@ dependencies { implementation(project(":simplified-services-api")) implementation(project(":simplified-taskrecorder-api")) implementation(project(":simplified-threads")) + implementation(project(":simplified-ui-errorpage")) implementation(project(":simplified-ui-screen")) implementation(project(":simplified-ui-thread-api")) implementation(project(":simplified-viewer-spi")) @@ -38,13 +40,13 @@ dependencies { implementation(libs.androidx.constraintlayout.solver) implementation(libs.androidx.coordinatorlayout) implementation(libs.androidx.core) - implementation(libs.androidx.emoji2) - implementation(libs.androidx.emoji2.views) - implementation(libs.androidx.emoji2.views.helper) implementation(libs.androidx.core.ktx) implementation(libs.androidx.core.splashscreen) implementation(libs.androidx.customview) implementation(libs.androidx.drawerlayout) + implementation(libs.androidx.emoji2) + implementation(libs.androidx.emoji2.views) + implementation(libs.androidx.emoji2.views.helper) implementation(libs.androidx.fragment) implementation(libs.androidx.fragment.ktx) implementation(libs.androidx.lifecycle.common) @@ -80,6 +82,7 @@ dependencies { implementation(libs.palace.audiobook.manifest.parser.extension.spi) implementation(libs.palace.audiobook.manifest.parser.webpub) implementation(libs.palace.audiobook.media3) + implementation(libs.palace.audiobook.parser.api) implementation(libs.palace.audiobook.views) implementation(libs.palace.drm.core) implementation(libs.palace.http.api) diff --git a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookPlayerActivity2.kt b/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookPlayerActivity2.kt index ac94a96b8..2be592248 100644 --- a/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookPlayerActivity2.kt +++ b/simplified-viewer-audiobook/src/main/java/org/librarysimplified/viewer/audiobook/AudioBookPlayerActivity2.kt @@ -7,6 +7,7 @@ import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.TxContextWrappingDelegate2 import androidx.fragment.app.DialogFragment import androidx.fragment.app.Fragment +import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.io7m.jfunctional.Some import io.reactivex.disposables.CompositeDisposable import org.librarysimplified.audiobook.api.PlayerBookmark @@ -46,6 +47,11 @@ import org.librarysimplified.audiobook.views.PlayerViewCommand import org.librarysimplified.services.api.Services import org.nypl.simplified.bookmarks.api.BookmarkServiceType import org.nypl.simplified.books.covers.BookCoverProviderType +import org.nypl.simplified.buildconfig.api.BuildConfigurationServiceType +import org.nypl.simplified.taskrecorder.api.TaskRecorder +import org.nypl.simplified.taskrecorder.api.TaskResult +import org.nypl.simplified.ui.errorpage.ErrorPageFragment +import org.nypl.simplified.ui.errorpage.ErrorPageParameters import org.slf4j.LoggerFactory import java.net.URI @@ -55,6 +61,7 @@ class AudioBookPlayerActivity2 : AppCompatActivity(R.layout.audio_book_player_ba LoggerFactory.getLogger(AudioBookPlayerActivity2::class.java) private lateinit var bookmarkService: BookmarkServiceType + private lateinit var buildConfig: BuildConfigurationServiceType private lateinit var coverService: BookCoverProviderType private val playerExtensions: List = listOf() @@ -77,6 +84,8 @@ class AudioBookPlayerActivity2 : AppCompatActivity(R.layout.audio_book_player_ba val services = Services.serviceDirectory() + this.buildConfig = + services.requireService(BuildConfigurationServiceType::class.java) this.bookmarkService = services.requireService(BookmarkServiceType::class.java) this.coverService = @@ -113,19 +122,10 @@ class AudioBookPlayerActivity2 : AppCompatActivity(R.layout.audio_book_player_ba } } - is AudioBookLoadingFragment2 -> { - PlayerModel.closeBookOrDismissError() - super.onBackPressed() - } - - null -> { + else -> { PlayerModel.closeBookOrDismissError() super.onBackPressed() } - - else -> { - throw IllegalStateException("Unrecognized fragment: $f") - } } } @@ -307,24 +307,115 @@ class AudioBookPlayerActivity2 : AppCompatActivity(R.layout.audio_book_player_ba state: PlayerModelState.PlayerManifestParseFailed ) { this.logger.error("onManifestParseFailed: {}", state) + + val task = TaskRecorder.create() + task.beginNewStep("Parsing manifest…") + state.failure.mapIndexed { index, error -> + this.logger.error("{}:{}: {}", error.line, error.column, error.message) + task.addAttribute( + "Parse Error [${index}]", + "${error.line}:${error.column}: ${error.message}" + ) + } + task.currentStepFailed("Parsing failed.", "error-manifest-parse") + + val alert = MaterialAlertDialogBuilder(this) + alert.setTitle(R.string.audio_book_player_error_book_open) + alert.setMessage(R.string.audio_book_manifest_parse_error) + alert.setNeutralButton(R.string.audio_book_player_details) { dialog, _ -> + openErrorPage(task.finishFailure()) + } + alert.setPositiveButton(R.string.audio_book_player_ok) { dialog, _ -> + dialog.dismiss() + this.finish() + } + alert.show() + } + + private fun openErrorPage(result: TaskResult.Failure<*>) { + this.switchFragment( + ErrorPageFragment.create( + ErrorPageParameters( + emailAddress = this.buildConfig.supportErrorReportEmailAddress, + body = "", + subject = "[palace-audiobook-error-report]", + attributes = sortedMapOf(), + taskSteps = result.steps + ) + ) + ) } private fun onBookOpenFailed( state: PlayerModelState.PlayerBookOpenFailed ) { this.logger.error("onBookOpenFailed: {}", state) + + val task = TaskRecorder.create() + task.beginNewStep("Opening book…") + task.currentStepFailed(state.message, "error-book-open") + + val alert = MaterialAlertDialogBuilder(this) + alert.setTitle(R.string.audio_book_player_error_book_open) + alert.setMessage(state.message) + alert.setNeutralButton(R.string.audio_book_player_details) { dialog, _ -> + openErrorPage(task.finishFailure()) + } + alert.setPositiveButton(R.string.audio_book_player_ok) { dialog, _ -> + dialog.dismiss() + this.finish() + } + alert.show() } private fun onManifestDownloadFailed( state: PlayerModelState.PlayerManifestDownloadFailed ) { this.logger.error("onManifestDownloadFailed: {}", state) + + val task = TaskRecorder.create() + task.beginNewStep("Downloading manifest…") + val serverData = state.failure.serverData + if (serverData != null) { + task.addAttribute("URI", serverData.uri.toString()) + task.addAttribute("Code", serverData.code.toString()) + task.addAttribute("ContentType", serverData.receivedContentType) + } + task.currentStepFailed(state.failure.message, "error-manifest-download") + + val alert = MaterialAlertDialogBuilder(this) + alert.setTitle(R.string.audio_book_player_error_book_open) + alert.setMessage(R.string.audio_book_manifest_download_error) + alert.setNeutralButton(R.string.audio_book_player_details) { dialog, _ -> + openErrorPage(task.finishFailure()) + } + alert.setPositiveButton(R.string.audio_book_player_ok) { dialog, _ -> + dialog.dismiss() + this.finish() + } + alert.show() } private fun onManifestLicenseChecksFailed( state: PlayerModelState.PlayerManifestLicenseChecksFailed ) { this.logger.error("onManifestLicenseChecksFailed: {}", state) + + val task = TaskRecorder.create() + task.beginNewStep("Checking license…") + task.currentStepFailed("License checks failed.", "error-manifest-license") + + val alert = MaterialAlertDialogBuilder(this) + alert.setTitle(R.string.audio_book_player_error_book_open) + alert.setMessage(R.string.audio_book_manifest_license_error) + alert.setNeutralButton(R.string.audio_book_player_details) { dialog, _ -> + openErrorPage(task.finishFailure()) + } + alert.setPositiveButton(R.string.audio_book_player_ok) { dialog, _ -> + dialog.dismiss() + this.finish() + } + alert.show() } @UiThread diff --git a/simplified-viewer-audiobook/src/main/res/values/strings.xml b/simplified-viewer-audiobook/src/main/res/values/strings.xml index 470f69d00..8b40c8940 100644 --- a/simplified-viewer-audiobook/src/main/res/values/strings.xml +++ b/simplified-viewer-audiobook/src/main/res/values/strings.xml @@ -13,4 +13,9 @@ Would you like to return it? Your Audiobook Has Finished Table Of Contents + OK + Details + The audiobook manifest could not be parsed. + The audiobook manifest could not be downloaded. + The audiobook manifest license checks failed. \ No newline at end of file From 354f5b0a065dfc76a5882c447b1271b69289e893 Mon Sep 17 00:00:00 2001 From: Mark Raynsford Date: Fri, 3 May 2024 11:05:43 +0000 Subject: [PATCH 06/11] Update changelog. --- README-CHANGES.xml | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/README-CHANGES.xml b/README-CHANGES.xml index 3e2abce62..1a30abf82 100644 --- a/README-CHANGES.xml +++ b/README-CHANGES.xml @@ -522,8 +522,17 @@ - - + + + + + + + + + + + From bc33e00ef84933a36c215966052cc27cf9c4af6a Mon Sep 17 00:00:00 2001 From: Mark Raynsford Date: Mon, 6 May 2024 10:33:34 +0000 Subject: [PATCH 07/11] Use tag prefix file. --- .ci-local/tag-prefix.conf | 8 ++++++++ .ci-local/tag-template.conf | 10 ---------- 2 files changed, 8 insertions(+), 10 deletions(-) create mode 100644 .ci-local/tag-prefix.conf delete mode 100644 .ci-local/tag-template.conf diff --git a/.ci-local/tag-prefix.conf b/.ci-local/tag-prefix.conf new file mode 100644 index 000000000..741894a14 --- /dev/null +++ b/.ci-local/tag-prefix.conf @@ -0,0 +1,8 @@ +palace + +# +# The first line of this file specifies a prefix to use to generate tags when releases are tagged +# by CI. Everything else in the file is ignored. +# +# The final name of the tag will be ${TAG_PREFIX}-${VERSION_NAME}. +# diff --git a/.ci-local/tag-template.conf b/.ci-local/tag-template.conf deleted file mode 100644 index 1bf9700e7..000000000 --- a/.ci-local/tag-template.conf +++ /dev/null @@ -1,10 +0,0 @@ -palace-${VERSION_NUM} - -# -# The first line of this file specifies a template to use to generate tags when releases are tagged -# by CI. Everything else in the file is ignored. -# -# In the template, ${VERSION_NUM} will be substituted with the version number of the release. -# Example: -# xyz-${VERSION_NUM} -# From 5bce54d5a9d5a9c52dabc88b97cb6441cd540470 Mon Sep 17 00:00:00 2001 From: Mark Raynsford Date: Mon, 6 May 2024 10:46:19 +0000 Subject: [PATCH 08/11] Update CI scripts. --- .ci | 2 +- .github/workflows/android-release.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.ci b/.ci index e8a4c8f43..af8cd2460 160000 --- a/.ci +++ b/.ci @@ -1 +1 @@ -Subproject commit e8a4c8f43974112581c2da0519074e8abae7d5fa +Subproject commit af8cd2460e82fb8e8208761da89bc8d3f13af254 diff --git a/.github/workflows/android-release.yml b/.github/workflows/android-release.yml index 3f7919015..b6394e192 100644 --- a/.github/workflows/android-release.yml +++ b/.github/workflows/android-release.yml @@ -16,7 +16,7 @@ jobs: - name: Verify release branch run: .ci/ci-verify-release-branch.sh - name: Finish release - run: .ci/ci-release-finish.sh --tag + run: .ci/ci-release-finish.sh - name: Create GitHub release uses: softprops/action-gh-release@v2 env: From c16ae68dec2b85151e4e3697a15392c6dc95793e Mon Sep 17 00:00:00 2001 From: Mark Raynsford Date: Mon, 6 May 2024 11:06:52 +0000 Subject: [PATCH 09/11] Use new audiobooks release. --- org.thepalaceproject.android.platform | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/org.thepalaceproject.android.platform b/org.thepalaceproject.android.platform index b90bb370b..ba1a010a1 160000 --- a/org.thepalaceproject.android.platform +++ b/org.thepalaceproject.android.platform @@ -1 +1 @@ -Subproject commit b90bb370b7045d54ed5143cb2e03c6f2ba57ebae +Subproject commit ba1a010a10730dbf2e1608be1d586123f8f967b4 From 64f414195a3a5d03950bbe90da80ca791cb9e9b6 Mon Sep 17 00:00:00 2001 From: Mark Raynsford Date: Mon, 6 May 2024 11:43:02 +0000 Subject: [PATCH 10/11] Update platform. --- org.thepalaceproject.android.platform | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/org.thepalaceproject.android.platform b/org.thepalaceproject.android.platform index ba1a010a1..1404647de 160000 --- a/org.thepalaceproject.android.platform +++ b/org.thepalaceproject.android.platform @@ -1 +1 @@ -Subproject commit ba1a010a10730dbf2e1608be1d586123f8f967b4 +Subproject commit 1404647de26c332c6bc1bd3bbc354132c47ab184 From 494d3df10f617044fefa68049221640e1c45efa1 Mon Sep 17 00:00:00 2001 From: Mark Raynsford Date: Mon, 6 May 2024 11:44:41 +0000 Subject: [PATCH 11/11] Mark version numbers --- README-CHANGES.xml | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README-CHANGES.xml b/README-CHANGES.xml index 1a30abf82..e311cb5f5 100644 --- a/README-CHANGES.xml +++ b/README-CHANGES.xml @@ -522,7 +522,7 @@ - + diff --git a/gradle.properties b/gradle.properties index 40e0fdcc7..db12a8c94 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ POM_SCM_CONNECTION=scm:git:git://github.com/ThePalaceProject/android-core POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/ThePalaceProject/android-core POM_SCM_URL=http://github.com/ThePalaceProject/android-core POM_URL=http://github.com/ThePalaceProject/android-core -VERSION_NAME=1.12.0-SNAPSHOT +VERSION_NAME=1.13.0-SNAPSHOT VERSION_CODE_BASE=70000 android.useAndroidX=true