Thursday, July 14, 2011

Android #11: Working with WebViews

WebView is a view that displays web pages. WebViews can be used to render HTML content within your Android application. Typically a WebView uses the Android web kit browser within, but the difference or benefit we achieve is that we can embed HTML content from within our native application. A WebView uses the same rendering and JavaScript engine as the browser, but it runs under the control of your application. Another main feature in this approach is that we can have Activity-to-JavaScript communication possible. That is, a JavaScript function can invoke an Activity function and vice versa.

Embedding a WebView into an application activity

Android platform provides the API for rendering arbitrary HTML within your app by allowing the built-in web browser as a widget within the app. HTML content can come from anywhere, either from any of the below
  • Local HTML files stored in your assets of your Android app
  • Downloaded from the web
  • HTML strings
Here is an example on dealing with the three WebViews embedding three different sources of HTML content (as above) and also demonstrates on how to bind Java and JavaScript code for activity and web content communication




layout/main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical" android:layout_width="fill_parent"
android:layout_height="fill_parent"> <TextView android:layout_width="fill_parent"
android:layout_height="wrap_content" android:text="WebViewDemo"
android:padding="4dip" android:textSize="16sp" />

<WebView android:id="@+id/webview1" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_weight="0" />

<WebView android:id="@+id/webview2" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_weight="0" />

<WebView android:id="@+id/webview3" android:layout_width="fill_parent"
android:layout_height="wrap_content" android:layout_weight="0" />

</LinearLayout>

WebViewActivity.java
package in.satworks.android.samples.activity;import android.app.Activity;
import android.os.Bundle;
import android.os.Handler;
import android.webkit.JsResult;
import android.webkit.WebChromeClient;
import android.webkit.WebSettings;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.Toast;

/** * This class demonstrates on how to embed a WebView in your activity. Also
* demonstrates how to have JavaScript in the WebView call into the activity,
* and how the activity can invoke a JavaScript function.
*/

public class WebViewActivity extends Activity { private WebView mWebView1, mWebView2, mWebView3;
private Handler mHandler = new Handler();

/** Called when the activity is first created. */ @Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);

// First WebView with HTML Strings
mWebView1 = (WebView) findViewById(R.id.webview1);
mWebView1.loadData("This is plain HTML text string - BOLDED and
COLORED RED
",
"text/html", "utf-8");

// Second WebView with Local HTML file stored under Android Assets
mWebView2 = (WebView) findViewById(R.id.webview2);
mWebView2.setWebChromeClient(new MyWebChromeClient());
mWebView2.loadUrl("file:///android_asset/demo.html");
WebSettings webSettings2 = mWebView2.getSettings();
// By default, the JavaScript support of the WebView object we are
// working with is turned off.
// In order to turn on the web view support for the JavaScript language
// we should call the setJavaScriptEnabled() method.
webSettings2.setJavaScriptEnabled(true);
// register interface class containing methods to be exposed
// to JavaScript. Takes two params namely, java object we need to expose
// and the reference name to use within JavaScript code
mWebView2.addJavascriptInterface(new MyJavaScriptInterface(),
"androidmethodref");

// Third WebView with external HTML reference mWebView3 = (WebView) findViewById(R.id.webview3);
mWebView3.loadUrl("http://www.google.com/");
WebSettings webSettings3 = mWebView3.getSettings();
webSettings3.setJavaScriptEnabled(true);
// Below is required in order to embed the HTML response output within
// the current activity instead of moving out to Android Browser window
mWebView3.setWebViewClient(new MyWebViewClient());

}/**
* WebViewClient provides a way to embed the WebView call of loadUrl within
* the app layout instead of opening a new independent Android browser
* window
*/
final class MyWebViewClient extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
return super.shouldOverrideUrlLoading(view, url);
}

}/**
* WebChromeClient provides a hook for calling "alert" from JavaScript.
* Useful for debugging JavaScript.
*/
final class MyWebChromeClient extends WebChromeClient {
@Override
public boolean onJsAlert(WebView view, String url, String message,
JsResult result) {
System.out.println("This message is from Javascript alert=>"
+ message);
Toast.makeText(
getApplicationContext(),
"Activity to WebView Communication: \n" + message
+ " from JavaScript function", Toast.LENGTH_SHORT)
.show();
result.confirm();
return true;
}
}

