Skip to content

Instantly share code, notes, and snippets.

@Josuhu
Last active October 27, 2023 13:45
Show Gist options
  • Save Josuhu/2e68dfc15637cc7dd3249c81230747b6 to your computer and use it in GitHub Desktop.
Save Josuhu/2e68dfc15637cc7dd3249c81230747b6 to your computer and use it in GitHub Desktop.
Kotlin classes for GDPR consent
// TODO For implementation see coding video series https://youtube.com/playlist?list=PLQVB-tSJSr656zCJoViyF3MYlKGljVTT7&si=4xY8dV7XkrV_7f1f
class ConsentTracker(val context: Context) {
private val TAG = "ConsentTracker"
private val myLogger = MyLogging() // TODO Log with your own logger
fun isUserConsentValid(): Boolean {
val isGdpr = isGDPR()
val canShowPersAds = canShowPersonalizedAds()
val canShowAds = canShowAds()
val consentValidity = if (!isGdpr) { true } else canShowPersAds || canShowAds
myLogger.logThis(TAG, "isUserConsentValid: $consentValidity," +
" GDPR: $isGdpr, PersAds: $canShowPersAds, Ads: $canShowAds", Log.DEBUG)
return consentValidity
}
private fun isGDPR(): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val gdpr = prefs.getInt("IABTCF_gdprApplies", 0)
return gdpr == 1
}
private fun canShowAds(): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
//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)
}
private fun canShowPersonalizedAds(): Boolean {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
//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)
}
}
}
// TODO For implementation see coding video series https://youtube.com/playlist?list=PLQVB-tSJSr656zCJoViyF3MYlKGljVTT7&si=4xY8dV7XkrV_7f1f
class MyGdpr(val context: Context) {
@Suppress("PrivatePropertyName")
private val TAG = "MyGdpr"
private val myLogging = MyLogging() // TODO Log with your own logger
private val consentInformation = UserMessagingPlatform.getConsentInformation(context)
private var consentForm: ConsentForm? = null
private val myToasts = MyToasts(context) // TODO Toast with your own toaster
/**IN PRODUCTION CALL AT ONCREATE FOR CONSENT FORM CHECK*/
fun updateConsentInfo(
activity: Activity,
underAge: Boolean,
viewModel: MainViewModel,
consentTracker: ConsentTracker,
initAds: () -> Unit
) {
val params = ConsentRequestParameters
.Builder()
// .setAdMobAppId(context.getString(R.string.AdMob_App_ID))
.setTagForUnderAgeOfConsent(underAge)
.build()
requestConsentInfoUpdate(
activity = activity,
params = params,
viewModel = viewModel,
consentTracker = consentTracker,
initAds = { initAds() }
)
}
/**ONLY TO DEBUG EU & NONE EU GEOGRAPHICS
* EU: ConsentDebugSettings.DebugGeography.DEBUG_GEOGRAPHY_EEA
* NOT EU: ConsentDebugSettings.DebugGeography.DEBUG_GEOGRAPHY_NOT_EEA
* DISABLED: ConsentDebugSettings.DebugGeography.DEBUG_GEOGRAPHY_DISABLED
* requestConsentInfoUpdate() logs the hashed id when run*/
fun updateConsentInfoWithDebugGeographics(
activity: Activity,
georaph: Int,
viewModel: MainViewModel,
consentTracker: ConsentTracker,
initAds: () -> Unit
) {
val debugSettings = ConsentDebugSettings.Builder(context)
.setDebugGeography(georaph)
.addTestDeviceHashedId(/*TODO hashedID string here*/) //Insert your test devices hashedID
.build()
val params = ConsentRequestParameters
.Builder()
.setConsentDebugSettings(debugSettings)
// .setAdMobAppId(context.getString(R.string.AdMob_App_ID))
.build()
requestConsentInfoUpdate(
activity = activity,
params = params,
consentTracker = consentTracker,
viewModel = viewModel,
initAds = { initAds() }
)
}
private fun requestConsentInfoUpdate(
activity: Activity,
params: ConsentRequestParameters,
viewModel: MainViewModel,
consentTracker: ConsentTracker,
initAds: () -> Unit
) {
consentInformation.requestConsentInfoUpdate(
activity,
params,
{ // The consent information state was updated, ready to check if a form is available.
if (consentInformation.isConsentFormAvailable) {
loadForm(activity, viewModel, consentTracker, initAds = { initAds() })
} else { viewModel.consentPermit.value = isConsentObtained(consentTracker) }
},
{ formError ->
myLogging.logThis(TAG, "requestConsentInfoUpdate: ${formError.message}", Log.ERROR)
}
)
}
private fun loadForm(
activity: Activity,
viewModel: MainViewModel,
consentTracker: ConsentTracker,
initAds: () -> Unit
) { // Loads a consent form. Must be called on the main thread.
UserMessagingPlatform.loadConsentForm(
context,
{ _consentForm ->
// Take form if needed later
consentForm = _consentForm
when (consentInformation.consentStatus) {
ConsentInformation.ConsentStatus.REQUIRED -> {
myLogging.logThis(TAG, "consentForm is required to show", Log.DEBUG)
consentForm?.show(
activity,
) { formError ->
// Log error
if (formError != null) {
myLogging.logThis(TAG, "consentForm show ${formError.message}", Log.ERROR)
}
// App can start requesting ads.
if (consentInformation.consentStatus == ConsentInformation.ConsentStatus.OBTAINED) {
myLogging.logThis(TAG, "consentForm is Obtained", Log.DEBUG)
viewModel.consentPermit.value = isConsentObtained(consentTracker)
initAds()
}
// Handle dismissal by reloading form.
loadForm(activity, viewModel, consentTracker, initAds)
}
}
else -> { viewModel.consentPermit.value = isConsentObtained(consentTracker) }
}
},
{ formError ->
myLogging.logThis(TAG, "loadForm Failure: ${formError.message}", Log.ERROR)
},
)
}
fun reUseExistingConsentForm(
activity: Activity,
viewModel: MainViewModel,
consentTracker: ConsentTracker,
initAds: () -> Unit
) {
if (consentInformation.isConsentFormAvailable) {
myLogging.logThis(TAG, "reUseExistingConsentForm", Log.DEBUG)
consentForm?.show(
activity,
) { formError ->
// Log error
if (formError != null) {
myLogging.logThis(TAG, "consentForm show ${formError.message}", Log.ERROR)
}
// App can start requesting ads.
if (consentInformation.consentStatus == ConsentInformation.ConsentStatus.OBTAINED) {
myLogging.logThis(TAG, "consentForm is Obtained", Log.DEBUG)
viewModel.consentPermit.value = isConsentObtained(consentTracker)
initAds()
}
// Handle dismissal by reloading form.
loadForm(activity, viewModel, consentTracker, initAds)
}
} else {
myToasts.toastText("Consent form not available, check internet connection.")
viewModel.consentPermit.value = isConsentObtained(consentTracker)
}
}
/**RETURNS TRUE IF EU/UK IS TRULY OBTAINED OR NOT REQUIRED ELSE FALSE*/
private fun isConsentObtained(consentTracker: ConsentTracker): Boolean {
val obtained = consentTracker.isUserConsentValid() && consentInformation.consentStatus == ConsentInformation.ConsentStatus.OBTAINED
val notRequired = consentInformation.consentStatus == ConsentInformation.ConsentStatus.NOT_REQUIRED
val isObtained = obtained || notRequired
myLogging.logThis(TAG, "isConsentObtained or not required: $isObtained", Log.DEBUG)
return isObtained
}
/**RESET ONLY IF TRULY REQUIRED. E.G FOR TESTING OR USER WANTS TO RESET CONSENT SETTINGS*/
fun resetConsent() {
consentInformation.reset()
}
}
@Josuhu
Copy link
Author

Josuhu commented Oct 27, 2023

For implementation in to Activity, see coding video series https://youtube.com/playlist?list=PLQVB-tSJSr656zCJoViyF3MYlKGljVTT7&si=4xY8dV7XkrV_7f1f

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment