diff --git a/dev-app/src/main/java/com/uid2/dev/MainActivity.kt b/dev-app/src/main/java/com/uid2/dev/MainActivity.kt index 8738e11..e12bee1 100644 --- a/dev-app/src/main/java/com/uid2/dev/MainActivity.kt +++ b/dev-app/src/main/java/com/uid2/dev/MainActivity.kt @@ -10,6 +10,7 @@ import com.uid2.dev.network.AppUID2Client import com.uid2.dev.ui.MainScreen import com.uid2.dev.ui.MainScreenViewModel import com.uid2.dev.ui.MainScreenViewModelFactory +import com.uid2.devapp.R class MainActivity : ComponentActivity() { @@ -22,6 +23,9 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + + //TODO: Configure for EUID/UID2, set title, pass env to viewModel +// setTitle(R.string.app_name_euid) setContent { MaterialTheme { MainScreen(viewModel) diff --git a/dev-app/src/main/res/values/strings.xml b/dev-app/src/main/res/values/strings.xml index 71d8e9b..688f94d 100644 --- a/dev-app/src/main/res/values/strings.xml +++ b/dev-app/src/main/res/values/strings.xml @@ -1,5 +1,6 @@ UID2 SDK Dev App + EUID SDK Dev App Email Phone Number diff --git a/sdk/src/main/java/com/uid2/EUIDManager.kt b/sdk/src/main/java/com/uid2/EUIDManager.kt new file mode 100644 index 0000000..6710cbf --- /dev/null +++ b/sdk/src/main/java/com/uid2/EUIDManager.kt @@ -0,0 +1,107 @@ +package com.uid2 + +import android.content.Context +import com.uid2.UID2Manager.Companion +import com.uid2.UID2Manager.Companion.APPLICATION_ID_DEFAULT +import com.uid2.UID2Manager.Companion.UID2_API_URL_DEFAULT +import com.uid2.network.DefaultNetworkSession +import com.uid2.network.NetworkSession +import com.uid2.storage.FileStorageManager +import com.uid2.storage.FileStorageManager.Store.EUID +import com.uid2.storage.StorageManager +import com.uid2.utils.InputUtils +import com.uid2.utils.Logger +import com.uid2.utils.TimeUtils +import kotlinx.coroutines.Dispatchers + +public class EUIDManager { + + public companion object { + public data class Environment private constructor( + val serverUrl: String + ) { + public companion object { + // AWS EU West 2 (London) + public val london: Environment = Environment("https://prod.euid.eu/v2") + + // Equivalent to `london` + public val production: Environment = london + public fun custom(serverUrl: String): Environment { + return Environment(serverUrl) + } + } + } + + private var serverUrl: String = UID2_API_URL_DEFAULT + private var applicationId: String = APPLICATION_ID_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], along with a [NetworkSession] that will be responsible + * for making any required network calls. + * + * @param context The context to initialise from. This will be used to obtain the package's metadata to extract + * the API URL. + * @param networkSession A custom [NetworkSession] which can be used for making any required network calls. + * The default implementation supported by the SDK can be found as [DefaultNetworkSession]. + */ + @JvmStatic + @JvmOverloads + @Throws(InitializationException::class) + public fun init( + context: Context, + environment: Environment = Environment.production, + networkSession: NetworkSession = DefaultNetworkSession(), + isLoggingEnabled: Boolean = false, + ) { + if (instance != null) { + throw InitializationException() + } + + this.serverUrl = environment.serverUrl + this.applicationId = context.packageName + this.networkSession = networkSession + this.storageManager = FileStorageManager(context.applicationContext, EUID) + this.isLoggingEnabled = isLoggingEnabled + } + + /** + * Returns True if the manager is already initialised, otherwise False. + */ + @JvmStatic + public fun isInitialized(): Boolean = instance != null + + /** + * Gets the current singleton instance of the manager. + * + * @throws InitializationException Thrown if the manager has not yet been initialised. + */ + @JvmStatic + public fun getInstance(): UID2Manager { + val storage = storageManager ?: throw InitializationException() + val logger = Logger(isLoggingEnabled) + + return instance ?: UID2Manager( + UID2Client( + apiUrl = serverUrl, + session = networkSession, + applicationId = applicationId, + logger = logger, + ), + storage, + TimeUtils, + InputUtils(), + Dispatchers.Default, + true, + logger, + ).apply { + instance = this + } + } + + } +} diff --git a/sdk/src/main/java/com/uid2/UID2Manager.kt b/sdk/src/main/java/com/uid2/UID2Manager.kt index c5b3ed3..4a776da 100644 --- a/sdk/src/main/java/com/uid2/UID2Manager.kt +++ b/sdk/src/main/java/com/uid2/UID2Manager.kt @@ -24,6 +24,8 @@ import com.uid2.data.IdentityStatus.REFRESH_EXPIRED import com.uid2.data.UID2Identity import com.uid2.network.DefaultNetworkSession import com.uid2.network.NetworkSession +import com.uid2.storage.FileStorageManager +import com.uid2.storage.FileStorageManager.Store.UID2 import com.uid2.storage.StorageManager import com.uid2.utils.InputUtils import com.uid2.utils.Logger @@ -533,12 +535,36 @@ public class UID2Manager internal constructor( } public companion object { + public data class Environment private constructor( + val serverUrl: String + ) { + public companion object { + // AWS US East (Ohio) + public val ohio: Environment = Environment("https://prod.uidapi.com") + // AWS US West (Oregon) + public val oregon: Environment = Environment("https://usw.prod.uidapi.com") + // AWS Asia Pacific (Singapore) + public val singapore: Environment = Environment("https://sg.prod.uidapi.com") + // AWS Asia Pacific (Sydney) + public val sydney: Environment = Environment("https://au.prod.uidapi.com") + // AWS Asia Pacific (Tokyo) + public val tokyo: Environment = Environment("https://jp.prod.uidapi.com") + + // Equivalent to `ohio` + public val production: Environment = ohio + + public fun custom(serverUrl: String): Environment { + return Environment(serverUrl) + } + } + } + private const val TAG = "UID2Manager" // The default API server. - private const val UID2_API_URL_DEFAULT = "https://prod.uidapi.com" + internal const val UID2_API_URL_DEFAULT = "https://prod.uidapi.com" - private const val APPLICATION_ID_DEFAULT = "unknown" + internal const val APPLICATION_ID_DEFAULT = "unknown" private const val PACKAGE_NOT_AVAILABLE = "Identity not available" private const val PACKAGE_AD_TOKEN_NOT_AVAILABLE = "advertising_token is not available or is not valid" @@ -577,20 +603,47 @@ public class UID2Manager internal constructor( @JvmStatic @JvmOverloads @Throws(InitializationException::class) + @Deprecated( + message = "Initialize with a custom Environment rather than a serverUrl String", + replaceWith = ReplaceWith("init(context, environment, networkSession, isLoggingEnabled)"), + level = DeprecationLevel.WARNING + ) public fun init( context: Context, serverUrl: String = UID2_API_URL_DEFAULT, networkSession: NetworkSession = DefaultNetworkSession(), isLoggingEnabled: Boolean = false, + ) { + init2(context, Environment.custom(serverUrl), networkSession, isLoggingEnabled) + } + + /** + * Initializes the class with the given [Context], along with a [NetworkSession] that will be responsible + * for making any required network calls. + * + * @param context The context to initialise from. This will be used to obtain the package's metadata to extract + * the API URL. + * @param networkSession A custom [NetworkSession] which can be used for making any required network calls. + * The default implementation supported by the SDK can be found as [DefaultNetworkSession]. + */ + @JvmStatic + @JvmOverloads + @JvmName("initWithEnvironment") + @Throws(InitializationException::class) + public fun init2( + context: Context, + environment: Environment = Environment.production, + networkSession: NetworkSession = DefaultNetworkSession(), + isLoggingEnabled: Boolean = false, ) { if (instance != null) { throw InitializationException() } - this.serverUrl = serverUrl + this.serverUrl = environment.serverUrl this.applicationId = context.packageName this.networkSession = networkSession - this.storageManager = StorageManager.getInstance(context.applicationContext) + this.storageManager = FileStorageManager(context.applicationContext, UID2) this.isLoggingEnabled = isLoggingEnabled } diff --git a/sdk/src/main/java/com/uid2/storage/FileStorageManager.kt b/sdk/src/main/java/com/uid2/storage/FileStorageManager.kt index bbd07e7..1a0edde 100644 --- a/sdk/src/main/java/com/uid2/storage/FileStorageManager.kt +++ b/sdk/src/main/java/com/uid2/storage/FileStorageManager.kt @@ -18,6 +18,11 @@ internal class FileStorageManager( private val ioDispatcher: CoroutineDispatcher = Dispatchers.IO, ) : StorageManager { + enum class Store(val filename: String) { + UID2(UID2_FILE_IDENTITY), + EUID(EUID_FILE_IDENTITY) + } + // For storage, we use the parent filesDir which is part of the Application's internal storage. This internal // storage is sandboxed to prevent any other app, or even the user, from accessing it directly. We rely on Android // keeping this file secure. @@ -25,7 +30,7 @@ internal class FileStorageManager( // On Android 10+, this location is also likely encrypted. // // https://developer.android.com/training/data-storage/app-specific#internal-access-files - constructor(context: Context) : this({ File(context.filesDir, FILE_IDENTITY) }) + constructor(context: Context, store: Store) : this({ File(context.filesDir, store.filename) }) // This lazy value *should* only be requested on the ioDispatcher. private val identityFile: File by lazy { identityFileFactory() } @@ -59,7 +64,8 @@ internal class FileStorageManager( } private companion object { - const val FILE_IDENTITY = "uid2_identity.json" + const val UID2_FILE_IDENTITY = "uid2_identity.json" + const val EUID_FILE_IDENTITY = "euid_identity.json" const val KEY_STATUS = "identity_status" // The character set used for both reading and writing to the file. diff --git a/sdk/src/main/java/com/uid2/storage/StorageManager.kt b/sdk/src/main/java/com/uid2/storage/StorageManager.kt index b3c30ff..a20fa32 100644 --- a/sdk/src/main/java/com/uid2/storage/StorageManager.kt +++ b/sdk/src/main/java/com/uid2/storage/StorageManager.kt @@ -22,15 +22,4 @@ internal interface StorageManager { * Clears any previously stored data. */ suspend fun clear(): Boolean - - companion object { - private var instance: StorageManager? = null - - /** - * Gets the single instance of the FileStorageManager. - */ - fun getInstance(context: Context) = instance ?: FileStorageManager(context).apply { - instance = this - } - } } diff --git a/securesignals-gma/src/main/java/com/uid2/securesignals/gma/EUIDMediationAdapter.kt b/securesignals-gma/src/main/java/com/uid2/securesignals/gma/EUIDMediationAdapter.kt new file mode 100644 index 0000000..78e1e0f --- /dev/null +++ b/securesignals-gma/src/main/java/com/uid2/securesignals/gma/EUIDMediationAdapter.kt @@ -0,0 +1,76 @@ +package com.uid2.securesignals.gma + +import android.content.Context +import com.google.android.gms.ads.AdError +import com.google.android.gms.ads.mediation.InitializationCompleteCallback +import com.google.android.gms.ads.mediation.MediationConfiguration +import com.google.android.gms.ads.mediation.rtb.RtbAdapter +import com.google.android.gms.ads.mediation.rtb.RtbSignalData +import com.google.android.gms.ads.mediation.rtb.SignalCallbacks +import com.uid2.EUIDManager +import com.uid2.UID2 +import com.google.android.gms.ads.mediation.VersionInfo as GmaVersionInfo + +/** + * An implementation of Google's GMS RtbAdapter that integrates UID2 tokens, accessed via the UID2Manager. + */ +public class EUIDMediationAdapter : RtbAdapter() { + + /** + * Gets the version of the UID2 SDK. + */ + @Suppress("DEPRECATION") + public override fun getSDKVersionInfo(): GmaVersionInfo = UID2.getVersionInfo().let { + GmaVersionInfo(it.major, it.minor, it.patch) + } + + /** + * Gets the version of the UID2 Secure Signals plugin. + */ + @Suppress("DEPRECATION") + public override fun getVersionInfo(): GmaVersionInfo = PluginVersion.getVersionInfo().let { + GmaVersionInfo(it.major, it.minor, it.patch) + } + + /** + * Initialises the UID2 SDK with the given Context. + */ + override fun initialize( + context: Context, + initializationCompleteCallback: InitializationCompleteCallback, + mediationConfigurations: MutableList, + ) { + // It's possible that the UID2Manager is already initialised. If so, it's a no-op. + if (!EUIDManager.isInitialized()) { + EUIDManager.init(context) + } + + // After we've asked to initialize the manager, we should wait until it's complete before reporting success. + // This will potentially allow any previously persisted identity to be fully restored before we allow any + // signals to be collected. + EUIDManager.getInstance().addOnInitializedListener(initializationCompleteCallback::onInitializationSucceeded) + } + + /** + * Collects the UID2 advertising token, if available. + */ + override fun collectSignals(rtbSignalData: RtbSignalData, signalCallbacks: SignalCallbacks) { + EUIDManager.getInstance().let { manager -> + val token = manager.getAdvertisingToken() + if (token != null) { + signalCallbacks.onSuccess(token) + } else { + // We include the IdentityStatus in the "error" to have better visibility on why the Advertising Token + // was not present. There are a number of valid reasons why we don't have a token, but we are still + // required to report these as "failures". + signalCallbacks.onFailure( + AdError( + manager.currentIdentityStatus.value, + "No Advertising Token", + "UID2", + ), + ) + } + } + } +}