Trust Signals: Mobile Sensor SDK

EARLY ACCESS

This page shows you how to install and use the Trust Signals Mobile Sensor SDK for Android and iOS. The SDK collects contextual data from the user’s device and sends it, through your collection proxy, to the Futurae Collection API.

Prerequisites

  • Android: Android 6.0 (API level 23) or later, Kotlin 1.9 or later, and compileSdk 36.
  • iOS: iOS 16 or later, built with Xcode 16 or later and Swift 5.9 or later.
  • A collection proxy on your backend. The SDK sends observations to it, never directly to Futurae. See Collection proxy.

Before you integrate, read Core Concepts, which explains the identifiers you pass to the SDK.

Release information

The release history is published on GitHub for Android and iOS. The API references are at futurae-technologies.github.io/android-trust-signals-sdk for Android and futurae-technologies.github.io/ios-trust-signals-sdk for iOS.

How the SDK works

The Android and iOS SDKs implement the same model:

  • Collect on demand. One call collects sensor data and uploads it immediately, for example just before an authentication attempt.
  • Collect on schedule. A recurring background job collects and uploads at an interval you set. Schedules survive app restarts.
  • Identifiers per collection. Every collection carries its own credentials and identifiers, so one SDK instance can tag different collections differently.

Each collection follows the same path:

  1. Your app calls collectAndUpload(), or a scheduled job starts.
  2. The SDK collects data from every source it has permission for, within the collection timeout.
  3. The SDK sends POST to the collectionUrl you configured, which is your collection proxy.
  4. Your proxy sets the collection token and the identity fields, and forwards the request to the Collection API, which replies 202 Accepted.
  5. On demand, the SDK returns the collection to your app.

Installation

Android

The SDK is distributed through GitHub Packages. Add the dependency to your app module:

// app/build.gradle.kts
implementation("com.futurae.sdk:trust-signals:<version>")

Replace <version> with the latest release tag. GitHub Packages requires authentication. The repository setup is in the Android SDK README.

iOS

The SDK is distributed through Swift Package Manager. In Xcode, choose File → Add Package Dependencies, enter https://github.com/Futurae-Technologies/ios-trust-signals-sdk.git, set the dependency rule to Exact Version with the latest release tag, and add TrustSignalsSDK to your target. Swift Package Manager needs an exact version for pre-release versions such as 0.1.0-alpha.

Or add it in Package.swift:

.package(url: "https://github.com/Futurae-Technologies/ios-trust-signals-sdk.git", exact: "<version>")

Full setup instructions are in the iOS SDK README.

Permissions

The SDK collects only what your app has permission for. A missing permission means the related data source reports nothing: the rest of the collection proceeds and uploads normally, and only the signals that depend on that data are affected. Decide when and how to ask the user for each permission as part of your app’s own flow.

In the tables below, Proximity means both Latest Proximity and Historical Proximity.

Android

The SDK declares its permissions in its own manifest, and they are merged into your app automatically. Normal permissions need no action. Runtime permissions must be requested by your app before the SDK can use them.

Runtime permissions, which your app must request:

PermissionAPI levelCollectsUsed by
ACCESS_FINE_LOCATIONAllLocation, Wi-Fi scan, Bluetooth scan, nearby devicesProximity, Geolocation, Geovelocity
ACCESS_COARSE_LOCATIONAllApproximate location, when precise location is deniedGeolocation, Geovelocity
BLUETOOTH_SCAN31 and laterBluetooth scan, nearby devicesProximity
BLUETOOTH_CONNECT31 and laterConnected Bluetooth peripheralsProximity
NEARBY_WIFI_DEVICES33 and laterWi-Fi scanProximity
READ_PHONE_STATEAllPhone-call stateActive Call

Normal permissions, granted automatically:

PermissionPurpose
BLUETOOTH, BLUETOOTH_ADMINBluetooth on API level 30 and earlier, declared with maxSdkVersion="30"
INTERNETUploading observations
ACCESS_WIFI_STATE, CHANGE_WIFI_STATEWi-Fi state collection
ACCESS_NETWORK_STATE, CHANGE_NETWORK_STATENetwork state collection
FOREGROUND_SERVICEThe location foreground service
FOREGROUND_SERVICE_LOCATIONThe location foreground service type, on API level 34 and later

