Skip to content

Commit

Permalink
Implement logging support
Browse files Browse the repository at this point in the history
  • Loading branch information
Ian Bird authored and IanDBird committed Feb 26, 2024
1 parent ac1fae2 commit c8a1133
Show file tree
Hide file tree
Showing 6 changed files with 112 additions and 18 deletions.
4 changes: 2 additions & 2 deletions dev-app/src/main/java/com/uid2/dev/DevApplication.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ class DevApplication : Application() {

// Initialise the UID2Manager class. We will use it's DefaultNetworkSession rather than providing our own
// custom implementation. This can be done to allow wrapping something like OkHttp.
UID2Manager.init(this.applicationContext)
UID2Manager.init(context = this, isLoggingEnabled = true)

// Alternatively, we could initialise the UID2Manager with our own custom NetworkSession...
// UID2Manager.init(this.applicationContext, OkNetworkSession())
// UID2Manager.init(this.applicationContext, OkNetworkSession(), true)

// For the development app, we will enable a strict thread policy to ensure we have suitable visibility of any
// issues within the SDK.
Expand Down
23 changes: 19 additions & 4 deletions sdk/src/main/java/com/uid2/UID2Client.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.uid2.network.NetworkRequestType
import com.uid2.network.NetworkSession
import com.uid2.network.RefreshPackage
import com.uid2.network.RefreshResponse
import com.uid2.utils.Logger
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
Expand All @@ -21,6 +22,7 @@ import java.net.URL
internal class UID2Client(
private val apiUrl: String,
private val session: NetworkSession,
private val logger: Logger = Logger(),
private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO,
) {
// The refresh endpoint is built from the given API root, along with our known refresh path appended. If the
Expand Down Expand Up @@ -48,8 +50,13 @@ internal class UID2Client(
refreshToken: String,
refreshResponseKey: String,
): RefreshPackage = withContext(ioDispatcher) {
logger.i(TAG) { "Refreshing identity" }

// Check to make sure we have a valid endpoint to hit.
val url = apiRefreshUrl ?: throw InvalidApiUrlException()
val url = apiRefreshUrl ?: run {
logger.e(TAG) { "Error determining identity refresh API" }
throw InvalidApiUrlException()
}

// Build the request to refresh the token.
val request = NetworkRequest(
Expand All @@ -64,19 +71,27 @@ internal class UID2Client(
// Attempt to make the request via the provided NetworkSession.
val response = session.loadData(url, request)
if (response.code != HttpURLConnection.HTTP_OK) {
logger.e(TAG) { "Client details failure: ${response.code}" }
throw RefreshTokenException(response.code)
}

// The response should be an encrypted payload. Let's attempt to decrypt it using the key we were provided.
val payload = DataEnvelope.decrypt(refreshResponseKey, response.data, true)
?: throw PayloadDecryptException()
val payload = DataEnvelope.decrypt(refreshResponseKey, response.data, true) ?: run {
logger.e(TAG) { "Error decrypting response from client details" }
throw PayloadDecryptException()
}

// The decrypted payload should be JSON which we can parse.
val refreshResponse = RefreshResponse.fromJson(JSONObject(String(payload, Charsets.UTF_8)))
return@withContext refreshResponse?.toRefreshPackage() ?: throw InvalidPayloadException()
return@withContext refreshResponse?.toRefreshPackage() ?: run {
logger.e(TAG) { "Error parsing response from client details" }
throw InvalidPayloadException()
}
}

private companion object {
const val TAG = "UID2Client"

// The relative path of the API's refresh endpoint
const val API_REFRESH_PATH = "/v2/token/refresh"
}
Expand Down
51 changes: 41 additions & 10 deletions sdk/src/main/java/com/uid2/UID2Manager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import com.uid2.extensions.getMetadata
import com.uid2.network.DefaultNetworkSession
import com.uid2.network.NetworkSession
import com.uid2.storage.StorageManager
import com.uid2.utils.Logger
import com.uid2.utils.TimeUtils
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
Expand Down Expand Up @@ -83,6 +84,7 @@ public class UID2Manager internal constructor(
private val timeUtils: TimeUtils,
defaultDispatcher: CoroutineDispatcher,
initialAutomaticRefreshEnabled: Boolean,
private val logger: Logger,
) {
private val scope = CoroutineScope(defaultDispatcher + SupervisorJob())

Expand Down Expand Up @@ -156,6 +158,10 @@ public class UID2Manager internal constructor(
initialized = scope.launch {
// Attempt to load the Identity from storage. If successful, we can notify any observers.
storageManager.loadIdentity().let {
if (it.first != null) {
logger.i(TAG) { "Restoring previously persisted identity" }
}

validateAndSetIdentity(it.first, it.second, false)
}
}
Expand All @@ -168,6 +174,7 @@ public class UID2Manager internal constructor(
* This will also be persisted locally, so that when the application re-launches, we reload this Identity.
*/
public fun setIdentity(identity: UID2Identity): Unit = afterInitialized {
logger.i(TAG) { "Setting external identity" }
validateAndSetIdentity(identity, null)
}

Expand All @@ -177,6 +184,7 @@ public class UID2Manager internal constructor(
public fun resetIdentity(): Unit = afterInitialized {
currentIdentity ?: return@afterInitialized

logger.i(TAG) { "Resetting identity" }
setIdentityInternal(null, NO_IDENTITY, true)
}

Expand All @@ -185,7 +193,10 @@ public class UID2Manager internal constructor(
*/
public fun refreshIdentity(): Unit = afterInitialized {
// If we have a valid Identity, let's refresh it.
currentIdentity?.let { refreshIdentityInternal(it) }
currentIdentity?.let {
logger.i(TAG) { "Refreshing identity" }
refreshIdentityInternal(it)
}
}

/**
Expand All @@ -207,6 +218,8 @@ public class UID2Manager internal constructor(
private fun refreshIdentityInternal(identity: UID2Identity) = scope.launch {
try {
refreshToken(identity).retryWhen { _, attempt ->
logger.i(TAG) { "Refreshing (Attempt: $attempt)" }

// The delay between retry attempts is based upon how many attempts we have previously had. After a
// number of sequential failures, we will increase the delay.
val delayMs = if (attempt < REFRESH_TOKEN_FAILURE_RETRY_THRESHOLD) {
Expand All @@ -221,10 +234,12 @@ public class UID2Manager internal constructor(
getIdentityPackage(identity, false).valid
}.single().let {
result ->
logger.i(TAG) { "Successfully refreshed identity" }
validateAndSetIdentity(result.identity, result.status)
}
} catch (_: UID2Exception) {
} catch (ex: UID2Exception) {
// This will happen after we decide to no longer try to update the identity, e.g. it's no longer valid.
logger.e(TAG, ex) { "Error when trying to refresh identity" }
}
}

Expand Down Expand Up @@ -313,6 +328,8 @@ public class UID2Manager internal constructor(
checkRefreshExpiresJob = scope.launch {
val timeToCheck = timeUtils.diffToNow(it.refreshExpires) + EXPIRATION_CHECK_TOLERANCE_MS
delay(timeToCheck)

logger.i(TAG) { "Detected refresh has expired" }
validateAndSetIdentity(it, null, true)
}
}
Expand All @@ -323,6 +340,8 @@ public class UID2Manager internal constructor(
checkIdentityExpiresJob = scope.launch {
val timeToCheck = timeUtils.diffToNow(it.identityExpires) + EXPIRATION_CHECK_TOLERANCE_MS
delay(timeToCheck)

logger.i(TAG) { "Detected identity has expired" }
validateAndSetIdentity(it, null, true)
}
}
Expand All @@ -336,12 +355,18 @@ public class UID2Manager internal constructor(
) {
// Process Opt Out.
if (status == OPT_OUT) {
logger.i(TAG) { "User opt-out detected" }
setIdentityInternal(null, OPT_OUT)
return
}

// Check to see the validity of the Identity, updating our internal state.
val validity = getIdentityPackage(identity, currentIdentity == null)

logger.i(TAG) {
"Updating identity (Identity: ${validity.identity != null}, Status: ${validity.status}, " +
"Updating Storage: $updateStorage)"
}
setIdentityInternal(validity.identity, validity.status, updateStorage)
}

Expand Down Expand Up @@ -396,6 +421,8 @@ public class UID2Manager internal constructor(
}

public companion object {
private const val TAG = "UID2Manager"

// The default API server.
private const val UID2_API_URL_KEY = "uid2_api_url"
private const val UID2_API_URL_DEFAULT = "https://prod.uidapi.com"
Expand All @@ -420,15 +447,10 @@ public class UID2Manager internal constructor(
private var api: String = UID2_API_URL_DEFAULT
private var networkSession: NetworkSession = DefaultNetworkSession()
private var storageManager: StorageManager? = null
private var isLoggingEnabled: Boolean = false

private var instance: UID2Manager? = null

/**
* Initializes the class with the given [Context].
*/
@JvmStatic
public fun init(context: Context): Unit = init(context, DefaultNetworkSession())

/**
* Initializes the class with the given [Context], along with a [NetworkSession] that will be responsible
* for making any required network calls.
Expand All @@ -439,8 +461,13 @@ public class UID2Manager internal constructor(
* The default implementation supported by the SDK can be found as [DefaultNetworkSession].
*/
@JvmStatic
@JvmOverloads
@Throws(InitializationException::class)
public fun init(context: Context, networkSession: NetworkSession) {
public fun init(
context: Context,
networkSession: NetworkSession = DefaultNetworkSession(),
isLoggingEnabled: Boolean = false,
) {
if (instance != null) {
throw InitializationException()
}
Expand All @@ -449,7 +476,8 @@ public class UID2Manager internal constructor(

this.api = metadata?.getString(UID2_API_URL_KEY, UID2_API_URL_DEFAULT) ?: UID2_API_URL_DEFAULT
this.networkSession = networkSession
this.storageManager = StorageManager.getInstance(context)
this.storageManager = StorageManager.getInstance(context.applicationContext)
this.isLoggingEnabled = isLoggingEnabled
}

/**
Expand All @@ -466,16 +494,19 @@ public class UID2Manager internal constructor(
@JvmStatic
public fun getInstance(): UID2Manager {
val storage = storageManager ?: throw InitializationException()
val logger = Logger(isLoggingEnabled)

return instance ?: UID2Manager(
UID2Client(
api,
networkSession,
logger,
),
storage,
TimeUtils(),
Dispatchers.Default,
true,
logger,
).apply {
instance = this
}
Expand Down
30 changes: 30 additions & 0 deletions sdk/src/main/java/com/uid2/utils/Logger.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package com.uid2.utils

import android.util.Log

/**
* Simple logger class that wraps Android's [Log].
*/
internal class Logger(private val isEnabled: Boolean = false) {
fun v(tag: String, throwable: Throwable? = null, message: () -> String = { "" }) {
if (isEnabled) {
Log.v(tag, message(), throwable)
}
}

fun d(tag: String, throwable: Throwable? = null, message: () -> String = { "" }) {
if (isEnabled) {
Log.d(tag, message(), throwable)
}
}

fun i(tag: String, throwable: Throwable? = null, message: () -> String = { "" }) {
if (isEnabled) {
Log.i(tag, message(), throwable)
}
}

fun e(tag: String, throwable: Throwable? = null, message: () -> String = { "" }) {
Log.e(tag, message(), throwable)
}
}
9 changes: 9 additions & 0 deletions sdk/src/test/java/com/uid2/UID2ClientTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import com.uid2.data.UID2Identity
import com.uid2.network.NetworkRequest
import com.uid2.network.NetworkResponse
import com.uid2.network.NetworkSession
import com.uid2.utils.Logger
import kotlinx.coroutines.runBlocking
import org.json.JSONObject
import org.junit.Assert.assertEquals
Expand All @@ -23,6 +24,7 @@ import org.mockito.kotlin.whenever
@RunWith(MockitoJUnitRunner::class)
class UID2ClientTest {
private val networkSession: NetworkSession = mock()
private val logger: Logger = mock()

private val url = "https://test.dev"
private val refreshToken = "RefreshToken"
Expand All @@ -33,6 +35,7 @@ class UID2ClientTest {
val client = UID2Client(
"this is not a url",
networkSession,
logger,
)

// Verify that when we have configured the client with an invalid URL, that it throws the appropriate exception
Expand All @@ -47,6 +50,7 @@ class UID2ClientTest {
val client = UID2Client(
url,
networkSession,
logger,
)

// Configure the network session to report a failure.
Expand All @@ -63,6 +67,7 @@ class UID2ClientTest {
val client = UID2Client(
url,
networkSession,
logger,
)

whenever(networkSession.loadData(any(), any())).thenReturn(
Expand All @@ -80,6 +85,7 @@ class UID2ClientTest {
val client = UID2Client(
url,
networkSession,
logger,
)

whenever(networkSession.loadData(any(), any())).thenReturn(
Expand All @@ -97,6 +103,7 @@ class UID2ClientTest {
val client = UID2Client(
url,
networkSession,
logger,
)

// Configure the network session to return a valid (encrypted) payload.
Expand All @@ -120,6 +127,7 @@ class UID2ClientTest {
val client = UID2Client(
url,
networkSession,
logger,
)

// Configure the network session to return a valid (encrypted) payload.
Expand All @@ -138,6 +146,7 @@ class UID2ClientTest {
val client = UID2Client(
url,
networkSession,
logger,
)

// Configure the network session to return a valid (encrypted) payload and allows us to capture the given
Expand Down
Loading

0 comments on commit c8a1133

Please sign in to comment.