{"slug": "a-share-target-activity-that-must-not-run-twice-why-it-finishes-itself-when-it", "title": "A share-target Activity that must not run twice: why it finishes itself when Android restores it", "summary": "A developer of the Android app Auto UPI QR described a restore-safe pattern for share-target Activities: by checking savedInstanceState == null in onCreate and calling finish() on any restore, the app prevents a payment-adjacent share flow from re-running after Android recreates the Activity with its original intent. The app also finishes on unrecognized intents, distinguishes 'no QR found' from read errors, and uses the API 33+ typed getParcelableExtra overload for EXTRA_STREAM.", "body_md": "*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.*\n\n[Auto UPI QR](https://play.google.com/store/apps/details?id=com.ashwar.autoupiqr&referrer=utm_source%3Ddevto%26utm_medium%3Darticle) 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.\n\nA 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`.\n\nFor 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.\n\n```\noverride fun onCreate(savedInstanceState: Bundle?) {\n    super.onCreate(savedInstanceState)\n    if (savedInstanceState != null) { finish(); return }\n    ...\n}\n```\n\nA 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.\n\n``` php\nwhen {\n    action == Intent.ACTION_SEND && type.startsWith(\"image/\") -> {\n        val uri = streamUri(intent) ?: return finish()\n        handleImage(uri)\n    }\n    action == Intent.ACTION_SEND && type.startsWith(\"text/\") -> {\n        val qr = UpiQr.findIn(intent.getStringExtra(Intent.EXTRA_TEXT))\n        if (qr == null) { toast(R.string.toast_no_upi_qr); finish() }\n        else paySheet.handle(qr) { finish() }\n    }\n    else -> finish()\n}\n```\n\nThree small decisions are worth copying:\n\n`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.\n`EXTRA_STREAM` is read differently on newer Android:\n\n```\nprivate fun streamUri(intent: Intent): Uri? =\n    if (Build.VERSION.SDK_INT >= 33) intent.getParcelableExtra(Intent.EXTRA_STREAM, Uri::class.java)\n    else intent.getParcelableExtra(Intent.EXTRA_STREAM)\n```\n\nThe 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()`.\n\n`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.\n\n`savedInstanceState == null`.`@Suppress(\"DEPRECATION\")` to the one function that needs it.\nAuto UPI QR is on Google Play: [https://play.google.com/store/apps/details?id=com.ashwar.autoupiqr](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.", "url": "https://wpnews.pro/news/a-share-target-activity-that-must-not-run-twice-why-it-finishes-itself-when-it", "canonical_source": "https://dev.to/ashwar_sadh_f7abe6f82c8e9/a-share-target-activity-that-must-not-run-twice-why-it-finishes-itself-when-android-restores-it-1g88", "published_at": "2026-11-04 06:18:03+00:00", "updated_at": "2026-11-04 06:24:56.138298+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Auto UPI QR", "Google Play", "Android"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/a-share-target-activity-that-must-not-run-twice-why-it-finishes-itself-when-it", "markdown": "https://wpnews.pro/news/a-share-target-activity-that-must-not-run-twice-why-it-finishes-itself-when-it.md", "text": "https://wpnews.pro/news/a-share-target-activity-that-must-not-run-twice-why-it-finishes-itself-when-it.txt", "jsonld": "https://wpnews.pro/news/a-share-target-activity-that-must-not-run-twice-why-it-finishes-itself-when-it.jsonld"}}