Monday, February 23, 2015

Android WebView: Interacting with your apps Java from the Web Page.

In my previous post, Android WebView: Interacting with the Web Page from Java, I showed you how to call javascript functions from your native Java code. I also briefly mentioned that on Android KitKat and above you have the option of receiving the result directly from the javascript call using the evaluateJavascript method. This is an easy way to handle any result that may have been returned from the javascript call.

The evaluateJavascript method is very helpful but does not handle all use cases in which you may need to execute native Java code initiated by your web page. One use case would be handling web events (like a button click) or for returning data from a method on Android Jelly Bean or below.

One word of warning before we begin, allowing an HTML page to execute native Java code is inherently insecure. Web pages are isolated from the app they're running in by design. Creating a bridge between the web page and the app means that javascript on the page may have the ability to do anything on the device for which the app that's hosting the web page has permissions for. It is extremely important that you keep this in mind when creating the bridge from the web page to your app.

There are three main things that you need to do in order to call native Java methods from javascript:

  • Create a javascript interface object which serves as the bridge between javascript and Java.
  • Add the interface to the Web View so that any page that it hosts can use the bridge.
  • Update your javascript to use the bridge.

Creating a Javascript Interface Object


In this very basic class I am declaring a listener interface which I have the caller pass in via the constructor. This is one way to make sure that you only allow javascript to interact with your app in the way you expect. By using a well defined interface you are making it more difficult for malicious javascript to execute arbitrary code.

To make a method visible to javascript the method needs to be decorated with the @JavascriptInterface attribute.

Notice that I'm calling the listener back using a handler and runnable. This is because the bridge class methods aren't executing on the main thread. We need to execute the listener callback on the main thread.

public class WebViewNativeBridge
{
    public interface OnSomeChangeListener {
        public void onSomeChange(String text);
    } 
    private OnSomeChangeListener listener = null;
    public WebViewNativeBridge(OnSomeChangeListener listener) {
        this.listener = listener;
    } 
    @JavascriptInterface @SuppressWarnings("unused")
    public void someChangeFromWeb(String text)
    {
        final String newText = text; 
        // This code is not executed in the UI thread
        // so we must force it to happen
        new Handler(Looper.getMainLooper()).post(new Runnable()
        {
            @Override
            public void run()
            {
                if(listener != null)
                    listener.onSomeChange(newText);
            }
        });
    }
}

Add The Javascript Bridge Object To Your WebView


The way you wire up your native javascript bridge is to add the javascript interface to your Web View using the addJavascriptInterface method. This method takes in two parameters, your bridge object and the key word you want to use to access the bridge from within javascript.

WebViewNativeBridge.OnSomeChangeListener listener
                                                        = this.getOnSomeChangeListener();
WebViewNativeBridge bridge = new WebViewNativeBridge(listener);
browser.addJavascriptInterface(bridge, "NativeBridge");

I created the bridge object with a custom listener I created:

private WebViewNativeBridge.OnSomeChangeListener getOnSomeChangeListener() {
    return new WebViewNativeBridge.OnSomeChangeListener() {
        public void onSomeChange(String val) {
            // do something with the value passed in
        }
    };
}

Update The Javascript To Use The Native Bridge Object


The last step is to call into your native Java code from javascript. Using the NativeBridge object we registered with the Web View we can call the someChangeFromWeb method from our javascript. Note, the name of the object you use in javascript has to be exactly the same as the second parameter you passed into the addJavascriptInterface method.
function notifyNativeOfChange() {
    NativeBridge.someChangeFromWeb("some change");
}

Monday, February 16, 2015

Android WebView: Interacting with the Web Page from Java.

In my previous post, Android WebView: Displaying web content in your app, I explained the basic's of setting up a WebView to display web content in your app. In this post I'm going to explain how you can interact with the web page you're displaying.

It's not a good idea to try to mess with web page content for pages that you don't control because those pages can change and those changes may break your app or provide a poor user experience in your app. But there are cases when you do want to interact with web pages that you control the content of.

One example is if you provide content in your app via a web page whose content structure changes often. Using a web browser to display this data in your app provides the benefit to your users of being able to update that content without updating the app itself.

The way to accomplish this while providing a good user experience that's less likely to break when you update your web site is to define a Javascript contract for interacting with the web content. This contract is made up of a specific set of Javascript functions that your page offers the app which provides a known outcome. This allows the web page to change and add additional content while adhering to the predefined contract and gracefully failing the web page when the contract needs to change.

Executing Javascript


