3

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):

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"]
                }
  }
]
Bob Groeneveld
  • 903
  • 1
  • 9
  • 19
  • One thing I came across implementing TWA is that in our case, the display mode should be set by the PWA, not by the Android TWA app. Check the web manifest of your PWA, make sure display mode is set to your desired mode, i.e. `fullscreen`. See https://developer.mozilla.org/en-US/docs/Web/Manifest/display for display mode documentation. See https://superpwa.com/doc/web-app-manifest-display-modes/ to have better idea of how each mode looks like when displayed via TWA. – Dat Pham Tat Aug 13 '20 at 12:32
  • 1
    Did you found any solution? – jkr Jan 31 '21 at 15:47

1 Answers1

0

Regarding Digital Asset Links validation, I'd recommend installing Peter's Asset Links Tool and using it to check the configuration. Make sure to double check the section on App Signing on the quick start guide too, as the signature changes when using it, causing validation to fail the app is downloaded from the Play Store (you have to update assetlinks.json to make it work).

You also seem to be using the custom-tabs-client library, which is deprecated and would recommend moving to android-browser-helper, which is the recommended one for Trusted Web Activity. If you indeed want to use a lower level library, androidx.browser would be the one to use (I really recommend using android-browser-helper.

android-browser-helper includes a LauncherActivity that makes things very easy, as most aspects can be configured from AndroidManifest.xml, but it also expects to launch the Trusted Web Activity from the home screen. The twa-basic demo shows how to use the LauncherActivity.

For other use-cases, the TwaLauncher can be used (it's used by the LauncherActivity itself). The twa-custom-launcher demo shows how to use it. The source code for the LauncherActivity can also be helpul.

Finally, if the goal is to simply launch a Progressive Web App from the homescreen, Bubblewrap is a Node.js command-line tool that automates the process.

Regarding immersive mode, here's how it's setup in the twa-basic demo. If using TwaLauncher, the LauncherActivity code to use is here.

When using Bubblewrap, choose fullscreen for the Display Mode when creating the project to launch the app in immersive mode.

Bonus: Since you mentioned wanting to use a WebView fallback implementation, you may be interested to know that android-browser-helper ships with a WebView fallback (disabled by default). The twa-webview-fallback demo shows how to use it.

andreban
  • 4,621
  • 1
  • 20
  • 49