Skip to main content

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.

Required from 1.3.0

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.
  • UNKNOWN is a real answer, and it restricts, exactly like OPTED_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​

FieldTypeMeaning
sellOrShareStatusConsentStateWhether the user opted out of the sale or sharing of their personal information. Governs restricted data processing.
personalizationStatusConsentStateWhether the user opted out of personalized advertising. Governs whether ad requests are non-personalized.
disclosureVersionString?The version of the privacy notice the user was shown, as an ISO date — for example "2026-07-30". null if you have none.
rdpOwnerRdpOwnerWhich 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​

ValueMeaningEffect on ads
NOT_OPTED_OUTThe user was asked and did not opt out.Permits
OPTED_OUTThe user opted out.Restricts
UNKNOWNYour 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:

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
}
}

Rules for the call:

  • Call it first. Nothing else in the Activation SDK works before it, and ActivationClient.instance throws IllegalStateException until it has run. Use ActivationClient.isInitialized if 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:

WhenWhat 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 noticeAssign ActivationClient.instance.privacy = updated (or call initialize(updated) again).
A different user signs inAssign 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.PUBLISHERRdpOwner.ACTUAL
Who writes gad_rdpYour app (or the consent platform or ad SDK in your app that manages it)The Activation SDK
What the SDK does with gad_rdpReads it. Never writes or clears it.Sets it to 1 whenever restricted processing is required; removes it when it no longer is.
Choose it whenAnything 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 1No 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).

The samples use ACTUAL

RdpOwner.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.

Under PUBLISHER, you must set gad_rdp yourself

With 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:

  1. Your Privacy values, as they stand at that moment.
  2. Actual's assessment of your privacy notice. The SDK reports the disclosureVersion you 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.
  3. The current state of gad_rdp.
Sell or sharePersonalizationRestricted mode requiredACTUALPUBLISHER
Not opted outNot opted outNoPersonalized¹Personalized — or non-personalized if your app has set gad_rdp
Not opted outOpted out / unknownNoNon-personalizedNon-personalized
Opted out / unknownanyanyNon-personalized; the SDK sets gad_rdpNon-personalized if gad_rdp is 1, otherwise no ad
anyanyYesNon-personalized; the SDK sets gad_rdpNon-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 set gad_rdp when 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.

SurfaceAdWhen no ad may be requested
Loading screen (while a receipt is processed)A Google Ad Manager adNo ad is shown; processing and the rest of the flow continue as normal.
Boost on the Receipt SummaryA rewarded ad, preloaded when the summary appearsThe Boost button is never shown — the boost is not offered and then refused.
Inline rewarded offers on the Receipt SummaryA rewarded ad, preloaded with the summaryNot offered.
A rewarded ad preloaded before the user's choices changedRe-checked when the shopper taps itDropped 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:

KeyValuesMeaning
npa1, 0Whether the request was non-personalized.
rdp1, 0Whether restricted data processing applied.
adid_statevalid, limited, unavailableWhether the operating system reports a usable advertising ID.
sdk_versionThe Activation SDK version, e.g. 1.3.0Which SDK version made the request.
app_versionYour app's versionName, or unknownWhich app version made the request.
Register the keys before you rely on the reports

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.Test registers 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. Under PUBLISHER, test both with and without gad_rdp set.
  • Read the log. With SDK logging enabled, every refused ad request logs a warning naming the reason, so a missing gad_rdp is 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 initial UNKNOWN values.

Pitfalls​

PitfallWhy it happensWhat to do
No ads at all after upgrading to 1.3.0RdpOwner.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 startupSomething — 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 UNKNOWNThe 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 restartThe 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 writerTwo 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 reportsThe custom-targeting keys were never registered.Register npa, rdp, adid_state, sdk_version, and app_version as predefined keys.