diff --git a/.idea/gradle.xml b/.idea/gradle.xml index ab01ab2..882ddbc 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -15,6 +15,7 @@ diff --git a/.idea/misc.xml b/.idea/misc.xml index 7bfef59..d5d35ec 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,6 @@ - + diff --git a/README.md b/README.md index f9b15a7..b5495d5 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,10 @@ Kotlin SDK for integrating with optable-sandbox from an Android application. - [Targeting API](#targeting-api) - [Witness API](#witness-api) - [Integrating GAM360](#integrating-gam360) +- [Identifying visitors arriving from Email newsletters](#identifying-visitors-arriving-from-email-newsletters) + - [Insert oeid into your Email newsletter template](#insert-oeid-into-your-email-newsletter-template) + - [Capture clicks on deep links in your application](#capture-clicks-on-deep-links-in-your-application) + - [Call tryIdentifyFromURI SDK API](#call-tryidentifyfromuri-sdk-api) - [Demo Applications](#demo-applications) - [Building](#building) @@ -51,7 +55,7 @@ Remember to replace `VERSION_TAG` with the latest or desired [SDK release](https To configure an instance of the SDK integrating with an [Optable](https://optable.co/) sandbox running at hostname `sandbox.customer.com`, from a configured application origin identified by slug `my-app`, you can instantiate the SDK from an Activity or Application `Context`, such as for example the following application `MainActivity`: -Kotlin: +#### Kotlin ```kotlin import co.optable.android_sdk.OptableSDK @@ -72,7 +76,7 @@ class MainActivity : AppCompatActivity() { } ``` -Java: +#### Java ```java import co.optable.android_sdk.OptableSDK; @@ -107,7 +111,7 @@ However, since production sandboxes only listen to TLS traffic, the above is rea To associate a user device with an authenticated identifier such as an Email address, or with other known IDs such as the Google Advertising ID, or even your own vendor or app level `PPID`, you can call the `identify` API as follows: -Kotlin: +#### Kotlin ```kotlin import co.optable.android_sdk.OptableSDK @@ -133,7 +137,7 @@ MainActivity.OPTABLE!! }) ``` -Java: +#### Java ```java import co.optable.android_sdk.OptableSDK; @@ -170,7 +174,7 @@ The frequency of invocation of `identify` is up to you, however for optimal iden To get the targeting key values associated by the configured sandbox with the device in real-time, you can call the `targeting` API as follows: -Kotlin: +#### Kotlin ```kotlin import co.optable.android_sdk.OptableSDK @@ -195,7 +199,7 @@ MainActivity.OPTABLE!! }) ``` -Java: +#### Java ```java import co.optable.android_sdk.OptableSDK; @@ -225,7 +229,7 @@ On success, the resulting key values are typically sent as part of a subsequent To send real-time event data from the user's device to the sandbox for eventual audience assembly, you can call the witness API as follows: -Kotlin: +#### Kotlin ```kotlin import co.optable.android_sdk.OptableSDK @@ -245,7 +249,7 @@ MainActivity.OPTABLE!! }) ``` -Java: +#### Java ```java import co.optable.android_sdk.OptableSDK; @@ -275,7 +279,7 @@ The specified event type and properties are associated with the logged event and We can further extend the above `targeting` example to show an integration with a [Google Ad Manager 360](https://admanager.google.com/home/) ad server account: -Kotlin: +#### Kotlin ```kotlin import co.optable.android_sdk.OptableSDK @@ -311,7 +315,7 @@ MainActivity.OPTABLE!! }) ``` -Java: +#### Java ```java import co.optable.android_sdk.OptableSDK; @@ -347,6 +351,62 @@ MainActivity.OPTABLE.targeting().observe(getViewLifecycleOwner(), result -> { Working examples are available in the Kotlin and Java SDK demo applications. +## Identifying visitors arriving from Email newsletters + +If you send Email newsletters that contain links to your application (e.g., deep links), then you may want to automatically _identify_ visitors that have clicked on any such links via their Email address. Incoming application traffic which is originating from a subscriber click on a link in a newsletter is considered to be implicitly authenticated by the recipient of the Email, therefore serving as an excellent source of linking of online user identities. + +### Insert oeid into your Email newsletter template + +To enable automatic identification of visitors originating from your Email newsletter, you first need to include an **oeid** parameter in the query string of all links to your website in your Email newsletter template. The value of the **oeid** parameter should be set to the SHA256 hash of the lowercased Email address of the recipient. For example, if you are using [Braze](https://www.braze.com/) to send your newsletters, you can easily encode the SHA256 hash value of the recipient's Email address by setting the **oeid** parameter in the query string of any links to your application as follows: + +``` +oeid={{${email_address} | downcase | sha2}} +``` + +The above example uses various personalization tags as documented in [Braze's user guide](https://www.braze.com/docs/user_guide/personalization_and_dynamic_content/) to dynamically insert the required data into an **oeid** parameter, all of which should make up a _part_ of the destination URL in your template. + +### Capture clicks on deep links in your application + +In order for your application to open on devices where it is installed when a link to your domain is clicked, you need to [configure and prepare your application to handle deep links](https://developer.android.com/training/app-links/deep-linking) first. + +### Call tryIdentifyFromURI SDK API + +When Android launches your app after a user clicks on a link, it will start your app activity with your configured _intent filters_. You can then obtain the `Uri` of the link by calling `getData()`, and pass it to the SDK's `tryIdentifyFromURI()` API which will automatically look for `oeid` in the query parameters of the `Uri` and call `identify` with its value if found. + +For example, you can call `getData()` on the incoming `Intent` from your `onCreate()` activity lifecycle callback as follows: + +#### Kotlin + +```kotlin +override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.main) + ... + val data: Uri? = intent?.data + if (data != null) { + MainActivity.OPTABLE!!.tryIdentifyFromURI(data) + } + ... +} +``` + + #### Java + +```java +@Override +public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main); + ... + Intent intent = getIntent(); + Uri data = intent.getData(); + if (data != null) { + MainActivity.OPTABLE.tryIdentifyFromURI(data); + } + ... +} +``` + ## Demo Applications The Kotlin and Java demo applications show a working example of `identify`, `targeting`, and `witness` APIs, as well as an integration with the [Google Ad Manager 360](https://admanager.google.com/home/) ad server, enabling the targeting of ads served by GAM360 to audiences activated in the [Optable](https://optable.co/) sandbox. diff --git a/android_sdk/build.gradle b/android_sdk/build.gradle index 09b55fc..69b6e41 100644 --- a/android_sdk/build.gradle +++ b/android_sdk/build.gradle @@ -5,6 +5,13 @@ plugins { apply plugin: 'com.android.library' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' +apply plugin: 'de.mobilej.unmock' + +unMock { + keep "android.net.Uri" + keepStartingWith "libcore." + keepAndRename "java.nio.charset.Charsets" to "xjava.nio.charset.Charsets" +} android { compileSdkVersion 30 diff --git a/android_sdk/src/main/java/co/optable/android_sdk/OptableSDK.kt b/android_sdk/src/main/java/co/optable/android_sdk/OptableSDK.kt index e306f1a..18f0681 100644 --- a/android_sdk/src/main/java/co/optable/android_sdk/OptableSDK.kt +++ b/android_sdk/src/main/java/co/optable/android_sdk/OptableSDK.kt @@ -5,6 +5,7 @@ package co.optable.android_sdk import android.content.Context +import android.net.Uri import android.text.TextUtils import androidx.lifecycle.LiveData import androidx.lifecycle.MutableLiveData @@ -163,6 +164,23 @@ class OptableSDK @JvmOverloads constructor(context: Context, host: String, app: return this.identify(idList) } + /* + * tryIdentifyFromURI(uri) is a helper that attempts to find a valid-looking "oeid" + * parameter in the specified uri's query string parameters and, if found, calls + * this.identify(listOf(oeid)). + * + * The use for this is when handling incoming app universal/deep links which might + * contain an "oeid" value with the SHA256(downcase(email)) of an incoming user, such + * as encoded links in newsletter Emails sent by the application developer. + */ + fun tryIdentifyFromURI(uri: Uri) { + val oeid = Companion.eidFromURI(uri) + + if (oeid.length > 0) { + this.identify(listOf(oeid)) + } + } + /* * targeting() calls the Optable Sandbox "targeting" API, which returns the key-value targeting * data matching the user/device/app. @@ -260,5 +278,29 @@ class OptableSDK @JvmOverloads constructor(context: Context, host: String, app: fun cid(ppid: String): String { return "c:" + ppid.trim() } + + /* + * eidFromURI(uri) is a helper that returns a type-prefixed ID based on the query string + * oeid=sha256value parameters in the specified uri, if one is found. Otherwise, it returns + * an empty string. + * + * The use for this is when handling incoming deep links which might contain an "oeid" value + * with the SHA256(downcase(email)) of a user, such as encoded links in newsletter Emails + * sent by the application developer. Such hashed Email values can be used in calls to + * identify() + */ + fun eidFromURI(uri: Uri): String { + // We first convert the Uri to a lowercase string then re-parse it so that we are + // not dependent on case-sensitivity of the "oeid" query parameter: + var oeid = Uri.parse(uri.toString().toLowerCase()).getQueryParameter("oeid") + + if ((oeid == null) || (oeid.length != 64) || + (oeid.matches("^[a-f0-9]$".toRegex(RegexOption.IGNORE_CASE)))) + { + return "" + } + + return "e:" + oeid.toLowerCase() + } } } \ No newline at end of file diff --git a/android_sdk/src/test/java/co/optable/android_sdk/OptableSDKUnitTest.kt b/android_sdk/src/test/java/co/optable/android_sdk/OptableSDKUnitTest.kt index 1362655..851ac1e 100644 --- a/android_sdk/src/test/java/co/optable/android_sdk/OptableSDKUnitTest.kt +++ b/android_sdk/src/test/java/co/optable/android_sdk/OptableSDKUnitTest.kt @@ -4,6 +4,7 @@ */ package co.optable.android_sdk +import android.net.Uri import org.junit.Test import org.junit.Assert.* @@ -61,4 +62,36 @@ class OptableSDKUnitTest { assertNotEquals(unexpected, OptableSDK.cid("foobarBAZ-01234#98765.!!!")) } + + @Test + fun eidFromURI_isCorrect() { + val url = "http://some.domain.com/some/path?some=query&something=else&oeid=a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3&foo=bar&baz" + val expected = "e:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3" + + assertEquals(expected, OptableSDK.eidFromURI(Uri.parse(url))) + } + + @Test + fun eidFromURI_returnsEmptyWhenOeidAbsent() { + val url = "http://some.domain.com/some/path?some=query&something=else" + val expected = "" + + assertEquals(expected, OptableSDK.eidFromURI(Uri.parse(url))) + } + + @Test + fun eidFromURI_expectsSHA256() { + val url = "http://some.domain.com/some/path?some=query&something=else&oeid=AAAAAAAa665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3&foo=bar&baz" + val expected = "" + + assertEquals(expected, OptableSDK.eidFromURI(Uri.parse(url))) + } + + @Test + fun eidFromURI_ignoresCase() { + val url = "http://some.domain.com/some/path?some=query&something=else&oEId=A665A45920422F9D417E4867EFDC4FB8A04A1F3FFF1FA07E998E86f7f7A27AE3&foo=bar&baz" + val expected = "e:a665a45920422f9d417e4867efdc4fb8a04a1f3fff1fa07e998e86f7f7a27ae3" + + assertEquals(expected, OptableSDK.eidFromURI(Uri.parse(url))) + } } \ No newline at end of file diff --git a/build.gradle b/build.gradle index 33d6ece..e6d8617 100644 --- a/build.gradle +++ b/build.gradle @@ -8,6 +8,7 @@ buildscript { dependencies { classpath "com.android.tools.build:gradle:4.0.1" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "de.mobilej.unmock:UnMockPlugin:0.7.6" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files