Android v1.0.0.3

Specifications

SpecValue
Min SDK23 (Android 6.0)
LanguageKotlin
CoroutinesRequired
Serializationkotlinx.serialization
DIKoin (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:

Kotlin
dependencyResolutionManagement {
    repositories {
        google()
        mavenCentral()
        maven {
            url = uri("${rootProject.projectDir}/repo")
        }
    }
}

3. Add dependencies

In your app-level build.gradle.kts:

Kotlin
dependencies {
    implementation("com.trustingsocial:behaviors:1.0.0.3")
}

4. ProGuard / R8

The SDK ships consumer ProGuard rules automatically. No additional configuration needed.

Configuration

Kotlin
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

FieldTypeRequiredDefaultDescription
appIdStringYes—Your app's package name (application ID)
baseUrlStringYes—Base URL for TrustVision API
metadataConfigMetaDataCollectionConfigYes—Device metadata collection settings
uploadBlockSizeIntNo10Number of batch entries before auto-upload
sensorConfigSensorConfigNoSensorConfig()Sensor data collection settings
keyboardConfigKeyboardConfigNoKeyboardConfig()Keyboard tracking settings
touchConfigTouchConfigNoTouchConfig()Touch tracking settings
screenTransitionBlockSizeIntNo1Screen transition batch size
appStateBlockSizeIntNo1Number of app-state events per upload batch
batchExtraMap<String, String>NoemptyMap()Custom key-value metadata included in every batch upload
retryConfigRetryConfigNoRetryConfig()Upload retry queue settings
sslCertificatesList<Int>?NonullRaw 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.

Kotlin
val config = TSBehavioralConfig(
    appId = "com.example.app",
    baseUrl = "https://api.trustingsocial.com",
    metadataConfig = MetaDataCollectionConfig(intervalMillis = 2000),
    sslCertificates = listOf(R.raw.my_server_cert),
)

MetaDataCollectionConfig

FieldTypeRequiredDefaultDescription
intervalMillisLongYes—Metadata collection interval in milliseconds
enabledBooleanNotrueEnable/disable metadata collection
typeModesMap<MetaDataType, MetaDataCollectionMode>NoDeviceDescriptions→MANUAL, DeviceBehaviors→INTERVAL, UserBehaviors→INTERVALCollection mode per metadata type. INTERVAL = repeating timer; MANUAL = explicit calls only
collectOnceTypesSet<MetaDataType>No{DeviceDescriptions}Types collected once at session start and stop (fire-and-forget boundary snapshots)

SensorConfig

FieldTypeRequiredDefaultDescription
samplingIntervalMsLongNo20L (50Hz)Sensor sampling interval in milliseconds. Must be > 0
sensorTypesList<SensorType>NoAll 4 typesSensors to collect: Gyroscope, Magnetometer, Accelerometer, Light
enabledBooleanNotrueEnable/disable sensor collection

KeyboardConfig

FieldTypeRequiredDefaultDescription
trackRawTextBooleanNofalseWhether to collect raw typed text. When false, only character type is recorded
enabledBooleanNotrueEnable/disable keyboard tracking

TouchConfig

FieldTypeRequiredDefaultDescription
enabledBooleanNotrueEnable/disable touch tracking

RetryConfig

FieldTypeRequiredDefaultDescription
enabledBooleanNotrueEnable/disable the retry queue
maxRetriesIntNo2Max immediate retries per request (0–10). Total calls = value + 1
retryDelayMsLongNo1000Base delay before first retry in milliseconds (> 0)
maxDelayMsLongNo5000Maximum delay cap in milliseconds (≥ retryDelayMs)
maxRetryAttemptsIntNo3Maximum retry-queue attempts before dropping a failed batch (≥ 1)
maxRetryStorageItemsIntNo50Maximum items in retry storage before FIFO eviction (≥ 1)
finalFlushTimeoutMsLongNo10000Timeout in milliseconds for flush at session end (≥ 1)
retryItemDelayMsLongNo500Delay 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:

Kotlin
suspend fun initialize(context: Context, config: TSBehavioralConfig): TSDeviceInitInfo

Parameters:

ParameterTypeRequiredDescription
contextContextYesAndroid context (Application or Activity)
configTSBehavioralConfigYesSDK configuration

Returns: TSDeviceInitInfo

FieldTypeDescription
deviceIdStringUnique device identifier (UUID)
bootstrapKeyIdStringHardware key ID for encryption
isRegisteredBooleanWhether device was previously registered
encryptedDevicePublicKeyPayloadStringAES-GCM sealed device public key (base64). Pass to device_public_key in register API
encryptedSecretKeyStringRSA-OAEP encrypted AES secret key (base64). Pass to encrypted_secret_key in register API

Throws: TSSessionError.KeystoreError, TSSessionError.CryptoError

Example:

Kotlin
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:

Kotlin
suspend fun markDeviceRegistered(isRegistered: Boolean, customerUserDeviceId: String)

Parameters:

ParameterTypeRequiredDescription
isRegisteredBooleanYesRegistration status (true to mark as registered)
customerUserDeviceIdStringYesUnique user-device identifier returned by your server

Throws: TSSessionError.NotInitialized

Example:

Kotlin
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:

Kotlin
suspend fun startCollect(context: Context? = null)

Parameters:

ParameterTypeRequiredDescription
contextContext?NoOptional context override. Uses the context from initialize() if null

Throws: TSSessionError.NotInitialized, TSSessionError.DeviceNotRegistered, TSSessionError.NetworkError, TSSessionError.InvalidChallengeResponse, TSSessionError.ManagerStartError

Example:

Kotlin
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 after startCollect() 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:

Kotlin
fun updateScreenTransition(value: String)
fun updateScreenTransition(value: ScreenTransitionCollectorListener)

Parameters (String overload):

ParameterTypeRequiredDescription
valueStringYesName of the screen being navigated to

Parameters (Listener overload):

ParameterTypeRequiredDescription
valueScreenTransitionCollectorListenerYesObject implementing getScreenName(): String

Example — using String:

Kotlin
// 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:

Kotlin
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:

Kotlin
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:

ParameterTypeRequiredDescription
activityActivityYesActivity containing EditText fields to track
viewViewYesView hierarchy containing EditText fields. Use fragment.requireView() for Fragments
editTextEditTextYesSpecific EditText field to track

Throws: TSSessionError.NotInitialized

Example:

Kotlin
// 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:

Kotlin
suspend fun stopKeyboardTracking()
suspend fun stopKeyboardTracking(editText: EditText)

Parameters (EditText overload):

ParameterTypeRequiredDescription
editTextEditTextYesSpecific EditText field to stop tracking

Throws: TSSessionError.NotInitialized

Example:

Kotlin
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:

Kotlin
fun addKeyboardTrackingData(data: KeyboardData)

Parameters:

ParameterTypeRequiredDescription
dataKeyboardDataYesKeyboard event data

KeyboardData fields:

FieldTypeRequiredDefaultDescription
timestampLongYes—Event timestamp in milliseconds
dataString?NonullCharacter value (only sent if trackRawText is true)
textLengthInt?NonullCurrent text field length
typeKeyboardDataTypeNoUnknownType of keystroke
viewIdString?NonullIdentifier for the input field

KeyboardDataType values: LowerText, UpperText, Number, Spacing, SpecialCharacters, Delete, CursorMoving, Mixed, Unknown

Example:

Kotlin
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:

Kotlin
fun trackTextChange(oldText: String, newText: String, viewId: String)

Parameters:

ParameterTypeRequiredDescription
oldTextStringYesText value before the change
newTextStringYesText value after the change
viewIdStringYesStable identifier for the input field

Example — Jetpack Compose:

Kotlin
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:

Kotlin
fun trackTouchEvent(event: MotionEvent, activity: WeakReference<Activity>)
fun trackTouchEvent(event: PointerEventDto, activity: WeakReference<Activity>)

Parameters (MotionEvent overload):

ParameterTypeRequiredDescription
eventMotionEventYesAndroid motion event from dispatchTouchEvent
activityWeakReference<Activity>YesWeak reference to the current Activity

Parameters (PointerEventDto overload):

ParameterTypeRequiredDescription
eventPointerEventDtoYesPre-converted pointer event data
activityWeakReference<Activity>YesWeak reference to the current Activity

PointerEventDto fields:

FieldTypeDescription
actionNameString?Action name: ACTION_DOWN, ACTION_POINTER_DOWN, ACTION_MOVE, ACTION_UP, ACTION_POINTER_UP
actionMaskedInt?MotionEvent action constant
pointerIdInt?Pointer identifier
globalXDouble?Screen X coordinate
globalYDouble?Screen Y coordinate

Example — using MotionEvent (recommended):

Kotlin
class MyActivity : AppCompatActivity() {

    override fun dispatchTouchEvent(event: MotionEvent): Boolean {
        TSBehavioralSDK.trackTouchEvent(event, WeakReference(this))
        return super.dispatchTouchEvent(event)
    }
}

Example — using PointerEventDto:

Kotlin
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:

Kotlin
suspend fun finalizeCollect(): TSFinalizeResult

Returns: TSFinalizeResult

FieldTypeDescription
nonceString?Session nonce from challenge
challengeIdString?Challenge ID — pass to backend for scoring
lastSeqIntLast uploaded batch sequence number
batchIdString?Last batch identifier
droppedBatchCountIntNumber of event batches dropped without reaching the server (data loss). Defaults to 0; 0 on a clean finalize
lastUploadErrorString?Description of the last upload error that caused a drop, or null when none

Observability (v1.0.0.3): droppedBatchCount and lastUploadError let you detect silent data loss after finalize. A non-zero droppedBatchCount means some collected batches never reached the server; inspect lastUploadError for the cause.

Throws: TSSessionError.NotInitialized

Example:

Kotlin
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:

Kotlin
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()
Asyncsuspendsuspend
Flush dataYesNo
Drain upload queueYes (waits)No
Data loss riskNonePossible
Use caseNormal completionApp backgrounding

Example:

Kotlin
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:

Kotlin
suspend fun clearDeviceIdentity()

Throws: TSSessionError.NotInitialized, TSSessionError.KeystoreError

Example:

Kotlin
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.

ErrornameCondition
NotInitializednotInitializedSDK not initialized. Call initialize() first
DeviceNotRegistereddeviceNotRegisteredServer-authoritative: the server reports the device is not registered. Call markDeviceRegistered() after registering with your backend
SslPinningError(cause)sslPinningErrorTLS/SSL pinning validation failed — the server certificate did not match a pinned certificate
ChallengeExpiredchallengeExpiredServer challenge expired. Retry startCollect()
InvalidChallengeResponseinvalidChallengeResponseEmpty or invalid challenge from server
NotStartednotStartedNo active collection session. Call startCollect() first
KeystoreError(cause)keychainErrorAndroid Keystore operation failed
CryptoError(cause)cryptoErrorEncryption or signing operation failed
NetworkErrornetworkErrorHTTP request failed. Exposes httpCode: Int? and serverErrorCode: String? (the server's errors[].code). Message is formatted "HTTP <code>: <serverMessage>"
SerializationError(cause)serializationErrorFailed to encode/decode a request or response payload
UnknownError(cause)unknownErrorAn unclassified error — fallback for exceptions that do not map to any known category
ManagerStartError(cause)managerStartErrorFailed 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).

Kotlin
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.

Kotlin
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

FieldTypeSourceDescription
device_idStringinitResult.deviceIdSDK-generated device identifier
key_pair_idStringinitResult.bootstrapKeyIdBootstrap key pair identifier
device_public_keyStringinitResult.encryptedDevicePublicKeyPayloadAES-GCM sealed device public key (base64)
encrypted_secret_keyStringinitResult.encryptedSecretKeyRSA-OAEP encrypted AES secret key (base64)
cus_user_idStringYour appCustomer user identifier from your system
protocolString—Always "encrypted"
JSON
{
  "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

JSON
{
  "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:

HeaderValue
AuthorizationTV {accessKeyId}:{signature}
X-TV-TimestampISO 8601 UTC timestamp

Kotlin Example

Kotlin
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

Kotlin
// 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)