Skip to content

id5io/id5-sdk-android

Repository files navigation

ID5 Mobile SDK for Android

The ID5 Mobile SDK delivers the ID5 ID to Android apps. The host initialises the SDK with a partner id, optionally provides user signals (hashed email, hashed phone, partner-supplied user id), and observes the identifier through a listener. Initialisation is non-blocking, the SDK runs entirely on its own threads and exposes a single global provider for the rest of the app.

Contents

Usage / Integration

Requirements

Value
Min SDK 21 (Android 5.0)
Compile SDK 36
Java source / target 17
Required dependencies none
Optional dependency com.google.android.gms:play-services-ads-identifier for GAID (maid)

The SDK declares play-services-ads-identifier as compileOnly. Add it as a runtime dependency in the host app (directly or transitively via the Google Mobile Ads SDK) if maid should be sent on the fetch. Without it, GAID lookup returns the zero UUID and maid is omitted from the request.

On Android 13+ (API 33+), the host must also declare the com.google.android.gms.permission.AD_ID permission in its manifest:

<uses-permission android:name="com.google.android.gms.permission.AD_ID" />

Without it the lookup returns the zero UUID even when Play Services is present. See the Play Console advertising ID policy for the publisher-side obligations.

Install

The SDK is published to Maven Central.

build.gradle.kts (app module):

dependencies {
    implementation("io.id5:id5-sdk-core:0.1.0")

    // Optional, enables GAID (maid) on the fetch request.
    implementation("com.google.android.gms:play-services-ads-identifier:+")
}

Or, Groovy build.gradle:

dependencies {
    implementation 'io.id5:id5-sdk-core:0.1.0'

    // Optional, enables GAID (maid) on the fetch request.
    implementation 'com.google.android.gms:play-services-ads-identifier:+'
}

The SDK ships its own consumer-rules.pro with -keep rules for the public API (io.id5.sdk.core.**). No additional ProGuard / R8 configuration is required in the host app when minification is enabled in release builds.

Quick start

Initialise the SDK once you have everything you need to pass to it (partnerId and any user signals like hashedEmail, hashedPhone, partnerUserId). Initialising before signals are known means they will be missing from the first fetch; the SDK does not pick up signal changes from a second initialize() call (it is a no-op after the first, see Advanced for reset()).

Typical placement is Application.onCreate() when all signals are known at app start, or right after the host has them (e.g. post-login).

Java:

public class MyApp extends Application {
    @Override
    public void onCreate() {
        super.onCreate();

        PartnerConfig partner = PartnerConfig.builder()
                .partnerId(<your-partner-id>)
                .hashedEmail(HashedSignal.fromEmail("user@example.com"))   // optional
                .build();

        ProviderConfig config = ProviderConfig.builder()
                .partner(partner)
                .build();

        ID5SDK.initialize(this, config);
    }
}

Kotlin:

class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()

        val partner = PartnerConfig.builder()
            .partnerId(<your-partner-id>)
            .hashedEmail(HashedSignal.fromEmail("user@example.com"))   // optional
            .build()

        val config = ProviderConfig.builder()
            .partner(partner)
            .build()

        ID5SDK.initialize(this, config)
    }
}

Attach a listener anywhere in the app. If an outcome has already been delivered, the listener replays it immediately:

Java:

ID5IdProvider provider = ID5IdProvider.getInstance();

ID5IdListener listener = new ID5IdListener() {
    @Override public void onId5IdReady(ID5Id id) {
        // id.userId() is the encrypted ID5 ID
    }
    @Override public void onNoId(NoIdReason reason) {
        // RESTRICTED, NO_CONSENT, or UNKNOWN
    }
    @Override public void onError(Exception error) {
        // ConsentTimeoutException, ProvisioningTimeoutException, or network/HTTP error
    }
};

provider.addListener(listener);

Kotlin:

val provider = ID5IdProvider.getInstance()

val listener = object : ID5IdListener {
    override fun onId5IdReady(id: ID5Id) { /* id.userId() is the encrypted ID5 ID */ }
    override fun onNoId(reason: NoIdReason) { /* RESTRICTED, NO_CONSENT, UNKNOWN */ }
    override fun onError(error: Exception) { /* timeouts or network/HTTP error */ }
}

