TWA Integration
Trusted Web Activity (TWA) apps load a web UI inside a native Android shell. Unlike WebView with the WebBridge plugin, there is no stable official channel between native Android code and the loaded web page. The app must still behave like a native Android app for attribution, so install and session traffic must be sent from native code.
See the TWA example project and for a working reference implementation.
To build a Trusted Web Activity (TWA) in Android, you primarily rely on two official, complementary libraries provided by Google. Depending on whether you want a zero-code wrapper app, a highly customized hybrid app, or an offline-first experience, you can mix and match different implementation approaches.
The Core Libraries
To implement a TWA, you must include these dependencies in your app's build.gradle:
androidx.browser:browser: The Jetpack core support library providing low-level components for Custom Tabs and TWA protocols.com.google.androidbrowserhelper:androidbrowserhelper: Google's recommended abstraction library built on top of Jetpack. It handles most boilerplate code, splash screens, default browser fallbacks (like WebView), and token validation.
The Implementation Methods
There are three primary methods to construct and launch a TWA, ranging from declarative (no Java/Kotlin code) to fully manual.
1. The Declarative Manifest Method (No-Code Wrapper)
If you only need a wrapper app that launches your Progressive Web App (PWA) immediately when the app icon is clicked, you do not need to write any Kotlin or Java code. You simply declare Google's pre-built activity in your AndroidManifest.xml.
- Main Class:
com.google.androidbrowserhelper.trusted.LauncherActivity - How it works: You point your main launcher intent directly to this class in your manifest and supply metadata tags (like
android.support.customtabs.trusted.DEFAULT_URL) to configure your PWA's URL, splash screen colors, and orientation.
2. The Custom Launcher Activity Method (Hybrid / Logic Control)
If you need to perform actions before the web content loads—such as checking for network connectivity, loading native local configurations, or rendering an Android native offline fallback screen—you can subclass the helper library's launcher.
- Main Class: Extend
com.google.androidbrowserhelper.trusted.LauncherActivity - Core Methods used:
shouldLaunchImmediately(): Override this to returnfalseso the TWA doesn't open right away.launchTwa(): Manually invoke this method once your native setup or connectivity checks are complete.
3. The Fully Manual / Custom Activity Method
If you want to trigger the TWA inside a regular native Android layout (e.g., clicking a native button inside a MainActivity to launch the fullscreen web app), you use low-level intent builders. This is the manual approach you were recalling.
-
Main Classes:
-
androidx.browser.trusted.TrustedWebActivityIntentBuilder(The builder used to construct the custom TWA target intent). -
androidx.browser.customtabs.CustomTabsIntent(The underlying intent mechanism). -
How it works: You initialize the builder with your target URI, customize configurations (like status bar color), extract the intent, and use standard native code to start it:
// Customize your TWA here if needed
val url = Uri.parse("https://yourpwa.com")
val builder = TrustedWebActivityIntentBuilder(url)
val twaIntent = builder.build(customTabsSession)
TrustedWebUtils.launchAsTrustedWebActivity(this, twaIntent)
Basic Idea
According to the above, there are different patterns to build a TWA app, but the basics usually the same.
- Android native starts.
- It use a class or implement an interface from google TWA library to implement a TWA container.
- A url is set for the container as launching url to load the web UI. By default, the container starts as soon as possible it created and setting the url is a method overriding of the implemented interface.
- From there, there is no way for native and web to communicate each other.
As you can see, we have only one opportunity to pass some data from native-side to web-side and it's the launching url's query parameters.
So the general implementation should be respect this, because you need pass information like GAID (for Android) or IDFA (for iOS) to Adtrace web sdk to init it correctly and make the attribution and sending event completely from your web-side:
- Android native starts.
- It use a class or implement an interface from google TWA library to implement a TWA container.
- It's needed to disable/delayed the auto-launching behavior of the TWA parent class. By doing this, we have control on preparing native data to be sent through the query parameters.
- when we ensure all the parameters we need to pass are ready, we can launch/resume the twa activity.
- an url is set for the container including the required query parameters as launching url to load the web UI.
Due to the requirement for the parameters like GAID, you have to implement using manual code. so the method 1 can not be used if you want to use Adtrace.
So make sure you are using one of the methods Custom Launcher Activity or Fully Manual / Custom Activity.
This document covers the method Custom Launcher Activity as the recommended method due to its simplicity and low-code requiement.
Add Dependencies
add the following dependencies in your app's build.gradle:
dependencies {
implementation 'com.google.androidbrowserhelper:androidbrowserhelper:2.6.2'
implementation 'io.adtrace:android-sdk:2.6.1'
implementation 'com.google.android.gms:play-services-ads-identifier:18.2.0'
}
replace the versions with the latest versions available or the versions that compatible with you current gradle version.
Set proguard rules
if you use proguard in your project make sure you add these rules in your project's proguard-rules.pro
-keep class io.adtrace.sdk.** { *; }
-keep interface io.adtrace.sdk.** { *; }
-keep enum io.adtrace.sdk.** { *; }
-keep class com.google.android.gms.common.ConnectionResult {
int SUCCESS;
}
-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient {
com.google.android.gms.ads.identifier.AdvertisingIdClient$Info getAdvertisingIdInfo(android.content.Context);
}
-keep class com.google.android.gms.ads.identifier.AdvertisingIdClient$Info {
java.lang.String getId();
boolean isLimitAdTrackingEnabled();
}
Fetch native data and build launch url
In your application main launcher you need to get GAID (and other advertising ids, or push token) from native side and build the launching url with the query parameters included, before launching twa.
your activity should be like this:
package com.example.twa;
import android.net.Uri;
import android.os.Bundle;
import io.adtrace.sdk.AdTrace;
public class LauncherActivity extends com.google.androidbrowserhelper.trusted.LauncherActivity {
private String mGoogleAdId;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AdTrace.getGoogleAdId(this, googleAdId -> {
mGoogleAdId = googleAdId != null ? googleAdId : "";
launchTwa(); // only call `launchTwa` when all parameters you need. for example you may also want to fetch push token. so you can check whether both values is ready before calling `launchTwa`
});
}
@Override
protected boolean shouldLaunchImmediately() {
return false; // setting this to false will disable the auto-launch behavior of the launcher activity and give you control on the time of the TWA launch
}
@Override
protected Uri getLaunchingUrl() {
// this method will be launched after `launchTwa` is called.
Uri base = Uri.parse("YOUR_WEB_PAGE_BASE_URL");
if (mGoogleAdId == null) {
mGoogleAdId = "";
}
/**
* append your query parameters here, they will get, parsed and pass to the Adtrace web sdk in the web application side.
* the parameters name can be anything, you just need to use same key in native and web side.
*/
return base.buildUpon()
.appendQueryParameter("gps_adid", mGoogleAdId)
.build();
}
}
Create your main Application class in the project. it's should be a simple class and there is no need to implement any method for Adtrace integration unless you need custom implementation for own usage.implement
package com.example.twa;
import android.app.Application;
public class GlobalApplication extends Application {
}
Setup your application manifest
Then you need make sure you have configured these two class and the necessary permissions in your application manifest:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.twa">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="com.google.android.gms.permission.AD_ID" />
<application
android:name=".GlobalApplication"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:theme="@style/Theme.AppCompat.Light.NoActionBar"
android:usesCleartextTraffic="true">
<activity
android:name="com.example.twa.LauncherActivity"
android:exported="true"
android:launchMode="singleTask"
android:theme="@android:style/Theme.Translucent.NoTitleBar">
<meta-data
android:name="android.support.customtabs.trusted.DEFAULT_URL"
android:value="YOUR_WEB_PAGE_BASE_URL" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="YOUR_DEEP_LINK_BASE_URL" />
</intent-filter>
</activity>
<meta-data
android:name="asset_statements"
android:resource="@string/asset_statements" />
<meta-data
android:name="web_manifest_url"
android:value="YOUR_TWA_BASE_HOST_URL/manifest.json" />
<meta-data
android:name="twa_generator"
android:value="pwabuilder" />
</application>
</manifest>
Setup your application manifest.json
Create a file with name manifest.json in your application assets folder and put this content. modify its value according to your application information
{
"name": "TWA App",
"short_name": "TWA App",
"start_url": "/test/demo.html",
"scope": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#ffffff",
"icons": [
{
"src": "/icons/icon-192.svg",
"sizes": "192x192",
"type": "image/svg+xml"
},
{
"src": "/icons/icon-512.svg",
"sizes": "512x512",
"type": "image/svg+xml"
}
]
}
Fetch and parse query parameters in web side
Read Web SDK integration step to figure out how you should setup web sdk side. make sure you are using sdk version 2.4.0 and upper when you implement TWA integration.
you just need one more step before calling its init method. you must parse the query parameters first and pass them to the method.
<script type="text/javascript">
function getQueryParam(name) {
var params = new URLSearchParams(window.location.search);
return params.get(name);
}
var gpsAdid = getQueryParam('gps_adid'); // the parameter must match with the key you set in the native-side
/**
* initSdk: initiate adtrace sdk instance, do it only once per page load
*/
Adtrace.initSdk({
appToken: 'YOUR_APP_TOKEN', // required
environment: 'production', // required, 'production' or 'sandbox' in case you are testing SDK locally with your web app
gps_adid: gpsAdid
// other parameters according to web sdk integration document.
});
// use the Adtrace instance as usual according to the related web sdk documentation.
<script>
The init method accepts these parameters for TWA support.
/** Optional. Google Advertising ID for TWA attribution. */
gps_adid?: string;
/** Optional. IDFA for TWA attribution. */
idfa?: string;
/** Optional. Huawei Advertising ID for TWA attribution. */
oaid?: string;
/** Optional. Android ID for TWA attribution. */
android_uuid?: string;
/** Optional. facebook advertising ID for TWA attribution. */
fb_id?: string;
/** Optional. Amazon Advertising ID for TWA attribution. */
fire_adid?: string;
/** Optional. Persistent iOS ID for TWA attribution. */
persistent_ios_uuid?: string;
/** Optional. iOS ID for TWA attribution. */
ios_uuid?: string;
/** Optional. idfv for TWA attribution. */
idfv?: string;
/** Optional. Primary Dedupe Token for TWA attribution. */
primary_dedupe_token?: string;
/** Optional. Push token to be sent with each request. */
push_token?: string;