This project's full code is in GitHub @ https://github.com/shravyanayani/AndroidPdfVoiceReader
Why Choose Native Apps?
Native apps on Android mobile devices are designed to take full advantage of the underlying platform’s features and capabilities. In this case, the Text-to-Speech (TTS) functionality of the phone can be seamlessly integrated, allowing the app to read PDF content aloud without limitations or external dependencies.
Beyond feature access, Android native apps also excel in performance and responsiveness. Since they are optimized for the specific operating system, they can handle tasks like parsing text and generating speech more efficiently, resulting in a smoother, faster, and more reliable user experience.
Why Create PDF Voice Reader?
PDF is one of the most widely used document formats, valued for its portability across devices and operating systems. Books, research papers, articles, and even web pages or documents can easily be saved as PDFs, making them a universal standard for digital reading.
While PDFs are convenient for distribution, reading them visually isn’t always practical or desirable. In many situations—such as while driving, before sleep, exercising, or when reducing screen time—having the document read aloud can be far more convenient.
Although there are existing apps in app stores that provide PDF-to-speech functionality, many of them come with drawbacks. They often include intrusive advertisements and lack the customization options users truly need. For example, most do not allow skipping repetitive elements like headers, footers, or page numbers, which disrupt the listening experience.
By creating PDF Voice Reader, these limitations can be overcome. The app not only eliminates ads but also offers greater flexibility, allowing users to tailor the reading experience to their needs. This makes it a more personalized, efficient, and user-friendly solution for anyone who wants to consume PDF content through voice.
Key Features of PDF Voice Reader
1. Native Text-to-Speech (TTS) Integration
PDF Voice Reader leverages the built-in Text-to-Speech engine of the mobile operating system. This ensures seamless performance without external dependencies. The app converts the text extracted from a PDF into high-quality speech, using the same voice and settings already available on the Android device. Users can also customize the voice directly from their device’s system settings.
2. File Selection with Native File Picker
Users can easily select a PDF file from their device using the native Android file picker dialog. Once chosen, the selected document is displayed in the app, ready to be read aloud. This makes the process quick, intuitive, and consistent with the device’s user experience.
3. Playback Controls
The app includes simple but powerful controls for listening:
Play/Pause/Resume the reading at any time.
Adjust the reading speed through a dropdown menu with options for slower or faster playback.
4. Page Navigation
Reading doesn’t have to start at the beginning of a document. Users can:
Enter a specific page number to jump directly to that section.
Restart playback from the chosen page once the controls are activated.
This feature is especially useful for textbooks, research papers, or long-form PDFs.
Use Next Page and Previous Page buttons to skip directly to different sections.
5. Phrase Ignoring for Cleaner Listening
One of the most unique features of PDF Voice Reader is the ability to ignore repetitive phrases such as headers, footers, or page numbers.
Users can add these phrases to an “Ignore List” so they won’t be read aloud.
Each ignored phrase is displayed in a list with a delete icon, allowing users to manage or remove phrases at any time.
This customization significantly improves the listening experience, making the content flow more naturally.
6. Android Theme and Controls
The PDF Voice Reader app is built using Android native controls, ensuring a familiar look, feel, and behavior consistent with other Android apps. This not only enhances user-friendliness but also makes the interface more intuitive, as users can rely on the interactions they already know. Additionally, the app automatically adapts to the system’s chosen theme—whether light mode or dark mode—providing a seamless and visually consistent experience.
Why Android Studio for Building PDF Voice Reader ?
To create the PDF Voice Reader Android app from scratch, I chose Android Studio as the development environment. Android Studio is the official IDE (Integrated Development Environment) for Android app development, designed specifically for building, testing, and deploying apps on Android devices. Its tight integration with the native Android SDK makes it the most reliable and future-proof choice for native development.
1. Access to Native SDKs
Android Studio comes bundled with the latest and previous versions of the Android SDK, ensuring compatibility across a wide range of Android versions. This is critical for building apps that not only use the newest platform features but also remain accessible to users on slightly older devices.
2. Built-In Device Simulators
One of Android Studio's most powerful features is its built-in Android Simulator, which allows developers to test the app on multiple device models and Android versions without needing the physical hardware. This makes it possible to verify performance, behavior, and UI responsiveness across a wide variety of scenarios, saving significant development time.
3. Standardized Layouts and Controls
Android Studio also provides native UI components and layout tools that strictly follow Android Platform's Interface Guidelines. By leveraging these, the PDF Voice Reader app automatically inherits key Android features such as theming (light and dark modes), accessibility standards, and a familiar look-and-feel. This ensures the app feels natural to users while maintaining high compatibility with Android design principles.
4. Streamlined Development Workflow
From code editing and debugging to interface design and deployment, Android Studio offers a comprehensive workflow in one place. This integration reduces complexity and allows for faster, more efficient development compared to using third-party tools.
Why Use Kotlin for PDF Voice Reader?
For developing the PDF Voice Reader app, I chose Kotlin as the programming language. Kotlin is Google’s modern, powerful, and intuitive language designed specifically for building apps across the Android ecosystem, including phones, tablets, wear devices and so on.
1. Native Performance and Compatibility
Kotlin is fully integrated with the Android SDK and development tools, making it the best choice for achieving native performance. Apps written in Kotlin run efficiently, take advantage of the latest Android features, and integrate seamlessly with system services like Text-to-Speech.
2. Simplicity and Readability
Kotlin’s syntax is clean, concise, and expressive, making it easier to write and maintain code compared to older languages like Java. This simplicity helps speed up development while reducing the chances of errors, making the codebase more maintainable over time.
3. Safety and Reliability
One of Kotlin’s strengths is its focus on safety. Features like strong typing, optionals, and automatic memory management help catch errors early during compilation rather than at runtime. This leads to more reliable and stable apps—crucial for providing a smooth reading experience to users.
4. Modern Features for Faster Development
Kotlin offers powerful features such as closures, generics, and structured concurrency, which make coding more efficient and expressive. These modern tools enable developers to implement features like customizable playback or phrase filtering with less code and greater clarity.
5. Future-Proof and Actively Supported
Kotlin is actively maintained and improved by Google and the open-source community. Choosing Kotlin ensures the app will remain compatible with future versions of Android and benefit from ongoing performance improvements, security updates, and new language features.
Steps to Create the Project in Android Studio
Since this is a single-screen app, we can start with the Empty Activity app template in Android Studio. Follow these steps:
Open Android Studio
From File menu, select New , select New Project, select Phone and Tablet section, select Empty Activity, click Next
Configure Project with following Settings
Name: PDFReadAloud
Package Name: com.productivity
Language: Kotlin
Minimum SDK: API 33(can chose the SDK version of your choice)
Build Configuration Language: Kotlin
Click Finish to generate the project.
At this point, Android Studio will scaffold the project with the necessary files and structure, and you’ll be ready to start coding the app.
Significant Code Fragments
Code to open a PDF file selection dialog and display file name.
Button(
onClick = {
pdfPickerLauncher.launch(arrayOf("application/pdf"))
},
modifier = Modifier.fillMaxWidth()
) {
Icon(
imageVector = Icons.Default.FileOpen,
contentDescription = "Select PDF File"
)
Spacer(modifier = Modifier.width(8.dp))
Text("Select PDF File")
}
selectedPdfName?.let {
Text(
text = "Selected file: $it",
style = MaterialTheme.typography.bodyMedium
)
}
fun openPdfFile(uri: Uri): Boolean {
try {
closeCurrentDocument()
val inputStream = context.contentResolver.openInputStream(uri)
pdfDocument = PDDocument.load(inputStream)
totalPages = pdfDocument?.numberOfPages ?: 0
currentPageNumber = 1
if (totalPages > 0) {
_state.value = ReaderState.Loaded(currentPageNumber, totalPages)
return true
} else {
_state.value = ReaderState.Error("No pages found in PDF")
return false
}
} catch (e: Exception) {
_state.value = ReaderState.Error("Failed to open PDF: ${e.message}")
return false
}
}
Code to select a specific page number to begin reading from.
Text(
text = "Page Controls",
style = MaterialTheme.typography.titleMedium
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
OutlinedTextField(
value = pageNumber,
onValueChange = { value ->
if (value.isEmpty() || value.all { it.isDigit() }) {
pageNumber = value
}
},
label = { Text("Page #") },
keyboardOptions = KeyboardOptions(
keyboardType = KeyboardType.Number,
imeAction = ImeAction.Done
),
keyboardActions = KeyboardActions(
onDone = { keyboardController?.hide() }
),
modifier = Modifier.weight(1f)
)
when (readerState) {
is PdfReaderService.ReaderState.Loaded,
is PdfReaderService.ReaderState.Paused -> {
Text(
text = "of ${(readerState as? PdfReaderService.ReaderState.Loaded)?.totalPages
?: (readerState as? PdfReaderService.ReaderState.Paused)?.totalPages
?: (readerState as? PdfReaderService.ReaderState.Reading)?.totalPages
?: 0}",
modifier = Modifier
.padding(horizontal = 8.dp)
.align(Alignment.CenterVertically)
)
}
else -> {
Text(
text = "of ${(readerState as? PdfReaderService.ReaderState.Loaded)?.totalPages
?: (readerState as? PdfReaderService.ReaderState.Paused)?.totalPages
?: (readerState as? PdfReaderService.ReaderState.Reading)?.totalPages
?: 0}",
modifier = Modifier
.padding(horizontal = 8.dp)
.align(Alignment.CenterVertically)
)
fun nextPage() {
if (currentPageNumber < totalPages) {
readPage(currentPageNumber + 1)
}
}
fun previousPage() {
if (currentPageNumber > 1) {
readPage(currentPageNumber - 1)
}
}
fun readPage(pageNumber: Int = currentPageNumber) {
if (!isInitialized || pdfDocument == null) {
_state.value = ReaderState.Error("Reader not initialized or no PDF loaded")
return
}
if (pageNumber < 1 || pageNumber > totalPages) {
_state.value = ReaderState.Error("Invalid page number")
return
}
try {
currentPageNumber = pageNumber
val stripper = PDFTextStripper()
stripper.startPage = pageNumber
stripper.endPage = pageNumber
currentText = stripper.getText(pdfDocument).lowercase()
// Apply excluded text filtering
var textToRead = currentText
excludedTexts.forEach { excludedText ->
if (excludedText.isNotBlank()) {
textToRead = textToRead.replace(excludedText.lowercase(), "")
}
}
if (textToRead.isBlank()) {
_state.value = ReaderState.Reading(currentPageNumber, totalPages)
if (continuousReading && currentPageNumber < totalPages) {
// If page is blank and continuous reading is enabled, skip to next page
readPage(currentPageNumber + 1)
} else {
}
return
}
_state.value = ReaderState.Reading(currentPageNumber, totalPages)
stopReading()
textToSpeech?.speak(textToRead, TextToSpeech.QUEUE_FLUSH, null, "pdf_page_$pageNumber")
} catch (e: Exception) {
_state.value = ReaderState.Error("Failed to read page: ${e.message}")
}
}
fun goToPage(pageNumber: Int) {
val validPageNumber = min(max(1, pageNumber), totalPages)
readPage(validPageNumber)
}
fun setContinuousReading(enabled: Boolean) {
continuousReading = enabled
}
fun stopReading() {
textToSpeech?.stop()
if (_state.value is ReaderState.Reading) {
_state.value = ReaderState.Paused(currentPageNumber, totalPages)
}
}
fun closeCurrentDocument() {
stopReading()
pdfDocument?.close()
pdfDocument = null
currentText = ""
totalPages = 0
_state.value = ReaderState.Idle
}
fun shutdown() {
stopReading()
textToSpeech?.shutdown()
textToSpeech = null
closeCurrentDocument()
}
Code to change the reading speed, either faster or slower.
OutlinedButton(
onClick = { isSpeedMenuExpanded = true }
) {
Text("Speed: ${selectedSpeed}x")
}
DropdownMenu(
expanded = isSpeedMenuExpanded,
onDismissRequest = { isSpeedMenuExpanded = false },
modifier = Modifier.wrapContentSize()
) {
speedOptions.forEach { speed ->
DropdownMenuItem(
onClick = {
selectedSpeed = speed
pdfReaderService.setReadingSpeed(speed)
isSpeedMenuExpanded = false
},
text = { Text("${speed}x") }
)
}
}
fun setReadingSpeed(speedFactor: Float) {
textToSpeech?.setSpeechRate(speedFactor)
}
Code to pause, stop, or restart the reading.
// Previous Page Button
val isPreviousEnabled = when (readerState) {
is PdfReaderService.ReaderState.Loaded -> (readerState as PdfReaderService.ReaderState.Loaded).currentPage > 1
is PdfReaderService.ReaderState.Reading -> (readerState as PdfReaderService.ReaderState.Reading).currentPage > 1
is PdfReaderService.ReaderState.Paused -> (readerState as PdfReaderService.ReaderState.Paused).currentPage > 1
else -> false
}
Button(
onClick = { pdfReaderService.previousPage() },
enabled = isPreviousEnabled,
modifier = Modifier.weight(1f)
) {
Text("Prev Page")
}
Spacer(modifier = Modifier.width(4.dp))
// Next Page Button
val isNextEnabled = when (readerState) {
is PdfReaderService.ReaderState.Loaded -> {
(readerState as PdfReaderService.ReaderState.Loaded).currentPage < (readerState as PdfReaderService.ReaderState.Loaded).totalPages
}
is PdfReaderService.ReaderState.Reading -> {
(readerState as PdfReaderService.ReaderState.Reading).currentPage < (readerState as PdfReaderService.ReaderState.Reading).totalPages
}
is PdfReaderService.ReaderState.Paused -> {
(readerState as PdfReaderService.ReaderState.Paused).currentPage < (readerState as PdfReaderService.ReaderState.Paused).totalPages
}
else -> false
}
Button(
onClick = { pdfReaderService.nextPage() },
enabled = isNextEnabled,
modifier = Modifier.weight(1f)
) {
Text("Next Page")
//Spacer(modifier = Modifier.width(4.dp))
}
fun pauseReading() {
if (textToSpeech?.isSpeaking == true) {
textToSpeech?.stop()
_state.value = ReaderState.Paused(currentPageNumber, totalPages)
}
}
fun resumeReading() {
if (_state.value is ReaderState.Paused) {
val textToRead = currentText
textToSpeech?.speak(textToRead, TextToSpeech.QUEUE_FLUSH, null, "pdf_resume_$currentPageNumber")
_state.value = ReaderState.Reading(currentPageNumber, totalPages)
}
}
Code to add phrases to an exclusion list and remove them individually when needed.
Text(
text = "Exclude Text",
style = MaterialTheme.typography.titleMedium
)
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth()
) {
OutlinedTextField(
value = excludeText,
onValueChange = { excludeText = it },
label = { Text("Text to exclude") },
keyboardOptions = KeyboardOptions(imeAction = ImeAction.Done),
keyboardActions = KeyboardActions(
onDone = {
if (excludeText.isNotBlank()) {
coroutineScope.launch {
preferenceRepository.addExcludedText(excludeText)
excludeText = ""
}
}
keyboardController?.hide()
}
),
modifier = Modifier.weight(1f)
)
Spacer(modifier = Modifier.width(8.dp))
IconButton(
onClick = {
if (excludeText.isNotBlank()) {
coroutineScope.launch {
preferenceRepository.addExcludedText(excludeText)
excludeText = ""
}
}
}
) {
Icon(
imageVector = Icons.Default.Add,
contentDescription = "Add excluded text"
)
}
}
Spacer(modifier = Modifier.height(8.dp))
if (excludedTexts.isNotEmpty()) {
Divider()
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Excluded Texts:",
style = MaterialTheme.typography.bodyMedium
)
FlowRow(
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier.fillMaxWidth()
) {
excludedTexts.forEach { text ->
ExcludedTextChip(
text = text,
onRemove = {
coroutineScope.launch {
preferenceRepository.removeExcludedText(text)
}
}
)
}
}
}
}
suspend fun addExcludedText(text: String) {
context.dataStore.edit { preferences ->
val currentList = preferences[EXCLUDED_TEXT_KEY]?.split(",") ?: emptyList()
if (text.isNotBlank() && !currentList.contains(text)) {
val newList = currentList.toMutableList().apply { add(text) }
preferences[EXCLUDED_TEXT_KEY] = newList.joinToString(",")
}
}
}
suspend fun removeExcludedText(text: String) {
context.dataStore.edit { preferences ->
val currentList = preferences[EXCLUDED_TEXT_KEY]?.split(",") ?: emptyList()
val newList = currentList.filter { it != text }
preferences[EXCLUDED_TEXT_KEY] = newList.joinToString(",")
}
}
This project's full code is in GitHub @ https://github.com/shravyanayani/AndroidPdfVoiceReader
Top comments (0)