iOS

iOS permissions are configured in Info.plist and in your app’s entitlements.

Info.plist key or entitlementCollectsUsed by
NSLocationWhenInUseUsageDescriptionGPS location, Wi-Fi network informationProximity, Geolocation, Geovelocity
NSBluetoothAlwaysUsageDescriptionBluetooth scan, Bluetooth peripherals, nearby devicesProximity
NSLocalNetworkUsageDescriptionNearby devices, through local network discoveryProximity
NSBonjourServicesNearby devices. Required together with NSLocalNetworkUsageDescription.Proximity
Access WiFi Information entitlement (com.apple.developer.networking.wifi-info)SSID of the connected Wi-Fi networkProximity

Phone-call state needs no permission on iOS.

Add the purpose strings to Info.plist, describing why your app uses each capability:

<key>NSLocationWhenInUseUsageDescription</key>
<string>Used for Trust Signals location verification.</string>
<key>NSBluetoothAlwaysUsageDescription</key>
<string>Used for Trust Signals Bluetooth device scanning.</string>
<key>NSLocalNetworkUsageDescription</key>
<string>Used for Trust Signals nearby device discovery.</string>

Enable the Access WiFi Information capability in Xcode, under Signing & Capabilities.

Nearby-device discovery browses only the Bonjour service types declared under NSBonjourServices. iOS silently skips any type that is not listed, so an incomplete list reduces nearby-device coverage without producing an error. To match the SDK’s full coverage, declare all 38 types:

<key>NSBonjourServices</key>
<array>
    <string>_smb._tcp.</string>
    <string>_privet._tcp.</string>
    <string>_device-info._tcp.</string>
    <string>_sftp-ssh._tcp.</string>
    <string>_airplay._tcp.</string>
    <string>_scanner._tcp.</string>
    <string>_mediaremotetv._tcp.</string>
    <string>_rdlink._tcp.</string>
    <string>_rfb._tcp.</string>
    <string>_uscan._tcp.</string>
    <string>_companion-link._tcp.</string>
    <string>_apple-mobdev2._tcp.</string>
    <string>_b._dns-sd._udp.</string>
    <string>_afpovertcp._tcp.</string>
    <string>_nfs._tcp.</string>
    <string>_webdav._tcp.</string>
    <string>_ftp._tcp.</string>
    <string>_ssh._tcp.</string>
    <string>_eppc._tcp.</string>
    <string>_http._tcp.</string>
    <string>_telnet._tcp.</string>
    <string>_printer._tcp.</string>
    <string>_ipp._tcp.</string>
    <string>_pdl-datastream._tcp.</string>
    <string>_riousbprint._tcp.</string>
    <string>_daap._tcp.</string>
    <string>_dpap._tcp.</string>
    <string>_ichat._tcp.</string>
    <string>_presence._tcp.</string>
    <string>_ica-networking._tcp.</string>
    <string>_airport._tcp.</string>
    <string>_xserveraid._tcp.</string>
    <string>_distcc._tcp.</string>
    <string>_apple-sasl._tcp.</string>
    <string>_workstation._tcp.</string>
    <string>_servermgr._tcp.</string>
    <string>_raop._tcp.</string>
    <string>_xcs2p._tcp.</string>
</array>

If you use scheduled collections, also enable the Location, Background Fetch and Background Processing background modes, under Signing & Capabilities → Background Modes, and register the SDK’s background task at launch, as described in Registering the background task on iOS.

Initialization

Initialize the SDK once, before calling any other SDK method, at every entry point of your app. App launch, background tasks and push handlers each start in a fresh context with no shared state. On Android these are distinct classes. On iOS they can be distinct processes, and a push extension does not share the app’s local observation cache unless you configure an App Group.

TSConfiguration is set once, at initialization, and applies to every collection:

