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.
| UI | Kotlin and Jetpack Compose, minSdk 26, targetSdk 36 |
|---|---|
| Input | ML Kit Document Scanner (one page) or Android Photo Picker (one image) |
| OCR | Bundled ML Kit Text Recognition v2, Korean model, which also reads Latin script |
| Supplementary | ML Kit Entity Extraction (Korean and English models) and Barcode Scanning for contact QR codes |
| Storage | Room database plus app-private image files |
| Export | Contacts 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.
- The image is decoded once and QR scanning starts in parallel with OCR.
- OCR lines are kept as ML Kit returns them, with their boxes, and numbered once. All candidates refer to those IDs.
- Korean and English rules produce field candidates. Entity Extraction adds phone, email, URL, and address candidates when its model is already on the device.
- 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.
- 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.
- Name. Candidates are scored from hints: an explicit name label, a title on the same or a neighbouring line, a two-to-four syllable Hangul shape, a spaced Latin name, proximity to the company, and a tall text box. These are score hints, not exclusion rules, because a logo or company name can be the largest text. Spaced names such as
홍 길 동keep the printed text and normalise only the candidate value.대표이사 홍길동is split into title and name. When scores tie, three Hangul syllables with spaces removed (family name plus given name) come first, which also joins unevenly spaced forms such as가 상민. - Degrees. Degrees such as
공학박사,공학 박사,박사과정,석사수료,Ph.D., andMBAare removed from name candidates and appended to the title astitle / degree, or stand alone when there is no title. Before this rule, the degree in이사 / 공학박사beat the real name on a test device. - Company and title. Company markers such as
(주),주식회사,Co., Ltd.,Inc., andLLC, and title dictionaries in both languages. A company name is never invented from an email domain, and a company is never moved into the name slot. - Phone. Mobile, office, direct, fax, main, and extension labels in both languages are kept as separate values;
C.PandCPon Korean cards are read as mobile. The printed text and the normalised value are stored separately. A country code is added only when it is printed or when the user chose a default country in Settings; a Korean card does not get+82just for being Korean, because it may list an overseas office. A printed(0)after an international prefix is left for review rather than dropped. - Email and web. A different email domain and website domain are not treated as an error; groups, agencies, and mail services make that common. Both stay as candidates.
- Other names. A card that prints
Hong Gil-dong / 홍길동can hold both as a primary name and an alternate name. The alternate is not treated as a phonetic name, and the app never generates romanisations or translations.
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.
- Entity Extraction is called only when its model is already on the device, checked on every request. If a model is missing the request continues without it, and a download is requested on Wi-Fi only, so a download the user did not start does not use mobile data. The home screen shows a line while a model is being prepared. Downloading contacts the ML Kit model server and does not send card content.
- Entity spans that point outside the OCR text are ignored for that path only; the OCR result stays usable. Entity phone numbers can inherit a label from the line above when the boxes are adjacent.
- QR scanning is independent of OCR. When OCR fails but exactly one contact QR code is found, the app can still fill the review screen from it.
- When a QR value agrees with the printed value, the field notes the agreement. When it differs, the printed value stays primary, the QR value becomes an alternative, and the field is marked as a print and QR conflict. A QR result never counts as a user edit.
- Several contact QR codes are not merged into one. URL QR codes are kept as separate link candidates, are never opened or followed automatically, and are not exported as the contact's website. The link row has a browser icon button (accessible name Open link): only when the user taps it, and only for an http or https address, the app hands the link to Android to open. Other schemes such as
intent:,tel:, orjavascript:get no button; a value without a scheme getshttps://, and an upper-case scheme from a QR code is lower-cased. - QR candidates carry their own source and index and are never disguised as OCR line IDs, so the shared “cited line exists” check applies only to OCR candidates.
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 app | ContactsContract.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. |
|---|---|
| vCard | Version 3.0, UTF-8, CRLF. Every phone, email, and website is written; alternate name, department, memo, and extensions go into NOTE. |
- Opening the contacts editor is not treated as saving. The user can still cancel there.
- Only the first phone and email use dedicated keys. Extra values could be passed as
Insert.DATA, but the platform documentation says an editor may drop data it does not show, so the note is the preserved path until that is measured in the Samsung and Google contacts apps. - The alternate name is never placed in the phonetic name field.
- The vCard does not guess family and given names. The full name goes into
FNand the given-name slot ofN. - Text values escape backslash, comma, semicolon, and newline, and URLs are written as URIs rather than escaped text. Lines are folded at 75 octets, counting the leading space and never splitting a surrogate pair.
- Note labels come from the language chosen in the app, so an English interface does not write Korean labels into a contact.
- The vCard file name contains a timestamp, not the person's name, so the receiving app's history does not record it. Files are shared from the app cache through a FileProvider URI with a temporary read grant; files older than 24 hours are removed at export time and at app start.
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.
- Cards live in one Room table and images in app-private storage. Nothing is written to the gallery. Deleting a card is a hard delete that also removes its image. The list has a checkbox on each row; checking cards shows a
Delete (N)button that removes them together, in one statement, after a confirmation. - Database version 2 removed the
statuscolumn that held the draft or confirmed state. A Room auto-migration drops the column and keeps existing records. - Multi-value fields are stored with a length-prefixed encoding that keeps only the value, label, extension, normalised value, and whether the user edited it. OCR text, candidate evidence, and QR identifiers are not stored, so evidence highlights exist only during the review that follows recognition. Alternatives survive only as text left in the saved memo.
- Images sit under
noBackupFilesDir, and the database is excluded from cloud backup and device-to-device transfer throughfullBackupContentanddataExtractionRules. - Logs, exception messages, and ad requests do not contain names, phone numbers, emails, or companies.
- A kept image still shows everything printed on the card. The app does not promise to redact images.
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.
- Only one side of one card per image. Front and back merging, several cards in one image, search, sorting, groups, and thumbnails are outside version 1.
- There is no duplicate check against existing contacts, because that would need
READ_CONTACTS. - Compound titles written as one word, such as
개발팀장, are not yet recognized as titles, and the alternate-name heuristic can miss all-caps Latin names; the user checks these on the review screen. - Extensions are shown but cannot yet be edited as a separate field, and no message appears on a device with no contacts app.
- Korean address quality from Entity Extraction is still a hypothesis to measure.