Skip to content

Commit

Permalink
Moments API service
Browse files Browse the repository at this point in the history
  • Loading branch information
tamayok committed Apr 23, 2024
1 parent b0781e6 commit e253b4a
Show file tree
Hide file tree
Showing 11 changed files with 324 additions and 3 deletions.
30 changes: 28 additions & 2 deletions mobile/src/main/java/com/tealium/mobile/TealiumHelper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,16 @@ import com.tealium.remotecommanddispatcher.RemoteCommands
import com.tealium.remotecommanddispatcher.remoteCommands
import com.tealium.remotecommands.RemoteCommand
import com.tealium.tagmanagementdispatcher.TagManagement
import com.tealium.tagmanagementdispatcher.sessionCountingEnabled
import com.tealium.visitorservice.VisitorProfile
import com.tealium.visitorservice.VisitorService
import com.tealium.visitorservice.VisitorUpdatedListener
import com.tealium.visitorservice.momentsapi.EngineResponse
import com.tealium.visitorservice.momentsapi.ErrorCode
import com.tealium.visitorservice.momentsapi.MomentsApi
import com.tealium.visitorservice.momentsapi.MomentsApiRegion
import com.tealium.visitorservice.momentsapi.ResponseListener
import com.tealium.visitorservice.momentsapi.momentsApi
import com.tealium.visitorservice.momentsapi.momentsApiRegion
import java.util.concurrent.TimeUnit

