Web apps for mobile devices

Strange as it seems, there is a way to develop applications for mobile devices, where a) most of the code is portable across different platforms; b) based on open standards; c) most of the logic is written with an interpreted language. It's called 'web apps'.

Yet more strange is seeing the mobile world 'leapfrogging' the desktop in this respect. Every OS has had the choice of adopting a similar technology as a fully-supported way to write applications.

Web apps are, as the name says, based on "Web" technologies: Javascript, HTML, CSS, etc. which are called collectively HTML5. Such an app runs on a 'sandbox', which can be:

The second option is more powerful, because any platform API that is not exposed to Javascript may be bridged by the native application 'shell'.

Being standard, chances are that almost 100% of the HTML5 code rus unchanged in every platform, as well as on a regular desktop browser. A practical fact increases the portability: almost every mobile HTML renderer is based on WebKit.

My recent endeavours on web apps are related to the Web HP-12C emulator. I wanted to create versions for Android and iPhone, expending the minimum effort possible. Since it is a Web-based app already, using the web app approach promised to be easy.

Android

Creating and publishing a package for Android is really easy, so I chose the native app + HTML widget approach from the beginning.

Indeed, creating an Activity with a WebKit inside, and throwing the Web page contents in it was done in mere seconds. Then, came the UI refining.

First, a small change in HTML:

<meta name="viewport" content="width=450" />
This WebKit-only header item specifies the "virtual resolution" of the HTML renderer space. The result is then scaled to fit in actual width of WebKit widget on screen.

I specified 450 pixels because the original emulator has exactly this width. Key positions, display segments etc. are all based on this number. Changing width would imply recalculating a lot of things. It was some trouble that I didn't want to take at first version.

I believe that a Web page with fixed resolution is (or should be) more the exception than the rule. In case of pages based on relative sizing and positioning, the viewport's width can be set to device-width.

If you don't set the viewport at all, WebKit will try to find one that fits your page, plus some spare margin. It works but in the case of calculator I wanted to use as much of screen as possible.

Thamt brings the second problem: proportion. The emulator is 450x281 pixels exactly. If the screen has an aspect ratio that is flatter than that, the lower portion will spill out of the screen.

So I would need to set either the width or the height of viewport, depending on actual display resolution. As it turned out, setting height did not help, and WebKit does not choose the 'best' direction if you set both height and width in viewport.

The solution I found was to keep viewport width equal to 450, leave viewport's height alone, and set real widget width from outside, causing virtual height to fit. The following code determines if there is a need to do this:

public class Andro12CActivity extends Activity {
    private int getMargin(double width, double height) {
        double proportion = width / height;
        double hp_proportion = 450.0 / 281.0;
        int margin = 0;

        if (proportion > hp_proportion) {
            // limit is height; add margins to eat excess width
            margin = (int) (width - (height * hp_proportion)) / 2;
        } else {
            // limit is width; viewport takes care of this
            margin = 0;
        }

        return margin;
    }

The following private class intercepts navigation actions like loading the URL. I'm not sure it is really needed in my app, I probably followed some initial tutorial and this code came along:

    private class HelloWebViewClient extends WebViewClient {
        @Override
        public boolean shouldOverrideUrlLoading(WebView view, String url) {
            view.loadUrl(url);
            return true;
        }
    }

The following methods are to be accessible from Javascript. Standard emulator uses cookies to save calculator's state, but it does not work as expected in a web-app, so I implement state saving/restoring from outside. A way to show "alerts" was implemented too.

    public String recvCookie() {
        SharedPreferences sp = getPreferences(Activity.MODE_PRIVATE);
        String cookie = sp.getString("c1", "empt");
        return cookie;
    }
    
    public void alertJS(String text) {
        Toast.makeText(this, "Alert JS:" + text, Toast.LENGTH_SHORT).show();
    }
    
    public void sendCookie(String c) {
        SharedPreferences sp = getPreferences(Activity.MODE_PRIVATE);
        SharedPreferences.Editor ed = sp.edit();
        ed.putString("c1", c);
        ed.commit();
   }

These methods are not automatically available to Javscript API. We are going to export them into JS space, later.

They also mean that original emulator code needed to be changed in order to save and restore state. I "patched" the original save/restore methods so I just needed a second Javascript file instead of an Android-specific engine (which is compressed/obfuscated).

Then we have a big onCreate method for Activity, which does pretty much all of setup:

    /** Called when the activity is first created. */
    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
        
        WebView webview = (WebView) findViewById(R.id.webview);
        webview.getSettings().setJavaScriptEnabled(true);
        webview.setWebViewClient(new HelloWebViewClient());

In the following command, we export an object called Portal to Javascript, which is linked to this, that is, the Activity object itself, making recvCookie and sendCookie methods available to Javascript side.

        webview.addJavascriptInterface(this, "Portal");

The following setup is related to the nature of application. A true calculator does not get 'focus' on a button, so the emulator should not get focus either:

