From eda02d59906657e90c528704777ca4e902a6aa8d Mon Sep 17 00:00:00 2001 From: John Rosendahl Date: Mon, 9 Nov 2026 19:01:18 -0700 Subject: [PATCH] Make OptableConfig options settable after construction host, path, insecure, apiKey, customUserAgent and cacheTtl were internal vals, so Java callers had to pass every preceding constructor parameter to set one. Make them public vars, like origin and consents and like the iOS SDK. apiKey, customUserAgent and cacheTtl now apply from the next call. host, path and insecure build the base URL and storage keys, so they are read when OptableSDK is created; the KDoc and README say so. Co-Authored-By: Claude Opus 5.5 --- README.md | 9 ++- .../main/java/co/optable/sdk/OptableConfig.kt | 22 ++++--- .../java/co/optable/sdk/core/LocalStorage.kt | 4 +- .../co/optable/sdk/core/UserAgentHolder.kt | 6 +- .../java/co/optable/sdk/OptableConfigTest.kt | 20 +++++++ .../co/optable/sdk/core/LocalStorageTest.kt | 14 +++++ .../optable/sdk/core/UserAgentHolderTest.kt | 57 +++++++++++++++++++ 7 files changed, 118 insertions(+), 14 deletions(-) create mode 100644 android_sdk/src/test/java/co/optable/sdk/core/UserAgentHolderTest.kt diff --git a/README.md b/README.md index 3ea430a..6df3b7a 100644 --- a/README.md +++ b/README.md @@ -200,13 +200,20 @@ val config = OptableConfig( ) ``` -`origin` can also be set after the config has been created, which is the convenient form from Java: +`origin`, and every other optional parameter, can also be set after the config has been created, which is the +convenient form from Java: ```java OptableConfig config = new OptableConfig(this, "prebidtest", "android-sdk"); config.setOrigin("https://www.acmeco.com"); +config.setApiKey(API_KEY); +config.setCacheTtl(60 * 60L); +optable = new OptableSDK(config); ``` +`host`, `path`, `insecure` and `consents` are read when the `OptableSDK` instance is created, so set them before +creating it (use `setConsents()` to change consents afterwards). The other parameters apply from the next call. + ### OptableIdentifier There are many fields to identify the users. You can provide any user information using the `OptableIdentifier` class. diff --git a/android_sdk/src/main/java/co/optable/sdk/OptableConfig.kt b/android_sdk/src/main/java/co/optable/sdk/OptableConfig.kt index 391bdf7..2f89363 100644 --- a/android_sdk/src/main/java/co/optable/sdk/OptableConfig.kt +++ b/android_sdk/src/main/java/co/optable/sdk/OptableConfig.kt @@ -6,17 +6,23 @@ import android.content.Context /** * Configuration class for Optable integration. * + * The optional settings can also be set after construction, which is how Java callers set them without passing + * every preceding parameter. Settings marked "read when `OptableSDK` is created" must be set before the config is + * passed to `OptableSDK`; changing them later has no effect on that instance. The others apply from the next call. + * * @param tenant The tenant name associated with the configuration. E.g. `acmeco.optable.co` => `acmeco`. * @param originSlug The DCN's Source Slug. E.g. `acmeco-sdk`. * @param host The hostname of the Optable endpoint. Default value is "na.edge.optable.co". - * @param path The API path to be appended to the host. Default value is "v2". + * Read when `OptableSDK` is created. + * @param path The API path to be appended to the host. Default value is "v2". Read when `OptableSDK` is created. * @param insecure Boolean flag that determines if insecure HTTP should be used instead of HTTPS. Default is false. + * Read when `OptableSDK` is created. * @param apiKey An optional API key for authentication. If the API Endpoint is enabled as private, a Service Account API key will be required. * @param customUserAgent An optional custom user agent string for network requests. * @param skipAdvertisingIdDetection Boolean flag to skip the detection of advertising IDs. Default is false. * @param consents Optional `OptableConsents` object for providing custom consent information. If not provided, default values will be used. + * Read when `OptableSDK` is created; use `OptableSDK.setConsents` to change them afterwards. * @param origin An optional value for the HTTP `Origin` header sent with Optable API requests. Unrelated to `originSlug`. - * Settable after construction, which is how Java callers set it without passing every preceding parameter. * @param cacheTtl The targeting cache time to live in seconds. Default value is 24 hours. * A value of zero or less disables the cache. */ @@ -24,15 +30,15 @@ class OptableConfig @JvmOverloads constructor( providedContext: Context, internal val tenant: String, internal val originSlug: String, - internal val host: String = "na.edge.optable.co", - internal val path: String = "v2", - internal val insecure: Boolean = false, - internal val apiKey: String? = null, - internal val customUserAgent: String? = null, + var host: String = "na.edge.optable.co", + var path: String = "v2", + var insecure: Boolean = false, + @Volatile var apiKey: String? = null, + @Volatile var customUserAgent: String? = null, internal val skipAdvertisingIdDetection: Boolean = false, var consents: OptableConsents = OptableConsents(), var origin: String? = null, - internal val cacheTtl: Long = DEFAULT_TARGETING_CACHE_TTL_SECONDS, + @Volatile var cacheTtl: Long = DEFAULT_TARGETING_CACHE_TTL_SECONDS, ) { companion object { diff --git a/android_sdk/src/main/java/co/optable/sdk/core/LocalStorage.kt b/android_sdk/src/main/java/co/optable/sdk/core/LocalStorage.kt index 75382fe..78b6079 100644 --- a/android_sdk/src/main/java/co/optable/sdk/core/LocalStorage.kt +++ b/android_sdk/src/main/java/co/optable/sdk/core/LocalStorage.kt @@ -18,7 +18,7 @@ import org.json.JSONObject * Manages SharedPreferences for Optable SDK. */ internal class LocalStorage( - config: OptableConfig, + private val config: OptableConfig, ) { companion object { @@ -31,7 +31,7 @@ internal class LocalStorage( private val passportKey = generateUniqueKey("PASS", config) private val targetingKey = generateUniqueKey("TGT", config) private val targetingTimestampKey = generateUniqueKey("TGT_TS", config) - private val targetingCacheTtlMs = config.cacheTtl.coerceAtMost(Long.MAX_VALUE / 1000) * 1000L + private val targetingCacheTtlMs get() = config.cacheTtl.coerceAtMost(Long.MAX_VALUE / 1000) * 1000L private val id5SignatureKey = generateUniqueKey("ID5SIG", config) fun getPassport(): String? { diff --git a/android_sdk/src/main/java/co/optable/sdk/core/UserAgentHolder.kt b/android_sdk/src/main/java/co/optable/sdk/core/UserAgentHolder.kt index 84669de..ff5c2e9 100644 --- a/android_sdk/src/main/java/co/optable/sdk/core/UserAgentHolder.kt +++ b/android_sdk/src/main/java/co/optable/sdk/core/UserAgentHolder.kt @@ -5,12 +5,12 @@ import android.webkit.WebSettings import co.optable.sdk.OptableConfig internal class UserAgentHolder( - config: OptableConfig, + private val config: OptableConfig, ) { - private val cachedUserAgent: String? = config.customUserAgent ?: defaultUserAgent(config.context) + private val cachedDefaultUserAgent: String? by lazy { defaultUserAgent(config.context) } - fun getUserAgent() = cachedUserAgent + fun getUserAgent() = config.customUserAgent ?: cachedDefaultUserAgent private fun defaultUserAgent(context: Context): String? { diff --git a/android_sdk/src/test/java/co/optable/sdk/OptableConfigTest.kt b/android_sdk/src/test/java/co/optable/sdk/OptableConfigTest.kt index 4afc023..4e2dca8 100644 --- a/android_sdk/src/test/java/co/optable/sdk/OptableConfigTest.kt +++ b/android_sdk/src/test/java/co/optable/sdk/OptableConfigTest.kt @@ -76,6 +76,26 @@ class OptableConfigTest { assertEquals(60L, config.cacheTtl) } + @Test + fun `optional properties should be settable after construction`() { + val config = OptableConfig(mockContext, "test-tenant", "test-slug") + + config.host = "custom.host.com" + config.path = "v3" + config.insecure = true + config.apiKey = "test-api-key" + config.customUserAgent = "TestAgent/1.0" + config.cacheTtl = 60L + + assertEquals("custom.host.com", config.host) + assertEquals("v3", config.path) + assertTrue(config.insecure) + assertEquals("test-api-key", config.apiKey) + assertEquals("TestAgent/1.0", config.customUserAgent) + assertEquals(60L, config.cacheTtl) + assertEquals("http://custom.host.com/v3/", config.getBaseUrl()) + } + @Test fun `getBaseUrl should return HTTPS url when insecure is false`() { val config = OptableConfig( diff --git a/android_sdk/src/test/java/co/optable/sdk/core/LocalStorageTest.kt b/android_sdk/src/test/java/co/optable/sdk/core/LocalStorageTest.kt index 8d0893b..dfc9a5b 100644 --- a/android_sdk/src/test/java/co/optable/sdk/core/LocalStorageTest.kt +++ b/android_sdk/src/test/java/co/optable/sdk/core/LocalStorageTest.kt @@ -194,6 +194,20 @@ class LocalStorageTest { assertNull(storage.getTargeting()) } + @Test + fun `getTargeting should apply a TTL changed after construction`() { + val config = OptableConfig(mockContext, "testhost", "testtenant", "testslug") + val storage = LocalStorage(config) + val targetingKey = getPrivateKey(storage, "targetingKey") + every { mockSharedPreferences.getString(targetingKey, null) } returns cachedTargetingJson(storage) + cacheTimestamp(storage, System.currentTimeMillis() - 3 * HOUR_MS) + assertEquals(targeting.gamTargetingKeywords, storage.getTargeting()?.gamTargetingKeywords) + + config.cacheTtl = 2 * 60 * 60L + + assertNull(storage.getTargeting()) + } + @Test fun `getTargeting should keep the cache valid when the configured TTL is huge`() { val storage = LocalStorage( diff --git a/android_sdk/src/test/java/co/optable/sdk/core/UserAgentHolderTest.kt b/android_sdk/src/test/java/co/optable/sdk/core/UserAgentHolderTest.kt new file mode 100644 index 0000000..83f3394 --- /dev/null +++ b/android_sdk/src/test/java/co/optable/sdk/core/UserAgentHolderTest.kt @@ -0,0 +1,57 @@ +package co.optable.sdk.core + +import android.content.Context +import android.webkit.WebSettings +import co.optable.sdk.OptableConfig +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.unmockkAll +import io.mockk.verify +import org.junit.After +import org.junit.Assert.assertEquals +import org.junit.Before +import org.junit.Test + +class UserAgentHolderTest { + + private lateinit var context: Context + private lateinit var config: OptableConfig + + @Before + fun setup() { + context = mockk() + config = OptableConfig(mockk { every { applicationContext } returns context }, "tenant", "slug") + mockkStatic(WebSettings::class) + every { WebSettings.getDefaultUserAgent(context) } returns "DefaultAgent/1.0" + } + + @After + fun teardown() { + unmockkAll() + } + + @Test + fun `getUserAgent should return the default user agent when no custom one is set`() { + assertEquals("DefaultAgent/1.0", UserAgentHolder(config).getUserAgent()) + } + + @Test + fun `getUserAgent should apply a custom user agent changed after construction`() { + val holder = UserAgentHolder(config) + + config.customUserAgent = "CustomAgent/1.0" + assertEquals("CustomAgent/1.0", holder.getUserAgent()) + + config.customUserAgent = null + assertEquals("DefaultAgent/1.0", holder.getUserAgent()) + } + + @Test + fun `getUserAgent should not read the default user agent when a custom one is set`() { + config.customUserAgent = "CustomAgent/1.0" + + assertEquals("CustomAgent/1.0", UserAgentHolder(config).getUserAgent()) + verify(exactly = 0) { WebSettings.getDefaultUserAgent(any()) } + } +}