provider.addListener(listener)

The SDK holds listeners weakly. Retain a strong reference (a field, not a local) for as long as you want callbacks, or the listener can be garbage-collected before it ever fires.

The SDK waits for the host CMP to publish a complete IAB consent snapshot before it fetches. Make sure a CMP is integrated and runs at app start; see Consent for details.

Callbacks run on the SDK's internal worker thread. To dispatch elsewhere (e.g. the main thread), see Advanced and Threading.

Consent

The SDK is consent-gated. It will not fetch until a complete IAB consent snapshot is available, and certain consent states block local caching.

CMP requirement

The SDK does not present any UI and does not call user-facing consent APIs. Integrate a Consent Management Platform (CMP) that writes the IAB TCF v2 / GPP keys to the host app's default SharedPreferences, and initialise it before ID5SDK.initialize(...). If the CMP has not produced a complete snapshot by the time the SDK starts, the SDK waits.

Keys the SDK reads

The default Consent Provider reads from PreferenceManager.getDefaultSharedPreferences(applicationContext):

Key Role
IABTCF_gdprApplies GDPR applies flag (1 / 0)
IABTCF_TCString TCF v2 consent string
IABTCF_PurposeConsents Per-purpose consent bits
IABTCF_VendorConsents Per-vendor consent bits (1-indexed, bit 131 = ID5)
IABGPP_HDR_GppString GPP consent envelope
IABGPP_GppSID GPP active section IDs (section 2 = TCFEU2)
IABGPP_TCFEU2_PurposeConsents Per-purpose consent bits, GPP-nested fallback
IABGPP_TCFEU2_VendorConsents Per-vendor consent bits, GPP-nested fallback
IABUSPrivacy_String US Privacy (CCPA) string

Initialisation order

  1. Application starts.
  2. CMP initialises (and shows its banner if needed under EU rules).
  3. CMP writes the keys above to SharedPreferences.
  4. ID5SDK.initialize(...).

Calling initialize before the CMP is wired up is safe, the SDK simply waits. To bound that wait, set consentTimeout(...) (milliseconds) on the ProviderConfig. Default is 0, which means wait forever. On timeout the listener receives ConsentTimeoutException through onError, like any other failure, and the SDK does not retry on its own until consent is available, call refreshNow() (or restart the process) to try again. See Reading the ID for what happens to the listener and the configured refresh strategy after an error.

Device access

Once consent is complete, the SDK reads two TCF signals to decide whether it may access the device. The same rule gates both the on-device cache and the Play Services GAID lookup:

  • Under GDPR (IABTCF_gdprApplies == 1, or GPP section 2 active): Purpose 1 must be granted and vendor 131 (ID5) must be granted. Otherwise the cache is cleared, the GAID is never read (maid is omitted from the fetch), and the SDK proceeds without them (fetch still fires).
  • Outside GDPR: device access is always allowed; Purpose 1 and vendor consent are not consulted.

CCPA opt-out does not block device access; it is enforced server-side by the fetch.

Custom Consent Provider

If the host app sources consent from somewhere other than the default SharedPreferences (a non-IAB CMP, a wrapping library, etc.), pass a custom ConsentProvider to the ProviderConfig. See Advanced.

Reading the ID

Two ways to consume the ID:

1. Listener (recommended). Notified on every delivery, including the replay of the most recent outcome when the listener is added.

provider.addListener(new ID5IdListener() {
    @Override public void onId5IdReady(ID5Id id) {
        String userId = id.userId();          // encrypted ID5 ID
        boolean fresh = !id.isFromCache();    // true on a network delivery
        // ...
    }
    @Override public void onNoId(NoIdReason reason) { /* ... */ }
    @Override public void onError(Exception error) { /* ... */ }
});

2. One-shot read. Returns the most recently delivered ID, or null if none has been delivered yet (consent still pending, fetch still in flight, or the SDK has been stopped).

