diff --git a/dev-app/src/main/java/com/uid2/dev/DevApplication.kt b/dev-app/src/main/java/com/uid2/dev/DevApplication.kt index 13710b8..fa42248 100644 --- a/dev-app/src/main/java/com/uid2/dev/DevApplication.kt +++ b/dev-app/src/main/java/com/uid2/dev/DevApplication.kt @@ -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. diff --git a/sdk/src/main/java/com/uid2/UID2Client.kt b/sdk/src/main/java/com/uid2/UID2Client.kt index 5b65fa3..4b626d7 100644 --- a/sdk/src/main/java/com/uid2/UID2Client.kt +++ b/sdk/src/main/java/com/uid2/UID2Client.kt @@ -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 @@ -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 @@ -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( @@ -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" } diff --git a/sdk/src/main/java/com/uid2/UID2Manager.kt b/sdk/src/main/java/com/uid2/UID2Manager.kt index ae1ec52..af9f115 100644 --- a/sdk/src/main/java/com/uid2/UID2Manager.kt +++ b/sdk/src/main/java/com/uid2/UID2Manager.kt @@ -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 @@ -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()) @@ -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) } } @@ -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) } @@ -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) } @@ -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) + } } /** @@ -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) { @@ -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" } } } @@ -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) } } @@ -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) } } @@ -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) } @@ -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" @@ -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. @@ -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() } @@ -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 } /** @@ -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 } diff --git a/sdk/src/main/java/com/uid2/utils/Logger.kt b/sdk/src/main/java/com/uid2/utils/Logger.kt new file mode 100644 index 0000000..5966658 --- /dev/null +++ b/sdk/src/main/java/com/uid2/utils/Logger.kt @@ -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) + } +} diff --git a/sdk/src/test/java/com/uid2/UID2ClientTest.kt b/sdk/src/test/java/com/uid2/UID2ClientTest.kt index 0d9a617..43ab5fa 100644 --- a/sdk/src/test/java/com/uid2/UID2ClientTest.kt +++ b/sdk/src/test/java/com/uid2/UID2ClientTest.kt @@ -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 @@ -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" @@ -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 @@ -47,6 +50,7 @@ class UID2ClientTest { val client = UID2Client( url, networkSession, + logger, ) // Configure the network session to report a failure. @@ -63,6 +67,7 @@ class UID2ClientTest { val client = UID2Client( url, networkSession, + logger, ) whenever(networkSession.loadData(any(), any())).thenReturn( @@ -80,6 +85,7 @@ class UID2ClientTest { val client = UID2Client( url, networkSession, + logger, ) whenever(networkSession.loadData(any(), any())).thenReturn( @@ -97,6 +103,7 @@ class UID2ClientTest { val client = UID2Client( url, networkSession, + logger, ) // Configure the network session to return a valid (encrypted) payload. @@ -120,6 +127,7 @@ class UID2ClientTest { val client = UID2Client( url, networkSession, + logger, ) // Configure the network session to return a valid (encrypted) payload. @@ -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 diff --git a/sdk/src/test/java/com/uid2/UID2ManagerTest.kt b/sdk/src/test/java/com/uid2/UID2ManagerTest.kt index 4c10997..9cb7b1a 100644 --- a/sdk/src/test/java/com/uid2/UID2ManagerTest.kt +++ b/sdk/src/test/java/com/uid2/UID2ManagerTest.kt @@ -10,6 +10,7 @@ import com.uid2.data.IdentityStatus.REFRESH_EXPIRED import com.uid2.data.UID2Identity import com.uid2.network.RefreshPackage import com.uid2.storage.StorageManager +import com.uid2.utils.Logger import com.uid2.utils.TimeUtils import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -50,6 +51,7 @@ class UID2ManagerTest { private val client: UID2Client = mock() private val storageManager: StorageManager = mock() private val timeUtils: TimeUtils = mock() + private val logger: Logger = mock() private lateinit var manager: UID2Manager private val initialIdentity = withRandomIdentity() @@ -108,7 +110,7 @@ class UID2ManagerTest { @Test fun `resets identity immediately after initialisation`() = runTest(testDispatcher) { // Create a new instance of the manager but *don't* allow it to finish initialising (loading previous identity) - val manager = UID2Manager(client, storageManager, timeUtils, testDispatcher, false).apply { + val manager = UID2Manager(client, storageManager, timeUtils, testDispatcher, false, logger).apply { onIdentityChangedListener = listener checkExpiration = false } @@ -428,7 +430,14 @@ class UID2ManagerTest { listener: UID2ManagerIdentityChangedListener?, initialCheckExpiration: Boolean = false, ): UID2Manager { - return UID2Manager(client, storageManager, timeUtils, dispatcher, initialAutomaticRefreshEnabled).apply { + return UID2Manager( + client, + storageManager, + timeUtils, + dispatcher, + initialAutomaticRefreshEnabled, + logger, + ).apply { onIdentityChangedListener = listener checkExpiration = initialCheckExpiration