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:

images/screenshot3-small.png

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.

images/screenshot1-small.png

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.

images/screenshot2-small.png

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

Author: Grégoire Détrez

Date: 2011-07-08 15:10:28 CEST

HTML generated by org-mode 6.36c in emacs 23