ID5Id id = provider.getRecentId5Id();
if (id != null) {
    String userId = id.userId();
}

Prefer the listener. getRecentId5Id() is a snapshot that may not yet exist at the moment of the call; a listener guarantees a callback as soon as the SDK has something to deliver.

Errors and listener lifecycle

onError carries any failure the SDK could not recover from: ConsentTimeoutException, ProvisioningTimeoutException, or a network / HTTP error. None of them unregister the listener, it stays attached until the host calls removeListener(...) or stop(). A late successful fetch can still reach the same listener afterwards (e.g. after a ProvisioningTimeoutException, where the underlying fetch keeps running and a later success delivers onId5IdReady).

The listener is the host's hook for reacting to any outcome; the SDK does not impose a policy of its own. Handle an error however the app needs from inside the callback: call stop() or removeListener(...) to tear down, refreshNow() to retry, or nothing at all to wait for the next delivery.

An error never changes the configured refresh strategy, and does not by itself schedule a retry: under scheduledAuto the timer is re-armed only after a successful fetch, so a failed refresh pauses auto-refresh until the gate next transitions to allowed or you call refreshNow(). Under ON_ID_ACCESS the next stale read re-triggers a refresh (rate-limited to one per 30s).

What ID5Id carries

ID5Id is an immutable record:

Field Type Meaning
userId String The encrypted ID5 ID.
signature String ID5 SDK internal value. Server-issued signature paired with userId; the SDK sends it back on every refresh fetch for cache chaining. Treated as opaque by the host.
isFromCache boolean true when this delivery came from the on-device cache, false when it came from a fresh network response.
createdAtMs long Epoch-ms when the server first issued this ID. 0 if the server omitted it.
fetchedAtMs long Epoch-ms when the SDK received this response. 0 if unknown.
expiresAtMs long Epoch-ms after which the SDK considers this delivery expired. Used by refresh strategies.
eidsAsJson Map<String, JSONObject> Per-EID payloads keyed by name (e.g. "id5id", "euid", "gpid"); the actual set depends on the partner's server-side configuration. Each value is the EID's JSON object, compatible with the OpenRTB extended-identifier format. Pass-through, the SDK does not interpret the contents. Unmodifiable, never null.

Signals

The SDK sends a set of signals on each fetch. Some are collected automatically from the platform; others are supplied by the integrator on the PartnerConfig.

Integrator-supplied signals

All optional. Set what the host has at initialisation time.

Field Sent as Notes
hashedEmail hem SHA-256 hex of a normalised email. Build with HashedSignal.fromEmail("user@example.com") (raw, normalisation handled by the SDK) or HashedSignal.fromHash("<64-hex>") (already hashed).
hashedPhone phone SHA-256 hex of a normalised phone (digits only, leading + preserved). Build with HashedSignal.fromPhone("+44 20 7946 0958") or HashedSignal.fromHash("<64-hex>").
partnerUserId puid Pass-through identifier supplied by the partner. Verbatim, not hashed.
allowedVendors allowed_vendors List<String> of ID5 partners consented for all GDPR purposes, passed verbatim. Each entry is either the partner's IAB GVL ID (e.g. "131" for ID5 itself) or an ID5 partner id of the form "ID5-xxx" (e.g. "ID5-478"; ask your ID5 representative before using this form). Only list vendors for which the host has obtained the necessary consent.

Email and phone normalisation matches the iOS SDK and the ID5 server-side expectation:

  • Email: trimmed, lowercased, then for Gmail / Googlemail addresses the local part has +... suffixes stripped and dots removed.
  • Phone: trimmed, all non-digit characters removed, a leading + is preserved if present.

fromEmail / fromPhone return null if the input is unusable (syntactically invalid email, or a phone that contains any letter or fewer than 5 digits). fromHash returns null if the string is not a 64-character hex.

HashedSignal email = HashedSignal.fromEmail(rawEmail);

PartnerConfig partner = PartnerConfig.builder()
        .partnerId(<your-partner-id>)
        .hashedEmail(email)             // may be null, that's fine
        .partnerUserId("crm-12345")
        .allowedVendors(List.of("33", "131"))
        .build();