FieldTypeRequiredDescription
collectionUrlString on Android, URL on iOSYesThe full URL of your collection proxy endpoint.
collectionTimeoutMs on Android, collectionTimeout on iOSLong on Android, Duration on iOSNoThe maximum time to wait for the data sources before returning a partial result. The default is 20 seconds. The timeoutMs argument on Android, or timeout: on iOS, of collect() and collectAndUpload() overrides it for one call. On iOS, scheduled collections always use the configured value.
collectionReuseWindow on iOSDurationNoHow old signals may be when a scheduled collection reuses them instead of reading the sensors again, for example when several accounts come due at almost the same time. The default, zero, means no reuse. On-demand calls never reuse signals.
requestTimeout on iOSDurationNoThe HTTP timeout for each upload to your proxy, independent of the collection timeout. The default is 15 seconds.
permissionPolicy on iOSTSPermissionPolicyNoOverrides how the SDK resolves permissions, to force specific permission states in tests or demo apps without prompting the user. The default defers to the system.

On Android, initialize in Application.onCreate():

import com.futurae.sdk.ts.TrustSignalsSDK
import com.futurae.sdk.ts.model.public.TSConfiguration

TrustSignalsSDK.initialize(
    context = this,
    configuration = TSConfiguration(
        collectionUrl = "https://your-backend.example.com/trust-signals/collections"
    )
)

On iOS, initialize on app launch, for example in AppDelegate or App.init:

import TrustSignals

await TrustSignalsSDK.initialize(
    TSConfiguration(
        collectionUrl: URL(string: "https://your-backend.example.com/trust-signals/collections")!
    )
)

Registering the background task on iOS

On iOS, scheduled collections run in the background only if your app registers the SDK’s background task. Call registerBackgroundTasks(identifier:) in application(_:didFinishLaunchingWithOptions:), before that method returns. The call is synchronous and independent of initialize(_:), because iOS rejects background task handlers registered after launch completes.

import TrustSignals
import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    func application(
        _ application: UIApplication,
        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
    ) -> Bool {
        TrustSignalsSDK.registerBackgroundTasks(identifier: "com.example.mybank.trust-signals")

        Task {
            await TrustSignalsSDK.initialize(
                TSConfiguration(
                    collectionUrl: URL(string: "https://your-backend.example.com/trust-signals/collections")!
                )
            )
        }

        return true
    }
}

List the same identifier under BGTaskSchedulerPermittedIdentifiers in Info.plist:

<key>BGTaskSchedulerPermittedIdentifiers</key>
<array>
    <string>com.example.mybank.trust-signals</string>
</array>

registerBackgroundTasks(identifier:) returns false if the identifier is missing from that list, or if the call came too late. Background collection is then unavailable, while everything else keeps working. Nothing runs in the background until you schedule a collection. Each background launch runs the accounts that are due, then requests the next launch, until you call stopScheduledCollections().

Collecting observations

Credentials and identifiers

Every upload, on demand or scheduled, takes a TSCredentials value on both platforms. All its fields are required:

FieldRequiredDescription
serviceIdYesYour tenant’s UUID. It must equal the service_id claim of your collection token.
unitIdYesThe static identifier of this app, such as mybank-ios.
accountIdYesThe end user these observations belong to. Your proxy overwrites it.
accessTokenYesThe value the SDK sends as Bearer token to your proxy. Use a placeholder, or your app’s own session credential for your backend. Never the Futurae collection token.
interactionIdYesThe app installation, such as your installation identifier. Your proxy overwrites it.
verificationStatusYesA TSVerificationStatus value: VERIFIED, UNVERIFIED or FRAUD on Android, .verified, .unverified or .fraud on iOS. Your proxy overwrites it.

What each identifier means, and its format rules, are in Identifiers. The recommended values are in Identifier Strategy.

Both SDKs reject invalid credentials before they read any sensor:

  • On Android, TSCredentials checks its values when you create it. If an identifier does not match its format rules, or accessToken is blank, it throws IllegalArgumentException, so the invalid value never reaches collectAndUpload() or scheduleCollections().
  • On iOS, collectAndUpload() and scheduleCollections() check the credentials they receive, and reject invalid input with one of the TSError cases below.
