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), optionalloggercallback (levels: warning, error) for crash-reporting wiring. - Direct deep links: parse App Links / custom scheme URLs matching
/\{orgKey}/\{shortCode}, resolve via backend, emit typedOptoLinkData. - 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 (
_pendingDeferredsemantics). - Device identity: SDK-generated UUID
deviceIdpersisted on-device; autoPOST /sdk/sessionregistration;setIdentity(externalId)/clearIdentity()rebind to anEndUser. - Events: explicit
trackEvent(name, properties)only — the SDK never auto-fires events. - Fingerprint (5-field whitelist): language, timezone, osVersion, deviceModel, platform.
2. The backend surface the SDK consumes (exact, verified in optolink-backend/src/)
| Route | Auth | Body / Response | Rate 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/) | none | link 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 / clear | CLIENT tier | Set/ClearIdentityDto → \{endUserId, rebound} / \{cleared} | 60/min per key |
POST /sdk/events (app-event/sdk-events.controller.ts) | CLIENT tier | TrackEventDto (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.jsonand 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: oneOptoLinkError(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 addeddynamic_app_link_componentsin 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_guaranteedonly 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 explicitinit()+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'slaunchReceivedDeeplink(Uri): Booleanlets 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.startupinitializer instead of forcing Application subclassing. - Platform: minSdk 23 is the modern library recommendation (competitor floor is 21); explicit API mode;
binary-compat-validatorin 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)
- 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.
- minSdk: 21 (competitor floor + Flutter parity) vs 23 (modern).
- HTTP stack: OkHttp (one dep, standard) vs HttpURLConnection (zero-dep ethos, Node precedent).
- API style: suspend + Flow with typed sealed
OptoLinkResult(modern) vs listener callbacks (competitor-compat). Flutter'sonLinkstream +getInitialLink()maps naturally to Flow + suspend. - Init shape: explicit
OptoLink.initialize(config)in Application (Flutter parity) vs auto-init via androidx.startup / ContentProvider (Branch-style, footgun-prone). - Multi-store referrer support: Google-only (simple) vs Huawei/Samsung/Xiaomi plugins (module sprawl).
- Device identifier: random UUID in SharedPreferences/DataStore (current Flutter behavior) vs App Set ID augmentation.
- Packaging: single
optolink-androidAAR vs core +optolink-referrermodule split. Git dependency (Flutter-style) vs Maven Central. - Language interop: Kotlin-only with
@JvmOverloads/compat surface, or design for Java consumers too.
6. Recommended next step
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.