Skip to Content
DevelopersSDKsAndroid SDKQuickstart

Android SDK

The Active Reach Android SDK tracks user behavior, identifies users, and manages FCM push tokens on Android 5.0+ (API 21+).

Current version: ai.active-reach:android-sdk:1.6.0 (Maven Central).

Brand canon. The Maven coordinate is ai.active-reach:android-sdk, but the Kotlin import path you actually write in your code is ai.aegis.sdk.*. This is intentional: “Aegis” is the internal source-tree name; “Active Reach” is the customer-facing identifier on the registry. The class Aegis and its companion AegisConfig keep the internal name; only the Gradle coordinate switched.

Installation

Add the dependency to your app-level build.gradle.kts:

dependencies { implementation("ai.active-reach:android-sdk:1.6.0") }

Initialization

Initialize in your Application class:

import ai.aegis.sdk.Aegis import ai.aegis.sdk.AegisConfig class MyApp : Application() { override fun onCreate() { super.onCreate() Aegis.init( context = this, writeKey = "YOUR_WRITE_KEY", config = AegisConfig(workspaceId = "YOUR_WORKSPACE_ID") ) } }

Core tracking

// Track a custom event Aegis.track("order_completed", mapOf( "order_id" to "ORD-123", "amount" to 4999, "currency" to "INR" )) // Identify a user Aegis.identify("user_456", mapOf( "name" to "Priya Sharma", "email" to "[email protected]", "plan" to "pro" )) // Track a screen view Aegis.screen("Product Detail", mapOf( "product_id" to "SKU-A" )) // Group a user into a company Aegis.group("company_789", mapOf("name" to "Acme Corp")) // Merge anonymous + identified sessions Aegis.alias("user_456") // Clear identity on logout Aegis.reset() // Flush before app termination Aegis.flush()

Event governance

The Android SDK ships with client-side event-name cap enforcement — byte-identical hashing with the web SDK, Python control-plane, and iOS SDK. When you bootstrap the SDK, the server returns a compact Bloom filter of your registered event names + a counter of remaining headroom. The SDK uses this to drop novel names locally once you hit your plan cap.

Integration

import ai.aegis.sdk.Aegis import ai.aegis.sdk.governance.EventGovernanceHint // 1. Bootstrap against control-plane (typically in your app onboarding). val bootstrap: BootstrapResult = bootstrap( apiHost = "https://api.active-reach.ai", writeKey = BuildConfig.AEGIS_WRITE_KEY, ) // 2. Initialize the SDK. Aegis.initialize(application, BuildConfig.AEGIS_WRITE_KEY, AegisConfig( apiHost = "https://api.active-reach.ai", )) // 3. Feed the hint to the governor. Aegis.ingestGovernanceHint(bootstrap.eventGovernance)

Pass null to disable (fail-open — gateway remains authoritative):

Aegis.ingestGovernanceHint(null)

What the SDK does

Identical semantics to the web + iOS SDKs — bloom-check for known names, per-instance memo of novel names so repeating a name doesn’t double-charge, drop-and-coalesce for novel names past the cap, fail-open during the server’s 7-day grace window.

Drops surface in two ways:

  1. A coalesced aegis.client.name_governor_dropped meta event rides the next batch flush — ops dashboards see the pattern
  2. A single Log.w("AegisGovernor", "…") on the first dropped name per session — developers see the issue in logcat immediately

The EventGovernanceHint type

