CS 491 Lecture 13 – Wevents Part II
Agenda
- design an app for managing family events
- hook up to a remote database
- merge PostgreSQL, PHP, JSON, and Android
Ice Cream Sandwich
It was unveiled yesterday.
Asynchronous networking
Okay, our database backend is in order. Our PHP scripts will serve as the bridge between the database and clients, who will perform all transactions through HTTP. Let’s make our Android client, starting with an activity for authentication. It uses a simple TableLayout. On button-click, we’d like to pass the username and password off the authenticate PHP script. If the PHP script reports success, we can move to our main activity. Otherwise, we sit tight.
We’ve got to ship stuff off to our PHP script. That means a trip over the network. That means don’t do it in the main thread. We could do it in a thread in our Activity, but Activitys can get destroyed if they’re not focused. Let’s farm our network tasks out to a Service. And not just any service. An IntentService, which is ideal for handling asynchronous tasks:
public class WeventsService extends IntentService {
public WeventsService() {
super("WebGetService");
}
@Override
protected void onHandleIntent(final Intent intent) {
// perform some task
}
}
That onHandleIntent method will get called whenever startService is issued. Our service will ultimately have many clients, all of whose requests will funnel into onHandleIntent. So, we need some means of specifying the action we’d like the service to perform. Let’s create some constants for these actions:
public static final String ACTION_GET_ALL = "org.twodee.wevents.ACTION_GET_ALL";
public static final String ACTION_AUTHENTICATE = "org.twodee.wevents.ACTION_AUTHENTICATE";
public static final String ACTION_UPDATE_WIDGET = "org.twodee.wevents.ACTION_UPDATE_WIDGET";
We can issue a request in AuthenticateActivity with:
Intent intent = new Intent(WeventsApplication.ACTION_AUTHENTICATE, null, AuthenticateActivity.this, WeventsService.class);
startService(intent);
We’re using the only Intent constructor that lets us specify both an action and an explicit intent-satisfier. It’d be a good idea to Toast the action in onHandleIntent before moving on:
Toast.makeText(this, intent.getAction(), Toast.LENGTH_SHORT).show();
Reading from a network
On these actions, we want our service to go out and make an HTTP request. For this, Android ships with Apache’s HTTP API, which Google recommends over Java’s. A simple fetch looks like:
private Bundle fetch(String url) {
int resultCode = 0;
String content = null;
HttpClient client = new DefaultHttpClient();
HttpGet request = new HttpGet(url);
try {
ResponseHandler<String> handler = new BasicResponseHandler();
content = client.execute(request, handler);
} catch (ClientProtocolException e) {
resultCode = 1;
content = "client protocol error";
} catch (IOException e) {
resultCode = 1;
content = "I/O error";
}
client.getConnectionManager().shutdown();
Bundle bundle = new Bundle();
bundle.putString("content", content);
bundle.putInt("resultCode", resultCode);
return bundle;
}
Don’t forget! Service code runs in the main thread. We need to spawn a new fetch thread to avoid ANRs. But once fetched, what do we do with the result?
Communicating back to clients
We’d seen in Lonely Phone that apps can bind a service and access it via a local binder. But what if we want the service to push data to an Activity? For that, we need a ResultReceiver. This class imposes a callback onReceiveResult that gets called when someone sends something to it. Unfortunately, it’s a class, not an interface. Since Java doesn’t support multiple inheritance, let’s create another class which advertises its own interface and passes the results along:
public class WeventsReceiver extends ResultReceiver {
private Receiver receiver;
public WeventsReceiver(Handler handler) {
super(handler);
}
public void setReceiver(Receiver receiver) {
this.receiver = receiver;
}
@Override
protected void onReceiveResult(int resultCode, Bundle resultData) {
if (receiver != null) {
receiver.onReceiveResult(resultCode, resultData);
}
}
public interface Receiver {
public void onReceiveResult(int resultCode, Bundle resultData);
}
}
Now our client Activitys can implement WeventsReceiver.Receiver. They can handle the content however they choose:
public class AuthenticateActivity extends Activity implements WeventsReceiver.Receiver {
// ...
@Override
public void onReceiveResult(int resultCode,
Bundle resultData) {
// ...
}
}
The last step is pass our receiver along when we issue the intent to the service:
intent.putExtra("receiver", receiver);
On receiving the intent, the service can run a thread like this:
new Thread(new Runnable() {
@Override
public void run() {
ResultReceiver receiver = intent.getParcelableExtra("receiver");
Bundle bundle = fetch("http://www.uwec.edu/");
receiver.send(bundle.getInt("resultCode"), bundle);
stopSelf();
}
}).start();
Passing parameters via HTTP
To authenticate with the database server, we need to get our username and password to the PHP script. HTTP defines two commands, GET and POST that can help. Both request types can pass along parameters to the web server. GET embeds them in the URL, while POST sends them along after the header. Mostly they are equivalent for our purposes. We’ll use POST.
Our AuthenticateActivity needs to send along the form values via the Intent. The service then picks them off and creates a list of NameValuePairs (Apache’s creation):
ArrayList<NameValuePair> postParameters = new ArrayList<NameValuePair>();
postParameters.add(new BasicNameValuePair("username", intent.getStringExtra("username")));
postParameters.add(new BasicNameValuePair("password", intent.getStringExtra("password")));
Our fetch routine changes a bit:
private Bundle fetch(String url,
ArrayList<NameValuePair> postParameters) {
int resultCode = 0;
String content = null;
HttpClient client = new DefaultHttpClient();
HttpPost request = new HttpPost(url);
try {
UrlEncodedFormEntity formEntity = new UrlEncodedFormEntity(postParameters);
request.setEntity(formEntity);
ResponseHandler<String> handler = new BasicResponseHandler();
content = client.execute(request, handler);
} catch (UnsupportedEncodingException e1) {
resultCode = 1;
content = "bad encoding";
} catch (ClientProtocolException e) {
resultCode = 1;
content = "client protocol error";
} catch (IOException e) {
resultCode = 1;
content = "I/O error";
}
client.getConnectionManager().shutdown();
Bundle bundle = new Bundle();
bundle.putString("content", content);
bundle.putInt("resultCode", resultCode);
return bundle;
}
In PHP, we use our POST data instead of hardcoded values:
<?php
$db = pg_connect("dbname=databasename user=${_POST['username']} password=${_POST['password']}");
if ($db) {
echo 'ok';
} else {
echo 'boo';
}
?>
Now, we pass the result back to the AuthenticateActivity. Neat.
Wevents List
After authentication, we probably want to pop up a view with the list of events. It’ll need to ask the service to perform a query asynchronously and await the results, which will be a JSON structure. Let’s translate the set of events to a bulleted list:
static String jsonEventsToList(String json) {
StringBuilder builder = new StringBuilder();
try {
JSONArray jsonArray = new JSONArray(json);
builder.append("<ol>");
for (int i = 0; i < jsonArray.length(); ++i) {
JSONObject jsonObject = jsonArray.getJSONObject(i);
builder.append("• ");
builder.append(jsonObject.getString("description"));
builder.append("<br/>");
}
builder.append("</ol>");
} catch (JSONException e) {
builder.append("Couldn't parse result!");
}
return builder.toString();
}
Gotcha
There’s an issue we need to address yet. If a client Activity changes configurations or closes while the service is performing an operation, we’re going to run into trouble.