Identifiers
Every identifier Android has ever offered has been narrowed, scoped or removed. The pattern is consistent: an identifier that survives uninstall and crosses apps is a tracking mechanism, and tracking mechanisms get regulated.
An identifier is the difference between "someone used the app" and "this specific person used the app, on this device, and can be found again". Android spent a decade deciding which of those an app is allowed to know.
TL;DR#
- Identifiers are classified by three properties: resettable, scoped, and survives uninstall.
- IMEI, serial and MAC are hardware identifiers. Apps lost access to all three between Android 9 and 10.
- ANDROID_ID was device-wide; since Android 8.0 it is scoped to the app-signing key.
- The Advertising ID is the sanctioned cross-app identifier: resettable by the user, and zeroed when the user opts out.
- App-set IDs (a UUID you generate and store) are the recommended approach for your own analytics and account linking.
- There is no identifier that is both stable across installs and available to every app. That is the point.
Mental model#
Think of identity documents.
A passport number is a hardware identifier: it identifies the device, it does not change, and it is the same for everyone who looks at it. Handing it out is a serious decision.
A library card is a scoped identifier: it identifies you to one institution, it is useless elsewhere, and it can be cancelled and reissued.
A loyalty number is resettable: you can ask for a new one, and the old one stops working.
The Android trajectory has been to move every identifier from passport toward library card — and to delete the ones that could not be reissued.
Where the analogy breaks#
A passport is issued by a state that knows who you are. Android's scoping is cryptographic and per-installation, and the "authority" is a signing key rather than a person. Also, an app can generate its own loyalty number and store it, which no real loyalty scheme allows — the constraint is not on creating identifiers, it is on sharing them.
The big picture#
Text description of this diagram
A comparison of five identifier families and what a normal app can do with each today. IMEI and serial number required only READ_PHONE_STATE historically and are now privileged-only, returning null or throwing for ordinary apps since Android 10. MAC addresses were readable and stable; since Android 10 they are randomised per network, including in scan results. ANDROID_ID was a device-wide value identical for every app; since Android 8.0 it is scoped to the app-signing key and the user. The Advertising ID is the sanctioned cross-app identifier, resettable and zeroed on opt-out, restricted by Play policy to advertising and analytics. An app-set ID is a UUID you generate and store, scoped to the installation, lost on uninstall, not shared with other apps, and the recommended default for your own backend. An account ID remains the only correct answer for cross-device identity.
The column that matters is "survives uninstall". An identifier that disappears when the app is uninstalled cannot be used to re-identify a user who deliberately started over.
Deep dive#
The taxonomy#
Android's own guidance classifies identifiers along three axes, and the classification is the most useful thing in the document:1
| Property | Meaning | Example |
|---|---|---|
| Resettable | The user can ask for a new value | Advertising ID |
| Scoped | The value differs per app or per signer | ANDROID_ID since 8.0 |
| Survives uninstall | The value is the same after reinstall | Hardware identifiers (historically) |
A good identifier for analytics is resettable and scoped. A hardware identifier is neither, which is why it was the first thing removed.
Hardware identifiers#
IMEI (the modem's identity), serial number and MAC address were all readable by ordinary apps in early Android. All three are now restricted:
- Android 9: background sensor and network access tightened; non-SDK interfaces restricted.
- Android 10:
READ_PRIVILEGED_PHONE_STATEis required for IMEI and serial; ordinary apps getnullor aSecurityException. MAC randomisation becomes standard for Wi-Fi scanning.2
The Android 10 change is worth reading precisely, because the migration advice is the interesting part: use a resettable identifier, or an app-scoped ID, or — best — nothing at all.
ANDROID_ID#
Settings.Secure.ANDROID_ID is a 64-bit value generated when the device is first set up. For years
it was device-wide and stable, which made it the unofficial tracking identifier of choice.
Since Android 8.0, it is scoped to the app-signing key and the user. Two apps signed by different keys see different values on the same device; the same app reinstalled with the same key sees the same value.3
That scoping is why ANDROID_ID is still usable for fraud detection and licence checks within one app, and useless for cross-app correlation.
// Scoped to the app-signing key and the user. Not a device identifier.
val androidId: String = Settings.Secure.getString(
contentResolver,
Settings.Secure.ANDROID_ID,
)The Advertising ID#
The Advertising ID is the sanctioned exception: a resettable, user-controllable identifier intended for advertising and analytics. It has two important properties:
- The user can reset it in Settings, which breaks the link to past activity.
- The user can delete it, which zeroes the value. Apps must handle an all-zeros ID.
On devices where the user has opted out of ad personalisation, the Advertising ID returns a zeroed value.
unverified(the Advertising ID's availability and the exact opt-out behaviour have changed with Privacy Sandbox work; confirm against the current Play services documentation)The Play policy around it is as important as the API: using the Advertising ID for anything other than advertising or analytics, or linking it to a persistent identifier, is a policy violation.
App-set IDs: the recommended approach#
For your own backend, the correct answer is an identifier you generate and store:
object InstallationId {
private const val PREFS = "installation"
private const val KEY = "id"
fun get(context: Context): String {
val prefs = context.getSharedPreferences(PREFS, Context.MODE_PRIVATE)
prefs.getString(KEY, null)?.let { return it }
val id = UUID.randomUUID().toString()
prefs.edit().putString(KEY, id).apply()
return id
}
}Three properties make this the right default:
- It is yours, so no platform change can revoke it.
- It is scoped to the installation, so it disappears on uninstall.
- It is not shared with other apps, so it is not a tracking vector.
The trade-off is that it is lost on reinstall and not shared across devices — which is exactly the property that makes it acceptable.
Why identifiers were locked down#
The regulatory backdrop is the GDPR's concept of personal data and the ePrivacy rules on device storage, but the immediate cause was simpler: an identifier that survives uninstall and crosses apps is a tracking mechanism, and once one exists, every analytics SDK uses it.
The Android 10 change and the Play policy that followed did not invent a new principle. They enforced one the platform had been inconsistent about for a decade.
What to do instead#
| Goal | Wrong | Right |
|---|---|---|
| Count installs | IMEI | Play Console install metrics, or an app-set ID |
| Recognise a returning user | ANDROID_ID | Your own account ID, or a stored app-set ID |
| Attribute an ad | IMEI or MAC | Advertising ID, within policy |
| Detect fraud | Device serial | Play Integrity API verdicts, plus your own signals |
| Sync across devices | Any device ID | The user's account |
The last row is the one teams resist. Cross-device sync is an account problem, and solving it with a device identifier works only until the user changes device — at which point it silently fails.
The questions to ask before using any identifier#
Four questions, in order. If any answer is uncomfortable, the identifier is wrong for the job.
Does it survive uninstall? If yes, it links a user across a decision they made to start over. That is the property regulators care about most.
Is it visible to other apps? If yes, it is a join key, and any SDK that reads it can correlate your app's data with another's.
Can the user reset it? If yes, a reset must actually work — the old value must stop appearing, which means you cannot cache it indefinitely.
What happens if it changes? Every identifier eventually changes: the user resets it, the device is replaced, the app is reinstalled. Your backend must treat a changed identifier as a new installation, not as an error.
The last question is the one teams miss. A login flow that keys an account to a device identifier breaks when the user upgrades their phone, and the failure looks like data loss.
The Play policy layer#
The platform APIs are only half the constraint. Google Play policy separately restricts how the
Advertising ID may be used, requires disclosure of the identifiers an app collects, and limits
which apps may request USE_EXACT_ALARM or the QUERY_ALL_PACKAGES permission.
That means a technically permitted call can still be a policy violation. The pattern is the same as with permissions: the platform decides what is possible, and the store decides what is allowed to be distributed. An app that reads the Advertising ID for analytics without declaring it is within the API and outside the policy.
How it evolved#
| API | Version | What changed |
|---|---|---|
| 18 | 4.3 Jelly Bean |
|
| 23 | 6.0 Marshmallow |
|
| 28 | 9 Pie |
|
| 29 | 10 |
|
| 30 | 11 |
|
| 31 | 12 |
|
| 33 | 13 Tiramisu |
|
| 37 | 17 Cinnamon Bun |
|
Under the hood#
- Settings.javawhere ANDROID_ID is defined and documented
- TelephonyManager.javathe IMEI APIs and their permission checks
- IdentifierService.javathe modern scoped-identifier service
Curiosity corner#
- ANDROID_ID is not random per app. It is derived deterministically from the signing key and the user, so two apps with the same signing key see the same value.
- A zeroed Advertising ID is a valid response, not an error. Treating it as "missing" and generating a fallback is a policy violation.
- MAC randomisation applies to scan results too, not just the device's own interface, which is why Wi-Fi-based proximity tracking got much harder.
Try it#
adb shell settings get secure android_id
Example output
a1b2c3d4e5f60718adb shell dumpsys package com.example.app | grep READ_PHONE_STATE
Example output
android.permission.READ_PHONE_STATE: granted=trueadb shell am start -a android.settings.ADVANCED_PRIVACY_SETTINGS
Example output
Settings → Privacy → Ads → Reset advertising IDBuild it#
Generate an app-set ID and store it. Then reinstall the app and observe that it changed. That observation — that the identifier is scoped to the installation — is the design, not a bug.
Check yourself#
Check yourself
Put these privacy changes in the order they arrived.
Drag, or use the arrows to reorder.
- 1Selected photos access
- 2Privacy Dashboard, mic and camera indicators, approximate location
- 3One-time permissions and permission auto-reset
- 4Runtime permissions
- 5Scoped storage and background location as a separate grant
Sources#
Sources
Every version number, API level, class name and date on this page is traced to one of these. Pages are re-verified against them; last_verified above says when.
- docsUnique identifiers best practicesdeveloper.android.comaccessed 2026-10-27↩
- docsBehavior changes: Apps targeting Android 10developer.android.comaccessed 2026-10-27↩
- docsPerubahan Perilaku Android 8.0developer.android.comaccessed 2026-10-27↩
- docsPerubahan perilaku: aplikasi yang menargetkan API level 28+developer.android.comaccessed 2026-10-27↩
- docsManifest.permission | API referencedeveloper.android.comaccessed 2026-10-27↩
- docsPrivacy Sandbox on Androiddeveloper.android.comaccessed 2026-10-27↩
- docsPermissions on Android | Privacydeveloper.android.comaccessed 2026-10-27↩