Docs

Install Trust for Android

Install

Three edits: the repository, the dependency and the routing key.

settings.gradle.kts: Repository published at release.

app/build.gradle.kts

dependencies {
    implementation("io.adhedge:install-trust:1.0.0")
}

AndroidManifest.xml, with the routing key from SDK setup. It is public and ships in your APK.

<application>
    <meta-data
        android:name="io.adhedge.installtrust.ROUTING_KEY"
        android:value="YOUR_ROUTING_KEY" />
</application>

If the key is missing or wrong, no installs reach your app in the console.

No init call. Your coding agent can add it for you. Then link a Cloud project to your app in Play Console. We walk you through it.

Verify on a dev phone

Open any build with the SDK. Setup marks SDK seen, and the install shows on Installs within minutes.

A debug or sideloaded build shows as UNVERIFIED with PI_UNRECOGNIZED_VERSION, with source Non-Play or Unknown. Non-Play installs never count in the Trusted rate or the Overview totals. Reasons to expect:

Verdicts

VerdictMeaningBilled
TRUSTEDPlay verified the app and its license. Behavior flags alone never downgrade a phone with strong integrity.Yes
UNVERIFIEDOne red flag, or Play couldn't confirm it.No
SUSPECTRed flags from two different checks.No

Verdicts update as behavior comes in. Rooting alone never blocks an install. Play Games on PC is never blocked.

Reason codes

Red flags from two different checks make a SUSPECT. ROOT_HEURISTIC and PI_NO_DEVICE_INTEGRITY count as one check. CERT_UNKNOWN is its own check, so an unregistered signing key on a build Play didn't recognize counts as two.

CodeMeaningNegative
PI_NO_DEVICE_INTEGRITYNo device integrity from Play.Yes
PI_VIRTUAL_DEVICEPlay Games on PC.No
PI_UNRECOGNIZED_VERSIONPlay doesn't recognize the app binary.Yes
PI_UNLICENSEDNo Play license for the install.Yes
PI_ACCESS_RISK_CONTROLLINGAnother app can control the screen.Yes
INSTALLER_NOT_PLAYNot installed by Play.Yes
CERT_UNKNOWNUnknown signing certificate.Yes
VERSION_NOT_PLAY_SERVEDA build Play isn't serving.Yes
ADB_ENABLEDUSB debugging on.Yes
TEST_KEYSTest-keys system build.Yes
EMULATOR_HEURISTICLooks like an emulator.Yes
ROOT_HEURISTICSigns of root.Yes
NO_INTERACTION_D0No taps on install day.Yes
LOW_INPUT_ENTROPYTap timing less varied than your app's baseline. Needs 20 taps.Yes
REINSTALL_90DReinstall within 90 days.Yes
LEGACY_COHORTInstalled before the SDK.No
SAMPLED_OUTNot sampled for attestation.No
LABEL_REAL_USERYour app labeled it real_user. Never used in the verdict.No

LOW_INPUT_ENTROPY fires only with at least 20 install-day taps and a tap interval variance below your app's baseline. The baseline is the 5th percentile of that variance over the prior 28 days of installs with 20 or more taps, and needs 100 such installs.

One in-app behavior code (LOW_INPUT_ENTROPY or NO_INTERACTION_D0) never moves an install off TRUSTED when Play recognized the app, the install is licensed and the device has strong integrity. The code stays listed.

Conversion events

Integration status

ToolTagged modeBlocking mode needsState
Firebase / GA4App firesFirebase app ID and GA4 API secret. Firebase has seen the device. Sent within 48 hours.Not tested on a live campaign yet
AppsFlyerApp firesS2S token. Your app's advertising ID for Google Ads postbacks.Not tested on a live campaign yet
AdjustApp fires, with event tokensS2S token, event tokens, your app's advertising ID and the device IP.Not tested on a live campaign yet
MetaApp firesBlocking won't send conversions to Meta.No server path
TikTokApp firesBlocking won't send conversions to TikTok.No server path

