androidinterview.com

Android System Design Interview Questions

Can we identify the users who have uninstalled our application?

Tier: Less commonDifficulty: Medium

There is no callback. Android never tells your app it has been uninstalled, because by then nothing of yours is left running to hear it. You infer it instead, and there are two signals worth naming. Firebase Analytics collects an app_remove event when a package is removed from an Android device. The other signal is delivery, since a push token that comes back as unregistered from FCM means that token is no longer usable, which can happen for reasons other than uninstalling.

What I'd clarify first

  • Is this feeding a reengagement campaign or a churn dashboard. One has to be right about a named person, the other only has to be right in aggregate.
  • Do we already run Firebase Analytics with the BigQuery export, because that decides whether the supported route is even available.
  • Is the app on Play with Google Play services present, or sideloaded into a market where it is not.

The supported signal, app_remove

Firebase Analytics collects app_remove automatically when an application package is uninstalled. It is Android only, and it counts the removal whatever the app was installed from, which is where it differs from Play Console's uninstall number. Reading it per user means the BigQuery export rather than the dashboard, because the dashboard aggregates.

The event arrives against the pseudonymous app instance rather than against your own user id. So you do the join yourself, by storing the Firebase app instance id next to your user record while the app is still installed. Two caveats are worth saying out loud. It depends on Google Play services being present on the device, and it depends on the user not having opted out of analytics collection.

The fallback, a dead push token

If you are not on Firebase Analytics, infer it from push delivery. Consume failures from ordinary, justified notification delivery and maintain token freshness. With the FCM HTTP v1 API a token that no longer belongs to an installed app fails with HTTP 404 and the error UNREGISTERED. A 400 with INVALID_ARGUMENT can mean the same thing, but only once you are sure the payload itself is valid.

Say UNREGISTERED and not NotRegistered. The legacy HTTP and XMPP APIs that returned the old name in a results array were shut down in 2024. The v1 API reports per message rather than per batch.

Why this is a signal, not a fact

  • Doze and App Standby defer a data only message, so a perfectly live install can miss the probe for a long time.
  • An app in the stopped state, force stopped or never launched since install, receives no FCM at all.
  • Clearing app data invalidates the token, and so does a rotation the app has not registered again yet.
  • FCM garbage collects an Android registration after 270 days of inactivity, so a token that still works is only weak evidence the install is alive.

Invalidate an UNREGISTERED token immediately. Keep installation reachability separate from account status, since repeated failures for one token cannot distinguish uninstall from data clearing or token replacement.

What you cannot get either way

Play Console reports uninstalls by acquisition channel, device and retention cohort, which is genuinely useful for a dashboard and tells you nothing about which named user left. There is no API that hands you "this user uninstalled", and that is working as intended rather than an oversight.

How I'd build this on Android

I'd track whether each installation can still receive messages, rather than mark a whole user account as uninstalled. One person can have several devices. A token can also become invalid after token rotation or clearing app data. UNREGISTERED means stop sending to that token and remove or disable it on the server. Sending again does not prove the person uninstalled the app.

// Android entry point delegates to an injected installation repository.
class PushTokenRegistrar(private val installations: InstallationRepository) {
    suspend fun onTokenChanged(token: String) {
        installations.persistPendingToken(token)
        installations.scheduleRegistration()
    }
}

I'd call the registrar when FCM reports a new token and check the current token again after signed in startup. FirebaseMessagingService.onNewToken is not a suspend function and cannot run indefinitely. Its adapter can do short work or save the registration and schedule an upload. The repository saves the token with its account and installation identity, and WorkManager sends it later. Server FCM credentials never belong in the APK.

There is no uninstall screen that needs a ViewModel or Compose subscription. The backend uses normal send failures and analytics signals, keeping track of where each signal came from. One unreachable phone does not mean the person has stopped using their account. I'd avoid high priority silent messages sent only to check whether the app is installed. Linking analytics identities also needs the app's consent and identity reset rules.

I'd check these cases.

  • Token rotation offline.
  • Two devices on one account.
  • Signing out before registration uploads.
  • An invalid token with an otherwise valid message.

A reinstall counts as a new installation unless the product explicitly supports linking it. See FCM token management and automatically collected analytics events.

Tradeoffs I'd call out

  • app_remove against delivery feedback. Analytics supports removal measurement where collection is available. Normal push failures support token cleanup but do not prove user level churn.
  • Precision against reach. app_remove needs Play services and analytics consent. Delivery feedback works where push works and describes token validity rather than a certain uninstall.
  • What you do with the answer. Anything built on this should treat uninstalled as very likely uninstalled and tolerate a false positive. The natural next step is a reengagement email or SMS to someone you can no longer push to, and that needs a consent you can point at.

Read more Optimize for Doze and App Standby (opens in a new tab)