DenialShield
android · on-device ocr · kotlin + compose

A denial letter goes in. The appeal comes out.

DenialShield photographs a health-insurance denial, reads it on the phone, pulls out the claim number, the stated reason and the policy language the insurer cites, and drafts the appeal letter back. No account, no server, no upload — the letter never leaves the device it was photographed on.

What arrives in the post
Really Great Health LLC
123 Wellness Way, Suite 100
Healthville, USA 12345

Explanation of Benefits - Service Denial

Date: December 31, 2025

Claim Number: 0000000000
Policy Number: 111222333444

Dear John Smith,

We have denied the following service:

Service/Device: Continuous Glucose
Monitor (CGM), Model X
Date of Service: December 15, 2025

Reason for Denial:
The requested medical device is not
covered under your current policy.
Coverage is limited to a diagnosis of
Type 1 Diabetes; our records indicate
Type 2 Diabetes.

According to your policy language:
"Continuous Glucose Monitors are a
covered benefit only for members with
a confirmed diagnosis of Type 1
Diabetes Mellitus..."

You have the right to appeal within
180 days of the date of this letter.
read on device
Claim number0000000000
ServiceContinuous Glucose Monitor (CGM), Model X
Denial reasonNot covered under current policy
Policy language cited“…covered benefit only for members with a confirmed diagnosis of Type 1 Diabetes Mellitus…”
Denial codenone printed on this letter
What you send back
Subject: Appeal for Denial of Claim
0000000000 - John Smith

[INSURANCE COMPANY NAME]
[INSURANCE COMPANY ADDRESS]

Dear Appeals Department,

I am writing to formally appeal the
denial of claim number 0000000000 for
services provided on 12/15/2025.

Reason for denial provided: not covered
under your current policy.

According to my policy language:
"Continuous Glucose Monitors are a
covered benefit only for members with
a confirmed diagnosis of Type 1
Diabetes Mellitus..."

I believe this denial is in error
because the services provided are
medically necessary and covered under
the terms of my policy as stated above.

[INSERT SUPPORTING RECORDS AND DATES]

Thank you,

John Smith

Five stages, all of them local

Each stage hands text to the next. Nothing in this chain opens a socket, which is what makes the privacy claim on the front of the app checkable rather than promised.

STAGE 01

Capture

Camera, gallery or file picker — a denial arrives as paper, a screenshot, or a PDF in email, and all three have to work.

ActivityResultContracts
STAGE 02

Recognize

On-device text recognition for photos, embedded-text extraction for PDFs. Large captures are subsampled before decode so a 50MP photo does not exhaust memory.

ML Kit · PDFBox
STAGE 03

Extract

Sentence-level scan for the vocabulary that carries meaning in a denial — coverage, exclusion, medical necessity, prior authorization — keeping the passages an appeal has to answer.

Kotlin
STAGE 04

Draft

Engines are tried in preference order. One that cannot run on this device declines and the next answers, so the user always ends up with a letter.

RebuttalEngine
STAGE 05

Export

Rendered to PDF and handed to the share sheet — email, messaging, print — through a scoped content URI rather than a raw file path.

FileProvider

Everything inside one boundary

Compose talks to a ViewModel, the ViewModel talks to a repository, and the repository owns the database. Documents enter through a separate path and meet the claim at the drafting stage. The dashed line is the device edge: no arrow crosses it.

THIS DEVICE — no network call in any path below Compose screens Home · Intake · Detail MainViewModel StateFlow · status DenialRepository Room · Flow DocumentProcessor ML Kit OCR · PDFBox Policy extraction sentence scan RebuttalGenerator engines in order Template engine always available On-device model not configured here PdfExporter FileProvider · share

The pipeline around the pipeline

Every push runs the same gates, and every reusable workflow is pinned to a commit SHA rather than a tag — a tag can be force-moved, and these jobs hold signing secrets.

Build APK workflow status Security scan workflow status Gitleaks scan workflow status

Continuous integration

Shared workflows live in a separate library and are consumed by every repository, so a fix to the build lands everywhere at once.

  • pr-gate — lint, test, build; cancels superseded runs, skips docs-only changes
  • ci — the same three on every push to main
  • android-apk — release build, signed when the keystore secret exists, debug-signed when it does not
  • Least-privilege permissions: declared per workflow

Security scanning

Two scanners on every push and pull request, with the results filed where they are actually read.

  • trivy — dependency and config scan, SARIF uploaded to the Security tab
  • gitleaks — full history, fetch-depth: 0; a shallow clone would miss a secret that was committed and later removed
  • SARIF upload is skipped for fork pull requests, whose token cannot write it

What this build is

This repository is a public showcase build. Drafting runs through the template engine: it fills a fixed appeal structure from the captured claim and leaves bracketed placeholders where a person has to write the rest. The on-device model path is present as an interface and is not configured here.

Document capture, OCR, PDF extraction, policy-language extraction, storage and export are the real implementations and run as described.

A production implementation exists. Contact the author about requirements.