Skip to main content

Android Native SDK — Kickoff Research Brief

Synthesis of 4 parallel research streams (2026-09-22), feeding the kickoff of optolink-android/ (Kotlin, Maven-distributed). Sources: Flutter SDK code study · Node SDK + backend API study · competitor web research (android-sdk-competitive-research.md, same dir) · Android engineering best-practice web research.


1. What the Android SDK must do (behavior contract from the Flutter sibling)​

The Flutter SDK (~1,300 lines Dart) defines the product behavior an Android SDK should mirror:

  • Init: OptoLink.initialize(config) — config = apiKey (opl_…), orgKey (4-char org key in every short URL), baseUrl (resolution domain), timeout (10s), retries (2, exponential backoff), optional logger callback (levels: warning, error) for crash-reporting wiring.
  • Direct deep links: parse App Links / custom scheme URLs matching /\{orgKey}/\{shortCode}, resolve via backend, emit typed OptoLinkData.
  • Deferred deep links: on first launch, match ladder against the backend POST /match: P1 clipboard token → P1.5 install referrer (opl_… token from Play Install Referrer) → P2 decomposed fingerprint exact → P2.5 weighted confidence scoring → P3 IP-only fuzzy. Every match carries a confidence level (exact / high / medium / low) + matchMethod (CLIPBOARD | INSTALL_REFERRER | FINGERPRINT_EXACT | FINGERPRINT_SCORED | IP_FUZZY | NONE).
  • Pending-deferred replay: deferred result is held and delivered on first listener subscription (_pendingDeferred semantics).
  • Device identity: SDK-generated UUID deviceId persisted on-device; auto POST /sdk/session registration; setIdentity(externalId) / clearIdentity() rebind to an EndUser.
  • Events: explicit trackEvent(name, properties) only — the SDK never auto-fires events.
  • Fingerprint (5-field whitelist): language, timezone, osVersion, deviceModel, platform.
