Background
I've used Admob for showing ads for many years on my apps. At some point I had to add the new GDPR consent dialog so that it could be used according to the new EU rules, and Admob SDK automatically uses its results.
This worked fine as I used the SDK by Google and Google already owns Admob to use this SDK.
The problem
Recently I've added mediation support, to include various ad-sources in order to have some competition between them:
Sadly, as opposed to Admob, very few ad-sources automatically get the value out of what the user has chosen. In fact I've found only very few that do it: AppLovin and probably also Mintegral (needs extra code before initialization, but that's it).
For most of the ad-sources, it says to pass the data of whether the user has accepted or not, in code, to the SDK.
Not only that, but in addition to the GDPR rules, there are new ones for the US itself (COPPA):
Thing is, I can't find what is the value I need to set for them, not of GDPR for the various ad-networks, and also not of the new US rule.
Even Applovin (and probably Mintegral too) doesn't check the US part of the SDK.
What I've found
In the past, just for Analytics to see the situation, I've used these:
For example, I created this:
@WorkerThread
fun canShowPersonalizedAds(context: Context): Boolean {
val prefs = PreferenceUtil.getDefaultSharedPreferences(context)
//.md#in-app-details
//;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 ->
(hasVendorLI && hasAttribute(purposeLI, p)) ||
(hasVendorConsent && hasAttribute(purposeConsent, p))
}
}
And also this:
/**
* Checks the stored IABTCF configuration and returns one of the values defined in [AdConfiguration],
* based on the necessary minimum consent/interest defined here:
*/
fun detectAdConfiguration(context: Context): AdConfiguration {
// default string for "no consent", used in cases where no configuration has previously been stored
val defaultPurposeString = "0000000000"
// IABTCF strings are stored in SharedPreferences
val sharedPrefs = PreferenceUtil.getDefaultSharedPreferences(context)
//
//limited ads: nothing for 1, consent/legitimate: 2,7,9,10
//non personalized ads: Consent: 1 , consent/legitimate: 2,7,9,10
//personalized: consent: 1,3,4 consent/legitimate: 2,7,9,10
//
// relevant strings are those for purpose consent or legitimate interest, as well as vendors
val tcConsentString = sharedPrefs
.getString("IABTCF_PurposeConsents", defaultPurposeString) ?: defaultPurposeString
val tcInterestString = sharedPrefs
.getString("IABTCF_PurposeLegitimateInterests", defaultPurposeString)
?: defaultPurposeString
// Log.d("AppLog", "detectAdConfiguration tcConsentString:$tcConsentString tcInterestString:$tcInterestString tcVendorString:$tcVendorString ")
// in any case we need at least legitimate interest for purposes N = 2, 7, 9 and 10,
// stored in positions N-1 of either purpose string:
val sufficientInterest =
(tcConsentString.getOrNull(1) == '1' || tcInterestString.getOrNull(1) == '1') &&
(tcConsentString.getOrNull(6) == '1' || tcInterestString.getOrNull(6) == '1') &&
(tcConsentString.getOrNull(8) == '1' || tcInterestString.getOrNull(8) == '1') &&
(tcConsentString.getOrNull(9) == '1' || tcInterestString.getOrNull(9) == '1')
if (!sufficientInterest) {
return AdConfiguration.None
}
val tcVendorString = sharedPrefs.getString("IABTCF_VendorConsents", "0") ?: "0"
// Log.d("AppLog", "tcVendorString:$tcVendorString")
// TODO vendor configuration is variable, so needs to be defined by the individual developer
// - run app and make sure that a valid configuration is stored
// - have the app log the value of [tcVendorString], then copy that value to the following line
// - repeat if ad configuration changes, perhaps make this value available via remote configuration instead
val goodVendorConfiguration = context.getString(R.string.ad_consent_expected_good_vendor_configuration)
// if the stored string is shorter than what is necessary, at least some vendors will not be
// configured properly.
if (tcVendorString.length < goodVendorConfiguration.length) {
return AdConfiguration.Unclear
}
// we need consent for the following purposes N, stored in positions N-1 of the consent string:
// 1, 3 and 4 to show all ads
// 1 to show non-personalized ads
// no consent to show limited ads
val maxAdDisplayConfiguration = when {
tcConsentString.getOrNull(0) != '1' -> AdConfiguration.Limited
tcConsentString.getOrNull(2) == '1' && tcConsentString.getOrNull(3) == '1' -> AdConfiguration.All
else -> AdConfiguration.NonPersonalized
}
// build a regex that must match all '1' but not the '0' characters in goodVendorConfiguration,
// and allows this configuration to be shorter than the string it is compared with
val vendorRegex = Regex(goodVendorConfiguration.replace("0", ".").plus(".*"))
//if the regex matches, at least some ads should be served; if not, vendor string is unclear
return if (vendorRegex.matches(tcVendorString)) {
maxAdDisplayConfiguration
} else {
return AdConfiguration.Unclear
}
}
But I think I can only use these for detection for Admob, and not specifically for the various ad-sources. Also it's only for GDPR and not also of US rules.
I also tried to ask Admob and ad-sourecs companies for help about this, but they just pass the responsibility for this to the other side, and I'm in the middle...
Later I've tried to check what happens in the sharedPreferences (using registerOnSharedPreferenceChangeListener
) . Seems that when IABTCF_gdprApplies
key points to integer 1, it means it's subject to GDPR rules.
As for US regulations, I can see that "IABGPP_GppSID
and IABGPP_HDR_GppString
keys" are mentioned in the docs:
But I don't understand how to read the details about them. I can see that if I do choose "Don't sell or share my data", I get these added to the default SharedPreferences:
<string name="IABGPP_GppSID">7</string>
<string name="IABGPP_HDR_GppString">DBABL~BVQVAAAAAg</string>
So I guess that just having them is enough for the check, perhaps...
It seems it has a bug though, because when I change again the choice in the dialog, nothing seems to change...
The questions
Given any ad-source X from the list of supported ad-sources of Admob medaition, is it ok to use one of the functions I've mentioned above for GDPR ?
What should I do about the US rules, as I couldn't even find where the choice is saved by the user? What I've found about the weird SharedPreferences file seems unreliable...
If I choose to be strict there, would it affect all countries in the world, or only those that are affected by these rules?