Add new Android voice translator sample app

This adds a simple voice translator Android app that uses
the JNI bindings to the PGF C runtime.

Caveats:

- Since the C runtime doesn't compile for Android right now,
  I've bundled an old copy, along with its Java bindings.
  That should be removed once the C runtime compiels for Android
  again.

- Adding an automated build would be nice.

- Replacing the grammar requires editing a Java file, that should 
  really be more dynamic.
This commit is contained in:
bringert
2013-10-04 16:26:46 +00:00
parent 0d9d926131
commit 6adc9f7be4
45 changed files with 1236 additions and 147 deletions

View File

@@ -3,11 +3,7 @@
<classpathentry kind="src" path="src"/>
<classpathentry kind="src" path="gen"/>
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES">
<attributes>
<attribute name="org.eclipse.jdt.launching.CLASSPATH_ATTR_LIBRARY_PATH_ENTRY" value="jni"/>
</attributes>
</classpathentry>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES"/>
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
<classpathentry kind="output" path="bin/classes"/>
</classpath>

View File

@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>GFTranslator</name>
<name>GFVoiceExample</name>
<comment></comment>
<projects>
</projects>

View File

@@ -2,19 +2,21 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="org.grammaticalframework.ui.android"
android:versionCode="1"
android:versionName="1.0">
android:versionName="1.0" >
<uses-sdk
android:minSdkVersion="8"
android:targetSdkVersion="8" />
android:targetSdkVersion="18" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<application
android:allowBackup="true"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:theme="@style/AppTheme">
android:theme="@style/AppTheme" >
<activity
android:name="org.grammaticalframework.ui.android.MainActivity"
android:name=".MainActivity"
android:label="@string/app_name" >
<intent-filter>
<action android:name="android.intent.action.MAIN" />

66
src/ui/android/README Normal file
View File

