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