๐Ÿง  Android AI Projects and Production

Build an AI Document Summarizer for Android

A document summarizer is a great portfolio project: it combines file access, text extraction, chunking for long inputs, LLM calls, streaming UI and local storage. Here's a complete architecture and the key code for each part.

โœ๏ธ Yunok Tech ๐Ÿ“… Updated โฑ 3 min read ๐ŸŽฏ Intermediate
On this page
  1. Architecture
  2. 1. Pick a document
  3. 2. Extract text
  4. 3. Chunk long text
  5. 4. Map-reduce summarisation
  6. 5. Save history
  7. Quality and trust
  8. Extensions
  9. Frequently asked questions
  10. Conclusion

Our AI Text Summarization guide covers summarising a block of text. This project goes end to end: picking real documents, handling inputs longer than the model accepts, and building a usable app around it.

Architecture

Document summarizer pipeline The user picks a document with the system file picker. A text extractor handles text files, or sends PDFs to a multimodal cloud model or OCR. A chunker splits long text. A summarizer creates chunk summaries then a final summary, streaming to the UI. Summaries are saved in Room. Pick fileSAF Extracttext / PDF Chunkif long Summarizemap โ†’ reduce SaveRoom UI: progress per stage ยท streamed final summary ยท history list
Each stage is a separate class you can test on its own.

1. Pick a document

Open a document (no storage permission)Kotlin
val openDoc = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
    uri?.let(viewModel::onDocumentPicked)
}
Button(onClick = { openDoc.launch(arrayOf("application/pdf", "text/plain")) }) {
    Text("Choose document")
}

2. Extract text

Plain text is easy. For PDFs, there are several options:

  • Cloud multimodal model: send the PDF directly to Gemini through Firebase AI Logic, which accepts PDFs as inline data (within size limits). Simplest, and handles layout.
  • On-device: render pages with PdfRenderer to bitmaps and run ML Kit text recognition. Private, but slower.
  • A PDF text library: for text-based PDFs. Check licences.
TextExtractor.ktKotlin
class TextExtractor(private val context: Context, private val recognizer: TextRecognizer) {

    suspend fun extract(uri: Uri): String = withContext(Dispatchers.IO) {
        when (context.contentResolver.getType(uri)) {
            "text/plain" -> context.contentResolver.openInputStream(uri)!!.bufferedReader().use { it.readText() }
            "application/pdf" -> ocrPdf(uri)
            else -> throw UnsupportedDocumentException()
        }
    }

    private suspend fun ocrPdf(uri: Uri): String {
        val pfd = context.contentResolver.openFileDescriptor(uri, "r") ?: return ""
        return PdfRenderer(pfd).use { renderer ->
            buildString {
                for (i in 0 until minOf(renderer.pageCount, MAX_PAGES)) {
                    renderer.openPage(i).use { page ->
                        val bmp = Bitmap.createBitmap(page.width * 2, page.height * 2, Bitmap.Config.ARGB_8888)
                        bmp.eraseColor(Color.WHITE)
                        page.render(bmp, null, null, PdfRenderer.Page.RENDER_MODE_FOR_DISPLAY)
                        appendLine(recognizer.process(InputImage.fromBitmap(bmp, 0)).await().text)
                        bmp.recycle()
                    }
                }
            }
        }
    }
}

3. Chunk long text

Models have input limits (Gemini Nano summarisation accepts under ~4,000 tokens; cloud models accept far more, but cost grows with input). Split on paragraph boundaries:

Chunker.ktKotlin
fun chunk(text: String, maxChars: Int = 8_000): List<String> {
    val chunks = mutableListOf<String>()
    val current = StringBuilder()
    for (para in text.split(Regex("\\n\\s*\\n"))) {
        if (current.length + para.length > maxChars && current.isNotEmpty()) {
            chunks += current.toString(); current.clear()
        }
        current.append(para.take(maxChars)).append("\n\n")
    }
    if (current.isNotBlank()) chunks += current.toString()
    return chunks
}
// Character counts approximate tokens (~4 chars/token in English); keep a safety margin.

4. Map-reduce summarisation

DocumentSummarizer.ktKotlin
class DocumentSummarizer(private val llm: Llm) {   // Llm: generate(prompt): String, stream(prompt): Flow<String>

    fun summarize(text: String, onProgress: (Int, Int) -> Unit): Flow<String> = flow {
        val chunks = chunk(text)
        val partials = if (chunks.size == 1) chunks else chunks.mapIndexed { i, c ->
            onProgress(i + 1, chunks.size)
            llm.generate("Summarise this section in 5 bullet points. Use only the text.\n\n$c")
        }
        emitAll(
            llm.stream(
                "Combine these section summaries into one summary: a 2-sentence overview, " +
                "then up to 8 bullet points. Don't add facts not present.\n\n" +
                partials.joinToString("\n\n")
            )
        )
    }
}

Run chunk summaries with limited parallelism to stay within rate limits, and stream only the final step. Render it as in Streaming in Compose.

5. Save history

SummaryEntity.ktKotlin
@Entity(tableName = "summaries")
data class SummaryEntity(
    @PrimaryKey(autoGenerate = true) val id: Long = 0,
    val title: String,
    val summary: String,
    val sourceChars: Int,
    val modelSource: String,          // "on-device" or "cloud"
    val createdAt: Long,
)
// Store the summary, not the full document, unless users need it (privacy + storage).

Quality and trust

  • Label summaries as AI-generated, and show which model source was used.
  • Prompt for faithfulness ("use only the text"), and spot-check with an evaluation set. See Evaluating AI.
  • Disclose cloud processing, and offer on-device-only mode for sensitive documents.
  • Handle failures per stage with clear messages.

Extensions

Ask the document

Add Q&A with retrieval. See RAG on Android.

Share target

Accept shared text or files from other apps.

Background processing

Long documents via WorkManager with a completion notification.

Frequently asked questions

How long a document can I summarise?

With map-reduce, length is limited mainly by time and cost. Cap pages or characters and tell users.

Will scanned PDFs work?

Yes, with OCR or a multimodal model, but quality depends on scan quality.

Conclusion

A solid summarizer picks files via SAF, extracts text (or sends PDFs to a multimodal model), chunks long inputs, summarises with map-reduce, streams the final result, and stores history, with clear AI labelling and privacy choices.

Next project: an AI Photo Caption Generator.

Official references

๐Ÿ“ฑ Apps built by Yunok Tech

Real Android apps on Google Play, free to download.

See all our apps โ†’

Ready to build real Android applications?

Every AI feature still runs inside an Android app that has to be fast, reliable and well structured. Strengthen the Kotlin, Jetpack Compose and architecture skills that AI integrations depend on, then apply these AI concepts with confidence.

Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.

Explore the Android course and take your next step as an Android developer.

Share: LinkedIn X / Twitter WhatsApp