RouteAuthBody / ResponseRate limit
POST /match (resolution/)none (X-API-Key soft)MatchRequestDto (clipboardToken?, deviceId?, installReferrer?, fingerprint fields) → \{matched, matchMethod, matchConfidence, matchScore?, linkId, path, params}30/min per IP
GET /:orgKey/:shortCode + /data (resolution/)nonelink data \{linkId, path, params}500/sec per IP
POST /sdk/session (identity/sdk.controller.ts)Bearer opl_sdk_… (CLIENT tier)DeviceSessionDto → \{ok, deviceId, firstSeenAt, lastSeenAt}60/min per key
POST /sdk/identity / clearCLIENT tierSet/ClearIdentityDto → \{endUserId, rebound} / \{cleared}60/min per key
POST /sdk/events (app-event/sdk-events.controller.ts)CLIENT tierTrackEventDto (eventName, properties ≤10,240 bytes, rejected not truncated) → \{ok}600/min per key
  • Key tier: mobile SDKs use the CLIENT tier (opl_sdk_…) — a SERVER key (opl_api_…) auths but gets 403 on /sdk/*. CLIENT keys stay visible in the portal and keep authenticating 24h after rotation (grace for shipped apps).
  • Click tracking: 100% server-side (resolution endpoints fire it). SDK contributes only realistic headers/UA and deviceId. The SDK does not call any tracking endpoint.
  • AppConfig (ANDROID): bundleId, storeUrl, sha256Fingerprints[], uriScheme?, fallbackWebUrl? — consumed by the backend when serving /.well-known/assetlinks.json and the redirect page. The SDK never fetches it.
  • Retry precedent (Node SDK): 429 → honor retryAfterSeconds; connection errors always retry; 5xx retries reads only; shared retry budget across chained requests. Defaults: timeout 10s, retries 2. Error hierarchy: one OptoLinkError(status, message, requestId?, retryAfterSeconds?) + OptoLinkConnectionError (status 0).

3. What competitors do (full detail in android-sdk-competitive-research.md)​

  • Deferred deep linking 2025/26: Play Install Referrer API is the deterministic backbone — not deprecated, fraud-hardened (Kochava/vmobify). Vendors read multiple store referrers (Branch: Google/Huawei/Samsung/Xiaomi; Adjust: plugin modules; AppsFlyer: + Meta referrer).
  • App Links: verified (autoVerify + assetlinks.json) opens directly. Android 15 added dynamic_app_link_components in assetlinks.json (server-side path matchers) — Google's Oct-2025 "preferred way to link" post-FDL.
  • Clipboard matching is dead: Android 10+ blocks background clipboard; Android 12 shows a system toast on reads; Android 14 timing rules broke late-read DDL. Only first-launch-foreground reads are technically possible.
  • Fingerprinting: permitted as fallback on Android (unlike iOS), but Play policy caps it (no bridging ad-ID resets, no persistent-ID linking). Branch exposes +match_guaranteed only at 100% confidence.
  • FDL shutdown (Aug 2025): Google built no first-party DDL replacement — the whole market moved to vendors. Validation for OptoLink's niche.
  • Init/lifecycle: all init in Application.onCreate; Branch's auto-init is a documented footgun (ERR_BRANCH_ALREADY_INITIALIZED); AppsFlyer V7 moved to explicit init() + start(); install-vs-open resolved server-side, exposed via typed callbacks.
  • API style split: params-map listeners (Branch JSONObject) vs typed result objects (AppsFlyer V7 DeepLinkResult — the modern fix; Adjust's launchReceivedDeeplink(Uri): Boolean lets the app control routing).
  • Footprint floor: minSdk 21 across Branch/Adjust/AppsFlyer/Kochava; converged pattern = lean core + optional modules, no UI deps, no auto-added permissions.
  • Criticisms to design against: FTC v. Kochava, "Out of Control" oversharing report, Play rejections blamed on embedded SDKs, opaque hash-named blobs (auditability), stringly-typed payloads, manifest-merge friction.

4. Android engineering best practice (2025/26)​

  • Kotlin-first API: suspend functions + Flow (no bare listener interfaces); callbackFlow to bridge legacy. Sealed result types over exceptions for expected outcomes.
  • Init: lazy/config-object init, optional androidx.startup initializer instead of forcing Application subclassing.
  • Platform: minSdk 23 is the modern library recommendation (competitor floor is 21); explicit API mode; binary-compat-validator in CI for API stability; dokka for KDoc publishing.
  • Distribution: Maven Central via vanniktech plugin (Central Portal); consumer ProGuard/R8 rules shipped in the artifact; semver + changelog gate.
  • HTTP: OkHttp is the accepted single dependency for network SDKs (zero-dep HttpURLConnection possible but bare); no extra deps beyond that.
  • Privacy: attribution/analytics is a non-ads use case → App Set ID or own install ID, never GAID; don't read clipboard (policy risk, toast UX); Play Data safety form implications for any collected identifier.

5. Open decisions before design (the kickoff agenda)​

  1. Clipboard leg: keep (parity with Flutter; works only first-launch-foreground, toast on 12+) or drop (privacy-clean, Google-advised-against)? Backend keeps supporting it regardless.
  2. minSdk: 21 (competitor floor + Flutter parity) vs 23 (modern).
  3. HTTP stack: OkHttp (one dep, standard) vs HttpURLConnection (zero-dep ethos, Node precedent).
  4. API style: suspend + Flow with typed sealed OptoLinkResult (modern) vs listener callbacks (competitor-compat). Flutter's onLink stream + getInitialLink() maps naturally to Flow + suspend.
  5. Init shape: explicit OptoLink.initialize(config) in Application (Flutter parity) vs auto-init via androidx.startup / ContentProvider (Branch-style, footgun-prone).
  6. Multi-store referrer support: Google-only (simple) vs Huawei/Samsung/Xiaomi plugins (module sprawl).
  7. Device identifier: random UUID in SharedPreferences/DataStore (current Flutter behavior) vs App Set ID augmentation.
  8. Packaging: single optolink-android AAR vs core + optolink-referrer module split. Git dependency (Flutter-style) vs Maven Central.
  9. Language interop: Kotlin-only with @JvmOverloads/compat surface, or design for Java consumers too.

Chart the wayfinder map for this effort: destination = approved Android SDK spec (API surface + architecture + packaging plan), with the open decisions above as the first grilling tickets.