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.
@@ -3,11 +3,7 @@
|
|||||||
<classpathentry kind="src" path="src"/>
|
<classpathentry kind="src" path="src"/>
|
||||||
<classpathentry kind="src" path="gen"/>
|
<classpathentry kind="src" path="gen"/>
|
||||||
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
|
<classpathentry kind="con" path="com.android.ide.eclipse.adt.ANDROID_FRAMEWORK"/>
|
||||||
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.LIBRARIES">
|
<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.DEPENDENCIES"/>
|
<classpathentry exported="true" kind="con" path="com.android.ide.eclipse.adt.DEPENDENCIES"/>
|
||||||
<classpathentry kind="output" path="bin/classes"/>
|
<classpathentry kind="output" path="bin/classes"/>
|
||||||
</classpath>
|
</classpath>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<projectDescription>
|
<projectDescription>
|
||||||
<name>GFTranslator</name>
|
<name>GFVoiceExample</name>
|
||||||
<comment></comment>
|
<comment></comment>
|
||||||
<projects>
|
<projects>
|
||||||
</projects>
|
</projects>
|
||||||
|
|||||||
@@ -2,19 +2,21 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
package="org.grammaticalframework.ui.android"
|
package="org.grammaticalframework.ui.android"
|
||||||
android:versionCode="1"
|
android:versionCode="1"
|
||||||
android:versionName="1.0">
|
android:versionName="1.0" >
|
||||||
|
|
||||||
<uses-sdk
|
<uses-sdk
|
||||||
android:minSdkVersion="8"
|
android:minSdkVersion="8"
|
||||||
android:targetSdkVersion="8" />
|
android:targetSdkVersion="18" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.RECORD_AUDIO" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:icon="@drawable/ic_launcher"
|
android:icon="@drawable/ic_launcher"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:theme="@style/AppTheme">
|
android:theme="@style/AppTheme" >
|
||||||
<activity
|
<activity
|
||||||
android:name="org.grammaticalframework.ui.android.MainActivity"
|
android:name=".MainActivity"
|
||||||
android:label="@string/app_name" >
|
android:label="@string/app_name" >
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN" />
|
||||||
|
|||||||
66
src/ui/android/README
Normal 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
|
||||||
BIN
src/ui/android/assets/ResourceDemo.pgf
Normal file
92
src/ui/android/build.xml
Normal 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>
|
||||||
BIN
src/ui/android/libs/android-support-v4.jar
Normal file
BIN
src/ui/android/libs/armeabi/libjpgf.so
Normal file
BIN
src/ui/android/libs/libjpgf.jar
Normal file
20
src/ui/android/proguard-project.txt
Normal 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 *;
|
||||||
|
#}
|
||||||
BIN
src/ui/android/res/drawable-hdpi/ic_action_switch.png
Normal file
|
After Width: | Height: | Size: 436 B |
BIN
src/ui/android/res/drawable-hdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 4.4 KiB |
BIN
src/ui/android/res/drawable-hdpi/ic_mic.png
Normal file
|
After Width: | Height: | Size: 665 B |
BIN
src/ui/android/res/drawable-mdpi/ic_action_switch.png
Normal file
|
After Width: | Height: | Size: 327 B |
BIN
src/ui/android/res/drawable-mdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 2.7 KiB |
BIN
src/ui/android/res/drawable-mdpi/ic_mic.png
Normal file
|
After Width: | Height: | Size: 437 B |
BIN
src/ui/android/res/drawable-xhdpi/ic_action_switch.png
Normal file
|
After Width: | Height: | Size: 547 B |
BIN
src/ui/android/res/drawable-xhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 5.9 KiB |
BIN
src/ui/android/res/drawable-xhdpi/ic_mic.png
Normal file
|
After Width: | Height: | Size: 783 B |
BIN
src/ui/android/res/drawable-xxhdpi/ic_launcher.png
Normal file
|
After Width: | Height: | Size: 9.8 KiB |
|
Before Width: | Height: | Size: 35 KiB |
@@ -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>
|
||||||
|
Before Width: | Height: | Size: 2.5 KiB |
@@ -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>
|
||||||
85
src/ui/android/res/layout/activity_main.xml
Normal 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>
|
||||||
11
src/ui/android/res/layout/first_person_utterance.xml
Normal 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"
|
||||||
|
/>
|
||||||
8
src/ui/android/res/layout/languages_item.xml
Normal 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"
|
||||||
|
/>
|
||||||
12
src/ui/android/res/layout/second_person_utterance.xml
Normal 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"
|
||||||
|
/>
|
||||||
8
src/ui/android/res/values-sw600dp/dimens.xml
Normal 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>
|
||||||
9
src/ui/android/res/values-sw720dp-land/dimens.xml
Normal 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>
|
||||||
11
src/ui/android/res/values-v11/styles.xml
Normal 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>
|
||||||
12
src/ui/android/res/values-v14/styles.xml
Normal 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>
|
||||||
7
src/ui/android/res/values/dimens.xml
Normal 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>
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<resources>
|
<resources>
|
||||||
|
|
||||||
<string name="app_name">GFTranslator</string>
|
<string name="app_name">GF Translator</string>
|
||||||
<string name="action_settings">Settings</string>
|
|
||||||
<string name="hello_world">Hello world!</string>
|
<string name="microphone">Microphone</string>
|
||||||
|
<string name="switch_languages">Switch languages</string>
|
||||||
|
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
20
src/ui/android/res/values/styles.xml
Normal 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>
|
||||||
240
src/ui/android/src/org/grammaticalframework/ui/android/ASR.java
Normal 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
// }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||