The advertising ID is sent only if your app already declares it, and never when the user deleted it or limited ad tracking. Our server forwards it to your account and stores none of it.

Label real users

Call InstallTrust.label("real_user") when a user hits a milestone only people reach, like finishing onboarding.

Labels grade the Ad protection checks. They never change a verdict.

Ad request gate

On Pro, call InstallTrust.shouldRequestAds() before each ad load call. It answers on the device, instantly.

Gated: new installs from Play or Unknown with a final SUSPECT verdict, which takes red flags from two different checks. Rooting and no device integrity are one check. An unregistered signing key is its own check, so a build Play didn't recognize signed with an unregistered key counts as two. Never Non-Play, existing users or pending installs. Phones pick up changes within an hour.

if (InstallTrust.shouldRequestAds()) {
    interstitialAd.loadAd()
}

The gate is part of blocking mode. One switch on Ad protection turns on both: verified_install sent from our server, and shouldRequestAds() gating. There is no separate ad gate switch. Turn blocking off anytime; devices pick it up within an hour.

Reconciliation

Upload any network's install report. We compare it with the first opens the SDK saw.

ColumnAccepted headersRequired
DateDay, Date, Event dateYes
InstallsInstalls, App installs, Conversions, Conv., All conv.Yes
CampaignCampaign, Campaign name, Source / mediumNo
CostAny header starting with CostNo

What the SDK sends

The install record below is sent once per install. A field is empty when the Android version can't provide it. No permissions beyond network access. The behavior group is the exception: it is not sent with the install record, and it is described in full under Behavior.

Behavior

Batched on device and posted separately from the install record: at most 20 events, held at most 7 days, sent on the SDK's background thread. Kept 90 days. Play purposes: Fraud prevention, security, and compliance; Analytics. The collector runs only while tier 4 is on in the app's SDK config.

FieldWhat it isAndroid sourceSent
Taps in a sessionHow many touches the session had, as a number. On SESSION_END. No coordinates, no view, no content.Window.Callback, ACTION_DOWN countPer session
Session lengthHow long the session lasted, in milliseconds. On SESSION_END. The variance of tap intervals is derived on our side from these pairs.ActivityLifecycleCallbacksPer session
First-touch delayMilliseconds from process start to the first touch after install. On FIRST_INTERACTION.SystemClock.elapsedRealtimeOnce
Next-day returnThat the app was opened again on the day after the install day. On D1_RETURN. One flag. No session count.Install day + 1, on deviceOnce
Event timeThe device clock when the event happened, epoch milliseconds. Our server also records when it arrived.System.currentTimeMillisPer event
Conversion adapter resultWhich adapter fired your conversion and whether the vendor SDK accepted the call. On ADAPTER_FIRED.Adapter return valuePer fire
Late referrerThe Play Install Referrer fields above, when Play returned them after the install record was already sent. On REFERRER_LATE.InstallReferrerClientAt most once

Nothing else rides on these events. There is no coordinate, no view ID, no screen name, no text, and no free-text field of any kind.

How taps are counted, and what it costs

The SDK registers one Application.ActivityLifecycleCallbacks and, as each Activity is created, wraps that Activity's Window.Callback. There is no View.OnTouchListener on your views, no accessibility service, and no input injection.

On the dispatch path the wrapper does two things per event: increments an int when the action is ACTION_DOWN, then calls through to the callback that was there before. It allocates nothing per event, reads no field of the MotionEvent besides the action, holds no reference to it after the call returns, and touches no disk or network. Batching, storage and sending happen on the SDK's single background thread, never in dispatch.

A measured per-event figure on reference devices is not published yet; that benchmark is the open item on this path. If your app needs one before code review, ask us and we will run it against your build. Under React Native and Flutter the same native wrapper does the counting, and neither wrapper adds anything on the JavaScript or Dart side.

Each field with its Android source, and what we never collect: data safety.

For code review

License

The SDK, its wrappers, and the wire schema are Apache-2.0. Scoring runs on our side.