summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorbringert <bringert@google.com>2013-10-04 16:26:46 +0000
committerbringert <bringert@google.com>2013-10-04 16:26:46 +0000
commita35b286ce040919a71d27e5bf696886237c6c470 (patch)
tree4e54cc1994a7f0760f76b0efe8c9e85ef38ec9ef /src
parentf156a5e8f9c903f132541e1c34f1c9f5c1ee69af (diff)
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.
Diffstat (limited to 'src')
-rw-r--r--src/ui/android/.classpath6
-rw-r--r--src/ui/android/.project2
-rw-r--r--src/ui/android/AndroidManifest.xml10
-rw-r--r--src/ui/android/README66
-rw-r--r--src/ui/android/assets/ResourceDemo.pgfbin0 -> 3265678 bytes
-rw-r--r--src/ui/android/build.xml92
-rw-r--r--src/ui/android/libs/android-support-v4.jarbin0 -> 556198 bytes
-rw-r--r--src/ui/android/libs/armeabi/libjpgf.sobin0 -> 100528 bytes
-rw-r--r--src/ui/android/libs/libjpgf.jarbin0 -> 6277 bytes
-rw-r--r--src/ui/android/proguard-project.txt20
-rw-r--r--src/ui/android/res/drawable-hdpi/ic_action_switch.pngbin0 -> 436 bytes
-rw-r--r--src/ui/android/res/drawable-hdpi/ic_launcher.pngbin0 -> 4541 bytes
-rw-r--r--src/ui/android/res/drawable-hdpi/ic_mic.pngbin0 -> 665 bytes
-rw-r--r--src/ui/android/res/drawable-mdpi/ic_action_switch.pngbin0 -> 327 bytes
-rw-r--r--src/ui/android/res/drawable-mdpi/ic_launcher.pngbin0 -> 2785 bytes
-rw-r--r--src/ui/android/res/drawable-mdpi/ic_mic.pngbin0 -> 437 bytes
-rw-r--r--src/ui/android/res/drawable-xhdpi/ic_action_switch.pngbin0 -> 547 bytes
-rw-r--r--src/ui/android/res/drawable-xhdpi/ic_launcher.pngbin0 -> 6059 bytes
-rw-r--r--src/ui/android/res/drawable-xhdpi/ic_mic.pngbin0 -> 783 bytes
-rw-r--r--src/ui/android/res/drawable-xxhdpi/ic_launcher.pngbin0 -> 9991 bytes
-rw-r--r--src/ui/android/res/drawable/brushed_metal.pngbin36233 -> 0 bytes
-rw-r--r--src/ui/android/res/drawable/first_person_utterance_bg.xml6
-rw-r--r--src/ui/android/res/drawable/icon.pngbin2574 -> 0 bytes
-rw-r--r--src/ui/android/res/drawable/second_person_utterance_bg.xml6
-rw-r--r--src/ui/android/res/layout/activity_main.xml85
-rw-r--r--src/ui/android/res/layout/first_person_utterance.xml11
-rw-r--r--src/ui/android/res/layout/languages_item.xml8
-rw-r--r--src/ui/android/res/layout/second_person_utterance.xml12
-rw-r--r--src/ui/android/res/values-sw600dp/dimens.xml8
-rw-r--r--src/ui/android/res/values-sw720dp-land/dimens.xml9
-rw-r--r--src/ui/android/res/values-v11/styles.xml11
-rw-r--r--src/ui/android/res/values-v14/styles.xml12
-rw-r--r--src/ui/android/res/values/dimens.xml7
-rw-r--r--src/ui/android/res/values/strings.xml7
-rw-r--r--src/ui/android/res/values/styles.xml20
-rw-r--r--src/ui/android/src/org/grammaticalframework/ui/android/ASR.java240
-rw-r--r--src/ui/android/src/org/grammaticalframework/ui/android/ConversationView.java62
-rw-r--r--src/ui/android/src/org/grammaticalframework/ui/android/Language.java31
-rw-r--r--src/ui/android/src/org/grammaticalframework/ui/android/LanguageSelector.java51
-rw-r--r--src/ui/android/src/org/grammaticalframework/ui/android/LanguagesAdapter.java16
-rw-r--r--src/ui/android/src/org/grammaticalframework/ui/android/LocaleUtils.java42
-rw-r--r--src/ui/android/src/org/grammaticalframework/ui/android/MainActivity.java202
-rw-r--r--src/ui/android/src/org/grammaticalframework/ui/android/TTS.java63
-rw-r--r--src/ui/android/src/org/grammaticalframework/ui/android/Translator.java144
-rw-r--r--src/ui/android/src/se/fnord/android/layout/PredicateLayout.java134
45 files changed, 1236 insertions, 147 deletions
diff --git a/src/ui/android/.classpath b/src/ui/android/.classpath
index 2f27127f0..7bc01d9a9 100644
--- a/src/ui/android/.classpath
+++ b/src/ui/android/.classpath
@@ -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>
diff --git a/src/ui/android/.project b/src/ui/android/.project
index abf063e43..2773e5ef0 100644
--- a/src/ui/android/.project
+++ b/src/ui/android/.project
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
- <name>GFTranslator</name>
+ <name>GFVoiceExample</name>
<comment></comment>
<projects>
</projects>
diff --git a/src/ui/android/AndroidManifest.xml b/src/ui/android/AndroidManifest.xml
index 9fef8e112..9728b71aa 100644
--- a/src/ui/android/AndroidManifest.xml
+++ b/src/ui/android/AndroidManifest.xml
@@ -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" />
diff --git a/src/ui/android/README b/src/ui/android/README
new file mode 100644
index 000000000..1d3bda521
--- /dev/null
+++ b/src/ui/android/README
@@ -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 \ No newline at end of file
diff --git a/src/ui/android/assets/ResourceDemo.pgf b/src/ui/android/assets/ResourceDemo.pgf
new file mode 100644
index 000000000..c40f38a1d
--- /dev/null
+++ b/src/ui/android/assets/ResourceDemo.pgf
Binary files differ
diff --git a/src/ui/android/build.xml b/src/ui/android/build.xml
new file mode 100644
index 000000000..a10a91491
--- /dev/null
+++ b/src/ui/android/build.xml
@@ -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>
diff --git a/src/ui/android/libs/android-support-v4.jar b/src/ui/android/libs/android-support-v4.jar
new file mode 100644
index 000000000..cf12d2839
--- /dev/null
+++ b/src/ui/android/libs/android-support-v4.jar
Binary files differ
diff --git a/src/ui/android/libs/armeabi/libjpgf.so b/src/ui/android/libs/armeabi/libjpgf.so
new file mode 100644
index 000000000..69014824d
--- /dev/null
+++ b/src/ui/android/libs/armeabi/libjpgf.so
Binary files differ
diff --git a/src/ui/android/libs/libjpgf.jar b/src/ui/android/libs/libjpgf.jar
new file mode 100644
index 000000000..e472505d3
--- /dev/null
+++ b/src/ui/android/libs/libjpgf.jar
Binary files differ
diff --git a/src/ui/android/proguard-project.txt b/src/ui/android/proguard-project.txt
new file mode 100644
index 000000000..f2fe1559a
--- /dev/null
+++ b/src/ui/android/proguard-project.txt
@@ -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 *;
+#}
diff --git a/src/ui/android/res/drawable-hdpi/ic_action_switch.png b/src/ui/android/res/drawable-hdpi/ic_action_switch.png
new file mode 100644
index 000000000..5449a32b8
--- /dev/null
+++ b/src/ui/android/res/drawable-hdpi/ic_action_switch.png
Binary files differ
diff --git a/src/ui/android/res/drawable-hdpi/ic_launcher.png b/src/ui/android/res/drawable-hdpi/ic_launcher.png
new file mode 100644
index 000000000..a6c350aea
--- /dev/null
+++ b/src/ui/android/res/drawable-hdpi/ic_launcher.png
Binary files differ
diff --git a/src/ui/android/res/drawable-hdpi/ic_mic.png b/src/ui/android/res/drawable-hdpi/ic_mic.png
new file mode 100644
index 000000000..f79ff489b
--- /dev/null
+++ b/src/ui/android/res/drawable-hdpi/ic_mic.png
Binary files differ
diff --git a/src/ui/android/res/drawable-mdpi/ic_action_switch.png b/src/ui/android/res/drawable-mdpi/ic_action_switch.png
new file mode 100644
index 000000000..ecf7d0347
--- /dev/null
+++ b/src/ui/android/res/drawable-mdpi/ic_action_switch.png
Binary files differ
diff --git a/src/ui/android/res/drawable-mdpi/ic_launcher.png b/src/ui/android/res/drawable-mdpi/ic_launcher.png
new file mode 100644
index 000000000..204c58a8f
--- /dev/null
+++ b/src/ui/android/res/drawable-mdpi/ic_launcher.png
Binary files differ
diff --git a/src/ui/android/res/drawable-mdpi/ic_mic.png b/src/ui/android/res/drawable-mdpi/ic_mic.png
new file mode 100644
index 000000000..8f7f55cf9
--- /dev/null
+++ b/src/ui/android/res/drawable-mdpi/ic_mic.png
Binary files differ
diff --git a/src/ui/android/res/drawable-xhdpi/ic_action_switch.png b/src/ui/android/res/drawable-xhdpi/ic_action_switch.png
new file mode 100644
index 000000000..b5da00fb2
--- /dev/null
+++ b/src/ui/android/res/drawable-xhdpi/ic_action_switch.png
Binary files differ
diff --git a/src/ui/android/res/drawable-xhdpi/ic_launcher.png b/src/ui/android/res/drawable-xhdpi/ic_launcher.png
new file mode 100644
index 000000000..7f212cc6b
--- /dev/null
+++ b/src/ui/android/res/drawable-xhdpi/ic_launcher.png
Binary files differ
diff --git a/src/ui/android/res/drawable-xhdpi/ic_mic.png b/src/ui/android/res/drawable-xhdpi/ic_mic.png
new file mode 100644
index 000000000..13d21274a
--- /dev/null
+++ b/src/ui/android/res/drawable-xhdpi/ic_mic.png
Binary files differ
diff --git a/src/ui/android/res/drawable-xxhdpi/ic_launcher.png b/src/ui/android/res/drawable-xxhdpi/ic_launcher.png
new file mode 100644
index 000000000..da2c7a235
--- /dev/null
+++ b/src/ui/android/res/drawable-xxhdpi/ic_launcher.png
Binary files differ
diff --git a/src/ui/android/res/drawable/brushed_metal.png b/src/ui/android/res/drawable/brushed_metal.png
deleted file mode 100644
index c2f03fe7d..000000000
--- a/src/ui/android/res/drawable/brushed_metal.png
+++ /dev/null
Binary files differ
diff --git a/src/ui/android/res/drawable/first_person_utterance_bg.xml b/src/ui/android/res/drawable/first_person_utterance_bg.xml
new file mode 100644
index 000000000..9eb02aef1
--- /dev/null
+++ b/src/ui/android/res/drawable/first_person_utterance_bg.xml
@@ -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> \ No newline at end of file
diff --git a/src/ui/android/res/drawable/icon.png b/src/ui/android/res/drawable/icon.png
deleted file mode 100644
index a07c69fa5..000000000
--- a/src/ui/android/res/drawable/icon.png
+++ /dev/null
Binary files differ
diff --git a/src/ui/android/res/drawable/second_person_utterance_bg.xml b/src/ui/android/res/drawable/second_person_utterance_bg.xml
new file mode 100644
index 000000000..4acf07c67
--- /dev/null
+++ b/src/ui/android/res/drawable/second_person_utterance_bg.xml
@@ -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> \ No newline at end of file
diff --git a/src/ui/android/res/layout/activity_main.xml b/src/ui/android/res/layout/activity_main.xml
new file mode 100644
index 000000000..b0ccab0ea
--- /dev/null
+++ b/src/ui/android/res/layout/activity_main.xml
@@ -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> \ No newline at end of file
diff --git a/src/ui/android/res/layout/first_person_utterance.xml b/src/ui/android/res/layout/first_person_utterance.xml
new file mode 100644
index 000000000..55779ae8f
--- /dev/null
+++ b/src/ui/android/res/layout/first_person_utterance.xml
@@ -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"
+ />
diff --git a/src/ui/android/res/layout/languages_item.xml b/src/ui/android/res/layout/languages_item.xml
new file mode 100644
index 000000000..d5f47ab27
--- /dev/null
+++ b/src/ui/android/res/layout/languages_item.xml
@@ -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"
+ />
diff --git a/src/ui/android/res/layout/second_person_utterance.xml b/src/ui/android/res/layout/second_person_utterance.xml
new file mode 100644
index 000000000..416d85328
--- /dev/null
+++ b/src/ui/android/res/layout/second_person_utterance.xml
@@ -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"
+ />
diff --git a/src/ui/android/res/values-sw600dp/dimens.xml b/src/ui/android/res/values-sw600dp/dimens.xml
new file mode 100644
index 000000000..44f01db75
--- /dev/null
+++ b/src/ui/android/res/values-sw600dp/dimens.xml
@@ -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>
diff --git a/src/ui/android/res/values-sw720dp-land/dimens.xml b/src/ui/android/res/values-sw720dp-land/dimens.xml
new file mode 100644
index 000000000..61e3fa8fb
--- /dev/null
+++ b/src/ui/android/res/values-sw720dp-land/dimens.xml
@@ -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>
diff --git a/src/ui/android/res/values-v11/styles.xml b/src/ui/android/res/values-v11/styles.xml
new file mode 100644
index 000000000..3c02242ad
--- /dev/null
+++ b/src/ui/android/res/values-v11/styles.xml
@@ -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>
diff --git a/src/ui/android/res/values-v14/styles.xml b/src/ui/android/res/values-v14/styles.xml
new file mode 100644
index 000000000..a91fd0372
--- /dev/null
+++ b/src/ui/android/res/values-v14/styles.xml
@@ -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>
diff --git a/src/ui/android/res/values/dimens.xml b/src/ui/android/res/values/dimens.xml
new file mode 100644
index 000000000..55c1e5908
--- /dev/null
+++ b/src/ui/android/res/values/dimens.xml
@@ -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>
diff --git a/src/ui/android/res/values/strings.xml b/src/ui/android/res/values/strings.xml
index 0d7b9a606..26601be1b 100644
--- a/src/ui/android/res/values/strings.xml
+++ b/src/ui/android/res/values/strings.xml
@@ -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>
diff --git a/src/ui/android/res/values/styles.xml b/src/ui/android/res/values/styles.xml
new file mode 100644
index 000000000..6ce89c7ba
--- /dev/null
+++ b/src/ui/android/res/values/styles.xml
@@ -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>
diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/ASR.java b/src/ui/android/src/org/grammaticalframework/ui/android/ASR.java
new file mode 100644
index 000000000..ef6df5198
--- /dev/null
+++ b/src/ui/android/src/org/grammaticalframework/ui/android/ASR.java
@@ -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);
+ }
+
+}
diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/ConversationView.java b/src/ui/android/src/org/grammaticalframework/ui/android/ConversationView.java
new file mode 100644
index 000000000..3923d13a0
--- /dev/null
+++ b/src/ui/android/src/org/grammaticalframework/ui/android/ConversationView.java
@@ -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);
+ }
+ }
+
+}
diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/Language.java b/src/ui/android/src/org/grammaticalframework/ui/android/Language.java
new file mode 100644
index 000000000..8adc74609
--- /dev/null
+++ b/src/ui/android/src/org/grammaticalframework/ui/android/Language.java
@@ -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();
+ }
+
+} \ No newline at end of file
diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/LanguageSelector.java b/src/ui/android/src/org/grammaticalframework/ui/android/LanguageSelector.java
new file mode 100644
index 000000000..d3148cda4
--- /dev/null
+++ b/src/ui/android/src/org/grammaticalframework/ui/android/LanguageSelector.java
@@ -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);
+ }
+
+}
diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/LanguagesAdapter.java b/src/ui/android/src/org/grammaticalframework/ui/android/LanguagesAdapter.java
new file mode 100644
index 000000000..e39ed7bd9
--- /dev/null
+++ b/src/ui/android/src/org/grammaticalframework/ui/android/LanguagesAdapter.java
@@ -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);
+ }
+
+}
diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/LocaleUtils.java b/src/ui/android/src/org/grammaticalframework/ui/android/LocaleUtils.java
new file mode 100644
index 000000000..9fb048908
--- /dev/null
+++ b/src/ui/android/src/org/grammaticalframework/ui/android/LocaleUtils.java
@@ -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);
+ }
+}
diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/MainActivity.java b/src/ui/android/src/org/grammaticalframework/ui/android/MainActivity.java
new file mode 100644
index 000000000..fba9987d6
--- /dev/null
+++ b/src/ui/android/src/org/grammaticalframework/ui/android/MainActivity.java
@@ -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);
+// }
+ }
+ }
+}
diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/TTS.java b/src/ui/android/src/org/grammaticalframework/ui/android/TTS.java
new file mode 100644
index 000000000..6993a3fc6
--- /dev/null
+++ b/src/ui/android/src/org/grammaticalframework/ui/android/TTS.java
@@ -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");
+ }
+ }
+
+ }
+}
diff --git a/src/ui/android/src/org/grammaticalframework/ui/android/Translator.java b/src/ui/android/src/org/grammaticalframework/ui/android/Translator.java
new file mode 100644
index 000000000..9ecdb104e
--- /dev/null
+++ b/src/ui/android/src/org/grammaticalframework/ui/android/Translator.java
@@ -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);
+ }
+
+}
diff --git a/src/ui/android/src/se/fnord/android/layout/PredicateLayout.java b/src/ui/android/src/se/fnord/android/layout/PredicateLayout.java
deleted file mode 100644
index 4734d4618..000000000
--- a/src/ui/android/src/se/fnord/android/layout/PredicateLayout.java
+++ /dev/null
@@ -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;
- }
- }
- }
-} \ No newline at end of file