diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 62b6cc03..00000000 --- a/.eslintignore +++ /dev/null @@ -1,4 +0,0 @@ -./android -./ios -./assets -./node_modules/**/*.js diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index c86af9c2..00000000 --- a/.eslintrc.js +++ /dev/null @@ -1,19 +0,0 @@ -module.exports = { - root: true, - extends: ['eslint:recommended', 'plugin:react/recommended', '@react-native'], - plugins: ['simple-import-sort'], - parserOptions: { - ecmaVersion: 12, - parser: '@babel/eslint-parser', - requireConfigFile: false, - sourceType: 'module', - ecmaFeatures: { - jsx: true, - }, - }, - rules: { - 'react/no-string-refs': 0, - 'no-alert': 0, - 'simple-import-sort/imports': 2, - }, -}; diff --git a/ACKNOWLEDGEMENTS.md b/ACKNOWLEDGEMENTS.md index 3051dd07..f6650a43 100644 --- a/ACKNOWLEDGEMENTS.md +++ b/ACKNOWLEDGEMENTS.md @@ -21,40 +21,6 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. --------------------------------------------------------------------------------- -## AutoValue (https://github.com/google/auto/tree/master/value) - -Copyright 2013 Google, Inc. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - --------------------------------------------------------------------------------- -## AutoValue: Parcel Extension (https://github.com/rharter/auto-value-parcel) - -Copyright 2015 Ryan Harter. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. - -------------------------------------------------------------------------------- ## Barcode Scanner Libraries for Android (https://github.com/dm77/barcodescanner) @@ -2182,33 +2148,6 @@ desirable to choose a license that granted us the same protections for new code that were granted to the IJG for code derived from their software. --------------------------------------------------------------------------------- -## LoopKit (https://github.com/LoopKit/LoopKit) - -The MIT License (MIT) - -Copyright (c) 2015 Nathan Racklyeft -Copyright (c) 2016 LoopKit Authors - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - - -------------------------------------------------------------------------------- ## Mantle (https://github.com/Mantle/Mantle) @@ -3869,6 +3808,241 @@ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. +-------------------------------------------------------------------------------- +## socket.io-client-swift (https://github.com/socketio/socket.io-client-swift) + +The MIT License (MIT) + +Copyright (c) 2014-2015 Erik Little + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + + + +This library makes use of the following third party libraries: + +Starscream +---------- + +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "{}" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright {yyyy} {name of copyright owner} + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + -------------------------------------------------------------------------------- ## SQLite (https://www.sqlite.org/) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe9f4d79..891520b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ ## Newest release +### 2.15.0 - 13 Dec 2024 + +- Adds a new `NotificationCenter` class that can be used to subscribe to Nutrient document, annotation, and analytics events. (J#HYB-448) +- Adds the ability to show or hide the back and forward action buttons using the new `showActionButtons` property. (J#HYB-98) +- Updates to Nutrient Android SDK 2024.8.1. +- Fixes an issue where the `exportXFDF` API on Android did not export all annotations. (J#HYB-546) +- Fixes an issue where the `exportXFDF` API on Android required the `Forms` license capability. (J#HYB-577) +- Fixes an issue where the back button was missing on Android when using the `PSPDFKit.present` API. (J#HYB-549) + +## Previous releases + ### 2.14.0 - 30 Oct 2024 - Adds the ability to hide the main toolbar on Android using a combination of configuration and style properties. (J#HYB-431) @@ -9,8 +20,6 @@ - Fixes an issue where a crash occurred when using the `toolbar.toolbarMenuItems` property and `enterAnnotationCreationMode` API on Android. (J#HYB-517) - Fixes an issue where some annotations would not be deleted on iOS when using the `removeAnnotations` API. (J#HYB-518) -## Previous releases - ### 2.13.0 - 10 Sep 2024 - Adds TypeScript type support to the `annotationPresets` property on the `PSPDFKitView` component. (J#HYB-395) @@ -182,7 +191,7 @@ ### 2.2.0 - 14 Feb 2022 -- This release requires you to update your Android project's `compileSdkVersion` to version 31. Please refer to [our migration guide](https://pspdfkit.com/guides/react-native/migration-guides/react-native-2-2-migration-guide) for this release. +- This release requires you to update your Android project's `compileSdkVersion` to version 31. Please refer to [our migration guide](https://www.nutrient.io/guides/react-native/migration-guides/react-native-2-2-migration-guide) for this release. - Adds a `destroyView()` function to `PSPDFKitView` to be used as a workaround for crash caused by a [`react-native-screens` issue](https://github.com/software-mansion/react-native-screens/issues/1300) when navigating back. (#32960) - Improves the file structure of the Catalog sample project for better readability. (#32685) - Improves the file structure of the NativeCatalog sample project for better readability. (#32887) diff --git a/README.md b/README.md index 65248e70..368ffcbe 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ Nutrient offers support for customers with an active SDK license via https://sup Are you evaluating our SDK? That's great, we're happy to help out! The Nutrient React Native SDK is a commercial product and requires the purchase of a license key when used in production. By default, this library will initialize in demo mode, placing a watermark on each PDF and limiting usage to 60 minutes. -To purchase a license for production use, please reach out to us via https://www.nutrient.io/contact-sales. +To purchase a license for production use, please reach out to us via https://www.nutrient.io/sdk/contact-sales. To initialize the Nutrient React Native SDK using a license key, call either of the following before using any other Nutrient SDK APIs or features: @@ -102,7 +102,7 @@ See our [Getting Started on React Native guide](https://www.nutrient.io/getting- repositories { mavenLocal() + maven { - + url 'https://my.pspdfkit.com/maven/' + + url 'https://my.nutrient.io/maven/' + } } } diff --git a/android/.settings/org.eclipse.buildship.core.prefs b/android/.settings/org.eclipse.buildship.core.prefs index 8bc72b12..318de163 100644 --- a/android/.settings/org.eclipse.buildship.core.prefs +++ b/android/.settings/org.eclipse.buildship.core.prefs @@ -2,7 +2,7 @@ arguments=--init-script /var/folders/3v/qy3ssjxs2m7d97yc60nrl2l00000gn/T/db3b08f auto.sync=false build.scans.enabled=false connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(8.9)) -connection.project.dir=../../../android +connection.project.dir= eclipse.preferences.version=1 gradle.user.home= java.home=/Users/erhardbrand/Library/Java/JavaVirtualMachines/jdk-17.0.8.jdk/Contents/Home diff --git a/android/build.gradle b/android/build.gradle index d0c68c17..7363180a 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -15,7 +15,7 @@ * Contains gradle configuration constants */ ext { - PSPDFKIT_VERSION = '2024.6.1' + PSPDFKIT_VERSION = '2024.8.1' } buildscript { diff --git a/android/src/main/java/com/pspdfkit/react/ConfigurationAdapter.java b/android/src/main/java/com/pspdfkit/react/ConfigurationAdapter.java index 83923bd5..c561b271 100644 --- a/android/src/main/java/com/pspdfkit/react/ConfigurationAdapter.java +++ b/android/src/main/java/com/pspdfkit/react/ConfigurationAdapter.java @@ -31,6 +31,7 @@ import com.pspdfkit.configuration.page.PageLayoutMode; import com.pspdfkit.configuration.page.PageScrollDirection; import com.pspdfkit.configuration.page.PageScrollMode; +import com.pspdfkit.configuration.search.SearchType; import com.pspdfkit.configuration.sharing.ShareFeatures; import com.pspdfkit.configuration.signatures.SignatureSavingStrategy; import com.pspdfkit.preferences.PSPDFKitPreferences; @@ -93,6 +94,7 @@ public class ConfigurationAdapter { private static final String SHOW_DOCUMENT_INFO_VIEW = "showDocumentInfoView"; private static final String SHOW_SETTINGS_MENU = "showSettingsMenu"; private static final String SHOW_DEFAULT_TOOLBAR = "showDefaultToolbar"; + private static final String SHOW_ACTION_BUTTONS = "showActionButtons"; // Thumbnail Options private static final String SHOW_THUMBNAIL_BAR = "showThumbnailBar"; @@ -328,6 +330,10 @@ public ConfigurationAdapter(@NonNull final Context context, ReadableMap configur if (key != null) { configureShowDefaultToolbar(configuration.getBoolean(key)); } + key = getKeyOrNull(configuration, SHOW_ACTION_BUTTONS); + if (key != null) { + configureShowActionButtons(configuration.getBoolean(key)); + } } } @@ -443,7 +449,7 @@ private void configureFitPageToWidth(final boolean fitPageToWidth) { } private void configureInlineSearch(final boolean inlineSearch) { - final int searchType = inlineSearch ? PdfActivityConfiguration.SEARCH_INLINE : PdfActivityConfiguration.SEARCH_MODULAR; + final SearchType searchType = inlineSearch ? SearchType.INLINE : SearchType.MODULAR; configuration.setSearchType(searchType); } @@ -721,6 +727,15 @@ private void configureShowDefaultToolbar(final boolean showDefaultToolbar) { } } + private void configureShowActionButtons(final boolean showActionButtons) { + if (showActionButtons) { + // Set it back to the default, which is AUTOMATIC_HIDE_SINGLE + configuration.showNavigationButtons(); + } else { + configuration.hideNavigationButtons(); + } + } + public PdfActivityConfiguration build() { return configuration.build(); } diff --git a/android/src/main/java/com/pspdfkit/react/NutrientNotificationCenter.kt b/android/src/main/java/com/pspdfkit/react/NutrientNotificationCenter.kt new file mode 100644 index 00000000..6f8d679b --- /dev/null +++ b/android/src/main/java/com/pspdfkit/react/NutrientNotificationCenter.kt @@ -0,0 +1,354 @@ +package com.pspdfkit.react + +import android.os.Bundle +import android.graphics.PointF +import com.facebook.react.bridge.Arguments +import com.facebook.react.bridge.ReactContext +import com.facebook.react.bridge.WritableMap +import com.pspdfkit.PSPDFKit +import com.pspdfkit.analytics.AnalyticsClient +import com.pspdfkit.annotations.Annotation +import com.pspdfkit.forms.ChoiceFormElement +import com.pspdfkit.forms.ComboBoxFormElement +import com.pspdfkit.forms.EditableButtonFormElement +import com.pspdfkit.forms.FormElement +import com.pspdfkit.forms.FormField +import com.pspdfkit.forms.TextFormElement +import com.pspdfkit.react.helper.JsonUtilities +import org.json.JSONObject + +class CustomAnalyticsClient: AnalyticsClient { + override fun onEvent(name: String, data: Bundle?) { + NutrientNotificationCenter.analyticsReceived(name, data) + } +} + +enum class NotificationEvent(val value: String) { + DOCUMENT_LOADED("documentLoaded"), + DOCUMENT_LOAD_FAILED("documentLoadFailed"), + DOCUMENT_PAGE_CHANGED("documentPageChanged"), + ANNOTATIONS_ADDED("annotationsAdded"), + ANNOTATION_CHANGED("annotationChanged"), + ANNOTATIONS_REMOVED("annotationsRemoved"), + ANNOTATIONS_SELECTED("annotationsSelected"), + ANNOTATIONS_DESELECTED("annotationsDeselected"), + ANNOTATION_TAPPED("annotationTapped"), + TEXT_SELECTED("textSelected"), + FORM_FIELD_VALUES_UPDATED("formFieldValuesUpdated"), + FORM_FIELD_SELECTED("formFieldSelected"), + FORM_FIELD_DESELECTED("formFieldDeselected"), + ANALYTICS("analytics"); +} + +object NutrientNotificationCenter { + private var internalReactContext: ReactContext? = null + private var isInUse = false + private var customAnalyticsClient = CustomAnalyticsClient() + + fun setReactContext(ctx: ReactContext) { + internalReactContext = ctx + } + + fun setIsNotificationCenterInUse(inUse: Boolean) { + isInUse = inUse + } + + fun getIsNotificationCenterInUse(): Boolean { + return isInUse + } + + private fun sendEvent( + eventName: String, + params: WritableMap + ) { + internalReactContext + ?.getJSModule(ReactContext.RCTDeviceEventEmitter::class.java) + ?.emit(eventName, params) + } + + fun documentLoaded(documentID: String) { + val jsonData = Arguments.createMap() + jsonData.putString("event", NotificationEvent.DOCUMENT_LOADED.value) + jsonData.putString("documentID", documentID) + sendEvent(NotificationEvent.DOCUMENT_LOADED.value, jsonData) + } + + fun documentLoadFailed() { + val jsonData = Arguments.createMap() + jsonData.putString("event", NotificationEvent.DOCUMENT_LOAD_FAILED.value) + sendEvent(NotificationEvent.DOCUMENT_LOAD_FAILED.value, jsonData) + } + + fun documentPageChanged(pageIndex: Int, documentID: String) { + val jsonData = Arguments.createMap() + jsonData.putString("event", NotificationEvent.DOCUMENT_PAGE_CHANGED.value) + jsonData.putInt("pageIndex", pageIndex) + jsonData.putString("documentID", documentID) + sendEvent(NotificationEvent.DOCUMENT_PAGE_CHANGED.value, jsonData) + } + + fun annotationsChanged(changeType: String, annotation: Annotation, documentID: String) { + when (changeType) { + "changed" -> { + try { + val instantJson = JSONObject(annotation.toInstantJson()) + val annotationsList = mutableListOf>() + val annotationMap = JsonUtilities.jsonObjectToMap(instantJson) + annotationMap["uuid"] = annotation.uuid + annotationsList.add(annotationMap) + val nativeAnnotationsList = Arguments.makeNativeArray(annotationsList) + + val jsonData = Arguments.createMap() + jsonData.putString("event", NotificationEvent.ANNOTATION_CHANGED.value) + jsonData.putArray("annotations", nativeAnnotationsList) + jsonData.putString("documentID", documentID) + sendEvent(NotificationEvent.ANNOTATION_CHANGED.value, jsonData) + } catch (e: Exception) { + // Could not decode annotation data + } + } + "removed" -> { + val annotationsList = mutableListOf>() + val annotationMap = HashMap() + annotation.name?.let { annotationMap["name"] = it } + annotation.creator?.let { annotationMap["creatorName"] = it } + annotationMap["uuid"] = annotation.uuid + annotationsList.add(annotationMap) + val nativeAnnotationsList = Arguments.makeNativeArray(annotationsList) + + val jsonData = Arguments.createMap() + jsonData.putString("event", NotificationEvent.ANNOTATIONS_REMOVED.value) + jsonData.putArray("annotations", nativeAnnotationsList) + jsonData.putString("documentID", documentID) + sendEvent(NotificationEvent.ANNOTATION_CHANGED.value, jsonData) + } + "added" -> { + try { + val instantJson = JSONObject(annotation.toInstantJson()) + val annotationsList = mutableListOf>() + val annotationMap = JsonUtilities.jsonObjectToMap(instantJson) + annotationMap["uuid"] = annotation.uuid + annotationsList.add(annotationMap) + val nativeAnnotationsList = Arguments.makeNativeArray(annotationsList) + + val jsonData = Arguments.createMap() + jsonData.putString("event", NotificationEvent.ANNOTATIONS_ADDED.value) + jsonData.putArray("annotations", nativeAnnotationsList) + jsonData.putString("documentID", documentID) + sendEvent(NotificationEvent.ANNOTATIONS_ADDED.value, jsonData) + } catch (e: Exception) { + // Could not decode annotation data + } + } + } + } + + fun didSelectAnnotations(annotation: Annotation, documentID: String) { + try { + val instantJson = JSONObject(annotation.toInstantJson()) + val annotationsList = mutableListOf>() + val annotationMap = JsonUtilities.jsonObjectToMap(instantJson) + annotationMap["uuid"] = annotation.uuid + annotationsList.add(annotationMap) + val nativeAnnotationsList = Arguments.makeNativeArray(annotationsList) + + val jsonData = Arguments.createMap() + jsonData.putString("event", NotificationEvent.ANNOTATIONS_SELECTED.value) + jsonData.putArray("annotations", nativeAnnotationsList) + jsonData.putString("documentID", documentID) + sendEvent(NotificationEvent.ANNOTATIONS_SELECTED.value, jsonData) + } catch (e: Exception) { + // Could not decode annotation data + } + } + + fun didDeselectAnnotations(annotation: Annotation, documentID: String) { + try { + val instantJson = JSONObject(annotation.toInstantJson()) + val annotationsList = mutableListOf>() + val annotationMap = JsonUtilities.jsonObjectToMap(instantJson) + annotationMap["uuid"] = annotation.uuid + annotationsList.add(annotationMap) + val nativeAnnotationsList = Arguments.makeNativeArray(annotationsList) + + val jsonData = Arguments.createMap() + jsonData.putString("event", NotificationEvent.ANNOTATIONS_DESELECTED.value) + jsonData.putArray("annotations", nativeAnnotationsList) + jsonData.putString("documentID", documentID) + sendEvent(NotificationEvent.ANNOTATIONS_DESELECTED.value, jsonData) + } catch (e: Exception) { + // Could not decode annotation data + } + } + + fun didTapAnnotation(annotation: Annotation, pointF: PointF, documentID: String) { + try { + val instantJson = JSONObject(annotation.toInstantJson()) + val annotationMap = JsonUtilities.jsonObjectToMap(instantJson) + annotationMap["uuid"] = annotation.uuid + val nativeAnnotationMap = Arguments.makeNativeMap(annotationMap) + + val pointMap = mapOf("x" to pointF.x, "y" to pointF.y) + val nativePointMap = Arguments.makeNativeMap(pointMap) + + val jsonData = Arguments.createMap() + jsonData.putString("event", NotificationEvent.ANNOTATION_TAPPED.value) + jsonData.putMap("annotation", nativeAnnotationMap) + jsonData.putMap("annotationPoint", nativePointMap) + jsonData.putString("documentID", documentID) + sendEvent(NotificationEvent.ANNOTATION_TAPPED.value, jsonData) + } catch (e: Exception) { + // Could not decode annotation data + } + } + + fun didSelectText(text: String, documentID: String) { + val jsonData = Arguments.createMap() + jsonData.putString("event", NotificationEvent.TEXT_SELECTED.value) + jsonData.putString("text", text) + jsonData.putString("documentID", documentID) + sendEvent(NotificationEvent.TEXT_SELECTED.value, jsonData) + } + + fun formFieldValuesUpdated(formField: FormField, documentID: String) { + try { + val annotation = formField.formElement.annotation + val instantJson = JSONObject(annotation.toInstantJson()) + val annotationsList = mutableListOf>() + val annotationMap = JsonUtilities.jsonObjectToMap(instantJson) + annotationMap["uuid"] = annotation.uuid + + (formField.formElement as? TextFormElement).let { textFormElement -> + if (textFormElement != null) { + annotationMap["value"] = textFormElement.text + } + } + (formField.formElement as? EditableButtonFormElement).let { buttonFormElement -> + if (buttonFormElement != null) { + annotationMap["value"] = if (buttonFormElement.isSelected) "selected" else "deselected" + } + } + (formField.formElement as? ComboBoxFormElement).let { comboBoxFormElement -> + if (comboBoxFormElement != null) { + annotationMap["value"] = if (comboBoxFormElement.isCustomTextSet) comboBoxFormElement.customText else comboBoxFormElement.selectedIndexes + } + } + (formField.formElement as? ChoiceFormElement).let { choiceFormElement -> + if (choiceFormElement != null) { + annotationMap["value"] = choiceFormElement.selectedIndexes + } + } + + annotationsList.add(annotationMap) + val nativeAnnotationsList = Arguments.makeNativeArray(annotationsList) + + val jsonData = Arguments.createMap() + jsonData.putString("event", NotificationEvent.FORM_FIELD_VALUES_UPDATED.value) + jsonData.putArray("annotations", nativeAnnotationsList) + jsonData.putString("documentID", documentID) + sendEvent(NotificationEvent.FORM_FIELD_VALUES_UPDATED.value, jsonData) + } catch (e: Exception) { + // Could not decode annotation data + } + } + + fun didSelectFormField(formElement: FormElement, documentID: String) { + try { + val annotation = formElement.annotation + val instantJson = JSONObject(annotation.toInstantJson()) + val annotationMap = JsonUtilities.jsonObjectToMap(instantJson) + annotationMap["uuid"] = annotation.uuid + + (formElement as? TextFormElement).let { textFormElement -> + if (textFormElement != null) { + annotationMap["value"] = textFormElement.text + } + } + (formElement as? EditableButtonFormElement).let { buttonFormElement -> + if (buttonFormElement != null) { + annotationMap["value"] = if (buttonFormElement.isSelected) "selected" else "deselected" + } + } + (formElement as? ComboBoxFormElement).let { comboBoxFormElement -> + if (comboBoxFormElement != null) { + annotationMap["value"] = if (comboBoxFormElement.isCustomTextSet) comboBoxFormElement.customText else comboBoxFormElement.selectedIndexes + } + } + (formElement as? ChoiceFormElement).let { choiceFormElement -> + if (choiceFormElement != null) { + annotationMap["value"] = choiceFormElement.selectedIndexes + } + } + + val nativeAnnotationMap = Arguments.makeNativeMap(annotationMap) + val jsonData = Arguments.createMap() + jsonData.putString("event", NotificationEvent.FORM_FIELD_SELECTED.value) + jsonData.putMap("annotation", nativeAnnotationMap) + jsonData.putString("documentID", documentID) + sendEvent(NotificationEvent.FORM_FIELD_SELECTED.value, jsonData) + } catch (e: Exception) { + // Could not decode annotation data + } + } + + fun didDeSelectFormField(formElement: FormElement, documentID: String) { + try { + val annotation = formElement.annotation + val instantJson = JSONObject(annotation.toInstantJson()) + val annotationMap = JsonUtilities.jsonObjectToMap(instantJson) + annotationMap["uuid"] = annotation.uuid + + (formElement as? TextFormElement).let { textFormElement -> + if (textFormElement != null) { + annotationMap["value"] = textFormElement.text + } + } + (formElement as? EditableButtonFormElement).let { buttonFormElement -> + if (buttonFormElement != null) { + annotationMap["value"] = if (buttonFormElement.isSelected) "selected" else "deselected" + } + } + (formElement as? ComboBoxFormElement).let { comboBoxFormElement -> + if (comboBoxFormElement != null) { + annotationMap["value"] = if (comboBoxFormElement.isCustomTextSet) comboBoxFormElement.customText else comboBoxFormElement.selectedIndexes + } + } + (formElement as? ChoiceFormElement).let { choiceFormElement -> + if (choiceFormElement != null) { + annotationMap["value"] = choiceFormElement.selectedIndexes + } + } + + val nativeAnnotationMap = Arguments.makeNativeMap(annotationMap) + val jsonData = Arguments.createMap() + jsonData.putString("event", NotificationEvent.FORM_FIELD_DESELECTED.value) + jsonData.putMap("annotation", nativeAnnotationMap) + jsonData.putString("documentID", documentID) + sendEvent(NotificationEvent.FORM_FIELD_DESELECTED.value, jsonData) + } catch (e: Exception) { + // Could not decode annotation data + } + } + + fun analyticsEnabled() { + PSPDFKit.addAnalyticsClient(customAnalyticsClient) + } + + fun analyticsDisabled() { + PSPDFKit.removeAnalyticsClient(customAnalyticsClient) + } + + fun analyticsReceived(event: String, attributes: Bundle?) { + val jsonData = Arguments.createMap() + val attributesMap = Arguments.createMap() + if (attributes != null) { + for (key in attributes.keySet()) { + attributesMap.putString(key, attributes.getString(key)) + } + } + jsonData.putString("analyticsEvent", event) + jsonData.putMap("attributes", attributesMap) + jsonData.putString("event", NotificationEvent.ANALYTICS.value) + sendEvent(NotificationEvent.ANALYTICS.value, jsonData) + } +} \ No newline at end of file diff --git a/android/src/main/java/com/pspdfkit/react/PDFDocumentModule.kt b/android/src/main/java/com/pspdfkit/react/PDFDocumentModule.kt index 708dd9f0..57a6cbfa 100644 --- a/android/src/main/java/com/pspdfkit/react/PDFDocumentModule.kt +++ b/android/src/main/java/com/pspdfkit/react/PDFDocumentModule.kt @@ -9,15 +9,17 @@ import com.facebook.react.bridge.ReactMethod import com.facebook.react.bridge.ReadableArray import com.facebook.react.bridge.ReadableMap import com.facebook.react.module.annotations.ReactModule +import com.pspdfkit.LicenseFeature +import com.pspdfkit.PSPDFKit import com.pspdfkit.annotations.Annotation import com.pspdfkit.annotations.AnnotationProvider.ALL_ANNOTATION_TYPES import com.pspdfkit.annotations.AnnotationType +import com.pspdfkit.document.ImageDocument import com.pspdfkit.document.PdfDocument import com.pspdfkit.document.formatters.DocumentJsonFormatter import com.pspdfkit.document.formatters.XfdfFormatter import com.pspdfkit.document.providers.ContentResolverDataProvider import com.pspdfkit.document.providers.DataProvider -import com.pspdfkit.internal.model.ImageDocumentImpl import com.pspdfkit.react.helper.ConversionHelpers.getAnnotationTypes import com.pspdfkit.react.helper.DocumentJsonDataProvider import com.pspdfkit.react.helper.JsonUtilities @@ -27,17 +29,19 @@ import org.json.JSONObject import java.io.ByteArrayOutputStream import java.util.EnumSet +data class DocumentData(val document: PdfDocument, var imageDocument: ImageDocument?) + @ReactModule(name = PDFDocumentModule.NAME) class PDFDocumentModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) { - private var documents = mutableMapOf() + private var documents = mutableMapOf() private var documentConfigurations = mutableMapOf>() override fun getName(): String { return NAME } - - private fun getDocument(reference: Int): PdfDocument? { + + private fun getDocument(reference: Int): DocumentData? { return this.documents[reference] } @@ -45,18 +49,29 @@ class PDFDocumentModule(reactContext: ReactApplicationContext) : ReactContextBas return this.documentConfigurations[reference] } - fun setDocument(document: PdfDocument, reference: Int) { - this.documents[reference] = document + fun setDocument(document: PdfDocument, imageDocument: ImageDocument?, reference: Int) { + val docData = DocumentData(document, imageDocument) + // If this document has already been set and contained an imageDocument, retain the imageDocument + this.getDocument(reference)?.imageDocument?.let { + if (docData.imageDocument == null) { + docData.imageDocument = it + } + } + this.documents[reference] = docData } fun updateDocumentConfiguration(key: String, value: Any, reference: Int) { - val currentConfiguration = documentConfigurations[reference] - currentConfiguration?.set(key, value) + var currentConfiguration = documentConfigurations[reference] + if (currentConfiguration == null) { + currentConfiguration = mutableMapOf() + } + currentConfiguration[key] = value + documentConfigurations[reference] = currentConfiguration } @ReactMethod fun getDocumentId(reference: Int, promise: Promise) { try { - promise.resolve(this.getDocument(reference)?.documentIdString) + promise.resolve(this.getDocument(reference)?.document?.documentIdString) } catch (e: Throwable) { promise.reject("getDocumentId error", e) } @@ -64,7 +79,7 @@ class PDFDocumentModule(reactContext: ReactApplicationContext) : ReactContextBas @ReactMethod fun invalidateCacheForPage(reference: Int, pageIndex: Int, promise: Promise) { try { - this.getDocument(reference)?.invalidateCacheForPage(pageIndex) + this.getDocument(reference)?.document?.invalidateCacheForPage(pageIndex) promise.resolve(true) } catch (e: Throwable) { promise.reject("invalidateCacheForPage error", e) @@ -73,7 +88,7 @@ class PDFDocumentModule(reactContext: ReactApplicationContext) : ReactContextBas @ReactMethod fun invalidateCache(reference: Int, promise: Promise) { try { - this.getDocument(reference)?.invalidateCache() + this.getDocument(reference)?.document?.invalidateCache() promise.resolve(true) } catch (e: Throwable) { promise.reject("invalidateCache error", e) @@ -82,16 +97,15 @@ class PDFDocumentModule(reactContext: ReactApplicationContext) : ReactContextBas @ReactMethod fun save(reference: Int, promise: Promise) { try { - this.getDocument(reference)?.let { - if (it is ImageDocumentImpl.ImagePdfDocumentWrapper) { - val metadata = this.getDocumentConfiguration(reference)?.get("imageSaveMode")?.equals("flattenAndEmbed") == true - if (it.imageDocument.saveIfModified(metadata)) { - promise.resolve(true) - } - } else { - it.saveIfModified() - promise.resolve(true) - } + this.getDocument(reference)?.imageDocument?.let { + val metadata = this.getDocumentConfiguration(reference)?.get("imageSaveMode")?.equals("flattenAndEmbed") == true + promise.resolve(it.saveIfModified(metadata)) + return + } + this.getDocument(reference)?.document?.let { + it.saveIfModified() + promise.resolve(true) + return } } catch (e: Throwable) { promise.reject("save error", e) @@ -100,7 +114,7 @@ class PDFDocumentModule(reactContext: ReactApplicationContext) : ReactContextBas @ReactMethod fun getAllUnsavedAnnotations(reference: Int, promise: Promise) { try { - this.getDocument(reference)?.let { + this.getDocument(reference)?.document?.let { val outputStream = ByteArrayOutputStream() DocumentJsonFormatter.exportDocumentJsonAsync(it, outputStream) .subscribeOn(Schedulers.io()) @@ -123,7 +137,7 @@ class PDFDocumentModule(reactContext: ReactApplicationContext) : ReactContextBas @ReactMethod fun getAnnotations(reference: Int, type: String?, promise: Promise) { try { - this.getDocument(reference)?.let { + this.getDocument(reference)?.document?.let { it.annotationProvider.getAllAnnotationsOfTypeAsync(if (type == null) ALL_ANNOTATION_TYPES else getAnnotationTypes(Arguments.makeNativeArray(arrayOf(type)))) .toList() .subscribeOn(Schedulers.io()) @@ -154,7 +168,7 @@ class PDFDocumentModule(reactContext: ReactApplicationContext) : ReactContextBas @ReactMethod fun getAnnotationsForPage(reference: Int, pageIndex: Int, type: String?, promise: Promise) { try { - this.getDocument(reference)?.let { + this.getDocument(reference)?.document?.let { if (pageIndex > it.pageCount-1) { promise.reject(RuntimeException("Specified page index is out of bounds")) @@ -192,7 +206,7 @@ class PDFDocumentModule(reactContext: ReactApplicationContext) : ReactContextBas @ReactMethod fun removeAnnotations(reference: Int, instantJSON: ReadableArray, promise: Promise) { try { - this.getDocument(reference)?.let { + this.getDocument(reference)?.document?.let { val instantJSONArray: List> = instantJSON.toArrayList().filterIsInstance>() var annotationsToDelete: ArrayList = ArrayList() @@ -227,7 +241,7 @@ class PDFDocumentModule(reactContext: ReactApplicationContext) : ReactContextBas @ReactMethod fun addAnnotations(reference: Int, instantJSON: ReadableMap, promise: Promise) { try { - this.getDocument(reference)?.let { + this.getDocument(reference)?.document?.let { val json = JSONObject(instantJSON.toHashMap() as Map<*, *>?) val dataProvider: DataProvider = DocumentJsonDataProvider(json) DocumentJsonFormatter.importDocumentJsonAsync(it, dataProvider) @@ -246,7 +260,7 @@ class PDFDocumentModule(reactContext: ReactApplicationContext) : ReactContextBas @ReactMethod fun importXFDF(reference: Int, filePath: String, promise: Promise) { try { - this.getDocument(reference)?.let { + this.getDocument(reference)?.document?.let { var importPath = filePath; if (Uri.parse(importPath).scheme == null) { importPath = "file:///$filePath"; @@ -276,7 +290,7 @@ class PDFDocumentModule(reactContext: ReactApplicationContext) : ReactContextBas @ReactMethod fun exportXFDF(reference: Int, filePath: String, promise: Promise) { try { - this.getDocument(reference)?.let { + this.getDocument(reference)?.document?.let { var exportPath = filePath; if (Uri.parse(exportPath).scheme == null) { exportPath = "file:///$filePath"; @@ -289,17 +303,16 @@ class PDFDocumentModule(reactContext: ReactApplicationContext) : ReactContextBas } val allAnnotations = it.annotationProvider.getAllAnnotationsOfType(ALL_ANNOTATION_TYPES) - val allFormFields = it.formProvider.formFields + var allFormFields: List = emptyList() + if (PSPDFKit.getLicenseFeatures().contains(LicenseFeature.FORMS)) { + allFormFields = it.formProvider.formFields + } XfdfFormatter.writeXfdfAsync(it, allAnnotations, allFormFields, outputStream) - XfdfFormatter.parseXfdfAsync(it, ContentResolverDataProvider((Uri.parse(exportPath)))) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe( - { annotations -> - for (annotation in annotations) { - it.annotationProvider.addAnnotationToPage(annotation) - } + { val result = JSONObject() result.put("success", true) result.put("filePath", filePath) diff --git a/android/src/main/java/com/pspdfkit/react/PSPDFKitModule.java b/android/src/main/java/com/pspdfkit/react/PSPDFKitModule.java index 2adc0b82..5b12bf06 100644 --- a/android/src/main/java/com/pspdfkit/react/PSPDFKitModule.java +++ b/android/src/main/java/com/pspdfkit/react/PSPDFKitModule.java @@ -20,9 +20,11 @@ import android.os.Bundle; import android.os.Handler; import android.os.Looper; +import android.view.View; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.appcompat.app.ActionBar; import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentActivity; @@ -47,12 +49,16 @@ import com.pspdfkit.react.helper.PSPDFKitUtils; import com.pspdfkit.ui.PdfActivity; import com.pspdfkit.ui.PdfFragment; +import com.pspdfkit.ui.search.PdfSearchView; +import com.pspdfkit.ui.search.PdfSearchViewInline; +import com.pspdfkit.views.ReactMainToolbar; import java.io.File; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashMap; import java.util.Map; +import java.util.Objects; public class PSPDFKitModule extends ReactContextBaseJavaModule implements Application.ActivityLifecycleCallbacks, ActivityEventListener { @@ -89,6 +95,7 @@ public PSPDFKitModule(ReactApplicationContext reactContext) { public void initialize() { super.initialize(); getReactApplicationContext().addActivityEventListener(this); + NutrientNotificationCenter.INSTANCE.setReactContext(getReactApplicationContext()); } @Override @@ -96,6 +103,16 @@ public String getName() { return "PSPDFKit"; } + @ReactMethod + public void addListener(String eventName) { + // Required to support NativeEventEmitter + } + + @ReactMethod + public void removeListeners(Integer count) { + // Required to support NativeEventEmitter + } + @ReactMethod public void present(@NonNull String document, @NonNull ReadableMap configuration, @Nullable Promise promise) { if(PSPDFKitUtils.isValidPdf(document)) { @@ -290,6 +307,30 @@ public void processAnnotations(@NonNull final String processingMode, } } + @ReactMethod + public void handleListenerAdded(String event, @Nullable Promise promise) { + NutrientNotificationCenter.INSTANCE.setIsNotificationCenterInUse(true); + if (event.equals("analytics")) { + NutrientNotificationCenter.INSTANCE.analyticsEnabled(); + } + if (promise != null) { + promise.resolve(1); + } + } + + @ReactMethod + public void handleListenerRemoved(@Nullable String event, boolean isLast, @Nullable Promise promise) { + if (isLast) { + NutrientNotificationCenter.INSTANCE.setIsNotificationCenterInUse(false); + } + if (event.equals("analytics")) { + NutrientNotificationCenter.INSTANCE.analyticsDisabled(); + } + if (promise != null) { + promise.resolve(1); + } + } + private static PdfProcessorTask.AnnotationProcessingMode getProcessingModeFromString(@NonNull final String mode) { if ("print".equalsIgnoreCase(mode)) { return PdfProcessorTask.AnnotationProcessingMode.PRINT; @@ -325,14 +366,30 @@ public void onActivityStarted(Activity activity) { @Override public synchronized void onActivityResumed(Activity activity) { resumedActivity = activity; - if (resumedActivity instanceof PdfActivity && onPdfActivityOpenedTask != null) { - // Run our queued up task when a PdfActivity is displayed. - onPdfActivityOpenedTask.run(); - onPdfActivityOpenedTask = null; + if (resumedActivity instanceof PdfActivity pdfActivity) { + if (onPdfActivityOpenedTask != null) { + // Run our queued up task when a PdfActivity is displayed. + onPdfActivityOpenedTask.run(); + onPdfActivityOpenedTask = null; + } + + try { + ActionBar ab = pdfActivity.getSupportActionBar(); + ab.setDisplayHomeAsUpEnabled(true); + ReactMainToolbar mainToolbar = pdfActivity.findViewById(R.id.pspdf__toolbar_main); + mainToolbar.setNavigationOnClickListener(v -> { + pdfActivity.onBackPressed(); + }); + PdfSearchView searchView = pdfActivity.getPSPDFKitViews().getSearchView(); + if (searchView instanceof PdfSearchViewInline searchViewInline) { + searchViewInline.findViewById(com.pspdfkit.R.id.pspdf__search_btn_back).setVisibility(View.GONE); + } + } catch (Exception e) { + // Could not add back button to main toolbar + } // We notify the called as soon as the document is loaded or loading failed. if (lastPresentPromise != null) { - PdfActivity pdfActivity = (PdfActivity) resumedActivity; pdfActivity.getPdfFragment().addDocumentListener(new SimpleDocumentListener() { @Override public void onDocumentLoaded(@NonNull PdfDocument document) { diff --git a/android/src/main/java/com/pspdfkit/views/PdfView.java b/android/src/main/java/com/pspdfkit/views/PdfView.java index 7c650dc0..31fce6df 100644 --- a/android/src/main/java/com/pspdfkit/views/PdfView.java +++ b/android/src/main/java/com/pspdfkit/views/PdfView.java @@ -44,14 +44,16 @@ import com.facebook.react.bridge.ReadableMap; import com.facebook.react.common.MapBuilder; import com.facebook.react.uimanager.events.EventDispatcher; +import com.pspdfkit.LicenseFeature; import com.pspdfkit.PSPDFKit; import com.pspdfkit.annotations.Annotation; import com.pspdfkit.annotations.AnnotationFlags; -import com.pspdfkit.annotations.AnnotationProvider; import com.pspdfkit.annotations.AnnotationType; import com.pspdfkit.annotations.configuration.FreeTextAnnotationConfiguration; import com.pspdfkit.configuration.activity.PdfActivityConfiguration; +import com.pspdfkit.configuration.search.SearchType; import com.pspdfkit.configuration.sharing.ShareFeatures; +import com.pspdfkit.document.DocumentSource; import com.pspdfkit.document.ImageDocumentLoader; import com.pspdfkit.document.PdfDocument; import com.pspdfkit.document.PdfDocumentLoader; @@ -65,7 +67,6 @@ import com.pspdfkit.forms.EditableButtonFormElement; import com.pspdfkit.forms.FormField; import com.pspdfkit.forms.TextFormElement; -import com.pspdfkit.internal.model.ImageDocumentImpl; import com.pspdfkit.listeners.OnVisibilityChangedListener; import com.pspdfkit.listeners.SimpleDocumentListener; import com.pspdfkit.react.PDFDocumentModule; @@ -359,7 +360,7 @@ public void setDocument(@Nullable String documentPath, ReactApplicationContext r .observeOn(AndroidSchedulers.mainThread()) .subscribe(pdfDocument -> { PdfView.this.document = pdfDocument; - reactApplicationContext.getNativeModule(PDFDocumentModule.class).setDocument(pdfDocument, this.getId()); + reactApplicationContext.getNativeModule(PDFDocumentModule.class).setDocument(pdfDocument, null, this.getId()); reactApplicationContext.getNativeModule(PDFDocumentModule.class).updateDocumentConfiguration("imageSaveMode", imageSaveMode, this.getId()); setupFragment(false); }, throwable -> { @@ -375,12 +376,12 @@ public void setDocument(@Nullable String documentPath, ReactApplicationContext r }); } else { if (PSPDFKitUtils.isValidImage(documentPath)) { - documentOpeningDisposable = ImageDocumentLoader.openDocumentAsync(getContext(), Uri.parse(documentPath)) + documentOpeningDisposable = ImageDocumentLoader.openDocumentAsync(getContext(), new DocumentSource(Uri.parse(documentPath))) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(imageDocument -> { PdfView.this.document = imageDocument.getDocument(); - reactApplicationContext.getNativeModule(PDFDocumentModule.class).setDocument(imageDocument.getDocument(), this.getId()); + reactApplicationContext.getNativeModule(PDFDocumentModule.class).setDocument(imageDocument.getDocument(), imageDocument, this.getId()); reactApplicationContext.getNativeModule(PDFDocumentModule.class).updateDocumentConfiguration("imageSaveMode", imageSaveMode, this.getId()); setupFragment(false); }, throwable -> { @@ -394,7 +395,7 @@ public void setDocument(@Nullable String documentPath, ReactApplicationContext r .observeOn(AndroidSchedulers.mainThread()) .subscribe(pdfDocument -> { PdfView.this.document = pdfDocument; - reactApplicationContext.getNativeModule(PDFDocumentModule.class).setDocument(pdfDocument, this.getId()); + reactApplicationContext.getNativeModule(PDFDocumentModule.class).setDocument(pdfDocument, null, this.getId()); reactApplicationContext.getNativeModule(PDFDocumentModule.class).updateDocumentConfiguration("imageSaveMode", imageSaveMode, this.getId()); setupFragment(false); }, throwable -> { @@ -681,7 +682,7 @@ private void preparePdfFragment(@NonNull PdfFragment pdfFragment) { @Override public void onDocumentLoaded(@NonNull PdfDocument document) { if (reactApplicationContext != null) { - reactApplicationContext.getNativeModule(PDFDocumentModule.class).setDocument(document, getId()); + reactApplicationContext.getNativeModule(PDFDocumentModule.class).setDocument(document, null, getId()); } manuallyLayoutChildren(); if (pageIndex <= document.getPageCount()-1) { @@ -692,8 +693,12 @@ public void onDocumentLoaded(@NonNull PdfDocument document) { }); pdfFragment.addOnTextSelectionModeChangeListener(pdfViewModeController); + pdfFragment.addOnTextSelectionChangeListener(pdfViewModeController); pdfFragment.addDocumentListener(pdfViewDocumentListener); + pdfFragment.addOnFormElementSelectedListener(pdfViewDocumentListener); + pdfFragment.addOnFormElementDeselectedListener(pdfViewDocumentListener); pdfFragment.addOnAnnotationSelectedListener(pdfViewDocumentListener); + pdfFragment.addOnAnnotationDeselectedListener(pdfViewDocumentListener); pdfFragment.addOnAnnotationUpdatedListener(pdfViewDocumentListener); if (pdfFragment.getDocument() != null) { pdfFragment.getDocument().getFormProvider().addOnFormFieldUpdatedListener(pdfViewDocumentListener); @@ -856,15 +861,14 @@ public Disposable selectAnnotations(final int requestId, ReadableArray jsonAnnot public boolean saveCurrentDocument() throws Exception { if (fragment != null) { try { - if (fragment.getDocument() instanceof ImageDocumentImpl.ImagePdfDocumentWrapper) { - boolean metadata = this.imageSaveMode.equals("flattenAndEmbed") ? true : false; - if (((ImageDocumentImpl.ImagePdfDocumentWrapper) fragment.getDocument()).getImageDocument().saveIfModified(metadata)) { + if (fragment.getPdfFragment() != null && fragment.getPdfFragment().getImageDocument() != null) { + boolean metadata = this.imageSaveMode.equals("flattenAndEmbed"); + if ((fragment.getPdfFragment().getImageDocument().saveIfModified(metadata))) { // Since the document listeners won't be called when manually saving we also dispatch this event here. eventDispatcher.dispatchEvent(new PdfViewDocumentSavedEvent(getId())); return true; } - } - else { + } else { if (fragment.getDocument().saveIfModified()) { // Since the document listeners won't be called when manually saving we also dispatch this event here. eventDispatcher.dispatchEvent(new PdfViewDocumentSavedEvent(getId())); @@ -1146,7 +1150,11 @@ public Disposable exportXFDF(final int requestId, String filePath) { if (outputStream == null) return null; List annotations = fragment.getDocument().getAnnotationProvider().getAllAnnotationsOfType(ALL_ANNOTATION_TYPES); - List formFields = fragment.getDocument().getFormProvider().getFormFields(); + + List formFields = Collections.emptyList(); + if (PSPDFKit.getLicenseFeatures().contains(LicenseFeature.FORMS)) { + formFields = fragment.getDocument().getFormProvider().getFormFields(); + } String finalFilePath = filePath; return XfdfFormatter.writeXfdfAsync(fragment.getDocument(), annotations, formFields, outputStream) @@ -1186,7 +1194,7 @@ public JSONObject convertConfiguration() { config.put("androidGrayScale", configuration.getConfiguration().isToGrayscale()); config.put("userInterfaceViewMode", ConfigurationAdapter.getStringValueForConfigurationItem(configuration.getUserInterfaceViewMode())); - config.put("inlineSearch", configuration.getSearchType() == PdfActivityConfiguration.SEARCH_INLINE ? true : false); + config.put("inlineSearch", configuration.getSearchType() == SearchType.INLINE ? true : false); config.put("immersiveMode", configuration.isImmersiveMode()); config.put("toolbarTitle", configuration.getActivityTitle()); config.put("androidShowSearchAction", configuration.isSearchEnabled()); diff --git a/android/src/main/java/com/pspdfkit/views/PdfViewDocumentListener.java b/android/src/main/java/com/pspdfkit/views/PdfViewDocumentListener.java index 8d612dd3..57c51b4f 100644 --- a/android/src/main/java/com/pspdfkit/views/PdfViewDocumentListener.java +++ b/android/src/main/java/com/pspdfkit/views/PdfViewDocumentListener.java @@ -13,6 +13,7 @@ package com.pspdfkit.views; +import android.annotation.SuppressLint; import android.graphics.PointF; import android.view.MotionEvent; @@ -28,6 +29,7 @@ import com.pspdfkit.forms.FormField; import com.pspdfkit.forms.FormListeners; import com.pspdfkit.listeners.DocumentListener; +import com.pspdfkit.react.NutrientNotificationCenter; import com.pspdfkit.react.events.PdfViewAnnotationChangedEvent; import com.pspdfkit.react.events.PdfViewAnnotationTappedEvent; import com.pspdfkit.react.events.PdfViewDocumentLoadedEvent; @@ -35,10 +37,11 @@ import com.pspdfkit.react.events.PdfViewDocumentSavedEvent; import com.pspdfkit.ui.special_mode.controller.AnnotationSelectionController; import com.pspdfkit.ui.special_mode.manager.AnnotationManager; +import com.pspdfkit.ui.special_mode.manager.FormManager; import java.util.List; -class PdfViewDocumentListener implements DocumentListener, AnnotationManager.OnAnnotationSelectedListener, AnnotationProvider.OnAnnotationUpdatedListener, FormListeners.OnFormFieldUpdatedListener { +class PdfViewDocumentListener implements DocumentListener, AnnotationManager.OnAnnotationSelectedListener, AnnotationManager.OnAnnotationDeselectedListener, AnnotationProvider.OnAnnotationUpdatedListener, FormListeners.OnFormFieldUpdatedListener, FormManager.OnFormElementSelectedListener, FormManager.OnFormElementDeselectedListener { @NonNull private final PdfView parent; @@ -64,12 +67,13 @@ public void setDisableAutomaticSaving(boolean disableAutomaticSaving) { @Override public void onDocumentLoaded(@NonNull PdfDocument pdfDocument) { + NutrientNotificationCenter.INSTANCE.documentLoaded(pdfDocument.getDocumentIdString()); eventDispatcher.dispatchEvent(new PdfViewDocumentLoadedEvent(parent.getId())); } @Override public void onDocumentLoadFailed(@NonNull Throwable throwable) { - + NutrientNotificationCenter.INSTANCE.documentLoadFailed(); } @Override @@ -95,6 +99,12 @@ public void onDocumentSaveCancelled(PdfDocument pdfDocument) { @Override public boolean onPageClick(@NonNull PdfDocument pdfDocument, int pageIndex, @Nullable MotionEvent motionEvent, @Nullable PointF pointF, @Nullable Annotation annotation) { if (annotation != null) { + if (NutrientNotificationCenter.INSTANCE.getIsNotificationCenterInUse()) { + parent.getPdfFragment().subscribe(pdfFragment -> { + String documentID = pdfFragment.getDocument().getDocumentIdString(); + NutrientNotificationCenter.INSTANCE.didTapAnnotation(annotation, pointF, documentID); + }); + } eventDispatcher.dispatchEvent(new PdfViewAnnotationTappedEvent(parent.getId(), annotation)); } return false; @@ -105,9 +115,16 @@ public boolean onDocumentClick() { return false; } + @SuppressLint("CheckResult") @Override public void onPageChanged(@NonNull PdfDocument pdfDocument, int pageIndex) { parent.updateState(pageIndex); + if (NutrientNotificationCenter.INSTANCE.getIsNotificationCenterInUse()) { + parent.getPdfFragment().subscribe(pdfFragment -> { + String documentID = pdfFragment.getDocument().getDocumentIdString(); + NutrientNotificationCenter.INSTANCE.documentPageChanged(pageIndex, documentID); + }); + } } @Override @@ -125,22 +142,50 @@ public boolean onPrepareAnnotationSelection(@NonNull AnnotationSelectionControll return !disableDefaultActionForTappedAnnotations; } + @SuppressLint("CheckResult") @Override public void onAnnotationSelected(@NonNull Annotation annotation, boolean annotationCreated) { + if (NutrientNotificationCenter.INSTANCE.getIsNotificationCenterInUse()) { + parent.getPdfFragment().subscribe(pdfFragment -> { + String documentID = pdfFragment.getDocument().getDocumentIdString(); + NutrientNotificationCenter.INSTANCE.didSelectAnnotations(annotation, documentID); + }); + } } + @SuppressLint("CheckResult") @Override public void onAnnotationCreated(@NonNull Annotation annotation) { + if (NutrientNotificationCenter.INSTANCE.getIsNotificationCenterInUse()) { + parent.getPdfFragment().subscribe(pdfFragment -> { + String documentID = pdfFragment.getDocument().getDocumentIdString(); + NutrientNotificationCenter.INSTANCE.annotationsChanged("added", annotation, documentID); + }); + } eventDispatcher.dispatchEvent(new PdfViewAnnotationChangedEvent(parent.getId(), PdfViewAnnotationChangedEvent.EVENT_TYPE_ADDED, annotation)); } + @SuppressLint("CheckResult") @Override public void onAnnotationUpdated(@NonNull Annotation annotation) { + if (NutrientNotificationCenter.INSTANCE.getIsNotificationCenterInUse()) { + parent.getPdfFragment().subscribe(pdfFragment -> { + String documentID = pdfFragment.getDocument().getDocumentIdString(); + NutrientNotificationCenter.INSTANCE.annotationsChanged("changed", annotation, documentID); + }); + } eventDispatcher.dispatchEvent(new PdfViewAnnotationChangedEvent(parent.getId(), PdfViewAnnotationChangedEvent.EVENT_TYPE_CHANGED, annotation)); } + @SuppressLint("CheckResult") @Override public void onAnnotationRemoved(@NonNull Annotation annotation) { + if (NutrientNotificationCenter.INSTANCE.getIsNotificationCenterInUse()) { + parent.getPdfFragment().subscribe(pdfFragment -> { + String documentID = pdfFragment.getDocument().getDocumentIdString(); + NutrientNotificationCenter.INSTANCE.annotationsChanged("removed", annotation, documentID); + }); + } eventDispatcher.dispatchEvent(new PdfViewAnnotationChangedEvent(parent.getId(), PdfViewAnnotationChangedEvent.EVENT_TYPE_REMOVED, annotation)); } @@ -149,8 +194,15 @@ public void onAnnotationZOrderChanged(int i, @NonNull List list, @No // Not required. } + @SuppressLint("CheckResult") @Override public void onFormFieldUpdated(@NonNull FormField formField) { + if (NutrientNotificationCenter.INSTANCE.getIsNotificationCenterInUse()) { + parent.getPdfFragment().subscribe(pdfFragment -> { + String documentID = pdfFragment.getDocument().getDocumentIdString(); + NutrientNotificationCenter.INSTANCE.formFieldValuesUpdated(formField, documentID); + }); + } Annotation annotation = formField.getFormElement().getAnnotation(); if (annotation != null) { eventDispatcher.dispatchEvent(new PdfViewAnnotationChangedEvent(parent.getId(), PdfViewAnnotationChangedEvent.EVENT_TYPE_CHANGED, annotation)); @@ -161,4 +213,37 @@ public void onFormFieldUpdated(@NonNull FormField formField) { public void onFormFieldReset(@NonNull FormField formField, @NonNull FormElement formElement) { // Not used. } + + @SuppressLint("CheckResult") + @Override + public void onAnnotationDeselected(@NonNull Annotation annotation, boolean b) { + if (NutrientNotificationCenter.INSTANCE.getIsNotificationCenterInUse()) { + parent.getPdfFragment().subscribe(pdfFragment -> { + String documentID = pdfFragment.getDocument().getDocumentIdString(); + NutrientNotificationCenter.INSTANCE.didDeselectAnnotations(annotation, documentID); + }); + } + } + + @SuppressLint("CheckResult") + @Override + public void onFormElementSelected(@NonNull FormElement formElement) { + if (NutrientNotificationCenter.INSTANCE.getIsNotificationCenterInUse()) { + parent.getPdfFragment().subscribe(pdfFragment -> { + String documentID = pdfFragment.getDocument().getDocumentIdString(); + NutrientNotificationCenter.INSTANCE.didSelectFormField(formElement, documentID); + }); + } + } + + @SuppressLint("CheckResult") + @Override + public void onFormElementDeselected(@NonNull FormElement formElement, boolean b) { + if (NutrientNotificationCenter.INSTANCE.getIsNotificationCenterInUse()) { + parent.getPdfFragment().subscribe(pdfFragment -> { + String documentID = pdfFragment.getDocument().getDocumentIdString(); + NutrientNotificationCenter.INSTANCE.didDeSelectFormField(formElement, documentID); + }); + } + } } diff --git a/android/src/main/java/com/pspdfkit/views/PdfViewModeController.java b/android/src/main/java/com/pspdfkit/views/PdfViewModeController.java index 3526c4f2..aa7368fe 100644 --- a/android/src/main/java/com/pspdfkit/views/PdfViewModeController.java +++ b/android/src/main/java/com/pspdfkit/views/PdfViewModeController.java @@ -13,8 +13,12 @@ package com.pspdfkit.views; +import android.annotation.SuppressLint; + import androidx.annotation.NonNull; +import com.pspdfkit.datastructures.TextSelection; +import com.pspdfkit.react.NutrientNotificationCenter; import com.pspdfkit.react.menu.AnnotationContextualToolbarGroupingRule; import com.pspdfkit.react.menu.ContextualToolbarMenuItemConfig; import com.pspdfkit.ui.forms.FormEditingBar; @@ -36,7 +40,7 @@ * Keeps track of the currently active mode and handles updating the toolbar states. */ class PdfViewModeController implements - TextSelectionManager.OnTextSelectionModeChangeListener, + TextSelectionManager.OnTextSelectionModeChangeListener, TextSelectionManager.OnTextSelectionChangeListener, ToolbarCoordinatorLayout.OnContextualToolbarLifecycleListener, FormEditingBar.OnFormEditingBarLifecycleListener { private final PdfView parent; @@ -161,4 +165,22 @@ public void onRemoveFormEditingBar(@NonNull FormEditingBar formEditingBar) { parent.updateState(); } + + @Override + public boolean onBeforeTextSelectionChange(@androidx.annotation.Nullable TextSelection textSelection, @androidx.annotation.Nullable TextSelection textSelection1) { + return true; + } + + @SuppressLint("CheckResult") + @Override + public void onAfterTextSelectionChange(@androidx.annotation.Nullable TextSelection textSelection, @androidx.annotation.Nullable TextSelection textSelection1) { + if (textSelection != null && textSelection.text != null) { + if (NutrientNotificationCenter.INSTANCE.getIsNotificationCenterInUse()) { + parent.getPdfFragment().subscribe(pdfFragment -> { + String documentID = pdfFragment.getDocument().getDocumentIdString(); + NutrientNotificationCenter.INSTANCE.didSelectText(textSelection.text, documentID); + }); + } + } + } } diff --git a/documentation/jsdoc-layout.tmpl b/documentation/jsdoc-layout.tmpl index b1232f5a..32810097 100644 --- a/documentation/jsdoc-layout.tmpl +++ b/documentation/jsdoc-layout.tmpl @@ -45,8 +45,20 @@ + + + + + + +