How to build a Touch-enabled Fire TV app using WebView and HTML5 web-app

Fire TV offers a variety of ways for developers to present content to customers. An HTML5 based web application could be a good candidate to provide a customer experience that is consistent with the rest of your service and have faster time to market, making it easier to deploy your apps in several marketplaces & devices. It also allows for an easier roll out of updates without having to build & release the whole app.

In this tutorial, you will learn how to create a wrapper app for Fire TV devices using Amazon WebView (AWV) technology that works using both D-Pad and touch interactions. The Fire TV documentation on UX Best Practices covers this basic aspects of app navigation and input.

 

The tutorial will cover 4 key steps:

  • Step 1: Declare Touchscreen hardware features
  • Step 2: Adding support for Amazon WebView (AWV) and enabling JavaScript
  • Step 3: Handling page navigation and device configuration changes
  • Step 4: Handling interactions between WebView and web application

 

Step 1: Declare Touchscreen hardware features

To enable basic touch interaction events such as tapping and dragging, add android.hardware.touchscreen declaration to your AndroidManifest.xml file. This feature indicates that the app is compatible with a device if that device has an actual touchscreen or emulates a touchscreen (“fake touch” interface). We need to add this as we will be targeting Fire TV devices with a touchscreen.

AndroidManifest.xml

<?xml version="1.0" encoding="utf-8" ?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    ...>

    <uses-feature
        android:name="android.hardware.touchscreen"
        android:required="false" />
    ...
</manifest>

Since your web application is hosted online, your wrapper app must have access to the Internet. To get internet access, request the INTERNET permission in your manifest file. For example:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="com.example.myapplication"
    tools:ignore="MissingLeanbackLauncher">

    <uses-feature
        android:name="android.hardware.touchscreen"
        android:required="false" />

    <uses-feature
        android:name="android.software.leanback"
        android:required="false" />

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:banner="@drawable/amazon_firetv"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:supportsRtl="true">
        <activity
            android:name=".MainActivity"
            android:configChanges="fontScale"
            android:label="@string/player_activity_name"
            android:launchMode="singleTop"
            android:theme="@style/AppTheme">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>

        <uses-library
            android:name="com.amazon.webview"
            android:required="false" />
    </application>

</manifest>

 

Step 2: Adding support for Amazon WebView (AWV) and enabling JavaScript

The [WebView](https://developer.android.com/reference/android/webkit/WebView) class is an extension of Android’s View class that allows you to render web pages as a part of your Activity layout. It does not include any features of a fully developed web browser, such as navigation controls or an address bar. All that WebView does, by default, is show a web page.

To add a WebView to your app in the layout, add the following code to your activity’s layout XML file:

activity_main.xml

<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    tools:context=".MainActivity">

    <WebView
        android:id="@+id/web_view"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:layout_gravity="center" />

</FrameLayout>

Now, within your Fire TV app, create an Activity that contains a WebView, then use that to display your online web application in onCreate()). To load a web page in the WebView, use loadUrl()). In the following example, mBaseUri, holds the address to your online hosted web application.

MainActivity.java

public class MainActivity extends Activity {

    // Address to your online hosted web application
    private final String mBaseUri = "https://mysampleapp.com";

    private Context mContext = null;
    private WebView mWebView;
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        mContext = this.getApplicationContext();

        // An activity is a single, focused thing that the customer can do.
        // Activity class takes care of creating a window for you in which
        // you can place your layout resource defining your UI.
        // In this example layout resource is defined in activity_main.xml.
        setContentView(R.layout.activity_main);

        // Retrieve the WebView widget in UI that you need to interact with.
        // In this example WebView widget is identified by "web_view" in activity_main.xml.
        mWebView = findViewById(R.id.web_view);

        mWebView.loadUrl(mBaseUrl);
    }
    ...
}

Amazon WebView (AWV) optimizations

All Fire TV devices running on Fire OS 5 and later include Amazon WebView (AWV) as a transparent replacement for the standard Android WebView class. AWV uses a custom build of Chromium that has been optimized specifically for Fire OS, devices providing faster and more efficient rendering, better video performance, and improved resource management.