iOS errorCause
noAccountsProvidedAn upload was requested without any credentials.
blankAccountIdaccountId is empty or only whitespace.
missingAccessTokenaccessToken is blank.
invalidIdentifier(field:reason:)An identifier breaks one of the format rules. field names it and reason explains the rule.

On iOS, on-demand calls throw these errors, and scheduled collections deliver them to the error handler.

Both SDKs accept several TSCredentials values in one call, one per account. collectAndUpload() then collects the data once and uploads one collection per account. Every upload is attempted, so a failure for one account does not cancel the others:

  • On Android, pass the values as separate arguments. The uploads run in parallel, and failures are reported in TSUploadException.
  • On iOS, pass an array. The call returns a TSUploadOutcome, which lists the accounts that reached the backend and the failure of each one that did not. It throws TSUploadError only when no account got through. That error carries the same per-account failures, and its rejectedAccountIds names the accounts whose credentials were rejected.

Collect and upload on demand

On Android, collectAndUpload() is available as a suspending function, which is the recommended form, and with callbacks. The optional timeoutMs argument overrides the configured collection timeout for one call:

import com.futurae.sdk.ts.TrustSignalsSDK
import com.futurae.sdk.ts.error.TSAuthenticationException
import com.futurae.sdk.ts.error.TSCollectionTimedOutException
import com.futurae.sdk.ts.error.TSNotInitializedException
import com.futurae.sdk.ts.error.TSUploadException
import com.futurae.sdk.ts.model.public.TSCredentials
import com.futurae.sdk.ts.model.public.TSVerificationStatus

val credentials = TSCredentials(
    serviceId          = "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    unitId             = "mybank-android",
    accountId          = "placeholder",                    // your proxy sets the real value
    accessToken        = "<your_proxy_credential>",        // never the Futurae collection token
    interactionId      = "placeholder",                    // your proxy sets the real value
    verificationStatus = TSVerificationStatus.VERIFIED     // your proxy sets the real value
)

// Coroutine
try {
    val collection = TrustSignalsSDK.collectAndUpload(credentials)
    // collection.observation contains everything collected
} catch (e: TSUploadException) {
    // One or more uploads failed: e.failures maps each failed accountId to its cause
    e.failures.forEach { (accountId, error) ->
        if (error is TSAuthenticationException) { /* your proxy answered 401 or 403 */ }
    }
} catch (e: TSCollectionTimedOutException) {
    // Every long-running data source timed out and nothing was collected
} catch (e: TSNotInitializedException) {
    // initialize() was not called first
}

// Coroutine, overriding the configured collection timeout for this call only
val quickCollection = TrustSignalsSDK.collectAndUpload(credentials, timeoutMs = 5_000L)

// Callback
TrustSignalsSDK.collectAndUpload(
    credentials,
    onSuccess = { collection -> /* runs on the main thread */ },
    onError   = { error -> /* runs on the main thread; a failed upload arrives as TSUploadException */ }
)

On Android, each call returns a TSCollection. It carries a collectionId, the observationTime, the sensorTag, the observation itself, and a timedOut flag. timedOut is true when at least one long-running data source did not finish before the deadline, so the collection is partial.

On iOS, collectAndUpload() is an asynchronous throwing function. Swift requires the TSCredentials arguments in the order shown. The optional timeout: argument overrides the configured collection timeout for one call, and a value of zero or less is ignored:

import TrustSignals

let credentials = TSCredentials(
    accountId: "placeholder",                    // your proxy sets the real value
    accessToken: "<your_proxy_credential>",      // never the Futurae collection token
    serviceId: "f47ac10b-58cc-4372-a567-0e02b2c3d479",
    unitId: "mybank-ios",
    interactionId: "placeholder",                // your proxy sets the real value
    verificationStatus: .verified                // your proxy sets the real value
)

