Skip to content

Commit 67281ed

Browse files
authored
Profile API implementation (#15)
* Profile API implementation * Update demo apps to show use of profile API * Update README with Profile API docs
1 parent 8f70220 commit 67281ed

File tree

10 files changed

+169
-15
lines changed

10 files changed

+169
-15
lines changed

DemoApp/DemoAppJava/app/src/main/java/co/optable/demoappjava/ui/GAMBanner/GAMBannerFragment.java

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public View onCreateView(@NonNull LayoutInflater inflater,
5353

5454
targetingDataView.setText(msg.toString());
5555
mPublisherAdView.loadAd(adRequest.build());
56+
profile();
5657
witness();
5758
});
5859
});
@@ -77,6 +78,7 @@ public View onCreateView(@NonNull LayoutInflater inflater,
7778

7879
targetingDataView.setText(msg.toString());
7980
mPublisherAdView.loadAd(adRequest.build());
81+
profile();
8082
witness();
8183
});
8284

@@ -90,9 +92,33 @@ public View onCreateView(@NonNull LayoutInflater inflater,
9092
return root;
9193
}
9294

95+
private void profile() {
96+
HashMap<String,Object> traits = new HashMap<String,Object>();
97+
traits.put("gender", "F");
98+
traits.put("age", 38);
99+
traits.put("hasAccount", true);
100+
101+
MainActivity.OPTABLE
102+
.profile(traits)
103+
.observe(getViewLifecycleOwner(), result -> {
104+
final StringBuilder msg = new StringBuilder();
105+
msg.append(targetingDataView.getText().toString());
106+
107+
if (result.getStatus() == OptableSDK.Status.SUCCESS) {
108+
msg.append("\n\nSuccess calling profile API to set user traits.\n\n");
109+
} else {
110+
msg.append("\n\nOptableSDK Error: " + result.getMessage() + "\n\n");
111+
}
112+
113+
targetingDataView.setText(msg.toString());
114+
});
115+
}
116+
93117
private void witness() {
94-
HashMap<String, String> eventProperties = new HashMap<String, String>();
118+
HashMap<String,Object> eventProperties = new HashMap<String,Object>();
95119
eventProperties.put("exampleKey", "exampleValue");
120+
eventProperties.put("exampleKey2", 123);
121+
eventProperties.put("exampleKey3", false);
96122

97123
MainActivity.OPTABLE
98124
.witness("GAMBannerFragment.loadAdButtonClicked", eventProperties)

DemoApp/DemoAppJava/app/src/main/res/layout/fragment_gambanner.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@
5858

5959
<TextView
6060
android:id="@+id/targetingDataView"
61-
android:layout_width="282dp"
62-
android:layout_height="168dp"
61+
android:layout_width="291dp"
62+
android:layout_height="346dp"
6363
android:layout_marginTop="100dp"
6464
app:layout_constraintEnd_toEndOf="parent"
6565
app:layout_constraintStart_toStartOf="parent"

DemoApp/DemoAppJava/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ buildscript {
55
jcenter()
66
}
77
dependencies {
8-
classpath 'com.android.tools.build:gradle:4.1.0'
8+
classpath 'com.android.tools.build:gradle:4.1.1'
99

1010
// NOTE: Do not place your application dependencies here; they belong
1111
// in the individual module build.gradle files

DemoApp/DemoAppKotlin/app/src/main/java/co/optable/androidsdkdemo/ui/GAMBanner/GAMBannerFragment.kt

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ class GAMBannerFragment : Fragment() {
5252

5353
targetingDataView.setText(msg)
5454
mPublisherAdView.loadAd(adRequest.build())
55+
profile()
5556
witness()
5657
})
5758
}
@@ -75,6 +76,7 @@ class GAMBannerFragment : Fragment() {
7576

7677
targetingDataView.setText(msg)
7778
mPublisherAdView.loadAd(adRequest.build())
79+
profile()
7880
witness()
7981
}
8082

@@ -88,11 +90,27 @@ class GAMBannerFragment : Fragment() {
8890
return root
8991
}
9092