Automatically collected signals

Sent as Source
appid Context.getPackageName() by default; override via PartnerConfig.applicationId(...).
ver, appVersion Literal "Unknown" by default; override via PartnerConfig.applicationVersion(...).
ua WebSettings.getDefaultUserAgent(context).
accept_language Current device locale (single tag).
maid, maid_type GAID via Play Services. Read only when consent grants device access (see Consent). Omitted when device access is not granted, Play Services is missing, the AD_ID permission is missing on Android 13+, or limit-ad-tracking is enabled. maid_type = "GAID".
origin, originVersion Hardcoded: "android-sdk" and ID5SDK.VERSION.

Set applicationVersion explicitly; the "Unknown" default carries no debugging value.

Recommended setup

A short checklist to get the strongest signal coverage and avoid silent omissions in the fetch:

  • Integrate a CMP that writes the IAB TCF v2 / GPP keys to default SharedPreferences and runs before ID5SDK.initialize(...). See Consent.
  • Add play-services-ads-identifier as a runtime dependency (directly, or transitively via the Google Mobile Ads SDK) so the SDK can read GAID.
  • Declare com.google.android.gms.permission.AD_ID in the host manifest for Android 13+ (API 33+). Without it, GAID lookup returns the zero UUID even when Play Services is present.
  • Pass hashedEmail and/or hashedPhone when the host has them. Prefer HashedSignal.fromEmail(raw) / HashedSignal.fromPhone(raw) so the SDK applies the canonical normalisation before hashing; the iOS SDK applies the same rules, so the resulting hash is consistent across platforms. Use HashedSignal.fromHash(hex) only when the value already arrives pre-hashed from another source. See Passing partner data to ID5 for the normalisation rules.
  • Set applicationVersion on the PartnerConfig. The default "Unknown" carries no debugging value.
  • Initialise late enough that the signals the host wants to send are already known. ID5SDK.initialize(...) is one-shot per process; signal changes after the first call require ID5SDK.reset() (see Advanced).

Configuration reference

PartnerConfig

Field Default Required Notes
partnerId yes Positive integer assigned by ID5. Validated at build() time.
applicationId Context.getPackageName() no Sent as appid. Override when the platform value is missing or differs from what ID5 should attribute the traffic to.
applicationVersion "Unknown" no Sent as both ver and appVersion. Set this explicitly.
hashedEmail no HashedSignal. Sent as hem. See Signals.
hashedPhone no HashedSignal. Sent as phone. See Signals.
partnerUserId no Pass-through identifier. Sent as puid.
allowedVendors no List<String> of vendor identifiers. Sent verbatim as allowed_vendors.

ProviderConfig

Field Default Required Notes
partner yes The PartnerConfig.
refreshStrategy RefreshStrategy.ON_START_ONLY no See Refresh strategies.
consentProvider CMP-backed ConsentProvider reading default SharedPreferences no See Consent and Advanced.
consentTimeout (ms) 0 (wait forever) no Maximum wait for the Consent Provider before the listener receives ConsentTimeoutException.
provisioningTimeout (ms) 0 (disabled) no Maximum wall-clock time for the whole initial provisioning flow (consent + fetch + first delivery). On timeout the listener receives ProvisioningTimeoutException; a late successful fetch is still applied (listeners may observe error followed by onId5IdReady). Should be >= consentTimeout.
fetchEndpoint https://api.id5-sync.com/gac/v1 no Override is intended for testing / staging environments only.

Both configurations are immutable, built with a fluent Builder. partner is the only required field on ProviderConfig; everything else has a sensible default.

Refresh strategies

All three strategies fetch once on initialize (subject to consent and cache rules); they differ in what happens after the first delivery. Pick the one that matches the host's runtime.

Use case Strategy
App that just reads the ID once near start, refreshes only on host-triggered events (login, profile change). RefreshStrategy.ON_START_ONLY (default)
Long-running app that needs the ID to stay fresh in the background while it is active. RefreshStrategy.scheduledAuto(gate)
App that reads the ID rarely and prefers refresh on read instead of a timer. RefreshStrategy.ON_ID_ACCESS

