Last active
October 27, 2023 13:45
-
-
Save Josuhu/2e68dfc15637cc7dd3249c81230747b6 to your computer and use it in GitHub Desktop.
Kotlin classes for GDPR consent
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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) | |
} | |
} | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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() | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
For implementation in to Activity, see coding video series https://youtube.com/playlist?list=PLQVB-tSJSr656zCJoViyF3MYlKGljVTT7&si=4xY8dV7XkrV_7f1f