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
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -350,6 +350,64 @@ On success, the resulting key values are typically sent as part of a subsequent
either call `targeting()` before each ad call, or in parallel periodically, caching the resulting key values which you
then provide in ad calls.

#### Resolver hints (ID5 Mobile In-App)

Some third-party identity resolvers, such as [ID5](https://id5.io/) Mobile In-App, require additional context to return
an identifier. The SDK automatically attaches the following parameters to every `targeting` call:

- `bundle` — the app's package name.
- `ver` — the app's version name.
- `ua` — the device user agent (the same value sent in the `User-Agent` header; overridable via `customUserAgent`).

You can supply additional hint identifiers (HIDs) through an overload of `targeting()` that accepts a `hids` list. Each
hint is encoded exactly like the primary identifiers — with its Optable type prefix (for example `e:`, `p:`, `a:`, `g:`,
`i6:`, and custom `c1:`…`c9:` used for IDFV or a publisher-provided user ID), and, as with the primary `ids` list, the
device's Google Advertising ID is added automatically when available.

The ID5 signature is managed by the SDK: when a `targeting` response returns one, it is cached on the device and resent
automatically on subsequent `targeting` calls to increase ID5 resolve rates and accuracy. No application code is required.

##### Kotlin

```kotlin
val ids = listOf(OptableIdentifier.Email("test@test.com"))
val hids = listOf(
OptableIdentifier.IPv6("2001:0db8:5b96:0000:0000:426f:8e17:642a"),
OptableIdentifier.PhoneNumber("+1(555)1234567"),
OptableIdentifier.AppleIdfa("a1b2c3d4-e5f6-7890-abcd-ef0123456789"),
OptableIdentifier.Custom("c1", "idfv-or-puid-value"),
)
optable.targeting(ids, hids) { result ->
when (result) {
is OptableResult.Success<OptableTargeting> -> {
// Targeting key-values are available via result.data.
}

is OptableResult.Error<OptableTargeting> -> {
Log.d(TAG, "Targeting error: ${result.message}")
}
}
}
```

##### Java

```java
ArrayList<OptableIdentifier> hids = Lists.newArrayList(
new OptableIdentifier.IPv6("2001:0db8:5b96:0000:0000:426f:8e17:642a"),
new OptableIdentifier.PhoneNumber("+1(555)1234567"),
new OptableIdentifier.AppleIdfa("a1b2c3d4-e5f6-7890-abcd-ef0123456789"),
new OptableIdentifier.Custom("c1", "idfv-or-puid-value")
);
optable.targeting(ids, hids, result -> {
if (result instanceof OptableResult.Success<OptableTargeting> success) {
// success.getData() exposes the targeting key-values.
} else if (result instanceof OptableResult.Error<OptableTargeting> error) {
Log.d(TAG, "Targeting error: " + error.getMessage());
}
});
```

#### Caching Targeting Data

The `targeting` API will automatically cache resulting key value data in client storage on success. You can subsequently
Expand Down
34 changes: 31 additions & 3 deletions android_sdk/src/main/java/co/optable/sdk/OptableSDK.kt
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ class OptableSDK(
private val storage = LocalStorage(config)
private val consentsManager = ConsentsManager(storage)
private val googleAdIdManager = GoogleAdIdManager(config)
private val userAgentHolder = UserAgentHolder(config)
private val appInfoHolder = AppInfoHolder(config)
private val networkClient = createNetworkClient()
private val useCases = UseCases()
private val identifiersEncoder = IdentifiersEncoder(googleAdIdManager)
Expand Down Expand Up @@ -135,15 +137,42 @@ class OptableSDK(
* whitespace-stripped, or encrypted). Additional identifiers may be supplied through the `Raw` or `Custom` type.
*/
fun targeting(ids: List<OptableIdentifier>, listener: OptableResultListener<OptableTargeting>) {
targeting(ids, emptyList(), listener)
}

/**
* Calls the Optable Sandbox "targeting" API, which returns the key-value targeting
* data matching the user/device/app and part of the OpenRTB code snippet,
* additionally forwarding [hids] (hint identifiers) for third-party identity resolvers.
* @see OptableTargeting
*
* This type accepts multiple optional identifier values. When EIDs are generated, each supported
* identifier is encoded according to its identifier type (for example, some values are normalized,
* whitespace-stripped, or encrypted). Additional identifiers may be supplied through the `Raw` or `Custom` type.
*/
fun targeting(
ids: List<OptableIdentifier>,
hids: List<OptableIdentifier>,
listener: OptableResultListener<OptableTargeting>,
) {
scope.launch {
googleAdIdManager.deferredTask.await()
val ids = identifiersEncoder.encode(ids)
val response = networkClient.targeting(ids)
val encodedIds = identifiersEncoder.encode(ids)
val encodedHids = identifiersEncoder.encode(hids)
val response = networkClient.targeting(
ids = encodedIds,
hids = encodedHids,
bundle = appInfoHolder.bundle ,
version = appInfoHolder.appVersion,
userAgent = userAgentHolder.getUserAgent(),
id5Signature = storage.getId5Signature(),
)

val optableResult = when (response) {
is NetworkResponse.Success -> {
val targeting = useCases.parseTargetingResponse(response.result)
storage.setTargeting(targeting)
useCases.parseId5Signature(response.result)?.let { storage.setId5Signature(it) }
OptableResult.Success(targeting)
}

Expand Down Expand Up @@ -208,7 +237,6 @@ class OptableSDK(
}

private fun createNetworkClient(): NetworkClient {
val userAgentHolder = UserAgentHolder(config)
val requestInterceptor = RequestInterceptor(config, storage, userAgentHolder, consentsManager)
val responseInterceptor = ResponseInterceptor(storage)
return NetworkClient(config, requestInterceptor, responseInterceptor)
Expand Down
44 changes: 44 additions & 0 deletions android_sdk/src/main/java/co/optable/sdk/core/AppInfoHolder.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package co.optable.sdk.core

import android.content.Context
import android.util.Log
import co.optable.sdk.OptableConfig

/**
* Resolves and caches platform-specific application information used by resolvers such as
* ID5 Mobile In-App and Audigent Hadron:
* - [bundle]: the application's package name (the mobile app's unique identifier).
* - [appVersion]: the application's version name.
*
* Both values are resolved once, at construction, and fall back to `null` (never an empty
* string) when they cannot be determined.
*/
internal class AppInfoHolder(
config: OptableConfig,
) {

val bundle: String? = resolveBundle(config.context)
val appVersion: String? = resolveAppVersion(config.context)

private fun resolveBundle(context: Context): String? {
return try {
context.packageName?.takeIf { it.isNotBlank() }
} catch (e: Exception) {
Log.w("OptableSDK", "Can't resolve app bundle: ${e.message}")
null
}
}

private fun resolveAppVersion(context: Context): String? {
return try {
context.packageManager
.getPackageInfo(context.packageName, 0)
.versionName
?.takeIf { it.isNotBlank() }
} catch (e: Exception) {
Log.w("OptableSDK", "Can't resolve app version: ${e.message}")
null
}
}

}
12 changes: 12 additions & 0 deletions android_sdk/src/main/java/co/optable/sdk/core/LocalStorage.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ internal class LocalStorage(
private val prefs = PreferenceManager.getDefaultSharedPreferences(config.context)
private val passportKey = generateUniqueKey("PASS", config)
private val targetingKey = generateUniqueKey("TGT", config)
private val id5SignatureKey = generateUniqueKey("ID5SIG", config)

fun getPassport(): String? {
return prefs.getString(passportKey, null)
Expand Down Expand Up @@ -63,6 +64,17 @@ internal class LocalStorage(
fun clearTargeting() {
prefs.edit(commit = true) {
remove(targetingKey)
remove(id5SignatureKey)
}
}

fun getId5Signature(): String? {
return prefs.getString(id5SignatureKey, null)
}

fun setId5Signature(signature: String) {
prefs.edit(commit = true) {
putString(id5SignatureKey, signature)
}
}

Expand Down
35 changes: 35 additions & 0 deletions android_sdk/src/main/java/co/optable/sdk/core/UseCases.kt
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,41 @@ class UseCases {
return OptableTargeting(gamTargetingKeywords, openRtbJson, targetingData)
}

fun parseId5Signature(responseJson: JsonObject): String? {
try {
val eids = responseJson
.getAsJsonObject("ortb2")
?.getAsJsonObject("user")
?.getAsJsonArray("eids") ?: return null

val refs = responseJson.getAsJsonObject("refs")

for (eid in eids) {
val eidObj = eid.asJsonObject
val source = eidObj.get("source")?.asString
if (source == null || !source.contains("id5", ignoreCase = true)) continue

val uids = eidObj.getAsJsonArray("uids") ?: continue
for (uid in uids) {
val ref = uid.asJsonObject
.getAsJsonObject("ext")
?.getAsJsonObject("optable")
?.get("ref")
?.asString ?: continue

val signature = refs
?.getAsJsonObject(ref)
?.get("signature")
?.asString
if (!signature.isNullOrBlank()) return signature
}
}
} catch (e: Exception) {
Log.d("OptableSDK", "Can't parse ID5 signature: ${e.message}")
}
return null
}

private fun parseTargetingData(responseJson: JsonObject): JSONObject {
var targetingData = JSONObject()
try {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,16 @@ internal class NetworkClient(
}
}

suspend fun targeting(ids: List<String>): NetworkResponse<JsonObject> {
suspend fun targeting(
ids: List<String>,
hids: List<String>,
bundle: String?,
version: String?,
userAgent: String?,
id5Signature: String?,
): NetworkResponse<JsonObject> {
return runSafe {
edgeService.targeting(ids)
edgeService.targeting(ids, hids, bundle, version, userAgent, id5Signature)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,14 @@ import retrofit2.http.Query
interface EdgeService {

@GET("targeting")
suspend fun targeting(@Query("id") idList: List<String>): Response<JsonObject>
suspend fun targeting(
@Query("id") idList: List<String>,
@Query("hid") hidList: List<String>,
@Query("bundle") bundle: String?,
@Query("ver") version: String?,
@Query("ua") userAgent: String?,
@Query("id5_signature") id5Signature: String?,
): Response<JsonObject>

@POST("identify")
suspend fun identify(@Body ids: List<String>): Response<Unit>
Expand Down
Loading