@@ -0,0 +1,66 @@
= Overview =
This directory contains a sample Android app tht uses
the Android speech recognition and TTS APIs along with
JNI bindings to the C PGF runtime to implement a simple
speech translation app.
= Requirements =
1. Android SDK: http://developer.android.com/sdk/
installed in $ANDROID_SDK_LOCATION
2. Android NDK: http://developer.android.com/tools/sdk/ndk/
installed in $ANDROID_NDK_LOCATION
= Building =
Set up Android project:
# Creates local.properties, not to be checked in
$ $ANDROID_SDK_LOCATION/tools/android update project -p .
Build libs/libjpgf.jar:
$ (cd ../../runtime/java && javac org/grammaticalframework/pgf/*.java && jar -cf libjpgf.jar org/grammaticalframework/pgf/*.class)
$ cp ../../runtime/java/libjpgf.jar libs
Build JNI code:
$ cd jni
$ $ANDROID_NDK_LOCATION/ndk-build
Build APK:
$ ant debug
Install on your device:
$ ant debug install
or:
$ adb install -r bin/MainActivity-debug.apk
= Changing the grammar =
1. Replace assets/ResourceDemo.pgf
2. Edit Translator.java to point to the new file and include its metadata
= Developing in Eclipse =
1. Install Android ADT
2. Eclipse > File > Import > Existing Projects into Workspace > Next
3. Select root directory...
4. Select GF/src/ui/android
5. Finish

Binary file not shown.

92
src/ui/android/build.xml Normal file
View File

@@ -0,0 +1,92 @@
<?xml version="1.0" encoding="UTF-8"?>
<project name="MainActivity" default="help">
<!-- The local.properties file is created and updated by the 'android' tool.
It contains the path to the SDK. It should *NOT* be checked into
Version Control Systems. -->
<property file="local.properties" />
<!-- The ant.properties file can be created by you. It is only edited by the
'android' tool to add properties to it.
This is the place to change some Ant specific build properties.
Here are some properties you may want to change/update:
source.dir
The name of the source directory. Default is 'src'.
out.dir
The name of the output directory. Default is 'bin'.
For other overridable properties, look at the beginning of the rules
files in the SDK, at tools/ant/build.xml
Properties related to the SDK location or the project target should
be updated using the 'android' tool with the 'update' action.
This file is an integral part of the build system for your
application and should be checked into Version Control Systems.
-->
<property file="ant.properties" />
<!-- if sdk.dir was not set from one of the property file, then
get it from the ANDROID_HOME env var.
This must be done before we load project.properties since
the proguard config can use sdk.dir -->
<property environment="env" />
<condition property="sdk.dir" value="${env.ANDROID_HOME}">
<isset property="env.ANDROID_HOME" />
</condition>
<!-- The project.properties file is created and updated by the 'android'
tool, as well as ADT.
This contains project specific properties such as project target, and library
dependencies. Lower level build properties are stored in ant.properties
(or in .classpath for Eclipse projects).
This file is an integral part of the build system for your
application and should be checked into Version Control Systems. -->
<loadproperties srcFile="project.properties" />
<!-- quick check on sdk.dir -->
<fail
message="sdk.dir is missing. Make sure to generate local.properties using 'android update project' or to inject it through the ANDROID_HOME environment variable."
unless="sdk.dir"
/>
<!--
Import per project custom build rules if present at the root of the project.
This is the place to put custom intermediary targets such as:
-pre-build
-pre-compile
-post-compile (This is typically used for code obfuscation.
Compiled code location: ${out.classes.absolute.dir}
If this is not done in place, override ${out.dex.input.absolute.dir})
-post-package
-post-build
-pre-clean
-->
<import file="custom_rules.xml" optional="true" />
<!-- Import the actual build file.
To customize existing targets, there are two options:
- Customize only one target:
- copy/paste the target into this file, *before* the
<import> task.
- customize it to your needs.
- Customize the whole content of build.xml
- copy/paste the content of the rules files (minus the top node)
into this file, replacing the <import> task.
- customize to your needs.
***********************
****** IMPORTANT ******
***********************
In all cases you must update the value of version-tag below to read 'custom' instead of an integer,
in order to avoid having your file be overridden by tools such as "android update project"
-->
<!-- version-tag: 1 -->
<import file="${sdk.dir}/tools/ant/build.xml" />
</project>

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,20 @@
# To enable ProGuard in your project, edit project.properties
# to define the proguard.config property as described in that file.
#
# Add project specific ProGuard rules here.
# By default, the flags in this file are appended to flags specified
# in ${sdk.dir}/tools/proguard/proguard-android.txt
# You can edit the include path and order by changing the ProGuard
# include property in project.properties.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# Add any project specific keep options here:
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

Binary file not shown.

After

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 665 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 437 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 547 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 783 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="4dp" />
<solid android:color="#75CD75" />
</shape>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<corners android:radius="4dp" />
<solid android:color="#7575CD" />
</shape>

View File

@@ -0,0 +1,85 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="match_parent"
android:layout_width="match_parent"
>
<RelativeLayout
android:id="@+id/top_bg"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_alignParentTop="true"
android:layout_alignParentLeft="true"
android:layout_alignParentRight="true"
android:padding="8dp"
android:background="#C0C0C0"
>
<ImageView
android:id="@+id/start_stop"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_alignTop="@+id/source_language"
android:layout_alignBottom="@+id/target_language"
android:layout_alignParentRight="true"
android:padding="8dp"
android:src="@drawable/ic_mic"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/microphone"
/>
<ImageView
android:id="@+id/switch_languages"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_alignTop="@+id/source_language"
android:layout_alignBottom="@+id/target_language"
android:layout_toLeftOf="@id/start_stop"
android:padding="8dp"
android:src="@drawable/ic_action_switch"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/switch_languages"
/>
<org.grammaticalframework.ui.android.LanguageSelector
android:id="@+id/source_language"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:layout_toLeftOf="@id/switch_languages"
android:padding="0dp"
/>
<org.grammaticalframework.ui.android.LanguageSelector
android:id="@+id/target_language"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:layout_below="@id/source_language"
android:layout_toLeftOf="@id/switch_languages"
android:padding="0dp"
/>
</RelativeLayout>
<org.grammaticalframework.ui.android.ConversationView
android:id="@+id/conversation"
android:layout_height="match_parent"
android:layout_width="match_parent"
android:layout_alignParentLeft="true"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_below="@id/top_bg"
>
<LinearLayout
android:id="@+id/conversation_content"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:orientation="vertical"
android:padding="16dp"
>
</LinearLayout>
</org.grammaticalframework.ui.android.ConversationView>
</RelativeLayout>

View File

@@ -0,0 +1,11 @@
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginRight="32dp"
android:layout_gravity="left"
android:padding="8dp"
android:textSize="20sp"
android:background="@drawable/first_person_utterance_bg"
/>

View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:padding="8dp"
android:textSize="20sp"
/>

View File

@@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_height="wrap_content"
android:layout_width="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginLeft="32dp"
android:layout_gravity="right"
android:padding="8dp"
android:textSize="20sp"
android:background="@drawable/second_person_utterance_bg"
/>

View File

@@ -0,0 +1,8 @@
<resources>
<!--
Customize dimensions originally defined in res/values/dimens.xml (such as
screen margins) for sw600dp devices (e.g. 7" tablets) here.
-->
</resources>

View File

@@ -0,0 +1,9 @@
<resources>
<!--
Customize dimensions originally defined in res/values/dimens.xml (such as
screen margins) for sw720dp devices (e.g. 10" tablets) in landscape here.
-->
<dimen name="activity_horizontal_margin">128dp</dimen>
</resources>

View File

@@ -0,0 +1,11 @@
<resources>
<!--
Base application theme for API 11+. This theme completely replaces
AppBaseTheme from res/values/styles.xml on API 11+ devices.
-->
<style name="AppBaseTheme" parent="android:Theme.Holo.Light">
<!-- API 11 theme customizations can go here. -->
</style>
</resources>

View File

@@ -0,0 +1,12 @@
<resources>
<!--
Base application theme for API 14+. This theme completely replaces
AppBaseTheme from BOTH res/values/styles.xml and
res/values-v11/styles.xml on API 14+ devices.
-->
<style name="AppBaseTheme" parent="android:Theme.Holo.Light.DarkActionBar">
<!-- API 14 theme customizations can go here. -->
</style>
</resources>

View File

@@ -0,0 +1,7 @@
<resources>
<!-- Default screen margins, per the Android Design guidelines. -->
<dimen name="activity_horizontal_margin">16dp</dimen>
<dimen name="activity_vertical_margin">16dp</dimen>
</resources>

View File

@@ -1,8 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">GFTranslator</string>
<string name="action_settings">Settings</string>
<string name="hello_world">Hello world!</string>
<string name="app_name">GF Translator</string>
<string name="microphone">Microphone</string>
<string name="switch_languages">Switch languages</string>
</resources>

View File

@@ -0,0 +1,20 @@
<resources>
<!--
Base application theme, dependent on API level. This theme is replaced
by AppBaseTheme from res/values-vXX/styles.xml on newer devices.
-->
<style name="AppBaseTheme" parent="android:Theme.Light">
<!--
Theme customizations available in newer API levels can go in
res/values-vXX/styles.xml, while customizations related to
backward-compatibility can go here.
-->
</style>
<!-- Application theme. -->
<style name="AppTheme" parent="AppBaseTheme">
<!-- All customizations that are NOT specific to a particular API-level can go here. -->
</style>
</resources>

View File

@@ -0,0 +1,240 @@
package org.grammaticalframework.ui.android;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.speech.RecognitionListener;
import android.speech.RecognizerIntent;
import android.speech.SpeechRecognizer;
import android.text.TextUtils;
import android.util.Log;
import java.util.ArrayList;
/**
* Convenience wrapper around the {@link SpeechRecognizer} API.
*/
public class ASR {
private static final boolean DBG = true;
private static final String TAG = "ASR";
private final Context mContext;
private SpeechRecognizer mSpeechRecognizer;
private String mLanguage = null;
private State mState = State.IDLE;
private Listener mListener;
public static enum State {
IDLE, INITIALIZING, WAITING_FOR_SPEECH, RECORDING, WAITING_FOR_RESULTS;
}
public ASR(Context context) {
mContext = context;
if (SpeechRecognizer.isRecognitionAvailable(context)) {
mSpeechRecognizer = SpeechRecognizer.createSpeechRecognizer(context);
mSpeechRecognizer.setRecognitionListener(new MyRecognitionListener());
}
}
public void setListener(Listener listener) {
mListener = listener;
}
public void setLanguage(String language) {
mLanguage = language;
}
public void startRecognition() {
Intent intent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
if (!TextUtils.isEmpty(mLanguage)) {
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, mLanguage);
}
intent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
RecognizerIntent.LANGUAGE_MODEL_FREE_FORM);
intent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, 2);
intent.putExtra(RecognizerIntent.EXTRA_PARTIAL_RESULTS, true);
// Weird, this shouldn't be required, but on ICS it seems to be
intent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE,
mContext.getPackageName());
mSpeechRecognizer.startListening(intent);
setState(State.INITIALIZING);
}
public void stopRecognition() {
mSpeechRecognizer.stopListening();
setState(State.IDLE);
}
public boolean isRunning() {
return mState != State.IDLE;
}
private void setState(State newState) {
if (DBG) Log.d(TAG, "Entering state: " + newState);
mState = newState;
if (mListener != null) {
mListener.onStateChanged(mState);
}
}
public State getState() {
return mState;
}
public void destroy() {
if (mSpeechRecognizer != null) {
mSpeechRecognizer.destroy();
mSpeechRecognizer = null;
}
}
private void handlePartialInput(String text) {
if (mListener != null) {
mListener.onPartialInput(text);
}
}
private void handleSpeechInput(String text) {
if (mListener != null) {
mListener.onSpeechInput(text);
}
}
private class MyRecognitionListener implements RecognitionListener {
@Override
public void onReadyForSpeech(Bundle params) {
if (DBG) Log.d(TAG, "onReadyForSpeech");
setState(State.WAITING_FOR_SPEECH);
}
@Override
public void onBeginningOfSpeech() {
if (DBG) Log.d(TAG, "onBeginningOfSpeech");
setState(State.RECORDING);
}
@Override
public void onBufferReceived(byte[] buffer) {
// Ignore
}
@Override
public void onRmsChanged(float rmsdB) {
if (DBG) Log.d(TAG, "onRmsChanged(" + rmsdB + ")");
}
@Override
public void onEndOfSpeech() {
if (DBG) Log.d(TAG, "onEndOfSpeech");
setState(State.WAITING_FOR_RESULTS);
}
@Override
public void onError(int error) {
if (DBG) Log.d(TAG, "Error: " + errorMessage(error) + " (" + error + ")");
setState(State.IDLE);
}
private String errorMessage(int speechRecognizerError) {
switch(speechRecognizerError) {
case SpeechRecognizer.ERROR_NETWORK_TIMEOUT:
return "network timeout";
case SpeechRecognizer.ERROR_NETWORK:
return "network";
case SpeechRecognizer.ERROR_AUDIO:
return "audio";
case SpeechRecognizer.ERROR_SERVER:
return "server";
case SpeechRecognizer.ERROR_CLIENT:
return "client";
case SpeechRecognizer.ERROR_SPEECH_TIMEOUT:
return "timeout waiting for speech";
case SpeechRecognizer.ERROR_NO_MATCH:
return "no match found";
case SpeechRecognizer.ERROR_RECOGNIZER_BUSY:
return "recognizer busy";
case SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS:
return "insufficient permissions (missing RECORD_AUDIO?)";
default:
return "unknown";
}
}
@Override
public void onEvent(int eventType, Bundle params) {
if (DBG) Log.d(TAG, "onEvent(" + eventType + ")");
}
@Override
public void onPartialResults(Bundle bundle) {
if (DBG) {
StringBuilder sb = new StringBuilder();
sb.append("onPartialResults:");
appendResults(sb, bundle);
Log.d(TAG, sb.toString());
}
String result = getResult(bundle);
if (!TextUtils.isEmpty(result)) {
handlePartialInput(result);
}
}
@Override
public void onResults(Bundle bundle) {
if (DBG) {
StringBuilder sb = new StringBuilder();
sb.append("onResults:");
appendResults(sb, bundle);
Log.d(TAG, sb.toString());
}
setState(State.IDLE);
String result = getResult(bundle);
if (!TextUtils.isEmpty(result)) {
handleSpeechInput(result);
}
}
private String getResult(Bundle bundle) {
ArrayList<String> results =
bundle.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
if (results != null && !results.isEmpty()) {
return results.get(0);
} else {
return null;
}
}
private void appendResults(StringBuilder sb, Bundle bundle) {
ArrayList<String> results =
bundle.getStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION);
float[] scores = bundle.getFloatArray(SpeechRecognizer.CONFIDENCE_SCORES);
if (results != null) {
int size = results.size();
for (int i = 0; i < size; i++) {
sb.append("\n> ").append(results.get(i));
if (scores != null && i < scores.length) {
sb.append(" [").append(scores[i]).append("]");
}
}
}
}
}
public interface Listener {
void onPartialInput(String input);
void onSpeechInput(String input);
void onStateChanged(State newState);
}
}

