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.
The restore problem
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.
One line
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.
The rest is dispatch by what was shared
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:
- Unknown shape means finish. Anything that is not an image or text send goes to
else -> finish(). A share target should never sit open on an intent it doesn't understand. - Every path ends in
finish(). Found, not found and error all close the Activity (the success path closes via the pay-sheet callback). There is no state where an invisible Activity lingers in the back stack. - Distinct outcomes, distinct messages. Scanning returns a sealed result:
Found,NoUpiQrorError. "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.
The Parcelable API split
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().
Scanning off the main thread
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.
Takeaways
- A share target that acts on an intent should act only when
savedInstanceState == null. - Make "I don't understand this", "nothing found" and "failed" three different exits.
- Finish on every path; never leave an invisible Activity behind.
- Scope the coroutine to the lifecycle so back-out cancels the scan.
- Confine
@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.