        webview.setFocusable(false);
        webview.setFocusableInTouchMode(false);
        webview.setVerticalScrollBarEnabled(false);
        webview.setHorizontalScrollBarEnabled(false);

Block any scroll event that user might trigger inadvertently. This is again something that might not be done in a 'normal' web app. Remember that we set viewport and proportion so calculator always fit wholly on screen.

        webview.setOnTouchListener(new View.OnTouchListener() {
            public boolean onTouch(View v, MotionEvent event) {
              return (event.getAction() == MotionEvent.ACTION_MOVE);
            }
          });

Load the HTML 'page', whose files are at project's assets/ folder:

        webview.loadUrl("file:///android_asset/index.html");

Then we grab display real size, calculate margins for the HTML renderer (if necessary) and set. There is one problem: we can't discover the status bar height (at least I didn't find any API to do that), so I estimated it as 24 -- not before losing a lot of time trying to solve it in a more elegant way...

If estimative is wrong, a narrow strip of emulator's bottom will fall out of screen. It is not a problem (provided that status bar has a 'sane' height) because the lowest twenty-something pixels of emulator are decorative anyway.

        DisplayMetrics metrics = new DisplayMetrics();
        getWindowManager().getDefaultDisplay().getMetrics(metrics);
        int height = metrics.heightPixels - 24; // discount for status bar
        int width = metrics.widthPixels;
        int margin = getMargin(0.0 + width, 0.0 + height);
        ViewGroup.MarginLayoutParams p = ((ViewGroup.MarginLayoutParams) webview.getLayoutParams());
        p.setMargins(margin, 0, margin, 0);
        webview.setLayoutParams(p);
    }

Here, we ignore orientation changes. I wanted the calculator to be dead set in landscape mode. (By the way, orientation is set and soft keyboard is disabled at project manifest XML).

   
    @Override
    public void onConfigurationChanged(Configuration newConfig) {
      // ignore orientation/keyboard change
      super.onConfigurationChanged(newConfig);
    }
}

And that's it. The Andro12C app that you can find at App Market is basically this Activity class, bundled with Web emulator's files so it works offline.

Touch events

The first version of Andro12C seemed to be very slow. You touched a key, and it took almost one second to react. Initially I blamed Javascript (you know, the thing is interpreted, so we subconsciously think it should be slow!).

But the actual problem was the onClick event. In a normal desktop, it is perfect. But in a touch device, the UI must detect when user touches a finger, and withdraws it without dragging. Only after all these events the onClick macro-event is fired.

The general solution was suggested by a friend: get the raw touch-down event. In fact, mobile WebKit has the onTouchDown event. So it was just a matter of replacing it on emulator code.

iOS (iPhone, iPad, iPod Touch), as web app

iOS defines more formally a 'web app', that runs on browser, but can be 'saved' and whose icon appears along with native applications, and it works offline. When iPhone was launched, there was no SDK, so this was actually the only way to write an 'application' for it.

The 'app' is merely a (slightly modified) Web page that still works in desktop browsers. The main advantage is that application distribution is very easy; it is merely a Web page. No need to pay US$ 99/year to distribute it! The main disvantage is that you can't make a package of such an app and distribute it via App Store, so you can't make money on it (not directly, at least).

Since I hadn't enrolled as an iOS developer at the time, I adopted this strategy. An webapp of mine can be found here. (But I plan to make a 'native' version for iOS, too. That will be described in another article.) UPDATE: the "native" webapp for iOS is described in the end of this very article.

Let's begin by seeing the iOS-specific augmentations in HTML file:

<html manifest="cache.manifest">
<head>
<title>iPhone-12C</title>
<link rel=STYLESHEET href="hp12c-iphone.css" type="text/css">
<SCRIPT SRC="hp12c-min-android.js" TYPE="text/javascript"></script>
<SCRIPT SRC="hp12c-iphone-patch.js" TYPE="text/javascript"></script>
<meta name="viewport" content="user-scalable=0, width=460"/>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-touch-fullscreen" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<link rel="shortcut icon" href="favicon.ico">
<link rel="apple-touch-icon" href="hp12c-iphone-icon.png"/>

The most important addition is the manifest. The cache.manifest file, that we will see later, is what enables the web app to be saved, work offline and be updated when a new version is available at original site.

One funny thing: the offline caching only worked if the manifest file had exactly the above name: cache.manifest. It looks like an iOS 4.3 bug. Any other name, and the app would not work offline (even though the manifest was being fetched).

Then we have the Javascript engine (with touch event fix inherited from Android), a Javascript "patch" for webapp-specific APIs (seen later). As in Android, I set the viewport to a fixed pixel width. In case of iOS, I don't need to worry about different screen sizes, because proportion is (still) fixed in iOS devices.

Then we have some 'pragmas' like apple-mobile-web-app-capable, apple-touch-fullscreen that mean exactly what they say. The apple-mobile-web-app-status-bar-style changes to status bar style to match better the emulator's colors. The apple-touch-icon refers to an icon file that must be exactly 57x57; this will be the icon shown among other applications, when webapp is saved.