do {
    let collection = try await TrustSignalsSDK.collectAndUpload(credentials)
    // collection.observation contains everything collected
} catch TSError.authenticationFailed {
    // Your proxy answered 401 or 403
} catch TSError.uploadRejected {
    // Your proxy answered with another non-2xx status
} catch TSError.uploadTransportFailed {
    // The request did not complete: DNS, TLS or timeout
} catch TSError.collectionTimedOut(let after) {
    // No data source finished within `after`
} catch TSError.notInitialized {
    // initialize(_:) was not called first, or stopAll() was called
} catch {
    // Invalid credentials, see Credentials and identifiers
}

// Override the configured collection timeout for this call only
let quickCollection = try await TrustSignalsSDK.collectAndUpload(credentials, timeout: .seconds(5))

// Several accounts: one sweep of the sensors, one upload per account
let outcome = try await TrustSignalsSDK.collectAndUpload([credentialsForUser1, credentialsForUser2])

Upload errors are thrown to the caller. An on-demand upload that is still running when the app moves to the background completes there.

Collect without uploading

collect() gathers the same data without sending it, for example to inspect what the SDK collects during development:

// Android, coroutine
val collection = TrustSignalsSDK.collect()
val quickCollection = TrustSignalsSDK.collect(timeoutMs = 5_000L)   // overrides the configured timeout

// Android, callback
TrustSignalsSDK.collect(
    onSuccess = { collection -> /* runs on the main thread */ },
    onError   = { error -> /* runs on the main thread */ }
)
// iOS
let collection = try await TrustSignalsSDK.collect()
let quickCollection = try await TrustSignalsSDK.collect(timeout: .seconds(5))   // overrides the configured timeout

Scheduled collections

scheduleCollections() starts a recurring background job that collects and uploads at the interval you set. Futurae recommends every 3 hours, see When to collect. Each account has its own schedule: calling scheduleCollections() again for the same account replaces that account’s schedule without affecting others.

On Android, each TSCredentials value gets its own independent periodic job, so you can stop one account’s schedule without affecting the others. The minimum interval is 15 minutes, available as TrustSignalsSDK.MIN_COLLECTION_INTERVAL, and a shorter interval throws IllegalArgumentException:

// Android
import kotlin.time.Duration.Companion.hours

TrustSignalsSDK.scheduleCollections(
    collectionInterval = 3.hours,
    credentials
)

TrustSignalsSDK.stopScheduledCollections("user-account-id")          // stop one account's schedule
TrustSignalsSDK.stopScheduledCollections("account-1", "account-2")   // stop several accounts' schedules
TrustSignalsSDK.stopScheduledCollections()                           // stop every schedule

On iOS, the default interval is 30 minutes. There are three forms:

  • scheduleCollections(collectionInterval:credentials:) with one TSCredentials, for one account.
  • The same call with an array of TSCredentials, for several accounts on a shared interval.
  • scheduleCollections(_:) with an array of TSScheduledRequest, for several accounts, each on its own interval.

Calling scheduleCollections() again with refreshed credentials, or with accounts added or removed, updates the schedule without interrupting it. Only a changed interval restarts an account’s timer. The schedules property returns the interval of every scheduled account.

// iOS: one account
await TrustSignalsSDK.scheduleCollections(
    collectionInterval: .seconds(10800),   // 3 hours
    credentials: credentials
)

// iOS: several accounts, each on its own interval
await TrustSignalsSDK.scheduleCollections([
    TSScheduledRequest(credentials: credentialsForUser1, collectionInterval: .seconds(10800)),
    TSScheduledRequest(credentials: credentialsForUser2, collectionInterval: .seconds(3600)),
])

let active = await TrustSignalsSDK.schedules   // [accountId: interval]

await TrustSignalsSDK.stopScheduledCollections(accountId: "user-account-id")   // stop one account's schedule
await TrustSignalsSDK.stopScheduledCollections()                               // stop every schedule; the SDK stays usable
await TrustSignalsSDK.stopAll()                                                // stop all SDK activity and release background resources