ON_START_ONLY (default)

Fetch once on start, then idle. refreshNow() (see Advanced) is the only way to refresh after that. Suitable for short-lived sessions or for apps that explicitly drive refreshes from lifecycle events of their own.

scheduledAuto(gate)

Fetch once on start, then schedule the next refresh at the response's expiresAt. Each refresh response resets the timer to its own expiresAt.

Scheduled refreshes should align with the host app's runtime expectations. The SDK does not assume what "an appropriate moment" means for the integrator's product; the integrator supplies a RefreshGate that reports when a scheduled refresh is currently allowed to fire.

ProviderConfig config = ProviderConfig.builder()
        .partner(partner)
        .refreshStrategy(RefreshStrategy.scheduledAuto(new ForegroundGate()))
        .build();

A scheduled refresh fires only when both conditions hold: the previous response has reached its expiresAt and the gate currently reports allowed. When the gate flips to blocked, any pending scheduled refresh is cancelled until it allows again.

The typical implementation bridges the app's foreground / background lifecycle. For continuously-active devices (kiosks, in-vehicle dashboards, headless services) use the built-in RefreshGate.unrestricted():

RefreshStrategy.scheduledAuto(RefreshGate.unrestricted())

refreshNow() always bypasses the gate.

ON_ID_ACCESS

Fetch once on start, then idle until the host reads the provider past the response's expiresAt. That read returns the cached value immediately and triggers an asynchronous refresh in the background; the next read sees the refreshed value. Self-throttled to at most one refresh per 30 seconds.

Suitable for apps that read the ID rarely and would prefer not to maintain a timer at all.

RefreshGate contract

RefreshGate is an interface the integrator implements for scheduledAuto:

public interface RefreshGate {
    void register(RefreshGateListener listener);
    void release();
}

public interface RefreshGateListener {
    void onRefreshAllowed();
    void onRefreshBlocked();
}
  • register(listener) is called once by the SDK. The implementation must call the listener synchronously inside register(...) to report the initial state, then call it asynchronously on every transition.
  • release() is called once from ID5IdProvider.stop(). The implementation should tear down any resources (lifecycle observers, receivers) it set up in register(...).
  • Listener callbacks may come from any thread; the SDK serialises them internally.

Advanced

Listener executor

By default, listener callbacks run on the SDK's internal worker thread (see Threading). To dispatch them somewhere else, pass an Executor. The host app is the only place that knows which thread is appropriate for its architecture (main thread, a UI scope, a coroutine dispatcher, a worker pool, etc.). For example, dispatching to the Android main thread looks like this:

Handler main = new Handler(Looper.getMainLooper());
provider.addListener(listener, main::post);

The Executor can be anything that implements java.util.concurrent.Executor.

Forcing a refresh

refreshNow() triggers a fetch regardless of strategy. It bypasses the cache lookup and (under scheduledAuto) the RefreshGate. Concurrent calls are deduplicated to a single network request.

provider.refreshNow();

Typical triggers: user logged in / out, CMP banner dismissed, profile updated.

Custom ConsentProvider

Use a custom provider when the host sources consent from somewhere other than the default SharedPreferences (a non-IAB CMP, a wrapping library, a server-pushed value, tests).

public interface ConsentProvider {
    Consent getConsent();
}

The provider must be safe to call from any thread and must return quickly (the SDK calls it from its hot path). Two built-in factories are available:

ConsentProvider.cmp(context)             // default; reads default SharedPreferences
ConsentProvider.constant(staticConsent)  // returns a fixed Consent on every call

Consent is a record carrying the same fields the SDK reads from SharedPreferences. gdprApplies is a nullable Boolean; every other field is a typed wrapper extending ConsentSignal<String> (TcfConsentString, PurposeConsents, VendorConsents, UsPrivacyString, GppString, GppSid) with a .raw() accessor and a NONE sentinel for "CMP did not write the key". For convenience, Consent.ofRaw(gdprApplies, tcfConsentString, purposeConsents, vendorConsents, usPrivacyString, gppString, gppSid) builds a Consent from raw strings, wrapping each (and mapping null to the corresponding NONE).

