drupal analytics

http://chiuki.github.com/progressive-webview

What is progressive enhancement?

  • Layered functionality
  • Graceful degradation

Amazon: Drop-down menu

Amazon: no javascript

Progressive enhancement on mobile

  • HTML + Javascript → Interactive
  • Webpage + Mobile wrapper → Native functionalities

WebView

  • Webkit component to show a web page in Android
  • Supports most HTML5 features
    • Application cache
    • Canvas
    • Geolocation
    • Web storage

Native functionalities

  • Accelerometer
  • Bluetooth
  • Camera
  • Contacts
  • Microphone
  • NFC
  • Storage
  • Telephony
  • Vibrate

Progressive enhancement on Android

  • Embed WebView
  • Write a javascript bridge
  • Enhance webpage with Android functionalities when javascript bridge is available

Android intro

Hello World

public void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);

  WebView mWebView = new WebView(this);
  setContentView(mWebView);

  mWebView.getSettings().setJavaScriptEnabled(true);
  mWebView.loadUrl("file:///android_asset/hello.html");
  mWebView.addJavascriptInterface(
    new JavaScriptBridge(this), "MyAndroid");
}

JavascriptBridge

private class JavaScriptBridge {
  private Context mContext;

  public JavaScriptBridge(Context context) {
    mContext = context;
  }

  public void showToast(String msg) {
    Toast.makeText(mContext, msg, Toast.LENGTH_SHORT).show();
  }
}

hello.html

<html>
  <head>
    <script type="text/javascript">
      function sayHello() {
        if (typeof MyAndroid === 'undefined') {
          alert('Hello');
        } else {
          MyAndroid.showToast('Hello');
        }
      }
    </script>
  </head>
  <body>
    <a href="javascript:sayHello();">Say hello</a>
  </body>
</html>

Hello in browser

Hello in app

Access the phone book

<html>
  <head>
    <script type="text/javascript">
      function pickContact() {
        if (typeof MyAndroid === 'undefined') {
          return;
        } else {
          MyAndroid.pickContact();
        }
      }
    </script>
  </head>
  <body>
    <label id="emailLabel" onClick="pickContact();">Email</label>
    <input id="email" type="text" />
  </body>
</html>

Android: startActivityForResult

private class JavaScriptBridge {
  // Constructor omitted

  public void pickContact() {
    Intent intent = new Intent(Intent.ACTION_PICK,
      ContactsContract.Contacts.CONTENT_URI);
    startActivityForResult(intent, R.id.request_code_pick_contact);
  }
}

res/values/ids.xml

<resources>
  <item name="request_code_pick_contact" type="id" />
</resources>

Android: onActivityResult

protected void onActivityResult(int requestCode, int resultCode,
    Intent data) {
  if (resultCode != RESULT_OK) {
    return;
  }
  switch (requestCode) {
    case R.id.request_code_pick_contact:
      fillEmail(data);
      break;
  }
}

Android: fillEmail

private void fillEmail(Intent data) {
  String id = String.valueOf(ContentUris.parseId(data.getData()));
  Cursor cursor = getContentResolver().query(
      Email.CONTENT_URI, null,
      Email.CONTACT_ID + "=?",
      new String[]{ id }, null);

  if (cursor.moveToFirst()) {
    int index = cursor.getColumnIndex(Email.DATA);
    String email = cursor.getString(index);
    mWebView.loadUrl("javascript:fillEmail('" + email + "')");
  } else {
    Toast.makeText(this, R.string.email_error, Toast.LENGTH_SHORT).show();
  }

  cursor.close();
}

Javascript: fillEmail

function fillEmail(email) {
  var input = document.getElementById('email');
  input.value = email;
}
<label id="emailLabel" onClick="pickContact();">Email</label>
<input id="email" type="text" />

Email field in browser

Email field in app

Android: Contacts

Email address from Contacts

Show actionable items

function init() {
  if (typeof MyAndroid === 'undefined') {
    return;
  }
  // Make the email label look like a link in Android
  var label = document.getElementById('emailLabel');
  label.style.color = '#00f';
  label.style.textDecoration = 'underline';
}
<body onLoad="init()">
  <label id="emailLabel" onClick="pickContact();">Email</label>
  <input id="email" type="text" />
</body>

Add photo

<body>
  <label>Photo</label>
  <input id="photoData" type="hidden" />
  <div>
    <img id="photo" style="display: none;" />
  </div>
  <input type="file" value="Pick photo"
      onClick="return pickPhoto();" />
</body>

Add photo

<script type="text/javascript">
  function pickPhoto() {
    if (typeof MyAndroid === 'undefined') {
      return true;  // Let the browser handle it
    }
    MyAndroid.pickPhoto();
    return false;
  }