View File

@@ -0,0 +1,62 @@
package org.grammaticalframework.ui.android;
import android.content.Context;
import android.util.AttributeSet;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import android.widget.ScrollView;
import android.widget.TextView;
public class ConversationView extends ScrollView {
private LayoutInflater mInflater;
private ViewGroup mContent;
public ConversationView(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public ConversationView(Context context, AttributeSet attrs) {
super(context, attrs);
}
public ConversationView(Context context) {
super(context);
}
@Override
protected void onFinishInflate() {
super.onFinishInflate();
mContent = (ViewGroup) findViewById(R.id.conversation_content);
mInflater = LayoutInflater.from(getContext());
}
public void addFirstPersonUtterance(CharSequence text) {
addUtterance(R.layout.first_person_utterance, text);
}
public void addSecondPersonUtterance(CharSequence text) {
addUtterance(R.layout.second_person_utterance, text);
}
private void addUtterance(int res, CharSequence text) {
TextView view = (TextView) mInflater.inflate(res, mContent, false);
view.setText(text);
mContent.addView(view);
post(new Runnable() {
public void run() {
fullScroll(FOCUS_DOWN);
}
});
}
public void updateLastUtterance(CharSequence text) {
int count = mContent.getChildCount();
if (count > 0) {
TextView view = (TextView) mContent.getChildAt(count - 1);
view.setText(text);
}
}
}

View File

@@ -0,0 +1,31 @@
package org.grammaticalframework.ui.android;
public class Language {
private final String mLangCode;
private final String mLangName;
private final String mConcrete;
public Language(String langCode, String langName, String concrete) {
mLangCode = langCode;
mLangName = langName;
mConcrete = concrete;
}
public String getLangCode() {
return mLangCode;
}
public String getLangName() {
return mLangName;
}
String getConcrete() {
return mConcrete;
}
@Override
public String toString() {
return getLangName();
}
}

View File

@@ -0,0 +1,51 @@
package org.grammaticalframework.ui.android;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.AdapterView;
import android.widget.Spinner;
import java.util.List;
public class LanguageSelector extends Spinner {
public LanguageSelector(Context context, AttributeSet attrs, int defStyle) {
super(context, attrs, defStyle);
}
public LanguageSelector(Context context, AttributeSet attrs) {
super(context, attrs);
}
public LanguageSelector(Context context) {
super(context);
}
public void setLanguages(List<Language> languages) {
setAdapter(new LanguagesAdapter(getContext(), languages));
}
public void setSelectedLanguage(Language selected) {
setSelection(((LanguagesAdapter) getAdapter()).getPosition(selected));
}
public void setOnLanguageSelectedListener(final OnLanguageSelectedListener listener) {
setOnItemSelectedListener(new OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
if (listener != null) {
listener.onLanguageSelected((Language) parent.getItemAtPosition(position));
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {
}
});
}
public interface OnLanguageSelectedListener {
void onLanguageSelected(Language language);
}
}

View File

@@ -0,0 +1,16 @@
package org.grammaticalframework.ui.android;
import android.content.Context;
import android.widget.ArrayAdapter;
import android.widget.SpinnerAdapter;
import java.util.List;
public class LanguagesAdapter extends ArrayAdapter<Language> implements SpinnerAdapter {
public LanguagesAdapter(Context context, List<Language> objects) {
super(context, R.layout.languages_item, objects);
}
}

View File

@@ -0,0 +1,42 @@
package org.grammaticalframework.ui.android;
import android.text.TextUtils;
import java.util.Locale;
/**
* Collections of utils to handle locales.
*/
public class LocaleUtils {
/**
* Parses a locale string formatted by {@link Locale#toString()}.
*
* @return the parsed {@code Locale} or {@code defaultLocale} if the input was null or empty.
*/
public static Locale parseJavaLocale(String localeString, Locale defaultLocale) {
if (TextUtils.isEmpty(localeString)) {
return defaultLocale;
}
final char separator = '_';
int pos1 = localeString.indexOf(separator);
if (pos1 == -1) {
return new Locale(localeString);
}
String language = localeString.substring(0, pos1);
int start2 = pos1 + 1;
int pos2 = localeString.indexOf(separator, start2);
if (pos2 == -1) {
return new Locale(language, localeString.substring(start2));
}
String country = localeString.substring(start2, pos2);
int start3 = pos2 + 1;
int pos3 = localeString.indexOf(separator, start3);
String variant = (pos3 == -1)
? localeString.substring(start3)
: localeString.substring(start3, pos3);
return new Locale(language, country, variant);
}
}

View File

@@ -0,0 +1,202 @@
package org.grammaticalframework.ui.android;
import android.app.Activity;
import android.os.AsyncTask;
import android.os.Bundle;
import android.speech.SpeechRecognizer;
import android.util.Log;
import android.view.View;
import android.view.View.OnClickListener;
import android.widget.ImageView;
import org.grammaticalframework.ui.android.ASR.State;
import org.grammaticalframework.ui.android.LanguageSelector.OnLanguageSelectedListener;
public class MainActivity extends Activity {
private static final boolean DBG = true;
private static final String TAG = "DemoActivity";
private static final boolean FAKE_SPEECH = false;
private ImageView mStartStopButton;
private ConversationView mConversationView;
private LanguageSelector mSourceLanguageView;
private LanguageSelector mTargetLanguageView;
private ImageView mSwitchLanguagesButton;
private ASR mAsr;
private TTS mTts;
private Translator mTranslator;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mStartStopButton = (ImageView) findViewById(R.id.start_stop);
mConversationView = (ConversationView) findViewById(R.id.conversation);
mSourceLanguageView = (LanguageSelector) findViewById(R.id.source_language);
mTargetLanguageView = (LanguageSelector) findViewById(R.id.target_language);
mSwitchLanguagesButton = (ImageView) findViewById(R.id.switch_languages);
mStartStopButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
if (mAsr.isRunning()) {
stopRecognition();
} else {
startRecognition();
}
}
});
mStartStopButton.setEnabled(SpeechRecognizer.isRecognitionAvailable(this));
mAsr = new ASR(this);
mAsr.setListener(new SpeechInputListener());
mTts = new TTS(this);
mTranslator = new Translator(this);
new AsyncTask<Void,Void,Void>() {
@Override
protected Void doInBackground(Void... params) {
mTranslator.init();
return null;
}
}.execute();
mSourceLanguageView.setLanguages(mTranslator.getAvailableSourceLanguages());
mSourceLanguageView.setSelectedLanguage(mTranslator.getSourceLanguage());
mSourceLanguageView.setOnLanguageSelectedListener(new OnLanguageSelectedListener() {
@Override
public void onLanguageSelected(Language language) {
onSourceLanguageSelected(language);
}
});
mTargetLanguageView.setLanguages(mTranslator.getAvailableTargetLanguages());
mTargetLanguageView.setSelectedLanguage(mTranslator.getTargetLanguage());
mTargetLanguageView.setOnLanguageSelectedListener(new OnLanguageSelectedListener() {
@Override
public void onLanguageSelected(Language language) {
onTargetLanguageSelected(language);
}
});
mSwitchLanguagesButton.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
onSwitchLanguages();
}
});
}
@Override
protected void onDestroy() {
if (mAsr != null) {
mAsr.destroy();
mAsr = null;
}
if (mTts != null) {
mTts.destroy();
mTts = null;
}
super.onDestroy();
}
void onSourceLanguageSelected(Language language) {
mTranslator.setSourceLanguage(language);
}
void onTargetLanguageSelected(Language language) {
mTranslator.setTargetLanguage(language);
}
public String getSourceLanguageCode() {
return mTranslator.getSourceLanguage().getLangCode();
}
public String getTargetLanguageCode() {
return mTranslator.getTargetLanguage().getLangCode();
}
void onSwitchLanguages() {
Language newSource = mTranslator.getTargetLanguage();
Language newTarget = mTranslator.getSourceLanguage();
mSourceLanguageView.setSelectedLanguage(newSource);
mTargetLanguageView.setSelectedLanguage(newTarget);
}
private void startRecognition() {
mConversationView.addFirstPersonUtterance("...");
if (FAKE_SPEECH) {
handleSpeechInput("where is the hotel");
} else {
mAsr.setLanguage(getSourceLanguageCode());
mAsr.startRecognition();
}
}
private void stopRecognition() {
mAsr.stopRecognition();
}
private void handlePartialSpeechInput(String input) {
mConversationView.updateLastUtterance(input);
}
private void handleSpeechInput(final String input) {
mConversationView.updateLastUtterance(input);
new AsyncTask<Void,Void,String>() {
@Override
protected String doInBackground(Void... params) {
return mTranslator.translate(input);
}
@Override
protected void onPostExecute(String result) {
outputText(result);
}
}.execute();
}
private void outputText(String text) {
if (DBG) Log.d(TAG, "Speaking: " + text);
mConversationView.addSecondPersonUtterance(text);
if (!FAKE_SPEECH) {
mTts.setLanguage(getTargetLanguageCode());
mTts.speak(text);
}
}
private class SpeechInputListener implements ASR.Listener {
@Override
public void onPartialInput(String input) {
handlePartialSpeechInput(input);
}
@Override
public void onSpeechInput(String input) {
handleSpeechInput(input);
}
@Override
public void onStateChanged(State newState) {
// if (newState == ASR.State.IDLE) {
// mStartStopButton.setImageResource(R.drawable.mic_idle);
// } else {
// mStartStopButton.setImageResource(R.drawable.mic_open);
// }
}
}
}

