We make an Android app that currenlty uses WebView to show web content full screen.
This works but the performance depends strongly on the version of the WebVeiw component and that is not always updated when the Chrome browser is updated (there is a relatively complicated relationship between the WebView component and the Chrome browser over the different Android versions). From presentations on the subject from Google we conclude that using TWA we would likely get better and more consistent performance since the TWA functionality is updated together with the Chrome browser. We therefore want to use TWA with a fallback to WebView when TWA is not present (our app runs on Android 4.4 and newer).
The app needs to perform some more logic than just showing the web-content so we cannot get away with defining TWA/WebView in the Manifest only. Checking for the ability to use TWA and either starting TWA or falling back to WebView in the MainActivity.java was implemented. However, when using TWA both the URL/Address Bar and Bottom Navigation Bar remain visible.
URL/Address Bar: As far as we know, to make the TWA not show URL/Address Bar the domain that is shown in the TWA has to have a /.well-known/assetlinks.json file that "matches" the certificate of the Android app. Two pages with information and useful links about this are https://developers.google.com/web/android/trusted-web-activity/integration-guide and https://developer.android.com/training/app-links/verify-site-associations. The assetlinks.json was created using https://developers.google.com/digital-asset-links/tools/generator and successfully checked with https://digitalassetlinks.googleapis.com/v1/statements:list?source.web.site=website_url&relation=delegate_permission/common.handle_all_urls
During testing on virtual device (Android API level 29, Chrome v83) we enabled the "Enable command line on non-rooted devices" flag of Chrome and in a terminal did
$ adb shell "echo '_ --disable-digital-asset-link-verification-for-url=\"website_url"' > /data/local/tmp/chrome-command-line"
After that Chrome shows a warning message but the URL/Address Bar is still present.
Bottom Navigation Bar: Chrome v80 and newer should support removing the Bottom Navigation Bar using the immersive option: https://bugs.chromium.org/p/chromium/issues/detail?id=965329#c18 Although using the options described for creating a full screen app (https://developer.android.com/training/system-ui/immersive#java) the Bottom Navigation Bar is still showing.
How do we remove the URL/Address Bar and Bottom Navigation Bar, basically how do we make the web content shown in TWA full screen?
We looked at the following example apps to see what we need to do to get TWA working but found nothing that worked (although it is not unthinkable that we missed something essential):
- https://github.com/GoogleChromeLabs/svgomg-twa
- https://github.com/tsuyosh/TrustedWebActivitiesDemo
- https://github.com/GoogleChrome/android-browser-helper
- https://github.com/elliatab/TwaDemo
- https://github.com/thanhtungka91/TwaDemoJava
Relevant content of our project files:
Manifest.json
<application
android:name="main_application"
android:hardwareAccelerated="true"
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:banner="@drawable/banner"
android:label="@string/app_name"
android:usesCleartextTraffic="true"
android:networkSecurityConfig="@xml/network_security_config"
android:theme="@style/AppTheme" >
<activity
android:name="main_activity"
android:hardwareAccelerated="true"
android:label="@string/app_name"
android:launchMode = "singleInstance"
android:keepScreenOn="true" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LEANBACK_LAUNCHER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
</application>
MainActivity.java
public class MainActivity extends Activity implements IServiceCallbacks {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Set up looks of the view
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
requestWindowFeature(Window.FEATURE_NO_TITLE);
View decorView = getWindow().getDecorView();
decorView.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener() {
@Override
public void onSystemUiVisibilityChange(int visibility) {
// Note that system bars will only be "visible" if none of the
// LOW_PROFILE, HIDE_NAVIGATION, or FULLSCREEN flags are set.
if ((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0) {
// bars are visible => user touched the screen, make the bars disappear again in 2 seconds
Handler handler = new Handler();
handler.postDelayed(new Runnable() {
public void run() {
hideBars();
}
}, 2000);
} else {
// The system bars are NOT visible => do nothing
}
}
});
decorView.setKeepScreenOn(true);
setContentView(R.layout.activity_main);
// create Trusted Web Access or fall back to a WebView
String chromePackage = CustomTabsClient.getPackageName(this, TrustedWebUtils.SUPPORTED_CHROME_PACKAGES, true);
if (chromePackage != null) {
if (!chromeVersionChecked) {
TrustedWebUtils.promptForChromeUpdateIfNeeded(this, chromePackage);
chromeVersionChecked = true;
}
if (savedInstanceState != null && savedInstanceState.getBoolean(MainActivity.TWA_WAS_LAUNCHED_KEY)) {
this.finish();
} else {
this.twaServiceConnection = new MainActivity.TwaCustomTabsServiceConnection();
CustomTabsClient.bindCustomTabsService(this, chromePackage, this.twaServiceConnection);
}
} else {
// set up WebView
}
}
private class TwaCustomTabsServiceConnection extends CustomTabsServiceConnection {
public void onCustomTabsServiceConnected(ComponentName componentName, CustomTabsClient client) {
CustomTabsSession session = MainActivity.this.getSession(client);
CustomTabsIntent intent = MainActivity.this.getCustomTabsIntent(session);
Uri url = Uri.parse("http://our_url");
TrustedWebUtils.launchAsTrustedWebActivity(MainActivity.this, intent, url);
MainActivity.this.twaWasLaunched = true;
}
public void onServiceDisconnected(ComponentName componentName) {
}
}
protected void hideBars() {
if (getWindow() != null) {
View decorView = getWindow().getDecorView();
decorView.setSystemUiVisibility(
// Hide the nav bar and status bar
View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_FULLSCREEN
// Set the content to appear under the system bars so that the
// content doesn't resize when the system bars hide and show.
| View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
| View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
| View.SYSTEM_UI_FLAG_LAYOUT_STABLE
// Enables regular immersive mode
| View.SYSTEM_UI_FLAG_IMMERSIVE
);
}
// Remember that you should never show the action bar if the
// status bar is hidden, so hide that too if necessary.
ActionBar actionBar = getActionBar();
if (actionBar != null) {
actionBar.hide();
}
}
}
build.gradle
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
repositories {
jcenter()
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.0.0'
}
}
allprojects {
repositories {
jcenter()
google()
maven { url "https://jitpack.io" }
}
}
build.gradle
apply plugin: 'com.android.application'
android {
compileSdkVersion 29
buildToolsVersion '29.0.2'
defaultConfig {
applicationId application_id
minSdkVersion 19
targetSdkVersion 29
}
buildTypes {
release {
minifyEnabled true
debuggable false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
}
debug {
minifyEnabled false
debuggable true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.txt'
jniDebuggable true
renderscriptDebuggable true
renderscriptOptimLevel 3
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}
dependencies {
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.leanback:leanback:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.webkit:webkit:1.2.0'
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'com.github.GoogleChrome.custom-tabs-client:customtabs:master'
}
/.well-known/assetlinks.json
[
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target" : { "namespace": "android_app", "package_name": "our package name",
"sha256_cert_fingerprints": ["11:22:33:44"]
}
},
{
"relation": ["delegate_permission/common.handle_all_urls"],
"target" : { "namespace": "android_app", "package_name": "our other package name",
"sha256_cert_fingerprints": ["11:22:33:44"]
}
}
]