Custom RefreshGate

Implement RefreshGate to bridge scheduledAuto to the host's notion of "currently active". Sketch using ProcessLifecycleOwner:

class ForegroundGate implements RefreshGate {
    private final LifecycleEventObserver observer = (source, event) -> { /* report */ };
    private RefreshGateListener listener;

    @Override public void register(RefreshGateListener listener) {
        this.listener = listener;
        boolean started = ProcessLifecycleOwner.get()
                .getLifecycle().getCurrentState()
                .isAtLeast(Lifecycle.State.STARTED);
        if (started) listener.onRefreshAllowed(); else listener.onRefreshBlocked();
        ProcessLifecycleOwner.get().getLifecycle().addObserver(observer);
    }

    @Override public void release() {
        ProcessLifecycleOwner.get().getLifecycle().removeObserver(observer);
    }
}

See Refresh strategies for the contract.

Stopping and resetting

ID5IdProvider.stop() shuts down the current provider: cancels scheduled work, releases resources, drops listeners, clears in-memory state. The singleton reference held by ID5SDK is not cleared; a subsequent ID5SDK.initialize(...) returns the stopped instance with a warning and the new config is ignored.

ID5SDK.reset() calls stop() on the current provider and clears the singleton reference, so the next ID5SDK.initialize(...) creates a fresh provider. Use this to swap configuration mid-process (e.g. picking up new signals after login).

ID5SDK.reset();
ID5SDK.initialize(context, newConfig);

Production apps typically initialise once per process and rely on process death to release resources; reset() is mainly useful in tests and in narrow configuration-change scenarios.

Trace mode

ID5SDK.setTraceEnabled(true) adds _trace=true to every outgoing fetch payload, which makes the server return a verbose response useful for ID5 support when diagnosing a specific issue. Toggleable at runtime from any thread; off by default. Leave off in production; enable only when ID5 support asks for it.

Threading

The SDK runs entirely off the caller's thread. ID5SDK.initialize(...) returns immediately; consent polling, the fetch, cache I/O and timer scheduling all happen on the SDK's own pools. No host thread is blocked by the SDK at any point. The goal is to run in the background out of the host app's way and deliver the best ID5 ID it can, as soon as it can.

Threads the SDK owns

Name Count Lifetime Purpose
id5-sdk-id-provider 1, daemon Same as the provider Single worker that runs the provisioning state machine: consent polling, cache lookup, GAID lookup, fetch dispatch, response handling, listener delivery.
id5-sdk-scheduler 1, daemon Same as the provider Scheduled-executor backing the timers used by RefreshStrategy.scheduledAuto and RefreshStrategy.ON_ID_ACCESS.

All threads are daemons, none of them keep the JVM alive once the host app exits.

Where listener callbacks run

By default, listener callbacks are invoked synchronously on id5-sdk-id-provider, the same worker that produced the outcome. The SDK does not impose a thread choice on the host because the host knows which thread suits its architecture (UI thread, coroutine dispatcher, worker pool, etc.). Pass an Executor on addListener(listener, executor) to dispatch elsewhere; see Advanced.

Implications for the host

  • The SDK never calls into a Handler, Looper or Activity by itself; it cannot leak a UI context.
  • Listener implementations should not block the worker. If a callback needs to do heavy work (database writes, network calls, large JSON parses), either dispatch it via an executor or marshal it onto the host's own thread.
  • The pool names above appear verbatim in profilers, ANR traces and crash reports, useful when triaging issues that touch the SDK.

Privacy and Play Console Data Safety

What the SDK collects

The SDK sends a request to https://api.id5-sync.com/gac/v1 on initialisation and on every refresh. The full request payload is described in Signals; the privacy-relevant entries are:

