I've been working through this myself and while I don't have answers to all your questions, I have figured out a few of them.
The UMP writes its output to some strings in SharedPreferences
, outlined here. You can write some helper methods to query these strings to find out what level of ad consent the user has given or whether the user is EEA or not.
How to check if the user is EEA? You can check the IABTCF_gdprApplies
integer in SharedPreferences and if it is 1, the user is EEA. If it is 0 the user is not.
How to get the consent type? This part gets more complicated. The Google docs here outline what permissions are needed for personalized and non-personalized ads. To get this you need to look at 4 strings from the SharedPreference: IABTCF_PurposeConsents
, IABTCF_PurposeLegitimateInterests
, IABTCF_VendorConsents
and IABTCF_VendorLegitimateInterests
. As others have noted, it is nearly impossible for a user to actually select the non-personalized ad configuration since they have to not only select "Store Information on Device" but also scroll through hundreds of non-alphabetically organized vendors to find and also select "Google" (vendor ID 755 in those strings). This means that for all practical purposes they will either select personalized ads (Consent All) or have a nice ad-free app they paid nothing for. You can at least use these checks to put up a paywall, disable Cloud features, or otherwise handle that scenario as you see fit.
I made some helper methods to find these states.
Kotlin
fun isGDPR(): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
val gdpr = prefs.getInt("IABTCF_gdprApplies", 0)
return gdpr == 1
}
fun canShowAds(): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
//https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md#in-app-details
//https://support.google.com/admob/answer/9760862?hl=en&ref_topic=9756841
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, index=googleId)
val hasGoogleVendorLI = hasAttribute(vendorLI, index=googleId)
// Minimum required for at least non-personalized ads
return hasConsentFor(listOf(1), purposeConsent, hasGoogleVendorConsent)
&& hasConsentOrLegitimateInterestFor(listOf(2,7,9,10), purposeConsent, purposeLI, hasGoogleVendorConsent, hasGoogleVendorLI)
}
fun canShowPersonalizedAds(): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(applicationContext)
//https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md#in-app-details
//https://support.google.com/admob/answer/9760862?hl=en&ref_topic=9756841
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, index=googleId)
val hasGoogleVendorLI = hasAttribute(vendorLI, index=googleId)
return hasConsentFor(listOf(1,3,4), 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)
}
}
Note
PreferenceManager.getDefaultSharedPreferences
is not deprecated - you just need to make sure to include the androidx import (import androidx.preference.PreferenceManager
). If you include the wrong one (import android.preference.PreferenceManager
), it will be marked as deprecated.
Swift
func isGDPR() -> Bool {
let settings = UserDefaults.standard
let gdpr = settings.integer(forKey: "IABTCF_gdprApplies")
return gdpr == 1
}
// Check if a binary string has a "1" at position "index" (1-based)
private func hasAttribute(input: String, index: Int) -> Bool {
return input.count >= index && String(Array(input)[index-1]) == "1"
}
// Check if consent is given for a list of purposes
private func hasConsentFor(_ purposes: [Int], _ purposeConsent: String, _ hasVendorConsent: Bool) -> Bool {
return purposes.allSatisfy { i in hasAttribute(input: purposeConsent, index: i) } && hasVendorConsent
}
// Check if a vendor either has consent or legitimate interest for a list of purposes
private func hasConsentOrLegitimateInterestFor(_ purposes: [Int], _ purposeConsent: String, _ purposeLI: String, _ hasVendorConsent: Bool, _ hasVendorLI: Bool) -> Bool {
return purposes.allSatisfy { i in
(hasAttribute(input: purposeLI, index: i) && hasVendorLI) ||
(hasAttribute(input: purposeConsent, index: i) && hasVendorConsent)
}
}
private func canShowAds() -> Bool {
let settings = UserDefaults.standard
//https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md#in-app-details
//https://support.google.com/admob/answer/9760862?hl=en&ref_topic=9756841
let purposeConsent = settings.string(forKey: "IABTCF_PurposeConsents") ?? ""
let vendorConsent = settings.string(forKey: "IABTCF_VendorConsents") ?? ""
let vendorLI = settings.string(forKey: "IABTCF_VendorLegitimateInterests") ?? ""
let purposeLI = settings.string(forKey: "IABTCF_PurposeLegitimateInterests") ?? ""
let googleId = 755
let hasGoogleVendorConsent = hasAttribute(input: vendorConsent, index: googleId)
let hasGoogleVendorLI = hasAttribute(input: vendorLI, index: googleId)
// Minimum required for at least non-personalized ads
return hasConsentFor([1], purposeConsent, hasGoogleVendorConsent)
&& hasConsentOrLegitimateInterestFor([2,7,9,10], purposeConsent, purposeLI, hasGoogleVendorConsent, hasGoogleVendorLI)
}
private func canShowPersonalizedAds() -> Bool {
let settings = UserDefaults.standard
//https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md#in-app-details
//https://support.google.com/admob/answer/9760862?hl=en&ref_topic=9756841
// required for personalized ads
let purposeConsent = settings.string(forKey: "IABTCF_PurposeConsents") ?? ""
let vendorConsent = settings.string(forKey: "IABTCF_VendorConsents") ?? ""
let vendorLI = settings.string(forKey: "IABTCF_VendorLegitimateInterests") ?? ""
let purposeLI = settings.string(forKey: "IABTCF_PurposeLegitimateInterests") ?? ""
let googleId = 755
let hasGoogleVendorConsent = hasAttribute(input: vendorConsent, index: googleId)
let hasGoogleVendorLI = hasAttribute(input: vendorLI, index: googleId)
return hasConsentFor([1,3,4], purposeConsent, hasGoogleVendorConsent)
&& hasConsentOrLegitimateInterestFor([2,7,9,10], purposeConsent, purposeLI, hasGoogleVendorConsent, hasGoogleVendorLI)
}
Edit: Example integration
Here is an example implementation of a ConsentHelper
method (in Kotlin) for managing calling the UMP SDK and handling the results. This would be called on app load (e.g. in the activity onCreate
) with
ConsentHelper.obtainConsentAndShow(activity) {
// code to load ads
}
This handles waiting to initialize the MobileAds SDK until after obtaining consent, and then uses a callback to begin loading ads after the consent workflow is complete.
object ConsentHelper {
private var isMobileAdsInitializeCalled = AtomicBoolean(false)
private var showingForm = false
private var showingWarning = false
private fun initializeMobileAdsSdk(context: Context) {
if (isMobileAdsInitializeCalled.getAndSet(true)) {
return
}
// Initialize the Google Mobile Ads SDK.
MobileAds.initialize(context)
}
// Called from app settings to determine whether to
// show a button so the user can launch the dialog
fun isUpdateConsentButtonRequired(context: Context) : Boolean {
val consentInformation = UserMessagingPlatform.getConsentInformation(context)
return consentInformation.privacyOptionsRequirementStatus ==
ConsentInformation.PrivacyOptionsRequirementStatus.REQUIRED
}
// Called when the user clicks the button to launch
// the CMP dialog and change their selections
fun updateConsent(context: Activity) {
UserMessagingPlatform.showPrivacyOptionsForm(context) { error ->
val ci = UserMessagingPlatform.getConsentInformation(context)
handleConsentResult(context, ci, loadAds = {})
}
}
// Called from onCreate or on app load somewhere
fun obtainConsentAndShow(context: AppCompatActivity, loadAds: ()->Unit) {
val params = if( BuildConfig.DEBUG ) {
val debugSettings = ConsentDebugSettings.Builder(context)
.setDebugGeography(ConsentDebugSettings.DebugGeography.DEBUG_GEOGRAPHY_EEA)
.addTestDeviceHashedId("YOUR_DEVICE_ID") // Get ID from Logcat
.build()
ConsentRequestParameters
.Builder()
.setTagForUnderAgeOfConsent(false)
.setConsentDebugSettings(debugSettings)
.build()
}
else {
ConsentRequestParameters
.Builder()
.setTagForUnderAgeOfConsent(false)
.build()
}
val ci = UserMessagingPlatform.getConsentInformation(context)
ci.requestConsentInfoUpdate(
context,
params,
{ // Load and show the consent form. Add guard to prevent showing form more than once at a time.
if( showingForm ) return@requestConsentInfoUpdate
showingForm = true
UserMessagingPlatform.loadAndShowConsentFormIfRequired(context) { error: FormError? ->
showingForm = false
handleConsentResult(context, ci, loadAds)
}
},
{ error ->
// Consent gathering failed.
Log.w("AD_HANDLER", "${error.errorCode}: ${error.message}")
})
// Consent has been gathered already, load ads
if( ci.canRequestAds() ) {
initializeMobileAdsSdk(context.applicationContext)
loadAds()
}
}
private fun handleConsentResult(context: Activity, ci: ConsentInformation, loadAds: ()->Unit) {
// Consent has been gathered.
if( ci.canRequestAds() ) {
initializeMobileAdsSdk(context.applicationContext)
logConsentChoices(context)
loadAds()
}
else {
// This is an error state - should never get here
logConsentChoices(context)
}
}
private fun logConsentChoices(context: Activity) {
// After completing the consent workflow, check the
// strings in SharedPreferences to see what they
// consented to and act accordingly
val canShow = canShowAds(context)
val isEEA = isGDPR(context)
// Check what level of consent the user actually provided
println("TEST: user consent choices")
println("TEST: is EEA = $isEEA")
println("TEST: can show ads = $canShow")
println("TEST: can show personalized ads = ${canShowPersonalizedAds(context)}")
if( !isEEA ) return
// handle user choice, activate trial mode, etc
}
}