Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -689,9 +689,11 @@ If not set, SDK will try to fetch data from SharedPreferences (key `IABTCF_gdprA
- `gdprConsent` TCF EU v2 consent string. If not set, SDK will try to fetch data from SharedPreferences (key `IABTCF_TCString`), as stated in
[standard](https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md#in-app-details).
- `gpp` GPP privacy string.
If not set, SDK will try to fetch data from SharedPreferences (key `IABGPP_2_TCString`), as stated in
[standard](https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md#in-app-details)
If not set, SDK will try to fetch data from SharedPreferences (key `IABGPP_HDR_GppString`), as stated in
[standard](https://github.com/InteractiveAdvertisingBureau/Global-Privacy-Platform/blob/main/Core/CMP%20API%20Specification.md)
- `gppSid` A comma-separated list of up to two sections applicable in a given GPP privacy string. This value is required when gpp is present.
If not set and `gpp` is read from SharedPreferences key `IABGPP_HDR_GppString`, SDK will also read key `IABGPP_GppSID`, as stated in
[standard](https://github.com/InteractiveAdvertisingBureau/Global-Privacy-Platform/blob/main/Core/CMP%20API%20Specification.md)
- `req` Optable privacy regulation override, which can be one of: `gdpr`, `can`, `us`, or `null` and will override all other privacy regulations when present.

**NOTE**: The custom values provided to the `OptableConfig` will override all other privacy regulations when present.
Expand Down
6 changes: 4 additions & 2 deletions android_sdk/src/main/java/co/optable/sdk/OptableConsents.kt
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,11 @@ package co.optable.sdk
* @param gdprConsent TCF EU v2 consent string. If not set, SDK will try to fetch data from SharedPreferences (key `IABTCF_TCString`), as stated in
* [standard](https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md#in-app-details).
* @param gpp GPP privacy string.
* If not set, SDK will try to fetch data from SharedPreferences (key `IABGPP_2_TCString`), as stated in
* [standard](https://github.com/InteractiveAdvertisingBureau/GDPR-Transparency-and-Consent-Framework/blob/master/TCFv2/IAB%20Tech%20Lab%20-%20CMP%20API%20v2.md#in-app-details)
* If not set, SDK will try to fetch data from SharedPreferences (key `IABGPP_HDR_GppString`), as stated in
* [standard](https://github.com/InteractiveAdvertisingBureau/Global-Privacy-Platform/blob/main/Core/CMP%20API%20Specification.md)
* @param gppSid A comma-separated list of up to two sections applicable in a given GPP privacy string. This value is required when gpp is present.
* If not set and `gpp` is read from SharedPreferences key `IABGPP_HDR_GppString`, SDK will also read key `IABGPP_GppSID`, as stated in
* [standard](https://github.com/InteractiveAdvertisingBureau/Global-Privacy-Platform/blob/main/Core/CMP%20API%20Specification.md)
* @param reg Optable privacy regulation override, which can be one of: `gdpr`, `can`, `us`, or `null` and will override all other privacy regulations when present.
*/
data class OptableConsents(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,14 @@ internal class ConsentsManager(
}

fun gppSid(): String? {
return customConsents.gppSid
if (customConsents.gppSid != null) {
return customConsents.gppSid
}
// The CMP's section IDs are only used with the CMP's GPP string, never with a custom one.
if (customConsents.gpp == null) {
return localStorage.getGppSid()
}
return null
}

fun customRegulation(): String? {
Expand Down
27 changes: 25 additions & 2 deletions android_sdk/src/main/java/co/optable/sdk/core/LocalStorage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,10 @@ internal class LocalStorage(
companion object {
private const val KEY_SUBJECT_TO_GDPR = "IABTCF_gdprApplies"
private const val KEY_GDPR_CONSENT = "IABTCF_TCString"
private const val KEY_GPP_CONSENT = "IABGPP_2_TCString"
private const val KEY_GPP_STRING = "IABGPP_HDR_GppString"
private const val KEY_GPP_SID = "IABGPP_GppSID"
// Not an IAB key, read before IABGPP_HDR_GppString was. Kept as a fallback for apps that write it.
private const val KEY_LEGACY_GPP_CONSENT = "IABGPP_2_TCString"
}

private val prefs = PreferenceManager.getDefaultSharedPreferences(config.context)
Expand Down Expand Up @@ -121,13 +124,33 @@ internal class LocalStorage(

fun getGppConsent(): String? {
try {
return prefs.getString(KEY_GPP_CONSENT, null)
return getNonBlankString(KEY_GPP_STRING) ?: getNonBlankString(KEY_LEGACY_GPP_CONSENT)
} catch (e: Exception) {
Log.e("OptableSDK", "Can't get GPP consent: ${e.message}")
}
return null
}

/**
* The CMP stores section IDs separated by underscores ("2_7"); the Edge API takes them comma-separated ("2,7").
* Only returned alongside the CMP's own GPP string, never with the legacy key's.
*/
fun getGppSid(): String? {
try {
if (getNonBlankString(KEY_GPP_STRING) == null) {
return null
}
return getNonBlankString(KEY_GPP_SID)?.replace("_", ",")
} catch (e: Exception) {
Log.e("OptableSDK", "Can't get GPP SID: ${e.message}")
}
return null
}

private fun getNonBlankString(key: String): String? {
return prefs.getString(key, null)?.takeIf { it.isNotBlank() }
}

private fun generateUniqueKey(kind: String, config: OptableConfig): String {
val sfx = "${config.host}/${config.tenant}/${config.originSlug}"
return "OPTABLE_" + kind + "_" + Base64.encodeToString(sfx.toByteArray(), 0)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,27 @@ class ConsentsManagerTest {
assertNull(consentsManager.gppSid())
}

@Test
fun `gppSid should return value from localStorage when gpp is not in customConsents`() {
`when`(localStorage.getGppSid()).thenReturn("2,7")
consentsManager = ConsentsManager(localStorage)
assertEquals("2,7", consentsManager.gppSid())
}

@Test
fun `gppSid should not use localStorage value when gpp comes from customConsents`() {
`when`(localStorage.getGppSid()).thenReturn("2,7")
consentsManager = ConsentsManager(localStorage, OptableConsents(gpp = "test_gpp"))
assertNull(consentsManager.gppSid())
}

@Test
fun `gppSid from customConsents should win over localStorage value`() {
`when`(localStorage.getGppSid()).thenReturn("2,7")
consentsManager = ConsentsManager(localStorage, OptableConsents(gppSid = "8"))
assertEquals("8", consentsManager.gppSid())
}

@Test
fun `customRegulation should return value from customConsents`() {
val regString = "test_reg"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,9 @@ class LocalStorageTest {
private companion object {
private const val KEY_SUBJECT_TO_GDPR = "IABTCF_gdprApplies"
private const val KEY_GDPR_CONSENT = "IABTCF_TCString"
private const val KEY_GPP_CONSENT = "IABGPP_2_TCString"
private const val KEY_GPP_STRING = "IABGPP_HDR_GppString"
private const val KEY_GPP_SID = "IABGPP_GppSID"
private const val KEY_LEGACY_GPP_CONSENT = "IABGPP_2_TCString"
private const val HOUR_MS = 60 * 60 * 1000L
}

Expand Down Expand Up @@ -281,16 +283,70 @@ class LocalStorageTest {
@Test
fun `getGppConsent should return value from SharedPreferences`() {
val consent = "test-gpp-consent"
every { mockSharedPreferences.getString(KEY_GPP_CONSENT, null) } returns consent
every { mockSharedPreferences.getString(KEY_GPP_STRING, null) } returns consent
every { mockSharedPreferences.getString(KEY_LEGACY_GPP_CONSENT, null) } returns "legacy-gpp-consent"
assertEquals(consent, localStorage.getGppConsent())
}

@Test
fun `getGppConsent should fall back to the legacy key`() {
val consent = "legacy-gpp-consent"
every { mockSharedPreferences.getString(KEY_GPP_STRING, null) } returns null
every { mockSharedPreferences.getString(KEY_LEGACY_GPP_CONSENT, null) } returns consent
assertEquals(consent, localStorage.getGppConsent())
}

@Test
fun `getGppConsent should return null when not set`() {
every { mockSharedPreferences.getString(KEY_GPP_CONSENT, null) } returns null
every { mockSharedPreferences.getString(KEY_GPP_STRING, null) } returns null
every { mockSharedPreferences.getString(KEY_LEGACY_GPP_CONSENT, null) } returns null
assertNull(localStorage.getGppConsent())
}

@Test
fun `getGppConsent should fall back to the legacy key when the IAB key is blank`() {
val consent = "legacy-gpp-consent"
every { mockSharedPreferences.getString(KEY_GPP_STRING, null) } returns ""
every { mockSharedPreferences.getString(KEY_LEGACY_GPP_CONSENT, null) } returns consent
assertEquals(consent, localStorage.getGppConsent())
}

@Test
fun `getGppConsent should return null when both keys are blank`() {
every { mockSharedPreferences.getString(KEY_GPP_STRING, null) } returns ""
every { mockSharedPreferences.getString(KEY_LEGACY_GPP_CONSENT, null) } returns " "
assertNull(localStorage.getGppConsent())
}

@Test
fun `getGppSid should convert underscores to commas`() {
every { mockSharedPreferences.getString(KEY_GPP_STRING, null) } returns "test-gpp-consent"
every { mockSharedPreferences.getString(KEY_GPP_SID, null) } returns "2_7"
assertEquals("2,7", localStorage.getGppSid())
}

@Test
fun `getGppSid should return null when not set`() {
every { mockSharedPreferences.getString(KEY_GPP_STRING, null) } returns "test-gpp-consent"
every { mockSharedPreferences.getString(KEY_GPP_SID, null) } returns null
assertNull(localStorage.getGppSid())
}

@Test
fun `getGppSid should return null when blank`() {
every { mockSharedPreferences.getString(KEY_GPP_STRING, null) } returns "test-gpp-consent"
every { mockSharedPreferences.getString(KEY_GPP_SID, null) } returns ""
assertNull(localStorage.getGppSid())
}

@Test
fun `getGppSid should return null when the GPP string only comes from the legacy key`() {
every { mockSharedPreferences.getString(KEY_GPP_STRING, null) } returns null
every { mockSharedPreferences.getString(KEY_LEGACY_GPP_CONSENT, null) } returns "legacy-gpp-consent"
every { mockSharedPreferences.getString(KEY_GPP_SID, null) } returns "2_7"
assertNull(localStorage.getGppSid())
}

private fun getPrivateKey(instance: Any, fieldName: String): String {
val field: Field = instance.javaClass.getDeclaredField(fieldName)
field.isAccessible = true
Expand Down
Loading