To enable support for AWV, specify com.amazon.webview shared library that the application must be linked against. This tells the system to include the library’s code in the class loader for the package.

AndroidManifest.xml

<application
    android:allowBackup="true"
    android:banner="@drawable/amazon_firetv"
    android:icon="@drawable/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true">
    ...

    <uses-library
        android:name="com.amazon.webview"
        android:required="false" />

</application>

To take a full advantage of various optimizations available with AWV, you can also enable additional functionalities by passing command line options in onCreate()) method of your app’s Activity that contains a WebView. For example:

MainActivity.java

public class MainActivity extends Activity {

    private static final String TAG = MainActivity.class.getSimpleName();

    ...
    private WebView mWebView;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        ArrayList<String> cmd_args = new ArrayList<>();
        cmd_args.add("enable-system-key-events-processing");
        cmd_args.add("enable-awv-mode");
        cmd_args.add("amazon-player-type=exoplayer");
        cmd_args.add("enable-experimental-webkit-features");
        cmd_args.add("enable-use-wide-viewport");
        cmd_args.add("enable-full-paint-on-dom-update");
        cmd_args.add("enable-devtools-experiments");
        try {
            Class<?> webViewExtensionClassObj = Class.forName("com.amazon.webview.extensions.WebViewExt");
            Method setOptionsMethod = webViewExtensionClassObj.getDeclaredMethod("setEngineOptions", List.class);
            setOptionsMethod.invoke(webViewExtensionClassObj, cmd_args);
        } catch (Exception e) {
            Log.e(TAG, "com.amazon.webview load Exception: " + Log.getStackTraceString(e));
        }
        ...

        mWebView = findViewById(R.id.web_view);
        ...
    }
    ...
}

Note: Enabling AWV support using com.amazon.webview and optimization via flags added in Activity class is completely optional. Developers are advised to measure performance of their applications against individual flags.

 

Enabling JavaScript

As your web application is most likely to use JavaScript for enabling various customer interactions and communicate with your backend services, you must enable JavaScript for your WebView.

JavaScript is disabled in a WebView by default. You must enable it through the WebSettings attached to your WebView. To achieve this retrieve WebSettings with getSettings()), then enable JavaScript with setJavaScriptEnabled()).

MainActivity.java

public class MainActivity extends Activity {

    private final String mBaseUrl = "https://mysampleapp.com";

    private Context mContext = null;
    private WebView mWebView;
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
        mWebView = findViewById(R.id.web_view);
        
        WebSettings webSettings = mWebView.getSettings();
        webSettings.setJavaScriptEnabled(true);
        ...

        mWebView.loadUrl(mBaseUrl);
    }
    ...
}

WebSettings provides access to a variety of other settings that you might find useful. For example, if you’re developing a web application that’s designed specifically for the WebView in your Fire TV app, then you can define a custom user agent string with setUserAgentString()), then query the custom user agent in your web page to verify that the client requesting your web page is actually your Fire TV app.

 

Step 3: Handling page navigation and device configuration changes

Note: If your web application is built as a SPA (Single-page application) that only loads a single web document, and then updates the body content of that single document via JavaScript APIs, you can skip this step.

 

Navigation for non-SPA web applications

In case your web application requires customers to click a link for navigating to a different section of the app, for example a navigation menu, when the customer clicks a link in your WebView, the default behaviour is for Fire TV to launch an app that handles URLs. Usually, the default web browser opens and loads the destination URL. However, you can override this behavior for your WebView, so links open within your WebView. You can then allow the customer to navigate backward and forward through their web page history that’s maintained by your WebView.

Note: For security reasons, the Fire OS browser app does not share its application data with your web application.

 

To open all links clicked by the customer, provide a WebViewClient for your WebView, using setWebViewClient()). Creating a custom WebViewClient that overrides the shouldOverrideUrlLoading()) gives the host application a chance to take control when a URL is about to be loaded in the current WebView.

In the following example we are creating a CustomWebViewClient as an inner class of Activity and an instance of this new WebViewClient for the WebView.

MainActivity.java

public class MainActivity extends Activity {

    private final String mBaseUrl = "https://mysampleapp.com";

    private WebView mWebView;
    private CustomWebViewClient webViewClient;
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...

        mWebView = findViewById(R.id.web_view);
        
        webViewClient = new CustomWebViewClient();
        mWebView.setWebViewClient(webViewClient);
        ...

        mWebView.loadUrl(mBaseUrl);
    }
    ...
}

class CustomWebViewClient extends WebViewClient {

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
        view.loadUrl(request.getUrl().toString());

        return super.shouldOverrideUrlLoading(view, request);
    }
}

Device configuration changes in response to accessibility features customization

There are various accessibility features available on Fire TV. One of them is the Text Banner, an on-screen text banner that will help customers with visual impairments view the text on their screen. When the customer changes accessibility features during the runtime, activity state changes occur causing device configuration changes. This could interfere with WebView object’s activity to be destroyed and a new activity to be created, which also creates a new WebView object that loads the destroyed object’s URL.

In order to handle configuration changes during runtime so that your application doesn’t need to update resources during a specific configuration change and avoid the activity restart, declare that your activity handles the fontScale configuration change itself which prevents the system from restarting your activity.

AndroidManifest.xml

<activity
    android:name=".MainActivity"
    android:configChanges="fontScale"
    android:label="@string/player_activity_name"
    android:launchMode="singleTop"
    android:theme="@style/AppTheme">
    ...
</activity>

Now, when the configurations change, MainActivity does not restart. Instead, the MainActivity receives a call to onConfigurationChanged(). This method is passed a Configuration object that specifies the new device configuration. By reading fields in the Configuration, you can determine the new configuration and make appropriate changes by updating the resources used in your interface. At the time this method is called, your activity’s Resources object is updated to return resources based on the new configuration, so you can easily reset elements of your UI without the system restarting your activity.

MainActivity.java

public class MainActivity extends Activity {

    ...

    @Override
    public void onConfigurationChanged(final Configuration c) {
        super.onConfigurationChanged(c);

        mWebView.getSettings().setTextZoom((int) (c.fontScale * 100));
    }
}

That’s all you need for a basic WebView that displays a web page. Additionally, you can customise your WebView by modifying the following:

  • Enabling fullscreen support with WebChromeClient. This class is also called when a WebView needs permission to alter the web application’s UI, such as creating or closing windows and sending JavaScript dialogs to the customer.
  • Handling events that impact content rendering, such as errors on form submissions or navigation with WebViewClient. You can also use this subclass to intercept URL loading.

 

Step 4: Handling interactions between WebView and web application

 

Dispatching D-PAD events from webview

All Amazon Fire TV remote controls generate KeyEvent events for button presses, as any Android input device does. You can handle controller button input with standard Android event listener interfaces and callbacks. Neither the Amazon Fire TV Remote nor the Voice Remote raises motion events (from the Android MotionEvent class).

Note: The remote controllers for Fire TV Edition has some additional buttons — such as volume up/down, power, and specific app buttons that allow to directly open those apps. However, these buttons can’t be mapped to events in third-party apps.

 

You can greatly enhance the customer experience in your application by managing the D-Pad interactions. The Android framework provides APIs for detecting and processing D-Pad (or any other controller) inputs. In your Activity class, override the dispatchKeyEvent()) method, and add customizations as needed. The following example shows how to change the “BACK” key on D-Pad to go back in the WebView consistently, and exit the app if there is no history to navigate. Overriding the dispatchKeyEvent() method simultaneously allows sending the key events to WebView which can be handled in your web application using JavaScript to perform desired actions.

MainActivity.java

public class MainActivity extends Activity {

    private final String mBaseUrl = "https://mysampleapp.com";