93+
private fun profile() {
94+
MainActivity.OPTABLE!!
95+
.profile(
96+
hashMapOf("gender" to "F", "age" to 38, "hasAccount" to true)
97+
)
98+
.observe(viewLifecycleOwner, Observer { result ->
99+
var msg = targetingDataView.text.toString()
100+
if (result.status == OptableSDK.Status.SUCCESS) {
101+
msg += "\n\nSuccess calling profile API to set traits on user.\n\n"
102+
} else {
103+
msg += "\n\nOptableSDK Error: ${result.message}\n\n"
104+
}
105+
targetingDataView.setText(msg)
106+
})
107+
}
108+
91109
private fun witness() {
92110
MainActivity.OPTABLE!!
93111
.witness(
94112
"GAMBannerFragment.loadAdButtonClicked",
95-
hashMapOf("exampleKey" to "exampleValue")
113+
hashMapOf("exampleKey" to "exampleValue", "anotherExample" to 123, "foo" to false)
96114
)
97115
.observe(viewLifecycleOwner, Observer { result ->
98116
var msg = targetingDataView.text.toString()

DemoApp/DemoAppKotlin/app/src/main/res/layout/fragment_gambanner.xml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@
5858

5959
<TextView
6060
android:id="@+id/targetingDataView"
61-
android:layout_width="282dp"
62-
android:layout_height="168dp"
61+
android:layout_width="281dp"
62+
android:layout_height="366dp"
6363
android:layout_marginTop="100dp"
6464
app:layout_constraintEnd_toEndOf="parent"
6565
app:layout_constraintStart_toStartOf="parent"

DemoApp/DemoAppKotlin/build.gradle

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ buildscript {
66
jcenter()
77
}
88
dependencies {
9-
classpath 'com.android.tools.build:gradle:4.1.0'
9+
classpath 'com.android.tools.build:gradle:4.1.1'
1010
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
1111

1212
// NOTE: Do not place your application dependencies here; they belong

README.md

Lines changed: 56 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Kotlin SDK for integrating with optable-sandbox from an Android application.
77
- [Installing](#installing)
88
- [Using](#using)
99
- [Identify API](#identify-api)
10+
- [Profile API](#profile-api)
1011
- [Targeting API](#targeting-api)
1112
- [Witness API](#witness-api)
1213
- [Integrating GAM360](#integrating-gam360)
@@ -170,6 +171,58 @@ Since the `sendGoogleAdIDBoolean` value provided to `identify()` is `true`, the
170171

171172
The frequency of invocation of `identify` is up to you, however for optimal identity resolution we recommended to call the `identify()` method on your SDK instance every time you authenticate a user, as well as periodically, such as for example once every 15 to 60 minutes while the application is being actively used and an internet connection is available.
172173

174+
### Profile API
175+
176+
To associate key value traits with the user's device, for eventual audience assembly, you can call the profile API as follows:
177+
178+
#### Kotlin
179+
180+
```kotlin
181+
import co.optable.android_sdk.OptableSDK
182+
import my.org.app.MainActivity
183+
import android.util.Log
184+
...
185+
MainActivity.OPTABLE!!
186+
.profile(hashMapOf("gender" to "F", "age" to 38, "hasAccount" to true))
187+
.observe(viewLifecycleOwner, Observer { result ->
188+
if (result.status == OptableSDK.Status.SUCCESS) {
189+
           Log.i("Profile API Success... ")
190+
} else {
191+
// result.status is OptableSDK.Status.ERROR
192+
// result.message is the error message
193+
Log.e("Profile API Error: ${result.message}")
194+
}
195+
})
196+
```
197+
198+
#### Java
199+
200+
```java
201+
import co.optable.android_sdk.OptableSDK;
202+
import co.optable.demoappjava.MainActivity;
203+
import android.util.Log;
204+
import java.util.HashMap;
205+
...
206+
HashMap<String,Object> traits = new HashMap<String,Object>();
207+
traits.put("gender", "F");
208+
traits.put("age", 38);
209+
traits.put("hasAccount", true);
210+
211+
MainActivity.OPTABLE
212+
.profile(traits)
213+
.observe(getViewLifecycleOwner(), result -> {
214+
if (result.getStatus() == OptableSDK.Status.SUCCESS) {
215+
Log.i(null, "Profile API Success... ");
216+
} else {
217+
// result.getStatus() is OptableSDK.Status.ERROR
218+
// result.getMessage() is the error message
219+
Log.e(null, "Profile API Error: " + result.getMessage());
220+
}
221+
});
222+
```
223+
224+
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<String,Any>` (or `HashMap<String,Object>` 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.
225+
173226
### Targeting API
174227

175228
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;
299352
import android.util.Log;
300353
import java.util.HashMap;
301354
...
302-
HashMap<String, String> eventProperties = new HashMap<String, String>();
355+
HashMap<String,Object> eventProperties = new HashMap<String,Object>();
303356
eventProperties.put("exampleKey", "exampleValue");
304357

305358
MainActivity.OPTABLE
@@ -315,7 +368,7 @@ MainActivity.OPTABLE
315368
});
316369
```
317370

318-
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<String,String>`, and should consist only of string key-value pairs.
371+
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<String,Any>` (or `HashMap<String,Object>` 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.
319372

320373
### Integrating GAM360
321374

@@ -432,7 +485,7 @@ override fun onCreate(savedInstanceState: Bundle?) {
432485
}
433486
```
434487

435-
#### Java
488+
#### Java
436489

437490
```java
438491
@Override

android_sdk/src/main/java/co/optable/android_sdk/OptableSDK.kt

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,23 @@ import java.security.MessageDigest
2525
*/
2626
typealias OptableIdentifyInput = List<String>
2727

28+
/*
29+
* Profile API expects user traits:
30+
*/
31+
typealias OptableProfileTraits = HashMap<String,Any>
32+
2833
/*
2934
* Witness API expects event properties:
3035
*/
31-
typealias OptableWitnessProperties = HashMap<String, String>
36+
typealias OptableWitnessProperties = HashMap<String,Any>
3237

3338
/*
34-
* Identify and Witness APIs usually just return {}... Void would be better but that results in
35-
* retrofit2 error when parsing response, even when the API responded successfully, since {} is
36-
* technically a HashMap:
39+
* Identify, Profile, and Witness APIs usually just return {}... Void would be better but that
40+
* results in retrofit2 error when parsing response, even when the API responded successfully,
41+
* since {} is technically a HashMap:
3742
*/
3843
typealias OptableIdentifyResponse = HashMap<Any,Any>
44+
typealias OptableProfileResponse = HashMap<Any,Any>
3945
typealias OptableWitnessResponse = HashMap<Any,Any>
4046

4147
/*
@@ -181,6 +187,44 @@ class OptableSDK @JvmOverloads constructor(context: Context, host: String, app:
181187
}
182188
}
183189

190+
/*
191+
* profile(traits) calls the Optable Sandbox "profile" API in order to associate the
192+
* specified keyvalue OptableProfileTraits 'traits', which can be subsequently used for
193+
* audience assembly.
194+
*
195+
* It is asynchronous, so the caller may call observe() on the returned LiveData and expect
196+
* an instance of Response<OptableProfileResponse> in the result. Success can be checked by
197+
* comparing result.status to OptableSDK.Status.SUCCESS. Note that result.data!! will point
198+
* to an empty HashMap on success, and can therefore be ignored.
199+
*/
200+
fun profile(traits: OptableProfileTraits):
201+
LiveData<Response<OptableProfileResponse>> {
202+
val liveData = MutableLiveData<Response<OptableProfileResponse>>()
203+
val client = this.client
204+
205+
GlobalScope.launch {
206+
val response = client.Profile(traits)
207+
when (response) {
208+
is EdgeResponse.Success -> {
209+
liveData.postValue(Response.success(response.body))
210+
}
211+
is EdgeResponse.ApiError -> {
212+
liveData.postValue(Response.error(response.body))
213+
}
214+
is EdgeResponse.NetworkError -> {
215+
liveData.postValue(Response.error(
216+
Response.Error("NetworkError", "None")))
217+
}
218+
is EdgeResponse.UnknownError -> {
219+
liveData.postValue(Response.error(
220+
Response.Error("UnknownError", "None")))
221+
}
222+
}
223+
}
224+
225+
return liveData
226+
}
227+
184228
/*
185229
* targeting() calls the Optable Sandbox "targeting" API, which returns the key-value targeting
186230
* data matching the user/device/app.

android_sdk/src/main/java/co/optable/android_sdk/core/Client.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,14 @@ class Client(private val config: Config, private val context: Context) {
8282
return edgeService!!.Identify(this.config.app, idList)
8383
}
8484

85+
suspend fun Profile(traits: OptableProfileTraits):
86+
EdgeResponse<OptableProfileResponse, OptableSDK.Response.Error>
87+
{
88+
val profileBody = HashMap<String,Any>()
89+
profileBody.put("traits", traits)
90+
return edgeService!!.Profile(this.config.app, profileBody)
91+
}
92+
8593
suspend fun Targeting():
8694
EdgeResponse<OptableTargetingResponse, OptableSDK.Response.Error>
8795
{

android_sdk/src/main/java/co/optable/android_sdk/edge/EdgeService.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,11 @@ interface EdgeService {
1616
suspend fun Identify(@Path("app") app: String, @Body idList: OptableIdentifyInput):
1717
EdgeResponse<OptableIdentifyResponse, OptableSDK.Response.Error>
1818

19+
@POST("/{app}/profile")
20+
suspend fun Profile(@Path("app") app: String,
21+
@Body profileBody: HashMap<String,Any>):
22+
EdgeResponse<OptableProfileResponse, OptableSDK.Response.Error>
23+
1924
@GET("/{app}/targeting")
2025
suspend fun Targeting(@Path("app") app: String):
2126
EdgeResponse<OptableTargetingResponse, OptableSDK.Response.Error>

0 commit comments

Comments
 (0)