@Serializable data class EventGovernanceHint( @SerialName("bloom_algo") val bloomAlgo: String, // always "mmh3_x86_32_km" @SerialName("seed_a") val seedA: Int, // always 0 @SerialName("seed_b") val seedB: Int, // always 1 val k: Int, // typically 7 val m: Int, // power-of-2 bitarray size @SerialName("bloom_b64") val bloomB64: String, // base64 of the bitarray @SerialName("remaining_new_names") val remainingNewNames: Int? = null, @SerialName("grace_active") val graceActive: Boolean = false, @SerialName("ttl_seconds") val ttlSeconds: Int = 300, )

Decode directly from the /v1/sdk/bootstrap JSON via kotlinx.serialization:

val json = Json { ignoreUnknownKeys = true } val hint = json.decodeFromString<EventGovernanceHint>(jsonString)

Thread-safety

NameGovernor uses synchronized(lock) on all mutating paths (ingestHint, shouldSend, drainDropReport) — safe to call from any thread concurrently, including UI thread + background batch flush.

Push notifications

Register the FCM token:

FirebaseMessaging.getInstance().token.addOnSuccessListener { token -> Aegis.push.registerDeviceToken(token) }

Track delivery and clicks:

Aegis.push.trackDelivery(messageId = "msg_123") Aegis.push.trackClick(messageId = "msg_123")

Requires FCM setup — see Push setup guide.

Push engagement reporting (SDK ≥1.4.0)

The Android SDK ships with AegisPushTracker which fires push lifecycle events to the canonical ingestion endpoint without host-app code.

LifecycleHookWhat it posts
DeliveredAegisFirebaseMessagingService.onMessageReceivedevent_type: "push.delivered"
ClickedAegisPushClickActivity (registered by the SDK manifest)event_type: "push.clicked" with metadata.action_url
DismissedAegisPushDismissReceiver (wired via setDeleteIntent on NotificationCompat.Builder)event_type: "push.dismissed"
SubscribedAegis.push.registerDeviceToken(token)event_type: "push.subscribed"

To enable dismiss tracking on rich notifications you build yourself, set the SDK’s DeleteIntent on your builder:

val deleteIntent = AegisPushDismissReceiver.deleteIntent(context, messageId, campaignId) val notification = NotificationCompat.Builder(context, channelId) .setContentTitle(title) .setContentText(body) .setDeleteIntent(deleteIntent) .build()

Wire shape for all four signals: POST /v1/push/engagement — see Event ingestion.

Advanced features including deep linking, rich notification attachments, and background tracking are documented in the full Android SDK reference. Coming soon.

E-commerce helpers (Phase 1, SDK ≥1.1.0)

Use Aegis.ecommerce for the canonical 19-method tracker — same wire shape as the web SDK’s EcommerceTracker, so the cell-plane canonical_mapper consumes events identically across web + mobile.

import ai.aegis.sdk.ecommerce.* Aegis.ecommerce.productViewed(EcommerceProduct( productId = "sku-42", name = "Cotton Tee", price = 1299.0, currency = "INR", )) Aegis.ecommerce.addToCart(product) Aegis.ecommerce.orderCompleted(EcommerceOrder( orderId = "ord-100", value = 1299.0, products = listOf(product), )) // Back-in-stock waitlist — arms the catalog.back_in_stock journey trigger. Aegis.ecommerce.productWaitlisted(EcommerceWaitlist( product = product, channels = listOf(WaitlistChannel.WHATSAPP), ))

Cart abandonment is detected server-side — the SDK only emits the canonical cart events.

Trait governance (Phase 1, SDK ≥1.1.0)

identify() and group() run their traits through a client-side TraitGovernor that mirrors the 5 ingestion guards the cell-plane backend enforces. Drops surface as Log.w(AegisTraitGovernor, …) lines — rate-limited to first 3 per (workspace, verdict) per session.

Behaviour:

  • camelCase keys auto-rewritten to snake_case
  • Reserved prefixes dropped (system., user., loyalty., cart., bill., $, _, …)
  • Long strings truncated; oversized strings rejected (DoS protection)
  • ISO-8601 / epoch values on date-keyed fields parsed to epoch-ms

The four-surface drift contract (Kotlin verdict → backend record_attribute_keys verdict → DB ingestion_dlq.verdict enum → frontend IngestionDLQVerdict union) is pinned by libs/mobile-sdk/tests/drift/trait-governor-verdicts.json. See Event governance → TraitGovernor for the full verdict matrix.

Multi-region cell selection (Phase 1, SDK ≥1.1.0)

AegisConfig now accepts cellEndpoints + preferredRegion + autoRegionDetection + workspaceId. When cellEndpoints is empty the SDK falls back to apiHost.

val config = AegisConfig( cellEndpoints = listOf( CellEndpoint(CellRegion.AP_SOUTH, "https://ap-south.api.aegis.ai", priority = 10), CellEndpoint(CellRegion.EU_CENTRAL, "https://eu-central.api.aegis.ai", priority = 20), ), preferredRegion = CellRegion.AP_SOUTH, autoRegionDetection = false, workspaceId = "ws_abc123", )

/v1/sdk/bootstrap handshake (Phase 1, SDK ≥1.1.0)

Aegis.bootstrap() performs the canonical handshake against your control-plane. On success the EventGovernanceHint is auto-applied to the NameGovernor.

val result = Aegis.bootstrap() // throws BootstrapError on 401/403 // result.propertyId, result.workspaceId, result.locationCodes

What’s next