    private Context mContext = null;
    private WebView mWebView;
    ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ...
    }
    
    ...
    
    @Override
    public boolean dispatchKeyEvent(KeyEvent event) {
        Log.d(TAG, "dispatchKeyEvent : KeyCode = " + event.getKeyCode() + " (" + KeyEvent.keyCodeToString(event.getKeyCode()) + ")");

        switch (event.getKeyCode()) {
            // Handle back button from D-Pad
            case KeyEvent.KEYCODE_BACK:
                Log.d(TAG, "dispatchKeyEvent : mWebView.canGoBack = " + mWebView.canGoBack());
                if (mWebView.canGoBack()) {
                    // Navigate back on WebView
                    mWebView.goBack();
                } else {
                    // WebView has no more history, exit the app
                    finish();
                }
        }

        return super.dispatchKeyEvent(event);
    }
}

After overriding the dispatchKeyEvent() method, when you press Play/Pause button from the D-Pad you should see following lines in ADB logs. KEYCODE_MEDIA_PLAY_PAUSE is represented as KeyCode = 85. Please follow this link to learn more about the Android KeyEvent constants, and the default behavior of those buttons on Fire TV devices.

D/MainActivity: dispatchKeyEvent : KeyCode = 85 (KEYCODE_MEDIA_PLAY_PAUSE)

Handling D-pad interactions in javascript

D-Pad interactions by customers are received as KeyboardEvent events in your web application’s JavaScript. To handle a KeyboardEvent, you need to follow these steps:

1.  Select the [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) on which the event will fire. Since we are using WebView to render the web application’s content in your Fire TV wrapper application, it is recommended that events are listened on the [Window](https://developer.mozilla.org/en-US/docs/Web/API/Window) object.

2.  Use [element.addEventListener()](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener) to register an event handler. For each key press on D-Pad, JavaScript receives two events,

a. keydown – when a key on the D-Pad is pressed down, and auto repeats if the key is pressed down for long.

b. keyup – when a key on the D-Pad is released.

 

In following example, we are using an open source HTML5 video player called Video.js to setup a basic media player experience. keydown event is triggered as soon as D-Pad interaction starts, for the demonstration purpose, we are setting the background of whole document to green color. When the key press is released, keyup event is triggered. The custom key handling mechanism is defined in handleKeyEvent() method.

index.html

// https://videojs.com/getting-started/
const player = videojs("video", {...});

// Custom Key Event Handler
const moveTime = 10; // Move time by number of seconds
const handleKeyEvent = (event) => {
    switch(event.code) {
        case "MediaPlayPause":
        case "NumpadEnter":
            // Check is the playback is paused
            if (player.paused()) {
                videojs.log('handleKeyEvent:', 'Play', player.currentTime());
                // Resume the playback
                player.play();
            } else {
                videojs.log('handleKeyEvent:', 'Pause', player.currentTime());
                // Pause the playback
                player.pause();
            }
            break;
        case "MediaRewind":
        case "ArrowLeft":
            // Move back the playback time by 10 seconds
            player.currentTime((player.currentTime() - moveTime) < 0 ? 0 : (player.currentTime() - moveTime));
            videojs.log('handleKeyEvent:', 'Rewind', player.currentTime());
            break;
        case "MediaFastForward":
        case "ArrowRight":
            // Move forward the playback time by 10 seconds
            player.currentTime(player.currentTime() + moveTime);
            videojs.log('handleKeyEvent:', 'Fast Forward', player.currentTime());
            break;
        default:
            videojs.log('handleKeyEvent:', 'Unhandled event.code =', event.code);
            break;
    }
}

// Listen for Keyboard Events dispatched by Fire TV app's WebView
window.addEventListener("keyup", (event) => {
    // Reset the background colour when key press is lifted
    document.body.style.background = "";

    if (event.key !== undefined) {
        console.log('keyup:', 'event.key =', event.key);
        // Handle the event with KeyboardEvent.key
        handleKeyEvent(event);
    }
});

window.addEventListener("keydown", (event) => {
    // Change background colour when key press is down
    document.body.style.background = "green";
});

 

Dispatching Touch events from webview

Although your app should not exclusively depend on touch gestures for basic behaviors, since this form of interaction may not be available on all Fire TV devices, adding a touch-based interaction to your app wrapper application can greatly increase its usefulness and appeal.

A touch gesture occurs when a customer places one of more fingers on the touch enabled device’s screen, and your application interprets that pattern of touch as a particular gesture. There are two phases to the gesture detection:

1.  Gather data about the touch events – When a customer places one or more fingers on the screen, this triggers the callback onTouchEvent()) on the View that received the touch events. For each sequence of touch events (position, pressure, size, addition of another finger, etc.) that is ultimately identified as a gesture, onTouchEvent()) is fired several times.