/** * JavaScript interface containing methods that can be invoked by the
* JavaScript within HTML. It also in turn invokes the JavaScript method
*/
final class MyJavaScriptInterface {

MyJavaScriptInterface() { }

/** * This method will be invoked via the JavaScript event from within HTML
* through the registered reference name 'androidmethodref'. This is not
* called on the UI thread. Post a runnable to invoke loadUrl on the UI
* thread.
*/
public void clickOnAndroid() {
System.out.println("Method - clickOnAndroid called from Javascript embedded within HTML (demo.html).");
Toast.makeText(
getApplicationContext(),
"WebView to Activity Communication: \nclickOnAndroid: Called from Javascript Event",
Toast.LENGTH_SHORT).show();
mHandler.post(new Runnable() {
// Call to the HTML JavaScript Method
public void run() {
mWebView2.loadUrl("javascript:greetUser()");
}
});
}
}

}

assets/demo.html
<!DOCTYPE HTML>
<html>
<head>
<script type="text/javascript">
// This function is invoked by the Android Activity
function greetUser()
{
alert("Welcome! "+document.forms[0].nameVal.value);
}
</script>
</head>
<form>
<body bgcolor="lavender">
<input type="text" name="nameVal" value="Sat"/>
<!-- Calls into the JavaScript interface for the activity //-->
<input type="button" value="Greet User" onClick="window.androidmethodref.clickOnAndroid()"/>
</body>
</form>
</html>

values/strings.xml
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">WebViewApp</string>
</resources>

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="in.satworks.android.samples.activity" android:versionCode="1"
android:versionName="1.0">
<application android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:name=".WebViewActivity" android:label="@string/app_name">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
<uses-sdk android:minSdkVersion="8" />

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

Reference:

Android #10: Working with Android Services

Activities and Content Providers are short lived components and can be shut down any time whereas Services are designed to keep running if required as an independent Activity. For example, they can check for any updates on RSS feed or playing background music even if the controlling Activity is no longer operating. So in order to create a application that runs on background of other current activities, one need to create a service.

Services are of two categories

  1. Unbound Services – They are services that run in background indefinitely. That is, unbound service is one which gets initiated when an activity is started and continues to run even when that activity is closed.

  2. Bound Services – They are services that run in background at the lifespan of a calling activity. That is, bound service is one which gets initiated when an activity is started and destroys or its life span ends when that activity is closed.
Working with Unbound Services:

Here is an example of unbound service that will be initiated by an application activity and even after that activity is closed, will find the service to be running in background.







layout/main.xml
<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:orientation="vertical" android:layout_width="fill_parent"

android:layout_height="fill_parent" android:gravity="center">

<TextView android:layout_width="fill_parent"

android:layout_height="wrap_content" android:text="Activity UnBound Services Demo"

android:textSize="20sp" android:padding="10dp" />

<Button android:layout_width="fill_parent"

android:layout_height="wrap_content" android:id="@+id/buttonStart"

android:text="Create/Start UnBound Service"></Button>

<Button android:layout_width="fill_parent"

android:layout_height="wrap_content" android:text="Stop UnBound Service" android:id="@+id/buttonStop">
</Button>

</LinearLayout>

SimpleActivityUnBoundService.java
package in.satworks.android.samples.service;import android.app.Service;

import android.content.Intent;

import android.os.IBinder;

import android.widget.Toast;

public class SimpleActivityUnBoundService extends Service {
 @Override

public IBinder onBind(Intent arg0) { return null;

}

@Override
// Service Created - Called only one time when when the service is first created

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

Toast.makeText(this, "Service onCreate called: Service Created", 3).show();}

@Override
// Service Stopped - Called when the service is destroyed

public void onDestroy() {super.onDestroy();

Toast.makeText(this, "Service onDestroy called: Service Stopped", 3).show();}

@Override
// Service Started - Called every time when user tries to start the same service

public void onStart(Intent intent, int startId) {super.onStart(intent, startId);

Toast.makeText(this, "Service onStart called: Service Started", 3).show();}

}

ServiceDemoActivity.java
package in.satworks.android.samples.activity;
import in.satworks.android.samples.R;
import in.satworks.android.samples.service.SimpleActivityUnBoundService;

import android.app.Activity;

import android.content.Intent;

import android.os.Bundle;

import android.view.View;

import android.view.View.OnClickListener;

import android.widget.Button;

import android.widget.Toast;

public class ServiceDemoActivity extends Activity implements OnClickListener{

Button buttonStart, buttonStop;

@Override

public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.main);

buttonStart = (Button) findViewById(R.id.buttonStart);

buttonStop = (Button) findViewById(R.id.buttonStop);

buttonStart.setOnClickListener(this);

buttonStop.setOnClickListener(this);

}

@Override
public void onClick(View v) {

switch (v.getId()) {

case R.id.buttonStart:

startService(new Intent(this, SimpleActivityUnBoundService.class));

break;

case R.id.buttonStop:

stopService(new Intent(this, SimpleActivityUnBoundService.class));

break;

}

}

@Override protected void onDestroy() {

super.onDestroy();

Toast.makeText(this, "Calling Activity Destoyed", 3).show();

}

}

values/strings.xml
<?xml version="1.0" encoding="utf-8"?>

<resources>

<string name="app_name">ServiceDemoApp</string>

</resources>

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

package="in.satworks.android.samples" android:versionCode="1"

android:versionName="1.0">
<application android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:name=".activity.ServiceDemoActivity" android:label="@string/app_name">

<intent-filter>

<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />

</intent-filter>

</activity>

<service android:enabled="true" android:name=".service.SimpleActivityUnBoundService" />

</application>

<uses-sdk android:minSdkVersion="8" />

</manifest>

Working with Bound Services:

Here is an example of bound service that will be initiated by an application activity and its lifecycle ends when the activity is closed.




layout/main.xml
<?xml version="1.0" encoding="utf-8"?>

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"

android:orientation="vertical" android:layout_width="fill_parent"

android:layout_height="fill_parent" android:gravity="center">

<TextView android:layout_width="fill_parent"

android:layout_height="wrap_content" android:text="Activity Bound Services Demo"

android:textSize="20sp" android:padding="10dp" />

<Button android:layout_width="fill_parent"

android:layout_height="wrap_content" android:id="@+id/buttonStart"

android:text="Start"></Button>

</LinearLayout>

SimpleActivityBoundService.java
package in.satworks.android.samples.service;import android.app.Service;

import android.content.Intent;

import android.os.IBinder;

import android.widget.Toast;

public class SimpleActivityBoundService extends Service { @Override

public IBinder onBind(Intent arg0) {

return null;

}

@Override // Service Created - Called only one time when when the service is first created

public void onCreate() {

super.onCreate();

Toast.makeText(this, "Service onCreate called: Service Created", 3).show();

}

@Override // Service Stopped - Called when the service is destroyed

public void onDestroy() {

super.onDestroy();

Toast.makeText(this, "Service onDestroy called: Service Stopped", 3).show();

}

}

ServiceDemoActivity.java
package in.satworks.android.samples.activity;import in.satworks.android.samples.R;

import in.satworks.android.samples.service.SimpleActivityBoundService;

import android.app.Activity;

import android.content.Intent;

import android.os.Bundle;

import android.view.View;

import android.view.View.OnClickListener;

import android.widget.Button;

import android.widget.Toast;

public class ServiceDemoActivity extends Activity implements OnClickListener{
Button buttonStart;

@Override public void onCreate(Bundle savedInstanceState) {

super.onCreate(savedInstanceState);

setContentView(R.layout.main);

buttonStart = (Button) findViewById(R.id.buttonStart);

buttonStart.setOnClickListener(this);

}

@Override public void onClick(View v) {

switch (v.getId()) {

case R.id.buttonStart:

bindService(new Intent(this, SimpleActivityBoundService.class), null, BIND_AUTO_CREATE);

break;

}

}

@Override protected void onDestroy() {

super.onDestroy();

Toast.makeText(this, "Calling Activity Destroyed", 3).show();

}

}

values/strings.xml
<?xml version="1.0" encoding="utf-8"?>

<resources>

<string name="app_name">ServiceDemoApp</string>

</resources>

AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>

<manifest xmlns:android="http://schemas.android.com/apk/res/android"

package="in.satworks.android.samples" android:versionCode="1"

android:versionName="1.0">
<application android:icon="@drawable/icon" android:label="@string/app_name">
<activity android:name=".activity.ServiceDemoActivity" android:label="@string/app_name">

<intent-filter>

<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />

</intent-filter>

</activity>

<service android:enabled="true" android:name=".service.SimpleActivityBoundService" />

</application>

<uses-sdk android:minSdkVersion="8" />

</manifest>

Best Practices: Cross-Context Dispatching and Session Handling

It’s been quite some time since I blogged. Thought of blogging this topic as recently we had faced a similar kind of problem that was faced earlier in another project. Just before going on to the problem and solution, let us first understand few basics and then move forward.

Understanding Session Tracking and JSESSIONID:

Basically a web container can use several methods to associate a session with a user, all of which involve passing an identifier between the client and the server. The identifier can be maintained on the client as a cookie, or the web component can include the identifier in every URL that is returned to the client.

JSESSIONID is a unique identifier of the HTTP session generated by the web container, stored within the cookie and is also the default cookie name. JSESSIONID cookie is created/sent when session is created. If cookies are turned off, the identifier is encoded in the URL link and if the cookies are turned on, the URL is simply empty and the identifier is available in the cookie itself.

Another misconception is that session gets created during first request which is not actually the case and totally up to the code to determine on when it should be created. To better understand this behavior,

  • In Java Servlet, new session is created when your code calls request.getSession() or request.getSession(true) for the first time. If you just want to get session if exists and do not want to create it if it doesn't exists, use request.getSession(false) that will return you a session if exists or null if it doesn’t. In this case, new session is not created and JSESSIONID cookie is not sent to the client browser that also means the session is not necessarily created on first request.

  • In JSP, new session is created whenever a call is made to the JSP page for the first time and uses the already available session for other subsequent requests. If we need to turn off this feature in a particular JSP page, use session=”false” page directive in which case session variable is not available on that JSP page at all.


Problem Description:

In case there are two or more different web application contexts accessed by the same user from the same domain, each request from the same user will generate cookie with its own session id that will get overridden and loose the session it had already set up.

By default all popular browser has the parent (main) window and the child (pop-up) window sharing the same cookie whenever the session is tracked using cookies. Assume the scenario where there are two different web applications deployed under the same server domain and each are accessed via their own context roots, the parent window accesses the first application and child window with the second application. In this case, as parent window and child window accesses two different applications from the same domain, session information gets overridden because the cookie name is same and the new Identifier (JSESSIONID) named cookie gets overridden due to which parent window looses it session data.

For instance, we had two different applications (say for example /app1 and /app2) running under the same domain (say for example http://www.samedomain.com/), parent window accesses the first application (http://www.samedomain.com/app1/) and through it, opens a child window with the second application (http://www.samedomain.com/app2) . When the parent window opens, it creates a cookie named with JSESSIONID and stores client session related information. As soon as the child window is opened, the existing session gets overridden by the app2 session as cookie names are same resulting in loss of session data in parent screen. Post this, whenever user navigates within parent screen for anything session specific will not fetch them the required data.

Solution Description:

Any of the below two approaches can be employed to prevent the session override problem

  1. Changing the cookie name from default value.

  2. Changing the cookie path (in case certain application server doesn’t allow to change the cookie name, we can create the same JSESSIONID named cookie into two different paths thus a work around way to prevent the override issue)


As each web container implementation differs even though they follow the same specification, above approaches can be employed using their proprietary configurations. Below are two different examples through which the above problem can be solved.

Illustration by Examples:

In Weblogic Application Server, changing the cookie name from default value. Suppose we have two applications deployed on Weblogic Application server, we can change the cookie name from its default JSESSIONID named cookie. This way, we can create application-specific cookies to avoid cookie override that in turn avoids the HTTP session override issue. Modify the weblogic.xml configuration specific to each application as below

<session-descriptor>
<session-param>
<param-name>CookieName</param-name>
<param-value>App1Cookie</param-value>
</session-param>
</session-descriptor>

By changing the cookie name, we ensure two different cookies created for each applications, thus ensuring that app1 and app2 sessions doesn't get overridden.

In Tomcat Web Server or JBoss Application server, changing the cookie path. We have the property named emptySessionPath used by the Apache connector’s configuration that states whether all cookie should be stored in the root folder “/” or not (otherwise) . This way, we can have cookies placed in two different location to avoid cookie override that in turn avoids HTTP session override issue. If emptySessionPath is enabled and set as “true” in Tomcat or JBoss, the JSESSIONID cookie is written to the root "/" path. If emptySessionPath is set to “false”, there are multiple cookies in the browser, one for each web app (none at the root), so different web apps are not re-writing each other's cookie. Modify the server.xml configuration specific to each application as below


<Connector port="8080" address="${jboss.bind.address}"maxThreads="5" maxHttpHeaderSize="8192"
emptySessionPath="false" protocol="HTTP/1.1" enableLookups="false" redirectPort="8443"
acceptCount="100" connectionTimeout="20000" disableUploadTimeout="true" />
<Connector port="8009" address="${jboss.bind.address}" protocol="AJP/1.3"
maxThreads="200" emptySessionPath="false" enableLookups="false" redirectPort="8443" />

By changing the emptySessionPath to "false", we ensure that the session cookies of app1 gets created at “/app1” instead of the default path of “/”, thus ensuring that app1 and app2 sessions doesn't get overridden.