Skip to content
Open
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, 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, 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 && localStorage.getGppConsent() != null) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also passes when the GPP string comes from the legacy key, so the CMP's section IDs get sent with a string the CMP didn't write. Should it only use them when IABGPP_HDR_GppString is set?

return localStorage.getGppSid()
}
return null
}

fun customRegulation(): String? {
Expand Down
19 changes: 17 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,25 @@ internal class LocalStorage(

fun getGppConsent(): String? {
try {
return prefs.getString(KEY_GPP_CONSENT, null)
return prefs.getString(KEY_GPP_STRING, null) ?: prefs.getString(KEY_LEGACY_GPP_CONSENT, null)
} 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").
*/
fun getGppSid(): String? {
try {
return prefs.getString(KEY_GPP_SID, null)?.replace("_", ",")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If the stored value is empty, this sends gpp_sid=. Should it return null when blank?

} catch (e: Exception) {
Log.e("OptableSDK", "Can't get GPP SID: ${e.message}")
}
return null
}

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,30 @@ class ConsentsManagerTest {
assertNull(consentsManager.gppSid())
}

@Test
fun `gppSid should return value from localStorage when gpp also comes from localStorage`() {
`when`(localStorage.getGppConsent()).thenReturn("local_storage_gpp")
`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.getGppConsent()).thenReturn("local_storage_gpp")
`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.getGppConsent()).thenReturn("local_storage_gpp")
`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,38 @@ 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 `getGppSid should convert underscores to commas`() {
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_SID, null) } returns null
assertNull(localStorage.getGppSid())
}

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