From 518ba4597c3578f9dd50431bdc893bbbc9c65dad Mon Sep 17 00:00:00 2001 From: Ankit Date: Thu, 21 May 2026 20:37:26 +0100 Subject: [PATCH 1/2] Handle null consent state during GDPR init --- .../kotlin/com/mparticle/kits/AppsFlyerKit.kt | 24 ++++++++------- .../com/mparticle/kits/AppsflyerKitTests.kt | 29 +++++++++++++++++++ 2 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/com/mparticle/kits/AppsFlyerKit.kt b/src/main/kotlin/com/mparticle/kits/AppsFlyerKit.kt index d4cd230..ec5dd63 100644 --- a/src/main/kotlin/com/mparticle/kits/AppsFlyerKit.kt +++ b/src/main/kotlin/com/mparticle/kits/AppsFlyerKit.kt @@ -496,23 +496,25 @@ class AppsFlyerKit : AppsFlyerConsentValues.DENIED.consentValue -> adPersonalizationConsentValue = false } - val clientConsentSettings = parseToNestedMap(consentState.toString()) + if (consentState != null) { + val clientConsentSettings = parseToNestedMap(consentState.toString()) - parseConsentMapping(settings[CONSENT_MAPPING]).iterator().forEach { currentConsent -> + parseConsentMapping(settings[CONSENT_MAPPING]).iterator().forEach { currentConsent -> - val isConsentAvailable = - searchKeyInNestedMap(clientConsentSettings, key = currentConsent.key) + val isConsentAvailable = + searchKeyInNestedMap(clientConsentSettings, key = currentConsent.key) - if (isConsentAvailable != null) { - val isConsentGranted: Boolean = - JSONObject(isConsentAvailable.toString()).opt("consented") as Boolean + if (isConsentAvailable != null) { + val isConsentGranted: Boolean = + JSONObject(isConsentAvailable.toString()).opt("consented") as Boolean - when (currentConsent.value) { - "ad_storage" -> adStorageConsentValue = isConsentGranted + when (currentConsent.value) { + "ad_storage" -> adStorageConsentValue = isConsentGranted - "ad_user_data" -> adUserDataConsentValue = isConsentGranted + "ad_user_data" -> adUserDataConsentValue = isConsentGranted - "ad_personalization" -> adPersonalizationConsentValue = isConsentGranted + "ad_personalization" -> adPersonalizationConsentValue = isConsentGranted + } } } } diff --git a/src/test/kotlin/com/mparticle/kits/AppsflyerKitTests.kt b/src/test/kotlin/com/mparticle/kits/AppsflyerKitTests.kt index e2dfbe3..8a0cfe4 100644 --- a/src/test/kotlin/com/mparticle/kits/AppsflyerKitTests.kt +++ b/src/test/kotlin/com/mparticle/kits/AppsflyerKitTests.kt @@ -244,6 +244,35 @@ class AppsflyerKitTests { Assert.assertEquals(false, notExpectedConsentKey3) } + @Test + @Throws(Exception::class) + fun testSetConsentWhenGDPRAppliesWithoutConsentStateUsesDefaults() { + val map = HashMap() + map["defaultAdStorageConsent"] = "Granted" + map["gdprApplies"] = "true" + map["consentMapping"] = + "[{\\\"jsmap\\\":null,\\\"map\\\":\\\"Performance\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_user_data\\\"},{\\\"jsmap\\\":null,\\\"map\\\":\\\"Marketing\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_personalization\\\"},{\\\"jsmap\\\":null,\\\"map\\\":\\\"testconsent\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_storage\\\"}]" + map["defaultAdUserDataConsent"] = "Denied" + map["defaultAdPersonalizationConsent"] = "Unspecified" + + kit.configuration = + KitConfiguration.createKitConfiguration(JSONObject().put("as", map.toMutableMap())) + + val method: Method = + AppsFlyerKit::class.java.getDeclaredMethod( + "setConsent", + ConsentState::class.java, + ) + method.isAccessible = true + method.invoke(kit, null) + + val afConsentResults = appsflyer.getConsentState() + Assert.assertEquals(true, afConsentResults["isUserSubjectToGDPR"]) + Assert.assertEquals(false, afConsentResults["hasConsentForDataUsage"]) + Assert.assertFalse(afConsentResults.containsKey("hasConsentForAdsPersonalization")) + Assert.assertEquals(true, afConsentResults["hasConsentForAdStorage"]) + } + @Test @Throws(Exception::class) fun testConsentWhenGDPRAppliedWithConsentDefaults() { From 4412a8cfe5efb55a4e99233f4dd51ced89bd9fe3 Mon Sep 17 00:00:00 2001 From: James Newman Date: Wed, 27 May 2026 10:06:15 -0400 Subject: [PATCH 2/2] test: assert no JSONException is logged on null ConsentState; idiomatic guard The existing testSetConsentWhenGDPRAppliesWithoutConsentStateUsesDefaults passes regardless of whether the null-consent guard in setConsent is present, because parseToNestedMap already catches the JSONException raised by JSONObject("null") and returns an empty map; the kit then falls through to defaults either way. The test documents the contract but does not prove the fix. Add testSetConsentWhenGDPRAppliesWithoutConsentStateDoesNotLogJsonError, which installs a recording Logger.AbstractLogHandler and asserts no Throwable was logged during setConsent(null). This test fails on the pre-guard code and passes with the guard in place, so it actually protects the behaviour the PR introduces. Also rewrites the guard from `if (consentState != null) { ... }` to `consentState?.let { state -> ... }` so the smart-cast is local to the block. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../kotlin/com/mparticle/kits/AppsFlyerKit.kt | 4 +- .../com/mparticle/kits/AppsflyerKitTests.kt | 73 +++++++++++++++++++ 2 files changed, 75 insertions(+), 2 deletions(-) diff --git a/src/main/kotlin/com/mparticle/kits/AppsFlyerKit.kt b/src/main/kotlin/com/mparticle/kits/AppsFlyerKit.kt index ec5dd63..cac16a3 100644 --- a/src/main/kotlin/com/mparticle/kits/AppsFlyerKit.kt +++ b/src/main/kotlin/com/mparticle/kits/AppsFlyerKit.kt @@ -496,8 +496,8 @@ class AppsFlyerKit : AppsFlyerConsentValues.DENIED.consentValue -> adPersonalizationConsentValue = false } - if (consentState != null) { - val clientConsentSettings = parseToNestedMap(consentState.toString()) + consentState?.let { state -> + val clientConsentSettings = parseToNestedMap(state.toString()) parseConsentMapping(settings[CONSENT_MAPPING]).iterator().forEach { currentConsent -> diff --git a/src/test/kotlin/com/mparticle/kits/AppsflyerKitTests.kt b/src/test/kotlin/com/mparticle/kits/AppsflyerKitTests.kt index 8a0cfe4..9ecc842 100644 --- a/src/test/kotlin/com/mparticle/kits/AppsflyerKitTests.kt +++ b/src/test/kotlin/com/mparticle/kits/AppsflyerKitTests.kt @@ -5,6 +5,7 @@ import android.content.Context import android.net.Uri import com.appsflyer.AppsFlyerLib import com.mparticle.MParticle +import com.mparticle.MParticle.LogLevel import com.mparticle.MParticleOptions import com.mparticle.commerce.CommerceEvent import com.mparticle.commerce.Product @@ -15,6 +16,7 @@ import com.mparticle.identity.IdentityApi import com.mparticle.identity.MParticleUser import com.mparticle.internal.CoreCallbacks import com.mparticle.internal.CoreCallbacks.KitListener +import com.mparticle.internal.Logger import org.json.JSONArray import org.json.JSONException import org.json.JSONObject @@ -273,6 +275,77 @@ class AppsflyerKitTests { Assert.assertEquals(true, afConsentResults["hasConsentForAdStorage"]) } + @Test + @Throws(Exception::class) + fun testSetConsentWhenGDPRAppliesWithoutConsentStateDoesNotLogJsonError() { + val map = HashMap() + map["gdprApplies"] = "true" + map["defaultAdStorageConsent"] = "Granted" + map["consentMapping"] = + "[{\\\"jsmap\\\":null,\\\"map\\\":\\\"Marketing\\\",\\\"maptype\\\":\\\"ConsentPurposes\\\",\\\"value\\\":\\\"ad_personalization\\\"}]" + + kit.configuration = + KitConfiguration.createKitConfiguration(JSONObject().put("as", map.toMutableMap())) + + val errorLog = RecordingLogHandler() + Logger.setLogHandler(errorLog) + Logger.setMinLogLevel(LogLevel.ERROR, true) + try { + val method: Method = + AppsFlyerKit::class.java.getDeclaredMethod( + "setConsent", + ConsentState::class.java, + ) + method.isAccessible = true + method.invoke(kit, null) + } finally { + Logger.setLogHandler(Logger.DefaultLogHandler()) + } + + Assert.assertNull( + "setConsent(null) must not invoke parseToNestedMap; got error: ${errorLog.lastError}", + errorLog.lastError, + ) + } + + private class RecordingLogHandler : Logger.AbstractLogHandler() { + var lastError: Throwable? = null + + override fun isADBLoggable( + tag: String?, + logLevel: Int, + ): Boolean = true + + override fun verbose( + error: Throwable?, + message: String?, + ) {} + + override fun info( + error: Throwable?, + message: String?, + ) {} + + override fun debug( + error: Throwable?, + message: String?, + ) {} + + override fun warning( + error: Throwable?, + message: String?, + ) {} + + override fun error( + error: Throwable?, + message: String?, + ) { + if (error != null) { + lastError = error + } + } + } + @Test @Throws(Exception::class) fun testConsentWhenGDPRAppliedWithConsentDefaults() {