After stopAll(), every call throws TSError.notInitialized until you call initialize(_:) again.

Android runs schedules as WorkManager periodic jobs. iOS runs them as background tasks, which need the registration at launch. Both survive app restarts. The operating system decides when background work actually runs, depending on the OS version, battery level and device activity, so expect fewer collections than the interval implies. Register an error handler before you schedule: it is the only way to learn about failures in background collections.

Observe collections on iOS

On iOS you can subscribe to a stream of every completed collection, on demand or scheduled:

for await collection in await TrustSignalsSDK.collections {
    // Process each collection as it completes
}

Sending observations through your proxy

The SDK sends every upload to the collectionUrl you set at initialization, which is your collection proxy. The real collection token and the user’s real identifiers are added there, so the app holds only placeholders and, optionally, its own session credential for your backend.

For every request it receives from a Sensor SDK, your proxy must:

  1. Identify the user. Use your own session mechanism to establish which authenticated user and session the request belongs to. Reject the request if it cannot.
  2. Set the identity fields. Overwrite accountId, interactionId and verificationStatus in the request body with the values your backend holds for that user and session, whatever the SDK sent.
  3. Set the collection token. Replace the Authorization header with Bearer <collection_token>, using the token your backend holds.
  4. Add the client IP address. Set the X-Forwarded-For header to the IP address of the device or browser that sent the request.
  5. Forward the request to the Collection API at https://ts-collection-public.futurae.com/api/v1/collections.

The forwarded request, and the status codes the SDK expects back, are in Collection proxy.

Error handling

Android

On-demand calls throw exceptions to the caller:

ExceptionWhen it is thrown
TSUploadExceptionOne or more uploads failed. Its failures map holds the cause for each failed accountId: TSAuthenticationException when your proxy answered 401 or 403, TSUploadRejectedException for another non-2xx status, or TSUploadTransportException when the request failed before a response arrived.
TSCollectionTimedOutExceptionEvery long-running data source timed out and nothing was collected. If some finished in time, the call returns a partial collection with timedOut set to true.
TSNotInitializedExceptionAn SDK method was called before initialize().
IllegalArgumentExceptionA TSCredentials value was created with an invalid identifier or a blank accessToken, see Credentials and identifiers. Or scheduleCollections() was given an interval shorter than MIN_COLLECTION_INTERVAL.

Errors from scheduled collections are delivered, on the main thread, to the handler registered with registerErrorHandler, together with the affected accountId. The schedule is not cancelled when an error occurs.

ErrorOn demandScheduled
Network or I/O failureIn TSUploadException.failures, as TSUploadTransportExceptionRetried up to 3 times by WorkManager, then delivered to the handler as TSUploadTransportException, with the IOException as its cause
401 or 403 from your proxyIn TSUploadException.failures, as TSAuthenticationException, not retriedDelivered to the handler as TSAuthenticationException, not retried
Other non-2xx statusIn TSUploadException.failures, as TSUploadRejectedException, not retriedDelivered to the handler as TSUploadRejectedException, not retried
Unexpected exception—Delivered to the handler, not retried

After a failed scheduled cycle, the schedule continues at the next interval.

iOS

All SDK errors are cases of TSError, except TSUploadError. collect() and collectAndUpload() throw their errors directly to the caller:

ErrorCauseValues it carries
authenticationFailedYour proxy answered 401 or 403.statusCode, body
uploadRejectedYour proxy answered with another non-2xx status.statusCode, body
uploadTransportFailedThe request failed before a response arrived: DNS, TLS or timeout.underlying
collectionTimedOutNo data source finished within the collection timeout. If some finished in time, the collection is returned with timedOut set instead.after
notInitializedAn SDK method was called before initialize(_:), or after stopAll().—
noAccountsProvided, blankAccountId, missingAccessToken, invalidIdentifierThe credentials are invalid. See Credentials and identifiers.field and reason, for invalidIdentifier
TSUploadErrorA call with several accounts, where no account got through.failures per account, and rejectedAccountIds

