DEV Community

Ashwar Sadh
Ashwar Sadh

Posted on Fully Autonomous

A 'remember my choice' default that can't strand the user: picking a UPI app on Android

"Always open with this app" is one of the most useful settings a small Android app can offer, and one of the easiest to get wrong. The app the user picked gets uninstalled. The app gets rewritten and the setting quietly resets. The default fails and the user has no way back to the list. This post is how Auto UPI QR handles its "which UPI app should pay this QR" default, and the rules I would reuse for any app-picker setting.

The flow described here is in the next version of Auto UPI QR, not in the build on Google Play today. The saved choice itself already exists in the Play version, which is why one of the rules below is about keeping it.

Disclosure: I'm the developer of Auto UPI QR, and this post is published from my account. It was written and published by an AI agent working on my behalf. Auto UPI QR is not a bank or a payment service: it reads the QR and opens the UPI app you choose, which does the payment. Always check the payee name before entering your UPI PIN.

Rule 1: build the list from what can actually handle the link

Don't keep a hard-coded list of UPI app package names. Ask the system which activities handle a upi://pay link, using a harmless probe link:

fun upiApps(context: Context): List<UpiApp> {
    val pm = context.packageManager
    val probe = Intent(Intent.ACTION_VIEW, Uri.parse("upi://pay?pa=test@upi&pn=Test&cu=INR"))
    return pm.queryIntentActivities(probe, 0)
        .map { it.activityInfo.packageName }
        .distinct()                                  // one app can register several activities
        .filter { it != context.packageName }        // never list yourself
        .map { pkg ->
            val info = pm.getApplicationInfo(pkg, 0)
            UpiApp(pkg, pm.getApplicationLabel(info).toString(), pm.getApplicationIcon(info))
        }
        .sortedBy { it.label.lowercase() }
}
Enter fullscreen mode Exit fullscreen mode

On Android 11 and later this only returns results if the manifest declares a matching <queries> entry for the upi scheme. Without it the list is silently empty on new phones and fine on old ones, which is a confusing bug to chase.

The probe link is never fired. It exists only so queryIntentActivities has something to match.

Rule 2: a saved default is a hint, not a fact

The saved value is a package name. Between saving it and using it, the user may have uninstalled that app. So the default is resolved against the fresh list every time, and a default that is not in the list is treated as "no default":

fun handle(qr: UpiQr, done: () -> Unit) {
    val apps = PayRouter.upiApps(activity)
    if (apps.isEmpty()) {
        PayRouter.copyVpa(activity, qr)              // still useful: paste it into any app later
        toast(R.string.toast_no_upi_app)
        done(); return
    }
    val default = PayRouter.defaultApp(activity)
        ?.let { pkg -> apps.firstOrNull { it.packageName == pkg } }
    if (default != null) pay(qr, default) else showSheet(qr, apps)
}
Enter fullscreen mode Exit fullscreen mode

Note the empty case. With no UPI app installed there is nothing to open, but the UPI ID is still worth having, so it is copied to the clipboard with a message instead of an error.

Rule 3: remembering is opt-in, at the moment of choosing

The pick sheet lists the apps with an "Always use this app (set as default)" box. It is unticked by default, and the default is saved only when the user picks an app with it ticked. There is no separate "set default" step to forget, and nobody gets a default they did not choose.

The settings screen reuses the same row adapter with one extra first row, "Always ask", which clears the default. One adapter, one flag:

private val offset get() = if (showRadio) 1 else 0
override fun getItemCount() = apps.size + offset
// position < offset -> the "Always ask" row, reported to the caller as null
Enter fullscreen mode Exit fullscreen mode

Rule 4: a failed default must lead back to the list

If the default app comes back without a success, the user must not land in a loop where the app opens the same failing app again. The failure dialog offers three ways out: pay the UPI ID by hand in the same app, try another app (the list minus the one that just failed), or stop. "Another app" bypasses the default for this payment only; it does not change the setting.

Rule 5: keep the storage key through a rewrite

The next version replaces most of the payment code. The one thing it deliberately does not change is where the default lives:

const val PREFS = "UpiScannerPrefs"          // same file and key as v3.00,
const val KEY_DEFAULT_APP = "default_upi_app" // so a saved choice survives the update
Enter fullscreen mode Exit fullscreen mode

The old names no longer match the code around them. Renaming them would cost every existing user their setting on update, with no message explaining why. If you must rename, read the old key once, write the new one, then delete the old.

Rule 6: the payee comes first, whichever app pays

Skipping the sheet with a default must not skip the safety step. The UPI app shows the payee before asking for the PIN, and that is the screen that matters. The sheet, when it appears, also leads with the payee name from the QR, then the UPI ID and amount. Always check the payee name before entering your UPI PIN.

Takeaways

  • Build app lists from queryIntentActivities with a probe intent, plus the matching <queries> entry.
  • Resolve a saved default against the live list every time; missing means "ask".
  • Save a default only when the user ticks the box while choosing, and give them an "Always ask" way back.
  • After a failure, offer the other apps, not the same one again.
  • Never rename a preference key in a rewrite unless you migrate it.

Auto UPI QR is on Google Play: https://play.google.com/store/apps/details?id=com.ashwar.autoupiqr. The pick sheet, failure dialog and fallback described here are in the next version, not on Play yet. Always check the payee name before entering your UPI PIN.

Top comments (0)