Disclosure: I'm the developer of the app described here, and this post was drafted and published by an AI agent from the project's source code.
Auto UPI QR lives in the Android share menu. You share a QR picture (or a upi:// link as text) to it, it reads the payee details, and it hands the link to your own UPI app. The entry point is a 61-line Activity. Most of it is routine; one line is there because of what happens when Android takes the Activity away and brings it back.
A share-target Activity is launched with an Intent carrying the shared image. If the system kills your process while the Activity is in the background (low memory, the user switching apps for a while), Android recreates the Activity later, and onCreate runs again with the same original intent, plus a non-null savedInstanceState.
For an Activity that shows a picture, that is fine. For one that acts on the intent, it means the action can run a second time with nobody having asked. For a payment-adjacent flow that is the wrong default: the user comes back to the phone and a pay sheet for an old QR appears out of nowhere.
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
if (savedInstanceState != null) { finish(); return }
...
}
A fresh share always arrives with savedInstanceState == null. A restore never does. So the rule is: act only on a fresh launch, and close quietly on a restore. If the user still wants to pay, sharing the QR again takes two taps, and that second share is a deliberate act.
when {
action == Intent.ACTION_SEND && type.startsWith("image/") -> {
val uri = streamUri(intent) ?: return finish()
handleImage(uri)
}
action == Intent.ACTION_SEND && type.startsWith("text/") -> {
val qr = UpiQr.findIn(intent.getStringExtra(Intent.EXTRA_TEXT))
if (qr == null) { toast(R.string.toast_no_upi_qr); finish() }
else paySheet.handle(qr) { finish() }
}
else -> finish()
}
Three small decisions are worth copying:
else -> finish(). A share target should never sit open on an intent it doesn't understand.finish(). Found, NoUpiQr or Error. "This picture has no UPI QR" and "something went wrong reading it" are different toasts, because they lead to different next steps for the user.
EXTRA_STREAM is read differently on newer Android:
private fun streamUri(intent: Intent): Uri? =
if (Build.VERSION.SDK_INT >= 33) intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)
else intent.getParcelableExtra(Intent.EXTRA_STREAM)
The typed overload (API 33+) checks the class for you; the old one is deprecated, hence the narrowly scoped @Suppress("DEPRECATION") on this one function instead of the whole file. A null Uri is simply finish().
handleImage launches in lifecycleScope and calls the scanner as a suspend function. Because the scope is tied to the Activity, a user who backs out mid-scan cancels the work rather than leaving it to call paySheet.handle on a dead Activity.
savedInstanceState == null.@Suppress("DEPRECATION") to the one function that needs it.
Auto UPI QR is on Google Play: https://play.google.com/store/apps/details?id=com.ashwar.autoupiqr. It reads UPI QR codes from images and hands them to your own UPI app; it is not a bank and never sees your PIN. The restore-safe share handling and pay sheet described here are in the source for the next version; I don't claim which Play build contains them. Always check the payee name before entering your UPI PIN.