collectorFailed(name:message:) reports a single data source that failed. The rest of the collection still completes, and only that signal is missing.

Errors from scheduled collections are delivered to the handler registered with registerErrorHandler(_:), together with the affected accountId. Failures are reported per account, and one failing account stops neither the others nor the schedule. The handler may be invoked off the main thread, so switch to the main actor before you touch the UI. Pass nil to remove the handler.

The SDK performs no automatic retries and keeps no offline queue, so each upload is a single attempt. After a failed scheduled cycle, the schedule continues at the next interval.

Recovering from authentication errors

An authentication error means your proxy rejected the upload. The Futurae collection token lives on your backend, so your proxy refreshes it, as described in Responding to the SDK, and the app is not involved. On the device, an authentication error usually means your proxy no longer accepts the credential the app passes as accessToken. Renew that credential, then stop only the failing account’s schedule and reschedule it with complete credentials:

import com.futurae.sdk.ts.TrustSignalsSDK
import com.futurae.sdk.ts.error.TSAuthenticationException
import com.futurae.sdk.ts.model.public.TSCredentials
import com.futurae.sdk.ts.model.public.TSVerificationStatus
import kotlin.time.Duration.Companion.hours

TrustSignalsSDK.registerErrorHandler { accountId, error ->
    when (error) {
        is TSAuthenticationException -> {
            TrustSignalsSDK.stopScheduledCollections(accountId)
            val credential = renewProxyCredential(accountId)   // your own method

            TrustSignalsSDK.scheduleCollections(
                collectionInterval = 3.hours,
                TSCredentials(
                    serviceId          = serviceId,
                    unitId             = unitId,
                    accountId          = accountId,
                    accessToken        = credential,
                    interactionId      = interactionId,
                    verificationStatus = TSVerificationStatus.VERIFIED
                )
            )
        }
        else -> {
            // Log or report other errors
        }
    }
}

The iOS pattern is the same, using registerErrorHandler and TSError.authenticationFailed.

Sample apps

Both SDK repositories include a runnable sample app:

  • Android: the sample directory holds a minimal Jetpack Compose app that demonstrates initialization, collect-and-upload for one and for several accounts, and scheduled background collections. It pulls the SDK from GitHub Packages, so it needs your GitHub Packages credentials, and it reads the collection URL from the TS_COLLECTION_URL Gradle property.
  • iOS: the test-app directory holds a minimal SwiftUI app. Open TrustSignalsTestApp.xcodeproj in Xcode and run it on a simulator or a device. It lets you configure the SDK, then collect, collect and upload, and schedule collections from a single screen.

API overview

The two SDKs are equivalent in capability. Differences are limited to naming conventions, error delivery and a few data points unique to each platform, and may grow as platform constraints evolve.

AspectAndroidiOS
LanguageKotlin / JavaSwift
Initializeinitialize(context, config)initialize(config), awaited
Collect without uploadingcollect()collect()
Collect and uploadcollectAndUpload(), for one or several accountscollectAndUpload(), for one or several accounts
Schedule collectionsscheduleCollections(), one job per accountscheduleCollections(), one schedule per account
Read active schedules—schedules
Stop schedulingstopScheduledCollections(), for one, several or all accountsstopScheduledCollections(), for one or all accounts
Stop all activity—stopAll(), which also releases background resources
Register an error handlerregisterErrorHandler()registerErrorHandler()
Error handler threadMain threadMay be off the main thread
Stream of collections—collections
Scheduling backendWorkManager periodic jobBackground tasks, registered with registerBackgroundTasks()
Upload errors on demandThrown as TSUploadException, with a cause per accountThrown as TSError cases, or as TSUploadError when no account got through
Error typesTSUploadException, with causes such as TSAuthenticationExceptionTSError cases, such as authenticationFailed
Platform-only dataManufacturer, developer mode, installer source, network state. See Data Collection and Privacy.Device name, vendor identifier, thermal state, low power mode, proximity sensor, screen brightness

The arguments of each method are described in Initialization and Collecting observations, and every error type is listed in Error handling.