In India, UPI QR codes travel as images: a supplier sends a screenshot of their QR in a chat, a landlord forwards a photo of the sticker on the door. The phone that received the image is also the phone you want to pay from, and you cannot point its camera at its own screen. This post walks through the small Android pipeline that gets you from "image in a chat" to "my UPI app is open with the UPI ID filled in", without the app ever needing the internet.
Disclosure: I'm the developer of Auto UPI QR, the app this pipeline comes from, and this post is published from my account. It was written and published by an AI agent working on my behalf.
Step 1: be a share target for images
The least-friction entry point is the system share sheet. The user opens the QR image wherever it already is (chat, gallery, browser), taps Share, and picks your app. That needs one intent filter on an activity:
<activity android:name=".ShareReceiverActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" />
</intent-filter>
</activity>
In the activity, the image arrives as a content URI, not a file path:
val uri: Uri? = if (Build.VERSION.SDK_INT >= 33) {
intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
} else {
@Suppress("DEPRECATION") intent.getParcelableExtra(Intent.EXTRA_STREAM)
}
Because the sharing app grants you temporary read access to that one URI, you need no storage permission at all. That matters for trust: an app that reads payment QRs should not also be able to browse every photo on the phone. A "choose image" button inside the app can use the system photo picker for the same reason.
Step 2: decode the QR on the device
Google's ML Kit barcode scanner has two flavours. The bundled one ships the model inside your APK; the unbundled one downloads it through Google Play services. For an offline promise, use the bundled artifact (com.google.mlkit:barcode-scanning) and restrict it to QR codes so it does less work:
val scanner = BarcodeScanning.getClient(
BarcodeScannerOptions.Builder()
.setBarcodeFormats(Barcode.FORMAT_QR_CODE)
.build()
)
val image = InputImage.fromFilePath(context, uri)
scanner.process(image)
.addOnSuccessListener { codes ->
val raw = codes.firstOrNull { it.rawValue?.startsWith("upi://", ignoreCase = true) == true }?.rawValue
if (raw == null) showNotUpiMessage() else handleUpi(raw)
}
.addOnFailureListener { showCouldNotReadMessage() }
Two practical notes:
-
Screenshots often contain more than one code, or a QR plus a logo that looks like one. Pick the first value that is actually a
upi://link instead of the first barcode found. - Say clearly when nothing was found. A cropped or blurred image fails silently if you let it; tell the user to share a sharper image.
With the bundled model you can leave the INTERNET permission out of the manifest entirely. Users (and reviewers) can check that in the app's permission list, which is a stronger statement than any privacy policy sentence.
Step 3: read the link, but do not trust it
A UPI QR decodes to an ordinary URI such as:
upi://pay?pa=shopname@bank&pn=Shop%20Name&cu=INR
pa is the payee's UPI ID and pn is the name the QR claims. Parse it with Uri.parse(raw) and getQueryParameter("pa"), and validate that pa looks like something@handle before going further. Keep in mind that anyone can put any pn into a QR they print. The name that counts is the one the UPI app shows after it looks the UPI ID up.
Step 4: hand off to the user's own UPI app
Your app should not be part of the payment at all. Let a UPI app the user already trusts do that part, with the user in charge:
val view = Intent(Intent.ACTION_VIEW, Uri.parse(upiLink))
startActivity(Intent.createChooser(view, getString(R.string.pay_with)))
Every installed UPI app registers for the upi:// scheme, so a chooser lists GPay, PhonePe, Paytm, BHIM, CRED and the rest. It is also worth copying the UPI ID to the clipboard (ClipboardManager.setPrimaryClip), so the user can paste it into any UPI app's "pay to UPI ID" screen if they prefer that route.
Step 5: keep nothing
After the hand-off there is nothing left for the app to do, so do not keep anything: no copy of the image, no list of UPI IDs, no history screen. That keeps the app small and leaves no data to leak.
The one thing to tell every user
A tool like this reads a picture and fills in a field; it cannot know whether the person who sent you the picture is honest. Put this line where users will see it: Always check the payee name before entering your UPI PIN. If the name in your UPI app does not match who you meant to pay, stop.
Where this ended up
Auto UPI QR is this pipeline as an app: share a QR image to it, it reads the UPI ID offline with ML Kit, copies it, and opens your UPI app with it filled in. You check the details and pay in your UPI app as usual. It has no internet permission, no ads, and stores nothing. It only reads the QR; the payment itself happens in your UPI app, and it never sees your UPI PIN.
Always check the payee name before entering your UPI PIN.
Questions about the ML Kit set-up or the intent hand-off are welcome in the comments.
Top comments (0)