View File

@@ -0,0 +1,63 @@
package org.grammaticalframework.ui.android;
import android.content.Context;
import android.speech.tts.TextToSpeech;
import android.util.Log;
import java.util.HashMap;
import java.util.Locale;
public class TTS {
private static final String TAG = "TTS";
private TextToSpeech mTts;
public TTS(Context context) {
mTts = new TextToSpeech(context, new InitListener());
}
public void setLanguage(String language) {
Locale locale = LocaleUtils.parseJavaLocale(language.replace('-', '_'),
Locale.getDefault());
int result = mTts.setLanguage(locale);
if (result == TextToSpeech.LANG_MISSING_DATA ||
result == TextToSpeech.LANG_NOT_SUPPORTED) {
Log.e(TAG, "Language is not available");
} else {
// TODO: the language may be available for the locale,
// but not for the specified country and variant.
}
}
// TODO: handle speak() calls before service connects
public void speak(String text) {
HashMap<String,String> params = new HashMap<String,String>();
// TODO: how can I get network / embedded fallback?
// Using both crashes the TTS engine if the offline data is not installed
// Using only one doesn't allow the other
// params.put(TextToSpeech.Engine.KEY_FEATURE_NETWORK_SYNTHESIS, "true");
// params.put(TextToSpeech.Engine.KEY_FEATURE_EMBEDDED_SYNTHESIS, "true");
mTts.speak(text, TextToSpeech.QUEUE_FLUSH, params);
}
public void destroy() {
if (mTts != null) {
mTts.stop();
mTts.shutdown();
}
}
private class InitListener implements TextToSpeech.OnInitListener {
@Override
public void onInit(int status) {
if (status == TextToSpeech.SUCCESS) {
Log.d(TAG, "Initialized TTS");
} else {
Log.e(TAG, "Failed to initialize TTS");
}
}
}
}