Sent as Source When omitted
hem PartnerConfig.hashedEmail (hashed by the SDK or supplied pre-hashed by the host) When the host does not provide it.
phone PartnerConfig.hashedPhone (same) When the host does not provide it.
puid PartnerConfig.partnerUserId (verbatim) When the host does not provide it.
maid, maid_type GAID via Play Services AdvertisingIdClient, read only when consent grants device access (see Consent) When consent does not grant device access (TCF Purpose 1 + ID5 vendor consent under GDPR), Play Services is missing, AD_ID permission is missing on Android 13+, or the user has Limit Ad Tracking enabled.
gdpr, gdpr_consent, us_privacy, gpp_sid, gpp_string Default Consent Provider (IAB keys in default SharedPreferences) When the host CMP did not write them.
appid, ver, appVersion, ua, accept_language Platform (package name, app version, default user agent, current locale) Not omitted.

The table above lists everything the SDK collects from the device or the host that could be considered personal or identifying.

The fetch payload also carries a small set of internal protocol fields, used purely for SDK housekeeping (request identification, version tagging, cache continuity, debug switches). They are not derived from the device or the user, they do not identify a person, and they exist solely to make the request / response cycle work:

Field Role
partner The partner id from PartnerConfig.
origin, originVersion Hardcoded SDK identifier and version.
signature Server-issued signature returned with the previous response, sent back so the server can chain fetches. Opaque to the host (see Reading the ID).
nb Counter of how many times the cached ID has been delivered to listeners since the last fetch wrote it.
_trace Boolean, sent only when trace mode is on.

The SDK does not read or transmit anything beyond the fields documented in Signals, Consent, and this internal-protocol table.

Local storage

The SDK persists its cache in a private SharedPreferences file:

Value
File name id5_sdk_cache
Mode MODE_PRIVATE (no other app on the device can read it)
Scope One namespace per partner (every key is suffixed _{partnerId})

Keys written:

Key Type Purpose
response_json_{partnerId} String Raw server response, served on cache hits.
cache_timestamp_{partnerId} long Timestamp of last save, used for expiry / staleness.
consent_hash_{partnerId} String (SHA-256 hex) Digest of the consent snapshot at save time, used to detect consent changes.
provisioned_count_{partnerId} int Number of times the cached ID has been delivered since the last fetch wrote it.
no_id_reason_{partnerId} String Cached "no id" reason, mutually exclusive with response_json.

The cache is cleared automatically when consent revokes device access (see Consent), when an entry is older than 90 days, or when the stored payload cannot be parsed.

Permissions

Permission Required for Notes
android.permission.INTERNET All SDK traffic Implied by AGP for application modules; declare explicitly for libraries or stripped manifests.
com.google.android.gms.permission.AD_ID GAID lookup on Android 13+ See Requirements.

Play Console Data Safety form

When publishing an app that integrates the SDK, declare the data the SDK contributes on the publisher's behalf. The exact wording depends on the host app's overall data collection, but the SDK alone contributes:

Google category Data type SDK fields Collected Shared with third parties Optional Purpose
Device or other IDs Device or other IDs GAID (maid) Yes, when available and consent grants device access Yes (ID5) Yes (user can reset / opt out via OS) Advertising or marketing
Personal info User IDs partnerUserId, hashed email, hashed phone, returned ID5 ID Only when explicitly supplied by host into PartnerConfig Yes (ID5) Yes Advertising or marketing

The category and data-type names follow Google's Data safety taxonomy. This mapping is guidance for a typical integration, not legal advice: you (the publisher) are the data controller and remain responsible for your final declaration, so confirm the exact categories with your own legal or privacy team. The SDK contributes no other Data safety categories (no location, contacts, messages, financial, health, or app-activity data).

Troubleshooting

Listener never fires

The SDK waits for a complete IAB consent snapshot before fetching. If the listener never receives anything:

  • Verify the CMP is initialised before ID5SDK.initialize(...) and is actually writing IAB keys to the default SharedPreferences. A quick check from a debug build:
    SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
    Log.d("ID5", "TCString=" + prefs.getString("IABTCF_TCString", null));
    Log.d("ID5", "GPP=" + prefs.getString("IABGPP_HDR_GppString", null));
  • Under GDPR, the keys appear only after the user interacts with the CMP banner. Without interaction the SDK waits forever (consentTimeout defaults to 0). Set a non-zero consentTimeout(...) if a deterministic outcome is needed; on timeout the listener receives ConsentTimeoutException.
  • Check that a strong reference to the listener is retained. The SDK holds listeners weakly, a listener created as a local variable can be garbage-collected before it fires.