</script>
<label>Photo</label>
<input id="photoData" type="hidden" />
<img id="photo" style="display: none;" />
<input type="file" value="Add photo" onClick="return pickPhoto();" />

Add photo on browser


Add photo on app

Android: Photo selected

Android: pickPhoto

public void pickPhoto() {
  Intent intent = new Intent(Intent.ACTION_PICK);
  intent.setType("image/*");
  startActivityForResult(intent, R.id.request_code_pick_photo);
}
protected void onActivityResult(int requestCode, int resultCode,
    Intent data) {
  if (resultCode != RESULT_OK) {
    return;
  }
  switch (requestCode) {
    case R.id.request_code_pick_photo:
      fillPhoto(getPhotoFromGallery(data));
      break;
  }
}

Android: getPhotoFromGallery

private Bitmap getPhotoFromGallery(Intent data) {
  Uri selectedImage = data.getData();

  String[] filePathColumn = { MediaStore.Images.Media.DATA };
  Cursor cursor = getContentResolver().query(
      selectedImage, filePathColumn, null, null, null);
  cursor.moveToFirst();

  int columnIndex = cursor.getColumnIndex(filePathColumn[0]);
  String path = cursor.getString(columnIndex);
  cursor.close();

  return BitmapFactory.decodeFile(path);
}

Pass photo to WebView

private void fillPhoto(Bitmap bitmap) {
  ByteArrayOutputStream stream = new ByteArrayOutputStream();
  bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
  byte[] byteArray = stream.toByteArray();
  String encoded
    = "data:image/png;base64," + Base64.encodeBytes(byteArray);
  mWebView.loadUrl("javascript:fillPhoto('" + encoded + "');");
}

Why Base64?

Javascript: Display photo

function fillPhoto(data) {
  var photoData = document.getElementById('photoData');
  photoData.value = data;
  var photo = document.getElementById('photo');
  photo.src = photoData.value;
  photo.style.display = photoData.value == '' ? 'none' : 'block';
}
<input id="photoData" type="hidden" />
<img id="photo" style="display: none;" />

Handle orientation change

protected void onSaveInstanceState(Bundle outState) {
  super.onSaveInstanceState(outState);
  mWebView.saveState(outState);
}

protected void onRestoreInstanceState(Bundle savedInstanceState) {
  super.onRestoreInstanceState(savedInstanceState);
  mWebView.restoreState(savedInstanceState);
}

Stash away the WebView

public class MainActivity extends Activity {
  private WebView mWebView = null;
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    init();
    setContentView(mWebView);
  }

  private void init() {
    if (mWebView != null) {
      return;
    }
    mWebView = new WebView(this);
    mWebView.getSettings().setJavaScriptEnabled(true);
    mWebView.loadUrl("file:///android_asset/index.html");
    mWebView.addJavascriptInterface(
      new JavaScriptBridge(this), "MyAndroid");
  }
}

Do not recreate Activity

<activity
  android:label="@string/app_name"
  android:name=".MainActivity"
  android:configChanges="keyboard|keyboardHidden|orientation" >

Back button

public boolean onKeyDown(int keyCode, KeyEvent event) {
  // Check if the key event was the Back button and if there is
  // history
  if ((keyCode == KeyEvent.KEYCODE_BACK) && mWebView.canGoBack()) {
    mWebView.goBack();
    return true;
  }

  // If it wasn't the Back key or there's no web page history, bubble
  // up to the default system behavior (probably exit the activity)
  return super.onKeyDown(keyCode, event);
}

Security

  • Any web page loaded in your WebView has access to your native calls!
  • Load web pages in your domain in the WebView
  • Defer to system for the rest
  • Bonus: Handles special urls too e.g. maps

Restrict by domain

private void init() {
  // Other init omitted
  mWebView.loadUrl(
    "http://www.sqisland.com/talks/progressive-webview/sample");
  mWebView.setWebViewClient(new MyWebViewClient());
}

private class MyWebViewClient extends WebViewClient {
  public boolean shouldOverrideUrlLoading(WebView view, String url) {
    if (Uri.parse(url).getHost().equals("www.sqisland.com")) {
      // This is my web site, OK to load page
      return false;
    }
    // Otherwise, the link is not for a page on my site, so launch
    // another Activity that handles URLs
    Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
    startActivity(intent);
    return true;
  }
}

Anyone can write an wrapper app

  • Sanitize all input
  • Embed private key

So, when should I do this?

  • Enhanced website
  • Web UI vs Native UI

PhoneGap

PhoneGap: load remote start url?

Summary

  • Embed WebView
  • Write a javascript bridge
  • if (typeof MyAndroid === 'undefined') {
      doSomethingInBrowser();
    } else {
      MyAndroid.doSomething();
    }

Thank you!