View File

@@ -0,0 +1,144 @@
package org.grammaticalframework.ui.android;
import android.content.Context;
import android.util.Log;
import org.grammaticalframework.pgf.Concr;
import org.grammaticalframework.pgf.Expr;
import org.grammaticalframework.pgf.PGF;
import org.grammaticalframework.pgf.ParseError;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.List;
public class Translator {
private static final String TAG = "Translator";
// TODO: allow changing
private String mGrammar = "ResourceDemo.pgf";
// TODO: build dynamically?
private Language[] mLanguages = {
new Language("en-US", "English", "ResourceDemoEng"),
new Language("de-DE", "German", "ResourceDemoGer"),
new Language("es-ES", "Spanish", "ResourceDemoSpa"),
new Language("fr-FR", "French", "ResourceDemoFre"),
};
private final Context mContext;
private Language mSourceLanguage;
private Language mTargetLanguage;
private PGF mPgf;
public Translator(Context context) {
mContext = context;
}
public Context getContext() {
return mContext;
}
public List<Language> getAvailableSourceLanguages() {
return Arrays.asList(mLanguages);
}
public List<Language> getAvailableTargetLanguages() {
return Arrays.asList(mLanguages);
}
public void setSourceLanguage(Language language) {
mSourceLanguage = language;
}
public void setTargetLanguage(Language language) {
mTargetLanguage = language;
}
public Language getSourceLanguage() {
return mSourceLanguage != null ? mSourceLanguage : mLanguages[0];
}
public Language getTargetLanguage() {
return mTargetLanguage != null ? mTargetLanguage : mLanguages[1];
}
/**
* Takes a lot of time. Must not be called on the main thread.
*/
public void init() {
ensureLoaded(mGrammar);
}
/**
* Takes a lot of time. Must not be called on the main thread.
*/
public String translate(String input) {
ensureLoaded(mGrammar);
return translateInternal(input);
}
private synchronized void ensureLoaded(String grammarName) {
if (mPgf != null) return;
try {
// TODO: use PGF API to read this directly from assets
Log.d(TAG, "Copying grammar...");
File file = copyAsset(grammarName);
Log.d(TAG, "Trying to open " + file);
mPgf = PGF.readPGF(file.getPath());
} catch (FileNotFoundException e) {
Log.e(TAG, "File not found", e);
} catch (IOException e) {
Log.e(TAG, "Error loading grammar", e);
}
}
private File copyAsset(String asset) throws IOException {
InputStream in = null;
OutputStream out = null;
try {
in = getContext().getAssets().open(asset);
out = getContext().openFileOutput(asset, Context.MODE_PRIVATE);
byte[] buf = new byte[4096];
int len;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
return getContext().getFileStreamPath(asset);
} finally {
if (in != null) {
in.close();
}
if (out != null) {
out.close();
}
}
}
protected String translateInternal(String input) {
try {
Concr sourceGrammar = getConcr(getSourceLanguage().getConcrete());
Expr expr = sourceGrammar.parseBest("S", input);
Concr targetGrammar = getConcr(getTargetLanguage().getConcrete());
String output = targetGrammar.linearize(expr);
return output;
} catch (ParseError e) {
Log.e(TAG, "Parse error: " + e);
return "parse error"; // TODO: no no no
}
}
private Concr getConcr(String name) {
return mPgf.getLanguages().get(name);
}
}