There are two main options you have to execute Javascript within a web page from native Java code. The option you choose will depend on what the minimum version of Android is that you are targeting in your app.

Executing Javascript on Android 4.4+


The preferred option is to use the evaluateJavascript method in the WebView class. This allows you to send some Javascript to the browser to execute and handle any expected result in native code. The  evaluateJavascript method was introduced in Android 4.4 with a new WebView class based on the Chromium.  Here's an example of how to scroll a web page to the top using this new method:

private void scrollWebPageToTop_KitKatOrAbove(WebView browser)
{
    String myScript = "window.scrollTo(0,0);";
    browser.evaluateJavascript(myScript, new ValueCallback<String>() {
        @Override
        public void onReceiveValue(String result) {
            // Do something with the result
           Log.d("JSEXAMPLE", result);
        }
    });
}

Executing Javascript on Android 4.3 or below


If you are targeting Android 4.3 or below you'll need to use the WebView's loadUrl method to execute your Javascript. Prefacing the your URL with "javascript:" lets the WebView know that you would like to execute Javascript in the context of the current page being displayed.

private void scrollWebPageToTop_JellyBeanOrBelow(WebView browser)
{
    String myScript = "javascript:window.scrollTo(0,0);";
    browser.loadUrl(myScript);
}

The first thing you may have noticed is that you don't get the ability to deal with the result of the Javascript method you just executed. If you are executing Javascript from which you expect a result you'll need to use the WebView's addJavascriptInterface method and explicitly call the callback you've registered. Next week's post will go into greater detail on using this method to allow your web page to trigger native Java code to deal with the result of your Javascript code execution.

For more information on the new WebView based on the Chromium browser see the Migrating to WebView in Android 4.4 page in the Web Apps section of the Android developer guides.

Monday, February 9, 2015

Android WebView: Displaying web content in your app

The web has become a ubiquitous medium for information and communication. We use the web to disseminate information about ourselves, our companies, and our thoughts. We consume our news and other information about the world on the web. We purchase our groceries and gifts on the web. We even communicate with our friends, families, and loved ones over the web.

When we think about the web for native mobile applications we tend to think of the web as a data transfer mechanism. We talk about the cloud, JSON, XML, and RSS; all of which are important but often we need the web as more than the backbone of information. There are a couple of reasons you may want to display web content in your app instead of a native view:

  • You want to show external content without leaving your app.
  • You have dynamic content whose structure changes often.
  • You want to display legal pages (terms of service, end user license agreements, etc) directly from your site.

Today I'm going to start a series on the Android WebView. In this post we'll talk about the basic's of setting up a WebView to display web content in your app. In other posts I'll explain how to interact with web content via Javascript from your app as well as interacting with your app from Javascript. Finally I'll go over some gotchas or hurdles that it's important to be aware of when using a web view in your app.

Initializing the WebView


Initializing the WebView should be done when your Activity or Fragment is created. While the default settings the WebView uses will probably work for most basic web pages, you can be more explicit about how your WebView acts. You specify your preferences using the WebSettings object on your WebView.

Obtaining your WebView's settings is as simple as calling getSettings() on the WebView object. Here's an example of changing some of the default WebView settings:

private void initializeWebView(Bundle savedInstanceState)
{
    /** make sure you have a WebView in your layout with the id: browser **/
    WebView browser = (WebView)getView().findViewById(R.id.browser);

    // I like my scroll bars inside the content
    browser.setScrollBarStyle(WebView.SCROLLBARS_OUTSIDE_OVERLAY);

    // allow web pages to execute Javascript.
    browser.getSettings().setJavaScriptEnabled(true);

    // if you know your web pages use a different encoding than utf-8 you can change it.
    browser.getSettings().setDefaultTextEncodingName("utf-8");

    // Start with content zoomed all the way out.
    browser.getSettings().setLoadWithOverviewMode(true);

    // Allow zooming with the default zoom controls
    browser.getSettings().setSupportZoom(true);
    browser.getSettings().setBuiltInZoomControls(true);

    // Change how the web page is laid out.
    // See WebSettings.LayoutAlgorithm for more detail.
    browser.getSettings().setLayoutAlgorithm(WebSettings.LayoutAlgorithm.NORMAL);

    // Allow the HTML viewport meta tag to specify the width
    browser.getSettings().setUseWideViewPort(true);

    // Used to handle external interactions. More details below.
    browser.setWebChromeClient(this.getWebChromeClient());

    // Used to handle page events. More details below.
    browser.setWebViewClient(this.getWebViewClient());
}

Loading A Web Page


