Business card fields, QR candidates, and contact export

The app does not put cards into your contacts automatically. OCR, Entity Extraction, and contact QR codes propose candidates, you check and edit the fields, and a card with a name is saved and then handed to the contacts app or shared as a vCard.

Source baseline: app 1.0.0 (1), commit ecebf71 · 23 September 2026

Scope and stack

Version 1 reads one side of a Korean or English business card per image, keeps a list of cards in the app, and exports a saved card to the contacts app or as a vCard file. There is no account, server, or sync. It is the second app built on the same on-device pipeline as Dotori Receipt Scanner, so this note covers what is different for cards.

UIKotlin and Jetpack Compose, minSdk 26, targetSdk 36
InputML Kit Document Scanner (one page) or Android Photo Picker (one image)
OCRBundled ML Kit Text Recognition v2, Korean model, which also reads Latin script
SupplementaryML Kit Entity Extraction (Korean and English models) and Barcode Scanning for contact QR codes
StorageRoom database plus app-private image files
ExportContacts insert intent and vCard 3.0 through a share sheet

A pure-Kotlin core module, libs/ai/core, was extracted from the receipt app when this second consumer appeared. It holds only the OCR line and document types, the field status and candidate types, and the check that cited line IDs exist. Card rules, the field model, and the merge policy stay in this app.

Recognition pipeline

Input, staging, and permissions follow the receipt app: two separate buttons, no stored scanner or picker URIs, EXIF-upright decoding, and no dangerous permissions in the app manifest, with the merged release manifest as the final check. Adding contacts needs no WRITE_CONTACTS either, because the app only opens the contacts app's own editor.

  1. The image is decoded once and QR scanning starts in parallel with OCR.
  2. OCR lines are kept as ML Kit returns them, with their boxes, and numbered once. All candidates refer to those IDs.
  3. Korean and English rules produce field candidates. Entity Extraction adds phone, email, URL, and address candidates when its model is already on the device.
  4. The app waits for the QR result and merges it before the review screen opens, so a late QR result cannot overwrite a field the user is already editing.
  5. If nothing was read, no empty review screen opens and the app reports that recognition failed.

The receipt row assembly is deliberately not reused. Receipts put a label and an amount at the two ends of a row, so the receipt app joins pieces at the same height. Cards print several fields on one line, such as T. 02-123-4567 F. 02-123-4568, and joining pieces would make them harder to separate. Splitting on labels, column detection, and proximity are handled by this app.

Field rules

Korean and English dictionaries are applied together, without a separate language identification step. Internally each field is Missing, Needs review, or User confirmed, and only a user edit produces the last state. These states are not shown on screen: there is no per-field check mark to tap, and every field can be edited directly.

Each field holds one value. Text no rule placed in any field, such as a logo, a slogan, or an address fragment, and the second and later candidates for company, department, title, and address are joined with / and pre-filled into the memo, because an alternative that is not chosen would otherwise be lost on save. The chosen name and alternate name are left out, and so are values that Entity Extraction or QR filled later. Choosing an alternative chip puts the displaced value back in its place in the memo, but only while the memo is still the untouched draft.

There is no draft or confirmed record state. A recognition result is not written to the database until the user saves or exports it, and Save, Add to Contacts, and Share vCard are enabled once the required name field is filled. A card without a readable name is saved only after the user types one; a company name is never moved into the name slot. Closing an unsaved result or an edited record asks whether to discard it.

Entity Extraction and QR

Both paths are supplementary. They fill fields the user has not edited and never replace a user edit or the primary OCR candidate automatically.

A Gemini Nano (ML Kit GenAI Prompt) path would help most with name, company, and title, but version 1 has only a debug-only availability probe. Release builds do not include the Prompt module, and no generated content reaches the user.

Contacts and vCard

Export buttons become available once the name is filled in. Pressing one saves the record first and then opens the intent, and saving or closing is blocked while that export is in progress.

Contacts appContactsContract.Intents.Insert: name, company, job title, first phone with its type, first email, and address. Alternate name, department, memo, extensions, further phones and emails, and all websites go into the note.
vCardVersion 3.0, UTF-8, CRLF. Every phone, email, and website is written; alternate name, department, memo, and extensions go into NOTE.

Storage, privacy, and backup

The storage, image retention, and backup rules match the receipt app, with one difference in weight: a business card is someone else's personal data.

Verification and known limits

Extraction, phone rules, merge, the form model, record encoding, contact export, export labels, and the vCard writer are covered by JVM unit tests using synthetic OCR text and fictional people; real cards are kept out of the repository. The release build inherits the ML Kit registrar keep rule that fixed a release-only crash in the receipt app.

Three fixes from a real device. Testing on an Android 11 test phone produced three problems. OCR inserted spaces around @, so spaced emails are now joined, but only when the domain ends in a common top-level domain, because the same shape is also a social media handle. A misread scheme fragment such as htps:// leaked into name alternatives and is now masked with the website range, without being corrected. And exported phones now use the normalised value only when it carries a country code; otherwise the printed form is kept, with any extension moved to the note.