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.




Monday, January 26, 2015

Why is it so difficult to watch live T.V. on a mobile device

The other day I was out to brunch with my wife and I was trying to see if I could catch some of the Dallas vs Greenbay playoff game. We're both pretty big football fans and were very curious about who the Seahawks would be facing in the NFC Conference Championship. I was extremely frustrated by the fact that I could not find the game anywhere.

I understand that there are one off ways to watch football in particular. But what about T.V. in general. I'm curious as to why there's not an equivalent to a cable subscription for mobile. Yes, most cable providers offer some form of internet T.V. but there's two problems.

  • You're required to have a home cable subscription.
  • Not all programs are available, specifically most live sporting events.

It feels like this is an opportunity for big cable (or an upstart trying to break in) to move forward towards the actual future of television. Right now big cable is desperately trying to hold on to the past because it's lucrative. But as Netflix and Amazon Prime Instant Video are showing, people's television consumption habits are changing.

Here's an outline of what what I believe would be wildly successful in regards to the future of television.

  • Provide a streaming cable subscription that DOES NOT require a home account.
    • Make the service work with Android, Fire, iOS, and Windows mobile.
  • Offer ALL the same channels (Network TV, ESPN, Comedy Central, etc) available with classic cable.
  • Offer an upgrade to access the last full season of shows.
    • This will allow people to catch up on missed episodes.
    • This will allow people that prefer to binge watch shows to do so when the season is over.
  • Offer an upgrade to remove the commercials. I'd pay an extra $10 a month for 0 commercials.
  • Partner with Amazon and Netflix to license their original content.
  • Allow people to pay with Amazon payments. (full disclosure... I work for Amazon).
    • Chances are people already have an Amazon account with a credit card on file.
    • Allows a technology company that understands security to handle the important payment processing.
Something like this is bound to happen sooner or later. Why not now?

Monday, January 19, 2015

Don't hate your users or potential users

The other day I had an incredibly frustrating experience with iCloud that was 100% avoidable. I'm not an iCloud user but a buddy of mine is and when he had his first kid he sent out an iCloud link to some pictures. Being out and about at the time I tried to open the link on my phone using the mobile Firefox browser. I was greeted with this not-so-friendly message:



Something I've gotten used to doing on my mobile when web pages act weird is to check the "request desktop version" button and guess what? The page loaded fine but I was left with more animosity towards iCloud and Apple for blatantly breaking an experience that should "just work".

So what can we learn from this?
  • Your customers aren't just the people who bought your products. 
  • Provide "correct" details when things aren't going to work.

Your customers aren't just the people who bought your products

Most businesses view their customers as the who buy their products or use their services. But in a day and age where social media has become a primary mechanism for sharing information your customer base is now extended to those that your customers share their content with.

When you're building a product that allows users to share information via a URL you are essentially making a contract with that user that their content will be accessible by those they share it with on a modern web browser.

In the iCloud case I'm baffled at why Apple would explicitly block Android users. I understand they're trying to get people to use their ecosystem, but they also need to face the reality that people in their ecosystem interact with people on the outside.

How do I know they're explicitly blocking Android users? I took a look at the difference between the request headers that Firefox sends normally on Android and what it sends when you check the "request desktop site". The big difference was in Firefox's user agent string

Mobile OnlyMobile: Mozilla/5.0 (Android; Mobile; rv:34.0) Gecko/34.0 Firefox/34.0
With Request Desktop Site CheckedMozilla/5.0 (X11; Linux x86_64; rv:34.0) Gecko/20100101 Firefox/34.0

While I don't think this is the case for iCloud, there may be completely valid reasons they're blocking Android mobile user specifically. For instance they may not have the resources, bandwidth, or experience to test their site fully on enough variations of the Android platform to support the experience. Even if that was the case, which I do not think it is, they need to be more explicit with their users and those trying to use the site that there are known and likely use cases where the experience is not going to be available.

This leads me to the next thing we can learn from this experience.

Provide "correct" details when things aren't going to work

The web is often looked at as an "easier" way to provide a unified experience with broader reach than providing a native experience on multiple platforms. But with that broad reach and unified experience comes more responsibility to provide that reach.

The above screenshot says that my browser isn't supported. That's not true, they don't support my operating system. I tried to open that link in Chrome for Android and the normal Android browser and all three of them had the same experience.

The screenshot also says that I may be able to use iCloud with my phones mail, calendar, and contacts. They know that I'm trying to access a photo album. How is what they're suggesting as a possible fix applicable to my situation? 

Providing your customers (or potential customers) a great experience means being helpful when things don't go as they expect. It's not enough to make the experience work as you expect, you need to make the experience work as THEY expect.

Monday, January 12, 2015

How To Become A Great Software Engineer

I've been in the software industry for well over a decade now and today I thought I would reflect on the qualities I've seen in great software engineers that aren't just about the code they write.

Great software engineers are curious about everything. Because great software engineers are curious about everything they tend to dive much deeper into learning technologies. They're not satisfied with just learning how to use something but want to know how it works as well. This helps the engineer to better pick the *correct* solutions for the task at hand. It's important to note that diving too deep can also be a crux as you get stuck in analysis paralysis. Great software engineers recognize when they've learned enough to move on a start delivering.

Great software engineers are passionate about what they build. Not matter what they're building they want it to be great. Whether it's a script, a site, a process, or an application they want it to be used and want it to be a positive reflection of their ability. Because of this they give 100% on everything they build.

Great software engineers don't want to reinvent the wheel. Because these engineers are so curious they naturally become aware of what software is out there. They play with lots of different technologies, frameworks, and platforms. This gives them better judgement about what the correct solutions are to integrate with and what software they need to build themselves.

Great software engineers understand that software engineering is about more than just code. Software is part of a larger system. A great software engineer thinks about hardware, security, operations, deployment, and the maintainability of code.

Great software engineers are willing to do the stuff no one else wants to do. This doesn't mean that they volunteer for all the crappy work. But they understand that sometimes you have to pay your dues. They also understand that the crappy work is a good way to practice their skills.

Great software engineers have learned to make others better. These engineers have learned that you can't do it by yourself. They've also learned that not having a cohesive system is an anti-pattern. Because of this they *want* those around them to be better. Therefore they invest in teaching and mentoring others. In turn they learn more about their knowledge gaps and become better engineers themselves.