Logo
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
  • 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

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

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'

  1. 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").
  1. 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), uncomment consentInformation.reset() to reset the settings, then comment it again.
    • Non-test mode: Comment out //.setConsentDebugSettings(null), uncomment consentInformation.reset() to reset the settings, then comment it again.
  • 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
}

Reference