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()) } + } +}