Skip to content

Instantly share code, notes, and snippets.

@piszoka
Created November 8, 2023 12:38
Show Gist options
  • Save piszoka/0467eb021b0ad82014d1c324efc5e511 to your computer and use it in GitHub Desktop.
Save piszoka/0467eb021b0ad82014d1c324efc5e511 to your computer and use it in GitHub Desktop.
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.ApplicationInfo
import android.util.Log
import androidx.preference.PreferenceManager
//## HINT: https://stackoverflow.com/a/70324431
//## HINT: https://groups.google.com/g/google-admob-ads-sdk/c/InmLIbFm8Xg/m/h4fhgUwmAgAJ
// https://groups.google.com/g/google-admob-ads-sdk/c/InmLIbFm8Xg/m/HLKLt2B1AgAJ
// https://support.google.com/admob/answer/9760862#consent-policies describes the requirements for the different types of serving
// modes for EU traffic:
//
//- Personalized ads - Consent for purposes 1,3,4, Legitimate Interest for 2, 7, 9, 10
//- Non personalized ads - Consent for purpose 1, Legitimate Interest for 2, 7, 9, 10
//- Limited ads - No consent for purpose 1.
//## https://groups.google.com/g/google-admob-ads-sdk/ --> search for UMP
// Further sources:
// https://support.google.com/admanager/answer/9461778
//
// Purposes:
// 1 - Store and/or access information on a device
// 2 - Select basic ads
// 3 - Create a personalised ads profile
// 4 - Select personalised ads
// 5 - Create a personalised content profile
// 6 - Select personalised content
// 7 - Measure ad performance
// 8 - Measure content performance
// 9 - Apply market research to generate audience insights
// 10 - Develop and improve products
//
// Special purposes:
// 1 - Ensure security, prevent fraud, and debug
// 2 - Technically deliver ads or content
//
// Features:
// 1 - Match and combine offline data sources
// 2 - Link different devices
// 3 - Receive and send automatically sent device characteristics for identification
//
// Special features:
// 1 - Use precise geolocation data
// 2 - Actively scan device characteristics for identification
@Suppress("MagicNumber")
object GoogleAdmobConsentUtils {
// 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
//## search here for Google: https://iabeurope.eu/vendor-list-tcf/
private const val GOOGLE_TCF_VENDOR_ID: Int = 755
private val Context.prefMngr: SharedPreferences get() = PreferenceManager.getDefaultSharedPreferences(this.applicationContext)
//## IABTCF_PurposeConsents - A string of 0's and 1's up to 10 entries long indicating whether the user provided consent for the
//## 10 different purposes
private val SharedPreferences.purposeConsents get() = this.getString("IABTCF_PurposeConsents", "") ?: ""
//## A string of 0s and 1s that is arbitrarily long, indicating whether a given vendor has been given consent for the previously
//## mentioned purposes. Each vendor has an ID indicating their position in the string. For example Google's ID is 755, so if
//## Google has been given consent then the 755th character in this string would be a "1". The full vendor list is available here:
//## https://iabeurope.eu/vendor-list-tcf/
private val SharedPreferences.vendorConsents get() = this.getString("IABTCF_VendorConsents", "") ?: ""
//## A string of 0's and 1's up to 10 entries long indicating whether the app has legitimate interest for the 10 different purposes
private val SharedPreferences.vendorLIs get() = this.getString("IABTCF_VendorLegitimateInterests", "") ?: ""
//## Similar to the vendor consent string, except that it indicates if the vendor has legitimate interest for the previously
//## indicated purposes.
private val SharedPreferences.purposeLIs get() = this.getString("IABTCF_PurposeLegitimateInterests", "") ?: ""
// whether the user is in the EEA
fun isGDPR(
ctx: Context
): Boolean = ctx.prefMngr.getInt("IABTCF_gdprApplies", 0) == 1
fun canShowAds(
ctx: Context
): Boolean {
val prefs: SharedPreferences = ctx.prefMngr
val purposeConsents: String = prefs.purposeConsents
val purposeLIs: String = prefs.purposeLIs
val hasGoogleVendorConsent: Boolean = prefs.vendorConsents.hasAttribute(index = GOOGLE_TCF_VENDOR_ID)
val hasGoogleVendorLI: Boolean = prefs.vendorLIs.hasAttribute(index = GOOGLE_TCF_VENDOR_ID)
debug(
ctx = ctx,
debugStr = "canShowAds",
consentForPurposes = listOf(1),
hasConsentOrLegitimateInterestForPurposes = listOf(2, 7, 9, 10),
purposeConsent = purposeConsents,
purposeLI = purposeLIs,
hasGoogleVendorConsent = hasGoogleVendorConsent,
hasGoogleVendorLI = hasGoogleVendorLI
)
// Minimum required for at least non-personalized ads
return hasConsentFor(
purposes = listOf(1),
purposeConsent = purposeConsents,
hasVendorConsent = hasGoogleVendorConsent
) &&
hasConsentOrLegitimateInterestFor(
purposes = listOf(2, 7, 9, 10),
purposeConsent = purposeConsents,
purposeLI = purposeLIs,
hasVendorConsent = hasGoogleVendorConsent,
hasVendorLI = hasGoogleVendorLI
)
}
fun canShowPersonalizedAds(
ctx: Context
): Boolean {
val prefs: SharedPreferences = ctx.prefMngr
val purposeConsents: String = prefs.purposeConsents
val purposeLIs: String = prefs.purposeLIs
val hasGoogleVendorConsent: Boolean = prefs.vendorConsents.hasAttribute(index = GOOGLE_TCF_VENDOR_ID)
val hasGoogleVendorLI: Boolean = prefs.vendorLIs.hasAttribute(index = GOOGLE_TCF_VENDOR_ID)
debug(
ctx = ctx,
debugStr = "canShowPersonalizedAds",
consentForPurposes = listOf(1, 3, 4),
hasConsentOrLegitimateInterestForPurposes = listOf(2, 7, 9, 10),
purposeConsent = purposeConsents,
purposeLI = purposeLIs,
hasGoogleVendorConsent = hasGoogleVendorConsent,
hasGoogleVendorLI = hasGoogleVendorLI
)
return hasConsentFor(
purposes = listOf(1, 3, 4),
purposeConsent = purposeConsents,
hasVendorConsent = hasGoogleVendorConsent
) &&
hasConsentOrLegitimateInterestFor(
purposes = listOf(2, 7, 9, 10),
purposeConsent = purposeConsents,
purposeLI = purposeLIs,
hasVendorConsent = hasGoogleVendorConsent,
hasVendorLI = hasGoogleVendorLI
)
}
// Check if a binary string has a "1" at position "index" (1-based)
private fun String.hasAttribute(
index: Int
): Boolean = this.getOrNull(index - 1) == '1'
// Check if consent is given for a list of purposes
private fun hasConsentFor(
purposes: List<Int>,
purposeConsent: String,
hasVendorConsent: Boolean
): Boolean = hasVendorConsent &&
purposes.all { p: Int -> purposeConsent.hasAttribute(index = p) }
// 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 = purposes.all { p: Int ->
(hasVendorLI && purposeLI.hasAttribute(index = p)) ||
(hasVendorConsent && purposeConsent.hasAttribute(index = p))
}
private fun debug(
ctx: Context,
debugStr: String,
consentForPurposes: List<Int>,
hasConsentOrLegitimateInterestForPurposes: List<Int>,
purposeConsent: String,
purposeLI: String,
hasGoogleVendorConsent: Boolean,
hasGoogleVendorLI: Boolean
) {
val isDebuggable: Boolean = 0 != (ctx.applicationInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE)
if (!isDebuggable) return
val purposes1: String = consentForPurposes.joinToString(separator = ", ") { purpose: Int ->
val hasConsent: Boolean =
hasConsentFor(purposes = listOf(purpose), purposeConsent = purposeConsent, hasVendorConsent = hasGoogleVendorConsent)
return@joinToString "$purpose-$hasConsent"
}
val purposes2: String = hasConsentOrLegitimateInterestForPurposes.joinToString(separator = ", ") { purpose: Int ->
val hasConsent: Boolean = hasConsentOrLegitimateInterestFor(
purposes = listOf(purpose),
purposeConsent = purposeConsent,
purposeLI = purposeLI,
hasVendorConsent = hasGoogleVendorConsent,
hasVendorLI = hasGoogleVendorLI
)
return@joinToString "$purpose-$hasConsent"
}
val outStr: String = listOf(
debugStr,
" hasGoogleVendorConsent: $hasGoogleVendorConsent",
" hasGoogleVendorLI: $hasGoogleVendorLI",
" hasConsent for purposes: $purposes1",
" hasConsentOrLegitimateInterest for purposes: $purposes2"
).joinToString(separator = "\n")
Log.d("GoogleAdmobConsentUtils", outStr)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment