FTS Indexing Example (legacy)
Index all documents of the catalog app and make them searchable. Uses an older API. For a modern example with directory support, see the non-legacy example.
/* * Copyright © 2025-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.content.Contextimport android.content.Intentimport android.graphics.Colorimport android.graphics.Typefaceimport android.os.Bundleimport android.os.Handlerimport android.os.Looperimport android.text.SpannableStringimport android.text.style.BackgroundColorSpanimport android.text.style.StyleSpanimport android.util.Logimport android.view.LayoutInflaterimport android.view.Menuimport android.view.MenuItemimport android.view.Viewimport android.view.ViewGroupimport android.widget.AbsListViewimport android.widget.BaseAdapterimport android.widget.ListViewimport android.widget.TextViewimport android.widget.Toastimport androidx.appcompat.app.AppCompatActivityimport androidx.appcompat.widget.SearchViewimport androidx.core.graphics.drawable.DrawableCompatimport com.google.android.material.snackbar.Snackbarimport com.pspdfkit.catalog.Rimport com.pspdfkit.catalog.SdkExampleimport com.pspdfkit.catalog.SdkExample.Companion.TAGimport com.pspdfkit.catalog.utils.Utilsimport com.pspdfkit.configuration.activity.PdfActivityConfigurationimport com.pspdfkit.document.DocumentSourceimport com.pspdfkit.document.PdfDocumentimport com.pspdfkit.document.PdfDocumentLoaderimport com.pspdfkit.document.library.IndexingOptionsimport com.pspdfkit.document.library.PdfLibraryimport com.pspdfkit.document.library.QueryOptionsimport com.pspdfkit.document.library.QueryPreviewResultimport com.pspdfkit.document.library.QueryResultListenerimport com.pspdfkit.document.providers.AssetDataProviderimport com.pspdfkit.ui.PdfActivityIntentBuilderimport io.reactivex.rxjava3.core.Observableimport java.io.IOExceptionimport java.lang.IllegalStateExceptionimport java.util.Locale
/** * This activity showcases [PdfLibrary] by indexing all PDFs of the catalog app, making them * searchable. * This example uses the legacy [PdfLibrary] API. For a more modern approach, see */class IndexedFullTextSearchLegacyExample(context: Context) : SdkExample(context, R.string.indexedFtsLegacyExampleTitle, R.string.indexedFtsLegacyExampleDescription) { override fun launchExample( context: Context, configuration: PdfActivityConfiguration.Builder ) { context.startActivity(Intent(context, IndexedFullTextSearchLegacyActivity::class.java)) }}
class IndexedFullTextSearchLegacyActivity : AppCompatActivity() {
companion object { /** Name of the Full-text search library file. */ private const val FTS_SEARCH_LIBRARY_NAME = "fts-library.db" }
/** UI-thread handler for updating the UI from a background thread. */ private val handler = Handler(Looper.getMainLooper())
/** List view adapter for displaying search results. */ private val adapter = SearchResultAdapter()
/** Contains document paths (relative to the assets) keyed by the document UID. */ private val indexedDocumentPaths = mutableMapOf<String, String>()
/** FTS indexing library. */ private lateinit var library: PdfLibrary
/** This [Snackbar] is used to show a ongoing indexing process. */ private var progressIndicator: Snackbar? = null
/** Search option set by the user, to ignore search results inside the document body. */ private var ignoreDocumentText = false
/** Search option set by the user, to ignore search results inside annotations. */ private var ignoreAnnotations = false
/** Create the Full-text search indexing library and prepare the search result list view. */ override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_fts_indexing)
// Open the PdfLibrary on a desired location. This example stores the FTS index library // inside the app's private database directory. try { val databaseFile = getDatabasePath(FTS_SEARCH_LIBRARY_NAME) if (!databaseFile.mkdirs() && (databaseFile.parentFile?.exists() != true)) { Log.w(TAG, "Could not create the FTS indexing database directory.") }
library = PdfLibrary(databaseFile.absolutePath) } catch (e: IOException) { Log.e(TAG, "Error while creating the FTS library database.", e) Toast.makeText( this, "Could not create FTS library - see logcat for error. Exiting example.", Toast.LENGTH_LONG ).show() finish() return }
// Prepare the list view and adapter for displaying search results. val searchResultsList = findViewById<ListView>(android.R.id.list) searchResultsList.adapter = adapter
// Create a search result click listener that launches the PdfActivity showing the results. searchResultsList.setOnItemClickListener { _, _, position, _ -> val clickedSearchResult = adapter.getItem(position) val clickedDocumentPath = indexedDocumentPaths[clickedSearchResult.uid]!! val dataProvider = AssetDataProvider(clickedDocumentPath)
// Open the touched search result on the correct page. val configuration = PdfActivityConfiguration.Builder(this) .page(clickedSearchResult.pageIndex) .build()
val intent = PdfActivityIntentBuilder.fromDataProvider(this, dataProvider) .configuration(configuration) .build() startActivity(intent) }
searchResultsList.setOnScrollListener(object : AbsListView.OnScrollListener { override fun onScrollStateChanged(view: AbsListView, scrollState: Int) { // Hide the keyboard when scrolling the list. if (scrollState == AbsListView.OnScrollListener.SCROLL_STATE_TOUCH_SCROLL || scrollState == AbsListView.OnScrollListener.SCROLL_STATE_FLING ) { Utils.hideKeyboard(view) } }
override fun onScroll( view: AbsListView, firstVisibleItem: Int, visibleItemCount: Int, totalItemCount: Int ) { // No-op } })
// Initially trigger document indexing. performIndexing() }
/** Set up search inside the action bar. */ override fun onCreateOptionsMenu(menu: Menu): Boolean { super.onCreateOptionsMenu(menu) menuInflater.inflate(R.menu.activity_fts_indexing, menu)
val item = menu.findItem(R.id.action_search) ?: throw IllegalStateException("No search menu item found.") val searchIcon = DrawableCompat.wrap(item.icon!!) DrawableCompat.setTint(searchIcon, Color.BLACK) item.icon = searchIcon item.expandActionView()
val searchView = getSearchView(item)
val checkboxIgnoreDocumentText = menu.findItem(R.id.checkboxIgnoreDocumentText) checkboxIgnoreDocumentText.setOnMenuItemClickListener { menuItem -> menuItem.isChecked = !menuItem.isChecked ignoreDocumentText = menuItem.isChecked performSearch(searchView.query.toString()) true }
val checkboxIgnoreAnnotations = menu.findItem(R.id.checkboxIgnoreAnnotations) checkboxIgnoreAnnotations.setOnMenuItemClickListener { menuItem -> menuItem.isChecked = !menuItem.isChecked ignoreAnnotations = menuItem.isChecked performSearch(searchView.query.toString()) true }
return true }
private fun getSearchView(item: MenuItem): SearchView { val searchView = item.actionView as SearchView searchView.setIconifiedByDefault(false) searchView.requestFocus() searchView.queryHint = "Search PDF documents..."
// Search is started as soon as the user starts writing. searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener { override fun onQueryTextSubmit(query: String): Boolean = false
override fun onQueryTextChange(newText: String): Boolean = performSearch(newText) }) return searchView }
/** Runs a search query on all indexed documents. */ private fun performSearch(query: String): Boolean { return if (query.length > 2) { library.stopSearch() val options = QueryOptions.Builder() .ignoreDocumentText(ignoreDocumentText) .ignoreAnnotations(ignoreAnnotations) .generateTextPreviews(true) .build()
library.search( query, options, object : QueryResultListener { override fun onSearchCompleted( searchString: String, results: Map<String, Set<Int>> ) { Log.d( TAG, "onSearchCompleted() called with: searchString = [$searchString], results = [$results]" ) }
override fun onSearchPreviewsGenerated( searchString: String, results: Map<String, Set<QueryPreviewResult>> ) { Log.d( TAG, "onSearchPreviewsGenerated() called with: searchString = [$searchString], results = [$results]" )
// Search results are returned on a background thread. Post the results // to the adapter, on the main thread. runOnUiThread { adapter.setSearchResults(results) } } } ) true } else { // Clear the search results if the query is too short. adapter.setSearchResults(null) false } }
/** Performs indexing of all available documents in the app's assets. */ private fun performIndexing() { val indexingQueue = mutableListOf<PdfDocument>()
// This example always clears the FTS index, just for demo purposes. In a real-world app // this is not necessary. library.clearIndex()
try { // List all top-level assets of the app and filter them for PDF files. val assets = Observable.fromArray(*assets.list("")!!) .filter { it.endsWith(".pdf") } .toList() .blockingGet()
// Open and collect all documents that should be indexed. for (asset in assets) { try { // Try to open the document and if successful, enqueue it for indexing. val document = PdfDocumentLoader.openDocumentAsync( this, DocumentSource(AssetDataProvider(asset)) ).blockingGet() indexingQueue.add(document)
// Store the asset's path and name using its UID. This allows us to retrieve the // document later when a search is performed. indexedDocumentPaths[document.uid] = asset } catch (ex: Exception) { // This example catches any action that happens while opening the document (e.g. // if a password would be needed). // If an exception is thrown, the document will not be indexed. Log.w( TAG, "Could not open document '$asset' from assets. See exception for reason.", ex ) } } } catch (e: IOException) { Log.e(TAG, "Error while trying to index all catalog app assets.", e) Toast.makeText( this, "Error listing asset files to index - see logcat for detailed error message.", Toast.LENGTH_LONG ).show() }
if (indexingQueue.isNotEmpty()) { // Start the actual document indexing. library.enqueueDocuments(indexingQueue, IndexingOptions())
// Show to the user that indexing is in progress. progressIndicator = Snackbar.make( findViewById(android.R.id.content), "Indexing...", Snackbar.LENGTH_INDEFINITE ) progressIndicator?.show() scheduleProgressIndicatorUpdate() } }
/** Hides progress snackbar as soon as [PdfLibrary] has finished document indexing. */ private fun scheduleProgressIndicatorUpdate() { handler.postDelayed({ progressIndicator?.let { indicator -> if (library.isIndexing) { scheduleProgressIndicatorUpdate() } else { indicator.dismiss() progressIndicator = null } } }, 1000) }
private class ViewHolder private constructor(val view: View) { val documentTitleTextView: TextView = view.findViewById(R.id.documentTitleTextView) val pageNumberTextView: TextView = view.findViewById(R.id.pageNumberTextView) val previewTextView: TextView = view.findViewById(R.id.previewTextView)
companion object { fun get(view: View?, parent: ViewGroup): ViewHolder { return if (view != null) { view.tag as ViewHolder } else { val newView = LayoutInflater.from(parent.context) .inflate(R.layout.item_fts_result, parent, false) val holder = ViewHolder(newView) newView.tag = holder holder } } } }
/** List view adapter for presenting search results. */ private inner class SearchResultAdapter : BaseAdapter() {
private val listItems = mutableListOf<QueryPreviewResult>()
fun setSearchResults(searchResults: Map<String, Set<QueryPreviewResult>>?) { listItems.clear()
searchResults?.let { results -> for (documentUID in results.keys) { results[documentUID]?.let { listItems.addAll(it) } } }
notifyDataSetChanged() }
override fun getCount(): Int = listItems.size
override fun getItem(position: Int): QueryPreviewResult = listItems[position]
override fun getItemId(position: Int): Long = position.toLong()
override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { val holder = ViewHolder.get(convertView, parent) val item = getItem(position) holder.documentTitleTextView.text = indexedDocumentPaths[item.uid] holder.pageNumberTextView.text = String.format( Locale.getDefault(), "Page %d", item.pageIndex + 1 )
// Highlight the actual search results phrase. val highlightedRange = item.rangeInPreviewText val previewText = SpannableString(item.previewText) previewText.setSpan( StyleSpan(Typeface.BOLD), highlightedRange.startPosition, highlightedRange.endPosition, 0 ) previewText.setSpan( BackgroundColorSpan(Color.YELLOW), highlightedRange.startPosition, highlightedRange.endPosition, 0 ) holder.previewTextView.text = previewText
return holder.view } }}This code sample is an example that illustrates how to use our SDK. Please adapt it to your specific use case.