- Published on
- ยท15 min read
Applying AdMob GDPR on Android
I apologize in advance for any awkward expressions in English.
English is not my native language, and I have relied on ChatGPT's assistance to proceed with the translation.
Overview
AdMob keeps warning to display GDPR messages.
Since creating GDPR messages in AdSense was straightforward, I assumed it would be similar for AdMob. However, AdMob required additional code in the application to directly check for consent. ๐ฑ
I'll share the details of what I implemented in the Smart Table Clock app.
For the implementation on React Native (Android), you can refer to Applying AdMob GDPR in React Native (Android).
๐ Caution
โ I do not assume legal responsibility. โ
I implemented this based on Google documentation, sample codes, and searches. However, there may be incorrect interpretations regarding policies or implementations. Please use this as a reference only.
Although I confirmed normal operation in test mode on a test device, I did not verify the actual behavior on devices in the EEA region.
GDPR Message Setup
Go to Google AdMob > GDPR > Click on [Create Message]
button.
Configure the settings according to your app. Here are my settings.
![Create Message](/static/images/blog/2023-11/how-to-gdpr-implement-android-01.jpg)
- My apps: I selected the app to apply the message to.
- Language: I specified 'English' as the language for displaying the message.
- Not consenting:
Enabled
- Close (Not consenting):
Disabled
After creating and publishing the message, you need to implement the code for the configured GDPR message to be displayed.
Result Screen
This is the screen I verified while in test mode.
On the left is the message displayed when the app is launched.
On the right, I added a button in the settings to change the GDPR settings. Clicking this button displays the message screen on the left.
![Result Screen](/static/images/blog/2023-11/how-to-gdpr-implement-android-02.jpg)
Implementation
1. Install with Gradle
Follow the instructions from Google AdMob, UMP. Add the following content to the dependencies
section of your app/build.gradle
file:
dependencies {
// ... existing content ...
implementation 'com.google.android.ump:user-messaging-platform:2.1.0'
}
Then sync your project.
2. App Measurement Delay
Follow the instructions in Google AdMob, UMP > GDPR IAB Support > Delay app measurement. Add the following content to your AndroidManifest.xml
file:
<manifest>
<application>
<!-- existing content -->
<!-- Delay app measurement until MobileAds.initialize() is called. -->
<meta-data
android:name="com.google.android.gms.ads.DELAY_APP_MEASUREMENT_INIT"
android:value="true"/>
</application>
</manifest>
3. Code
I fetched and applied the
GoogleMobileAdsConsentManager.kt
file from googleads-mobile-android-examples.The API provided by Google does not have a part that informs about user selection (consent / non-consent). It only has the
OBTAINED
status for the selection. For this reason, I added code to theGoogleMobileAdsConsentManager.kt
file to check whether there is consent or not.- I referred to stackoverflow - How to implement UMP SDK correctly for eu consent?.
- Android AdMob consent with UMP โ Personalized or Non-Personalized Ads in EEA provides a well-explained guide.
The content of the
GoogleMobileAdsConsentManager.kt
file is as follows:
package your_package_name
import android.app.Activity
import android.content.Context
import com.google.android.ump.ConsentDebugSettings
import com.google.android.ump.ConsentForm.OnConsentFormDismissedListener
import com.google.android.ump.ConsentInformation
import com.google.android.ump.ConsentRequestParameters
import com.google.android.ump.FormError
import com.google.android.ump.UserMessagingPlatform
/**
* The Google Mobile Ads SDK provides the User Messaging Platform (Google's IAB Certified consent
* management platform) as one solution to capture consent for users in GDPR impacted countries.
* This is an example and you can choose another consent management platform to capture consent.
*/
class GoogleMobileAdsConsentManager private constructor(context: Context) {
private val consentInformation: ConsentInformation =
UserMessagingPlatform.getConsentInformation(context)
/** Interface definition for a callback to be invoked when consent gathering is complete. */
fun interface OnConsentGatheringCompleteListener {
fun consentGatheringComplete(error: FormError?)
}
/** Helper variable to determine if the app can request ads. */
val canRequestAds: Boolean
get() = consentInformation.canRequestAds()
/** Helper variable to determine if the privacy options form is required. */
val isPrivacyOptionsRequired: Boolean
get() =
consentInformation.privacyOptionsRequirementStatus ==
ConsentInformation.PrivacyOptionsRequirementStatus.REQUIRED
// Code added for consent and non-consent checks [[
// https://itnext.io/android-admob-consent-with-ump-personalized-or-non-personalized-ads-in-eea-3592e192ec90
// https://stackoverflow.com/questions/65351543/how-to-implement-ump-sdk-correctly-for-eu-consent/68310602#68310602
fun isGDPR(context: Context): Boolean {
val prefs = context.getSharedPreferences(context.packageName + "_preferences", Context.MODE_PRIVATE)
val gdpr = prefs.getInt("IABTCF_gdprApplies", 0)
return gdpr == 1
}
fun canShowAds(context: Context): Boolean {
//val prefs = PreferenceManager.getDefaultSharedPreferences(context)
// Using PreferenceManager as it is deprecated
val prefs = context.getSharedPreferences(context.packageName + "_preferences", Context.MODE_PRIVATE)
val purposeConsent = prefs.getString("IABTCF_PurposeConsents", "") ?: ""
val vendorConsent = prefs.getString("IABTCF_VendorConsents", "") ?: ""
val vendorLI = prefs.getString("IABTCF_VendorLegitimateInterests", "") ?: ""
val purposeLI = prefs.getString("IABTCF_PurposeLegitimateInterests", "") ?: ""
val googleId = 755
val hasGoogleVendorConsent = hasAttribute(vendorConsent, googleId)
val hasGoogleVendorLI = hasAttribute(vendorLI, googleId)
// Personalized Ads: listOf(1, 3, 4)
return hasConsentFor(listOf(1), purposeConsent, hasGoogleVendorConsent)
&& hasConsentOrLegitimateInterestFor(
listOf(2,7,9,10),
purposeConsent,
purposeLI,
hasGoogleVendorConsent,
hasGoogleVendorLI
)
}
// Check if a binary string has a "1" at position "index" (1-based)
private fun hasAttribute(input: String, index: Int): Boolean {
return input.length >= index && input[index-1] == '1'
}
// Check if consent is given for a list of purposes
private fun hasConsentFor(purposes: List<Int>, purposeConsent: String, hasVendorConsent: Boolean): Boolean {
return purposes.all { p -> hasAttribute(purposeConsent, p)} && hasVendorConsent
}
// Check if a vendor either has consent or legitimate interest for a list of purposes
private fun hasConsentOrLegitimateInterestFor(purposes: List<Int>, purposeConsent: String, purposeLI: String, hasVendorConsent: Boolean, hasVendorLI: Boolean): Boolean {
return purposes.all { p ->
(hasAttribute(purposeLI, p) && hasVendorLI) ||
(hasAttribute(purposeConsent, p) && hasVendorConsent)
}
}
// Code added for consent and non-consent checks ]]
/**
* Helper method to call the UMP SDK methods to request consent information and load/show a
* consent form if necessary.
*/
fun gatherConsent(
activity: Activity,
onConsentGatheringCompleteListener: OnConsentGatheringCompleteListener
) {
// For testing purposes, you can force a DebugGeography of EEA or NOT_EEA.
val debugSettings =
ConsentDebugSettings.Builder(activity)
.setDebugGeography(ConsentDebugSettings.DebugGeography.DEBUG_GEOGRAPHY_EEA) // Forcibly setting to the EEA region
// Check your logcat output for the hashed device ID e.g.
// "Use new ConsentDebugSettings.Builder().addTestDeviceHashedId("ABCDEF012345")" to use
// the debug functionality.
.addTestDeviceHashedId("Your-test-device-hash-id")
.build()
val params = ConsentRequestParameters
.Builder()
//.setConsentDebugSettings(debugSettings) // Use only during testing
.build()
// consentInformation.reset() // Reconfigure consent status; use only during testing
// Requesting an update to consent information should be called on every app launch.
consentInformation.requestConsentInfoUpdate(
activity,
params,
{
UserMessagingPlatform.loadAndShowConsentFormIfRequired(activity) { formError ->
// Consent has been gathered.
onConsentGatheringCompleteListener.consentGatheringComplete(formError)
}
},
{ requestConsentError ->
onConsentGatheringCompleteListener.consentGatheringComplete(requestConsentError)
}
)
}
/** Helper method to call the UMP SDK method to show the privacy options form. */
fun showPrivacyOptionsForm(
activity: Activity,
onConsentFormDismissedListener: OnConsentFormDismissedListener
) {
UserMessagingPlatform.showPrivacyOptionsForm(activity, onConsentFormDismissedListener)
}
companion object {
@Volatile private var instance: GoogleMobileAdsConsentManager? = null
fun getInstance(context: Context) =
instance
?: synchronized(this) {
instance ?: GoogleMobileAdsConsentManager(context).also { instance = it }
}
}
}
4. Test Setup
Change 'Your-test-device-hash-id'
- In
GoogleMobileAdsConsentManager.kt
, replace 'Your-test-device-hash-id' with your device value.
- Your device's hashed ID: Copy and paste the value printed in the log, which looks like
Use new ConsentDebugSettings.Builder().addTestDeviceHashedId("Your device's hashed ID")
.
- Uncomment the
setConsentDebugSettings
section to apply the test settings.
- โ Use this only during testing, and be sure to comment it out before deploying. โ
val params = ConsentRequestParameters
.Builder()
.setConsentDebugSettings(debugSettings) // Uncomment for testing, use only during testing
.build()
5. Implementation
The app handles GDPR and ad loading in the
ClockFragment
.For countries with GDPR, it loads the consent/non-consent pop-up when GDPR is initially loaded. After that, when the settings button is clicked, it checks the consent status and reloads the consent pop-up if not consented.
Following recommendations, I allowed users to change their consent status in the settings.
ClockFragment.kt
onCreate
>checkAdMobGDPRConsent
: Load GDPR.onViewCreated
>binding.btnSetting.setOnClickListener
: Check GDPR consent status when the settings icon is clicked.
class ClockFragment : Fragment() {
private var interstitialAd: InterstitialAd? = null // AdMob Interstitial Ad related
private val isMobileAdsInitializeCalled = AtomicBoolean(false)
private lateinit var googleMobileAdsConsentManager: GoogleMobileAdsConsentManager
private var TAG = "ClockFragment"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Load GDPR
checkAdMobGDPRConsent()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// When the settings icon is clicked, navigate to the settings screen
binding.btnSetting.setOnClickListener {
val isGDPR = googleMobileAdsConsentManager.isGDPR(requireContext())
val canShowAds = googleMobileAdsConsentManager.canShowAds(requireContext())
// Check consent status if GDPR confirmation is required
if(isGDPR && !canShowAds){
googleMobileAdsConsentManager.showPrivacyOptionsForm(requireActivity()) { formError ->
if (formError != null) {
// Move to the settings screen in case of an error
Toast.makeText(requireContext(), formError.message, Toast.LENGTH_SHORT).show()
showSetting()
} else {
// Recheck the consent status
val result = googleMobileAdsConsentManager.canShowAds(requireContext())
// Move to the settings screen if consent is obtained
if(result){
showSetting()
}
}
}
} else{
showSetting()
}
}
// Omitted
}
private fun showSetting() {
// AdMob Interstitial Ad related, show ad when conditions are met
if (SETTING_CLICKED_COUNT % ADMOB_CHECK_COUNT == 0) {
showInterstitial()
}
else{
findNavController().navigate(R.id.action_clockFragment_to_previewAndSettingFragment)
SETTING_CLICKED_COUNT++
}
}
private fun checkAdMobGDPRConsent(){
googleMobileAdsConsentManager = GoogleMobileAdsConsentManager.getInstance(requireContext())
googleMobileAdsConsentManager.gatherConsent(
requireActivity(),
) { error ->
Log.i(
TAG,
"Consent gathering result: ${error?.message}, googleMobileAdsConsentManager.canRequestAds: ${googleMobileAdsConsentManager.canRequestAds}"
)
if (error != null) {
// Consent not obtained in current session.
Log.d(TAG, "${error.errorCode}: ${error.message}")
initializeMobileAdsSdk()
} else {
if (googleMobileAdsConsentManager.canRequestAds) {
initializeMobileAdsSdk()
}
}
}
// This sample attempts to load ads using consent obtained in the previous session.
if (googleMobileAdsConsentManager.canRequestAds) {
initializeMobileAdsSdk()
}
}
/**
* AdMob Interstitial Ad related
*/
private fun initializeMobileAdsSdk() {
if (isMobileAdsInitializeCalled.getAndSet(true)) {
return
}
val appContext = activity?.applicationContext ?: return
MobileAds.initialize(appContext) {}
loadInterstitialAd(appContext)
}
// Load the ad
private fun loadInterstitialAd(context: Context){
val adRequest = AdRequest.Builder().build()
InterstitialAd.load(context, getString(R.string.interstitial_ad_unit_id), adRequest,
object : InterstitialAdLoadCallback() {
// Omitted, interstitialAd = ad Or interstitialAd = null
})
}
private fun showInterstitial() {
// Omitted, display the ad
}
// Omitted
}
If GDPR message loading is not happening:
- Check on the Google AdMob site if GDPR has been published for the app.
- Verify if 'Your-test-device-hash-id' is correct.
When switching between test mode and non-test mode for testing and previous settings persist, preventing successful testing:
- Test mode: Uncomment
.setConsentDebugSettings(debugSettings)
, uncommentconsentInformation.reset()
to reset the settings, then comment it again. - Non-test mode: Comment out
//.setConsentDebugSettings(null)
, uncommentconsentInformation.reset()
to reset the settings, then comment it again.
- Test mode: Uncomment
If it loads when it shouldn't or doesn't load when it should:
- The selected information is stored in Local Storage. Therefore, clearing the storage for the app and relaunching resolved the issue.
- If that doesn't work, as a last resort, uninstall the app and then reinstall it, which resolved the issue.
SettingFragment.kt
Added a button in the settings to allow changing GDPR settings.
When the button is clicked, a GDPR message appears, allowing the user to choose consent/non-consent.
class SettingFragment : Fragment() {
private lateinit var googleMobileAdsConsentManager: GoogleMobileAdsConsentManager
// Omitted
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// Omitted
googleMobileAdsConsentManager = GoogleMobileAdsConsentManager.getInstance(requireContext())
binding.btnGdprSetting.isVisible = googleMobileAdsConsentManager.isPrivacyOptionsRequired
binding.btnGdprSetting.setOnClickListener {
googleMobileAdsConsentManager.showPrivacyOptionsForm(requireActivity()) { formError ->
if (formError != null) {
Toast.makeText(requireContext(), formError.message, Toast.LENGTH_SHORT).show()
}
}
}
}
// Omitted
}