From 67281ed85c84346c31927083c69becf68078dfff Mon Sep 17 00:00:00 2001 From: Bosko Milekic Date: Tue, 24 Nov 2020 10:00:04 -0500 Subject: [PATCH] Profile API implementation (#15) * Profile API implementation * Update demo apps to show use of profile API * Update README with Profile API docs --- .../ui/GAMBanner/GAMBannerFragment.java | 28 ++++++++- .../main/res/layout/fragment_gambanner.xml | 4 +- DemoApp/DemoAppJava/build.gradle | 2 +- .../ui/GAMBanner/GAMBannerFragment.kt | 20 ++++++- .../main/res/layout/fragment_gambanner.xml | 4 +- DemoApp/DemoAppKotlin/build.gradle | 2 +- README.md | 59 ++++++++++++++++++- .../java/co/optable/android_sdk/OptableSDK.kt | 52 ++++++++++++++-- .../co/optable/android_sdk/core/Client.kt | 8 +++ .../optable/android_sdk/edge/EdgeService.kt | 5 ++ 10 files changed, 169 insertions(+), 15 deletions(-) diff --git a/DemoApp/DemoAppJava/app/src/main/java/co/optable/demoappjava/ui/GAMBanner/GAMBannerFragment.java b/DemoApp/DemoAppJava/app/src/main/java/co/optable/demoappjava/ui/GAMBanner/GAMBannerFragment.java index 46520d1..79065a4 100644 --- a/DemoApp/DemoAppJava/app/src/main/java/co/optable/demoappjava/ui/GAMBanner/GAMBannerFragment.java +++ b/DemoApp/DemoAppJava/app/src/main/java/co/optable/demoappjava/ui/GAMBanner/GAMBannerFragment.java @@ -53,6 +53,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, targetingDataView.setText(msg.toString()); mPublisherAdView.loadAd(adRequest.build()); + profile(); witness(); }); }); @@ -77,6 +78,7 @@ public View onCreateView(@NonNull LayoutInflater inflater, targetingDataView.setText(msg.toString()); mPublisherAdView.loadAd(adRequest.build()); + profile(); witness(); }); @@ -90,9 +92,33 @@ public View onCreateView(@NonNull LayoutInflater inflater, return root; } + private void profile() { + HashMap traits = new HashMap(); + traits.put("gender", "F"); + traits.put("age", 38); + traits.put("hasAccount", true); + + MainActivity.OPTABLE + .profile(traits) + .observe(getViewLifecycleOwner(), result -> { + final StringBuilder msg = new StringBuilder(); + msg.append(targetingDataView.getText().toString()); + + if (result.getStatus() == OptableSDK.Status.SUCCESS) { + msg.append("\n\nSuccess calling profile API to set user traits.\n\n"); + } else { + msg.append("\n\nOptableSDK Error: " + result.getMessage() + "\n\n"); + } + + targetingDataView.setText(msg.toString()); + }); + } + private void witness() { - HashMap eventProperties = new HashMap(); + HashMap eventProperties = new HashMap(); eventProperties.put("exampleKey", "exampleValue"); + eventProperties.put("exampleKey2", 123); + eventProperties.put("exampleKey3", false); MainActivity.OPTABLE .witness("GAMBannerFragment.loadAdButtonClicked", eventProperties) diff --git a/DemoApp/DemoAppJava/app/src/main/res/layout/fragment_gambanner.xml b/DemoApp/DemoAppJava/app/src/main/res/layout/fragment_gambanner.xml index 93b9687..b382477 100644 --- a/DemoApp/DemoAppJava/app/src/main/res/layout/fragment_gambanner.xml +++ b/DemoApp/DemoAppJava/app/src/main/res/layout/fragment_gambanner.xml @@ -58,8 +58,8 @@ + var msg = targetingDataView.text.toString() + if (result.status == OptableSDK.Status.SUCCESS) { + msg += "\n\nSuccess calling profile API to set traits on user.\n\n" + } else { + msg += "\n\nOptableSDK Error: ${result.message}\n\n" + } + targetingDataView.setText(msg) + }) + } + private fun witness() { MainActivity.OPTABLE!! .witness( "GAMBannerFragment.loadAdButtonClicked", - hashMapOf("exampleKey" to "exampleValue") + hashMapOf("exampleKey" to "exampleValue", "anotherExample" to 123, "foo" to false) ) .observe(viewLifecycleOwner, Observer { result -> var msg = targetingDataView.text.toString() diff --git a/DemoApp/DemoAppKotlin/app/src/main/res/layout/fragment_gambanner.xml b/DemoApp/DemoAppKotlin/app/src/main/res/layout/fragment_gambanner.xml index 93b9687..03cafea 100644 --- a/DemoApp/DemoAppKotlin/app/src/main/res/layout/fragment_gambanner.xml +++ b/DemoApp/DemoAppKotlin/app/src/main/res/layout/fragment_gambanner.xml @@ -58,8 +58,8 @@ + if (result.status == OptableSDK.Status.SUCCESS) { +            Log.i("Profile API Success... ") + } else { + // result.status is OptableSDK.Status.ERROR + // result.message is the error message + Log.e("Profile API Error: ${result.message}") + } + }) +``` + +#### Java + +```java +import co.optable.android_sdk.OptableSDK; +import co.optable.demoappjava.MainActivity; +import android.util.Log; +import java.util.HashMap; +... +HashMap traits = new HashMap(); +traits.put("gender", "F"); +traits.put("age", 38); +traits.put("hasAccount", true); + +MainActivity.OPTABLE + .profile(traits) + .observe(getViewLifecycleOwner(), result -> { + if (result.getStatus() == OptableSDK.Status.SUCCESS) { + Log.i(null, "Profile API Success... "); + } else { + // result.getStatus() is OptableSDK.Status.ERROR + // result.getMessage() is the error message + Log.e(null, "Profile API Error: " + result.getMessage()); + } + }); +``` + +The specified traits are associated with the user's device and can be used for matching during audience assembly. The traits are of type `OptableProfileTraits` which is an alias for `HashMap` (or `HashMap` in Java), and should consist only of key-value pairs where the key is a string and the value is either a string, number, or boolean. + ### Targeting API 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: @@ -299,7 +352,7 @@ import co.optable.demoappjava.MainActivity; import android.util.Log; import java.util.HashMap; ... -HashMap eventProperties = new HashMap(); +HashMap eventProperties = new HashMap(); eventProperties.put("exampleKey", "exampleValue"); MainActivity.OPTABLE @@ -315,7 +368,7 @@ MainActivity.OPTABLE }); ``` -The specified event type and properties are associated with the logged event and which can be used for matching during audience assembly. The optional witness event properties are of type `OptableWitnessProperties` which is an alias for `HashMap`, and should consist only of string key-value pairs. +The specified event type and properties are associated with the logged event and which can be used for matching during audience assembly. The optional witness event properties are of type `OptableWitnessProperties` which is an alias for `HashMap` (or `HashMap` in Java), and should consist only of key-value pairs where the key is a string and the value is either a string, number, or boolean. ### Integrating GAM360 @@ -432,7 +485,7 @@ override fun onCreate(savedInstanceState: Bundle?) { } ``` - #### Java +#### Java ```java @Override 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 22b01ac..a71bb30 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 @@ -25,17 +25,23 @@ import java.security.MessageDigest */ typealias OptableIdentifyInput = List +/* + * Profile API expects user traits: + */ +typealias OptableProfileTraits = HashMap + /* * Witness API expects event properties: */ -typealias OptableWitnessProperties = HashMap +typealias OptableWitnessProperties = HashMap /* - * Identify and Witness APIs usually just return {}... Void would be better but that results in - * retrofit2 error when parsing response, even when the API responded successfully, since {} is - * technically a HashMap: + * Identify, Profile, and Witness APIs usually just return {}... Void would be better but that + * results in retrofit2 error when parsing response, even when the API responded successfully, + * since {} is technically a HashMap: */ typealias OptableIdentifyResponse = HashMap +typealias OptableProfileResponse = HashMap typealias OptableWitnessResponse = HashMap /* @@ -181,6 +187,44 @@ class OptableSDK @JvmOverloads constructor(context: Context, host: String, app: } } + /* + * profile(traits) calls the Optable Sandbox "profile" API in order to associate the + * specified keyvalue OptableProfileTraits 'traits', which can be subsequently used for + * audience assembly. + * + * It is asynchronous, so the caller may call observe() on the returned LiveData and expect + * an instance of Response in the result. Success can be checked by + * comparing result.status to OptableSDK.Status.SUCCESS. Note that result.data!! will point + * to an empty HashMap on success, and can therefore be ignored. + */ + fun profile(traits: OptableProfileTraits): + LiveData> { + val liveData = MutableLiveData>() + val client = this.client + + GlobalScope.launch { + val response = client.Profile(traits) + when (response) { + is EdgeResponse.Success -> { + liveData.postValue(Response.success(response.body)) + } + is EdgeResponse.ApiError -> { + liveData.postValue(Response.error(response.body)) + } + is EdgeResponse.NetworkError -> { + liveData.postValue(Response.error( + Response.Error("NetworkError", "None"))) + } + is EdgeResponse.UnknownError -> { + liveData.postValue(Response.error( + Response.Error("UnknownError", "None"))) + } + } + } + + return liveData + } + /* * targeting() calls the Optable Sandbox "targeting" API, which returns the key-value targeting * data matching the user/device/app. diff --git a/android_sdk/src/main/java/co/optable/android_sdk/core/Client.kt b/android_sdk/src/main/java/co/optable/android_sdk/core/Client.kt index 4fb732c..65f38dc 100644 --- a/android_sdk/src/main/java/co/optable/android_sdk/core/Client.kt +++ b/android_sdk/src/main/java/co/optable/android_sdk/core/Client.kt @@ -82,6 +82,14 @@ class Client(private val config: Config, private val context: Context) { return edgeService!!.Identify(this.config.app, idList) } + suspend fun Profile(traits: OptableProfileTraits): + EdgeResponse + { + val profileBody = HashMap() + profileBody.put("traits", traits) + return edgeService!!.Profile(this.config.app, profileBody) + } + suspend fun Targeting(): EdgeResponse { diff --git a/android_sdk/src/main/java/co/optable/android_sdk/edge/EdgeService.kt b/android_sdk/src/main/java/co/optable/android_sdk/edge/EdgeService.kt index 5fb56d4..0106f5f 100644 --- a/android_sdk/src/main/java/co/optable/android_sdk/edge/EdgeService.kt +++ b/android_sdk/src/main/java/co/optable/android_sdk/edge/EdgeService.kt @@ -16,6 +16,11 @@ interface EdgeService { suspend fun Identify(@Path("app") app: String, @Body idList: OptableIdentifyInput): EdgeResponse + @POST("/{app}/profile") + suspend fun Profile(@Path("app") app: String, + @Body profileBody: HashMap): + EdgeResponse + @GET("/{app}/targeting") suspend fun Targeting(@Path("app") app: String): EdgeResponse