getRecentId5Id() returns null

null means the SDK has not delivered an outcome yet (consent still pending, fetch in flight, or stop() has been called). It is not an error state. Use a listener, the SDK will call back as soon as it has something; if an outcome has already been delivered, addListener(...) replays it immediately.

SDK logs

The SDK writes to Android logcat under the tag ID5-SDK. Severity follows the standard Android Log.isLoggable(...) check, so the minimum level can be raised or lowered at runtime without rebuilding the SDK.

Filter SDK output:

adb logcat -s ID5-SDK:*

In Android Studio's Logcat tab, set the tag filter to ID5-SDK.

Set the minimum severity:

adb shell setprop log.tag.ID5-SDK DEBUG

Replace DEBUG with VERBOSE, INFO, WARN, ERROR or ASSERT as needed. The setting is per-device and persists until reboot or another setprop. The default level is whatever the device reports for an unset tag (typically INFO).

Each line is prefixed with the SDK thread name and the originating class, so a line that begins with [id5-sdk-id-provider] is from the main worker, [id5-sdk-scheduler] from the refresh timer, etc. (see Threading).

GAID is always missing on the fetch

maid and maid_type are omitted when:

  • Consent does not grant device access: under GDPR, TCF Purpose 1 and ID5 vendor consent (131) are required before the SDK reads the GAID at all. See Consent.
  • The host did not add com.google.android.gms:play-services-ads-identifier as a runtime dependency, see Requirements.
  • The host manifest does not declare com.google.android.gms.permission.AD_ID on Android 13+ (API 33+).
  • The user has Limit Ad Tracking enabled in device settings.
  • Play Services is missing on the device (older devices without Google services).

The middle two are host-side configuration; the rest are user / device state and not something the integrator can change.

ProvisioningTimeoutException

The whole initial flow (consent + fetch + first delivery) did not complete within provisioningTimeout. Typical causes:

  • provisioningTimeout is too tight for the CMP's wait time plus a round-trip on a slow network. The fetch itself can take seconds on a cold network connection; combined with a CMP banner that the user has to dismiss, a sub-2-second budget is unrealistic.
  • The CMP is not writing IAB keys at all (also triggers ConsentTimeoutException first if consentTimeout < provisioningTimeout).

A late successful fetch is still applied after the timeout, so a listener may observe onError followed by a later onId5IdReady.

Surfacing SDK threads in profilers and crash reports

SDK threads carry the names id5-sdk-id-provider and id5-sdk-scheduler, see Threading. Filtering on those names isolates SDK activity from host activity when triaging ANRs, crashes or CPU traces.

Sample app

A minimal integration is provided in sample-app/. Use it as a reference for how to wire the SDK into an app, the same pattern shown in Quick start: build a PartnerConfig, build a ProviderConfig, call ID5SDK.initialize(...), attach a listener.

The sample is not a typical end-user app. It is built as an inspection / debugging surface: it shows the delivered ID5Id, the consent snapshot the SDK observed, the timing of the fetch and the cache state. Useful when verifying a fresh integration end to end, or when reproducing an issue against a staging environment.

Once a partner id has been issued, plug it into the sample's PartnerConfig.partnerId(...) and run it on a device or emulator to validate the round-trip with real ID5 traffic before integrating into the host app.

License

Apache License 2.0, see LICENSE.

Local development

Open the project in Android Studio (recommended, handles the Android SDK / Gradle toolchain out of the box). From the command line:

# build the SDK
./gradlew :id5-sdk-core:assembleRelease

# unit tests
./gradlew :id5-sdk-core:test

# WireMock-backed end-to-end tests
./gradlew :integration-tests:test

# run the sample app (or use Android Studio's Run on :sample-app)
./gradlew :sample-app:installDebug