Persistent Tabs
Shows how to retain list of open tabs.
/* * Copyright © 2019-2026 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.app.Activityimport android.content.Contextimport android.content.Intentimport android.content.SharedPreferencesimport android.net.Uriimport android.os.Bundleimport android.widget.ImageViewimport androidx.core.content.editimport androidx.core.net.toUriimport com.pspdfkit.Nutrientimport com.pspdfkit.catalog.Rimport com.pspdfkit.catalog.SdkExampleimport com.pspdfkit.catalog.tasks.ExtractAssetTask.extractAsyncimport com.pspdfkit.catalog.utils.Utilsimport com.pspdfkit.configuration.activity.PdfActivityConfigurationimport com.pspdfkit.configuration.activity.TabBarHidingModeimport com.pspdfkit.document.ImageDocumentUtilsimport com.pspdfkit.document.download.DownloadJobimport com.pspdfkit.document.download.DownloadProgressFragmentimport com.pspdfkit.document.download.DownloadRequestimport com.pspdfkit.ui.DocumentCoordinatorimport com.pspdfkit.ui.DocumentDescriptorimport com.pspdfkit.ui.PdfActivityimport com.pspdfkit.ui.PdfActivityIntentBuilderimport io.reactivex.rxjava3.android.schedulers.AndroidSchedulersimport io.reactivex.rxjava3.core.Observableimport org.json.JSONArrayimport org.json.JSONObjectimport java.io.File
/** * This example shows how to persist list of opened documents/tabs between activity instances. */class PersistentTabsExample(context: Context) : SdkExample(context, R.string.persistentTabsExampleTitle, R.string.persistentTabsExampleDescription) {
@SuppressLint("CheckResult") override fun launchExample(context: Context, configuration: PdfActivityConfiguration.Builder) { configuration // Make the tab bar always visible. .setTabBarHidingMode(TabBarHidingMode.SHOW)
// We use shared preferences for storing tabs state in this example. // Preferences access is handled with TabsPreferences class. We don't val tabsPreferences = TabsPreferences.get(context) // Then retrieve document descriptors saved in the user's shared preferences. val restoredDocumentDescriptors = tabsPreferences.getDocumentDescriptors(context)
// If there are no document descriptors this this means that this example is running for // the first time. In this case, we'll extract a few documents from the app assets to be shown as tabs. if (restoredDocumentDescriptors == null) { val pdfFiles = listOf(WELCOME_DOC, "Aviation.pdf", "Annotations.pdf") val imageFiles = listOf("images/android.png")
val extractAssetsObservable = Observable.concat( Observable.fromIterable(pdfFiles) .flatMapSingle { assetName -> extractAsync(assetName, assetName, context, false, null) } // PdfActivity uses document descriptors to encapsulate all information required for opening a single document. // Create document descriptors for extracted files right away so we can pass them directly to PdfActivityIntentBuilder. .map { file -> DocumentDescriptor.fromUri(Uri.fromFile(file)) }, Observable.fromIterable(imageFiles) .flatMapSingle { assetName -> extractAsync(assetName, assetName, context, false, "png") } .map { val descriptor = DocumentDescriptor.imageDocumentFromUri(Uri.fromFile(it)) // File name of the image document is used as document title. Override this with a custom title. descriptor.setTitle("Android Image Document") descriptor } )
extractAssetsObservable // Collect the document descriptors into a single list. .toList() .observeOn(AndroidSchedulers.mainThread()) .subscribe { documentDescriptors -> // Launch the example activity with extracted documents in tabs. launchExampleActivity(context, documentDescriptors, configuration) } } else { // Launch the example activity with restored document descriptors and restore visible document. launchExampleActivity(context, restoredDocumentDescriptors, configuration, tabsPreferences.getVisibleDocumentIndex()) } }
private fun launchExampleActivity(context: Context, documentDescriptors: List<DocumentDescriptor>, configuration: PdfActivityConfiguration.Builder, visibleDocumentIndex: Int = 0) { val intentBuilder = if (documentDescriptors.isEmpty()) { PdfActivityIntentBuilder.emptyActivity(context) } else { PdfActivityIntentBuilder.fromDocumentDescriptor(context, *documentDescriptors.toTypedArray()) } intentBuilder.visibleDocument(visibleDocumentIndex) .configuration(configuration.build()) .activityClass(PersistentTabsActivity::class.java)
context.startActivity(intentBuilder.build()) }}
/** * We use [SharedPreferences] for storing tabs state in this example. * This class encapsulates the required serialization/deserialization of [DocumentDescriptor] data to JSON. */class TabsPreferences(private val preferences: SharedPreferences) {
companion object { // We use separate preferences const val PREFERENCES_NAME = "Nutrient.PersistentTabsExample"
const val JSON_DESCRIPTOR_URI = "uri" const val JSON_DESCRIPTOR_TITLE = "title"
const val PREF_DOCUMENT_DESCRIPTORS_JSON = "document_descriptors" const val PREF_VISIBLE_DOCUMENT_INDEX = "visible_document_index"
private var instance: TabsPreferences? = null
/** * Returns global singleton preferences instance. */ @Synchronized fun get(context: Context): TabsPreferences { if (instance == null) { val preferences = context.getSharedPreferences(PREFERENCES_NAME, Context.MODE_PRIVATE) instance = TabsPreferences(preferences) } return instance!! } }
/** * Stores list of document descriptors in preferences. */ fun setDocumentDescriptors(descriptors: List<DocumentDescriptor>) { // A simple JSON based format is used for serializing document descriptors. val descriptorsArray = JSONArray() for (descriptor in descriptors) { val descriptorJson = JSONObject() // The only data we need to restore tabs state is file Uri (we expect all document sources // to be Uri based for the sake of simplicity) and possible custom document title. descriptorJson.put(JSON_DESCRIPTOR_URI, descriptor.documentSource.fileUri) descriptorJson.put(JSON_DESCRIPTOR_TITLE, descriptor.customTitle) descriptorsArray.put(descriptorJson) } preferences.edit { putString(PREF_DOCUMENT_DESCRIPTORS_JSON, descriptorsArray.toString()) } }
/** * Returns the list of document descriptors stored in the shared preferences. * * @return List of document descriptors to display in tabs, empty list for cases where an empty * activity should be displayed and `null` if no previous state was stored in the preferences. */ fun getDocumentDescriptors(context: Context): List<DocumentDescriptor>? { val descriptorsJson = preferences.getString(PREF_DOCUMENT_DESCRIPTORS_JSON, null) ?: return null val descriptorsArray = JSONArray(descriptorsJson)
val documentDescriptors = mutableListOf<DocumentDescriptor>() for (i in 0 until descriptorsArray.length()) { val descriptorJson = descriptorsArray[i] as JSONObject val uri = descriptorJson.getString(JSON_DESCRIPTOR_URI) ?: continue val title = if (descriptorJson.has(JSON_DESCRIPTOR_TITLE)) descriptorJson.getString(JSON_DESCRIPTOR_TITLE) else null
val fileUri = uri.toUri() val documentDescriptor = if (ImageDocumentUtils.isImageUri(context, fileUri)) { DocumentDescriptor.imageDocumentFromUri(fileUri) } else { DocumentDescriptor.fromUri(fileUri) } if (title != null) { documentDescriptor.setTitle(title) } documentDescriptors.add(documentDescriptor) } return documentDescriptors }
/** * Sets index of currently visible document in the list of stored document descriptors. */ fun setVisibleDocumentIndex(visibleDocumentIndex: Int) { preferences.edit { putInt(PREF_VISIBLE_DOCUMENT_INDEX, visibleDocumentIndex) } }
/** * Returns index of currently visible document in the list of stored document descriptors. */ fun getVisibleDocumentIndex(): Int { return preferences.getInt(PREF_VISIBLE_DOCUMENT_INDEX, 0) }}
/** * This example activity shows multiple documents in tabs and persists * currently opened documents to preferences once left by the user. */class PersistentTabsActivity : PdfActivity() {
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState)
// Insert add tab button at the start of the tab bar. val tabsBar = pspdfKitViews.tabBar if (tabsBar != null) { val addTabButton = layoutInflater.inflate(R.layout.item_add_button, tabsBar, false) as ImageView addTabButton.setOnClickListener { addNewTab() } tabsBar.addView(addTabButton, 0) } }
override fun onStop() { // Save opened document descriptors and currently visible document index to preferences. val tabsPreferences = TabsPreferences.get(this)
val documents = documentCoordinator.documents tabsPreferences.setDocumentDescriptors(documents)
val visibleDocumentIndex = documents.indexOf(documentCoordinator.visibleDocument) tabsPreferences.setVisibleDocumentIndex(if (visibleDocumentIndex >= 0) visibleDocumentIndex else 0)
// Proceed with stopping the activity. super.onStop() }
private fun addNewTab() { // On Android 6.0+ we ask for SD card access permission. This isn't strictly necessary, but Nutrient // being able to access file directly will significantly improve performance. // Since documents can be annotated we ask for write permission as well. if (Utils.requestExternalStorageRwPermission(this, REQUEST_ASK_FOR_PERMISSION)) { showOpenFileDialog() } }
// We're temporarily suppressing the warning for startActivityForResult being deprecated. // Issue: https://github.com/PSPDFKit/PSPDFKit/issues/31881 @Suppress("DEPRECATION") private fun showOpenFileDialog() { val intent = Intent(Intent.ACTION_OPEN_DOCUMENT) intent.addCategory(Intent.CATEGORY_OPENABLE) intent.type = "*/*" // We accept PDF files and images (for image documents). intent.putExtra(Intent.EXTRA_MIME_TYPES, arrayOf("application/pdf", "image/*"))
// Set of the intent for result, so we can retrieve the Uri of the selected document. startActivityForResult(intent, REQUEST_OPEN_DOCUMENT) }
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) { super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == REQUEST_ASK_FOR_PERMISSION) { // We attempt to open document after permissions have been requested. // Nutrient can open documents without the permissions when SAF is used, however the access // without permissions will be significantly slower. showOpenFileDialog() } }
@Deprecated("Deprecated in Java") override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_OPEN_DOCUMENT) { if (resultCode == Activity.RESULT_OK && data != null) { val uri = data.data ?: return val isImageFile = ImageDocumentUtils.isImageUri(this, uri)
// Some URIs can be opened directly, including local filesystem, app assets, and content provider URIs. if (Nutrient.isOpenableUri(this, uri)) { showDocumentInNewTab(uri, isImageFile) } else { // The Uri cannot be directly opened. Download the PDF document from the uri, for local access.
// Find the DownloadProgressFragment for showing download progress, or create a new one. val downloadFragment = supportFragmentManager.findFragmentByTag( DOWNLOAD_PROGRESS_FRAGMENT ) as? DownloadProgressFragment ?: run { val job = DownloadJob.startDownload(DownloadRequest.Builder(this).uri(uri).build()) val downloadFragment1 = DownloadProgressFragment() downloadFragment1.show(supportFragmentManager, DOWNLOAD_PROGRESS_FRAGMENT) downloadFragment1.job = job downloadFragment1 }
// Once the download is complete we show the downloaded document in a new tab. downloadFragment.job.setProgressListener(object : DownloadJob.ProgressListenerAdapter() { override fun onComplete(output: File) { showDocumentInNewTab(Uri.fromFile(output), isImageFile) } }) } } } }
/** * Adds document from Uri to the [DocumentCoordinator] and makes it visible immediately. */ private fun showDocumentInNewTab(uri: Uri, isImageDocument: Boolean) { val documentDescriptor = if (isImageDocument) { DocumentDescriptor.imageDocumentFromUri(uri) } else { DocumentDescriptor.fromUri(uri) } documentCoordinator.addDocument(documentDescriptor) documentCoordinator.setVisibleDocument(documentDescriptor) }
companion object { private val REQUEST_OPEN_DOCUMENT = 1 private val REQUEST_ASK_FOR_PERMISSION = 2
private val DOWNLOAD_PROGRESS_FRAGMENT = "DownloadProgressFragment" }}This code sample is an example that illustrates how to use our SDK. Please adapt it to your specific use case.