JPGF ANDROID TUTORIAL
Table of Contents
1 Introduction
In this tutorial we'll go through the necessry steps to build a small translation application using a pgf grammar on android. The specification of the application are quite simple:
- the interface will display a text box, a "Translate!" button and a space to show translations.
- The grammar is loaded when the application is started.
- When the user enter a sentence in the textbox and click the button, the sentence is splited into tokens and the grammar is used to retreive translations
- the translations are then displayed in a list on the screen.
The final application should look like this:
2 Start the android application
This section is copied from the android "Hello world" tutorial. The exact syntax of the command may change in the future version of the SDK. Please refer to the official android developer website for the latest instructions.
Let's now create our android project. For that I use the command line tools bundeled with the android SDK. You can of course use the eclipse plugin to create the android project. Please refer to the page linked above for instructions on how to do that.
$ android create project \ --package com.example.translateapp \ --activity Translate \ --target 2 \ --path TranslateApp
This should create a new directory called PGFAndroid containing the basic structure of an android application. Right now the application doesn't do much, but it is still possible to test your application: enter the newly created directory
$ cd TranslateApp
buid and install the application (for this to work you need to have either a running emulator or a connected android device.)
$ ant install
3 Application interface
Now we will create the application interface. Let's take a look at what we want again.
To do that we will modify the main layout file. You can find it under res/layout/main.xml.
There is a text field with a button and then a big space to display the list of translation. The translation into the android UI langauge is pretty straitforward:
<?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"> <!-- The textbox where the user will enter a sentence --> <EditText android:id="@+id/edittext" android:layout_width="fill_parent" android:layout_height="wrap_content"/> <!-- The "Translate!" button --> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:onClick="translate" android:text="Translate!" /> <!-- the list to display the translations --> <ListView android:id="@+id/list" android:layout_width="fill_parent" android:layout_height="fill_parent" android:layout_weight="1"/> </LinearLayout>
Test your application again. The interface should now look like the screenshot.
4 Application code
4.1 Skeletton
Let's now take a look a the java code for the application. In the current state, the interfce should display properly and let ou enter text but it doesn't do much. It even crashes if you press the button.
This is because we didn't implement the translation()
function that is
specified as onClick
parametter for the button. Let's add a dummy
function for now, to understand how it works.
Open the file Translate.java file located in src/com/example/translateapp/, it should look like that:
package com.example.translateapp; import android.app.Activity; import android.os.Bundle; public class Translate extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); } }
Let's add our translate
function. According to the android developer
documentation, it should have the following signature: public void translate(View v)
Since we don't have to return anything, we can just add an empty
function:
public void translate(View v) { }
For this to work, we need to include the View
class from the android library:
import android.view.View;
Your code should now look like this:
package com.example.translateapp; import android.app.Activity; import android.os.Bundle; import android.view.View; public class Translate extends Activity { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); } public void translate(View v) { } }
4.2 Readint the input text and populating the list
The application doesn't crash anymore but it still doesn't do anything… let's look at this.
We will update our translate function so that it grabs the input text and copies it 10 times in the list. Reading the input text is done by first getting a handle to the coresponding view and then getting the text:
TextView tv = (TextView)findViewById(R.id.edittext); String input = tv.getText().toString();
And import the necessary class:
import android.widget.TextView;
Now, we can copy it in the list. First, we should setup a data
structure for the list, we will use a ArrayAdapter in this example. In
the onCreate
function, just add:
mArrayAdapter = new ArrayAdapter(this, R.layout.listitem); ListView list = (ListView)findViewById(R.id.list); list.setAdapter(mArrayAdapter);
Again we need to import some classes from the code to work:
import android.widget.ListView; import android.widget.ArrayAdapter;
and we need a new class member:
private ArrayAdapter mArrayAdapter;
The resource id R.layout.listitem references a new layout that controls how each item is displayed in the list. Let's use a very simple TextView. Create the file res/listitem.xml whith this content:
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content"/>
Finally, we can populate the list in the translate function:
//... getting the input string mArrayAdapter.clear(); for (int i = 0; i < 10 ; i++) mArrayAdapter.add(input);
Now your Translate.java file should look like this:
package com.example.translateapp; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.TextView; import android.widget.ListView; import android.widget.ArrayAdapter; public class Translate extends Activity { private ArrayAdapter mArrayAdapter; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); mArrayAdapter = new ArrayAdapter(this, R.layout.listitem); ListView list = (ListView)findViewById(R.id.list); list.setAdapter(mArrayAdapter); } public void translate(View v) { TextView tv = (TextView)findViewById(R.id.edittext); String input = tv.getText().toString(); mArrayAdapter.clear(); for (int i = 0; i < 10 ; i++) mArrayAdapter.add(input); } }
Run and test your application, you should be able to get something like the following screenshot.
5 Add he JPGF library and the pgf file
Now that we have a working application, let's do something useful.
First, we have to add the JPGF library to our project. Download the
latest version of the library from GitHub ;
and add the jar file to the libs
folder in your project.
Next, we add the pgf file itself. In this example, we will use the food grammar, but feel free to use your own file if you want.
We need to use extra option during the pgf compilation in order to make a pgf that is optimized and indexed. This allows android to cope more easily with big pgfs. Here is the command for the food grammar:
$ gf -make -s -optimize-pgf -mk-index Foods???.gf
now copy the file Foods.pgf into res/raw and rename it to foods.pgf (resource file names should contain only lower case letters.)
Your project directory should now look like this:
TranslateApp + libs: + JPGF-1.0rc1.jar + res: + layout/ + listitem.xml + main.xml + raw/ + foods.pgf + values/ + strings.xml src: + com/ + example/ + translateapp/ + Translate.java + AndroidManifest.xml + build.xml + local.properties + build.properties + default.properties + proguard.cfg + bin/ ... + gen/ ...
Compile and test the project again to make sure eveyting is in order.
6 Implement the pgf functions
Last but not least, we can now implement the translator functions. We need to do two things:
-
loading the pgf in memory in
onCreate
-
trandlate the input when
translate()
is called.
6.1 A word on performances
PGF operations are costly. On a cell phone, reading a PGF can take several seconds, depending on the size of the grammar and it is not unusual for parsing to take 1 or two seconds as well.
If we do that in the main thread for our application, called the UI thread, we will block the user interface. This is not very good practice and it could even lead the OS to believe that our application is stalled and to display an error message.
To avoid this problem we need to put the PGF computations in other threads. The android framework offers different ways to do that. In this tutorial, we'll use the AsynTask class.
Explaining who to use this class is not in the scope of this tutorial so I invite interested readers to look at the class documentation.
There is only three important method for our example in this class:
onPreExecute
- used to setup the interface when the task start (e.g. displaying a progress window.)
doInBackground
- used to do the expensive computation. This cannot modify the UI.
onPostExecute
- used to update the UI when the task is completed (e.g. removing the progress window.)
6.2 Loading the PGF
Loading a PGF file is done with the class PGFBuilder
. It offers two
static methods that both return a PGF object: fromFile
and
fromInputStream
. The first expect a file name. Since, i android
project, our file are better accessed by resource id, we will use the
second one and open an InputStream. This is done like this:
InputStream is = getResources().openRawResource(R.raw.foods);
Then we give this stream to PGFBuilder. In addition we give the list of desired concrete grammar so only those will be kept in memory, this allow us to be more efficient in memory usage.
PGF pgf = PGFBuilder.fromInputStream(is, new String[] {"FoodsEng", "FoodsCat"});
Now, as explained above, we will not do this directly in onCreate
to
avoid blocking the UI. Instead we need to subclass AsyncTask and read
the PGF in the doInBackground
method. In addition, we add the code
for the progress window:
/** * This class is used to load the PGF file asychronously. * It display a blocking progress dialog while doing so. */ private class LoadPGFTask extends AsyncTask<Void, Void, PGF> { private ProgressDialog progress; protected void onPreExecute() { // Display loading popup this.progress = ProgressDialog.show(Translate.this,"Translate","Loading grammar, please wait",true); } protected PGF doInBackground(Void... a) { int pgf_res = R.raw.foods; InputStream is = getResources().openRawResource(pgf_res); try { PGF pgf = PGFBuilder.fromInputStream(is, new String[] {"FoodsEng", "FoodsCat"}); return pgf; } catch (Exception e) { throw new RuntimeException(e); } } protected void onPostExecute(PGF result) { mPGF = result; if (this.progress != null) this.progress.dismiss(); // Remove loading popup } }
Finally, we need a new class member for the PGF
private PGF mPGF;
and we need to launch the task from onCreate
:
new LoadPGFTask().execute();
6.3 The translation task
Translation is just the combination of parsing and linearization. In
JPGF, those tasks are respectevly done with a Parser
and a
Lnearizer
object. Those are easily created given the PGF and the
concrete grammar:
Parser mParser = new Parser(mPGF, "FoodsEng"); Linearizer mLinearizer = new Linearizer(mPGF, "FoodsCat");
The parser object expect an array of tokens, which means that we need to tokenize the sentence first:
String[] tokens = entence.split(" ");
and returns a ParseState object from which we can retreive parse trees:
ParseState mParseState = parser.parse(token); Tree[] trees = (Tree[])mParseState.getTrees();
The Linearizer
takes a tree and return a string:
String s = mLinearizer.linearizeString(trees[0]);
Finally, once enclosed in an AsyncTask sub-class with the code for the progress window and some boilerplate code we get:
/** * This class is used to parse a sentence asychronously. * It display a blocking progress dialog while doing so. */ private class TranslateTask extends AsyncTask<String, Void, String[]> { private ProgressDialog progress; protected void onPreExecute() { // Display loading popup this.progress = ProgressDialog.show(Translate.this,"Translate","Parsing, please wait",true); } protected String[] doInBackground(String... s) { try { // Creating a Parser object for the FoodEng concrete grammar Parser mParser = new Parser(mPGF, "FoodsEng"); // Spliting the input (basic tokenization) String[] tokens = s[0].split(" "); // parsing the tokens ParseState mParseState = mParser.parse(tokens); Tree[] trees = (Tree[])mParseState.getTrees(); String[] translations = new String[trees.length]; // Creating a Linearizer object for the FoodCat concrete grammar Linearizer mLinearizer = new Linearizer(mPGF, "FoodsCat"); // Linearizing all the trees for (int i = 0 ; i < trees.length ; i++) { try { String t = mLinearizer.linearizeString(trees[i]); translations[i] = t; } catch (java.lang.Exception e) { translations[i] = "/!\\ Linearization error"; } } return translations; } catch (Exception e) { throw new RuntimeException(e); } } protected void onPostExecute(String[] result) { mArrayAdapter.clear(); for (String sentence : result) mArrayAdapter.add(sentence); if (this.progress != null) this.progress.dismiss(); // Remove loading popup } }
and again, we launch the task when appropriate: in the translate
method
public void translate(View v) { TextView tv = (TextView)findViewById(R.id.edittext); String input = tv.getText().toString(); new TranslateTask().execute(input); }
Add the right classes to the imports:
import android.app.ProgressDialog; import android.os.AsyncTask; import java.io.InputStream; import org.grammaticalframework.Linearizer; import org.grammaticalframework.PGF; import org.grammaticalframework.PGFBuilder; import org.grammaticalframework.Parser; import org.grammaticalframework.parser.ParseState; import org.grammaticalframework.Trees.Absyn.Tree;
6.4 Full Translate.java
Here is the final Translate.java
for this tutorial.
package com.example.translateapp; import android.app.Activity; import android.os.Bundle; import android.view.View; import android.widget.TextView; import android.widget.ListView; import android.widget.ArrayAdapter; import android.app.ProgressDialog; import android.os.AsyncTask; import java.io.InputStream; import org.grammaticalframework.Linearizer; import org.grammaticalframework.PGF; import org.grammaticalframework.PGFBuilder; import org.grammaticalframework.Parser; import org.grammaticalframework.parser.ParseState; import org.grammaticalframework.Trees.Absyn.Tree; public class Translate extends Activity { private ArrayAdapter mArrayAdapter; private PGF mPGF; /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); new LoadPGFTask().execute(); mArrayAdapter = new ArrayAdapter(this, R.layout.listitem); ListView list = (ListView)findViewById(R.id.list); list.setAdapter(mArrayAdapter); } public void translate(View v) { TextView tv = (TextView)findViewById(R.id.edittext); String input = tv.getText().toString(); new TranslateTask().execute(input); } /** * This class is used to load the PGF file asychronously. * It display a blocking progress dialog while doing so. */ private class LoadPGFTask extends AsyncTask<Void, Void, PGF> { private ProgressDialog progress; protected void onPreExecute() { // Display loading popup this.progress = ProgressDialog.show(Translate.this,"Translate","Loading grammar, please wait",true); } protected PGF doInBackground(Void... a) { int pgf_res = R.raw.foods; InputStream is = getResources().openRawResource(pgf_res); try { PGF pgf = PGFBuilder.fromInputStream(is, new String[] {"FoodsEng", "FoodsCat"}); return pgf; } catch (Exception e) { throw new RuntimeException(e); } } protected void onPostExecute(PGF result) { mPGF = result; if (this.progress != null) this.progress.dismiss(); // Remove loading popup } } /** * This class is used to parse a sentence asychronously. * It display a blocking progress dialog while doing so. */ private class TranslateTask extends AsyncTask<String, Void, String[]> { private ProgressDialog progress; protected void onPreExecute() { // Display loading popup this.progress = ProgressDialog.show(Translate.this,"Translate","Parsing, please wait",true); } protected String[] doInBackground(String... s) { try { // Creating a Parser object for the FoodEng concrete grammar Parser mParser = new Parser(mPGF, "FoodsEng"); // Spliting the input (basic tokenization) String[] tokens = s[0].split(" "); // parsing the tokens ParseState mParseState = mParser.parse(tokens); Tree[] trees = (Tree[])mParseState.getTrees(); String[] translations = new String[trees.length]; // Creating a Linearizer object for the FoodCat concrete grammar Linearizer mLinearizer = new Linearizer(mPGF, "FoodsCat"); // Linearizing all the trees for (int i = 0 ; i < trees.length ; i++) { try { String t = mLinearizer.linearizeString(trees[i]); translations[i] = t; } catch (java.lang.Exception e) { translations[i] = "/!\\ Linearization error"; } } return translations; } catch (Exception e) { throw new RuntimeException(e); } } protected void onPostExecute(String[] result) { mArrayAdapter.clear(); for (String sentence : result) mArrayAdapter.add(sentence); if (this.progress != null) this.progress.dismiss(); // Remove loading popup } } }
If you compile and test your application now, you should be able to
translate sentences from the Foods
grammar from English to
Catalan. Feel free to play with other languages and other grammars.
7 Links and contact
A few useful links:
- http://www.grammaticalframework.org
- The home page of the Grammatical Framework.
- http://developer.android.com/
- The android developer documentation
- PhraseDroid
- an example of full GF/android application
If you have questions, remarks or comment feel free to contact me at gregoire.detrez@gu.se
Date: 2011-07-08 15:10:28 CEST
HTML generated by org-mode 6.36c in emacs 23