Competitive Research: Android SDK Design of Deep-Linking / Attribution Platforms
Web research for the OptoLink native Android SDK. Vendors: Branch, AppsFlyer, Adjust, Singular, Kochava + the post-Firebase-Dynamic-Links landscape. Researched 2026; sources inline. Verified against vendor SDK source code where noted.
1. Deferred deep linking mechanisms on Android (2025/2026)
1.1 Play Install Referrer API — still the deterministic backbone, NOT deprecated
- The API is an AIDL service exposed by the Play Store app (v8.3.73+). The client library (
com.android.installreferrer:installreferrer:2.2) is a thin wrapper over it. Official page live and updated 2026-02-26 — no deprecation notice. https://developer.android.com/google/play/installreferrer/library - Returns: referrer URL, referrer-click timestamp, install-begin timestamp (both client- and server-side), instant-experience flag, app version at install. https://developer.android.com/google/play/installreferrer
- Reliability rules: referrer is valid 90 days, never changes unless the app is reinstalled; Google explicitly says to call it once on first execution, then
endConnection()to avoid leaks/perf problems. https://developer.android.com/google/play/installreferrer/library - Fraud: it replaced the deprecated
INSTALL_REFERRERbroadcast, which carried no timestamps and could be intercepted/spoofed by any app. The API's install-begin timestamp is what lets MMPs kill click injection (install seconds after a click on another app). Kochava's measurement of the API: click-injection fraud largely died after adoption. https://www.kochava.com/blog/click-injection-unmasked-an-impact-assessment-of-googles-install-referrer-api/ , https://vmobify.com/blog/google-play-install-referrer/ — click flooding (fake clicks before organic installs) remains the residual fraud vector since a valid-but-unrelated referrer overwrites attribution. - Referrer ecosystem beyond Play (all major SDKs read multiple stores):
- Branch reads Google Play + Huawei AppGallery + Samsung Galaxy Store + Xiaomi GetApps referrers in parallel (Kotlin coroutines, per-store
compileOnlydeps so app size grows only for the stores you ship to). Source code:InstallReferrers.ktin https://github.com/BranchMetrics/android-branch-deep-linking-attribution ; compatibility table: https://help.branch.io/developer-hub/docs/implement-attribution-methods - Adjust ships the same set as optional plugin modules (
sdk-plugin-huawei-referrer,sdk-plugin-samsung-referrer,sdk-plugin-xiaomi-referrer,sdk-plugin-vivo-referrer,sdk-plugin-meta-referrer). https://github.com/adjust/android_sdk - AppsFlyer: Google referrer is an app-side dependency; Samsung built-in since v6.1.1; Huawei needs
componentverifysdk(v6.14+); Meta install referrer read via the Facebook/Instagram app's Content Provider (needs FB App ID in manifest). https://dev.appsflyer.com/hc/docs/install-android-sdk
- Branch reads Google Play + Huawei AppGallery + Samsung Galaxy Store + Xiaomi GetApps referrers in parallel (Kotlin coroutines, per-store
1.2 App Links (verified vs unverified) + Android 15 "Dynamic App Links"
- Verified App Links (
autoVerify="true"+assetlinks.jsononhttps://domain/.well-known/assetlinks.json, SHA-256 cert fingerprints) open links directly with no disambiguation dialog; unverified links fall back to the chooser/browser. Verification can be manually re-triggered on Android 12+. https://developer.android.com/training/app-links/verify-applinks - New in Android 15+ (API 35): Dynamic App Links —
assetlinks.jsongained adynamic_app_link_componentsrelation extension: server-side path/fragment/query matchers and exclusions, periodically fetched and merged with static manifest filters. Devices ≤ Android 14 ignore the extension; malformed rules fall back to manifest-only. Rules can narrow but never expand manifest scope. https://developer.android.com/training/app-links/configure-assetlinks — Google's official announcement (Oct 2025) calls Dynamic App Links "the preferred way to link to content within your app," positioning it as the platform answer to FDL's death for the installed-app case. https://android-developers.googleblog.com/2025/10/dynamic-app-links-elevating-your.html - Design implication for OptoLink: host
assetlinks.json(with dynamic rules) from the link domain itself — this is exactly what Branch/AppsFlyer custom domains do.
1.3 Fingerprinting fallback
- Branch's ladder: install referrers (deterministic) → prior cookie↔device-ID pairing on their graph → "browser-to-app snapshot comparison" (IP v4/v6, OS, OS version, device model, user agent) as last resort.
+match_guaranteed=truein the init payload only when 100% confident; docs tell developers to gate sensitive actions (auto-login, PNR) on that flag. https://help.branch.io/developer-hub/docs/implement-attribution-methods - Airbridge: Android uniquely permits probabilistic fallback when the referrer is missing (Apple banned fingerprinting in iOS 14.5) — this is why Android DDL match rates are structurally better. https://www.airbridge.io/en/blog/deferred-deeplink-implementation-guide-for-ios-android
- Policy ceiling: Google Play prohibits bridging Advertising-ID resets via fingerprint and linking persistent identifiers (IMEI etc.) to sensitive data; the app developer is on the hook for what embedded SDKs do. https://support.google.com/googleplay/android-developer/answer/6048248 , https://support.google.com/googleplay/android-developer/answer/10144311 , https://developer.android.com/identity/user-data-ids
1.4 Clipboard — dead as a background channel
- Android 10+: "Unless your app is the default IME or is the app that currently has focus, your app cannot access clipboard data." Clipboard-based DDL (used heavily on iOS pre-ATT) is not viable on Android outside the focused app. https://developer.android.com/about/versions/10/privacy/changes
- Branch's iOS NativeLink™ (pasteboard-based, 100% match) is iOS-only; on Android their answer is install referrers. https://help.branch.io/developer-hub/docs/implement-attribution-methods
- Android 14 tightened timing/foreground-service behavior around first-launch reads, breaking old DDL patterns that read referrer/clipboard too late (secondary source, practitioner account). https://linkrunner.io/blog/android-14-deferred-deep-links
1.5 Post-Firebase-Dynamic-Links landscape
- FDL shut down Aug 25, 2025; all links (page.link + custom domains) return 404; Short Links & Link Stats APIs dead. https://firebase.google.com/support/dynamic-links-faq
- Google's official recommendation: for full feature parity use third-party vendors — named list: Adjust, Airbridge, AppsFlyer, Bitly, Branch, Kochava, Singular ("not vetted by Google"); for installed-app-only deep linking use App Links + Universal Links (migration guide provided). https://firebase.google.com/support/dynamic-links-faq , https://firebase.google.com/support/guides/app-links-universal-links
- Google built Dynamic App Links (see 1.2) as the platform answer but provides no first-party deferred deep linking — the platform guide's feature table leaves DDL unsatisfied ("handle deferred deep linking, or accept the loss" per third-party migration guides). https://link.boo/guides/firebase-dynamic-links-migration
- Vendor capture of the migration wave: AppsFlyer published an FDL-deprecation pitch (https://www.appsflyer.com/blog/mobile-marketing/fdl-deprecation-deep-linking/); Branch published a migration guide + accepts Firebase metadata export (https://help.branch.io/developer-hub/docs/migrating-firebase-dynamic-links-to-branch-links). Firebase provides a metadata export API to ease vendor migration. https://firebase.google.com/support/guides/export-dynamic-links
2. SDK init & lifecycle patterns
| Concern | Pattern observed (all five vendors) |
|---|---|
| Init location | Application.onCreate — every vendor. AppsFlyer: "recommended to initialize in the global Application class … to ensure the SDK can start in any scenario (for example, deep linking)". https://dev.appsflyer.com/hc/docs/integrate-android-sdk |
| Init shape | Config-object constructor + start, OR singleton accessor. Branch: Branch.getAutoInstance(this) in Application (https://help.branch.io/developer-hub/docs/android-basic-integration). AppsFlyer V7: init(devKey, …) in Application + explicit start() (start takes no args in V7 — dev key comes from init). https://dev.appsflyer.com/hc/docs/migrate-android-sdk-to-v7 . Adjust: Adjust.initSdk(AdjustConfig(ctx, appToken, env)) (verified in source: Adjust.java). https://github.com/adjust/android_sdk |
| Auto vs manual session init | Branch defaults to AUTO: an ActivityLifecycleObserver fires initSession (network v1/open) when the first activity reaches RESUMED — a failsafe "not to miss tracking sessions." Disabling it is an explicit opt-out (Branch.troubleshooting documents the resulting ERR_BRANCH_ALREADY_INITIALIZED conflicts when apps ALSO init manually). https://help.branch.io/developers-hub/docs/android-troubleshooting . Manual lifecycle style: initSession() in onStart(), closeSession() in onStop(). https://help.branch.io/developers-hub/docs/android-troubleshooting |
| Session definition | Branch: a session = app launched (cold start or resumed from task manager) and brought to foreground; the v1/open request is tied to foregrounding. Handling a new deep link clears session data and creates a new referred OPEN; sessionBuilder()...reInit() exists for re-init without losing state (in-place activity). https://help.branch.io/apidocs/requestopen , https://help.branch.io/developer-hub/docs/android-advanced-features |
| Install vs open distinction | Resolved server-side, exposed via callbacks: AppsFlyer onConversionDataSuccess(Map) fires on first open and every foreground, with is_first_launch key distinguishing install from open. https://dev.appsflyer.com/hc/docs/conversion-data-android . Branch returns +is_first_session-style flags plus +match_guaranteed in the init params JSONObject. https://help.branch.io/developer-hub/docs/implement-attribution-methods |
| Foreground/background re-attribution | AppsFlyer: conversion data re-delivered whenever app moves to foreground (same callback). https://dev.appsflyer.com/hc/docs/conversion-data-android . Branch: each foreground = new session/open; new Branch link = new referred open. (see session rows above) |
| Delayed init | Branch supports delaying init (e.g., to attach request metadata), with warnings about the auto-init race. https://help.branch.io/developer-hub/docs/android-advanced-features . AppsFlyer V7 introduced an explicit "session control model — you control when to start the first session" (iOS doc; Android aligned via start()). https://dev.appsflyer.com/hc/hc/docs/migrate-ios-sdk-to-v7 |
| Deferred-link delivery | All vendors deliver DDL through the same init callback (Branch) or a config-registered listener (Adjust OnDeferredDeeplinkResponseListener, Singular SingularLinkHandler, Kochava processDeeplink(null) check) — DDL is not a separate API surface from session init. Sources below in §3. |
| Out-of-the-box tracking | Branch auto-tracks clicks, opens, installs, reinstalls and impressions unless disabled. https://help.branch.io/developer-hub/docs/android-advanced-features |
3. API design for deep link routing, conversion & events
3.1 The two routing styles
A. Listener with params map (Branch, AppsFlyer, Kochava) — SDK hands the app a key/value payload; the app does routing:
- Branch:
BranchReferralInitListener \{ onInitFinished(JSONObject referringParams, BranchError error) }— a rawJSONObjectof link data + nullable error. Convenience gettergetLatestReferringParams(). Verified in source (Branch.java). https://github.com/BranchMetrics/android-branch-deep-linking-attribution , https://help.branch.io/developer-hub/docs/android-full-reference - AppsFlyer (legacy, V6):
AppsFlyerConversionListenerwith four methods you must override or it won't compile:onConversionDataSuccess(Map<String,Object>),onConversionDataFail,onAppOpenAttribution(Map),onAttributionFailure. https://dev.appsflyer.com/hc/docs/android-sdk-reference-appsflyerconversionlistener - Kochava:
processDeeplink(intent)for opens; passing null/empty URI queries for a deferred deeplink; v7 addedeventAutoSendparam controlling whether a_Deeplinkevent is auto-logged. https://support.kochava.com/articles/sdk-integration/android-sdk-integration/21013-android-using-the-sdk/
B. Typed result objects (AppsFlyer V7 "Unified Deep Linking", Adjust, Singular):
- AppsFlyer V7:
subscribeForDeepLink(DeepLinkListener)beforestart(); callbackonDeepLinking(DeepLinkResult)whereDeepLinkResulthasgetStatus()(FOUND/NOT_FOUND/ERROR) + typedDeepLink; UDL is now the required path (onAppOpenAttribution removed in V7). https://dev.appsflyer.com/hc/docs/dl_android_unified_deep_linking , https://dev.appsflyer.com/hc/docs/migrate-android-sdk-to-v7 - Adjust:
OnDeferredDeeplinkResponseListener \{ boolean launchReceivedDeeplink(Uri deeplink) }— returns whether Adjust should launch the resolved URI (return false to let the app route manually). Verified in source. Attribution is a separate typed callback:OnAttributionChangedListener(AdjustAttribution); sessions haveOnEventTrackingSucceeded/Failed,AdjustSessionSuccess/Failuretypes. https://github.com/adjust/android_sdk (sdk-core sources) - Singular:
SingularConfig.withSingularLink(intent, SingularLinkHandler)— resolves short links with a 10-second default timeout, callback carries deeplink + passthrough params + isDeferred flag; the same call handles deferred deep links (no separate DDL handler needed since the newer SDK versions).withOpenURI(URI)covers custom-scheme opens. https://support.singular.net/hc/en-us/articles/360037581952-Android-SDK-Basic-Integration , https://support.singular.net/hc/en-us/articles/35356520601755-Android-SDK-Supporting-Deep-Links
3.2 Conversion / attribution callbacks
- Branch: single init callback carries everything (
+match_guaranteed,+is_first_session, campaign data, referring link). No separate "attribution changed" stream in the core flow (state listener exists:BranchReferralStateChangedListener). https://help.branch.io/developer-hub/docs/android-full-reference - AppsFlyer: dedicated conversion listener (install) + UDL listener (opens); V7 removed the open-attribution duplication — one pipeline.
- Adjust: attribution is a first-class object (
AdjustAttribution: tracker/network/campaign/creative/adgroup/click label/cost type) delivered on change; alsoOnLastDeeplinkReadListener. https://github.com/adjust/android_sdk - Adjust re-attribution is explicit:
Adjust.processDeeplink(AdjustDeeplink, context)(verified in source) — app decides when a deeplink counts as a re-attribution touch.
3.3 Event logging APIs
- Branch:
BranchEvent(BRANCH_STANDARD_EVENT)for standard enums orBranchEvent(String)for custom; builder setters (incl. revenue, custom data,BranchUniversalObjectcontent attachment) thenlogEvent(). Verified in source (util/BranchEvent.java). https://github.com/BranchMetrics/android-branch-deep-linking-attribution - AppsFlyer:
AppsFlyerLib.getInstance().logEvent(context, eventName, Map)withAFInAppEventTypeconstants. https://dev.appsflyer.com/hc/docs/android-sdk-reference-appsflyerlib - Adjust:
AdjustEvent("eventToken")+setRevenue(double, currency),addCallbackParameter,addPartnerParameter, then staticAdjust.trackEvent(event). Verified in source. https://github.com/adjust/android_sdk - Takeaway for OptoLink: typed event objects (Adjust-style) give compile-time safety and Swagger-able shapes; string+map (AppsFlyer/Branch) is faster to ship but stringly-typed. Note OptoLink's Node SDK already uses typed link objects — mirror that.
4. Footprint: size, dependencies, minSdk, ProGuard
| SDK | minSdk | Dependencies (hard) | Notable optional/compileOnly deps | ProGuard |
|---|---|---|---|---|
| Branch 5.21.2 | 21 (gradle.properties ANDROID_BUILD_SDK_VERSION_MINIMUM=21) | Kotlin stdlib + coroutines, androidx.annotation, androidx.constraintlayout (!), com.android.installreferrer:installreferrer:2.2 (bundled as implementation) | GMS ads-identifier (AAID), Huawei OAID + referrer, Samsung referrer, Xiaomi referrer, Play Billing (v8, for purchase validation), androidx.browser (Custom Tabs) — all compileOnly, app opts in | Consumer rules are only -dontwarn lines (Huawei/MiUI/Samsung/GMS/billing); keeps nothing by default. Verified in repo: proguard-consumer.txt. https://github.com/BranchMetrics/android-branch-deep-linking-attribution |
| Adjust 5.8.0 | 21 (coreMinSdkVersion=21, compileSdk 37) | Core is self-contained (Java) | Same store-referrer set + OAID + IMEI + Google LVL + webbridge as separate plugin artifacts — core stays small | Ships full consumer keep rules (-keep public class com.adjust.sdk.** \{*;} + GMS/installreferrer keeps). MIT license. https://github.com/adjust/android_sdk |
| AppsFlyer V7 (7.x) | 21 (V6 line went to 19) — "SDK V7 requires API level 21 or higher"; also requires Kotlin 2.0+ in the app | com.appsflyer:af-android-sdk (single AAR; pulls play-services-integrity since 6.17.1 — excludable for non-Play distribution) | installreferrer 2.2 (app adds), Huawei componentverifysdk, Xiaomi homereferrer; Samsung built-in; play-services-appset for AppSet ID (auto-collect since 6.17.0, disableAppSetId() opt-out) | -keep class com.appsflyer.** \{ *; } + kotlin.jvm.internal keep. Also: SDK manifest auto-adds AD_ID permission since 6.8.0 (children apps must revoke it), excludes its prefs from backup via manifest merge (documented tools:replace conflicts), and requires hash-named asset files in the APK or network calls fail. https://dev.appsflyer.com/hc/docs/install-android-sdk-7 , https://dev.appsflyer.com/hc/docs/install-android-sdk |
| Kochava v7 | 21 ("minimum supported Android API level increased from 16 to 21") | com.kochava.measurement:base (mavenCentral) + opt-in feature modules | Store-referrer modules etc. — "add the base tracker dependency and any desired feature dependencies" | Docs-driven; modular keeps base lean. https://support.kochava.com/articles/sdk-integration/20986-android-sdk-integration/ , https://github.com/Kochava/android-kochavameasurement-releases |
| Singular 12.15.0 | not confirmed in accessible docs | Single SDK via custom Maven repo | — | See https://support.singular.net/hc/en-us/articles/360037581952-Android-SDK-Basic-Integration |
Design signals: (1) minSdk 21 is the converged floor (Kochava raised 16→21; AppsFlyer V7 19→21; Branch/Adjust at 21). (2) The modern footprint pattern is lean core + optional modules/compileOnly deps for store referrers & ad IDs — Branch and Kochava and Adjust all do it. (3) Branch's constraintlayout as a required implementation dependency is a bloat red flag — don't ship UI deps from a measurement SDK. (4) Nobody publishes byte-size promises anymore; module structure is the size story.
5. Criticisms & complaints worth designing against
- FTC v. Kochava (2022–2026): FTC sued Kochava for selling geolocation data traceable to sensitive locations (reproductive-health clinics, places of worship, shelters). Motion to dismiss denied Feb 2024; May 2026 stipulated order bans selling/sharing sensitive location data without consent. Lesson: an SDK that monetizes location/behavioral data invites regulatory action — keep OptoLink's SDK to click/deeplink scope. https://www.ftc.gov/news-events/news/press-releases/2022/08/ftc-sues-kochava-selling-data-tracks-people-reproductive-health-clinics-places-worship-other , https://www.ftc.gov/legal-library/browse/cases-proceedings/ftc-v-kochava-inc , https://www.ftc.gov/news-events/news/press-releases/2026/05/ftc-ban-kochava-subsidiary-selling-sensitive-location-data-settle-charges-they-sold-location-data
- Privacy research (Norwegian Consumer Council, "Out of Control," 2020): AppsFlyer flagged for "systemic oversharing" — e.g., Tinder transmitting special-category data to AppsFlyer/LeanPlum; part of the GDPR complaint wave vs Grindr/Match Group. Lesson: minimize payload, document exactly what leaves the device. https://storage02.forbrukerradet.no/media/2020/01/2020-01-14-out-of-control-final-version.pdf , https://techcrunch.com/2020/01/14/dating-and-fertility-apps-among-those-snitching-to-out-of-control-adtech-report-finds/
- Google Play rejections blamed on attribution SDKs: developers report app updates rejected over Data-safety declarations tied to
com.appsflyer:af-android-sdkidentifiers collection; Play's SDK-requirements policy makes the app developer responsible for embedded SDK behavior. Lesson: OptoLink SDK should collect the minimum and ship a manifest-merge + Data-safety declaration guide. https://github.com/AppsFlyerSDK/appsflyer-react-native-plugin/issues/517 , https://support.google.com/googleplay/android-developer/answer/13323374 - Opaque/obfuscated internals: AppsFlyer requires mysterious hash-named files under
assets/com/appsflyer/internal(four filesa-…d-) for the SDK to function — unauditable blobs in the app package; combined with-keep com.appsflyer.**this is a common auditor complaint. Lesson: readable, inspectable SDK code; no hidden asset blobs. https://dev.appsflyer.com/hc/docs/install-android-sdk - Auto-init footguns: Branch's automatic session init (RESUMED failsafe) collides with manual init (
ERR_BRANCH_ALREADY_INITIALIZED, "SDK already initialized" errors) — a documented support topic. Lesson: pick ONE init story and make the failure mode explicit. https://help.branch.io/developers-hub/docs/android-troubleshooting - Stringly-typed payloads: Branch's
JSONObject+ AppsFlyer'sMap<String,Object>callbacks put the typing burden on the app dev (and produce "why is this key missing" support tickets). AppsFlyer's V7 rewrite toward typedDeepLinkResultis an acknowledgment. (§3 sources) - Integration friction: AppsFlyer's manifest backup-rule exclusions cause documented merge conflicts needing
tools:replaceedits in every integrating app. Lesson: declare minimal manifest entries; avoid forcing app manifest surgery. https://dev.appsflyer.com/hc/docs/install-android-sdk - Platform-policy drift risk: Play policy prohibits fingerprint-bridging of ad-ID resets and persistent-ID linking; SDKs that silently collect device signals (IP/UA/device model — Branch's snapshot comparison) are one policy update away from breaking their apps. Lesson: make probabilistic matching opt-in and isolated. https://support.google.com/googleplay/android-developer/answer/10144311 , https://developer.android.com/identity/user-data-ids
- Vendor-hosted link rot: FDL's Aug-2025 404 apocalypse stranded apps whose links lived on
page.link. Lesson for OptoLink: links must live on the customer's own domain (assetlinks.json hosted by/for the customer), so our platform can never orphan their URLs the way Firebase did. https://firebase.google.com/support/dynamic-links-faq - ATT note: ATT itself is iOS-only; the Android analogs are the AAID
AD_IDpermission (API 33+), user ad-ID resets, and Play's Data-safety declarations — these are where Android "ATT-style" complaints land (see #3, #8). https://dev.appsflyer.com/hc/docs/install-android-sdk
6. Implications for the OptoLink Android SDK (short list)
- DDL pipeline: click → store link with install-referrer payload → first-launch
InstallReferrerClientbind (once, with timeout, thenendConnection()) → server match → same callback as link opens. Referrer payload = the reliable channel; fingerprinting optional & opt-in. - Init:
OptoLink.init(context, config)in Application.onCreate; config-object pattern; one auto-session story (recommend Branch-style lifecycle auto-init ON, with a single documented escape hatch). - API: typed
DeepLinkResult-style callbacks (status + data object), Adjust-style boolean-return DDL hook for app-controlled routing, typed event builder (OptoLinkEvent) — skip the JSONObject/Map legacy style. - Packaging: single small core artifact, minSdk 21, zero UI dependencies, consumer ProGuard keep rules shipped, no manifest surprises, no auto permissions.
- Support dynamic App Links hosting (
assetlinks.jsonwithdynamic_app_link_components) from the customer link domain — it's Google's stated preferred mechanism and the vendors' custom-domain model.