View File

@@ -1,134 +0,0 @@
package se.fnord.android.layout;
import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;
/**
* ViewGroup that arranges child views in a similar way to text, with them laid
* out one line at a time and "wrapping" to the next line as needed.
*
* Code licensed under CC-by-SA
*
* @author Henrik Gustafsson
* @see http://stackoverflow.com/questions/549451/line-breaking-widget-layout-for-android
* @license http://creativecommons.org/licenses/by-sa/2.5/
*
*/
public class PredicateLayout extends ViewGroup {
private int line_height;
public static class LayoutParams extends ViewGroup.LayoutParams {
public final int horizontal_spacing;
public final int vertical_spacing;
/**
* @param horizontal_spacing Pixels between items, horizontally
* @param vertical_spacing Pixels between items, vertically
*/
public LayoutParams(int horizontal_spacing, int vertical_spacing) {
this(0, 0, horizontal_spacing, vertical_spacing);
}
/**
* @param width
* @param height
* @param horizontal_spacing Pixels between items, horizontally
* @param vertical_spacing Pixels between items, vertically
*/
public LayoutParams(int width, int height, int horizontal_spacing, int vertical_spacing) {
super(width, height);
this.horizontal_spacing = horizontal_spacing;
this.vertical_spacing = vertical_spacing;
}
}
public PredicateLayout(Context context) {
super(context);
}
public PredicateLayout(Context context, AttributeSet attrs){
super(context, attrs);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
assert(MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.UNSPECIFIED);
final int width = MeasureSpec.getSize(widthMeasureSpec) - getPaddingLeft() - getPaddingRight();
int height = MeasureSpec.getSize(heightMeasureSpec) - getPaddingTop() - getPaddingBottom();
final int count = getChildCount();
int line_height = 0;
int xpos = getPaddingLeft();
int ypos = getPaddingTop();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
child.measure(
MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST),
MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
final int childw = child.getMeasuredWidth();
line_height = Math.max(line_height, child.getMeasuredHeight() + lp.vertical_spacing);
if (xpos + childw > width) {
xpos = getPaddingLeft();
ypos += line_height;
}
xpos += childw + lp.horizontal_spacing;
}
}
this.line_height = line_height;
if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.UNSPECIFIED){
height = ypos + line_height;
} else if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.AT_MOST){
if (ypos + line_height < height){
height = ypos + line_height;
}
}
setMeasuredDimension(width, height);
}
@Override
protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
return new LayoutParams(1, 1); // default of 1px spacing
}
@Override
protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
if (p instanceof LayoutParams)
return true;
return false;
}
@Override
protected void onLayout(boolean changed, int l, int t, int r, int b) {
final int count = getChildCount();
final int width = r - l;
int xpos = getPaddingLeft();
int ypos = getPaddingTop();
for (int i = 0; i < count; i++) {
final View child = getChildAt(i);
if (child.getVisibility() != GONE) {
final int childw = child.getMeasuredWidth();
final int childh = child.getMeasuredHeight();
final LayoutParams lp = (LayoutParams) child.getLayoutParams();
if (xpos + childw > width) {
xpos = getPaddingLeft();
ypos += line_height;
}
child.layout(xpos, ypos, xpos + childw, ypos + childh);
xpos += childw + lp.horizontal_spacing;
}
}
}
}