-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
11 changed files
with
324 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
46 changes: 46 additions & 0 deletions
46
visitorservice/src/main/java/com/tealium/visitorservice/momentsapi/EngineResponse.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
) | ||
} | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
visitorservice/src/main/java/com/tealium/visitorservice/momentsapi/ErrorCode.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
66 changes: 66 additions & 0 deletions
66
visitorservice/src/main/java/com/tealium/visitorservice/momentsapi/HttpClient.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} | ||
} | ||
} | ||
} | ||
|
||
} |
86 changes: 86 additions & 0 deletions
86
visitorservice/src/main/java/com/tealium/visitorservice/momentsapi/MomentsApiManager.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} |
10 changes: 10 additions & 0 deletions
10
visitorservice/src/main/java/com/tealium/visitorservice/momentsapi/MomentsApiRegion.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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") | ||
} |
43 changes: 43 additions & 0 deletions
43
visitorservice/src/main/java/com/tealium/visitorservice/momentsapi/MomentsApiService.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
7 changes: 7 additions & 0 deletions
7
visitorservice/src/main/java/com/tealium/visitorservice/momentsapi/ResponseListener.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} |
13 changes: 13 additions & 0 deletions
13
...torservice/src/main/java/com/tealium/visitorservice/momentsapi/TealiumConfigMomentsApi.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |