Joe Asks

^ Is All This Delay and Threading Stuff Really Necessary?

One reason you need to do it this way is to avoid making too many calls to the external web service. Imagine what happens as the user enters the word scissors. The program sees the word typed in a character at a time, first s, then c, then i, and so on, possibly with backspaces because nobody can remember how to spell scissors. Do you really want to make a web service request for every character? Not really. Besides putting unnecessary load on the server, it would be wasteful in terms of power. Each request requires the device's radio to transmit and receive several data packets, which uses up a bit of battery power. You want to wait until the user finishes typing before sending the request, but how do you tell they are done?

The algorithm used here is that as soon as the user types a letter, a delayed request is started. If they don't type another letter before the one-second delay is up, then the request goes through. Otherwise, the first request is removed from the request queue before it goes out. If the request is already in progress, we try to interrupt it. The same goes for language changes, except we use a smaller delay. The good news is that now that I've done it once for you, you can use the same pattern in your own asynchronous programs.

adapter.setDropDownViewResource(

android.R.layout.simple_spinner_dropdown_item); fromSpinner.setAdapter(adapter); toSpinner.setAdapter(adapter);

// Automatically select two spinner items fromSpinner.setSelection(8); // English (en) toSpinner.setSelection(11); // French (fr)

In Android, an Adapter is a class that binds a data source (in this case, the languages array defined in arrays.xml) to a user interface control (in this case, a spinner). We use the standard layouts provided by Android for individual items in the list and for the drop-down box you see when you select the spinner.

Next we set up the user interface handlers in the setListeners( ) routine (called from line 44 of onCreate()):

Download Translate/src/org/example/translate/Translate.java