From the point of view of HTML, customization for iOS is done! At Javascript side, we need to use a different status loading/storing mechanism, because again cookies won't behave as expected.

Again I opted to "patch" original methods instead of crafting a new engine just for iOS.

Hp12c_storage.prototype.save = function() {
	localStorage.setItem('HP12W', H.storage.save_memory2(H.machine));
};

Hp12c_storage.prototype.load = function() {
	var sserial = "" + localStorage.getItem('HP12W');
	...
};

One problem of a webapp is that browser's URL bar will still show up. A dirty trick to hide it is simply to scroll the page a bit upon loading. (Unfortunately the user stil can scroll the page back to show the bar.)

window.addEventListener("load",function() {
  // Set a timeout...
  setTimeout(function(){
    // Hide the address bar!
    window.scrollTo(0, 1);
  }, 0);
});

Then the cache manifest file. It is actually a HTML5 standard, so in theory it drives the cache of any HTML5-capable browser. It is capable of a number of tricks, like getting some files online and other offline, and/or using offline versions as fallbacks. But our app just wants to have everything offline, so the manifest is merely a list.

Two important details. First, the iOS 4.3 bug that breaks caching if the manifest has any name different from cache.manifest. Second, the manifest is verified bytewise for updates. Just touching the file won't update anything, even if the referred files have changed.

The solution is to add a comment, and change it every time the web app has been updated. This is probably best done automatically. A suggested comment is date/time of last update, as I did below:

CACHE MANIFEST
# 20110726 1604

hp12c-iphone.html
hp12c-min-android.js
hp12c-iphone-patch.js
hp12c-iphone.css
hp12c.jpg
hp12c-iphone-icon.png
lcd/lcda.png
lcd/lcdb.png
lcd/lcdc.png
lcd/lcdd.png
lcd/lcde.png
lcd/lcdf.png
lcd/lcdg.png
lcd/lcdp.png
lcd/lcdt.png
favicon.ico

iOS, "native" web app

Conceptually, the "native" iOS version of a webapp is exactly like Android version: a UIWebView element that loads local files. Even the view class name is the same.

Beginning with a XCode template project, I threw a UIWebView in the view using the interface builder and connected it to an outlet: html, which you will see in the controller code below.

Since the project template generates most of the code, I will only list the relevant parts. I chose to specify many UIWebView properties via interface builder, so there is a lot less setup in code than in Android verion.

- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSURL *url = [NSURL fileURLWithPath:[[NSBundle mainBundle]
			pathForResource:@"index" ofType:@"html"]
			isDirectory:NO];    
    [html loadRequest:[NSURLRequest requestWithURL:url]];
}

- (void)webViewDidFinishLoad:(UIWebView *)webView {
    // do any other setup when page is loaded...
}
Calling native methods from Javascript is not trivial as it was in Android. I employed a well-known trick that can be found in several Internet pages (I did not invent it):
- (BOOL) webView:(UIWebView *)view 
shouldStartLoadWithRequest:(NSURLRequest *)request 
 navigationType:(UIWebViewNavigationType)navigationType {
    
    NSString *requestString = [[request URL] absoluteString];
    NSArray *components = [requestString componentsSeparatedByString:@":"];
    
    if ([components count] > 1 &
	[(NSString *)[components objectAtIndex:0]
                     isEqualToString:@"some_id"]) {
	    if([(NSString *)[components objectAtIndex:1]
                            isEqualToString:@"some_method"]) {
                [self some_method];
            }
            return NO;
    }
    
    return YES;
}
It basically overrides the document load handling. In order to call some_method, you do the following in Javascript:
document.location = "some_id:some_method:1";
You can even pass parameters, separated by colons. Terrible :)

The actual web app HTML/CSS content must pay attention to the same details (viewport, touch events) already mentioned in previous flavors of web apps.

In the other hand, if your HTML content already works on Android and/or as a non-native web app, it's probably ready to be bundled as a "native" version. You just need to remove the cache manifest.

One nice thing about iOS is the small variety of display sizes: just two for iPhone (both sharing the same aspect) and one for iPad. You tailor the HTML for the desired targets and never worry again, while in Android we had to add a lot of code to cope with unexpected display sizes and proportions.

Regarding Javascript, the localStorage works fine in "native" version, as it did in web app, so no need to change the code. Calling native events needs the drudgery I showed some paragraphs above. That's basically it.

PhoneGap

I don't know PhoneGap, but looks like it is a very popular tool for HTML5 mobile development. It bundles all those tricks we have shown (and many, many more) so the developer can focus completely on HTML5 side. It also offers APIs for camera, accelerometer and all those goodies that can be found on mobiles.

I did not test it because I really wanted to understand how things work regarding HTML5 and mobile platforms (and, for instance, how PhoneGap can work). But for developers that need to ship fast and/or want to focus in HTML5 side, it's probably a good tool to adopt.

blog comments powered by Disqus