Once you have a WebView added to your app displaying web pages is as simple as calling loadUrl on the WebView. For example:

private void loadWebPage(String url)
{
    /** make sure you have a WebView in your layout with the id: browser **/
    WebView browser = (WebView)getView().findViewById(R.id.browser);
    browser.loadUrl(url);
}

Loading HTML Directly


Loading web data into your page doesn't have to be done using a remote URL. If you already have the HTML content you'd like to display (possibly from an RSS feed or offline content), you can do so very easily by first converting the content to Base64 and then creating a data URI to display the content. For example, you can generate a url for HTML content you already have that the WebView can display as follows:

private String getUrlFromHTML(String html)
{
    String base64Data = Base64.encodeToString(html.getBytes(), Base64.DEFAULT);
    return String.format("data:text/html;charset=utf-8;base64,%s", base64Data);
}

Advanced Browser Features


Handling Web Page Life-cycle and Other Events


There are several events associated with the web page life-cycle. For example there are events associated with web page loading being started/stopped. There are also events for web page errors as well as a login request from the web page. These web page events are handled via a WebViewClient. Your browser should, at a minimum, provide feedback to the user when a web page starts loading and when it stops loading. Here's an example of how you can handle those events:

protected WebViewClient getWebViewClient()
{
    return new WebViewClient()
    {
        @Override
        public void onPageStarted(WebView view, String url, Bitmap favicon)
        {
            /** provide feedback using a spinner or some other dialog to  
the user to let them know that you're doing some work **/
        }

        @Override
        public void onPageFinished(WebView view, String url)
        {

            /** hide the feedback that was provided to the user **/
        }
    };
}


External Browser Interactions


One of the more nuanced aspects of displaying web content in your app is when the web content wants to do something external to the current browser. In order to interact with the browser for some of these more advanced features you need to use a WebChromeClient. For example, playing back HTML5 video fullscreen is done using the WebChromeClient's onShowCustomView method. Other examples of advanced external browser interactions are reporting debug messages to the Javascript console, opening/closing additional browser windows, displaying messages from Javascript, and etc.

It's important to note that there is only one WebChromeClient. Because of this you'll likely want to subclass WebChromeClient if you plan to provide many of these advanced features in your app. Here's an example of creating a WebChromeClient that can update the progress indicator of a Fragments Activity.

private WebChromeClient getWebChromeClient()
{
    return new WebChromeClient()
    {
        public void onProgressChanged(WebView view, int progress)
        {
            if(getActivity() != null)
            {
                getActivity().setProgress(progress * 100);
            }
        }
    };
}

For a more detailed example of integrating a basic web browser into your app, see the BrowserFragment in my open source RSS Reader Android app.

Monday, February 2, 2015

Why Failure Is Important

If there was one thing that I wish college had prepared me for, that it didn't, it's to understand how important failure is to success. I know, that sounds like an oxymoron but it's absolutely true. You cannot be successful, truly successful for the long haul, without being able to embrace failure.

As a professional there are several reasons why you'll want to be able to embrace failure.

Without failure, there are no real risks

The airplane was a risk.
The automobile was a risk.
The internet was a risk.

Risks allow us to expand, to push the boundaries, to get better. Not all risks pan out the way we want but the important ones do.

Failure allows you to be a real person

I know it's hard to admit but you're human. Humans are fallible people. We make mistakes. Failure allows you to be a bit more humble and remember that you have flaws.

It's not until you are able to admit you have flaws that you can start to learn from them. Understanding our flaws makes us more resilient.

Failure builds trust

Plain and simple, there is not a person who has not walked this earth that has not made a mistake. There are only people that hide their mistakes. Attempting to hide our flaws is a barrier to earning trust.

People that are able to admit their mistakes are more trustworthy. It shows that you're concerned about more than just your outward appearance. It shows that you are able to look at yourself more objectively.

If you're able to look past your own mistakes, people trust that you're able to look past theirs.

Failure allows you to learn from your mistakes

Every failure is an opportunity to learn. The most successful people I know are people who are constantly learning. It's a curiosity about the world around them that drives them.

As a key part of the world you should be curious about yourself. Being curious about what makes you tick will help you to understand how to navigate around and beyond your failures.

Failure gives you the ability to change

By far the most important part of embracing failure that it gives us an opportunity to change, to get better. Are you impatient, do you not listen well, are you quick to anger, are you defensive? What are the failures that are getting in the way of you being successful today?

Taking the time to reflect on the situations where you've failed allows you to understand why you've failed and gives you an opportunity to succeed next time the situation arises.