private void setListeners() { // Define event listeners textWatcher = new TextWatcher() {

public void beforeTextChanged(CharSequence s, int start, int count, int after) { /* Do nothing */

public void onTextChanged(CharSequence s, int start, int before, int count) { queueUpdate(1000 /* milliseconds */);

public void afterTextChanged(Editable s) { /* Do nothing */

itemListener = new OnItemSelectedListener() {

public void onItemSelected(AdapterView parent, View v, int position, long id) { queueUpdate(200 /* milliseconds */);

public void onNothingSelected(AdapterView parent) { /* Do nothing */

// Set listeners on graphical user interface widgets origText.addTextChangedListener(textWatcher); fromSpinner.setOnItemSelectedListener(itemListener); toSpinner.setOnItemSelectedListener(itemListener);

We define two listeners: one that is called when the text to translate is changed and one that is called when the language is changed. queue-Update() puts a delayed update request on the main thread's to-do list using a Handler. We arbitrarily use a 1,000-millisecond delay for text changes and a 200-millisecond delay for language changes.

The update request is defined inside the initThreading( ) method:

Download Translate/src/org/example/translate/Translate.java

Line 1 private void initThreading() { guiThread = new Handler(); - transThread = Executors.newSingleThreadExecutor();

// This task does a translation and updates the screen updateTask = new Runnab1e() { public void run() {

// Get text to translate

String original = origText.getTextO.toStringO.trimO

// Cancel previous translation if there was one if (transPending != null) transPending.cancel(true);

// Take care of the easy case if (original.lengthO == 0) {

transText.setText(R.string.empty); retransText.setText(R.string.empty); } else {

// Let user know we're doing something transText.setText(R.string.translating);

retransText.setText(R.string.translating)

// Begin translation now but don't wait for it try {

TranslateTask translateTask = new Trans1ateTask( Translate.this, // reference to activity original, // original text getLang(fromSpinner), // from language getLang(toSpinner) // to language

transPending = transThread.submit(translateTask); } catch (RejectedExecutionException e) {

// Unable to start new task transText.setText(R.string.trans1ation_error);

retransText.setText(R.string.trans1ation_error);

We have two threads: the main Android thread used for the user interface and a translate thread that we'll create for running the actual translation job. We represent the first one with an Android Handler and the second with Java's ExecutorService.

Line 6 defines the update task, which will be scheduled by the queueUpdate! ) method. When it gets to run, it first fetches the current text to translate and then prepares to send a translation job to the translate thread. It cancels any translation that is already in progress (on line 13), takes care of the case where there is no text to translate (line 17), and fills in the two text controls where translated text will appear with

the string "Translating..." (line 21). That text will be replaced later by the actual translated text.

Finally, on line 26, we create an instance of TranslateTask, giving it a reference to the Translate activity so it can call back to change the text, a string containing the original text, and the short names of the two languages selected in the spinners. Line 32 submits the new task to the translation thread, returning a reference to the Future return value. in this case, we don't really have a return value since TranslateTask changes the GUi directly, but we use the Future reference back on line 13 to cancel the translation if necessary.

To finish up the Translate class, here are a few utility functions used in other places:

Download Translate/src/org/example/translate/Translate.java

/** Extract the language code from the current spinner item */ private String getLang(Spinner spinner) {

String result = spinner.getSelectedItem().toString();

result = result.substring(lparen + 1, rparen);

return result;

/** Request an update to start after a short delay */ private void queueUpdate(long delayMillis) {

// Cancel previous update if it hasn't started yet guiThread.removeCallbacks(updateTask);

// Start an update if nothing happens after a few milliseconds guiThread.postDelayed(updateTask, delayMillis);

/** Modify text on the screen (called from another thread) */ public void setTranslated(String text) { guiSetText(transText, text);

/** Modify text on the screen (called from another thread) */ public void setRetranslated(String text) { guiSetText(retransText, text);

/** All changes to the GUI must be done in the GUI thread */ private void guiSetText(final TextView view, final String text) { guiThread.post(new Runnable() { public void run() { view.setText(text);

The getLang() method figures out which item is currently selected in a spinner, gets the string for that item, and parses out the short language code needed by the Translation API.

queueUpdate( ) puts an update request on the main thread's request queue but tells it to wait a little while before actually running it. If there was already a request on the queue, it's removed.

The setTranslated() and setRetranslated() methods will be used by Trans-lateTask to update the user interface when translated results come back from the web service. They both call a private function called guiSet-Text(), which uses the Handler.post() method to ask the main GUI thread to update the text on a TextView control. This extra step is necessary because you can't call user interface functions from non-user-interface threads, and guiSetText() will be called by the translate thread.

Here is the res/values/strings.xml file for the Translate example:

Download Translate/res/values/strings.xml

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

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

<string name="from_text">From:</string>

<string name="to_text">To:</string>

<string name="back_text">And back again:</string>

<string name="original_hint">Enter text to translate</string>

<string name="empty"></string>

<string name="translating">Translating...</string> <string name="translation_error">(Translation error)</string> <string name="translation_interrupted">(Translation interrupted)</string> </resources>

Finally, here's the definition of the TranslateTask class:

Download Translate/src/org/example/translate/TranslateTask.java

package org.example.translate;

import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder;

import org.json.JSONException; import org.json.JSONObject;

import android.util.Log;

public class TranslateTask implements Runnable {

private static final String TAG = "TranslateTask"; private final Translate translate; private final String original, from, to;

TranslateTask(Translate translate, String original, String from, String to) { this.translate = translate; this.original = original; this.from = from; this.to = to;

// Translate the original text to the target language String trans = doTranslate(original, from, to); translate.setTranslated(trans);

// Then translate what we got back to the first language. // Ideally it would be identical but it usually isn't. String retrans = doTranslate(trans, to, from); // swapped translate.setRetranslated(retrans);

* Call the Google Translation API to translate a string from one

* language to another. For more info on the API see:

* http://code.google.com/apis/ajaxlanguage

private String doTranslate(String original, String from, String to) {

String result = translate.getResources().getString(

R.string.translation_error); HttpURLConnection con = null;

Log.d(TAG, "doTranslate(" + original + ", " + from + ", " + to + ")");

// Check if task has been interrupted if (Thread.interrupted())

throw new InterruptedException();

// Build RESTful query for Google API

String q = URLEncoder.encode(original, "UTF-8");

"http://ajax.googleapis.com/ajax/services/language/translate" + "?v=1.0" + "&q=" + q + "&langpair=" + from + "%7C" + to); con = (HttpURLConnection) url.openConnection(); con.setReadTimeout(10000 /* milliseconds */); con.setConnectTimeout(15000 /* milliseconds */);

con.setRequestMethod("CET"); con.addRequestProperty("Referer",

"http://www.pragprog.com/tit~les/eband3/he~l~lo-android"); con.setDolnput(true);

// Check if task has been interrupted if (Thread.interrupted())

throw new InterruptedException();

// Read results from the query BufferedReader reader = new BufferedReader(

new InputStreamReader(con.getInputStream(), "UTF-8")); String payload = reader.readLine(); reader.c1ose();

// Parse to get translated text JSONObject jsonObject = new JSONObject(payload); result = jsonObject.getJSONObject("responseData") .getString( "translatedText") ,rep1ace( "&#39;", "'") .rep1ace( "&amp;", "&");

// Check if task has been interrupted if (Thread.interrupted())

throw new InterruptedException();

Log.e(TAG, "IOException", e); } catch (JSONException e) {

Log.e(TAG, "JSONException", e); } catch (InterruptedException e) {

Log.d(TAG, "InterruptedException", e); result = trans1ate.getResources().getString( R.string.trans1ation_interrupted); } finally {

Log.d(TAG, " -> returned " + result); return result;

This is a nice example of calling a RESTful web service using HttpURL-Connection, parsing results in JavaScript Object Notation (JSON) format, and handling all sorts of network errors and requests for interruptions. I'm not going to explain it in detail here because it contains nothing Android-specific except for a few debugging messages.

0 0

Post a comment