Android v1.0.0.3
Specifications
| Spec | Value |
|---|---|
| Min SDK | 23 (Android 6.0) |
| Language | Kotlin |
| Coroutines | Required |
| Serialization | kotlinx.serialization |
| DI | Koin (isolated context) |
Installation
1. Add the SDK to your project
Get the library file behaviors.zip, extract and put all files into a repo folder in your Android project root:
your-project/
repo/
com/
trustingsocial/
behaviors/
1.0.0.3/
behaviors-1.0.0.3.aar
behaviors-1.0.0.3.pom
behaviors-1.0.0.3.module
2. Configure repositories
Add the local Maven repository in your project-level settings.gradle.kts:
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven {
url = uri("${rootProject.projectDir}/repo")
}
}
}
3. Add dependencies
In your app-level build.gradle.kts:
dependencies {
implementation("com.trustingsocial:behaviors:1.0.0.3")
}
4. ProGuard / R8
The SDK ships consumer ProGuard rules automatically. No additional configuration needed.
Configuration
import com.ts.behaviorsdk.external.TSBehavioralConfig
import com.ts.behaviorsdk.external.KeyboardConfig
import com.ts.behaviorsdk.external.TouchConfig
import com.ts.behaviorsdk.devicemetadata.MetaDataCollectionConfig
import com.ts.behaviorsdk.sensor.SensorConfig
import com.ts.behaviorsdk.data.SensorType
val config = TSBehavioralConfig(
appId = "your-app-id", // Your app's package name / application ID
baseUrl = "https://your-base-url/api", // Provided by TrustVision
metadataConfig = MetaDataCollectionConfig(
intervalMillis = 2000L,
enabled = true,
),
uploadBlockSize = 10,
sensorConfig = SensorConfig(
samplingIntervalMs = 20L,
sensorTypes = listOf(
SensorType.Gyroscope,
SensorType.Magnetometer,
SensorType.Accelerometer,
SensorType.Light,
),
enabled = true,
),
keyboardConfig = KeyboardConfig(
trackRawText = false,
enabled = true,
),
touchConfig = TouchConfig(
enabled = true,
),
screenTransitionBlockSize = 1,
appStateBlockSize = 1,
batchExtra = mapOf("env" to "production"),
retryConfig = RetryConfig(
enabled = true,
maxRetries = 2,
retryDelayMs = 1000L,
maxDelayMs = 5000L,
maxRetryAttempts = 3,
maxRetryStorageItems = 50,
finalFlushTimeoutMs = 10000L,
retryItemDelayMs = 500L,
),
)
TSBehavioralConfig
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
appId | String | Yes | — | Your app's package name (application ID) |
baseUrl | String | Yes | — | Base URL for TrustVision API |
metadataConfig | MetaDataCollectionConfig | Yes | — | Device metadata collection settings |
uploadBlockSize | Int | No | 10 | Number of batch entries before auto-upload |
sensorConfig | SensorConfig | No | SensorConfig() | Sensor data collection settings |
keyboardConfig | KeyboardConfig | No | KeyboardConfig() | Keyboard tracking settings |
touchConfig | TouchConfig | No | TouchConfig() | Touch tracking settings |
screenTransitionBlockSize | Int | No | 1 | Screen transition batch size |
appStateBlockSize | Int | No | 1 | Number of app-state events per upload batch |
batchExtra | Map<String, String> | No | emptyMap() | Custom key-value metadata included in every batch upload |
retryConfig | RetryConfig | No | RetryConfig() | Upload retry queue settings |
sslCertificates | List<Int>? | No | null | Raw certificate resource IDs to pin (e.g. R.raw.cert). See SSL Pinning |
SSL Pinning
Optional certificate pinning. Pass raw certificate resource IDs via sslCertificates; when the list is non-empty the SDK trusts only servers presenting one of these certificates on every API call. null or an empty list disables pinning and falls back to the system trust store.
val config = TSBehavioralConfig(
appId = "com.example.app",
baseUrl = "https://api.trustingsocial.com",
metadataConfig = MetaDataCollectionConfig(intervalMillis = 2000),
sslCertificates = listOf(R.raw.my_server_cert),
)
MetaDataCollectionConfig
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
intervalMillis | Long | Yes | — | Metadata collection interval in milliseconds |
enabled | Boolean | No | true | Enable/disable metadata collection |
typeModes | Map<MetaDataType, MetaDataCollectionMode> | No | DeviceDescriptions→MANUAL, DeviceBehaviors→INTERVAL, UserBehaviors→INTERVAL | Collection mode per metadata type. INTERVAL = repeating timer; MANUAL = explicit calls only |
collectOnceTypes | Set<MetaDataType> | No | {DeviceDescriptions} | Types collected once at session start and stop (fire-and-forget boundary snapshots) |
SensorConfig
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
samplingIntervalMs | Long | No | 20L (50Hz) | Sensor sampling interval in milliseconds. Must be > 0 |
sensorTypes | List<SensorType> | No | All 4 types | Sensors to collect: Gyroscope, Magnetometer, Accelerometer, Light |
enabled | Boolean | No | true | Enable/disable sensor collection |
KeyboardConfig
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
trackRawText | Boolean | No | false | Whether to collect raw typed text. When false, only character type is recorded |
enabled | Boolean | No | true | Enable/disable keyboard tracking |
TouchConfig
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
enabled | Boolean | No | true | Enable/disable touch tracking |
RetryConfig
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
enabled | Boolean | No | true | Enable/disable the retry queue |
maxRetries | Int | No | 2 | Max immediate retries per request (0–10). Total calls = value + 1 |
retryDelayMs | Long | No | 1000 | Base delay before first retry in milliseconds (> 0) |
maxDelayMs | Long | No | 5000 | Maximum delay cap in milliseconds (≥ retryDelayMs) |
maxRetryAttempts | Int | No | 3 | Maximum retry-queue attempts before dropping a failed batch (≥ 1) |
maxRetryStorageItems | Int | No | 50 | Maximum items in retry storage before FIFO eviction (≥ 1) |
finalFlushTimeoutMs | Long | No | 10000 | Timeout in milliseconds for flush at session end (≥ 1) |
retryItemDelayMs | Long | No | 500 | Delay between retry uploads in milliseconds (≥ 0) |
SDK Functions
TSBehavioralSDK is a Kotlin object (singleton). All control-flow methods are suspend functions and must be called from a coroutine scope. Touch, screen transition, and keyboard data methods are non-suspend and can be called from anywhere.
initialize
Initializes the SDK. Must be called first before any other method. Generates device identity (RSA key pair stored in Android Keystore) and returns device information needed for registration.
Calling initialize() again will finalize any existing session and reinitialize.
Signature:
suspend fun initialize(context: Context, config: TSBehavioralConfig): TSDeviceInitInfo
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
context | Context | Yes | Android context (Application or Activity) |
config | TSBehavioralConfig | Yes | SDK configuration |
Returns: TSDeviceInitInfo
| Field | Type | Description |
|---|---|---|
deviceId | String | Unique device identifier (UUID) |
bootstrapKeyId | String | Hardware key ID for encryption |
isRegistered | Boolean | Whether device was previously registered |
encryptedDevicePublicKeyPayload | String | AES-GCM sealed device public key (base64). Pass to device_public_key in register API |
encryptedSecretKey | String | RSA-OAEP encrypted AES secret key (base64). Pass to encrypted_secret_key in register API |
Throws: TSSessionError.KeystoreError, TSSessionError.CryptoError
Example:
lifecycleScope.launch {
try {
val initInfo = TSBehavioralSDK.initialize(applicationContext, config)
Log.d("BehavioralSDK", "Device ID: ${initInfo.deviceId}")
Log.d("BehavioralSDK", "Already registered: ${initInfo.isRegistered}")
} catch (e: TSSessionError.KeystoreError) {
Log.e("BehavioralSDK", "Keystore error: ${e.message}")
} catch (e: TSSessionError.CryptoError) {
Log.e("BehavioralSDK", "Crypto error: ${e.message}")
}
}
markDeviceRegistered
Marks the device as registered after your backend has completed server-side registration with TrustVision. Must be called after initialize() and before startCollect().
Registration status is persisted locally. On subsequent app launches, initInfo.isRegistered will return true and you can skip the registration step.
Signature:
suspend fun markDeviceRegistered(isRegistered: Boolean, customerUserDeviceId: String)
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
isRegistered | Boolean | Yes | Registration status (true to mark as registered) |
customerUserDeviceId | String | Yes | Unique user-device identifier returned by your server |
Throws: TSSessionError.NotInitialized
Example:
lifecycleScope.launch {
val initInfo = TSBehavioralSDK.initialize(applicationContext, config)
if (!initInfo.isRegistered) {
// Your backend calls TrustVision API: POST /v1/register_device
// with: device_id, key_pair_id, device_public_key, encrypted_secret_key, cus_user_id, protocol
val userDeviceId = yourBackend.registerDevice(
deviceId = initInfo.deviceId,
keyPairId = initInfo.bootstrapKeyId,
devicePublicKey = initInfo.encryptedDevicePublicKeyPayload,
encryptedSecretKey = initInfo.encryptedSecretKey,
cusUserId = "your-user-id",
)
TSBehavioralSDK.markDeviceRegistered(
isRegistered = true,
customerUserDeviceId = userDeviceId,
)
}
}
startCollect
Creates a challenge with the TrustVision server and starts all data collectors (sensors, metadata, touch, keyboard). Requires device to be registered.
If a previous session is still active, it will be auto-finalized before starting a new one.
Signature:
suspend fun startCollect(context: Context? = null)
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
context | Context? | No | Optional context override. Uses the context from initialize() if null |
Throws: TSSessionError.NotInitialized, TSSessionError.DeviceNotRegistered, TSSessionError.NetworkError, TSSessionError.InvalidChallengeResponse, TSSessionError.ManagerStartError
Example:
lifecycleScope.launch {
try {
TSBehavioralSDK.startCollect(this@MainActivity)
Log.d("BehavioralSDK", "Collection started")
} catch (e: TSSessionError.DeviceNotRegistered) {
Log.e("BehavioralSDK", "Register device first")
} catch (e: TSSessionError.NetworkError) {
Log.e("BehavioralSDK", "Network error: ${e.httpCode} ${e.message}")
}
}
updateScreenTransition (Required)
Important: You must call
updateScreenTransition()immediately afterstartCollect()and whenever the user navigates to a new screen. This is required for behavioral data to be correctly associated with screen context.
Records a screen transition event. Non-suspend. Silently no-ops if SDK is not initialized or session is not started.
Signatures:
fun updateScreenTransition(value: String)
fun updateScreenTransition(value: ScreenTransitionCollectorListener)
Parameters (String overload):
| Parameter | Type | Required | Description |
|---|---|---|---|
value | String | Yes | Name of the screen being navigated to |
Parameters (Listener overload):
| Parameter | Type | Required | Description |
|---|---|---|---|
value | ScreenTransitionCollectorListener | Yes | Object implementing getScreenName(): String |
Example — using String:
// Must call immediately after startCollect()
TSBehavioralSDK.startCollect(context)
TSBehavioralSDK.updateScreenTransition("main_screen")
// Then call again on every screen navigation
TSBehavioralSDK.updateScreenTransition("payment_screen")
Example — using ScreenTransitionCollectorListener:
class PaymentActivity : AppCompatActivity(), ScreenTransitionCollectorListener {
override fun getScreenName() = "payment_screen"
override fun onResume() {
super.onResume()
TSBehavioralSDK.updateScreenTransition(this)
}
}
startKeyboardTracking
Attaches TextWatchers to all EditText fields in the given Activity, View hierarchy, or a specific EditText. Automatically tracks typing patterns (character type, timing, text length).
Silently no-ops if startCollect() has not been called yet.
Signatures:
suspend fun startKeyboardTracking(activity: Activity)
suspend fun startKeyboardTracking(view: View)
suspend fun startKeyboardTracking(editText: EditText)
Uses android.app.Activity, android.view.View, and android.widget.EditText.
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
activity | Activity | Yes | Activity containing EditText fields to track |
view | View | Yes | View hierarchy containing EditText fields. Use fragment.requireView() for Fragments |
editText | EditText | Yes | Specific EditText field to track |
Throws: TSSessionError.NotInitialized
Example:
// In your Activity's onResume — tracks all EditTexts
override fun onResume() {
super.onResume()
lifecycleScope.launch {
TSBehavioralSDK.startKeyboardTracking(this@MyActivity)
}
}
// In a Fragment — tracks all EditTexts in the fragment's view
lifecycleScope.launch {
TSBehavioralSDK.startKeyboardTracking(fragment.requireView())
}
// Track a specific EditText
lifecycleScope.launch {
TSBehavioralSDK.startKeyboardTracking(emailEditText)
}
stopKeyboardTracking
Unregisters all keyboard tracking listeners. Auto-called by finalizeCollect(), so you only need to call this explicitly if you want to stop keyboard tracking before finalizing.
Signatures:
suspend fun stopKeyboardTracking()
suspend fun stopKeyboardTracking(editText: EditText)
Parameters (EditText overload):
| Parameter | Type | Required | Description |
|---|---|---|---|
editText | EditText | Yes | Specific EditText field to stop tracking |
Throws: TSSessionError.NotInitialized
Example:
lifecycleScope.launch {
TSBehavioralSDK.stopKeyboardTracking()
}
addKeyboardTrackingData
Sends keyboard tracking data manually. Use this for custom input handling where automatic tracking via startKeyboardTracking() is not suitable.
Non-suspend. Silently no-ops if SDK is not initialized or session is not started.
Signature:
fun addKeyboardTrackingData(data: KeyboardData)
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
data | KeyboardData | Yes | Keyboard event data |
KeyboardData fields:
| Field | Type | Required | Default | Description |
|---|---|---|---|---|
timestamp | Long | Yes | — | Event timestamp in milliseconds |
data | String? | No | null | Character value (only sent if trackRawText is true) |
textLength | Int? | No | null | Current text field length |
type | KeyboardDataType | No | Unknown | Type of keystroke |
viewId | String? | No | null | Identifier for the input field |
KeyboardDataType values: LowerText, UpperText, Number, Spacing, SpecialCharacters, Delete, CursorMoving, Mixed, Unknown
Example:
TSBehavioralSDK.addKeyboardTrackingData(
KeyboardData(
timestamp = System.currentTimeMillis(),
type = KeyboardDataType.LowerText,
textLength = 5,
viewId = "email_field",
)
)
trackTextChange
Stateless, framework-agnostic keyboard tracking entry point for Jetpack Compose TextField where TextWatcher/EditText is unavailable. Pass old and new text on every onValueChange — the SDK computes the string diff internally and classifies the keystroke type.
Non-suspend. Silently no-ops if SDK is not initialized or session is not started.
Signature:
fun trackTextChange(oldText: String, newText: String, viewId: String)
Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
oldText | String | Yes | Text value before the change |
newText | String | Yes | Text value after the change |
viewId | String | Yes | Stable identifier for the input field |
Example — Jetpack Compose:
var email by remember { mutableStateOf("") }
TextField(
value = email,
onValueChange = { newValue ->
TSBehavioralSDK.trackTextChange(
oldText = email,
newText = newValue,
viewId = "email_field",
)
email = newValue
},
label = { Text("Email") }
)
trackTouchEvent
Records a touch event. Non-suspend. Uses WeakReference<Activity> to prevent memory leaks. Silently no-ops if SDK is not initialized or session is not started.
Signatures:
fun trackTouchEvent(event: MotionEvent, activity: WeakReference<Activity>)
fun trackTouchEvent(event: PointerEventDto, activity: WeakReference<Activity>)
Parameters (MotionEvent overload):
| Parameter | Type | Required | Description |
|---|---|---|---|
event | MotionEvent | Yes | Android motion event from dispatchTouchEvent |
activity | WeakReference<Activity> | Yes | Weak reference to the current Activity |
Parameters (PointerEventDto overload):
| Parameter | Type | Required | Description |
|---|---|---|---|
event | PointerEventDto | Yes | Pre-converted pointer event data |
activity | WeakReference<Activity> | Yes | Weak reference to the current Activity |
PointerEventDto fields:
| Field | Type | Description |
|---|---|---|
actionName | String? | Action name: ACTION_DOWN, ACTION_POINTER_DOWN, ACTION_MOVE, ACTION_UP, ACTION_POINTER_UP |
actionMasked | Int? | MotionEvent action constant |
pointerId | Int? | Pointer identifier |
globalX | Double? | Screen X coordinate |
globalY | Double? | Screen Y coordinate |
Example — using MotionEvent (recommended):
class MyActivity : AppCompatActivity() {
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
TSBehavioralSDK.trackTouchEvent(event, WeakReference(this))
return super.dispatchTouchEvent(event)
}
}
Example — using PointerEventDto:
TSBehavioralSDK.trackTouchEvent(
PointerEventDto(
actionName = "ACTION_DOWN",
actionMasked = MotionEvent.ACTION_DOWN,
pointerId = 0,
globalX = 150.0,
globalY = 300.0,
),
WeakReference(this)
)
finalizeCollect
Stops all collectors, flushes remaining data, and waits for the upload queue to drain completely. Auto-stops keyboard tracking.
Pass the returned challengeId to your backend to trigger behavior score computation via POST /v1/behavior/score.
Signature:
suspend fun finalizeCollect(): TSFinalizeResult
Returns: TSFinalizeResult
| Field | Type | Description |
|---|---|---|
nonce | String? | Session nonce from challenge |
challengeId | String? | Challenge ID — pass to backend for scoring |
lastSeq | Int | Last uploaded batch sequence number |
batchId | String? | Last batch identifier |
droppedBatchCount | Int | Number of event batches dropped without reaching the server (data loss). Defaults to 0; 0 on a clean finalize |
lastUploadError | String? | Description of the last upload error that caused a drop, or null when none |
Observability (v1.0.0.3):
droppedBatchCountandlastUploadErrorlet you detect silent data loss after finalize. A non-zerodroppedBatchCountmeans some collected batches never reached the server; inspectlastUploadErrorfor the cause.
Throws: TSSessionError.NotInitialized
Example:
lifecycleScope.launch {
try {
val result = TSBehavioralSDK.finalizeCollect()
Log.d("BehavioralSDK", "Challenge ID: ${result.challengeId}")
Log.d("BehavioralSDK", "Last seq: ${result.lastSeq}")
if (result.droppedBatchCount > 0) {
Log.w(
"BehavioralSDK",
"Dropped ${result.droppedBatchCount} batches. Last error: ${result.lastUploadError}",
)
}
// Send challengeId to your backend for scoring
yourBackend.computeBehaviorScore(result.challengeId!!)
} catch (e: TSSessionError) {
Log.e("BehavioralSDK", "Finalize error: ${e.name} - ${e.message}")
}
}
stopCollect
Stops all collectors immediately without flushing data or draining the upload queue. This is the fast shutdown — use when you need to stop quickly (e.g., app backgrounding). May result in partial data loss.
Signature:
suspend fun stopCollect(): TSFinalizeResult
Returns: TSFinalizeResult (same structure as finalizeCollect, including the droppedBatchCount and lastUploadError observability fields). Because stopCollect() skips the flush and upload-queue drain, a non-zero droppedBatchCount is expected here.
Throws: TSSessionError.NotInitialized
finalizeCollect() | stopCollect() | |
|---|---|---|
| Async | suspend | suspend |
| Flush data | Yes | No |
| Drain upload queue | Yes (waits) | No |
| Data loss risk | None | Possible |
| Use case | Normal completion | App backgrounding |
Example:
lifecycleScope.launch {
try {
val result = TSBehavioralSDK.stopCollect()
Log.d("BehavioralSDK", "Stopped. Last seq: ${result.lastSeq}")
} catch (e: TSSessionError) {
Log.e("BehavioralSDK", "Stop error: ${e.message}")
}
}
clearDeviceIdentity
Clears all stored device identity (device ID, registration status, key pair) from DataStore and Android Keystore. You must call initialize() again after this to generate a new device identity.
Use when the user logs out or you need to reset the SDK completely.
Signature:
suspend fun clearDeviceIdentity()
Throws: TSSessionError.NotInitialized, TSSessionError.KeystoreError
Example:
lifecycleScope.launch {
try {
TSBehavioralSDK.clearDeviceIdentity()
Log.d("BehavioralSDK", "Device identity cleared")
// Must call initialize() again before using SDK
} catch (e: TSSessionError) {
Log.e("BehavioralSDK", "Clear error: ${e.message}")
}
}
Error Handling
All errors are subtypes of TSSessionError (sealed class extending Exception). Each error has a name property for machine-readable identification.
| Error | name | Condition |
|---|---|---|
NotInitialized | notInitialized | SDK not initialized. Call initialize() first |
DeviceNotRegistered | deviceNotRegistered | Server-authoritative: the server reports the device is not registered. Call markDeviceRegistered() after registering with your backend |
SslPinningError(cause) | sslPinningError | TLS/SSL pinning validation failed — the server certificate did not match a pinned certificate |
ChallengeExpired | challengeExpired | Server challenge expired. Retry startCollect() |
InvalidChallengeResponse | invalidChallengeResponse | Empty or invalid challenge from server |
NotStarted | notStarted | No active collection session. Call startCollect() first |
KeystoreError(cause) | keychainError | Android Keystore operation failed |
CryptoError(cause) | cryptoError | Encryption or signing operation failed |
NetworkError | networkError | HTTP request failed. Exposes httpCode: Int? and serverErrorCode: String? (the server's errors[].code). Message is formatted "HTTP <code>: <serverMessage>" |
SerializationError(cause) | serializationError | Failed to encode/decode a request or response payload |
UnknownError(cause) | unknownError | An unclassified error — fallback for exceptions that do not map to any known category |
ManagerStartError(cause) | managerStartError | Failed to start data collection managers |
NetworkError surfaces the server's semantic error code so you can branch on it. httpCode is the HTTP status (or null for a connectivity failure), and serverErrorCode mirrors the errors[].code value returned by the API (see the API reference).
try {
TSBehavioralSDK.startCollect(context)
} catch (e: TSSessionError.DeviceNotRegistered) {
// Server says the device is not registered — register via your backend first
} catch (e: TSSessionError.SslPinningError) {
// Certificate pinning failed — the server cert did not match a pinned certificate
} catch (e: TSSessionError.NetworkError) {
// Network issue: check e.httpCode, e.serverErrorCode, and e.message
} catch (e: TSSessionError) {
// Any other SDK error: use e.name and e.message
}
Complete Example
A full Activity integrating all SDK functions: initialization, device registration, data collection, touch tracking, keyboard tracking, screen transitions, and finalization.
import android.os.Bundle
import android.view.MotionEvent
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.ts.behaviorsdk.TSBehavioralSDK
import com.ts.behaviorsdk.external.TSBehavioralConfig
import com.ts.behaviorsdk.external.TSSessionError
import com.ts.behaviorsdk.devicemetadata.MetaDataCollectionConfig
import com.ts.behaviorsdk.sensor.SensorConfig
import com.ts.behaviorsdk.external.KeyboardConfig
import com.ts.behaviorsdk.external.TouchConfig
import com.ts.behaviorsdk.data.SensorType
import kotlinx.coroutines.launch
import java.lang.ref.WeakReference
class BehavioralActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_behavioral)
lifecycleScope.launch {
try {
// 1. Configure SDK
val config = TSBehavioralConfig(
appId = "your-app-id",
baseUrl = "https://your-base-url/api",
metadataConfig = MetaDataCollectionConfig(
intervalMillis = 2000L,
),
sensorConfig = SensorConfig(
samplingIntervalMs = 20L,
sensorTypes = listOf(
SensorType.Gyroscope,
SensorType.Magnetometer,
SensorType.Accelerometer,
SensorType.Light,
),
),
keyboardConfig = KeyboardConfig(
trackRawText = false,
),
touchConfig = TouchConfig(
enabled = true,
),
)
// 2. Initialize SDK
val initInfo = TSBehavioralSDK.initialize(
this@BehavioralActivity, config
)
// 3. Register device (skip if already registered)
if (!initInfo.isRegistered) {
// Your backend calls:
// POST /v1/register_device
// Body: {
// "device_id": initInfo.deviceId,
// "key_pair_id": initInfo.bootstrapKeyId,
// "device_public_key": initInfo.encryptedDevicePublicKeyPayload,
// "encrypted_secret_key": initInfo.encryptedSecretKey,
// "cus_user_id": "your-user-id",
// "protocol": "encrypted"
// }
// Response: { "data": { "customer_user_device_id": "..." } }
val userDeviceId = yourBackend.registerDevice(
deviceId = initInfo.deviceId,
keyPairId = initInfo.bootstrapKeyId,
devicePublicKey = initInfo.encryptedDevicePublicKeyPayload,
encryptedSecretKey = initInfo.encryptedSecretKey,
cusUserId = "your-user-id",
)
TSBehavioralSDK.markDeviceRegistered(
isRegistered = true,
customerUserDeviceId = userDeviceId,
)
}
// 4. Start data collection
TSBehavioralSDK.startCollect(this@BehavioralActivity)
// 5. Start keyboard tracking
TSBehavioralSDK.startKeyboardTracking(this@BehavioralActivity)
// 6. Record initial screen
TSBehavioralSDK.updateScreenTransition("main_screen")
} catch (e: TSSessionError) {
Log.e("BehavioralSDK", "Error [${e.name}]: ${e.message}")
}
}
}
// 7. Track touch events via dispatchTouchEvent
override fun dispatchTouchEvent(event: MotionEvent): Boolean {
TSBehavioralSDK.trackTouchEvent(event, WeakReference(this))
return super.dispatchTouchEvent(event)
}
// 8. Track screen transitions on navigation
private fun navigateToPayment() {
TSBehavioralSDK.updateScreenTransition("payment_screen")
// ... navigate to payment screen
}
// 9. Finalize when the flow is complete
private fun onFlowComplete() {
lifecycleScope.launch {
try {
val result = TSBehavioralSDK.finalizeCollect()
// Send challengeId to your backend to compute behavior score:
// POST /v1/behavior/score
// Body: { "challenge_id": result.challengeId }
yourBackend.computeBehaviorScore(result.challengeId!!)
} catch (e: TSSessionError) {
Log.e("BehavioralSDK", "Finalize error: ${e.message}")
}
}
}
// 10. Clear identity on logout
private fun onLogout() {
lifecycleScope.launch {
TSBehavioralSDK.clearDeviceIdentity()
// Must call initialize() again before next use
}
}
}
Server-Side Device Registration
After calling initialize(), your app must register the device with TrustVision backend before calling startCollect(). This is a server-side call — typically done from your app via an authenticated API client.
API Endpoint
POST {baseUrl}/v1/register_device
Content-Type: application/json
Authorization: TV {accessKeyId}:{signature}
X-TV-Timestamp: {ISO8601 UTC timestamp}
Request Body
| Field | Type | Source | Description |
|---|---|---|---|
device_id | String | initResult.deviceId | SDK-generated device identifier |
key_pair_id | String | initResult.bootstrapKeyId | Bootstrap key pair identifier |
device_public_key | String | initResult.encryptedDevicePublicKeyPayload | AES-GCM sealed device public key (base64) |
encrypted_secret_key | String | initResult.encryptedSecretKey | RSA-OAEP encrypted AES secret key (base64) |
cus_user_id | String | Your app | Customer user identifier from your system |
protocol | String | — | Always "encrypted" |
{
"device_id": "from initResult.deviceId",
"key_pair_id": "from initResult.bootstrapKeyId",
"device_public_key": "from initResult.encryptedDevicePublicKeyPayload",
"encrypted_secret_key": "from initResult.encryptedSecretKey",
"cus_user_id": "your-user-identifier",
"protocol": "encrypted"
}
Response
{
"data": {
"customer_user_device_id": "server-assigned-id"
}
}
HMAC-SHA256 Authentication
All API requests require HMAC-SHA256 signature in the Authorization header.
String to sign:
{METHOD}\n{PATH}\n{TIMESTAMP}
METHOD— HTTP method (e.g.,POST)PATH— API path without base URL (e.g.,/v1/register_device)TIMESTAMP— ISO 8601 UTC timestamp (e.g.,2026-03-31T12:00:00.000+0000)
Signature: Base64(HMAC-SHA256(accessKeySecret, stringToSign))
Headers:
| Header | Value |
|---|---|
Authorization | TV {accessKeyId}:{signature} |
X-TV-Timestamp | ISO 8601 UTC timestamp |
Kotlin Example
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.request.*
import io.ktor.http.*
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec
import android.util.Base64
@Serializable
data class RegisterDeviceRequest(
@SerialName("key_pair_id") val keyPairId: String,
@SerialName("device_public_key") val devicePublicKey: String,
@SerialName("encrypted_secret_key") val encryptedSecretKey: String,
@SerialName("cus_user_id") val cusUserId: String,
@SerialName("device_id") val deviceId: String,
val protocol: String = "encrypted",
)
@Serializable
data class RegisterDeviceResponse(
val data: Data,
) {
@Serializable
data class Data(
@SerialName("customer_user_device_id") val customerUserDeviceId: String,
)
}
// --- API Client ---
suspend fun registerDevice(
httpClient: HttpClient,
request: RegisterDeviceRequest,
): String {
val response: RegisterDeviceResponse = httpClient
.post("/api/v1/register_device") {
contentType(ContentType.Application.Json)
setBody(request)
}.body()
return response.data.customerUserDeviceId
}
// --- HMAC-SHA256 Signing ---
private fun createSignature(
method: String,
path: String,
accessKeySecret: String,
timestamp: String,
): String {
val stringToSign = "$method\n$path\n$timestamp"
val sha256Hmac = Mac.getInstance("HmacSHA256")
val secretKeySpec = SecretKeySpec(accessKeySecret.toByteArray(), "HmacSHA256")
sha256Hmac.init(secretKeySpec)
return Base64.encodeToString(
sha256Hmac.doFinal(stringToSign.toByteArray()),
Base64.NO_WRAP,
)
}
// --- Usage with Ktor Auth Plugin ---
val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ")
val timestamp = ZonedDateTime.now(ZoneId.of("UTC")).format(formatter)
val method = "POST"
val path = "/v1/register_device"
val signature = createSignature(method, path, accessKeySecret, timestamp)
// Set headers on request:
// "X-TV-Timestamp" to timestamp
// "Authorization" to "TV $accessKeyId:$signature"
Full Registration Flow
// 1. Initialize SDK
val initResult = TSBehavioralSDK.initialize(context, config)
// 2. Register device via your backend
val request = RegisterDeviceRequest(
keyPairId = initResult.bootstrapKeyId,
devicePublicKey = initResult.encryptedDevicePublicKeyPayload,
encryptedSecretKey = initResult.encryptedSecretKey,
cusUserId = "your-user-id",
deviceId = initResult.deviceId,
)
val customerUserDeviceId = registerDevice(httpClient, request)
// 3. Mark device as registered in SDK
TSBehavioralSDK.markDeviceRegistered(
isRegistered = true,
customerUserDeviceId = customerUserDeviceId,
)
// 4. Now you can start collecting
TSBehavioralSDK.startCollect(context)