Android Ad Consent
Starting with 1.3.0, the Activation SDK serves personalized ads where your user's choices and the law allow it, and restricts itself where they don't. The SDK cannot know those choices on its own: your app reports them through Privacy, and the SDK applies them to every Google ad request it makes — the ad on the loading screen and the rewarded ads behind boosts.
ActivationClient.initialize(privacy) is the only way to start the Activation SDK, and it takes these privacy values. Reading ActivationClient.instance before it throws. See SDK Initialization and the 1.3.1 upgrade notes.
At a glance:
- You supply four values. Every one is required — there are no defaults.
UNKNOWNis a real answer, and it restricts, exactly likeOPTED_OUT.- The values live in memory only. Supply them on every app start and again whenever they change.
- Before each ad request, the SDK combines your values with Actual's assessment of your privacy notice and decides one of three outcomes: a personalized request, a non-personalized request, or no request at all.
- A suppressed ad is not an error. The screen simply shows no ad and carries on.
Supplying privacy values
Privacy
| Field | Type | Meaning |
|---|---|---|
sellOrShareStatus | ConsentState | Whether the user opted out of the sale or sharing of their personal information. Governs restricted data processing. |
personalizationStatus | ConsentState | Whether the user opted out of personalized advertising. Governs whether ad requests are non-personalized. |
disclosureVersion | String? | The version of the privacy notice the user was shown, as an ISO date — for example "2026-07-30". null if you have none. |
rdpOwner | RdpOwner | Which party may write Google's app-wide restricted-data-processing preference. See Choosing rdpOwner. |
Privacy, ConsentState, and RdpOwner are in the com.actualplatform.activation package.
ConsentState
| Value | Meaning | Effect on ads |
|---|---|---|
NOT_OPTED_OUT | The user was asked and did not opt out. | Permits |
OPTED_OUT | The user opted out. | Restricts |
UNKNOWN | Your app has not resolved this user's answer (yet). | Restricts — exactly like OPTED_OUT |
UNKNOWN is a statement you make deliberately, not a missing value. Use it until your consent flow has an answer for the current user, then replace it.
Initialize in Application.onCreate()
Supply the values once at startup, through initialize. Map them from whatever your consent flow stores:
- Kotlin
- Java
import android.app.Application
import com.actualplatform.activation.ActivationClient
import com.actualplatform.activation.ConsentState
import com.actualplatform.activation.Privacy
import com.actualplatform.activation.RdpOwner
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// Must run before anything reads ActivationClient.instance — including
// dependency-injection singletons that reach for the SDK.
ActivationClient.initialize(currentPrivacy())
}
private fun currentPrivacy(): Privacy {
val consent = MyConsentStore.load(this) // your app's own consent storage
return Privacy(
sellOrShareStatus = consent.saleOrShareOptOut.toConsentState(),
personalizationStatus = consent.personalizedAdsOptOut.toConsentState(),
disclosureVersion = consent.noticeVersionShown, // e.g. "2026-07-30", or null
rdpOwner = RdpOwner.ACTUAL,
)
}
// true = opted out, false = did not opt out, null = not answered yet
private fun Boolean?.toConsentState(): ConsentState = when (this) {
true -> ConsentState.OPTED_OUT
false -> ConsentState.NOT_OPTED_OUT
null -> ConsentState.UNKNOWN
}
}
import android.app.Application;
import com.actualplatform.activation.ActivationClient;
import com.actualplatform.activation.ConsentState;
import com.actualplatform.activation.Privacy;
import com.actualplatform.activation.RdpOwner;
public class MyApplication extends Application {
@Override
public void onCreate() {
super.onCreate();
// Until your consent flow has resolved this user's answers, say so: UNKNOWN restricts.
ActivationClient.initialize(new Privacy(
ConsentState.UNKNOWN, // sellOrShareStatus
ConsentState.UNKNOWN, // personalizationStatus
null, // disclosureVersion
RdpOwner.ACTUAL // rdpOwner
));
}
}
Rules for the call:
- Call it first. Nothing else in the Activation SDK works before it, and
ActivationClient.instancethrowsIllegalStateExceptionuntil it has run. UseActivationClient.isInitializedif you need to check. - Order relative to the Paper Receipt SDK doesn't matter.
BlinkReceiptSdk.initialize()can run before or after it; the Paper Receipt SDK hands the Activation client its device identity once both have started. - Calling it again updates the values. A second
initialize(privacy)on a running client replaces the privacy values; it does not rebuild the client. - After
close(), call it again before using the SDK — it then creates a fresh client.
Keeping the values current
The SDK holds privacy values in memory for the life of the process and never writes them to disk. Supply them again whenever they could have changed:
| When | What to do |
|---|---|
| Every app start (including after a reinstall) | Call ActivationClient.initialize(privacy) from Application.onCreate() with the stored answers. |
| The user changes a choice, or is shown a new privacy notice | Assign ActivationClient.instance.privacy = updated (or call initialize(updated) again). |
| A different user signs in | Assign the new user's values. |
// Called by your consent flow whenever an answer changes.
fun onConsentChanged(updated: Privacy) {
ActivationClient.instance.privacy = updated
}
ActivationClient.getInstance().setPrivacy(updated);
A new value applies to the next ad request. A request already on its way to Google keeps the values it was made with, and nothing is latched for the session — the user's latest answer always wins from the next request on.
Choosing rdpOwner
Google Mobile Ads reads restricted data processing for an app from one place: the gad_rdp integer in the app's default SharedPreferences. There is one per app, and it applies to every Google ad request the app makes — your own ad placements as well as the Activation SDK's. rdpOwner declares who may write it.
RdpOwner.PUBLISHER | RdpOwner.ACTUAL | |
|---|---|---|
Who writes gad_rdp | Your app (or the consent platform or ad SDK in your app that manages it) | The Activation SDK |
What the SDK does with gad_rdp | Reads it. Never writes or clears it. | Sets it to 1 whenever restricted processing is required; removes it when it no longer is. |
| Choose it when | Anything else in your app already manages gad_rdp, or may in future. | The Activation SDK is the only code in your app that touches gad_rdp. |
Restricted processing is required, gad_rdp is not 1 | No ad is requested. | The SDK sets gad_rdp and the request goes ahead, non-personalized. |
Restricted processing is required whenever sellOrShareStatus is OPTED_OUT or UNKNOWN, or whenever Actual requires restricted mode for the privacy notice you reported (see How each ad request is decided).
ACTUALRdpOwner.ACTUAL is the simplest choice when the Activation SDK is the only code in your app that touches gad_rdp: the SDK keeps it in step with the user's answers and ads keep flowing — non-personalized while restriction applies. Switch to PUBLISHER as soon as anything else in your app (a consent platform, another ad SDK) manages gad_rdp.
PUBLISHER, you must set gad_rdp yourselfWith RdpOwner.PUBLISHER, the SDK never writes gad_rdp. If restricted processing is required and your app has not set gad_rdp to 1, there is no lawful request the SDK can make, so it requests no ad at all. That includes the period while sellOrShareStatus is still UNKNOWN: no ad is served until your app either sets gad_rdp or resolves the user's answer to NOT_OPTED_OUT.
Under PUBLISHER, write gad_rdp where Google reads it — the default SharedPreferences:
import androidx.preference.PreferenceManager
val defaults = PreferenceManager.getDefaultSharedPreferences(context)
// The user opted out of sale/sharing, or their answer is still unknown:
defaults.edit().putInt("gad_rdp", 1).apply()
// Restricted processing no longer applies:
defaults.edit().remove("gad_rdp").apply()
Because the key is app-wide, setting it also restricts any other Google ad requests your app makes — which is the point: it records the user's choice for the whole app.
How each ad request is decided
Immediately before every Google ad request, the SDK resolves the outcome from three inputs:
- Your
Privacyvalues, as they stand at that moment. - Actual's assessment of your privacy notice. The SDK reports the
disclosureVersionyou supplied to Actual's backend, which answers with each set of ad instructions whether this user's ads must run in restricted mode. If it can't confirm the notice — or a response doesn't say — the SDK treats restricted mode as required. - The current state of
gad_rdp.
| Sell or share | Personalization | Restricted mode required | ACTUAL | PUBLISHER |
|---|---|---|---|---|
| Not opted out | Not opted out | No | Personalized¹ | Personalized — or non-personalized if your app has set gad_rdp |
| Not opted out | Opted out / unknown | No | Non-personalized | Non-personalized |
| Opted out / unknown | any | any | Non-personalized; the SDK sets gad_rdp | Non-personalized if gad_rdp is 1, otherwise no ad |
| any | any | Yes | Non-personalized; the SDK sets gad_rdp | Non-personalized if gad_rdp is 1, otherwise no ad |
¹ If the SDK set gad_rdp for an earlier request, it removes it first, so a restriction that no longer applies doesn't linger.
Two guarantees hold whatever the inputs:
- Restricted data processing and a personalized request are never combined. Whenever restricted processing applies — from any source — the request is also marked non-personalized.
- Under
ACTUAL, a failed write never leaks through. If the SDK cannot setgad_rdpwhen it must, it requests no ad rather than letting Google process the request unrestricted.
How ads behave inside the SDK
Whatever the outcome, the shopper's flow is never blocked by consent. A request that is refused looks exactly like an ad that did not fill.
| Surface | Ad | When no ad may be requested |
|---|---|---|
| Loading screen (while a receipt is processed) | A Google Ad Manager ad | No ad is shown; processing and the rest of the flow continue as normal. |
| Boost on the Receipt Summary | A rewarded ad, preloaded when the summary appears | The Boost button is never shown — the boost is not offered and then refused. |
| Inline rewarded offers on the Receipt Summary | A rewarded ad, preloaded with the summary | Not offered. |
| A rewarded ad preloaded before the user's choices changed | Re-checked when the shopper taps it | Dropped if consent now forbids it; re-requested under the current values if it was requested under different ones. A creative requested under old answers is never shown. |
None of this reaches your app as an error: nothing arrives on onException, and the offer wall, scanning, and rewards work exactly as they would with no ad inventory.
Advertising ID
The SDK never reads, logs, or transmits the device's advertising identifier. It only records whether the operating system reports one as usable, limited, or unavailable, for reporting (see adid_state below). That state does not change the consent decision.
Ad Manager reporting keys
Every ad request the SDK makes carries the decision it was made under as Google Ad Manager custom targeting, so you can segment delivery and revenue by consent outcome in your Ad Manager reports:
| Key | Values | Meaning |
|---|---|---|
npa | 1, 0 | Whether the request was non-personalized. |
rdp | 1, 0 | Whether restricted data processing applied. |
adid_state | valid, limited, unavailable | Whether the operating system reports a usable advertising ID. |
sdk_version | The Activation SDK version, e.g. 1.3.0 | Which SDK version made the request. |
app_version | Your app's versionName, or unknown | Which app version made the request. |
Ad Manager only reports custom-targeting values that are registered in advance. In your Ad Manager network, add each key above as a predefined key with the values listed — for sdk_version and app_version, the versions you ship. Free-form values are still sent but do not appear in reports. See GAM MCM Setup for the account side of Ad Manager.
Testing
- Turn on test ads.
TestOptions.Testregisters the running device as a Google Mobile Ads test device, so ad requests are flagged as test requests and serve test creatives. Whether a given ad unit then fills depends on its inventory. See Debug Mode. - Walk the decision table. For each row that applies to your app, assign the values with
ActivationClient.instance.privacy, scan a receipt, and confirm the loading-screen ad and the Boost button behave as the table says. UnderPUBLISHER, test both with and withoutgad_rdpset. - Read the log. With SDK logging enabled, every refused ad request logs a warning naming the reason, so a missing
gad_rdpis easy to spot. - Check a cold start. Kill the app, relaunch it, and confirm your
Application.onCreate()supplies the user's stored answers again — not the initialUNKNOWNvalues.
Pitfalls
| Pitfall | Why it happens | What to do |
|---|---|---|
No ads at all after upgrading to 1.3.0 | RdpOwner.PUBLISHER with sellOrShareStatus UNKNOWN (or OPTED_OUT) and no gad_rdp — the SDK has no lawful request to make. | Set gad_rdp when restricted processing is required, resolve the user's answers, or use RdpOwner.ACTUAL if nothing else in your app manages gad_rdp. |
Crash reading ActivationClient.instance at startup | Something — often a dependency-injection singleton — touches the SDK before initialize(privacy) ran. | Call initialize(privacy) first in Application.onCreate(), before building your DI graph. |
Only ever passing UNKNOWN | The starting values compile and run, so nothing looks wrong. | UNKNOWN restricts every request. Supply the user's real answers as soon as your consent flow has them. |
| Users' choices "forgotten" after a restart | The SDK never persists privacy values. | Persist the answers in your app and supply them from Application.onCreate() on every launch. |
Choosing ACTUAL alongside another gad_rdp writer | Two writers of one app-wide key overwrite each other. | Use PUBLISHER whenever anything else in your app manages gad_rdp. |
| Consent segments missing from Ad Manager reports | The custom-targeting keys were never registered. | Register npa, rdp, adid_state, sdk_version, and app_version as predefined keys. |