E-Learning Example
Shows how to swap between documents and sync view state, annotations, and bookmarks.
/* * Copyright © 2020-2025 PSPDFKit GmbH. All rights reserved. * * The PSPDFKit Sample applications are licensed with a modified BSD license. * Please see License for details. This notice may not be removed from this file. */
package com.pspdfkit.catalog.examples.kotlin
import android.annotation.SuppressLintimport android.content.Contextimport android.net.Uriimport android.os.Bundleimport android.view.Menuimport android.view.MenuItemimport androidx.annotation.UiThreadimport com.pspdfkit.annotations.Annotationimport com.pspdfkit.annotations.AnnotationTypeimport com.pspdfkit.bookmarks.Bookmarkimport com.pspdfkit.catalog.Rimport com.pspdfkit.catalog.SdkExampleimport com.pspdfkit.catalog.tasks.ExtractAssetTask.extractimport com.pspdfkit.configuration.activity.PdfActivityConfigurationimport com.pspdfkit.configuration.page.PageLayoutModeimport com.pspdfkit.configuration.sharing.ShareFeaturesimport com.pspdfkit.document.PdfDocumentimport com.pspdfkit.preferences.PSPDFKitPreferencesimport com.pspdfkit.ui.DocumentDescriptorimport com.pspdfkit.ui.PdfActivityimport com.pspdfkit.ui.PdfActivityIntentBuilderimport com.pspdfkit.ui.special_mode.controller.AnnotationToolimport com.pspdfkit.utils.getSupportParcelableExtraimport io.reactivex.rxjava3.android.schedulers.AndroidSchedulersimport io.reactivex.rxjava3.core.BackpressureStrategyimport io.reactivex.rxjava3.core.Completableimport io.reactivex.rxjava3.core.Observableimport io.reactivex.rxjava3.disposables.Disposableimport io.reactivex.rxjava3.functions.Actionimport java.util.EnumSet
/** * This example shows how to swap between documents and sync view state, annotations, and bookmarks. */class ELearningExample(context: Context) : SdkExample(context, R.string.eLearningExampleTitle, R.string.eLearningExampleDescription) { override fun launchExample(context: Context, configuration: PdfActivityConfiguration.Builder) { // We'll disable redaction tool in annotation creation toolbar to prevent creation of redaction annotations. val annotationTools = mutableListOf(*AnnotationTool.values()) annotationTools.remove(AnnotationTool.REDACTION)
configuration // Turn off saving, so we have the clean original document every time the example is launched. .autosaveEnabled(false) // Use single page mode. .layoutMode(PageLayoutMode.SINGLE) // Disable all the menu items but annotations editing and bookmark list. .outlineEnabled(false) .documentInfoViewEnabled(false) .annotationListEnabled(false) .searchEnabled(false) .settingsMenuEnabled(false) .setEnabledShareFeatures(EnumSet.noneOf(ShareFeatures::class.java)) .printingEnabled(false) .thumbnailGridEnabled(false) .setRedactionUiEnabled(false) .enabledAnnotationTools(annotationTools)
// The annotation creator written into newly created annotations. If not set, or set to null // a dialog will normally be shown when creating an annotation, asking you to enter a name. // We are going to skip this part and set it as "John Doe" only if it was not yet set. if (!PSPDFKitPreferences.get(context).isAnnotationCreatorSet) { PSPDFKitPreferences.get(context).setAnnotationCreator("John Doe") } // Extracts the documents from the assets and loads the teacher version first. extract("Teacher.pdf", title, context) { teacherDocumentFile -> extract("Student.pdf", title, context) { studentDocumentFile -> val teacherDocumentUri = Uri.fromFile(teacherDocumentFile) val studentDocumentUri = Uri.fromFile(studentDocumentFile) val intent = PdfActivityIntentBuilder.fromUri(context, teacherDocumentUri) .configuration(configuration.build()) .activityClass(ELearningActivity::class) .build() intent.putExtra(ELearningActivity.STUDENT_URI_KEY, studentDocumentUri) intent.putExtra(ELearningActivity.TEACHER_URI_KEY, teacherDocumentUri) context.startActivity(intent) } } }}
class ELearningActivity : PdfActivity() {
/** List of annotations in instant JSON format that will be copied over the new document. */ private val serializedAnnotationsToTransfer: MutableList<String> = mutableListOf()
/** List of bookmarks that will be copied over the new document. */ private val bookmarksToTransfer: MutableList<Bookmark> = mutableListOf()
/** Student document URI. */ private lateinit var studentUri: Uri
/** Teacher document URI. */ private lateinit var teacherUri: Uri
/** Disposable in charge of switching the document. */ private var switchDocumentDisposable: Disposable? = null
/** Disposable in charge of loading the annotation to the new document. */ private var transferDataDisposable: Disposable? = null
/** Fragment state containing current page, zoom, and scroll. */ private var fragmentState: Bundle? = null
/** Flag to keep track of visibility of bookmark list view. */ private var isBookmarkListDisplayed: Boolean = false
/** * Create a button to switch between teacher and student documents. */ override fun onCreateOptionsMenu(menu: Menu): Boolean { super.onCreateOptionsMenu(menu) if (document == null || serializedAnnotationsToTransfer.isNotEmpty()) { return false } // Add a new switch button. val switchItem = menu.add(0, SWITCH_ITEM_ID, 0, "Switch") switchItem.setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM or MenuItem.SHOW_AS_ACTION_WITH_TEXT)
return true }
/** * Set the corresponding action for every button. */ override fun onOptionsItemSelected(item: MenuItem): Boolean { return when (item.itemId) { SWITCH_ITEM_ID -> { switchDocument() true } else -> { super.onOptionsItemSelected(item) } } }
override fun onPause() { switchDocumentDisposable.safelyDispose() transferDataDisposable.safelyDispose() super.onPause() }
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)
intent.getSupportParcelableExtra(STUDENT_URI_KEY, Uri::class.java)?.let { studentUri = it } intent.getSupportParcelableExtra(TEACHER_URI_KEY, Uri::class.java)?.let { teacherUri = it } }
@UiThread @SuppressLint("CheckResult") override fun onDocumentLoaded(document: PdfDocument) { // Take any serialized annotations that were stored upon switching, and add them to the loaded document. val addAnnotations = Observable.fromIterable(serializedAnnotationsToTransfer) .toFlowable(BackpressureStrategy.BUFFER) // Create an annotation from the instant JSON and add it to the document. .flatMapSingle(document.annotationProvider::createAnnotationFromInstantJsonAsync) // Once all annotations are transferred, we can clear the list. .doOnComplete(serializedAnnotationsToTransfer::clear) .ignoreElements()
// Take any bookmarks that were stored upon switching, and add them to the loaded document. val addBookmarks = Observable.fromIterable(bookmarksToTransfer) .flatMapCompletable(document.bookmarkProvider::addBookmarkAsync) // Once all bookmarks are transferred, we can clear the list. .doOnComplete(bookmarksToTransfer::clear)
// Iterate through the annotation list. transferDataDisposable = Completable .mergeArray( addAnnotations, addBookmarks ) .observeOn(AndroidSchedulers.mainThread()) .doOnComplete { // Restore page index, scroll and zoom. fragmentState?.let { requirePdfFragment().state = it fragmentState = null }
// Make sure the outline view shows the changes from transferring bookmarks and annotations. pspdfKitViews.outlineView?.setDocument(document, configuration.configuration)
// Restore bookmark list view visibility. if (isBookmarkListDisplayed) { pspdfKitViews.outlineView?.show() }
invalidateOptionsMenu() } .subscribe() }
/** * Switch the document when the button is tapped. This works in multiple steps: * - Fetching all annotations asynchronously, and convert them to Instant JSON format. * - Fetch all bookmarks. * - Save the view state. */ private fun switchDocument() { val document = document ?: return
// Fetch all annotations, serialize them, and collect them in a list. We keep them around in // memory, and will add them to the other document once it is loaded. val serializeAnnotations = document.annotationProvider .getAllAnnotationsOfTypeAsync(AnnotationType.entries.toSet()) .doOnSubscribe { serializedAnnotationsToTransfer.clear() } .map(Annotation::toInstantJson) // For some unsupported annotation types (like popup annotations) we don't offer serialization. // In these cases, `toInstantJson()` returns "null". We filter those from the serialized items. // those annotations that are not supported. .filter(::invalidInstantJson) // Collect all JSON annotations. .map(serializedAnnotationsToTransfer::add) .ignoreElements()
// Fetch the bookmarks for synchronization and collect them in a list. val fetchBookmarks = document.bookmarkProvider.bookmarksAsync .doOnSubscribe { bookmarksToTransfer.clear() } .map(bookmarksToTransfer::addAll) .ignoreElements()
// Run the collection of annotations and bookmarks asynchronously. switchDocumentDisposable = Completable .mergeArray( serializeAnnotations, fetchBookmarks ) // While the document switching is in progress, we disable all document interaction to // prevent changes to be made. .doOnSubscribe { requirePdfFragment().isDocumentInteractionEnabled = false } .observeOn(AndroidSchedulers.mainThread()) .doOnComplete { // Save current page index, scroll and zoom. fragmentState = requirePdfFragment().state
// Save bookmark list view visibility. isBookmarkListDisplayed = pspdfKitViews.outlineView?.isDisplayed ?: false
// Swap out the current document to eather the teacher or student version. if (document.documentSource.fileUri == teacherUri) { documentCoordinator.setDocument(DocumentDescriptor.fromUri(studentUri)) } else { documentCoordinator.setDocument(DocumentDescriptor.fromUri(teacherUri)) } invalidateOptionsMenu() } .subscribe() }
/** * If `disposable` is non-null, it will be disposed upon calling this method. If `disposable` is null, this method is a no-op. * This method will always return `null` which allows to null out any disposable reference in a single statement. If * the disposable is disposed `onDispose()` will be called. */ private fun Disposable?.safelyDispose(onDispose: Action? = null): Disposable? { if (this != null && !isDisposed) { dispose() onDispose?.run() } return null }
companion object { private const val SWITCH_ITEM_ID = 1234 const val STUDENT_URI_KEY = "TeacherStudentActivity.STUDENT_URI_KEY" const val TEACHER_URI_KEY = "TeacherStudentActivity.TEACHER_URI_KEY" }}
private fun invalidInstantJson(it: String) = it != "null"This code sample is an example that illustrates how to use our SDK. Please adapt it to your specific use case.