Auto UPI QR 的分享目標 Activity 為何在 Android 還原時自行關閉
A share-target Activity that must not run twice: why it finishes itself when Android restores it
Auto UPI QR 的 Android 分享目標 Activity 在系統還原時會自行結束,避免同一分享意圖再次觸發付款流程。Activity 只在 savedInstanceState 為 null 的新啟動時處理分享;還原時立即 finish(),而掃描工作綁定生命週期,使用者退出便會取消。
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.
來源:DEV Community · dev.to