2.  Interpret the data to see if it meets the criteria for any of the gestures your app supports. The gesture starts when the customer first touches the screen, continues as the system tracks the position of the customer’s finger(s), and ends by capturing the final event of the customer’s finger(s) leaving the screen. Throughout this interaction, the MotionEvent delivered to onTouchEvent()) provides the details of every interaction. Your app can use the data provided by the MotionEvent to determine if a gesture it cares about happened.

In the following example, we are intercepting touch events in app’s Activity class, overriding the onTouchEvent()) callback. The snippet uses getActionMasked()) to extract the action the customer performed from the event parameter. This gives you the raw data you need to determine if a gesture you care about occurred:

MainActivity.java

public class MainActivity extends Activity {
    
    ...
    
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        int action = event.getActionMasked();

        switch(action) {
            case (MotionEvent.ACTION_DOWN):
                Log.d(TAG,"Action was DOWN");
                return true;
            case (MotionEvent.ACTION_MOVE):
                Log.d(TAG,"Action was MOVE");
                return true;
            case (MotionEvent.ACTION_UP):
                Log.d(TAG,"Action was UP");
                return true;
            case (MotionEvent.ACTION_CANCEL):
                Log.d(TAG,"Action was CANCEL");
                return true;
            case (MotionEvent.ACTION_OUTSIDE) :
                Log.d(TAG,"Movement occurred outside bounds of current screen element");
                return true;
            default:
                return super.onTouchEvent(event);
        }
    }
}

Handling Touch interactions in JavaScript

Touch events are similar to mouse events except they support simultaneous touches and at different locations on the touch surface. The TouchEvent interface encapsulates all of the touch-points that are currently active. The Touch interface, which represents a single touchpoint, includes information such as the position of the touch point relative to the browser viewport.

Touch events consist of three interfaces (Touch, TouchEvent and TouchList) and the following event types:

  • touchstart – fired when a touch point is placed on the touch surface.
  • touchmove – fired when a touch point is moved along the touch surface.
  • touchend – fired when a touch point is removed from the touch surface.
  • touchcancel – fired when a touch point has been disrupted in an implementation-specific manner (for example, too many touch points are created).

Continuing from the previous example of HTML5 video player, we can now add some basic touch interactions on button elements defined within the player’s media control. playPauseControl element targets the play/pause button from the player, and muteControl targets audio the mute/un-mute button. You can learn more about JavaScript Touch Events from MDN Web Docs and a guide to adding touch events to your application is available on Google Web Fundamentals.

index.html

const player = videojs("video", {...});

// Listen for Touch Events dispatched by Fire TV app's WebView
// Play/Pause
let playPauseControl = document.getElementsByClassName('vjs-play-control')[0];
playPauseControl.addEventListener("click touchstart", (event) => {
    videojs.log('playPauseControl', 'touched');
});

// Mute/Un-mute
let muteControl = document.getElementsByClassName('vjs-mute-control')[0];
muteControl.addEventListener("click touchstart", (event) => {
    videojs.log('muteControl', 'touched');
});

 

Conclusion

If you are planning to deliver an existing HTML5 based web application as a part of a client application to Fire TV, this guide will help implement the necessary interactions and navigation required for a smooth customer experience. You can learn more about how to optimize Web Apps for Fire devices on the Amazon Developer Portal.

For more details, check out our documentation:

 

 

MayurAhir.jpg

Mayur Ahir is an Amazon Solutions Architect with domain expertise in developing cross-platform HTML5 applications for devices powered by Fire OS and Alexa, such as Fire TV, Fire Tablets, and Echo Show.