Android — Setup
This page takes an Android app from no consent management at all to a release build with Sirdata CMP, one step at a time: requirements, installation, initialization, the consent UI, reading consent, the settings entry point, US users, testing, the release build and the upgrade from 1.2.0.
1. Requirements
minSdk
23 (Android 6.0)
compileSdk
34
JDK running Gradle
17
Android Gradle Plugin
8.2.2
Kotlin
1.9.22
Host activity
a ComponentActivity — AppCompatActivity and FragmentActivity are subclasses
compileSdk34 comes from the SDK's AndroidX dependencies.androidx.activity1.8.2,androidx.core1.12.0 and Compose Material 3 1.2.0 declare 34 as their minimum compile SDK, and the Android Gradle Plugin stops the build of an app that compiles against less.Build tools. Version 2.0.0 was verified with Android Gradle Plugin 8.2.2 and Kotlin 1.9.22, in a debug build and in a minified release build; the Views snippets on this page also compile with Kotlin 2.2.0. The shared core (
shared-android) is compiled to Java 21 bytecode, which your build's D8 and R8 must accept: AGP 8.2.2 does, and earlier versions were not tested. Your own module can keep targeting Java 17.Your app does not need Jetpack Compose. The consent UI is written in Compose, but the SDK brings its own Compose dependencies: an app built with Views works unchanged.
Your app needs androidx.activity and kotlinx coroutines on its compile classpath. The SDK's API takes a ComponentActivity and returns coroutine types (StateFlow, suspend functions), but it does not expose those libraries to your code. Without them, the call sites fail to compile with Cannot access class 'androidx.activity.ComponentActivity' or Cannot access class 'kotlinx.coroutines.flow.StateFlow'. An app that depends on AppCompat (androidx.appcompat:appcompat) or on androidx.activity:activity-compose already has both; otherwise add them:
// app/build.gradle.kts — only if your app has neither AppCompat nor activity-compose
dependencies {
implementation("androidx.activity:activity-ktx:1.8.2")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.7.0")
}A partner ID and a config ID from the Sirdata Console. They select the publisher configuration the SDK downloads: texts, theme, purposes, partners, capping. The examples on this page use 1020 and "hmDnl": replace them with your own IDs.
A licence for production use. The SDK is proprietary software, under the Sirdata CMP Mobile SDK Proprietary Licence v1.1. Using it in an app distributed to end users requires a valid, current subscription to the Sirdata Consent Management Platform. A subscription taken through the Sirdata Console is enough: no signed agreement is required (sections 25.1 and 25.2). Without a subscription, the licence is a 90-day evaluation grant: internal evaluation, development, testing and integration, with no distribution to third parties or to an app store (section 1.4). Subscriptions: sirdata.com; licence questions: legal@sirdata.com.
2. Add the SDK to your project
Declare both repositories in settings.gradle.kts. The SDK is on Maven Central, and its AndroidX and Compose dependencies are only on Google's Maven repository. Projects created by Android Studio already declare both.
In an older project that declares its repositories in the root build.gradle, under allprojects { repositories { … } }, add them there instead.
Then add the dependency to your app module.
Kotlin DSL:
Groovy DSL:
Version catalog:
You only declare android-sdk. Gradle adds the rest:
com.sirdata.cmp:sharedand its Android variantcom.sirdata.cmp:shared-android, the core the Android and iOS SDKs share. Never declare them yourself.Jetpack Compose UI and Material 3 (Compose BOM 2024.02.00),
androidx.activity1.8.2 andandroidx.lifecycle2.7.0, for the consent UI.Ktor 2.3.9 over OkHttp, kotlinx.coroutines 1.8.0 and kotlinx.serialization 1.6.3, for the network.
AndroidSVG 1.4, to draw the SVG logos a configuration can carry.
When your app already uses a newer version of one of them, Gradle keeps the newer one.
There is nothing to add to your AndroidManifest.xml for the SDK itself: through manifest merging it brings the INTERNET and ACCESS_NETWORK_STATE permissions and declares its own consent activity, which is not exported. There is nothing to add for R8 either (step 9).
3. Initialize the SDK
Create an Application subclass, or open the one your app already has, and initialize the SDK in onCreate():
If the class is new, declare it on the <application> element of AndroidManifest.xml. Keep the attributes and the activities your manifest already has:
Four rules:
Before any ad or analytics SDK. A vendor SDK that reads storage before the CMP has identified itself behaves as though no CMP were present.
On the main thread.
Application.onCreate()runs there. Called from another thread,initializelogs the violation under the Logcat tagSirDataCMP, or throws when the configuration enablesdebugMode.Once per process. A second call throws
IllegalStateException("SirDataCMP is already initialized. Call shutdown() first."). Code that can run twice can testSirDataCMPApi.isInitialized()first.Before any other SDK call. Before
initialize, the string and boolean getters return""orfalse,awaitConsentDisplayDecision()answers at once that nothing should be displayed, andgetConsentState(),getConsentUpdates(),getGoogleConsentState(),acceptAll()and the other consent actions throwIllegalStateException. Initializing inApplication.onCreate()rules all of this out.
During the call, before any network request, the SDK writes IABTCF_CmpSdkID = 92 to the app's default SharedPreferences, together with IABTCF_CmpSdkVersion and IABTCF_PolicyVersion: a vendor SDK reading storage from then on finds a CMP. Everything else happens asynchronously afterwards: the SDK restores the stored consent, downloads the publisher configuration, the vendor lists and the texts, writes IABTCF_gdprApplies and the other keys that do not wait for a choice (listed in step 5), and fires the tcloaded event to in-app TCF listeners.
Options
For more control, build a SirDataCMPConfig:
gdprDefaultApplies(Boolean)
true
Whether GDPR applies while the configuration has not loaded yet and the device gives no country (SIM, network, then locale). Once the configuration has loaded, the server's answer replaces it.
debugMode(Boolean)
false
A call to initialize, shutdown or setCcpaOptOut made off the main thread throws instead of being logged. It changes nothing else, logging included.
apiRoot(String), cacheApiRoot(String), consentApiRoot(String)
https://api.consentframework.com/api/v1/public, https://choices.consentframework.com/api/v1/public, https://mychoice.consentframework.com/api/v1/public
The three hosts the SDK talks to. Change them only to go through your own proxy.
certificatePins(CertificatePins?)
none
Certificate pinning, per host name: CertificatePins(mapOf("api.consentframework.com" to listOf("sha256/…"))). A wrong pin blocks the SDK's requests.
language(String?), theme(CmpTheme)
—
Accepted, but no effect in 2.0.0 — see below.
language(...) and theme(...) do nothing in 2.0.0. The builder accepts them and nothing reads them, so do not rely on them to force a language or a dark theme. What actually decides:
The language of the consent UI is the language the configuration sets, when it sets one; otherwise the device language (
Locale.getDefault()); otherwise the configuration's default language; otherwise English.Its look is the configuration's theme from the Sirdata Console — colours, position, logo, text size. When the device is in dark mode and the configuration defines a dark theme, the dark one is used; otherwise the light one.
BuildConfig.DEBUG needs buildConfig enabled. Since Android Gradle Plugin 8.0, the BuildConfig class is generated only for modules that ask for it, so the snippet above does not compile in a new project until you add:
BuildConfig is generated in your module's namespace: the snippet needs no import because its package is that namespace. Elsewhere, import <your namespace>.BuildConfig.
4. Present the consent UI
Let the SDK decide whether to present the consent UI, from your launcher activity:
Do not test the TC string yourself: a partially answered consent must re-open the banner, an active capping window must not, and neither is visible from the string.
awaitConsentDisplayDecision() waits for the publisher configuration, then applies the same cascade as the web CMP: GDPR applicability, stored consent, cookie wall, capping windows, and whether the stored choice still covers every active purpose, partner and publisher purpose. It returns a ConsentDisplayDecision:
shouldDisplay— whether to present the consent UI;workflow— how:MAINfor the standard banner;APPLY_CHOICESwhen only partners are missing from the stored choice, to ask only about what is left to decide;COOKIEWALLfor the cookie wall below. Pass it toshowConsentBanneras it is;reason— why, which is what to log when no banner appears (see Logging).
showConsentBanner starts the SDK's consent activity on top of yours — the banner, then the purposes and partners screens, or the US screen for a US user (step 7) — fires the cmpuishown TCF event, and returns at once. The consent activity closes itself when the user records a choice or closes it, and your activity resumes.
The cookie wall. When the configuration enables it, a stored choice that does not grant purpose 1 (storing or accessing information on a device) brings the consent UI back at every launch, in the COOKIEWALL workflow, whatever the capping windows. Passing decision.workflow is what opens it as a cookie wall.
Jetpack Compose apps. Keep the same code in your activity's onCreate(), next to setContent { … }. From inside a composable, get the activity from LocalContext, as in step 6.
Bounding the wait on a splash screen
The unbounded wait lasts as long as initialization: on a dead network, that is the sum of the HTTP timeouts. Bound it with timeoutMillis:
Past the deadline the decision is provisional: it only knows whether GDPR applies, as estimated from the device, and whether a choice is stored. Its reason is GDPR_NOT_APPLIES when that estimate says GDPR does not apply, and CONFIG_NOT_LOADED otherwise. A stored but incomplete choice is therefore not asked again on that launch.
Re-opening from a settings screen
See step 6 below.
5. Read consent in your own code
The getters read the recorded choice, from storage, never the toggles the user is moving on screen.
getTcString(),getAddtlConsent(),getGppString(),getUspString(),getPurposeConsents()andgetVendorConsents()return"", andhasPurposeConsent()andhasVendorConsent()returnfalse, until a choice is recorded.Purpose and vendor IDs are those of the IAB Global Vendor List, starting at 1.
isGdprApplies()is the server's answer once the configuration has loaded — based on the country of the device's IP address, unless the configuration forces GDPR. Before that, it is an estimate from the device, withgdprDefaultApplieswhen the device gives no country.
Where the signals live. The SDK writes every signal under the standard IAB keys, in the app's default SharedPreferences file, <applicationId>_preferences — the one PreferenceManager.getDefaultSharedPreferences() opens. Compliant vendor SDKs read them on their own; the full list is in Sharing consent with vendor SDKs. The keys appear in this order:
during
initialize:IABTCF_CmpSdkID,IABTCF_CmpSdkVersion,IABTCF_PolicyVersion;once the configuration has loaded, before any choice:
IABTCF_gdprApplies,IABTCF_PolicyVersionagain (from the loaded vendor list),IABTCF_PublisherCC,IABTCF_PurposeOneTreatment,IABTCF_UseNonStandardTexts, the publisher bit strings (IABTCF_PublisherConsent,IABTCF_PublisherLegitimateInterestsand the custom-purpose strings) and, for a US user, the GPP keys with the US National section; a stored choice is also rewritten to match the current configuration;when a choice is recorded: the TC string, the purpose and vendor bit strings,
IABTCF_AddtlConsentand the GPP keys (IABGPP_HDR_GppString,IABGPP_GppSID);IABUSPrivacy_Stringonly after a US choice (step 7).
The publisher bit strings are therefore present on a first launch, holding the SDK's defaults: their presence does not mean a choice was recorded. IABTCF_TCString does.
React to changes rather than reading once:
getConsentUpdates() is a counter. It emits its current value as soon as you collect it, then a new value on every change of the SDK's selections: several while the SDK initializes and while a choice is saved, and one for each toggle the user flips in the consent UI before saving. Reading the recorded values in the collector, as above, gives a result that only changes once a choice is recorded.
A US choice — saved on the US screen, or recorded with setCcpaOptOut (step 7) — does not change the counter. Re-read getUspString() and getGppString() when the consent UI closes: your activity's onResume() runs then.
In-app TCF listeners cannot tell events apart in 2.0.0. A listener registered with tcfApi().execute("addEventListener", 2, …) is called on tcloaded, cmpuishown, useractioncomplete and cmpuiclosed, but the data it receives carries no eventStatus. Treat each call as "something may have changed" and read the recorded values.
Google Consent Mode. The SDK derives the Google consent signals from the recorded TCF choice:
That is what the consent adapters would forward; getLastForwardedConsentState() is what they last forwarded, null until a choice is recorded in the session. How each vendor SDK — Google Mobile Ads, Firebase, AppsFlyer, Adjust, Branch, Meta — receives consent, and what you have to do for it, is in Sharing consent with vendor SDKs.
Your own consent screen. acceptAll(), rejectAll(), saveConsent() and submitConsent(userAction) record a choice without the SDK's UI. They are suspend functions returning a ConsentResult — ConsentResult.Success or ConsentResult.Error(message) — and store the choice, fire useractioncomplete and forward it to the consent adapters, like the SDK's UI. recordAskLater() hides the banner for one hour, a fixed window, and records no choice.
6. Reopen from a settings screen
Users must be able to change or withdraw their choice at any time. Give them an entry point — a "Privacy settings" item in your settings screen, for example — that opens the consent UI in the MANUAL_DISPLAY workflow:
From a composable, get the activity from LocalContext:
What MANUAL_DISPLAY changes, compared with the banner shown at launch:
the banner shows a close cross at the top, and a "Reject all" button whatever refusal button and style the configuration sets — even none — so that the user can always withdraw;
the close cross closes without recording anything, except under the CNIL variant, where closing records the choice;
opening it restarts the configuration's capping window, during which the banner is not shown automatically;
it is counted in your Sirdata statistics as a revisit (
load:revisit, orccpa_load:revisitfor a user in the United States).
Keep the entry point available to every user, including when isGdprApplies() is false: for a US user it opens the US screen.
7. US users
US users never get the banner automatically. The display decision follows GDPR: for a user the server places outside its scope,
awaitConsentDisplayDecision()returnsshouldDisplay = falsewith the reasonGDPR_NOT_APPLIES, and that includes US users.The US screen opens when your app opens the consent UI. When the US regime applies,
showConsentBannershows the US screen in place of the TCF banner — titled "Your Privacy Choices" in English by default — with one opt-out control covering what the configuration offers (sale, sharing, targeted advertising, or a combination) and a "Save My Choices" button; when the configuration offers no opt-out, the screen shows neither. Your app therefore needs an entry point US users can reach: the settings entry of step 6 is one. Which label and placement your app needs is a question for your legal team.Saving the US screen records the choice through
setCcpaOptOut: it writesIABUSPrivacy_String, with its opt-out flag set toYorN, and re-encodes the GPP string (IABGPP_HDR_GppString,IABGPP_GppSID) with its US sections. Closing the screen without saving records nothing. Until a US choice is recorded,IABUSPrivacy_Stringis absent andgetUspString()returns"". A US choice does not change thegetConsentUpdates()counter (step 5).isCcpaApplies()is the server's answer, from the user's country and US state, once the configuration has loaded; before that, an estimate from the device's country.Call
setCcpaOptOut(optedOut)yourself only from a control the user operates, such as your own opt-out switch. It records that the notice was given and the user's answer, so calling it on your own —if (isCcpaApplies()) setCcpaOptOut(true), for example — records an opt-out the user never made. Call it on the main thread;isCcpaOptedOut()reads the answer back.
8. Test and verify the integration
Use a debug build on a device or an emulator, with Logcat filtered on the tags SirDataCMP and SirDataCMPManager.
GDPR applicability is decided by the server, from the country of the device's IP address, unless the configuration forces GDPR. Testing from a country where GDPR does not apply, you get no banner, with the reason
GDPR_NOT_APPLIES.To replay a first launch, clear the app's data (
adb shell pm clear <applicationId>), or callSirDataCMPApi.clearConsent()and restart the app.clearConsent()deletes the stored choice and, until the next launch or the next choice,IABTCF_CmpSdkIDas well.When you switch partner ID or config ID, clear the stored consent as well. A TC string belongs to the publisher configuration that collected it.
Three tools help. Read the stored keys of a debug build with run-as, replacing com.example.myapp with your applicationId, suffix included:
Surface a configuration that failed to load — the same reason Logcat shows after Config load failed: under SirDataCMPManager:
Call the in-app TCF API:
Then check, in this order:
Right after launch,
IABTCF_CmpSdkIDis92— before the network has answered.Logcat shows
Config loaded successfullyunderSirDataCMPManager.Config load failed: …means the configuration did not load: check the IDs and the network.pinganswerssuccess=trueandcmpId=92, withcmpStatus=stubuntil the configuration has loaded, thencmpStatus=loaded,cmpLoaded=trueandgdprApplies. The switch can come a moment afterawaitConsentDisplayDecision()returns.On a first launch, the decision has
shouldDisplay = trueand the reasonCONSENT_REQUIRED, and the banner appears. Logging shows how to log the decision.After "Accept all", storage holds
IABTCF_TCString,IABTCF_gdprApplies=1,IABTCF_PurposeConsents,IABTCF_VendorConsentsandIABGPP_HDR_GppString.IABUSPrivacy_Stringis absent: it only appears after a US choice.Relaunch the app (force-stop it, then open it): no banner. The reason is
CONSENT_OKafter "Accept all",CAPPING_ACTIVEafter "Reject all".Your settings entry reopens the banner with the close cross and a "Reject all" button.
A minified release build (step 9) shows the banner and records a choice too. Release builds are not debuggable, so
run-asdoes not work there: check through the getters.If your app embeds Firebase Analytics, AppsFlyer, Adjust or Branch,
getRegisteredAdapters()lists their adapters and, after a choice,getLastForwardedConsentState()shows what was forwarded.
9. Release build
R8 and ProGuard: nothing to add. The SDK's artifacts carry their own R8 rules, including the ones its consent adapters need. Rules you added for earlier versions —
-keep class com.sirdata.cmp.** { *; }, or a-dontwarnfor slf4j — can go: the SDK ships the-dontwarnitself, and a-keepon the whole SDK only stops R8 from shrinking it.debugModein debug builds only, through.debugMode(BuildConfig.DEBUG)as in step 3.Your own partner ID and config ID, not the example values.
A production licence: a current Sirdata CMP subscription, which the Sirdata Console provides (step 1).
Attribution notices. The SDK embeds open-source components under the Apache License 2.0, and slf4j under the MIT licence. An app that embeds it must make available to its users the
NOTICE-androidfile and the full texts of those two licences, in a place they can readily reach — your app's open-source licences screen, for example (section 26.1(b) of the licence). In 2.0.0 these files are not inside the Maven Central artifacts. Take them from the npm package@sirdata/react-native-cmp@2.0.0:npm pack @sirdata/react-native-cmp@2.0.0downloads it as a.tgzarchive holdingNOTICE-androidand, underTHIRD-PARTY-LICENSES/,Apache-2.0.txtandMIT-slf4j.txt(BSL-1.0.txtis for iOS). Or ask legal@sirdata.com.Google Play Data safety. The SDK exchanges data with Sirdata's servers: it downloads the configuration, the vendor lists and the texts, and it sends each recorded choice — the consent strings — and usage statistics about the consent UI (displays, clicks, answers). Your Data safety form must cover what third-party code in your app collects or shares; see Google's Provide information for Google Play's Data safety section.
Network allowlists, proxies and certificate pinning must let three hosts through:
api.consentframework.com,choices.consentframework.comandmychoice.consentframework.com. When the user opens a partner's storage details in the partners screen, the SDK also fetches that partner's device-storage disclosure from the URL the partner publishes.
10. Upgrading from 1.2.0
Bump the version to
2.0.0and rebuild. Four public data classes gained a field (GoogleConsentState,UserAction,Settings,Info); their previous constructors andcopysignatures are kept as overloads, so source code compiles unchanged, but a library compiled against 1.2.0 that calls them with default arguments must be recompiled.Remove the R8 rules you added for the SDK (step 9). 1.2.0 failed a minified build on
org.slf4j.impl.StaticLoggerBinder; 2.0.0 ships its own rules.If your code uses the SDK's consent UI classes:
ConsentButtonStyle.CLOSEis removed, andConsentUiState.scopeReminderOpensHostnamesis replaced byscopeReminderSegmentsandpurposesText2Segments, with no overload keeping the previous constructor.AppsFlyer: call
enableTCFDataCollection(true)beforestart()— see AppsFlyer.Firebase Analytics: the adapter now forwards each choice, minified release builds included. If your app calls Firebase's
setConsentitself for the signals the adapter sends — to work around 1.2.0, for example — remove those calls: see Firebase Analytics.Expect the settings entry to look different: in
MANUAL_DISPLAYthe banner now shows a "Reject all" button with the close cross at the top (step 6); 1.2.0 showed no refusal.Run the checks of step 8 again.
Every change is listed in Versions.
Next
Reference — every method and event
Sharing consent with vendor SDKs — the part that makes ads work
Logging — what the SDK logs, and reading the display decision
Troubleshooting — symptoms and fixes
Versions — what changed in each release
Mis à jour
Ce contenu vous a-t-il été utile ?