object TealiumHelper : ActivityDataCollector {
Expand All @@ -49,7 +55,8 @@ object TealiumHelper : ActivityDataCollector {
Modules.InAppPurchaseManager,
Modules.AutoTracking,
Modules.Media,
QueryParamProviderModule
QueryParamProviderModule,
Modules.MomentsApi
),
dispatchers = mutableSetOf(
Dispatchers.Collect,
Expand Down Expand Up @@ -81,6 +88,7 @@ object TealiumHelper : ActivityDataCollector {
// overrideConsentCategoriesKey = "my_consent_categories_key"

visitorIdentityKey = BuildConfig.IDENTITY_KEY
momentsApiRegion = MomentsApiRegion.OREGON
}

Tealium.create(BuildConfig.TEALIUM_INSTANCE, config) {
Expand Down Expand Up @@ -143,6 +151,7 @@ object TealiumHelper : ActivityDataCollector {
fun trackEvent(name: String, data: Map<String, Any>?) {
val eventDispatch = TealiumEvent(name, data)
Tealium[BuildConfig.TEALIUM_INSTANCE]?.track(eventDispatch)
getMomentsVisitorData()
}

fun trackPurchase(purchase: Purchase, data: Map<String, Any>?) {
Expand All @@ -152,6 +161,23 @@ object TealiumHelper : ActivityDataCollector {
)
}

fun getMomentsVisitorData() {
Tealium[BuildConfig.TEALIUM_INSTANCE]?.momentsApi?.requestVisitorDataForEngine(
"123",
object : ResponseListener<EngineResponse> {
override fun success(data: EngineResponse) {
Logger.dev(BuildConfig.TAG, "Visitor data badges: ${data.badges.toString()}")
Logger.dev(BuildConfig.TAG, "Visitor data audiences: ${data.audiences.toString()}")
Logger.dev(BuildConfig.TAG, "Visitor data properties: ${data.properties.toString()}")
}

override fun failure(errorCode: ErrorCode, message: String) {
Logger.dev(BuildConfig.TAG, "Moments API Error - ${errorCode}: $message")
}
}
)
}

fun retrieveDatalayer(): Map<String, Any>? {
return Tealium[BuildConfig.TEALIUM_INSTANCE]?.gatherTrackData()
}
Expand Down
5 changes: 4 additions & 1 deletion visitorservice/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.tealium.visitorservice" />
package="com.tealium.visitorservice" >

<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
</manifest>
11 changes: 11 additions & 0 deletions visitorservice/src/main/java/com/tealium/visitorservice/Utils.kt
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.tealium.visitorservice

import com.tealium.core.Logger
import org.json.JSONArray
import org.json.JSONObject
import java.lang.Exception

Expand Down Expand Up @@ -150,4 +151,14 @@ internal fun JSONObject.asTallies(): Map<String, Map<String, Double>> {
}
}
return map
}

fun JSONArray.asStringList(): List<String> {
val list = ArrayList<String>()
for (i in 0 until length()) {
val element = get(i) as String
list.add(element)
}

return list.toList()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.tealium.visitorservice.momentsapi

import com.tealium.core.JsonUtils
import com.tealium.visitorservice.asStringList
import org.json.JSONArray
import org.json.JSONObject

const val KEY_AUDIENCES = "audiences"
const val KEY_BADGES = "badges"
const val KEY_PROPERTIES = "properties"


data class EngineResponse(
val properties: Map<String, Any>? = null,
val badges: List<String>? = null,
val audiences: List<String>? = null
) {

companion object {
fun toJson(engineResponse: EngineResponse): JSONObject {
val json = JSONObject()

engineResponse.properties?.let {
json.put(KEY_PROPERTIES, JsonUtils.jsonFor(it))
}

engineResponse.badges?.let {
json.put(KEY_BADGES, JSONArray(it))
}

engineResponse.audiences?.let {
json.put(KEY_AUDIENCES, JSONArray(it))
}

return json
}

fun fromJson(json: JSONObject): EngineResponse {
return EngineResponse(
properties = json.optJSONObject(KEY_PROPERTIES)?.let { JsonUtils.mapFor(it) },
badges = json.optJSONArray(KEY_BADGES)?.asStringList(),
audiences = json.optJSONArray(KEY_AUDIENCES)?.asStringList()
)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.tealium.visitorservice.momentsapi

enum class ErrorCode(val value: Int) {
BAD_REQUEST(400 ),
ENGINE_NOT_ENABLED(403),
VISITOR_NOT_FOUND(404),
NOT_CONNECTED (0),
INVALID_JSON (1),
UNKNOWN_ERROR(2)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.tealium.visitorservice.momentsapi

import android.content.Context
import android.net.ConnectivityManager
import android.net.NetworkCapabilities
import android.os.Build
import com.tealium.core.Logger
import com.tealium.tealiumlibrary.BuildConfig
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext
import java.io.BufferedReader
import java.net.HttpURLConnection
import java.net.URL
import javax.net.ssl.HttpsURLConnection

interface NetworkClient {
fun isConnected(): Boolean
suspend fun get(url:URL, listener: ResponseListener<String>)
}

class HttpClient(private val context: Context) : NetworkClient {

private val connectivityManager: ConnectivityManager
get() = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager

private val activeNetworkCapabilities: NetworkCapabilities?
get() = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
try {
connectivityManager.getNetworkCapabilities(connectivityManager.activeNetwork)
} catch (ex: java.lang.Exception) {
Logger.qa(BuildConfig.TAG, "Error retrieving active network capabilities, ${ex.message}")
null
}
} else null

override fun isConnected(): Boolean {
return activeNetworkCapabilities?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?: false
}

override suspend fun get(url: URL, listener: ResponseListener<String>) = coroutineScope {
if (!isConnected()) {
listener.failure(ErrorCode.NOT_CONNECTED, "No connectivity established")
}

withContext(Dispatchers.IO) {
with(url.openConnection() as HttpURLConnection) {
requestMethod = "GET"
val reader: BufferedReader?
try {
when(responseCode) {
HttpsURLConnection.HTTP_OK -> {
reader = inputStream.bufferedReader(Charsets.UTF_8)
val response = reader.readText()
listener.success(response)
}
else -> listener.failure(ErrorCode.valueOf(responseCode.toString()), responseMessage)
}
} catch (ex: Exception) {
listener.failure(ErrorCode.UNKNOWN_ERROR, ex.message ?: "Unknown error fetching engine response")
}
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package com.tealium.visitorservice.momentsapi

import com.tealium.core.Logger
import com.tealium.core.TealiumContext
import com.tealium.core.network.ResourceRetriever
import com.tealium.visitorservice.BuildConfig
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import org.json.JSONException
import org.json.JSONObject
import java.net.URL

interface MomentsApiManager {
fun fetchEngineResponse(engineId: String, handler: ResponseListener<EngineResponse>)
}

class MomentsManager(
private val context: TealiumContext,
private val networkClient: NetworkClient = HttpClient(context.config.application),
private val backgroundScope: CoroutineScope = CoroutineScope(Dispatchers.IO)
) : MomentsApiManager {

override fun fetchEngineResponse(engineId: String, handler: ResponseListener<EngineResponse>) {
backgroundScope.launch {
val urlString = generateMomentsApiUrl(engineId)
networkClient.get(URL(urlString), object : ResponseListener<String> {
override fun success(data: String) {
try {
val json = JSONObject(data)
val vp = EngineResponse.fromJson(json)
handler.success(vp)
} catch (ex: Exception) {
handler.failure(ErrorCode.INVALID_JSON, "Invalid JSON VisitorProfile")
}
}

override fun failure(errorCode: ErrorCode, message: String) {
handler.failure(errorCode, message)
}
})
}
}

private suspend fun fetch(engineId: String): EngineResponse? {
val retriever = ResourceRetriever(
context.config,
generateMomentsApiUrl(engineId),
context.httpClient
).apply {
maxRetries = 3
useIfModifed = false
}

return retriever.fetch()?.let {
try {
val json = JSONObject(it)
EngineResponse.fromJson(json)
} catch (ex: JSONException) {
Logger.qa(BuildConfig.TAG, "Exception parsing retrieved JSON.")
null
}
}
}

internal fun generateMomentsApiUrl(engineId: String): String {
return DEFAULT_VISITOR_SERVICE_TEMPLATE
.replace(PLACEHOLDER_ACCOUNT, context.config.accountName)
.replace(PLACEHOLDER_REGION, context.config.momentsApiRegion!!.value)
.replace(PLACEHOLDER_ACCOUNT, context.config.accountName)
.replace(PLACEHOLDER_PROFILE, context.config.profileName)
.replace(PLACEHOLDER_ENGINE_ID, engineId)
.replace(PLACEHOLDER_VISITOR_ID, context.visitorId)
}

companion object {
const val PLACEHOLDER_REGION = "{{region}}"
const val PLACEHOLDER_ACCOUNT = "{{account}}"
const val PLACEHOLDER_PROFILE = "{{profile}}"
const val PLACEHOLDER_ENGINE_ID = "{{engineId}}"
const val PLACEHOLDER_VISITOR_ID = "{{visitorId}}"

const val DEFAULT_VISITOR_SERVICE_TEMPLATE =
"https://personalization-api.$PLACEHOLDER_REGION.prod.tealiumapis.com/personalization/accounts/$PLACEHOLDER_ACCOUNT/profiles/$PLACEHOLDER_PROFILE/engines/$PLACEHOLDER_ENGINE_ID/visitors/$PLACEHOLDER_VISITOR_ID?ignoreTapid=true"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.tealium.visitorservice.momentsapi

enum class MomentsApiRegion(val value: String) {
GERMANY("eu-central-1"),
US_EAST("us-east-1"),
SYDNEY("ap-southeast-2"),
OREGON("us-west-2"),
TOKYO("ap-northeast-1"),
HONG_KONG("ap-east-1")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.tealium.visitorservice.momentsapi

import com.tealium.core.Module
import com.tealium.core.ModuleFactory
import com.tealium.core.Modules
import com.tealium.core.Tealium
import com.tealium.core.TealiumContext
import com.tealium.visitorservice.BuildConfig
import java.lang.Exception

class MomentsApiService @JvmOverloads constructor(
private val context: TealiumContext,
private val momentsApiManager: MomentsApiManager = MomentsManager(context)
) : Module {

override var enabled: Boolean = true // change
override val name: String = MODULE_NAME

fun requestVisitorDataForEngine(engineId: String, responseListener: ResponseListener<EngineResponse>) {
momentsApiManager.fetchEngineResponse(engineId, responseListener)
}

companion object : ModuleFactory {
const val MODULE_NAME = "MomentsApi"
const val MODULE_VERSION = BuildConfig.LIBRARY_VERSION

override fun create(context: TealiumContext): Module {
context.config.momentsApiRegion?.let {
return MomentsApiService(context)
}
throw Exception("MomentsApi must have a region assigned. Ensure you have set one on TealiumConfig.")
}
}
}

val Modules.MomentsApi: ModuleFactory
get() = MomentsApiService

/**
* Returns the MomentsApi module for this Tealium instance.
*/
val Tealium.momentsApi: MomentsApiService?
get() = modules.getModule(MomentsApiService.MODULE_NAME) as? MomentsApiService
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.tealium.visitorservice.momentsapi

interface ResponseListener<T> {
fun success(data: T)
fun failure(errorCode: ErrorCode, message: String)
// fun failure(message: String)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.tealium.visitorservice.momentsapi

import com.tealium.core.TealiumConfig

const val MOMENTS_API_REGION = "moments_api_region"

var TealiumConfig.momentsApiRegion: MomentsApiRegion?
get() = options[MOMENTS_API_REGION] as? MomentsApiRegion
set(value) {
value?.let {
options[MOMENTS_API_REGION] = it
}
}

0 comments on commit e253b4a

Please sign in to comment.