diff --git a/.gitignore b/.gitignore index 5d5021c9fc97246e91c7ae6e4b6dd5bd82ed55d5..603b14077394cd2294ac6922fe619669630ef3ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,14 @@ -.gradle/ -.idea/ -app/app.iml -app/build/ -./build.gradle -build/ -gradle/ -gradlew -gradlew.bat -local.properties -speechutils.iml +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx diff --git a/README.md b/README.md index 650a2d2f49002d6832af07554586a990d0634fd8..f911801c2033ff5f6f938544a62d66f5ccdc0dcf 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,97 @@ -Speechutils -=========== +# speechutils -[![Codacy Badge](https://api.codacy.com/project/badge/grade/bc2e3589e2714093be39f876016b9ada)](https://www.codacy.com/app/kaljurand/speechutils) +**本项目是基于开源项目speechutils进行openharmony的移植和开发的,可以通过项目标签github地址()追踪到原项目版本** -Speechutils is an Android library that helps to implement apps that need to include speech-to-text and text-to-speech functionality. -For example, it provides methods for +## 项目介绍 -- audio recording and encoding -- aggregating speech-to-text and text-to-speech services -- playing audio cues before/after speech-to-text -- pausing the background audio during speech-to-text +- 项目名称:语音转换库 +- 所属系列:鸿蒙的第三方组件适配移植 +- 功能:可以通过在AbilitySlice中继承AsrBaseAbilitySlice来继承语音转文字功能,继承 TtsBaseAbilitySlice 来实现文字转语音功能 +方便:只需继承BaseAbilitySlice便可拥有转换功能 +灵活: 封装了多个控制方法直接使用即可 +- +- 项目移植状态: +- 调用差异:使用鸿蒙api与源项目调用方式不一样 +- 开发版本:sdk4,DevEco Studio2.1 beta2 +- 项目作者和维护人:王江涛 +- 联系方式:wangjiangtao003@chinasoftinc.com +- 原项目Doc地址: -Used by -------- +## 项目介绍 -- https://github.com/Kaljurand/K6nele -- https://github.com/Kaljurand/Arvutaja -- https://github.com/willblaschko/AlexaAndroid +- 编程语言:Java -Testing -------- +## 安装教程 - adb shell am instrument -w -r \ - -e package ee.ioc.phon.android.speechutils -e debug false \ - ee.ioc.phon.android.speechutils.test/androidx.test.runner.AndroidJUnitRunner +1. 在主项目entry中的grade文件依赖 +``` +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar', '*.har']) + implementation project(path: ':speechutils') +} +``` -Some tests currently fail: - Tests run: 142, Failures: 10 +如无法运行,删除项目.gradle,.idea,build,gradle,build.gradle文件, + +并依据自己的版本创建新项目,将新项目的对应文件复制到根目录下 +## 用法 +1.语音转文字功能 +在功能AbilityASlice继承AsrBaseAbilitySlice,实现 showAsrResult(String info)方法 +开始录音转换,直接使用startRecoding()方法,开始转换 ,转换成功后会回调到 showAsrResult(String info)方法中 +停止转换 需调用 stopRecoding()方法停止(注意在不适用的时候必须调用,否则会造成语音转文字失败) + +```java + recorderView.setTouchEventListener((component, touchEvent) -> { + switch (touchEvent.getAction()) { + case TouchEvent.PRIMARY_POINT_DOWN: + HiLog.info(LABEL_LOG, "按钮按下"); + startRecoding(); + break; + case TouchEvent.PRIMARY_POINT_UP: + HiLog.info(LABEL_LOG, "按钮松开"); + stopRecoding(); + break; + default: + break; + } + return false; + }); +``` +2. 文字转语音功能 + 在功能AbilityASlice 继承TtsBaseAbilitySlice,实现showInfo(String info)方法 +开始文字转语音,直接调用startTts("需要转换为语音的文字")方法,转换成功后会回调到 showInfo(String info)方法中 +停止转换 需调用 stopSpeaking 方法 停止(注意在不适用的时候必须调用,否则会造成文字转语音失败) + +```java + startPlay.setClickedListener(new Component.ClickedListener() { + @Override + public void onClick(Component component) { + startTts(textPhonetic.getText()); + } + }); + stopPlay.setClickedListener(new Component.ClickedListener() { + @Override + public void onClick(Component component) { + stopSpeaking(); + } + }); +``` + + +## 效果图 + + + + +## 测试信息 +CodeCheck代码测试无异常 + +CloudTest代码测试无异常 + +火绒安全病毒安全检测通过 + +测试员:李向涛 + +## 版本迭代 +0.0.1-SNAPSHOT diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index 3d2ddb6bb2835f4e329e62095a0300e7749296f9..0000000000000000000000000000000000000000 --- a/app/build.gradle +++ /dev/null @@ -1,33 +0,0 @@ -apply plugin: 'com.android.library' - -dependencies { - // Required -- JUnit 4 framework - testImplementation 'junit:junit:4.12' - - androidTestImplementation 'androidx.test.ext:junit:1.1.2' - androidTestImplementation 'androidx.test:rules:1.3.0' - // Optional -- Hamcrest library - androidTestImplementation 'org.hamcrest:hamcrest-library:1.3' - - implementation 'commons-io:commons-io:2.5' - implementation 'androidx.annotation:annotation:1.2.0' - implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'com.github.curious-odd-man:rgxgen:1.3' -} - -android { - compileSdkVersion rootProject.compileSdkVersion - - defaultConfig { - minSdkVersion 14 - targetSdkVersion 30 - versionCode 500 - versionName '0.5.00' - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/ee/ioc/phon/android/speechutils/RawAudioRecorderTest.java b/app/src/androidTest/java/ee/ioc/phon/android/speechutils/RawAudioRecorderTest.java deleted file mode 100644 index 5df8bd734c24ab424117c8025a5b63df27cfafb7..0000000000000000000000000000000000000000 --- a/app/src/androidTest/java/ee/ioc/phon/android/speechutils/RawAudioRecorderTest.java +++ /dev/null @@ -1,31 +0,0 @@ -package ee.ioc.phon.android.speechutils; - -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import static org.junit.Assert.assertTrue; - -@RunWith(AndroidJUnit4.class) -public class RawAudioRecorderTest { - - private RawAudioRecorder mRar; - - @Before - public void before() { - mRar = new RawAudioRecorder(); - } - - @Test - public void test01() { - assertTrue(mRar.getState().equals(AudioRecorder.State.READY)); - } - - @Test - public void test02() { - assertTrue(mRar.isPausing()); - } - -} \ No newline at end of file diff --git a/app/src/androidTest/java/ee/ioc/phon/android/speechutils/RecognitionServiceManagerTest.java b/app/src/androidTest/java/ee/ioc/phon/android/speechutils/RecognitionServiceManagerTest.java deleted file mode 100644 index 787de01bbb69d0546f8cc3e2e74a0a6498835a92..0000000000000000000000000000000000000000 --- a/app/src/androidTest/java/ee/ioc/phon/android/speechutils/RecognitionServiceManagerTest.java +++ /dev/null @@ -1,34 +0,0 @@ -package ee.ioc.phon.android.speechutils; - -import org.junit.Test; - -import static ee.ioc.phon.android.speechutils.RecognitionServiceManager.makeLangLabel; -import static org.junit.Assert.assertEquals; - -public class RecognitionServiceManagerTest { - - @Test - public void makeLangLabel01() { - assertEquals("?", makeLangLabel(null)); - } - - @Test - public void makeLangLabel02() { - assertEquals("Estonian (Estonia)", makeLangLabel("et-EE")); - } - - @Test - public void makeLangLabel03() { - assertEquals("German (Austria)", makeLangLabel("de-AT")); - } - - @Test - public void makeLangLabel04() { - assertEquals("Võro (Estonia)", makeLangLabel("vro-ee")); - } - - @Test - public void makeLangLabel05() { - assertEquals("", makeLangLabel("und")); - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/ee/ioc/phon/android/speechutils/editor/CommandTest.java b/app/src/androidTest/java/ee/ioc/phon/android/speechutils/editor/CommandTest.java deleted file mode 100644 index e28c369de2893e59c6ffde8a7bd8ab6646a91fe4..0000000000000000000000000000000000000000 --- a/app/src/androidTest/java/ee/ioc/phon/android/speechutils/editor/CommandTest.java +++ /dev/null @@ -1,115 +0,0 @@ -package ee.ioc.phon.android.speechutils.editor; - -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.util.regex.Pattern; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; - -@RunWith(AndroidJUnit4.class) -public class CommandTest { - - // Argument arrays are trimmed, i.e. nulls and empties are removed from the end. - // However, whitespace is not removed from the values. - private final Command c = makeCommand("c", "replaceSel", new String[]{"c"}); - private final Command c1 = makeCommand("c1", "replaceSel", new String[]{"c"}); - private final Command c2 = makeCommand("c2", "replaceSel", new String[]{"c2"}); - private final Command c3 = makeCommand("c3", "replaceSel", new String[]{"c", "", null}); - private final Command c4 = makeCommand("c4", "replaceSel", new String[]{"c", "d"}); - private final Command c5 = makeCommand("c5", "replaceSel", new String[]{}); - private final Command c6 = makeCommand("c6", "replaceSel", new String[]{"c", null, null}); - private final Command c7 = makeCommand("c7", "replaceSel", new String[]{"c", " "}); - - private final Command empty1 = makeCommand("empty1", "id", new String[]{}); - private final Command empty2 = makeCommand("empty2", "id", new String[]{""}); - private final Command empty3 = makeCommand("empty3", "id", new String[]{null}); - private final Command empty4 = makeCommand("empty4", "id", null); - - @Test - public void test01() { - assertTrue(c.equalsCommand(c1)); - } - - @Test - public void test02() { - assertFalse(c.equalsCommand(c2)); - } - - @Test - public void test03() { - assertTrue(c.equalsCommand(c3)); - } - - @Test - public void test04() { - assertFalse(c.equalsCommand(c4)); - } - - @Test - public void test05() { - assertFalse(c.equalsCommand(c5)); - } - - @Test - public void test06() { - assertTrue(c.equalsCommand(c6)); - } - - @Test - public void test07() { - assertFalse(c.equalsCommand(c7)); - } - - @Test - public void test08() { - assertTrue(empty1.equalsCommand(empty2) && empty2.equalsCommand(empty3) && empty3.equalsCommand(empty4) && empty4.equalsCommand(empty1)); - } - - @Test - public void testMakeUtt01() { - assertEquals(makeCommand("matcher").makeUtt(), "matcher"); - } - - @Test - public void testMakeUtt02() { - assertEquals(makeCommand("mat*cher*").makeUtt(), "mache"); - } - - @Test - public void testMakeUtt03() { - assertEquals(makeCommand("([123]00)").makeUtt(), "100"); - } - - @Test - public void testMakeUtt04() { - assertEquals(makeCommand("((?:3|2|1)00)").makeUtt(), "300"); - } - - @Test - public void testMakeUtt05() { - assertEquals(makeCommand("a b? c").makeUtt(), "a c"); - } - - @Test - public void testMakeUtt06() { - assertEquals(makeCommand("ab?c|d").makeUtt(), "ac"); - } - - @Test - public void testMakeUtt07() { - assertEquals(makeCommand("a b? c|d").makeUtt(), "a c"); - } - - private Command makeCommand(String label, String id, String[] args) { - return new Command(label, "", null, null, null, Pattern.compile("^" + label + "$"), "", id, args); - } - - private Command makeCommand(String utt) { - return new Command("^" + utt + "$", ""); - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditorTest.java b/app/src/androidTest/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditorTest.java deleted file mode 100644 index 42c9ec34fdf5a4e92e3cbade53e4a405128a1203..0000000000000000000000000000000000000000 --- a/app/src/androidTest/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditorTest.java +++ /dev/null @@ -1,1580 +0,0 @@ -package ee.ioc.phon.android.speechutils.editor; - -import android.content.Context; -import android.os.Build; -import android.view.inputmethod.EditorInfo; -import android.widget.EditText; - -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; - -import static androidx.test.platform.app.InstrumentationRegistry.getInstrumentation; -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNotNull; -import static org.junit.Assert.assertThat; -import static org.junit.Assert.assertTrue; - -// TODO: add tests for multiline input strings - -@RunWith(AndroidJUnit4.class) -public class InputConnectionCommandEditorTest { - private static final List URS; - - static { - // Simple replacements - List list1 = new ArrayList<>(); - list1.add(new Command("DELETE ME", "")); - list1.add(new Command("old_word", "new_word")); - list1.add(new Command("dollar sign", "\\$")); - list1.add(new Command("double backslash", "\\\\\\\\")); - list1.add(new Command("hyphen", "-")); - list1.add(new Command("space", " ")); - list1.add(new Command("newbullet", "\n- ")); - // TODO: are the following 2 supposed to be equivalent (did this change in Android 11?) - list1.add(new Command("prefix1(optional)?", "$1")); - list1.add(new Command("prefix2(optional|)", "$1")); - list1.add(new Command("replace with selection", "A @sel() B")); - - // Editor commands - List list2 = new ArrayList<>(); - list2.add(new Command("insert (.+)", "<>", "replace", new String[]{"<>", "$1"})); - list2.add(new Command("double (.+)", "<> <>", "replaceAll", new String[]{"<>", "$1"})); // TODO: replaceAll is not available - list2.add(new Command("s/(.*)/(.*)/", "", "replace", new String[]{"$1", "$2"})); - list2.add(new Command("connect (.*) and (.*)", "", "replace", new String[]{"$1 $2", "$1-$2"})); - list2.add(new Command("delete (.+)", "", "replace", new String[]{"$1"})); - list2.add(new Command("delete2 (.*)", "D2", "replace", new String[]{"$1", ""})); - list2.add(new Command("underscore (.*)", "", "replace", new String[]{"$1", "_$1_"})); - list2.add(new Command("select (.*)", "", "select", new String[]{"$1"})); - list2.add(new Command("selectAll", "", "selectAll")); - list2.add(new Command("resetSel", "", "moveRel", new String[]{"0"})); - // Add some text and then move to the beginning of the doc - list2.add(new Command("(.*)\\bmoveAbs0", "$1", "moveAbs", new String[]{"0"})); - list2.add(new Command("selection_replace (.*)", "", "replaceSel", new String[]{"$1"})); - list2.add(new Command("selection_underscore", "", "replaceSel", new String[]{"_@sel()_"})); - list2.add(new Command("replaceSelRe_noletters", "", "replaceSelRe", new String[]{"[a-z]", ""})); - list2.add(new Command("replaceSelRe_underscore", "", "replaceSelRe", new String[]{"(.+)", "_\\$1_"})); - //list2.add(new Command("replaceSelRe (.+?) .+ (.+)", "", "replaceSelRe", new String[]{"$1 (.+) $2", "$1 \\$1 $2"})); - list2.add(new Command("replaceSelRe ([^ ]+) .+ ([^ ]+)", "", "replaceSelRe", new String[]{"$1 ([^ ]+) $2", "$1 \\$1 $2"})); - list2.add(new Command("selection_quote", "", "replaceSel", new String[]{"\"@sel()\""})); - list2.add(new Command("selection_double", "", "replaceSel", new String[]{"@sel()@sel()"})); - // Hyphenates the current selection to the uttered number, adds brackets around the whole thing, - // and selects the uttered number. Notice the need to escape the closing bracket and the end marking dollar sign. - list2.add(new Command("selection_bracket ([0-9]+)", "", "replaceSel", new String[]{"(@sel()-$1)", "-([0-9]+)\\\\)\\$"})); - list2.add(new Command("selection_inc", "", "incSel")); - list2.add(new Command("selection_uc", "", "ucSel")); - list2.add(new Command("step back", "", "moveRel", new String[]{"-1"})); - list2.add(new Command("prev_sent", "", "selectReBefore", new String[]{"[.?!]()[^.?!]+[.?!][^.?!]+"})); - list2.add(new Command("first_number", "", "selectReAfter", new String[]{"(\\d)\\."})); - list2.add(new Command("second_number", "", "selectReAfter", new String[]{"(\\d)\\.", "2"})); - list2.add(new Command("next_word", "", "selectReAfter", new String[]{"\\b(.+?)\\b"})); - list2.add(new Command("next_next_word", "", "selectReAfter", new String[]{"\\b(.+?)\\b", "2"})); - list2.add(new Command("prev_sel", "", "selectReBefore", new String[]{"@sel()"})); - list2.add(new Command("next_sel", "", "selectReAfter", new String[]{"@sel()"})); - list2.add(new Command("code (\\d+)", "", "keyCode", new String[]{"$1"})); - list2.add(new Command("code letter (.)", "", "keyCodeStr", new String[]{"$1"})); - list2.add(new Command("undo (\\d+)", "", "undo", new String[]{"$1"})); - list2.add(new Command("combine (\\d+)", "", "combine", new String[]{"$1"})); - list2.add(new Command("apply (\\d+)", "", "apply", new String[]{"$1"})); - - // More simple replacements - List list3 = new ArrayList<>(); - list3.add(new Command("connect word1 and word2", "THIS SHOULD NOT MATCH")); - list3.add(new Command("(\\d+) times (\\d+)", "$1 * $2")); - // Positive lookbehind and lookahead - // Look-behind pattern matches must have a bounded maximum length - // list.add(new Command("(?<=\\d+) times_ (?=\\d+)", " * ")); - list3.add(new Command("(?<=\\d{0,8}) times_ (?=\\d+)", " * ")); - - URS = new ArrayList<>(); - URS.add(new UtteranceRewriter(Collections.unmodifiableList(list1))); - URS.add(new UtteranceRewriter(Collections.unmodifiableList(list2))); - URS.add(new UtteranceRewriter(Collections.unmodifiableList(list3))); - } - - private InputConnectionCommandEditor mEditor; - - @Before - public void before() { - Context context = getInstrumentation().getContext(); - EditText view = new EditText(context); - //view.setText("elas metsas mutionu, keset kuuski noori-vanu"); - EditorInfo editorInfo = new EditorInfo(); - //editorInfo.initialSelStart = 12; - //editorInfo.initialSelEnd = 19; - mEditor = new InputConnectionCommandEditor(context); - // maybe: new BaseInputConnection(view, true); - mEditor.setInputConnection(view.onCreateInputConnection(editorInfo)); - mEditor.setRewriters(URS); - } - - @Test - public void test01() { - assertNotNull(mEditor.getInputConnection()); - assertTrue(Op.NO_OP.isNoOp()); - } - - @Test - public void test02() { - add("start12345 67890"); - assertThatTextIs("Start12345 67890"); - runOp(mEditor.deleteLeftWord()); - assertThatTextIs("Start12345"); - runOp(mEditor.replace("12345", "")); - assertThatTextIs("Start"); - } - - @Test - public void test03() { - assertTrue(true); - } - - @Test - public void test04() { - addPartial("...123"); - addPartial("...124"); - assertThatTextIs("...124"); - add("...1245"); - runOp(mEditor.moveAbs(4)); - assertThat(getTextBeforeCursor(10), is("...1")); - add("-"); - assertThatTextIs("...1-245"); - add("undo 2", "-"); - assertThatTextIs("...1245-"); - } - - @Test - public void test05() { - add("a12345 67890_12345"); - runOp(mEditor.select("12345")); - assertThat(getTextBeforeCursor(2), is("0_")); - runOp(mEditor.deleteLeftWord()); - assertThatTextIs("A12345 67890_"); - runOp(mEditor.deleteLeftWord()); - assertThatTextIs("A12345"); - } - - @Test - public void test06() { - add("a12345 67890_12345"); - runOp(mEditor.replace("12345", "abcdef")); - runOp(mEditor.replaceSel(" ")); - runOp(mEditor.replace("12345", "ABC")); - assertThat(getTextBeforeCursor(2), is("BC")); - runOp(mEditor.replaceSel("\n")); - runOp(mEditor.replaceSel(" ")); - runOp(mEditor.moveAbs(9)); - assertThat(getTextBeforeCursor(2), is("67")); - } - - @Test - public void test07() { - add("123456789"); - runOp(mEditor.moveAbs(2)); - assertThat(getTextBeforeCursor(2), is("12")); - assertThatTextIs("123456789"); - } - - @Test - public void test008() { - add("double backslash"); - assertThatTextIs("\\\\"); - } - - @Test - public void test009() { - add("dollar sign"); - assertThatTextIs("$"); - } - - @Test - public void test10() { - add("test old_word test"); - assertThatTextIs("Test new_word test"); - } - - @Test - public void test11() { - add("test word1"); - add("s/word1/word2/"); - assertThatTextIs("Test word2"); - } - - @Test - public void test12() { - add("test word1 word2"); - add("connect word1 and word2"); - assertThatTextIs("Test word1-word2"); - } - - @Test - public void test13() { - add("test word1 word2"); - add("connect word1"); - add("and"); - assertThatUndoStackIs("[delete 4, delete 14, delete 16]"); - CommandEditorResult cer = mEditor.commitFinalResult("word2"); - assertTrue(cer.isSuccess()); - assertThat(cer.getRewrite().ppCommand(), is("replace (word1 word2) (word1-word2)")); - assertThatUndoStackIs("[undo replace2, delete 16]"); - assertThatTextIs("Test word1-word2"); - } - - @Test - public void test14() { - add("test word1"); - runOp(mEditor.replaceSel(" ")); - add("word2"); - assertThatTextIs("Test word1 word2"); - add("connect word1 and word2"); - assertThatTextIs("Test word1-word2"); - } - - @Test - public void test15() { - add("test word1"); - runOp(mEditor.replaceSel(" ")); - add("word2"); - assertThat(getTextBeforeCursor(11), is("word1 word2")); - runOp(mEditor.deleteAll()); - assertThat(getTextBeforeCursor(1), is("")); - } - - /** - * If command does not fully match then its replacement is ignored. - */ - @Test - public void test16() { - add("I will delete something"); - assertThatTextIs("I will delete something"); - } - - @Test - public void test17() { - add("there are word1 and word2..."); - add("select word1 and word2"); - runOp(mEditor.moveAbs(-100)); - assertThatTextIs("There are word1 and word2..."); - } - - @Test - public void test18() { - add("there are word1 and word2...", "select word1 and word2", "selection_replace REPL"); - assertThatTextIs("There are REPL..."); - } - - @Test - public void test19() { - add("there are word1 and word2...", "select word1 and word2", "selection_underscore"); - assertThatTextIs("There are _word1 and word2_..."); - undo(); - assertThatTextIs("There are word1 and word2..."); - } - - @Test - public void test20() { - add("a", "select a", "selection_double", "selection_double"); - runOp(mEditor.moveAbs(-1)); - assertThat(getTextBeforeCursor(5), is("AA")); - } - - @Test - public void test21() { - add("123456789", "select 3", "selection_inc"); - runOp(mEditor.moveRel(3)); - add("select 5", "selection_inc"); - assertThatTextIs("124466789"); - } - - @Test - public void test22() { - add("this is some word"); - add("select is some"); - add("selection_uc"); - assertThatTextIs("This IS SOME word"); - } - - @Test - public void test23() { - add("this is some word"); - runOp(mEditor.selectAll()); - add("selection_replace REPL"); - assertThatTextIs("REPL"); - } - - @Test - public void test24() { - add("test word1 word2"); - add("connect word1 and not_exist"); - assertThatTextIs("Test word1 word2"); - } - - @Test - public void test25() { - add("test word1 word2"); - undo(); - assertThatTextIs(""); - } - - @Test - public void test26() { - add("1234567890"); - add("step back"); - runOp(mEditor.moveRel(-1)); - undo(); - runOp(mEditor.deleteLeftWord()); - assertThatTextIs("0"); - undo(); - assertThatTextIs("1234567890"); - } - - // test27 - - @Test - public void test28() { - add("1234567890"); - runOp(mEditor.moveRel(-5)); - runOp(mEditor.moveRel(2)); - undo(2); - runOp(mEditor.moveRel(-1)); - runOp(mEditor.deleteLeftWord()); - undo(); - assertThatTextIs("1234567890"); - } - - /** - * old_word is rewritten into new_word and then changed using a command to NEWER_WORD - */ - @Test - public void test29() { - add("test old_word"); - assertThatTextIs("Test new_word"); - add("s/new_word/NEWER_WORD/"); - assertThatUndoStackIs("[undo replace2, delete 13]"); - assertThatTextIs("Test NEWER_WORD"); - undo(); - assertThatUndoStackIs("[delete 13]"); - assertThatTextIs("Test new_word"); - undo(); - assertThatTextIs(""); - } - - @Test - public void test30() { - runOp(mEditor.replaceSel(" ")); - assertThatTextIs(" "); - undo(); - assertThatTextIs(""); - } - - @Test - public void test30a() { - runOp(mEditor.replaceSel(null)); - assertThatTextIs(""); - undo(); - assertThatTextIs(""); - runOp(mEditor.replaceSel("")); - assertThatTextIs(""); - undo(); - assertThatTextIs(""); - } - - @Test - public void test31() { - add("there are word1 and word2...", "select word1 and word2", "selection_replace REPL"); - assertThatTextIs("There are REPL..."); - assertThatUndoStackIs("[deleteSurroundingText+commitText, setSelection, delete 28]"); - undo(); - assertThatTextIs("There are word1 and word2..."); - } - - /** - * Failed command must not change the editor content. - */ - @Test - public void test32() { - add("there are word1 and word2..."); - add("select nonexisting_word"); - undo(); - assertThatTextIs(""); - } - - /** - * Failed command must not change the editor content. - */ - @Test - public void test33() { - add("this is a text"); - add("this is another text"); - add("select nonexisting_word"); - undo(); - assertThatTextIs("This is a text"); - } - - /** - * Apply a command with a non-empty rewrite and undo it. - * First the command is undone, then the rewrite. - */ - @Test - public void test34() { - add("this_is_a_text"); - add("delete2 is_a"); - assertThatTextIs("This__text D2"); - assertThatUndoStackIs("[undo replace1, delete 3, delete 14]"); - undo(); - assertThatTextIs("This_is_a_text D2"); - undo(); - assertThatTextIs("This_is_a_text"); - } - - /** - * Undo a multi-segment command that succeeds. - */ - @Test - public void test35() { - add("test word1 word2"); - add("connect word1"); - add("and"); - CommandEditorResult cer = mEditor.commitFinalResult("word2"); - assertTrue(cer.isSuccess()); - assertThat(cer.getRewrite().ppCommand(), is("replace (word1 word2) (word1-word2)")); - assertThatUndoStackIs("[undo replace2, delete 16]"); - assertThatTextIs("Test word1-word2"); - undo(); - assertThatTextIs("Test word1 word2"); - } - - /** - * Undo a multi-segment command that fails. - */ - @Test - public void test36() { - add("test word1 word2"); - add("connect word1"); - add("and"); - CommandEditorResult cer = mEditor.commitFinalResult("nonexisting_word"); - assertFalse(cer.isSuccess()); - assertThat(cer.getRewrite().ppCommand(), is("replace (word1 nonexisting_word) (word1-nonexisting_word)")); - assertThatUndoStackIs("[delete 16]"); - assertThatTextIs("Test word1 word2"); - undo(); - assertThatTextIs(""); - } - - /** - * Dictating over a selection - */ - @Test - public void test37() { - add("this is a text"); - add("select is a"); - add("is not a"); - assertThatTextIs("This is not a text"); - } - - @Test - public void test38() { - add("this is a text"); - add("select is a"); - add("selection_replace is not a"); - assertThatTextIs("This is not a text"); - } - - @Test - public void test39() { - add("this is a text"); - add("select is a"); - add("selection_replace is not a"); - assertThatUndoStackIs("[deleteSurroundingText+commitText, setSelection, delete 14]"); - assertThatTextIs("This is not a text"); - undo(); - assertThatTextIs("This is a text"); - } - - @Test - public void test40() { - add("this is a text"); - add("select is a"); - add("selection_replace"); - assertThatUndoStackIs("[delete 17, setSelection, delete 14]"); - assertThatTextIs("This selection_replace text"); - add("is not a"); - assertThatUndoStackIs("[deleteSurroundingText+commitText, setSelection, delete 14]"); - assertThatTextIs("This is not a text"); - undo(); - assertThatUndoStackIs("[setSelection, delete 14]"); - assertThatTextIs("This is a text"); - undo(); - assertThatTextIs("This is a text"); - undo(); - assertThatTextIs(""); - } - - /** - * deleteLeftWord deletes the selection - */ - @Test - public void test41() { - add("1234567890"); - add("select 456"); - runOp(mEditor.deleteLeftWord()); - assertThatTextIs("1237890"); - undo(); - assertThatTextIs("1234567890"); - } - - - @Test - public void test42() { - add("test word1 word2"); - addPartial("connect word1 and word2"); - add("connect word1 and word2"); - assertThatTextIs("Test word1-word2"); - undo(); - assertThatTextIs("Test word1 word2"); - } - - @Test - public void test42a() { - add("test word1 word2"); - addPartial("connect word1 and word2"); - assertThatTextIs("Test word1 word2"); - runOp(mEditor.replace("word1 word2", "word1-word2")); - assertThatTextIs("Test word1-word2"); - } - - /** - * An existing selection should not matter if the command is not about selection - */ - @Test - public void test43() { - add("test word1 word2 word3"); - add("select word3"); - assertThatTextIs("Test word1 word2 word3"); - // Returns false if there is a selection - assertFalse(mEditor.commitPartialResult("connect word1 and word2")); - add("connect word1 and word2"); - assertThatTextIs("Test word1-word2 word3"); - undo(); - assertThatTextIs("Test word1 word2 word3"); - } - - /** - * Partial results should not have an effect on the command. - */ - @Test - public void test44() { - add("test word1", "."); - addPartial("s/word1"); - addPartial("s/word1/word2/"); - add("s/word1/word2/"); - assertThatTextIs("Test word2."); - } - - @Test - public void test45() { - add("sentence", "."); - add("sentence"); - assertThatTextIs("Sentence. Sentence"); - } - - @Test - public void test46() { - add("Sentence", "."); - addPartial("DELETE"); - assertThatTextIs("Sentence. DELETE"); - add("DELETE ME"); - assertThatTextIs("Sentence."); - } - - /** - * Auto-capitalization - */ - @Test - public void test47() { - addPartial("this is 1st test."); - add("this is 1st test. this is 2nd test."); - addPartial("this is 3rd"); - add("this is 3rd test."); - // TODO: 2nd sentence is lowercase, right? - assertThatTextIs("This is 1st test. this is 2nd test. This is 3rd test."); - add("delete this"); - assertThatTextIs("This is 1st test. this is 2nd test. is 3rd test."); - undo(); - // TODO: capitalization is not restored - assertThatTextIs("This is 1st test. this is 2nd test. This is 3rd test."); - } - - /** - * Undoing final texts - */ - @Test - public void test48() { - addPartial("this is 1st test."); - add("this is 1st test. This is 2nd test."); - addPartial("this is 3rd"); - add("this is 3rd test."); - assertThatTextIs("This is 1st test. This is 2nd test. This is 3rd test."); - undo(); - assertThatTextIs("This is 1st test. This is 2nd test."); - undo(); - assertThatTextIs(""); - } - - /** - * Regex based selection. - */ - @Test - public void test49() { - add("This is number 1. This is number 2."); - runOp(mEditor.selectReBefore("number ")); - add("#"); - assertThatTextIs("This is number 1. This is #2."); - } - - /** - * Regex based selection using capturing groups. - */ - @Test - public void test50() { - add("This is number 1. This is number 2."); - runOp(mEditor.selectReBefore("(\\d+)\\.")); - add("II"); - assertThatTextIs("This is number 1. This is number II."); - } - - /** - * Regex based selection using an empty capturing group. - */ - @Test - public void test51() { - add("This is number 1. This is number 2? This is", "prev_sent"); - add("yes,"); - assertThatTextIs("This is number 1. Yes, This is number 2? This is"); - add("undo 2"); - add("3"); - assertThatTextIs("This is number 1. This is number 2? This is 3"); - } - - // test52, test53 - - /** - * Apply a command multiple times. - */ - @Test - public void test54() { - add("6543210", "step back", "apply 4", "-"); - assertThatTextIs("65-43210"); - assertThatUndoStackIs("[delete 1, undo apply 4, setSelection, delete 7]"); - undo(); - assertThatTextIs("6543210"); - undo(); - assertThatUndoStackIs("[setSelection, delete 7]"); - add("-"); - assertThatTextIs("654321-0"); - } - - /** - * Delete a string multiple times. - */ - @Test - public void test55() { - add("6 5 4 3 2 1 0", "delete ", "apply 4", "-"); - assertThatTextIs("6 5-43210"); - add("undo 1"); - assertThatTextIs("6 543210"); - add("undo 1"); - assertThatTextIs("6 5 4 3 2 10"); - } - - /** - * Combine last 3 commands and apply the result 2 times. - */ - @Test - public void test56() { - add("0 a a a a b a", "select a", "selection_uc", "step back"); - assertThatTextIs("0 a a a a b A"); - add("-"); - assertThatTextIs("0 a a a a b -A"); - undo(); - assertThatTextIs("0 a a a a b A"); - assertThatOpStackIs("[moveRel, ucSel, select a]"); - add("combine 3"); - assertThatTextIs("0 a a a a b A"); - assertThatOpStackIs("[[select a, ucSel, moveRel] 3]"); - add("apply 2"); - assertThatTextIs("0 a a A A b A"); - } - - /** - * Search for a string multiple times (i.e. apply "select" multiple times). - * Change the 5th space with a hyphen. - */ - @Test - public void test57() { - add("6 5 4 3 2 1 0", "select ", "apply 4", "-"); - assertThatTextIs("6 5-4 3 2 1 0"); - undo(); - assertThatTextIs("6 5 4 3 2 1 0"); - undo(); - assertThatTextIs("6 5 4 3 2 1 0"); - add("-"); - assertThatTextIs("6 5 4 3 2 1-0"); - } - - @Test - public void test60() { - add("there are word1 and word2..."); - add("select word1 and word2"); - add("selection_uc"); - assertThatTextIs("There are WORD1 AND WORD2..."); - runOp(mEditor.moveAbs(-1)); - add("select word1 and word2"); - add("selection_quote"); - assertThatTextIs("There are \"WORD1 AND WORD2\"..."); - } - - /** - * TODO: incorrectly replaces with "_some_" instead of "_SOME_" - */ - @Test - public void test61() { - add("this is SOME word"); - add("underscore some"); - assertThatTextIs("This is _SOME_ word"); - } - - /** - * Same as before but using selection. - */ - @Test - public void test62() { - add("this is SOME word"); - add("select some"); - add("selection_underscore"); - assertThatTextIs("This is _SOME_ word"); - } - - // test63 - - /** - * Undoing a move restores the selection. - */ - @Test - public void test64() { - add("123 456 789"); - add("select 456"); - add("step back", "step back"); - undo(2); - add("selection_underscore"); - assertThatTextIs("123 _456_ 789"); - } - - /** - * Repeat the last utterance twice. - */ - @Test - public void test65() { - runOpFromText("123"); - runOpFromText("apply 2"); - assertThatTextIs("123 123 123"); - } - - /** - * Combine last 2 commands and apply the result 2 times. - */ - @Test - public void test66() { - add("0 a _ a _ a _", "s/a/b/", "s/_/*/"); - assertThatTextIs("0 a _ a * b _"); - add("combine 2"); - add("apply 2"); - assertThatTextIs("0 b * b * b _"); - } - - /** - * Perform regex search that fails. - */ - @Test - public void test67() { - add("Test 1."); - runOpThatFails(mEditor.selectReBefore("(\\d{2})\\.")); - add("more"); - assertThatTextIs("Test 1. More"); - } - - /** - * Perform undo that fails. - */ - @Test - public void test68() { - addPartial("Initial text"); - assertThatOpStackIs("[]"); - assertThatUndoStackIs("[]"); - runOpThatFails(mEditor.undo(1)); - assertThatOpStackIs("[]"); - assertThatUndoStackIs("[]"); - assertThatTextIs("Initial text"); - } - - /** - * Partial results are deleted, if followed by a command. - */ - @Test - public void test69() { - addPartial("Initial text"); - assertThatTextIs("Initial text"); - assertThatOpStackIs("[]"); - assertThatUndoStackIs("[]"); - add("select whatever"); - assertThatOpStackIs("[]"); - assertThatUndoStackIs("[]"); - assertThatTextIs(""); - } - - /** - * Search after the cursor, select the 1st match, and replace it. - */ - @Test - public void test70() { - add("This is number 1. This is number 2. This is number 3."); - runOp(mEditor.moveAbs(0)); - //CommandEditorManager.EditorCommand ec = CommandEditorManager.EDITOR_COMMANDS.get(CommandEditorManager.SELECT_RE_AFTER); - //runOp(ec.getOp(mEditor, new String[]{"(\\d)\\."})); - runOp(mEditor.selectReAfter("(\\d)\\.", 1)); - // TODO: fails currently - //add("first_number"); - add("I"); - assertThatTextIs("This is number I. This is number 2. This is number 3."); - } - - /** - * Search after the cursor, select the 2nd match, and replace it. - */ - @Test - public void test71() { - add("This is number 1. This is number 2. This is number 3."); - runOp(mEditor.moveAbs(0)); - runOp(mEditor.selectReAfter("(\\d)\\.", 2)); - // TODO: fails currently - //add("second_number"); - add("II"); - assertThatTextIs("This is number 1. This is number II. This is number 3."); - } - - @Test - public void test72() { - add("selectAll", "123 456", "selectAll", "resetSel"); - add("selection_replace !"); - assertThatTextIs("123 456!"); - } - - @Test - public void test73() { - add("123 456", "selectAll"); - add("selection_replace !"); - assertThatTextIs("!"); - undo(1); - add("selection_replace !"); - assertThatTextIs("!"); - } - - @Test - public void test74() { - add("123 456", "step back", "selection_replace _"); - assertThatTextIs("123 45_6"); - add("undo 1"); - assertThatTextIs("123 456"); - add("undo 1", "selection_replace _"); - assertThatTextIs("123 456_"); - } - - @Test - public void test75() { - Collection collection = new ArrayList<>(); - collection.add(mEditor.moveRel(-1)); - collection.add(mEditor.select("2")); - collection.add(mEditor.replaceSel("_")); - add("123 4562"); - runOp(mEditor.combineOps(collection)); - assertThatTextIs("1_3 4562"); - add("undo 1"); - assertThatTextIs("123 4562"); - add("-"); - assertThatTextIs("123 4562-"); - } - - @Test - public void test76() { - Collection collection = new ArrayList<>(); - collection.add(mEditor.moveRel(-1)); - collection.add(mEditor.select("2")); - collection.add(mEditor.replaceSel("_")); - add("123 123 12"); - for (Op op : collection) { - runOp(op); - } - assertThatTextIs("123 1_3 12"); - runOp(mEditor.combine(3)); - add("apply 1"); - assertThatOpStackIs("[apply, [moveRel, select 2, replaceSel] 3]"); - assertThatTextIs("1_3 1_3 12"); - } - - @Test - public void test77() { - Collection collection = new ArrayList<>(); - collection.add(mEditor.moveRel(-1)); - collection.add(mEditor.select(" ")); - collection.add(mEditor.replaceSel("-")); - collection.add(mEditor.select("2")); - collection.add(mEditor.replaceSel("_")); - add("123 4562"); - Op op = mEditor.combineOps(collection); - Op undo = op.run(); - assertThatTextIs("1_3-4562"); - Op undo1 = undo.run(); - assertThatTextIs("123 4562"); - undo1.run(); - assertThatTextIs("1_3-4562"); - } - - @Test - public void test78() { - runOpFromText("123"); - assertThatTextIs("123"); - runOpFromText("undo 1"); - assertThatTextIs(""); - runOpFromText("123"); - runOpFromText("select 2"); - runOpFromText("selection_replace _"); - assertThatTextIs("1_3"); - runOpFromText("undo 1"); - assertThatTextIs("123"); - } - - /** - * Failed command is not added to the undo stack. - */ - @Test - public void test79() { - runOpFromText("123"); - runOpFromText("456"); - assertThatTextIs("123 456"); - assertThatOpStackIs("[add 456, add 123]"); - assertThatUndoStackIs("[delete 4, delete 3]"); - runOpFromText("select 7"); - // Here stacks should remain the same because "select 7" fails. - assertThatOpStackIs("[add 456, add 123]"); - assertThatUndoStackIs("[delete 4, delete 3]"); - runOpFromText("undo 1"); - assertThatTextIs("123"); - } - - /** - * Calling selectReAfter N times vs passing N as the 2nd argument. - * Should be equivalent. - */ - @Test - public void test80() { - add("0 word1 word2 word3 word4 word5 word6"); - runOp(mEditor.moveAbs(1)); - runOp(mEditor.selectReAfter("\\b(.+?)\\b", 1)); - runOp(mEditor.replaceSel("[]")); - assertThatTextIs("0 [] word2 word3 word4 word5 word6"); - runOp(mEditor.selectReAfter("\\b(.+?)\\b", 1)); - runOp(mEditor.selectReAfter("\\b(.+?)\\b", 1)); - runOp(mEditor.replaceSel("[]")); - assertThatTextIs("0 [] word2 [] word4 word5 word6"); - runOp(mEditor.selectReAfter("\\b(.+?)\\b", 2)); - runOp(mEditor.replaceSel("[]")); - // TODO: this currently fails - assertThatTextIs("0 [] word2 [] word4 [] word6"); - } - - @Test - public void test82() { - add("Test 1."); - runOp(mEditor.selectReBefore("^")); - add("more"); - assertThatTextIs("MoreTest 1."); - } - - @Test - public void test83() { - add("Test 1."); - runOp(mEditor.selectReBefore("$")); - add("more"); - assertThatTextIs("Test 1. More"); - } - - /** - * Go to the beginning of the current sentence. - */ - @Test - public void test84() { - add("Sent 1? Sent, 2."); - //runOp(mEditor.selectReBefore("(?:^|[.?!]\\s+)()[^.?!]*[.?!]*")); - runOp(mEditor.selectReBefore("(?:^|[.?!]\\s+)()")); - add("more"); - assertThatTextIs("Sent 1? MoreSent, 2."); - } - - /** - * Go to the beginning of the current sentence. - */ - @Test - public void test85() { - add("Sent 1!"); - runOp(mEditor.selectReBefore("(?:^|[.?!]\\s+)()")); - add("more"); - assertThatTextIs("MoreSent 1!"); - } - - /** - * Go to the end of the current sentence. - */ - @Test - public void test86() { - add("Sent 1? Sent, 2."); - runOp(mEditor.moveAbs(0)); - runOp(mEditor.selectReAfter("(?:$|[.?!]\\s+)()", 1)); - add("more"); - assertThatTextIs("Sent 1? MoreSent, 2."); - } - - /** - * Go to the end of the next sentence. - */ - @Test - public void test87() { - add("Sent 1? Sent, 2."); - runOp(mEditor.moveAbs(0)); - runOp(mEditor.selectReAfter("(?:$|[.?!]\\s+)()", 2)); - add("more"); - assertThatTextIs("Sent 1? Sent, 2. More"); - } - - @Test - public void test88() { - add("123456789"); - runOp(mEditor.moveAbs(0)); - runOp(mEditor.selectReAfter("(.)|(\\d)", 5)); - add("_"); - assertThatTextIs("1234 _6789"); - } - - @Test - public void test89() { - add("1234"); - runOp(mEditor.selectAll()); - runOp(mEditor.replaceSelRe("(2)(3)", "$2$1")); - assertThatTextIs("1324"); - } - - @Test - public void test90() { - add("there are word1 and word2...", "select word1 and word2", "replaceSelRe_noletters"); - assertThatTextIs("There are 1 2..."); - undo(); - assertThatTextIs("There are word1 and word2..."); - } - - /** - * Put underscores around the selection. - */ - @Test - public void test91() { - add("there are word1 and word2...", "select word1 and word2", "replaceSelRe_underscore"); - assertThatTextIs("There are _word1 and word2_..."); - undo(); - assertThatTextIs("There are word1 and word2..."); - } - - /** - * Replace middle words in the match. - * TODO: fails - */ - @Test - public void test92() { - add("there are word1 and word2 word3...", "select are word1 and word2"); - assertThatTextIs("There are word1 and word2 word3..."); - add("replaceSelRe word1 whatever1 whatever2 word2"); - assertThatTextIs("There are word1 whatever1 whatever2 word2 word3..."); - } - - @Test - public void test93() { - add("insert 1"); - assertThatTextIs("1"); - } - - @Test - public void test95() { - add("123 456", "select 123", "resetSel"); - add("selection_replace !"); - assertThatTextIs("123! 456"); - } - - @Test - public void test96() { - add("010010001", "select 1", "select @sel()", "select @sel()"); - add("selection_replace !"); - assertThatTextIs("0!0010001"); - } - - /** - * "times" is replaced by "*" but only in the context of numbers. - * Unwanted implementation: consumes numbers, thus not all "times" are replaced. - */ - @Test - public void test97() { - add("times 1 times 2 times 3 times 4 times"); - assertThatTextIs("Times 1 * 2 times 3 * 4 times"); - } - - /** - * "times" is replaced by "*" but only in the context of numbers. - * Implementation with lookaround. - */ - @Test - public void test98() { - add("times_ 1 times_ 2 times_ 3 times_ 4 times_"); - assertThatTextIs("Times_ 1 * 2 * 3 * 4 times_"); - } - - /** - * Adds some text and then moves to the beginning of doc. - * TODO: not sure if the trailing space should be allowed. - */ - @Test - public void test99() { - add("123 moveAbs0", "456"); - assertThatTextIs("456123 "); - } - - @Test - public void test100() { - add("123", "456 moveAbs0"); - assertThatTextIs("123 456 "); - add("789"); - assertThatTextIs("789123 456 "); - } - - /** - * Because below there is always a selection before deleteChars is applied, - * it ignores its args and just deletes the selection. - */ - @Test - public void test101() { - add("1234567890"); - runOp(mEditor.moveRel(-5)); - runOp(mEditor.moveRelSel(2, 1)); - runOp(mEditor.deleteChars(100)); - assertThatTextIs("12345890"); - runOp(mEditor.moveRelSel(-2, 0)); - runOp(mEditor.deleteChars(1)); - assertThatTextIs("123890"); - // Cursor ends cross - runOp(mEditor.moveRelSel(-2, 1)); - runOp(mEditor.deleteChars(2)); - assertThatTextIs("1890"); - runOp(mEditor.moveRelSel(2, 1)); - runOp(mEditor.moveRelSel(1, 0)); - runOp(mEditor.deleteChars(1)); - assertThatTextIs("180"); - undo(9); - assertThatTextIs("1234567890"); - } - - @Test - public void test102() { - add("123 456 789"); - runOp(mEditor.moveAbs(5)); - runOp(mEditor.selectRe("\\d+", false)); - add("new"); - assertThatTextIs("123 new 789"); - } - - /** - * Deleting 😃 can be done by deleting a single char. - */ - @Test - public void test103() { - add("\uD83D\uDE03"); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - runOp(mEditor.deleteChars(-1)); - } else { - runOp(mEditor.deleteChars(-2)); - } - assertThatTextIs(""); - } - - @Test - public void test104() { - String initial = "123456789"; - add(initial); - runOp(mEditor.moveRel(-5)); - runOp(mEditor.deleteChars(-2)); - runOp(mEditor.deleteChars(3)); - assertThatTextIs("1289"); - runOp(mEditor.undo(2)); - assertThatTextIs(initial); - } - - @Test - public void test105() { - String str = "Word1 word2"; - add(str); - runOp(mEditor.deleteLeftWord()); - assertThatTextIs("Word1"); - undo(); - assertThatTextIs(str); - runOp(mEditor.deleteLeftWord()); - assertThatTextIs("Word1"); - } - - @Test - public void test106() { - add("1 2 3 1 2 3 1 2 3"); - runOp(mEditor.selectReBefore("2")); - add("apply 2"); - add("next_sel"); - add("*"); - assertThatTextIs("1 2 3 1 * 3 1 2 3"); - runOp(mEditor.selectReAfter("2", 1)); - add("prev_sel"); - add("*"); - assertThatTextIs("1 * 3 1 * 3 1 2 3"); - undo(2); - add("*"); - assertThatTextIs("1 2 3 1 * 3 1 * 3"); - runOp(mEditor.selectReBefore("1")); - add("apply 2"); - add("next_sel"); - add("*"); - assertThatTextIs("1 2 3 * * 3 1 * 3"); - undo(3); - add("*"); - assertThatTextIs("1 2 3 1 * 3 * * 3"); - } - - @Test - public void test107() { - add("test prefix1optional"); - assertThatTextIs("Test optional"); - } - - // "(opt)?" is equal to "(opt|)" - // In Android 10, the first returns null if opt is missing. - @Test - public void test108() { - add("test prefix1suffix"); - // assertThatTextIs("Test nullsuffix"); // Android 10 - assertThatTextIs("Test suffix"); // Android 11 - } - - @Test - public void test109() { - add("test prefix2prefix2suffix"); - assertThatTextIs("Test suffix"); - } - - /** - * Replace with selection if nothing has been selected. - * TODO: do we want this, i.e. may it would make sense if replacement resolved @sel() - */ - @Test - public void test110() { - add("replace with selection"); - assertThatTextIs("A @sel() B"); - } - - /** - * Replace with selection if something has been selected. - * TODO: do we want this, i.e. may it would make sense if replacement resolved @sel() - */ - @Test - public void test111() { - add("123456"); - runOp(mEditor.select("123")); - add("replace with selection"); - assertThatTextIs("A @sel() B456"); - } - - // Can't create handler inside thread that has not called Looper.prepare() - //@Test - public void test201() { - runOp(mEditor.copy()); - runOp(mEditor.paste()); - runOp(mEditor.paste()); - } - - //@Test - public void test202() { - add("1234567890"); - runOp(mEditor.keyLeft()); - runOp(mEditor.keyLeft()); - undo(); - runOp(mEditor.deleteLeftWord()); - assertThatTextIs("0"); - } - - /** - * Numeric keycode. - * TODO: Works in the app but not in the test. - */ - //@Test - public void test203() { - add("This is a test", "code 66"); - runOp(mEditor.keyCode(66)); - runOp(mEditor.keyCodeStr("A")); - assertThatTextIs("This is a testA"); - } - - /** - * Symbolic keycode - * TODO: Works in the app but not in the test. - */ - //@Test - public void test204() { - add("This is a test", "code letter B"); - assertThatTextIs("This is a testB"); - } - - /** - * TODO: Can't create handler inside thread that has not called Looper.prepare() - */ - //@Test - public void test205() { - add("test word1"); - runOp(mEditor.replaceSel(" ")); - add("word2"); - assertThatTextIs("Test word1 word2"); - runOp(mEditor.cutAll()); - assertThat(getTextBeforeCursor(1), is("")); - runOp(mEditor.paste()); - assertThatTextIs("Test word1 word2"); - } - - /** - * Single letter is always glued to the previous symbol, but possibly capitalized. - */ - @Test - public void test206() { - add("test."); - add("a"); - assertThatTextIs("Test.A"); - } - - @Test - public void test207() { - add("test!"); - add(" "); - add("abc"); - assertThatTextIs("Test! Abc"); - } - - /** - * Spacing around hyphen. - * TODO: do not capitalize after " - " - */ - @Test - public void test208() { - add("test", "-", "test", "hyphen", "test", "space", "hyphen", "space", "test", "newbullet", "test"); - assertThatTextIs("Test-test-test - Test\n- test"); - } - - /** - * Spacing around common punctuation. - */ - @Test - public void test209() { - add("test", "test", ",", "test", ":", "test", "!", "test", "(", "test", "test", ")", ".", "(", "test", ".", ")"); - assertThatTextIs("Test test, test: test! Test (Test test). (Test.)"); - } - - /** - * Spacing around common punctuation. - * Capitalization depends on how much left context is available. - * If left context contains only transparent characters then the text is capitalized. - */ - @Test - public void test210() { - add("test (", "test", "!", "test", "(", "test"); - assertThatTextIs("Test (test! Test (Test"); - } - - // TODO: make AsyncTask ops testable - // @Test - public void test211() { - add("123 456", "select 123"); - runOp(mEditor.getUrl("http://api.mathjs.org/v4/?expr=2-3", null)); - assertThatTextIs("-1 456"); - } - - /** - * selectReBefore interprets the selection as a plain string (not as a regex - */ - @Test - public void test212() { - add(". 2 ."); - runOp(mEditor.selectReBefore("\\.")); - runOp(mEditor.selectReBefore("@sel()")); - add("1"); - assertThatTextIs("1 2 ."); - } - - /** - * selectReAfter interprets the selection as a plain string (not as a regex - */ - @Test - public void test213() { - add(". 2 ."); - runOp(mEditor.selectReBefore("\\.")); - runOp(mEditor.selectReBefore("\\.")); - runOp(mEditor.selectReAfter("@sel()", 1)); - add("3"); - assertThatTextIs(". 2 3"); - } - - /** - * replaceSel + setting the cursor position (denoted by [] below). - * test[] - * test<[abc]> - * BEFORE: t< - * test<123[]> - */ - @Test - public void test214() { - add("test"); - runOp(mEditor.replaceSel("", "^.(...)")); - assertThat(getTextBeforeCursor(2), is("t<")); - add("123"); - assertThat(getTextBeforeCursor(2), is("23")); - } - - @Test - public void test215() { - add("test"); - runOp(mEditor.replaceSel("<>", "^.()")); - assertThat(getTextBeforeCursor(2), is("t<")); - add("123"); - assertThat(getTextBeforeCursor(2), is("23")); - } - - @Test - public void test216() { - runOp(mEditor.replaceSel("Hi ,", "")); - assertThat(getTextBeforeCursor(3), is("Hi ")); - add("World"); - assertThatTextIs("Hi World,"); - } - - @Test - public void test217() { - runOp(mEditor.replaceSel("Hi ,", "")); - assertThat(getTextBeforeCursor(3), is("E>,")); - add("World"); - assertThatTextIs("Hi , World"); - } - - /** - * TODO: fix undo of replaceSel/2 - */ - @Test - public void test218() { - runOp(mEditor.replaceSel("Hi ,", "")); - undo(); - add("World"); - assertThatTextIs("World"); - } - - @Test - public void test219() { - add("0123"); - runOp(mEditor.select("123")); - runOp(mEditor.replaceSel("<@sel()>", ".(.*).")); - assertThatTextIs("0<123>"); - add("456"); - assertThatTextIs("0<456>"); - } - - @Test - public void test220() { - add("1", "select 1", "selection_bracket 234"); - assertThatTextIs("(1-234)"); - assertThat(getTextBeforeCursor(1), is("-")); - assertThat(getTextAfterCursor(1), is(")")); - } - - private String getTextBeforeCursor(int n) { - return mEditor.getInputConnection().getTextBeforeCursor(n, 0).toString(); - } - - private String getTextAfterCursor(int n) { - return mEditor.getInputConnection().getTextAfterCursor(n, 0).toString(); - } - - private void addPartial(String... texts) { - for (String text : texts) { - assertTrue(mEditor.commitPartialResult(text)); - } - } - - private void add(String... texts) { - for (String text : texts) { - assertNotNull(mEditor.commitFinalResult(text)); - } - } - - private void undo() { - undo(1); - } - - private void undo(int steps) { - runOp(mEditor.undo(steps)); - } - - private void assertThatOpStackIs(String str) { - assertThat(mEditor.getOpStack().toString(), is(str)); - } - - private void assertThatUndoStackIs(String str) { - assertThat(mEditor.getUndoStack().toString(), is(str)); - } - - private void assertThatTextIs(String str) { - assertThat(mEditor.getText().toString(), is(str)); - } - - private void runOp(Op op) { - assertNotNull(op); - assertTrue(mEditor.runOp(op)); - // TODO: we could check for each op if undo works as expected - // do + undo - //assertNotNull(op.run().run()); - // do - //assertNotNull(op.run()); - } - - private void runOpThatFails(Op op) { - assertNotNull(op); - assertFalse(mEditor.runOp(op)); - } - - private void runOpFromText(String text) { - Op op = mEditor.getOpOrNull(text, true); - assertNotNull(op); - // Op can fail - mEditor.runOp(op); - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/ee/ioc/phon/android/speechutils/editor/RuleManagerTest.java b/app/src/androidTest/java/ee/ioc/phon/android/speechutils/editor/RuleManagerTest.java deleted file mode 100644 index 15dbb85d91f5c7d1f95af9a3c185c117f54dcfbb..0000000000000000000000000000000000000000 --- a/app/src/androidTest/java/ee/ioc/phon/android/speechutils/editor/RuleManagerTest.java +++ /dev/null @@ -1,79 +0,0 @@ -package ee.ioc.phon.android.speechutils.editor; - -import androidx.test.ext.junit.runners.AndroidJUnit4; - -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.util.regex.Pattern; - -import static org.junit.Assert.assertEquals; - -@RunWith(AndroidJUnit4.class) -public class RuleManagerTest { - - private static final Pattern UTTERANCE = Pattern.compile("", Constants.REWRITE_PATTERN_FLAGS); - private static final String COMMENT = ""; - - @Test - public void test01() { - assertEquals("Command\tComment\tLocale\tService\tApp\tUtterance\tLabel\tArg1\nreplaceSel\t\t\t\t\t\t\t", getTsv("")); - } - - @Test - public void test02() { - assertEquals("Command\tComment\tLocale\tService\tApp\tUtterance\tLabel\tArg1\nreplaceSel\t\t\t\t\t\t$\t\\$", getTsv("$")); - } - - @Test - public void test03() { - assertEquals("Command\tComment\tLocale\tService\tApp\tUtterance\tLabel\tArg1\nreplaceSel\t\t\t\t\t\t\\\t\\\\", getTsv("\\")); - } - - @Test - public void test04() { - assertEquals("Command\tComment\tLocale\tService\tApp\tUtterance\tLabel\tArg1\nreplaceSel\t\t\t\t\t\t@sel\t@sel", getTsv("@sel")); - } - - @Test - public void test05() { - assertEquals("Command\tComment\tLocale\tService\tApp\tUtterance\tLabel\tArg1\nreplaceSel\t\t\t\t\t\t \t ", getTsv(" ")); - } - - @Test - public void testMakeCommand01() { - assertEquals("\t\t\t\t\t\t\t\t\t", getCommandTsv("")); - } - - @Test - public void testMakeCommand02() { - assertEquals("\t\t\t\t\t\ttest\ttest\t\t", getCommandTsv("test")); - } - - @Test - public void testMakeCommand03() { - assertEquals("\t\t\t\t\t\t\\$\t$\t\t", getCommandTsv("$")); - } - - @Test - public void testMakeCommand04() { - // Command Comment Locale Service App Utterance Replacement Label Arg1 Arg2 - assertEquals("REPL\t\t\t\t\t\t\tREPL ($) (\\)\t\\$\t\\\\", getCommand2Tsv("REPL")); - } - - private String getTsv(String text) { - return new RuleManager().addRecent(text, UTTERANCE, COMMENT, "").toTsv(); - } - - private String getCommandTsv(String text) { - UtteranceRewriter.Rewrite rewrite = new UtteranceRewriter.Rewrite(text); - Command command = new RuleManager().makeCommand(rewrite, UTTERANCE, COMMENT); - return command.toTsv(UtteranceRewriter.DEFAULT_HEADER_COMMAND); - } - - private String getCommand2Tsv(String id) { - UtteranceRewriter.Rewrite rewrite = new UtteranceRewriter.Rewrite(id, "", new String[]{"$", "\\"}, new Command("", "")); - Command command = new RuleManager().makeCommand(rewrite, UTTERANCE, COMMENT); - return command.toTsv(UtteranceRewriter.DEFAULT_HEADER_COMMAND); - } -} \ No newline at end of file diff --git a/app/src/androidTest/java/ee/ioc/phon/android/speechutils/editor/UtteranceRewriterTest.java b/app/src/androidTest/java/ee/ioc/phon/android/speechutils/editor/UtteranceRewriterTest.java deleted file mode 100644 index e035927d17063362ab405435ac577502a31996e5..0000000000000000000000000000000000000000 --- a/app/src/androidTest/java/ee/ioc/phon/android/speechutils/editor/UtteranceRewriterTest.java +++ /dev/null @@ -1,137 +0,0 @@ -package ee.ioc.phon.android.speechutils.editor; - -import androidx.test.ext.junit.runners.AndroidJUnit4; -import android.util.Pair; - -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.List; -import java.util.SortedMap; -import java.util.TreeMap; - -import static org.hamcrest.Matchers.is; -import static org.junit.Assert.assertThat; - -@RunWith(AndroidJUnit4.class) -public class UtteranceRewriterTest { - - private static final List COMMANDS; - - private static final SortedMap HEADER; - - static { - SortedMap aMap = new TreeMap<>(); - aMap.put(0, UtteranceRewriter.HEADER_UTTERANCE); - aMap.put(1, UtteranceRewriter.HEADER_REPLACEMENT); - aMap.put(2, UtteranceRewriter.HEADER_COMMAND); - aMap.put(3, UtteranceRewriter.HEADER_ARG1); - aMap.put(4, UtteranceRewriter.HEADER_ARG2); - HEADER = Collections.unmodifiableSortedMap(aMap); - } - - static { - List list = new ArrayList<>(); - list.add(new Command("s/(.*)/(.*)/", "X", "replace", new String[]{"$1", "$2"})); - // Map <1><2><3> into 1, 2, 3 - // TODO: is there a better way - list.add(new Command("<([^>]+)>", "$1, ")); - list.add(new Command(", K6_STOP, ", "")); - COMMANDS = Collections.unmodifiableList(list); - } - - private UtteranceRewriter mUr; - - @Before - public void before() { - mUr = new UtteranceRewriter(COMMANDS, HEADER); - } - - @Test - public void test01() { - Command command = new Command("s/(.*)/(.*)/", "X", "replace", new String[]{"$1", "$2"}); - assertThat(command.toTsv(HEADER), is("s/(.*)/(.*)/\tX\treplace\t$1\t$2")); - Pair pair = command.parse("s/_/a/"); - assertThat(pair.first, is("X")); - assertThat(pair.second[0], is("_")); - assertThat(pair.second[1], is("a")); - } - - @Test - public void test02() { - rewrite("s/_/a/", "replace (_) (a)", "X"); - } - - @Test - public void test03() { - rewrite("<1><2><3>", null, "1, 2, 3"); - } - - @Test - public void test04() { - UtteranceRewriter ur = new UtteranceRewriter(""); - assertThat(ur.toTsv(), is("Utterance")); - } - - @Test - public void test05() { - UtteranceRewriter ur = new UtteranceRewriter("utt"); - assertThat(ur.toTsv(), is("Utterance\nutt")); - } - - @Test - public void test06() { - UtteranceRewriter ur = new UtteranceRewriter("utt1\nutt2"); - assertThat(ur.toTsv(), is("Utterance\nutt1\nutt2")); - } - - @Test - public void test07() { - UtteranceRewriter ur = new UtteranceRewriter("Utterance\nutt1\nutt2"); - assertThat(ur.toTsv(), is("Utterance\nutt1\nutt2")); - } - - @Test - public void test08() { - UtteranceRewriter ur = new UtteranceRewriter("utt1\trepl1\tignored1\nutt2\trepl2\tignored2\n"); - assertThat(ur.toTsv(), is("Utterance\tReplacement\nutt1\trepl1\nutt2\trepl2")); - } - - @Test - public void test09() { - UtteranceRewriter ur = new UtteranceRewriter("Utterance\tReplacement\nutt1\trepl1\tignored1\nutt2\trepl2\tignored2\n"); - assertThat(ur.toTsv(), is("Utterance\tReplacement\nutt1\trepl1\nutt2\trepl2")); - } - - @Test - public void test10() { - UtteranceRewriter ur = new UtteranceRewriter("Ignored\tUtterance\n\n#\t#\nignored\tutt1\n\t\t\nignored2\tutt2"); - assertThat(ur.toTsv(), is("Utterance\nutt1\nutt2")); - } - - @Test - public void test11() { - UtteranceRewriter ur = new UtteranceRewriter("" + - "Utterance\tReplacement\tCommand\tArg1\tIgnored\n" + - "utt\trepl\t\t\tignored\n"); - assertThat(ur.toTsv(), is("Utterance\tReplacement\tCommand\tArg1\nutt\trepl\t\t")); - assertThat(ur.getRewrite("p utt s").mStr, is("p repl s")); - } - - @Test - public void test12() { - String tsv = "Utterance\tReplacement\r\nf1\tf2\rg1\tg2\n\n"; - UtteranceRewriter ur = new UtteranceRewriter(tsv); - assertThat(ur.toTsv(), is("Utterance\tReplacement\nf1\tf2\ng1\tg2")); - } - - private void rewrite(String str1, String str2, String str3) { - UtteranceRewriter.Rewrite rewrite = mUr.getRewrite(str1); - assertThat(rewrite.ppCommand(), is(str2)); - assertThat(rewrite.mStr, is(str3)); - } - -} \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml deleted file mode 100644 index 530bc922d8136e15a3cd1b6eb4302f377eea87b2..0000000000000000000000000000000000000000 --- a/app/src/main/AndroidManifest.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/AbstractAudioRecorder.java b/app/src/main/java/ee/ioc/phon/android/speechutils/AbstractAudioRecorder.java deleted file mode 100644 index cf4b43144139c305f5c38fc2dae52f4d39448ae7..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/AbstractAudioRecorder.java +++ /dev/null @@ -1,466 +0,0 @@ -/* - * Copyright 2011-2016, Institute of Cybernetics at Tallinn University of Technology - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ee.ioc.phon.android.speechutils; - -import android.media.AudioFormat; - -import java.util.concurrent.atomic.AtomicLong; - -import ee.ioc.phon.android.speechutils.utils.AudioUtils; - -public abstract class AbstractAudioRecorder implements AudioRecorder { - - private static final int RESOLUTION = AudioFormat.ENCODING_PCM_16BIT; - private static final int BUFFER_SIZE_MULTIPLIER = 4; // was: 2 - private static final int DEFAULT_BUFFER_LENGTH_IN_MILLIS = 35000; - - private SpeechRecord mRecorder = null; - - private double mAvgEnergy = 0; - - private final int mSampleRate; - private final int mSamplesInOneSec; - private final int mSamplesInOneMilliSec; - private final boolean mAlwaysListen; - - // Recorder state - private State mState; - - // The complete space into which the recording in written. - // Its maximum length is about: - // 2 (bytes) * 1 (channels) * 30 (max rec time in seconds) * 44100 (times per second) = 2 646 000 bytes - // but typically is: - // 2 (bytes) * 1 (channels) * 20 (max rec time in seconds) * 16000 (times per second) = 640 000 bytes - final byte[] mRecording; - - // TODO: use: mRecording.length instead - private int mRecordedLength = 0; - private AtomicLong mRecordedSessionId = new AtomicLong(0L); - boolean mRecordingBufferIsFullWithData = false; - private final int mRecordingBufferLengthMillis; - - // The number of bytes the client has already consumed - private int mConsumedLength = 0; - private AtomicLong mConsumedSessionId = new AtomicLong(0L); - - // Buffer for output - private byte[] mBuffer; - - protected AbstractAudioRecorder(int audioSource, int sampleRate, int recordingBufferLengthMillis, boolean alwaysListen) { - mSampleRate = sampleRate; - // E.g. 1 second of 16kHz 16-bit mono audio takes 32000 bytes. - mSamplesInOneSec = RESOLUTION_IN_BYTES * CHANNELS * mSampleRate; - mSamplesInOneMilliSec = (int) ((double) mSamplesInOneSec / 1000.0); - mRecordingBufferLengthMillis = recordingBufferLengthMillis; - mRecording = new byte[mSamplesInOneMilliSec * mRecordingBufferLengthMillis]; - mAlwaysListen = alwaysListen; - } - - protected AbstractAudioRecorder(int audioSource, int sampleRate) { - this(audioSource, sampleRate, DEFAULT_BUFFER_LENGTH_IN_MILLIS, false); - } - - protected SpeechRecord createRecorder(int audioSource, int sampleRate, int bufferSize) { - if (mRecorder != null) - release(); - - mRecorder = new SpeechRecord(audioSource, sampleRate, AudioFormat.CHANNEL_IN_MONO, RESOLUTION, bufferSize, false, false, false); - if (getSpeechRecordState() != SpeechRecord.STATE_INITIALIZED) { - throw new IllegalStateException("SpeechRecord initialization failed"); - } - - return mRecorder; - } - - // TODO: remove - protected void createBuffer(int framePeriod) { - mBuffer = new byte[framePeriod * RESOLUTION_IN_BYTES * CHANNELS]; - } - - protected int getBufferSize() { - int minBufferSizeInBytes = SpeechRecord.getMinBufferSize(mSampleRate, AudioFormat.CHANNEL_IN_MONO, RESOLUTION); - if (minBufferSizeInBytes == SpeechRecord.ERROR_BAD_VALUE) { - throw new IllegalArgumentException("SpeechRecord.getMinBufferSize: parameters not supported by hardware"); - } else if (minBufferSizeInBytes == SpeechRecord.ERROR) { - Log.e("SpeechRecord.getMinBufferSize: unable to query hardware for output properties"); - minBufferSizeInBytes = mSampleRate * (120 / 1000) * RESOLUTION_IN_BYTES * CHANNELS; - } - int bufferSize = BUFFER_SIZE_MULTIPLIER * minBufferSizeInBytes; - Log.i("SpeechRecord buffer size: " + bufferSize + ", min size = " + minBufferSizeInBytes); - return bufferSize; - } - - /** - * Returns the recorded bytes since the last call, and resets the recording. - * - * @return bytes that have been recorded since this method was last called - */ - public synchronized byte[] consumeRecordingAndTruncate() { - int len = getConsumedLength(); - byte[] bytes = getCurrentRecording(len); - setRecordedLength(0); - setConsumedLength(0); - return bytes; - } - - public int getSampleRate() { - return mSampleRate; - } - - protected int getNumOfSamplesIn(int millis) { - return Math.abs(millis) * mSamplesInOneMilliSec; - } - - protected boolean isRecordedSessionSameAsConsumedSession() { - return mRecordedSessionId.get() == mConsumedSessionId.get(); - } - - /** - * Checking of the read status. - * The total recording array has been pre-allocated (e.g. for 35 seconds of audio). - * If it gets full (status == -5) then the recording is stopped. - */ - protected int getStatus(int numOfBytes, int len) { - Log.i("Read bytes: request/actual: " + len + "/" + numOfBytes); - if (numOfBytes < 0) { - Log.e("AudioRecord error: " + numOfBytes); - return numOfBytes; - } - if (numOfBytes > len) { - Log.e("Read more bytes than is buffer length: " + numOfBytes + ": " + len); - return -100; - } else if (numOfBytes == 0) { - Log.e("Read zero bytes"); - return -200; - } else if (mRecording.length < mRecordedLength + numOfBytes) { - Log.e("Recorder buffer overflow: " + mRecordedLength); - return -300; - } - return 0; - } - - /** - * Check if the consume pointer was crossed by the recorded pointer. As long as the consume - * pointer was not crossed, the consumption of the buffer may continue as usual and no sound gap - * will occur. Once the consume pointer was crossed (e.g. it was on sample 1000 and prior to this - * read the recorder was on sample 750 and now that it read the new sample it's on 1500), there's - * an audio gap between the consumer and the recorder that can not be filled (data is lost with - * no ability to get it back). Whenever this kind of cross occurs, the calling code changed the - * session id of the recorder so that if consume is called (from ContinuousRawAudioRecorder), - * it will not assume that the data is complete and could be fetched but it will act according to - * the SessionStartPointer configured (e.g. read the buffer from the beginning, from now, or from - * now - X millis) - * - * @param reachedTheEndOfRecordingBuffer - in case that in the read before the call to this method the recorder - * passed the end of the buffer and returned to the beginning - * @param numOfBytesRead - in the reading process - * @return true/false according to the above logic - */ - private boolean isConsumePointerCrossed(boolean reachedTheEndOfRecordingBuffer, int numOfBytesRead) { - return numOfBytesRead > 0 && - mRecordingBufferIsFullWithData && - isRecordedSessionSameAsConsumedSession() && - ((mRecordedLength - numOfBytesRead < mConsumedLength && mRecordedLength >= mConsumedLength) || - (reachedTheEndOfRecordingBuffer && mConsumedLength < mRecordedLength)); - } - - public long markNewRecordingSession() { - return mRecordedSessionId.incrementAndGet(); - } - - /** - * Copy data from the given recorder into the given buffer, and append to the complete recording. - * public int read (byte[] audioData, int offsetInBytes, int sizeInBytes) - */ - protected int read(SpeechRecord recorder, byte[] buffer) { - int len = buffer.length; - int numOfBytes = recorder.read(buffer, 0, len); - // handling mediaserver crashes here - // it doesn't happen a lot but it happens and the way to handle it is to fully restart - // the audio recorder - if (numOfBytes == 0 && mAlwaysListen) { - consumeRecordingAndTruncate(); - mBuffer = new byte[mBuffer.length]; - createRecorder(recorder.getAudioSource(), recorder.getSampleRate(), getBufferSize()); - start(); - } - - int status = getStatus(numOfBytes, len); - boolean reachedTheEndOfRecordingBuffer = false; - // if we need to keep on listening, when reaching the end of the recorded buffer, - // continue to write from the beginning. thus, we have a cyclic buffer - if (mAlwaysListen && status == -300) { - reachedTheEndOfRecordingBuffer = true; - status = 0; - // for use when consuming the recorded buffer, the buffer is now in it's cyclic phase - if (!mRecordingBufferIsFullWithData) - mRecordingBufferIsFullWithData = true; - } - - if (status == 0 && numOfBytes >= 0) { - if (!reachedTheEndOfRecordingBuffer) { - // arraycopy(Object src, int srcPos, Object dest, int destPos, int length) - // numOfBytes <= len, typically == len, but at the end of the recording can be < len. - System.arraycopy(buffer, 0, mRecording, mRecordedLength, numOfBytes); - mRecordedLength += numOfBytes; - } else { - int numOfBytesBeforeCyclic = mRecording.length - mRecordedLength; - System.arraycopy(buffer, 0, mRecording, mRecordedLength, numOfBytesBeforeCyclic); - System.arraycopy(buffer, numOfBytesBeforeCyclic, mRecording, 0, numOfBytes - numOfBytesBeforeCyclic); - - mRecordedLength = numOfBytes - numOfBytesBeforeCyclic; - } - - // increment the recorded session id in case that the consume pointer was crossed - if (isConsumePointerCrossed(reachedTheEndOfRecordingBuffer, numOfBytes)) { - Log.i("recorder session changed. mRecordedLength was: " + (mRecordedLength - numOfBytes) + " and now it is: " + mRecordedLength + " while the mConsumedLength is: " + mConsumedLength); - markNewRecordingSession(); - } - } - - return mAlwaysListen ? 0 : status; - } - - - /** - * @return recorder state - */ - public State getState() { - return mState; - } - - protected void setState(State state) { - mState = state; - } - - - /** - * @return bytes that have been recorded since the beginning - */ - public byte[] getCompleteRecording() { - return getCurrentRecording(0); - } - - - /** - * @return bytes that have been recorded since the beginning, with wav-header - */ - public byte[] getCompleteRecordingAsWav() { - return getRecordingAsWav(getCompleteRecording(), mSampleRate); - } - - - public static byte[] getRecordingAsWav(byte[] pcm, int sampleRate) { - return AudioUtils.getRecordingAsWav(pcm, sampleRate, RESOLUTION_IN_BYTES, CHANNELS); - } - - /** - * @return bytes that have been recorded since this method was last called - */ - public synchronized byte[] consumeRecording() { - byte[] bytes = getCurrentRecording(mConsumedLength); - if (bytes == null) - return null; - - // this is to avoid race (set the consumed length to be the recorded length though - // the last recording was empty while recorded length moved on - thus we always miss - // a part of the recording) - mConsumedLength = mRecordedLength; - mConsumedSessionId.set(mRecordedSessionId.get()); - return bytes; - } - - protected byte[] getCurrentRecording(int startPos) { - int len = getLength() - startPos; - byte[] bytes = new byte[len]; - System.arraycopy(mRecording, startPos, bytes, 0, len); - Log.i("Copied (raw) from pos: " + startPos + ", bytes: " + bytes.length); - return bytes; - } - - protected int getConsumedLength() { - return mConsumedLength; - } - - protected void setConsumedLength(int len) { - mConsumedLength = len; - } - - protected void setRecordedLength(int len) { - mRecordedLength = len; - } - - public int getLength() { - return mRecordedLength; - } - - /** - * @return true iff a speech-ending pause has occurred at the end of the recorded data - */ - public boolean isPausing() { - double pauseScore = getPauseScore(); - Log.i("Pause score: " + pauseScore); - return pauseScore > 7; - } - - /** - * @return volume indicator that shows the average volume of the last read buffer - */ - public float getRmsdb() { - long sumOfSquares = getRms(mRecordedLength, mBuffer.length); - double rootMeanSquare = Math.sqrt(sumOfSquares / (mBuffer.length / 2)); - if (rootMeanSquare > 1) { - // TODO: why 10? - return (float) (10 * Math.log10(rootMeanSquare)); - } - return 0; - } - - /** - *

In order to calculate if the user has stopped speaking we take the - * data from the last second of the recording, map it to a number - * and compare this number to the numbers obtained previously. We - * return a confidence score (0-INF) of a longer pause having occurred in the - * speech input.

- *

- *

TODO: base the implementation on some well-known technique.

- * - * @return positive value which the caller can use to determine if there is a pause - */ - private double getPauseScore() { - long t2 = getRms(mRecordedLength, mSamplesInOneSec); - if (t2 == 0) { - return 0; - } - double t = mAvgEnergy / t2; - mAvgEnergy = (2 * mAvgEnergy + t2) / 3; - return t; - } - - /** - *

Stops the recording (if needed) and releases the resources. - * The object can no longer be used and the reference should be - * set to null after a call to release().

- */ - public synchronized void release() { - if (mRecorder != null) { - if (mRecorder.getRecordingState() == SpeechRecord.RECORDSTATE_RECORDING) { - stop(); - } - mRecorder.release(); - mRecorder = null; - } - } - - /** - *

Starts the recording, and sets the state to RECORDING.

- */ - public void start() { - if (getSpeechRecordState() == SpeechRecord.STATE_INITIALIZED) { - mRecorder.startRecording(); - if (mRecorder.getRecordingState() == SpeechRecord.RECORDSTATE_RECORDING) { - setState(State.RECORDING); - new Thread() { - public void run() { - recorderLoop(mRecorder); - } - }.start(); - } else { - handleError("startRecording() failed"); - } - } else { - handleError("start() called on illegal state"); - } - } - - - /** - *

Stops the recording, and sets the state to STOPPED. - * If stopping fails then sets the state to ERROR.

- */ - public void stop() { - // We check the underlying SpeechRecord state trying to avoid IllegalStateException. - // If it still occurs then we catch it. - if (getSpeechRecordState() == SpeechRecord.STATE_INITIALIZED && - mRecorder.getRecordingState() == SpeechRecord.RECORDSTATE_RECORDING) { - try { - mRecorder.stop(); - setState(State.STOPPED); - } catch (IllegalStateException e) { - handleError("native stop() called in illegal state: " + e.getMessage()); - } - } else { - handleError("stop() called in illegal state"); - } - } - - protected void recorderLoop(SpeechRecord recorder) { - while (recorder.getRecordingState() == SpeechRecord.RECORDSTATE_RECORDING) { - int status = read(recorder, mBuffer); - if (status < 0) { - handleError("status = " + status); - break; - } - } - } - - - private long getRms(int end, int span) { - int begin = end - span; - if (begin < 0) { - begin = 0; - } - // make sure begin is even - if (0 != (begin % 2)) { - begin++; - } - - long sum = 0; - for (int i = begin; i < end; i += 2) { - short curSample = getShort(mRecording[i], mRecording[i + 1]); - sum += curSample * curSample; - } - return sum; - } - - - /* - * Converts two bytes to a short (assuming little endian). - * TODO: We don't need the whole short, just take the 2nd byte (the more significant one) - * TODO: Most Android devices are little endian? - */ - private static short getShort(byte argB1, byte argB2) { - //if (ByteOrder.nativeOrder().equals(ByteOrder.BIG_ENDIAN)) { - // return (short) ((argB1 << 8) | argB2); - //} - return (short) (argB1 | (argB2 << 8)); - } - - - protected void handleError(String msg) { - release(); - setState(State.ERROR); - Log.e(msg); - } - - private int getSpeechRecordState() { - if (mRecorder == null) { - return SpeechRecord.STATE_UNINITIALIZED; - } - return mRecorder.getState(); - } -} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/AudioCue.java b/app/src/main/java/ee/ioc/phon/android/speechutils/AudioCue.java deleted file mode 100644 index 573f9cf18fb49a7ea6b50d2055880c81f6cc651a..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/AudioCue.java +++ /dev/null @@ -1,61 +0,0 @@ -package ee.ioc.phon.android.speechutils; - -import android.content.Context; -import android.media.AudioManager; -import android.media.MediaPlayer; -import android.os.SystemClock; - -// TODO: add a method that calls back when audio is finished -public class AudioCue { - - private static final int DELAY_AFTER_START_BEEP = 200; - - private final Context mContext; - private final int mStartSound; - private final int mStopSound; - private final int mErrorSound; - - public AudioCue(Context context) { - mContext = context; - mStartSound = R.raw.explore_begin; - mStopSound = R.raw.explore_end; - mErrorSound = R.raw.error; - } - - public AudioCue(Context context, int startSound, int stopSound, int errorSound) { - mContext = context; - mStartSound = startSound; - mStopSound = stopSound; - mErrorSound = errorSound; - } - - public void playStartSoundAndSleep() { - if (playSound(mStartSound)) { - SystemClock.sleep(DELAY_AFTER_START_BEEP); - } - } - - - public void playStopSound() { - playSound(mStopSound); - } - - - public void playErrorSound() { - playSound(mErrorSound); - } - - - private boolean playSound(int sound) { - MediaPlayer mp = MediaPlayer.create(mContext, sound); - // create can return null, e.g. on Android Wear - if (mp == null) { - return false; - } - mp.setAudioStreamType(AudioManager.STREAM_MUSIC); - mp.setOnCompletionListener(MediaPlayer::release); - mp.start(); - return true; - } - -} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/AudioPauser.java b/app/src/main/java/ee/ioc/phon/android/speechutils/AudioPauser.java deleted file mode 100644 index bfaf44385a0afefabe884b8ba12d526a6a75e410..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/AudioPauser.java +++ /dev/null @@ -1,93 +0,0 @@ -package ee.ioc.phon.android.speechutils; - -import android.app.NotificationManager; -import android.content.Context; -import android.media.AudioManager; -import android.media.AudioManager.OnAudioFocusChangeListener; -import android.os.Build; - -/** - * Pauses the audio stream by requesting the audio focus and - * muting the music stream. - *

- * TODO: Test this with two interleaving instances of AudioPauser, e.g. - * TTS starts playing and calls the AudioPauser, at the same time - * the recognizer starts listening and also calls the AudioPauser. - */ -public class AudioPauser { - - private final boolean mIsMuteStream; - private final AudioManager mAudioManager; - private final OnAudioFocusChangeListener mAfChangeListener; - private int mCurrentVolume = 0; - private boolean isPausing = false; - - private AudioPauser(Context context, boolean isMuteStream) { - mAudioManager = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE); - mIsMuteStream = isMuteStream; - mAfChangeListener = focusChange -> Log.i("onAudioFocusChange" + focusChange); - } - - /** - * Creates and returns an AudioPauser. - * - * @param context Context - * @param isMuteStream if true then we additionally try to mute the audio stream. - * This does not succeed if the app is not allowed to - * "modify notification do not disturb policy" on Android N and higher. - * @return AudioPauser - */ - public static AudioPauser createAudioPauser(Context context, boolean isMuteStream) { - if (isMuteStream && Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - NotificationManager nm = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - if (nm != null && !nm.isNotificationPolicyAccessGranted()) { - isMuteStream = false; - } - } - return new AudioPauser(context, isMuteStream); - } - - public boolean isMuteStream() { - return mIsMuteStream; - } - - /** - * Requests audio focus with the goal of pausing any existing audio player. - * Additionally mutes the music stream, since some audio players might - * ignore the focus request. - * In other words, during the pause no sound will be heard, - * but whether the audio resumes from the same position after the pause - * depends on the audio player. - */ - public void pause() { - if (!isPausing) { - int result = mAudioManager.requestAudioFocus(mAfChangeListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN_TRANSIENT); - if (result == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) { - Log.i("AUDIOFOCUS_REQUEST_GRANTED"); - } - - if (mIsMuteStream) { - mCurrentVolume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC); - if (mCurrentVolume > 0) { - mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, 0, 0); - } - } - isPausing = true; - } - } - - - /** - * Abandons audio focus and restores the audio volume. - */ - public void resume() { - if (isPausing) { - mAudioManager.abandonAudioFocus(mAfChangeListener); - if (mIsMuteStream && mCurrentVolume > 0) { - mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC, mCurrentVolume, 0); - } - isPausing = false; - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/AudioRecorder.java b/app/src/main/java/ee/ioc/phon/android/speechutils/AudioRecorder.java deleted file mode 100644 index 6f8acef4ea336c8785bb4f7fc67b13b95ceab8e5..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/AudioRecorder.java +++ /dev/null @@ -1,41 +0,0 @@ -package ee.ioc.phon.android.speechutils; - -import android.media.MediaRecorder; - -public interface AudioRecorder { - int DEFAULT_AUDIO_SOURCE = MediaRecorder.AudioSource.VOICE_RECOGNITION; - int DEFAULT_SAMPLE_RATE = 16000; - short RESOLUTION_IN_BYTES = 2; - // Number of channels (MONO = 1, STEREO = 2) - short CHANNELS = 1; - - String getContentType(); - - State getState(); - - byte[] consumeRecordingAndTruncate(); - - byte[] consumeRecording(); - - void start(); - - float getRmsdb(); - - void release(); - - boolean isPausing(); - - enum State { - // recorder is ready, but not yet recording - READY, - - // recorder recording - RECORDING, - - // error occurred, reconstruction needed - ERROR, - - // recorder stopped - STOPPED - } -} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/ContinuousRawAudioRecorder.java b/app/src/main/java/ee/ioc/phon/android/speechutils/ContinuousRawAudioRecorder.java deleted file mode 100644 index 8dc32d78e11f53fe98b5b9abf2b554b3e96adc6b..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/ContinuousRawAudioRecorder.java +++ /dev/null @@ -1,290 +0,0 @@ -package ee.ioc.phon.android.speechutils; - -import java.util.concurrent.atomic.AtomicBoolean; - -import ee.ioc.phon.android.speechutils.utils.AudioUtils; - -/** - * This class should be used for continuous recording of audio. - * There are cases in which there's a need to constantly have the last X seconds - * of audio buffer ready for processing. Such is the case of activation after hotword - * detection. The hotword may be detected in an external device (e.g. DSP) which triggers - * Android in some way and then audio buffer processing task should start. - * There's a high probability that there's a mismatch between the trigger and the Android - * recorder. Usually the trigger will be handled a few milliseconds (~100) AFTER the real - * end-of-hotword and the Android recorder may have its own delay. So there's a need for - * a mechanism that will allow the developer to compensate between the various delays and - * enable audio processing of an audio buffer from a specific point in time (past, current - * of future). - *

- * For the purpose of efficiency and reduction of garbage collection, the recorded buffer - * is a cyclic one and the code handles the edge cases (gotten audio buffer is split - * between the end of the recording buffer and the beginning). - *

- * The class also handles the cyclic buffer consumption. While consuming the recorded - * buffer, the consumer is always behind or exactly on the producer pointer (chasing the - * recording). - * The cyclic structure can potentially create problematic situations wrt consumption. - * For example: what happens if for some reason, the consumer doesn't consume fast enough - * while the producer is advancing fast? At some point, the producer will pass the consumer - * pointer and now, the gap between the left behind consumer and the fast running producer - * is full with future data (data from consumer + X). This means that the buffer doesn't hold - * anymore the data that connects the point in time of the consumer and the point in time - * of the producer and there is no way that this can be solved. What can be done, and this - * is handled by the code, is to have the ability to know about this problem. - * We've introduced a concept of "sessions". - * The consumer and the producer are on the same session if the time gap between them - * (internally expressed in number of samples) is no more than X (size of the buffer in - * time units). Once this time gap increases above X, the do not share the same session anymore - * and the user can choose what to do with this state (start over, start from a number of millis - * back etc...) - */ -public class ContinuousRawAudioRecorder extends AbstractAudioRecorder { - - private static final int DEFAULT_BUFFER_LENGTH_IN_MILLIS = 2000; - private static final String LOG_FILTER = "continuous-recorder: "; - - private SessionStartPointer mSessionStartPointer = SessionStartPointer.beginningOfBuffer(); - private final AtomicBoolean mRecordingToFile = new AtomicBoolean(false); - - public static class SessionStartPointer { - - private int mSessionStartPointerMillis; - private static SessionStartPointer mBeginningOfBufferPosition = new SessionStartPointer(Integer.MIN_VALUE); - private static SessionStartPointer mNowPosition = new SessionStartPointer(0); - - private SessionStartPointer(int sessionStartPointerMillis) { - setSessionStartPointerMillis(sessionStartPointerMillis); - } - - private void setSessionStartPointerMillis(int sessionStartPointerMillis) { - mSessionStartPointerMillis = sessionStartPointerMillis; - } - - int getSessionStartPointerMillis() { - // in case that no one set the buffer length, return start pointer as now - if (this == mBeginningOfBufferPosition && mSessionStartPointerMillis == Integer.MIN_VALUE) - return mNowPosition.getSessionStartPointerMillis(); - - // in case that the requested start pointer is bigger than the buffer, return the buffer size - if (this != mBeginningOfBufferPosition && mSessionStartPointerMillis < mBeginningOfBufferPosition.getSessionStartPointerMillis()) - return mBeginningOfBufferPosition.getSessionStartPointerMillis(); - - return mSessionStartPointerMillis; - } - - static void setRecordingBufferLengthMillis(int recordingBufferLengthMillis) { - mBeginningOfBufferPosition.setSessionStartPointerMillis(-Math.abs(recordingBufferLengthMillis)); - } - - public static SessionStartPointer beginningOfBuffer() { - return mBeginningOfBufferPosition; - } - - public static SessionStartPointer now() { - return mNowPosition; - } - - public static SessionStartPointer someMillisBack(int millisBackToStartTheSessionFrom) { - return new SessionStartPointer(-Math.abs(millisBackToStartTheSessionFrom)); - } - - public static SessionStartPointer someSecondsBack(int secondsBackToStartTheSessionFrom) { - return new SessionStartPointer(-Math.abs(secondsBackToStartTheSessionFrom * 1000)); - } - - public static SessionStartPointer someMillisForward(int millisForwardToStartTheSessionFrom) { - return new SessionStartPointer(Math.abs(millisForwardToStartTheSessionFrom)); - } - - public static SessionStartPointer someSecondsForward(int secondsForwardToStartTheSessionFrom) { - return new SessionStartPointer(Math.abs(secondsForwardToStartTheSessionFrom * 1000)); - } - - public static SessionStartPointer someMillisFromLatest(int millisFromLatestToStartTheSessionFrom_NegativeIsPast_PositiveIsFuture) { - return new SessionStartPointer(millisFromLatestToStartTheSessionFrom_NegativeIsPast_PositiveIsFuture); - } - - public static SessionStartPointer someSecondsFromLatest(int secondsFromLatestToStartTheSessionFrom_NegativeIsPast_PositiveIsFuture) { - return new SessionStartPointer(secondsFromLatestToStartTheSessionFrom_NegativeIsPast_PositiveIsFuture * 1000); - } - } - - public ContinuousRawAudioRecorder(int audioSource, int sampleRate, int recordingBufferLengthMillis) { - super(audioSource, sampleRate, recordingBufferLengthMillis, true); - - // this is very important. We introduce the buffer length to the SessionStartPointer object - SessionStartPointer.setRecordingBufferLengthMillis(recordingBufferLengthMillis); - - try { - int bufferSize = getBufferSize(); - int framePeriod = bufferSize / (2 * RESOLUTION_IN_BYTES * CHANNELS); - createRecorder(audioSource, sampleRate, bufferSize); - createBuffer(framePeriod); - setState(State.READY); - } catch (Exception e) { - if (e.getMessage() == null) { - handleError("Unknown error occurred while initializing recorder"); - } else { - handleError(e.getMessage()); - } - } - } - - public ContinuousRawAudioRecorder(int sampleRate, int recordingBufferLengthMillis) { - this(DEFAULT_AUDIO_SOURCE, sampleRate, recordingBufferLengthMillis); - } - - public ContinuousRawAudioRecorder(int sampleRate) { - this(DEFAULT_AUDIO_SOURCE, sampleRate, DEFAULT_BUFFER_LENGTH_IN_MILLIS); - } - - public ContinuousRawAudioRecorder() { - this(DEFAULT_AUDIO_SOURCE, DEFAULT_SAMPLE_RATE, DEFAULT_BUFFER_LENGTH_IN_MILLIS); - } - - public String getContentType() { - return "audio/x-raw, layout=(string)interleaved, rate=(int)" + getSampleRate() + ", format=(string)S16LE, channels=(int)1"; - } - - public ContinuousRawAudioRecorder setSessionStartPointer(SessionStartPointer sessionStartPointer) { - mSessionStartPointer = sessionStartPointer; - return this; - } - - private int calculateNumOfSamplesToGoBack(int startPos) { - // if the consumed session is not the same as the recorded session - // get the data from the beginning of the buffer/desired session start pointer - if (!isRecordedSessionSameAsConsumedSession()) { - Log.i(LOG_FILTER + "Recorded session and consumed session are NOT the same. Grabbing the data from the session start position"); - - // there are cases in which due to delay in the recorder wrt the real world, we will need - // to wait for the exact moment. The session start point should be based on trial and error - if (mSessionStartPointer.getSessionStartPointerMillis() > 0) { - try { - Thread.sleep(mSessionStartPointer.getSessionStartPointerMillis()); - } catch (InterruptedException e) { - } - - return getNumOfSamplesIn(SessionStartPointer.now().getSessionStartPointerMillis()); - } - - int numOfSamplesToGoBack = getNumOfSamplesIn(mSessionStartPointer.getSessionStartPointerMillis()); - if (numOfSamplesToGoBack > mRecording.length) - return getNumOfSamplesIn(SessionStartPointer.beginningOfBuffer().getSessionStartPointerMillis()); - - return numOfSamplesToGoBack; - } - - Log.i(LOG_FILTER + "Recorded session and consumed session are the same. Working according to the consumed pointer"); - if (startPos == getLength()) { - Log.i(LOG_FILTER + "Consumed session pointer still points to the recorded session pointer. Nothing to do or to return"); - return -1; - } - - int numOfSamplesToGoBack = getLength() - startPos; - if (numOfSamplesToGoBack < 0) // cyclic buffer case - numOfSamplesToGoBack += mRecording.length; - - Log.i(LOG_FILTER + "Consumed session pointer is behind the recorded session pointer by: " + numOfSamplesToGoBack + " samples"); - return numOfSamplesToGoBack; - } - - @Override - protected byte[] getCurrentRecording(int startPos) { - - int numOfSamplesToGoBack = calculateNumOfSamplesToGoBack(startPos); - if (numOfSamplesToGoBack <= 0) { - Log.i(LOG_FILTER + "There are no samples that we need to take from the recording"); - return null; - } - - return getCurrentRecordingFrom(numOfSamplesToGoBack); - } - - private byte[] getCurrentRecordingFrom(int numOfSamplesToGoBack) { - byte[] buffer = new byte[numOfSamplesToGoBack]; - int currentLength = getLength(); - int potentialStartSample = currentLength - numOfSamplesToGoBack; - - if (potentialStartSample >= 0) { - Log.i(LOG_FILTER + "Start sample in the recording is a positive one. Copying from position: " + potentialStartSample + ", " + numOfSamplesToGoBack + " bytes"); - System.arraycopy(mRecording, potentialStartSample, buffer, 0, numOfSamplesToGoBack); - } else { - if (!mRecordingBufferIsFullWithData) { - Log.i(LOG_FILTER + "Start sample in the recording is a negative one. The buffer did not pass one cycle yet. Copying from position: 0, " + currentLength + " bytes"); - System.arraycopy(mRecording, 0, buffer, 0, currentLength); - } else { - // the potential start sample is out of the boundaries of the array to the negative side - potentialStartSample = Math.abs(potentialStartSample); - Log.i(LOG_FILTER + "Start sample in the recording is a negative one. The buffer passed at least one cycle. Copying from position: " + (mRecording.length - potentialStartSample) + ", " + potentialStartSample + " bytes and from position: 0, " + currentLength + " bytes"); - System.arraycopy(mRecording, mRecording.length - potentialStartSample, buffer, 0, potentialStartSample); - System.arraycopy(mRecording, 0, buffer, potentialStartSample, currentLength); - } - } - - return buffer; - } - - public byte[] pcmToWav(byte[] pcm) { - return AudioUtils.getRecordingAsWav(pcm, getSampleRate(), RESOLUTION_IN_BYTES, CHANNELS); - } - - private byte[] createWavHeader(int pcmDataLength) { - return AudioUtils.getWavHeader(pcmDataLength, getSampleRate(), RESOLUTION_IN_BYTES, CHANNELS); - } - - public void dumpBufferToWavFile(String wavFileFullPath) { - SessionStartPointer sessionStartPointer = mSessionStartPointer; - setSessionStartPointer(SessionStartPointer.beginningOfBuffer()); - AudioUtils.saveWavToFile(wavFileFullPath, pcmToWav(consumeRecording()), false); - setSessionStartPointer(sessionStartPointer); - } - - public void startRecording(final String wavFileFullPath) { - if (!mRecordingToFile.compareAndSet(false, true)) - return; - - new Thread(new Runnable() { - @Override - public void run() { - // in case that someone stopped the recording before the thread could start - if (!mRecordingToFile.get()) - return; - - int pcmDataLength = 0; - byte[] pcmData; - AtomicBoolean firstBuffer = new AtomicBoolean(true); - - while (mRecordingToFile.get()) { - - // No need to endlessly poll the recording. It will work without the sleep well but - // every 50ms is also good (keeps the CPU happier than without the sleep) - try { - Thread.sleep(50L); - } catch (InterruptedException e) { - mRecordingToFile.set(false); - return; - } - - pcmData = consumeRecording(); - if (pcmData == null || pcmData.length == 0) - continue; - - pcmDataLength += pcmData.length; - if (firstBuffer.compareAndSet(true, false)) - AudioUtils.saveWavToFile(wavFileFullPath, pcmToWav(pcmData), false); - else - AudioUtils.saveWavToFile(wavFileFullPath, pcmData, true); - } - - //now rewrite the wav header according to the new size - AudioUtils.saveWavHeaderToFile(wavFileFullPath, createWavHeader(pcmDataLength)); - } - }).start(); - } - - public void stopRecording() { - mRecordingToFile.compareAndSet(true, false); - } -} diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/EncodedAudioRecorder.java b/app/src/main/java/ee/ioc/phon/android/speechutils/EncodedAudioRecorder.java deleted file mode 100644 index 4e7aed132dd3390d646d4bde608458c1fb3c8deb..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/EncodedAudioRecorder.java +++ /dev/null @@ -1,307 +0,0 @@ -/* - * Copyright 2015-2016, Institute of Cybernetics at Tallinn University of Technology - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ee.ioc.phon.android.speechutils; - -import android.annotation.TargetApi; -import android.media.MediaCodec; -import android.media.MediaFormat; -import android.os.Build; - -import java.nio.ByteBuffer; -import java.util.List; - -import ee.ioc.phon.android.speechutils.utils.AudioUtils; - -/** - * Based on https://android.googlesource.com/platform/cts/+/jb-mr2-release/tests/tests/media/src/android/media/cts/EncoderTest.java - * Requires Android v4.1 / API 16 / JELLY_BEAN - */ -public class EncodedAudioRecorder extends AbstractAudioRecorder { - - // TODO: support other formats than FLAC - private static final String MIME = "audio/flac"; - private static final String CONTENT_TYPE = "audio/x-flac"; - //private static final String MIME = "audio/opus"; - //private static final String CONTENT_TYPE = "audio/x-opus"; - - // Stop encoding if output buffer has not been available that many times. - private static final int MAX_NUM_RETRIES_DEQUEUE_OUTPUT_BUFFER = 10; - - // Timeout in microseconds to dequeue a buffer - // TODO: not sure what values are best here (behaves weird with negative values and very large values) - private static final long DEQUEUE_INPUT_BUFFER_TIMEOUT = 10000; - private static final long DEQUEUE_OUTPUT_BUFFER_TIMEOUT = 10000; - - // TODO: Use queue of byte[] - private final byte[] mRecordingEnc; - private int mRecordedEncLength = 0; - private int mConsumedEncLength = 0; - - private int mNumBytesSubmitted = 0; - private int mNumBytesDequeued = 0; - - public EncodedAudioRecorder(int audioSource, int sampleRate) { - super(audioSource, sampleRate); - try { - int bufferSize = getBufferSize(); - createRecorder(audioSource, sampleRate, bufferSize); - int framePeriod = bufferSize / (2 * RESOLUTION_IN_BYTES * CHANNELS); - createBuffer(framePeriod); - setState(State.READY); - } catch (Exception e) { - if (e.getMessage() == null) { - handleError("Unknown error occurred while initializing recording"); - } else { - handleError(e.getMessage()); - } - } - // TODO: replace 35 with the max length of the recording - mRecordingEnc = new byte[RESOLUTION_IN_BYTES * CHANNELS * sampleRate * 35]; // 35 sec raw - } - - public EncodedAudioRecorder(int sampleRate) { - this(DEFAULT_AUDIO_SOURCE, sampleRate); - } - - public EncodedAudioRecorder() { - this(DEFAULT_AUDIO_SOURCE, DEFAULT_SAMPLE_RATE); - } - - /** - * TODO: the MIME should be configurable as the server might not support all formats - * (returning "Your GStreamer installation is missing a plug-in.") - * TODO: according to the server docs, for encoded data we do not need to specify the content type - * such as "audio/x-flac", but it did not work without (nor with "audio/flac"). - */ - public String getContentType() { - return CONTENT_TYPE; - } - - public synchronized byte[] consumeRecordingEncAndTruncate() { - int len = getConsumedEncLength(); - byte[] bytes = getCurrentRecordingEnc(len); - setRecordedEncLength(0); - setConsumedEncLength(0); - return bytes; - } - - /** - * @return bytes that have been recorded and encoded since this method was last called - */ - public synchronized byte[] consumeRecordingEnc() { - byte[] bytes = getCurrentRecordingEnc(getConsumedEncLength()); - setConsumedEncLength(getRecordedEncLength()); - return bytes; - } - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - @Override - protected void recorderLoop(SpeechRecord speechRecord) { - mNumBytesSubmitted = 0; - mNumBytesDequeued = 0; - MediaFormat format = MediaFormatFactory.createMediaFormat(MIME, getSampleRate()); - MediaCodec codec = getCodec(format); - if (codec == null) { - handleError("no codec found"); - } else { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - Log.i("Using codec: " + codec.getCanonicalName()); - } - int status = recorderEncoderLoop(codec, speechRecord); - if (Log.DEBUG) { - AudioUtils.showMetrics(format, mNumBytesSubmitted, mNumBytesDequeued); - } - if (status < 0) { - handleError("encoder error"); - } - } - } - - // TODO: we currently return the first suitable codec - private MediaCodec getCodec(MediaFormat format) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - List componentNames = AudioUtils.getEncoderNamesForType(format.getString(MediaFormat.KEY_MIME)); - for (String componentName : componentNames) { - Log.i("component/format: " + componentName + "/" + format); - MediaCodec codec = AudioUtils.createCodec(componentName, format); - if (codec != null) { - return codec; - } - } - } - return null; - } - - private int getConsumedEncLength() { - return mConsumedEncLength; - } - - private void setConsumedEncLength(int len) { - mConsumedEncLength = len; - } - - private void setRecordedEncLength(int len) { - mRecordedEncLength = len; - } - - private int getRecordedEncLength() { - return mRecordedEncLength; - } - - private void addEncoded(byte[] buffer) { - int len = buffer.length; - if (mRecordingEnc.length >= mRecordedEncLength + len) { - System.arraycopy(buffer, 0, mRecordingEnc, mRecordedEncLength, len); - mRecordedEncLength += len; - } else { - handleError("RecorderEnc buffer overflow: " + mRecordedEncLength); - } - } - - private byte[] getCurrentRecordingEnc(int startPos) { - int len = getRecordedEncLength() - startPos; - byte[] bytes = new byte[len]; - System.arraycopy(mRecordingEnc, startPos, bytes, 0, len); - Log.i("Copied (enc) from pos: " + startPos + ", bytes: " + bytes.length); - return bytes; - } - - /** - * Copy audio from the recorder into the encoder. - */ - @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - private int queueInputBuffer(MediaCodec codec, ByteBuffer[] inputBuffers, int index, SpeechRecord speechRecord) { - if (speechRecord == null || speechRecord.getRecordingState() != SpeechRecord.RECORDSTATE_RECORDING) { - return -1; - } - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - ByteBuffer inputBuffer = inputBuffers[index]; - inputBuffer.clear(); - int size = inputBuffer.limit(); - byte[] buffer = new byte[size]; - int status = read(speechRecord, buffer); - if (status < 0) { - handleError("status = " + status); - return -1; - } - inputBuffer.put(buffer); - codec.queueInputBuffer(index, 0, size, 0, 0); - return size; - } - return -1; - } - - /** - * Save the encoded (output) buffer into the complete encoded recording. - * TODO: copy directly (without the intermediate byte array) - */ - @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - private void dequeueOutputBuffer(MediaCodec codec, ByteBuffer[] outputBuffers, int index, MediaCodec.BufferInfo info) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - ByteBuffer buffer = outputBuffers[index]; - Log.i("size/remaining: " + info.size + "/" + buffer.remaining()); - if (info.size <= buffer.remaining()) { - final byte[] bufferCopied = new byte[info.size]; - buffer.get(bufferCopied); // TODO: catch BufferUnderflow - // TODO: do we need to clear? - // on N5: always size == remaining(), clearing is not needed - // on SGS2: remaining decreases until it becomes less than size, which results in BufferUnderflow - // (but SGS2 records only zeros anyway) - //buffer.clear(); - codec.releaseOutputBuffer(index, false); - addEncoded(bufferCopied); - if (Log.DEBUG) { - AudioUtils.showSomeBytes("out", bufferCopied); - } - } else { - Log.e("size > remaining"); - codec.releaseOutputBuffer(index, false); - } - } - } - - /** - * Reads bytes from the given recorder and encodes them with the given encoder. - * Uses the (deprecated) Synchronous Processing using Buffer Arrays. - *

- * Encoders (or codecs that generate compressed data) will create and return the codec specific - * data before any valid output buffer in output buffers marked with the codec-config flag. - * Buffers containing codec-specific-data have no meaningful timestamps. - */ - @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - private int recorderEncoderLoop(MediaCodec codec, SpeechRecord speechRecord) { - int status = -1; - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - codec.start(); - // Getting some buffers (e.g. 4 of each) to communicate with the codec - ByteBuffer[] codecInputBuffers = codec.getInputBuffers(); - ByteBuffer[] codecOutputBuffers = codec.getOutputBuffers(); - Log.i("input buffers " + codecInputBuffers.length + "; output buffers: " + codecOutputBuffers.length); - boolean doneSubmittingInput = false; - int numDequeueOutputBufferTimeout = 0; - int index; - while (true) { - if (!doneSubmittingInput) { - index = codec.dequeueInputBuffer(DEQUEUE_INPUT_BUFFER_TIMEOUT); - if (index >= 0) { - int size = queueInputBuffer(codec, codecInputBuffers, index, speechRecord); - if (size == -1) { - codec.queueInputBuffer(index, 0, 0, 0, MediaCodec.BUFFER_FLAG_END_OF_STREAM); - Log.i("enc: in: EOS"); - doneSubmittingInput = true; - } else { - Log.i("enc: in: " + size); - mNumBytesSubmitted += size; - } - } else { - Log.i("enc: in: timeout, will try again"); - } - } - MediaCodec.BufferInfo info = new MediaCodec.BufferInfo(); - index = codec.dequeueOutputBuffer(info, DEQUEUE_OUTPUT_BUFFER_TIMEOUT); - Log.i("enc: out: flags/index: " + info.flags + "/" + index); - if (index == MediaCodec.INFO_TRY_AGAIN_LATER) { - numDequeueOutputBufferTimeout++; - Log.i("enc: out: INFO_TRY_AGAIN_LATER: " + numDequeueOutputBufferTimeout); - if (numDequeueOutputBufferTimeout > MAX_NUM_RETRIES_DEQUEUE_OUTPUT_BUFFER) { - break; - } - } else if (index == MediaCodec.INFO_OUTPUT_FORMAT_CHANGED) { - MediaFormat format = codec.getOutputFormat(); - Log.i("enc: out: INFO_OUTPUT_FORMAT_CHANGED: " + format.toString()); - } else if (index == MediaCodec.INFO_OUTPUT_BUFFERS_CHANGED) { - codecOutputBuffers = codec.getOutputBuffers(); - Log.i("enc: out: INFO_OUTPUT_BUFFERS_CHANGED"); - } else { - dequeueOutputBuffer(codec, codecOutputBuffers, index, info); - mNumBytesDequeued += info.size; - numDequeueOutputBufferTimeout = 0; - if ((info.flags & MediaCodec.BUFFER_FLAG_END_OF_STREAM) != 0) { - Log.i("enc: out: EOS"); - status = 0; - break; - } - } - } - codec.stop(); - codec.release(); - Log.i("stopped and released codec"); - } - return status; - } -} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/Extras.java b/app/src/main/java/ee/ioc/phon/android/speechutils/Extras.java deleted file mode 100644 index 8bad7567e98ce64ffe6f0a22b88675537604ffa5..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/Extras.java +++ /dev/null @@ -1,243 +0,0 @@ -/* - * Copyright 2011-2017, Institute of Cybernetics at Tallinn University of Technology - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ee.ioc.phon.android.speechutils; - -/** - *

EXTRAs for RecognizerIntent and SpeechRecognizer, - * in addition to the standard Android EXTRAs defined as part of RecognizerIntent.

- * - * @author Kaarel Kaljurand - */ -public class Extras { - - /** - * String (must be a legal URL). - * URL of a speech recognition server. - */ - public static final String EXTRA_SERVER_URL = "ee.ioc.phon.android.extra.SERVER_URL"; - - /** - * String. - * Class name of the recognizer component name, e.g. - * ee.ioc.phon.android.speak/.service.WebSocketRecognitionService - */ - public static final String EXTRA_SERVICE_COMPONENT = "ee.ioc.phon.android.extra.SERVICE_COMPONENT"; - - /** - * String (must be a legal URL). - * URL of a PGF or JSGF grammar. - */ - public static final String EXTRA_GRAMMAR_URL = "ee.ioc.phon.android.extra.GRAMMAR_URL"; - - /** - * String. - * Identifier of the target language in the given PGF grammar (e.g. "Est", "Eng"). - */ - public static final String EXTRA_GRAMMAR_TARGET_LANG = "ee.ioc.phon.android.extra.GRAMMAR_TARGET_LANG"; - - /** - * String. - * Desired transcription. - * Using this extra, the user can specify to which string the enclosed audio should be transcribed. - */ - public static final String EXTRA_PHRASE = "ee.ioc.phon.android.extra.PHRASE"; - - /** - * String. - * Optional text prompt to read out to the user when asking them to speak in the RecognizerIntent activity. - * See also "android.speech.extra.PROMPT". - */ - public static final String EXTRA_VOICE_PROMPT = "ee.ioc.phon.android.extra.VOICE_PROMPT"; - - /** - * Bundle. - * Information about the editor in which the IME is running. - */ - public static final String EXTRA_EDITOR_INFO = "ee.ioc.phon.android.extra.EDITOR_INFO"; - - /** - * Boolean. - * True iff the recognition service should not stop after delivering the first result. - */ - public static final String EXTRA_UNLIMITED_DURATION = "ee.ioc.phon.android.extra.UNLIMITED_DURATION"; - - /** - * Boolean. - * True iff the server has sent final=true, i.e. the following hypotheses - * will not be transcriptions of the same audio anymore. - */ - public static final String EXTRA_SEMI_FINAL = "ee.ioc.phon.android.extra.SEMI_FINAL"; - - /** - * Boolean. - * True iff the recognizer should play audio cues to indicate start and end of - * recording, as well as error conditions. - */ - public static final String EXTRA_AUDIO_CUES = "ee.ioc.phon.android.extra.AUDIO_CUES"; - - /** - * Boolean. - * True iff another app should be used to view/evaluate/execute the recognition result. - * Used only by the app Arvutaja. - */ - public static final String EXTRA_USE_EXTERNAL_EVALUATOR = "ee.ioc.phon.android.extra.USE_EXTERNAL_EVALUATOR"; - - /** - * Boolean. - * Start the recognition session immediately without the user having to press a button. - */ - public static final String EXTRA_AUTO_START = "ee.ioc.phon.android.extra.AUTO_START"; - - /** - * Boolean. (Default: true in single window mode, false in multi-window mode) - * True iff voice search panel will be terminated after it launched the intent. - */ - public static final String EXTRA_FINISH_AFTER_LAUNCH_INTENT = "ee.ioc.phon.android.extra.FINISH_AFTER_LAUNCH_INTENT"; - - /** - * Boolean. - * In case of an audio/network/etc. error, finish the RecognizerIntent activity with the error code, - * allowing the caller to handle the error. - * Normally errors are handled by the activity so that the activity only returns with success. - * However, in certain situations it is useful to let the caller handle the errors. If this - * is desired then the caller can request the returning of the errors using this EXTRA. - */ - public static final String EXTRA_RETURN_ERRORS = "ee.ioc.phon.android.extra.RETURN_ERRORS"; - - /** - * Boolean. - * True iff caller is interested in the recorded audio data. - */ - public static final String EXTRA_GET_AUDIO = "android.speech.extra.GET_AUDIO"; - - /** - * String. - * Mime type of the returned audio data, if EXTRA_GET_AUDIO=true. - */ - public static final String EXTRA_GET_AUDIO_FORMAT = "android.speech.extra.GET_AUDIO_FORMAT"; - - /** - * Boolean. - * True iff continuous recognition should be used. - * Same as EXTRA_UNLIMITED_DURATION. - * Used on Chrome to talk to Google's recognizer? - * (http://src.chromium.org/svn/trunk/src/content/public/android/java/src/org/chromium/content/browser/SpeechRecognition.java) - */ - public static final String EXTRA_DICTATION_MODE = "android.speech.extra.DICTATION_MODE"; - - /** - *

Key used to retrieve an {@code ArrayList} from the {@link android.os.Bundle} passed to the - * {@link android.speech.RecognitionListener#onResults(android.os.Bundle)} and - * {@link android.speech.RecognitionListener#onPartialResults(android.os.Bundle)} methods. - * This list represents structured data:

- *
    - *
  • raw utterance of hypothesis 1 - *
  • linearization 1.1 - *
  • language code of linearization 1.1 - *
  • linearization 1.2 - *
  • language code of linearization 1.2 - *
  • ... - *
  • raw utterance of hypothesis 2 - *
  • ... - *
- *

- *

The number of linearizations for each hypothesis is given by an ArrayList from a bundle - * item accessible via the key RESULTS_RECOGNITION_LINEARIZATION_COUNTS. - * Both of these bundle items have to be present for the client to be able to use the results.

- */ - public static final String RESULTS_RECOGNITION_LINEARIZATIONS = "ee.ioc.phon.android.extra.RESULTS_RECOGNITION_LINEARIZATIONS"; - public static final String RESULTS_RECOGNITION_LINEARIZATION_COUNTS = "ee.ioc.phon.android.extra.RESULTS_RECOGNITION_LINEARIZATION_COUNTS"; - - /** - * Byte array. Currently not used. - */ - public static final String RESULTS_AUDIO_ENCODED = "ee.ioc.phon.android.extra.RESULTS_AUDIO_ENCODED"; - - /** - * String (must be a Java regular expression). - * Regular expression applied to the transcription result(s). - */ - public static final String EXTRA_RESULT_UTTERANCE = "ee.ioc.phon.android.extra.RESULT_UTTERANCE"; - - /** - * String (must be a Java regular expression replacement). - * Replacement applied to the transcription result(s) if EXTRA_RESULT_UTTERANCE matches. - */ - public static final String EXTRA_RESULT_REPLACEMENT = "ee.ioc.phon.android.extra.RESULT_REPLACEMENT"; - - /** - * String. - * Name of a command. - */ - public static final String EXTRA_RESULT_COMMAND = "ee.ioc.phon.android.extra.RESULT_COMMAND"; - - /** - * String. - * Content of the 1st argument of the command. - */ - public static final String EXTRA_RESULT_ARG1 = "ee.ioc.phon.android.extra.RESULT_ARG1"; - - /** - * String. - * Content of the 2nd argument of the command. - */ - public static final String EXTRA_RESULT_ARG2 = "ee.ioc.phon.android.extra.RESULT_ARG2"; - - /** - * Boolean. - * If @code{true} then the following EXTRAs are set in the following way: - * EXTRA_RESULT_UTTERANCE = "(.+)" - * EXTRA_RESULT_COMMAND = "activity" - * EXTRA_RESULT_ARG1 = "$1" - */ - public static final String EXTRA_RESULT_LAUNCH_AS_ACTIVITY = "ee.ioc.phon.android.extra.RESULT_LAUNCH_AS_ACTIVITY"; - - /** - * String[]. - * List of transcription results. - */ - public static final String EXTRA_RESULT_RESULTS = "ee.ioc.phon.android.extra.RESULT_RESULTS"; - - /** - * String[] (String can also be used to denote a single element list) - * List of names of rewrite tables that should apply to the transcription results. - */ - public static final String EXTRA_RESULT_REWRITES = "ee.ioc.phon.android.extra.RESULT_REWRITES"; - - /** - * String. - * Rewrite table (in TSV-format and with a header) that should apply to the transcription results. - */ - public static final String EXTRA_RESULT_REWRITES_AS_STR = "ee.ioc.phon.android.extra.RESULT_REWRITES_AS_STR"; - - /** - * Used only by the app Arvutaja. - * - * @deprecated instead use EXTRA_AUTO_START - */ - public static final String EXTRA_LAUNCH_RECOGNIZER = "ee.ioc.phon.android.extra.LAUNCH_RECOGNIZER"; - - /** - * android/speech/RecognizerIntent.html#ACTION_VOICE_SEARCH_HANDS_FREE (API 16) - */ - public static final String ACTION_VOICE_SEARCH_HANDS_FREE = "android.speech.action.VOICE_SEARCH_HANDS_FREE"; - - /** - * A non-standard (undocumented) android.speech.extra - */ - public static final String EXTRA_ADDITIONAL_LANGUAGES = "android.speech.extra.EXTRA_ADDITIONAL_LANGUAGES"; -} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/Log.java b/app/src/main/java/ee/ioc/phon/android/speechutils/Log.java deleted file mode 100644 index 0ecdd65eb208051822c5f03e887a8f2fb5551364..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/Log.java +++ /dev/null @@ -1,37 +0,0 @@ -package ee.ioc.phon.android.speechutils; - -import java.util.List; - -public class Log { - - public static final boolean DEBUG = BuildConfig.DEBUG; - - public static final String LOG_TAG = "speechutils"; - - public static void i(String msg) { - if (DEBUG) android.util.Log.i(LOG_TAG, msg); - } - - public static void i(List msgs) { - if (DEBUG) { - for (String msg : msgs) { - if (msg == null) { - msg = ""; - } - android.util.Log.i(LOG_TAG, msg); - } - } - } - - public static void e(String msg) { - if (DEBUG) android.util.Log.e(LOG_TAG, msg); - } - - public static void i(String tag, String msg) { - if (DEBUG) android.util.Log.i(tag, msg); - } - - public static void e(String tag, String msg) { - if (DEBUG) android.util.Log.e(tag, msg); - } -} diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/MediaFormatFactory.java b/app/src/main/java/ee/ioc/phon/android/speechutils/MediaFormatFactory.java deleted file mode 100644 index be306b9091999079f6a102fd272634edc46cf062..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/MediaFormatFactory.java +++ /dev/null @@ -1,52 +0,0 @@ -package ee.ioc.phon.android.speechutils; - -import android.annotation.TargetApi; -import android.media.MediaFormat; -import android.os.Build; - -public class MediaFormatFactory { - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - public static MediaFormat createMediaFormat(String mime, int sampleRate) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - MediaFormat format = new MediaFormat(); - // TODO: this causes a crash in MediaCodec.configure - //format.setString(MediaFormat.KEY_FRAME_RATE, null); - format.setInteger(MediaFormat.KEY_SAMPLE_RATE, sampleRate); - format.setInteger(MediaFormat.KEY_CHANNEL_COUNT, 1); - format.setString(MediaFormat.KEY_MIME, mime); - if ("audio/mp4a-latm".equals(mime)) { - format.setInteger(MediaFormat.KEY_AAC_PROFILE, 2); // TODO: or 39? - format.setInteger(MediaFormat.KEY_BIT_RATE, 64000); - } else if ("audio/flac".equals(mime)) { - //format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_AUDIO_FLAC); // API=21 - format.setInteger(MediaFormat.KEY_BIT_RATE, 64000); - //TODO: use another bit rate, does not seem to have effect always - //format.setInteger(MediaFormat.KEY_BIT_RATE, 128000); - // TODO: experiment with 0 (fastest/least) to 8 (slowest/most) - //format.setInteger(MediaFormat.KEY_FLAC_COMPRESSION_LEVEL, 0); - } else if ("audio/opus".equals(mime)) { - //format.setString(MediaFormat.KEY_MIME, MediaFormat.MIMETYPE_AUDIO_OPUS); // API=21 - format.setInteger(MediaFormat.KEY_BIT_RATE, 64000); - } else { - // TODO: assuming: "audio/amr-wb" - format.setInteger(MediaFormat.KEY_BIT_RATE, 23050); - } - return format; - } - return null; - } - - //final int kAACProfiles[] = { - // 2 /* OMX_AUDIO_AACObjectLC */, - // 5 /* OMX_AUDIO_AACObjectHE */, - // 39 /* OMX_AUDIO_AACObjectELD */ - //}; - - //if (kAACProfiles[k] == 5 && kSampleRates[i] < 22050) { - // // Is this right? HE does not support sample rates < 22050Hz? - // continue; - //} - // final int kSampleRates[] = {8000, 11025, 22050, 44100, 48000}; - // final int kBitRates[] = {64000, 128000}; -} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/RawAudioRecorder.java b/app/src/main/java/ee/ioc/phon/android/speechutils/RawAudioRecorder.java deleted file mode 100644 index e50953045303b5c095a6ae98e7c3e64e7e81c314..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/RawAudioRecorder.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2011-2015, Institute of Cybernetics at Tallinn University of Technology - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package ee.ioc.phon.android.speechutils; - -/** - *

Records raw audio using SpeechRecord and stores it into a byte array as

- *
    - *
  • signed
  • - *
  • 16-bit
  • - *
  • native endian
  • - *
  • mono
  • - *
  • 16kHz (recommended, but a different sample rate can be specified in the constructor)
  • - *
- *

- *

For example, the corresponding arecord settings are

- *

- *

- * arecord --file-type raw --format=S16_LE --channels 1 --rate 16000
- * arecord --file-type raw --format=S16_BE --channels 1 --rate 16000 (possibly)
- * 
- *

- * TODO: maybe use: ByteArrayOutputStream - * - * @author Kaarel Kaljurand - */ -public class RawAudioRecorder extends AbstractAudioRecorder { - - /** - *

Instantiates a new recorder and sets the state to INITIALIZING. - * In case of errors, no exception is thrown, but the state is set to ERROR.

- *

- *

Android docs say: 44100Hz is currently the only rate that is guaranteed to work on all devices, - * but other rates such as 22050, 16000, and 11025 may work on some devices.

- * - * @param audioSource Identifier of the audio source (e.g. microphone) - * @param sampleRate Sample rate (e.g. 16000) - */ - public RawAudioRecorder(int audioSource, int sampleRate) { - super(audioSource, sampleRate); - try { - int bufferSize = getBufferSize(); - int framePeriod = bufferSize / (2 * RESOLUTION_IN_BYTES * CHANNELS); - createRecorder(audioSource, sampleRate, bufferSize); - createBuffer(framePeriod); - setState(State.READY); - } catch (Exception e) { - if (e.getMessage() == null) { - handleError("Unknown error occurred while initializing recorder"); - } else { - handleError(e.getMessage()); - } - } - } - - - public RawAudioRecorder(int sampleRate) { - this(DEFAULT_AUDIO_SOURCE, sampleRate); - } - - - public RawAudioRecorder() { - this(DEFAULT_AUDIO_SOURCE, DEFAULT_SAMPLE_RATE); - } - - public String getContentType() { - return "audio/x-raw, layout=(string)interleaved, rate=(int)" + getSampleRate() + ", format=(string)S16LE, channels=(int)1"; - } -} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/RecognitionServiceManager.java b/app/src/main/java/ee/ioc/phon/android/speechutils/RecognitionServiceManager.java deleted file mode 100644 index a4f49f61721413287883b00d76c85ca3d1bc42fe..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/RecognitionServiceManager.java +++ /dev/null @@ -1,306 +0,0 @@ -package ee.ioc.phon.android.speechutils; - -import android.annotation.TargetApi; -import android.app.Activity; -import android.content.BroadcastReceiver; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.content.pm.ServiceInfo; -import android.content.res.XmlResourceParser; -import android.graphics.drawable.Drawable; -import android.os.Build; -import android.os.Bundle; -import android.speech.RecognitionService; -import android.speech.RecognizerIntent; -import android.text.TextUtils; -import android.util.Pair; - -import org.xmlpull.v1.XmlPullParser; -import org.xmlpull.v1.XmlPullParserException; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Locale; -import java.util.Set; - -public class RecognitionServiceManager { - private static String SEPARATOR = ";"; - private Set mInitiallySelectedCombos = new HashSet<>(); - private Set mCombosExcluded = new HashSet<>(); - - public interface Listener { - void onComplete(List combos, Set selectedCombos); - } - - /** - * @return true iff a RecognitionService with the given component name is installed - */ - public static boolean isRecognitionServiceInstalled(PackageManager pm, ComponentName componentName) { - List services = pm.queryIntentServices( - new Intent(RecognitionService.SERVICE_INTERFACE), 0); - for (ResolveInfo ri : services) { - ServiceInfo si = ri.serviceInfo; - if (si == null) { - Log.i("serviceInfo == null"); - continue; - } - if (componentName.equals(new ComponentName(si.packageName, si.name))) { - return true; - } - } - return false; - } - - /** - * On LOLLIPOP we use a builtin to parse the locale string, and return - * the name of the locale in the language of the current locale. In pre-LOLLIPOP we just return - * the formal name (e.g. "et-ee"), because the Locale-constructor is not able to parse it. - * - * @param localeAsStr Formal name of the locale, e.g. "et-ee" - * @return The name of the locale in the language of the current locale - */ - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - public static String makeLangLabel(String localeAsStr) { - // Just to make sure we do not get a NPE from Locale.forLanguageTag - if (localeAsStr == null || localeAsStr.isEmpty()) { - return "?"; - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - return Locale.forLanguageTag(localeAsStr).getDisplayName(); - } - return localeAsStr; - } - - public static String[] getServiceAndLang(String str) { - return TextUtils.split(str, SEPARATOR); - } - - public static String createComboString(String service, String locale) { - return service + SEPARATOR + locale; - } - - public static Pair unflattenFromString(String comboId) { - String serviceAsStr = ""; - String localeAsStr = ""; - String[] splits = getServiceAndLang(comboId); - if (splits.length > 0) { - serviceAsStr = splits[0]; - if (splits.length > 1) { - localeAsStr = splits[1]; - } - } - return new Pair<>(ComponentName.unflattenFromString(serviceAsStr), localeAsStr); - } - - /** - * @param str string like {@code ee.ioc.phon.android.speak/.HttpRecognitionService;et-ee} - * @return ComponentName in the input string - */ - public static ComponentName getComponentName(String str) { - String[] splits = getServiceAndLang(str); - return ComponentName.unflattenFromString(splits[0]); - } - - public static String getServiceLabel(Context context, String service) { - ComponentName recognizerComponentName = ComponentName.unflattenFromString(service); - return getServiceLabel(context, recognizerComponentName); - } - - public static String getServiceLabel(Context context, ComponentName recognizerComponentName) { - try { - PackageManager pm = context.getPackageManager(); - ServiceInfo si = pm.getServiceInfo(recognizerComponentName, 0); - return si.loadLabel(pm).toString(); - } catch (PackageManager.NameNotFoundException e) { - // ignored - } - return "[?]"; - } - - public static ServiceInfo getServiceInfo(Context context, ComponentName recognizerComponentName) { - try { - PackageManager pm = context.getPackageManager(); - return pm.getServiceInfo(recognizerComponentName, PackageManager.GET_META_DATA); - } catch (PackageManager.NameNotFoundException e) { - // ignored - } - return null; - } - - public static String getSettingsActivity(Context context, ServiceInfo si) - throws XmlPullParserException, IOException { - PackageManager pm = context.getPackageManager(); - XmlResourceParser parser = null; - try { - parser = si.loadXmlMetaData(pm, RecognitionService.SERVICE_META_DATA); - if (parser == null) { - throw new XmlPullParserException("No " + RecognitionService.SERVICE_META_DATA + " meta-data"); - } - - int type; - while ((type = parser.next()) != XmlPullParser.END_DOCUMENT - && type != XmlPullParser.START_TAG) { - } - - String nodeName = parser.getName(); - if (!"recognition-service".equals(nodeName)) { - throw new XmlPullParserException( - "Meta-data does not start with recognition-service tag"); - } - - return parser.getAttributeValue("http://schemas.android.com/apk/res/android", - "settingsActivity"); - } finally { - if (parser != null) parser.close(); - } - } - - public static Drawable getServiceIcon(Context context, ComponentName recognizerComponentName) { - try { - PackageManager pm = context.getPackageManager(); - ServiceInfo si = pm.getServiceInfo(recognizerComponentName, 0); - return si.loadIcon(pm); - } catch (PackageManager.NameNotFoundException e) { - // ignored - } - return null; - } - - public void setCombosExcluded(Set set) { - mCombosExcluded = set; - } - - public void setInitiallySelectedCombos(Set set) { - if (set == null) { - mInitiallySelectedCombos = new HashSet<>(); - - } else { - mInitiallySelectedCombos = set; - } - } - - /** - * @return list of currently installed RecognitionService component names flattened to short strings - */ - public List getServices(PackageManager pm) { - List services = new ArrayList<>(); - int flags = 0; - //int flags = PackageManager.GET_META_DATA; - List infos = pm.queryIntentServices( - new Intent(RecognitionService.SERVICE_INTERFACE), flags); - - for (ResolveInfo ri : infos) { - ServiceInfo si = ri.serviceInfo; - if (si == null) { - Log.i("serviceInfo == null"); - continue; - } - String pkg = si.packageName; - String cls = si.name; - // TODO: process si.metaData - String component = (new ComponentName(pkg, cls)).flattenToShortString(); - if (!mCombosExcluded.contains(component)) { - services.add(component); - } - } - return services; - } - - /** - * Collect together the languages supported by the given services and call back once done. - */ - public void populateCombos(Context activity, final Listener listener) { - List services = getServices(activity.getPackageManager()); - populateCombos(activity, services, listener); - } - - public void populateCombos(Context activity, List services, final Listener listener) { - populateCombos(activity, services, 0, listener, new ArrayList<>(), new HashSet<>()); - } - - public void populateCombos(Context activity, String service, final Listener listener) { - final List services = new ArrayList<>(); - services.add(service); - populateCombos(activity, services, listener); - } - - private void populateCombos(final Context activity, final List services, final int counter, final Listener listener, - final List combos, final Set selectedCombos) { - - if (services.size() == counter) { - listener.onComplete(combos, selectedCombos); - return; - } - - Intent intent = new Intent(RecognizerIntent.ACTION_GET_LANGUAGE_DETAILS); - // TODO: this seems to be only for activities that implement ACTION_WEB_SEARCH - //Intent intent = RecognizerIntent.getVoiceDetailsIntent(this); - - final String service = services.get(counter); - ComponentName serviceComponent = ComponentName.unflattenFromString(service); - if (serviceComponent != null) { - intent.setPackage(serviceComponent.getPackageName()); - // TODO: ideally we would like to query the component, because the package might - // contain services (= components) with different capabilities. - //intent.setComponent(serviceComponent); - } - - // This is needed to include newly installed apps or stopped apps - // as receivers of the broadcast. - intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES); - - activity.sendOrderedBroadcast(intent, null, new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - - ArrayList langs = new ArrayList<>(); - - if (getResultCode() == Activity.RESULT_OK) { - Bundle results = getResultExtras(true); - - // Supported languages - String prefLang = results.getString(RecognizerIntent.EXTRA_LANGUAGE_PREFERENCE); - ArrayList allLangs = results.getCharSequenceArrayList(RecognizerIntent.EXTRA_SUPPORTED_LANGUAGES); - - Log.i("Supported langs: " + prefLang + ": " + allLangs); - if (allLangs != null) { - langs.addAll(allLangs); - } - // We add the preferred language to the list of supported languages, if not already there. - if (prefLang != null && !langs.contains(prefLang)) { - langs.add(prefLang); - } - } - - // Make sure that the list of languages contains at least one member, - // "und", to be interpreted - // as "some unspecified languages" or "all languages" (but not "no languages"). - // We use the code "und" here, see also: - // - https://en.wikipedia.org/wiki/ISO_639-3 - // - https://android.googlesource.com/platform/libcore/+/refs/heads/master/ojluni/src/main/java/java/util/Locale.java - // Android-added: (internal only): ISO 639-3 generic code for undetermined languages. - // private static final String UNDETERMINED_LANGUAGE = "und"; - langs.add("und"); - - for (CharSequence lang : langs) { - String combo = service + SEPARATOR + lang; - if (!mCombosExcluded.contains(combo)) { - Log.i(combos.size() + ") " + combo); - combos.add(combo); - if (mInitiallySelectedCombos.contains(combo)) { - selectedCombos.add(combo); - } - } - } - - populateCombos(activity, services, counter + 1, listener, combos, selectedCombos); - } - }, null, Activity.RESULT_OK, null, null); - } -} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/SpeechRecord.java b/app/src/main/java/ee/ioc/phon/android/speechutils/SpeechRecord.java deleted file mode 100644 index 795dc9020e063c27ca94cb83f1cec1f8638d58e7..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/SpeechRecord.java +++ /dev/null @@ -1,108 +0,0 @@ -package ee.ioc.phon.android.speechutils; - -import android.media.AudioFormat; -import android.media.AudioRecord; -import android.media.MediaRecorder; -import android.media.audiofx.AcousticEchoCanceler; -import android.media.audiofx.AutomaticGainControl; -import android.media.audiofx.NoiseSuppressor; -import android.os.Build; - -/** - * The following takes effect only on Jelly Bean and higher. - * - * @author Kaarel Kaljurand - */ -public class SpeechRecord extends AudioRecord { - - public SpeechRecord(int sampleRateInHz, int bufferSizeInBytes) - throws IllegalArgumentException { - - this( - MediaRecorder.AudioSource.VOICE_RECOGNITION, - sampleRateInHz, - AudioFormat.CHANNEL_IN_MONO, - AudioFormat.ENCODING_PCM_16BIT, - bufferSizeInBytes, - false, - false, - false - ); - } - - - public SpeechRecord(int sampleRateInHz, int bufferSizeInBytes, boolean noise, boolean gain, boolean echo) - throws IllegalArgumentException { - - this( - MediaRecorder.AudioSource.VOICE_RECOGNITION, - sampleRateInHz, - AudioFormat.CHANNEL_IN_MONO, - AudioFormat.ENCODING_PCM_16BIT, - bufferSizeInBytes, - noise, - gain, - echo - ); - } - - - // This is a copy of the AudioRecord constructor - public SpeechRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes) - throws IllegalArgumentException { - - this(audioSource, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes, false, false, false); - } - - - public SpeechRecord(int audioSource, int sampleRateInHz, int channelConfig, int audioFormat, int bufferSizeInBytes, - boolean noise, boolean gain, boolean echo) - throws IllegalArgumentException { - - super(audioSource, sampleRateInHz, channelConfig, audioFormat, bufferSizeInBytes); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - Log.i("Trying to enhance audio because running on SDK " + Build.VERSION.SDK_INT); - - int audioSessionId = getAudioSessionId(); - - if (noise) { - if (NoiseSuppressor.create(audioSessionId) == null) { - Log.i("NoiseSuppressor: failed"); - } else { - Log.i("NoiseSuppressor: ON"); - } - } else { - Log.i("NoiseSuppressor: OFF"); - } - - if (gain) { - if (AutomaticGainControl.create(audioSessionId) == null) { - Log.i("AutomaticGainControl: failed"); - } else { - Log.i("AutomaticGainControl: ON"); - } - } else { - Log.i("AutomaticGainControl: OFF"); - } - - if (echo) { - if (AcousticEchoCanceler.create(audioSessionId) == null) { - Log.i("AcousticEchoCanceler: failed"); - } else { - Log.i("AcousticEchoCanceler: ON"); - } - } else { - Log.i("AcousticEchoCanceler: OFF"); - } - } - } - - - public static boolean isNoiseSuppressorAvailable() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - return NoiseSuppressor.isAvailable(); - } - return false; - } -} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/TtsLocaleMapper.java b/app/src/main/java/ee/ioc/phon/android/speechutils/TtsLocaleMapper.java deleted file mode 100644 index 8fd59c91bea4339dd38252695e7a8b9c7237faca..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/TtsLocaleMapper.java +++ /dev/null @@ -1,33 +0,0 @@ -package ee.ioc.phon.android.speechutils; - -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -public class TtsLocaleMapper { - - private static final List SIMILAR_LOCALES_ET; - - static { - List aListEt = new ArrayList<>(); - aListEt.add(new Locale("fi-FI")); - aListEt.add(new Locale("es-ES")); - SIMILAR_LOCALES_ET = Collections.unmodifiableList(aListEt); - } - - private static final Map> SIMILAR_LOCALES; - - static { - Map> aMap = new HashMap<>(); - aMap.put(new Locale("et"), SIMILAR_LOCALES_ET); - aMap.put(new Locale("et-EE"), SIMILAR_LOCALES_ET); - SIMILAR_LOCALES = Collections.unmodifiableMap(aMap); - } - - public static List getSimilarLocales(Locale locale) { - return SIMILAR_LOCALES.get(locale); - } -} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/TtsProvider.java b/app/src/main/java/ee/ioc/phon/android/speechutils/TtsProvider.java deleted file mode 100644 index d7c55bd4e0f652b0d76a30f78f9c1e3ddc1ce3a2..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/TtsProvider.java +++ /dev/null @@ -1,130 +0,0 @@ -package ee.ioc.phon.android.speechutils; - -import android.annotation.SuppressLint; -import android.content.Context; -import android.os.Build; -import android.speech.tts.TextToSpeech; -import android.speech.tts.UtteranceProgressListener; - -import java.util.HashMap; -import java.util.List; -import java.util.Locale; - -/** - * TODO: add the capability to aggregate different TTS engines with support different languages - * getEngines() on API level 14 - */ -public class TtsProvider { - - public interface Listener { - void onDone(); - } - - private static final String UTT_COMPLETED_FEEDBACK = "UTT_COMPLETED_FEEDBACK"; - - private final TextToSpeech mTts; - - public TtsProvider(Context context, TextToSpeech.OnInitListener listener) { - // TODO: use the 3-arg constructor (API 14) that supports passing the engine. - // Choose the engine that supports the selected language, if there are several - // then let the user choose. - mTts = new TextToSpeech(context, listener); - Log.i("Default TTS engine:" + mTts.getDefaultEngine()); - } - - public int say(String text) { - return say(text, null); - } - - @SuppressLint("NewApi") - public int say(String text, final Listener listener) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) { - mTts.setOnUtteranceProgressListener(new UtteranceProgressListener() { - - @Override - public void onDone(String utteranceId) { - if (listener != null) listener.onDone(); - } - - @Override - public void onError(String utteranceId) { - if (listener != null) listener.onDone(); - } - - @Override - public void onStart(String utteranceId) { - } - }); - } else { - mTts.setOnUtteranceCompletedListener(utteranceId -> { - if (listener != null) listener.onDone(); - }); - } - HashMap params = new HashMap<>(); - params.put(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, UTT_COMPLETED_FEEDBACK); - return mTts.speak(text, TextToSpeech.QUEUE_FLUSH, params); - } - - - /** - * Interrupts the current utterance and discards other utterances in the queue. - * - * @return {@ERROR} or {@SUCCESS} - */ - public int stop() { - // TODO: not sure which callbacks get called as a result of stop() - return mTts.stop(); - } - - - /** - * TODO: is the language available on any engine (not just the default) - */ - public boolean isLanguageAvailable(String localeAsStr) { - return mTts.isLanguageAvailable(new Locale(localeAsStr)) >= 0; - } - - - /** - * TODO: set the language, changing the engine if the default engine - * does not support this language - */ - public void setLanguage(Locale locale) { - mTts.setLanguage(locale); - } - - - /** - * TODO: add this logic to setLanguage and deprecate this method - */ - public Locale chooseLanguage(String localeAsStr) { - Locale locale = new Locale(localeAsStr); - Log.i("Choosing TTS for: " + localeAsStr + " -> " + locale); - // TODO: this can throw NPE in String.isEmpty in java.util.Locale.getISO3Language - // if garbage is given as input - if (mTts.isLanguageAvailable(locale) >= 0) { - Log.i("Chose: " + locale); - return locale; - } - List similarLocales = TtsLocaleMapper.getSimilarLocales(locale); - if (similarLocales != null) { - for (Locale l : similarLocales) { - if (mTts.isLanguageAvailable(l) >= 0) { - Log.i("Chose: " + l + " from " + similarLocales); - return l; - } - } - } - Log.i("Chose: " + "NULL from " + similarLocales); - return null; - } - - - /** - * Shuts down the TTS instance, resuming the audio if needed. - */ - public void shutdown() { - mTts.shutdown(); - } - -} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/Command.java b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/Command.java deleted file mode 100644 index 6d3bc42bb5d62437b42cba247a775420ee0699b6..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/Command.java +++ /dev/null @@ -1,411 +0,0 @@ -package ee.ioc.phon.android.speechutils.editor; - -import android.text.TextUtils; -import android.util.Pair; - -import androidx.annotation.NonNull; - -import com.github.curiousoddman.rgxgen.RgxGen; -import com.github.curiousoddman.rgxgen.iterators.StringIterator; - -import java.util.Arrays; -import java.util.Collection; -import java.util.HashMap; -import java.util.Map; -import java.util.SortedMap; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class Command { - - private static final String[] EMPTY_ARRAY = new String[0]; - - private final static String SEPARATOR = "<___>"; - private final String mLabel; - private final String mComment; - private final Pattern mLocale; - private final Pattern mService; - private final Pattern mApp; - private final Pattern mUtt; - private final String mReplacement; - private final String mCommand; - private final String[] mArgs; - private final String mArgsAsStr; - private final boolean mIsRepeatable; - - /** - * @param label short label for GUI - * @param comment free-form comment - * @param locale locale of the utterance - * @param service regular expression to match the recognizer service class name - * @param app regular expression to match the calling app package name - * @param utt regular expression with capturing groups to match the utterance - * @param replacement replacement string for the matched substrings, typically empty in case of commands - * @param id name of the command to execute, null if missing - * @param args arguments of the command - */ - public Command(String label, String comment, Pattern locale, Pattern service, Pattern app, Pattern utt, String replacement, String id, String[] args) { - mLabel = label; - mComment = comment; - mLocale = locale; - mService = service; - mApp = app; - mUtt = utt; - mReplacement = replacement == null ? "" : replacement; - mCommand = id; - if (args == null) { - mArgs = EMPTY_ARRAY; - } else { - int i = 0; - for (; i < args.length; i++) { - if (args[i] == null || args[i].isEmpty()) { - break; - } - } - mArgs = Arrays.copyOf(args, i); - } - mArgsAsStr = TextUtils.join(SEPARATOR, mArgs); - - mIsRepeatable = mCommand != null && ( - mCommand.equals("moveRel") - || mCommand.equals("moveRelSel") - || mCommand.equals("selectReBefore") - || mCommand.equals("selectReAfter") - || mCommand.equals("select") - || mCommand.equals("deleteChars") - ); - } - - public Command(String label, String comment, Pattern locale, Pattern service, Pattern app, Pattern utt, String replacement, String id) { - this(label, comment, locale, service, app, utt, replacement, id, null); - } - - - public Command(String utt, String replacement, String id, String[] args) { - this(null, null, null, null, null, Pattern.compile(utt, Constants.REWRITE_PATTERN_FLAGS), replacement, id, args); - } - - public Command(String utt, String replacement, String id) { - this(null, null, null, null, null, Pattern.compile(utt, Constants.REWRITE_PATTERN_FLAGS), replacement, id, null); - } - - public Command(String utt, String replacement) { - this(null, null, null, null, null, Pattern.compile(utt, Constants.REWRITE_PATTERN_FLAGS), replacement, null, null); - } - - public String getLabel() { - return mLabel; - } - - public String getComment() { - return mComment; - } - - public Pattern getLocale() { - return mLocale; - } - - public Pattern getService() { - return mService; - } - - public Pattern getApp() { - return mApp; - } - - public Pattern getUtterance() { - return mUtt; - } - - @NonNull - public String getReplacement() { - return mReplacement; - } - - public String getId() { - return mCommand; - } - - public String[] getArgs() { - return mArgs; - } - - /** - * TODO: experimental - * - * @param colId Column name - * @return field value from the given column, converted to String - */ - public String get(@NonNull String colId) { - switch (colId) { - case UtteranceRewriter.HEADER_LABEL: - return mLabel; - case UtteranceRewriter.HEADER_COMMENT: - return mComment; - case UtteranceRewriter.HEADER_LOCALE: - return mLocale.pattern(); - case UtteranceRewriter.HEADER_SERVICE: - return mService.pattern(); - case UtteranceRewriter.HEADER_APP: - return mApp.pattern(); - case UtteranceRewriter.HEADER_UTTERANCE: - return unre(mUtt.pattern()); - case UtteranceRewriter.HEADER_REPLACEMENT: - return mReplacement; - case UtteranceRewriter.HEADER_COMMAND: - return mCommand; - case UtteranceRewriter.HEADER_ARG1: - if (mArgs.length > 0) { - return mArgs[0]; - } - break; - case UtteranceRewriter.HEADER_ARG2: - if (mArgs.length > 1) { - return mArgs[1]; - } - break; - default: - break; - } - return null; - } - - /** - * Parses the given string. - * If the entire string matches the utterance pattern, then extracts the arguments as well. - * Example: - * str = replace A with B - * mUtt = replace (.*) with (.*) - * mReplacement = "" - * $1<___>$2 - * A<___>B - * m.replaceAll(mReplacement) = "" - * argsEvaluated = [A, B] - * - * @param str string to be matched - * @return pair of replacement and array of arguments - */ - public Pair parse(CharSequence str) { - Matcher m = mUtt.matcher(str); - String[] argsEvaluated = null; - // If the entire region matches then we evaluate the arguments as well - // TODO: rethink this: we could match a sub string and do something with the - // prefix and suffix - if (m.matches()) { - if (mArgsAsStr.isEmpty()) { - argsEvaluated = EMPTY_ARRAY; - } else { - try { - argsEvaluated = TextUtils.split(m.replaceAll(mArgsAsStr), SEPARATOR); - } catch (IndexOutOfBoundsException e) { - // TODO: rethink this hack; occurs in Matcher.group, e.g. when "No group 1" - argsEvaluated = new String[]{e.getLocalizedMessage()}; - } catch (IllegalArgumentException e2) { - // java.lang.IllegalArgumentException: Illegal group reference: group index is missing - // This probably occurs when the dollar sign is used as the end-of-line symbol, - // but interpreted here as a group reference (e.g. "$1"). - argsEvaluated = new String[]{e2.getLocalizedMessage()}; - } - } - } - try { - return new Pair<>(m.replaceAll(mReplacement), argsEvaluated); - } catch (ArrayIndexOutOfBoundsException e) { - // This happens if the replacement references a group that does not exist - // TODO: throw an exception - return new Pair<>("[ERROR: " + e.getLocalizedMessage() + "]", argsEvaluated); - } - } - - public Map toMap(Collection header) { - Map map = new HashMap<>(); - for (String colName : header) { - switch (colName) { - case UtteranceRewriter.HEADER_LABEL: - map.put(UtteranceRewriter.HEADER_LABEL, mLabel); - break; - case UtteranceRewriter.HEADER_COMMENT: - map.put(UtteranceRewriter.HEADER_COMMENT, mComment); - break; - case UtteranceRewriter.HEADER_LOCALE: - map.put(UtteranceRewriter.HEADER_LOCALE, mLocale.pattern()); - break; - case UtteranceRewriter.HEADER_SERVICE: - map.put(UtteranceRewriter.HEADER_SERVICE, mService.pattern()); - break; - case UtteranceRewriter.HEADER_APP: - map.put(UtteranceRewriter.HEADER_APP, mApp.pattern()); - break; - case UtteranceRewriter.HEADER_UTTERANCE: - map.put(UtteranceRewriter.HEADER_UTTERANCE, mUtt.pattern()); - break; - case UtteranceRewriter.HEADER_REPLACEMENT: - map.put(UtteranceRewriter.HEADER_REPLACEMENT, mReplacement); - break; - case UtteranceRewriter.HEADER_COMMAND: - map.put(UtteranceRewriter.HEADER_COMMAND, mCommand); - break; - case UtteranceRewriter.HEADER_ARG1: - if (mArgs.length > 0) { - map.put(UtteranceRewriter.HEADER_ARG1, mArgs[0]); - } - break; - case UtteranceRewriter.HEADER_ARG2: - if (mArgs.length > 1) { - map.put(UtteranceRewriter.HEADER_ARG2, mArgs[1]); - } - break; - default: - break; - } - } - return map; - } - - // TODO: simplify to accept List as input (because keys are not used) - // TODO: escape "#" if line starts with "#" (e.g. the Label starts with "#"). - public String toTsv(SortedMap header) { - StringBuilder sb = new StringBuilder(); - boolean isFirst = true; - for (SortedMap.Entry entry : header.entrySet()) { - if (isFirst) { - isFirst = false; - } else { - sb.append('\t'); - } - switch (entry.getValue()) { - case UtteranceRewriter.HEADER_LABEL: - sb.append(escape(mLabel)); - break; - case UtteranceRewriter.HEADER_COMMENT: - sb.append(escape(mComment)); - break; - case UtteranceRewriter.HEADER_LOCALE: - sb.append(escape(mLocale)); - break; - case UtteranceRewriter.HEADER_SERVICE: - sb.append(escape(mService)); - break; - case UtteranceRewriter.HEADER_APP: - sb.append(escape(mApp)); - break; - case UtteranceRewriter.HEADER_UTTERANCE: - sb.append(escape(mUtt)); - break; - case UtteranceRewriter.HEADER_REPLACEMENT: - sb.append(escape(mReplacement)); - break; - case UtteranceRewriter.HEADER_COMMAND: - sb.append(escape(mCommand)); - break; - case UtteranceRewriter.HEADER_ARG1: - if (mArgs.length > 0) { - sb.append(escape(mArgs[0])); - } - break; - case UtteranceRewriter.HEADER_ARG2: - if (mArgs.length > 1) { - sb.append(escape(mArgs[1])); - } - break; - default: - break; - } - } - return sb.toString(); - } - - /** - * TODO: where and why should one use this? - */ - @NonNull - public String toString() { - if (mCommand == null) { - return mUtt + "/" + mReplacement; - } - return mUtt + "/" + mReplacement + "/" + mCommand + "(" + mArgsAsStr + ")"; - } - - /** - * True if the given command is equal to this command. Here the equality - * only considers the replacement, and (if defined) the command ID and its arguments. - * This means that the activation pattern (utterance), context restrictions (command matcher), - * and labels and comments are ignored when testing the equality. - */ - public boolean equalsCommand(@NonNull Command that) { - if (getId() == null) { - return getReplacement().equals(that.getReplacement()); - } - return getReplacement().equals(that.getReplacement()) && - getId().equals(that.getId()) && - Arrays.equals(getArgs(), that.getArgs()); - } - - /** - * Work in progress. - * Map the Utterance-field (regex) to a string that is matched by this regex. - * TODO: return an iterator over all possible matches - */ - public String makeUtt() { - RgxGen rgxGen = new RgxGen(mUtt.pattern()); - StringIterator uniqueStrings = rgxGen.iterateUnique(); - if (uniqueStrings.hasNext()) { - return uniqueStrings.next(); - } - return null; - } - - public String getLabelOrString() { - String label = getLabel(); - if (label == null || label.isEmpty()) { - label = toString(); - } - return label; - } - - /** - * Some commands can be executed repeatedly, e.g. moving the cursor left or searching the - * remaining document for a given string. For other commands (copy, send) this does not make sense. - * The UI can implement such repeatability with a press-and-hold button, but then might be - * unable to support long press, scroll, swipe. - * Many commands are in principle repeatable (undo, paste, typing letter "a", ...) but soft - * keyboards do not commonly implement them with press-and-hold. We currently declare a small number - * of cursor, selection and deletion commands as repeatable, but in the future this should be - * overridable by the user in the rewrites table. - * - * @return True iff command is repeatable - */ - public boolean isRepeatable() { - return mIsRepeatable; - } - - /** - * Removes ^ and $ from the given regex - * TODO: experimental - */ - private static String unre(String re) { - if (re.startsWith("^")) re = re.substring(1); - if (re.endsWith("$")) re = re.substring(0, re.length() - 1); - return re; - } - - /** - * Maps newlines and tabs to literals of the form "\n" and "\t". - */ - private static String escape(Object str) { - if (str == null) { - return ""; - } - return str.toString().replace("\n", "\\n").replace("\t", "\\t"); - } - - /** - * Maps literals of the form "\n" and "\t" to newlines and tabs. - */ - public static String unescape(String str) { - if (str == null) { - return ""; - } - return str.replace("\\n", "\n").replace("\\t", "\t"); - } -} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/CommandEditor.java b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/CommandEditor.java deleted file mode 100644 index 2f7468a3f1506cf1b7512a64d22a4ef7b1052560..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/CommandEditor.java +++ /dev/null @@ -1,222 +0,0 @@ -package ee.ioc.phon.android.speechutils.editor; - -import android.view.inputmethod.ExtractedText; - -import java.util.Collection; -import java.util.Deque; -import java.util.List; - -/** - * Note that "cursor" is the same as "selection" in Android, i.e. a cursor has a start and end position - * which are not necessarily identical. - * Note that the meta commands (undo, etc.) cannot be applied to some commands, - * e.g. IME and context menu actions. - * Most methods return an operation (op), which when run returns an undo operation. - */ -public interface CommandEditor { - - // Go to the character at the given position. - // Negative positions start counting from the end of the text, e.g. - // -1 == end of text - Op moveAbs(int pos); - - /** - * Move the cursor forward (+) or backwards (-) by the given number of characters. - * Forward movement starts from the end of the selection, backward movement from the beginning of - * the selection. In case of 0, the selection is reset to its end position. - * - * @param numOfChars number of character positions - * @return Op - */ - Op moveRel(int numOfChars); - - /** - * Change either the start or the end of the selection by the given number of characters. - * - * @param numOfChars number of character positions - * @param type 0 for start, 1 for end - * @return Op - */ - Op moveRelSel(int numOfChars, int type); - - // Press Up-arrow key - Op keyUp(); - - // Press Down-arrow key - Op keyDown(); - - // Press Left-arrow key - Op keyLeft(); - - // Press Right-arrow key - Op keyRight(); - - // Press the key with the given code - Op keyCode(int code); - - // Press the key with the given symbolic name - Op keyCodeStr(String codeAsStr); - - // Move cursor left to the matching string - // Supports function @sel() to refer to the content of the current selection. - Op select(String str); - - /** - * Move cursor left to the matching regex. - * Supports function @sel() to refer to the content of the current selection. - * - * @param regex regular expression to be searched - * @return Op - */ - Op selectReBefore(String regex); - - /** - * Move cursor right to the Nth matching regex. - * Supports function @sel() to refer to the content of the current selection. - * - * @param regex regular expression to be searched - * @param n number of matches before stopping the search - * @return Op - */ - Op selectReAfter(String regex, int n); - - // Extend the cursor to match the given regex - Op selectRe(String regex, boolean applyToSelection); - - // Select all (note: not a context menu action) - Op selectAll(); - - // selectAll + replace cursor with empty string - Op deleteAll(); - - // TODO: clarify - // Apply a regular expression to the selection, - // and replace the matches with the given replacement string. - Op replaceSelRe(String regex, String repl); - - // Cut (Context menu action) - Op cut(); - - // Copy (Context menu action) - Op copy(); - - // Paste (Context menu action) - Op paste(); - - // selectAll + cut - Op cutAll(); - - // selectAll + copy - Op copyAll(); - - /** - * Delete the given numbers of character either to the left and to the right of the cursor. - * In case there is a selection, then the selection is deleted instead. - * - * @param numOfChars number of chars to delete, if negative then from the left, if positive then from the right - * @return Op - */ - Op deleteChars(int numOfChars); - - // Delete the word immediately to the left. - // In case there is a selection, then the selection is deleted. - Op deleteLeftWord(); - - // Replace text1 (left of cursor) with text2. - // Deletion can be performed by setting text2 to be an empty string. - Op replace(String text1, String text2); - - // Replace cursor with the given text - // Supports function @sel() to refer to the content of the current selection. - Op replaceSel(String text); - - // Replace cursor with the given text. - // Supports function @sel() to refer to the content of the current selection. - // Sets a new selection within the replacement using the first group of the matching regex. - Op replaceSel(String text, String regex); - - // Uppercase the text under the cursor - Op ucSel(); - - // Lowercase the text under the cursor - Op lcSel(); - - // Interpret the text under the cursor as an integer and increase it by 1 - Op incSel(); - - // Perform the given IME action - Op imeAction(int editorAction); - - // Jump to the previous field (IME action) - Op imeActionPrevious(); - - // Jump to the next field (IME action) - Op imeActionNext(); - - // Done (IME action) - Op imeActionDone(); - - // Go (IME action) - Op imeActionGo(); - - // Search (IME action) - Op imeActionSearch(); - - // Send (IME action) - Op imeActionSend(); - - // Undo the last N commands (or text entries) - Op undo(int n); - - // Combine the last N commands - Op combine(int n); - - // Apply the last command N times - Op apply(int n); - - // Start an activity from the given JSON-encoded Android Intent. - // Supports function @sel() to refer to the content of the current selection. - Op activity(String json); - - // Replace cursor with the response of the given URL. - // The arg (if not null) is encoded and concatenated to the URL. - // Executed by AsyncTask. - // E.g. url == "http://api.mathjs.org/v4/?expr="; arg == "@sel()+1" - Op getUrl(String url, String arg); - - // Commands that are not exposed to the end-user in CommandEditorManager - - CommandEditorResult commitFinalResult(String text); - - boolean commitPartialResult(String text); - - boolean runOp(Op op); - - boolean runOp(Op op, boolean undoable); - - ExtractedText getExtractedText(); - - CharSequence getText(); - - List getRewriters(); - - void setRewriters(List urs); - - Deque getOpStack(); - - Deque getUndoStack(); - - Op combineOps(Collection ops); - - /** - * Convert the given text into an op, if some rewrite rules trigger on it. - * Otherwise return null, unless always==true. - * - * @param text Input text - * @param always Return Op also if no rewrite triggers - * @return Op or null - */ - Op getOpOrNull(String text, boolean always); - - void reset(); -} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/CommandEditorManager.java b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/CommandEditorManager.java deleted file mode 100644 index e1fe5c6ac126fca1e7f09f5f3b5e26ffd08ed83d..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/CommandEditorManager.java +++ /dev/null @@ -1,206 +0,0 @@ -package ee.ioc.phon.android.speechutils.editor; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; - -/** - * Mapping of command names to function calls. - */ -public class CommandEditorManager { - - public static final String MOVE_ABS = "moveAbs"; - public static final String MOVE_REL = "moveRel"; - public static final String MOVE_REL_SEL = "moveRelSel"; - public static final String SELECT = "select"; - public static final String SELECT_RE_BEFORE = "selectReBefore"; - public static final String SELECT_RE_AFTER = "selectReAfter"; - public static final String REPLACE_SEL_RE = "replaceSelRe"; - public static final String SELECT_ALL = "selectAll"; - public static final String CUT = "cut"; - public static final String COPY = "copy"; - public static final String PASTE = "paste"; - public static final String CUT_ALL = "cutAll"; - public static final String DELETE_ALL = "deleteAll"; - public static final String COPY_ALL = "copyAll"; - public static final String DELETE_CHARS = "deleteChars"; - public static final String DELETE_LEFT_WORD = "deleteLeftWord"; - public static final String REPLACE = "replace"; - public static final String REPLACE_SEL = "replaceSel"; - public static final String UC_SEL = "ucSel"; - public static final String LC_SEL = "lcSel"; - public static final String INC_SEL = "incSel"; - public static final String KEY_UP = "keyUp"; - public static final String KEY_DOWN = "keyDown"; - public static final String KEY_LEFT = "keyLeft"; - public static final String KEY_RIGHT = "keyRight"; - public static final String KEY_CODE = "keyCode"; - public static final String KEY_CODE_STR = "keyCodeStr"; - public static final String IME_ACTION_PREVIOUS = "imeActionPrevious"; - public static final String IME_ACTION_NEXT = "imeActionNext"; - public static final String IME_ACTION_DONE = "imeActionDone"; - public static final String IME_ACTION_GO = "imeActionGo"; - public static final String IME_ACTION_SEARCH = "imeActionSearch"; - public static final String IME_ACTION_SEND = "imeActionSend"; - public static final String UNDO = "undo"; - public static final String COMBINE = "combine"; - public static final String APPLY = "apply"; - public static final String ACTIVITY = "activity"; - public static final String GET_URL = "getUrl"; - - public static final Map EDITOR_COMMANDS; - - static { - - Map aMap = new HashMap<>(); - - aMap.put(KEY_UP, (ce, args) -> ce.keyUp()); - - aMap.put(KEY_DOWN, (ce, args) -> ce.keyDown()); - - aMap.put(KEY_LEFT, (ce, args) -> ce.keyLeft()); - - aMap.put(KEY_RIGHT, (ce, args) -> ce.keyRight()); - - aMap.put(UNDO, (ce, args) -> ce.undo(getArgInt(args, 0, 1))); - - aMap.put(COMBINE, (ce, args) -> ce.combine(getArgInt(args, 0, 2))); - - aMap.put(APPLY, (ce, args) -> ce.apply(getArgInt(args, 0, 1))); - - aMap.put(MOVE_ABS, (ce, args) -> ce.moveAbs(getArgInt(args, 0, 1))); - - aMap.put(MOVE_REL, (ce, args) -> ce.moveRel(getArgInt(args, 0, 1))); - - aMap.put(MOVE_REL_SEL, (ce, args) -> ce.moveRelSel(getArgInt(args, 0, 1), getArgInt(args, 1, 1))); - - aMap.put(KEY_CODE, (ce, args) -> { - if (args != null && args.length > 0) { - try { - return ce.keyCode(Integer.parseInt(args[0])); - } catch (NumberFormatException e) { - // Intentional - } - } - return null; - }); - - aMap.put(KEY_CODE_STR, (ce, args) -> ce.keyCodeStr(getArgString(args, 0, null))); - - aMap.put(SELECT, (ce, args) -> ce.select(getArgString(args, 0, null))); - - aMap.put(SELECT_RE_BEFORE, (ce, args) -> ce.selectReBefore(getArgString(args, 0, null))); - - aMap.put(SELECT_RE_AFTER, (ce, args) -> { - if (args == null || args.length < 1) { - return null; - } - int n = 1; - if (args.length > 1) { - try { - n = Integer.parseInt(args[1]); - } catch (NumberFormatException e) { - // Intentional - } - } - return ce.selectReAfter(args[0], n); - }); - - aMap.put(REPLACE_SEL_RE, (ce, args) -> { - if (args == null || args.length < 2) { - return null; - } - return ce.replaceSelRe(args[0], args[1]); - }); - - aMap.put(DELETE_CHARS, (ce, args) -> ce.deleteChars(getArgInt(args, 0, -1))); - - aMap.put(DELETE_LEFT_WORD, (ce, args) -> ce.deleteLeftWord()); - - // Single-argument "replace" replaces by empty string (i.e. deletes) - aMap.put(REPLACE, (ce, args) -> { - if (args == null || args.length < 1) { - return null; - } - String text1 = args[0]; - String text2 = args.length > 1 ? args[1] : ""; - return ce.replace(text1, text2); - }); - - aMap.put(REPLACE_SEL, (ce, args) -> ce.replaceSel( - getArgString(args, 0, null), - getArgString(args, 1, null) - )); - - aMap.put(UC_SEL, (ce, args) -> ce.ucSel()); - - aMap.put(LC_SEL, (ce, args) -> ce.lcSel()); - - aMap.put(INC_SEL, (ce, args) -> ce.incSel()); - - aMap.put(SELECT_ALL, (ce, args) -> ce.selectAll()); - - aMap.put(CUT, (ce, args) -> ce.cut()); - - aMap.put(CUT_ALL, (ce, args) -> ce.cutAll()); - - aMap.put(DELETE_ALL, (ce, args) -> ce.deleteAll()); - - aMap.put(COPY, (ce, args) -> ce.copy()); - - aMap.put(COPY_ALL, (ce, args) -> ce.copyAll()); - - aMap.put(PASTE, (ce, args) -> ce.paste()); - - aMap.put(IME_ACTION_PREVIOUS, (ce, args) -> ce.imeActionPrevious()); - - aMap.put(IME_ACTION_NEXT, (ce, args) -> ce.imeActionNext()); - - aMap.put(IME_ACTION_DONE, (ce, args) -> ce.imeActionDone()); - - aMap.put(IME_ACTION_GO, (ce, args) -> ce.imeActionGo()); - - aMap.put(IME_ACTION_SEARCH, (ce, args) -> ce.imeActionSearch()); - - aMap.put(IME_ACTION_SEND, (ce, args) -> ce.imeActionSend()); - - aMap.put(ACTIVITY, (ce, args) -> ce.activity(getArgString(args, 0, null))); - - aMap.put(GET_URL, (ce, args) -> { - if (args == null || args.length < 1) { - return null; - } - String urlPrefix = args[0]; - String urlArg = args.length > 1 ? args[1] : null; - return ce.getUrl(urlPrefix, urlArg); - }); - - EDITOR_COMMANDS = Collections.unmodifiableMap(aMap); - } - - public interface EditorCommand { - Op getOp(CommandEditor commandEditor, String[] args); - } - - public static EditorCommand get(String id) { - return EDITOR_COMMANDS.get(id); - } - - private static String getArgString(String[] args, int idx, String defaultValue) { - if (args != null && args.length > idx) { - return args[idx]; - } - return defaultValue; - } - - private static int getArgInt(String[] args, int idx, int defaultValue) { - if (args != null && args.length > idx) { - try { - return Integer.parseInt(args[idx]); - } catch (NumberFormatException e) { - // Return defaultValue if number conversion fails - } - } - return defaultValue; - } -} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/CommandEditorResult.java b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/CommandEditorResult.java deleted file mode 100644 index 563ce517b320506b2e41c456a9905e878b2ecd13..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/CommandEditorResult.java +++ /dev/null @@ -1,28 +0,0 @@ -package ee.ioc.phon.android.speechutils.editor; - -public class CommandEditorResult { - - private final boolean mSuccess; - private final UtteranceRewriter.Rewrite mRewrite; - - public CommandEditorResult(boolean success, UtteranceRewriter.Rewrite rewrite) { - mSuccess = success; - mRewrite = rewrite; - } - - public boolean isCommand() { - return mRewrite.isCommand(); - } - - public UtteranceRewriter.Rewrite getRewrite() { - return mRewrite; - } - - public String getStr() { - return mRewrite.getStr(); - } - - public boolean isSuccess() { - return mSuccess; - } -} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/CommandMatcher.java b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/CommandMatcher.java deleted file mode 100644 index 8e9902f137c86af461665cd421ec038b0df4326b..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/CommandMatcher.java +++ /dev/null @@ -1,15 +0,0 @@ -package ee.ioc.phon.android.speechutils.editor; - -import java.util.regex.Pattern; - -public interface CommandMatcher { - - /** - * @param locale locale - * @param service service - * @param app app - * @return true iff the given patterns match - */ - boolean matches(Pattern locale, Pattern service, Pattern app); - -} diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/CommandMatcherFactory.java b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/CommandMatcherFactory.java deleted file mode 100644 index c05ad0fd97442b0e7c6598db3033634b238b10f2..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/CommandMatcherFactory.java +++ /dev/null @@ -1,34 +0,0 @@ -package ee.ioc.phon.android.speechutils.editor; - -import android.content.ComponentName; - -import ee.ioc.phon.android.speechutils.Log; - -public class CommandMatcherFactory { - - public static CommandMatcher createCommandFilter(final String localeAsStr, final ComponentName serviceComponent, final ComponentName appComponent) { - final String serviceClassName = serviceComponent == null ? null : serviceComponent.getClassName(); - // The Kõnele launcher (whose calling activity == null) can be matched using ":". - final String appClassName = appComponent == null ? ":" : appComponent.getClassName(); - return (localePattern, servicePattern, appPattern) -> { - Log.i("matches?: pattern: <" + localePattern + "> <" + servicePattern + "> <" + appPattern + ">"); - if (localeAsStr != null && localePattern != null) { - if (!localePattern.matcher(localeAsStr).find()) { - return false; - } - } - if (serviceClassName != null && servicePattern != null) { - if (!servicePattern.matcher(serviceClassName).find()) { - return false; - } - } - if (appPattern != null) { - if (!appPattern.matcher(appClassName).find()) { - return false; - } - } - Log.i("matches: " + localeAsStr + " " + serviceClassName + " " + appClassName); - return true; - }; - } -} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/Constants.java b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/Constants.java deleted file mode 100644 index 06982125001648c7a5364652e48027359957e4b9..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/Constants.java +++ /dev/null @@ -1,38 +0,0 @@ -package ee.ioc.phon.android.speechutils.editor; - -import java.util.Arrays; -import java.util.HashSet; -import java.util.Set; -import java.util.regex.Pattern; - -public class Constants { - - // Symbols that should not be preceded by space in a written text. - public static final Set CHARACTERS_STICKY_LEFT = - new HashSet<>(Arrays.asList(',', ':', ';', '.', '!', '?', '-', ')')); - - // Symbols after which the next word should be capitalized. - // We include ) because ;-) often finishes a sentence. - public static final Set CHARACTERS_EOS = - new HashSet<>(Arrays.asList('.', '!', '?', ')')); - - // These symbols stick to the next symbol, i.e. no whitespace is added in front of the - // following string. - public static final Set CHARACTERS_STICKY_RIGHT = - new HashSet<>(Arrays.asList('(', '[', '{', '<', '-')); - - // TODO: review these - public static final int REWRITE_PATTERN_FLAGS = Pattern.CASE_INSENSITIVE - | Pattern.UNICODE_CASE - | Pattern.MULTILINE - | Pattern.DOTALL; - - // Characters that are transparent (but not whitespace) when deciding if a word follows an EOS - // character and should thus be capitalized. - private static final Set CHARACTERS_TRANSPARENT = - new HashSet<>(Arrays.asList('(', '[', '{', '<')); - - public static final boolean isTransparent(char c) { - return Constants.CHARACTERS_TRANSPARENT.contains(c) || Character.isWhitespace(c); - } -} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditor.java b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditor.java deleted file mode 100644 index 2c5b0010350aaedf58a34428c51d43fd542dad2e..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/InputConnectionCommandEditor.java +++ /dev/null @@ -1,1472 +0,0 @@ -package ee.ioc.phon.android.speechutils.editor; - -import android.content.Context; -import android.content.SharedPreferences; -import android.content.res.Resources; -import android.os.AsyncTask; -import android.os.Build.VERSION; -import android.preference.PreferenceManager; -import android.text.TextUtils; -import android.util.Pair; -import android.view.KeyEvent; -import android.view.inputmethod.EditorInfo; -import android.view.inputmethod.ExtractedText; -import android.view.inputmethod.ExtractedTextRequest; -import android.view.inputmethod.InputConnection; - -import androidx.annotation.NonNull; - -import org.json.JSONException; - -import java.io.IOException; -import java.util.ArrayDeque; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Deque; -import java.util.List; -import java.util.NoSuchElementException; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import ee.ioc.phon.android.speechutils.Log; -import ee.ioc.phon.android.speechutils.utils.HttpUtils; -import ee.ioc.phon.android.speechutils.utils.IntentUtils; -import ee.ioc.phon.android.speechutils.utils.JsonUtils; - -import static android.os.Build.VERSION_CODES; - -/** - * TODO: this is work in progress - * TODO: keep track of added spaces - * TODO: the returned Op should never be null, however, run can return a null Op - */ -public class InputConnectionCommandEditor implements CommandEditor { - - // Maximum number of previous utterances that a command can contain - private static final int MAX_UTT_IN_COMMAND = 3; - - // Maximum number of characters that left-swipe is willing to delete - private static final int MAX_DELETABLE_CONTEXT = 100; - // Token optionally preceded by whitespace - private static final Pattern WHITESPACE_AND_TOKEN = Pattern.compile("\\s*\\w+"); - private static final String F_SELECTION = "@sel()"; - - private Context mContext; - private SharedPreferences mPreferences; - private Resources mRes; - - private CharSequence mTextBeforeCursor; - // TODO: Restrict the size of these stacks - - // The command prefix is a list of consecutive final results whose concatenation can possibly - // form a command. An item is added to the list for every final result that is not a command. - // The list if cleared if a command is executed. - private List mCommandPrefix = new ArrayList<>(); - private Deque mOpStack = new ArrayDeque<>(); - private Deque mUndoStack = new ArrayDeque<>(); - - private InputConnection mInputConnection; - - private List mRewriters; - - public InputConnectionCommandEditor(@NonNull Context context) { - mContext = context; - mPreferences = PreferenceManager.getDefaultSharedPreferences(context); - mRes = context.getResources(); - } - - public void setInputConnection(@NonNull InputConnection inputConnection) { - mInputConnection = inputConnection; - } - - protected @NonNull - InputConnection getInputConnection() { - return mInputConnection; - } - - @Override - public void setRewriters(List urs) { - mRewriters = urs; - reset(); - } - - @Override - public List getRewriters() { - return mRewriters; - } - - @Override - public boolean runOp(Op op) { - return runOp(op, true); - } - - @Override - public boolean runOp(Op op, boolean undoable) { - // TODO: why does this happen - //if (op == null) { - // return false; - //} - reset(); - Op undo = op.run(); - if (undo == null) { - // Operation failed; - return false; - } - if (undoable && !undo.isNoOp()) { - pushOp(op); - pushOpUndo(undo); - } - mTextBeforeCursor = getTextBeforeCursor(); - return true; - } - - @Override - public Op getOpOrNull(@NonNull final String text, boolean always) { - String newText = text; - for (UtteranceRewriter ur : mRewriters) { - if (ur == null) { - continue; - } - UtteranceRewriter.Rewrite rewrite = ur.getRewrite(text); - newText = rewrite.mStr; - if (rewrite.isCommand()) { - CommandEditorManager.EditorCommand ec = CommandEditorManager.get(rewrite.mId); - if (ec == null) { - return null; - } else { - if (newText.isEmpty()) { - return ec.getOp(this, rewrite.mArgs); - } - // If is command, then the 2 ops will be combined. - List ops = new ArrayList<>(); - ops.add(getCommitWithOverwriteOp(newText, true)); - ops.add(ec.getOp(this, rewrite.mArgs)); - return combineOps(ops); - } - } - } - if (always || !text.equals(newText)) { - return getCommitWithOverwriteOp(newText, false); - } - return null; - } - - @Override - public CommandEditorResult commitFinalResult(final String text) { - CommandEditorResult result = null; - if (mRewriters == null || mRewriters.isEmpty()) { - // If rewrites/commands are not defined (default), then selection can be dictated over. - commitWithOverwrite(text, true); - } else { - final ExtractedText et = getExtractedText(); - final String selectedText = getSelectedText(); - // Try to interpret the text as a command and if it is, then apply it. - // Otherwise write out the text as usual. - UtteranceRewriter.Rewrite rewrite = applyCommand(text); - String textRewritten = rewrite.mStr; - final int len = maybeCommit(textRewritten, !selectedText.isEmpty() && rewrite.isCommand()); - // TODO: add undo for setSelection even if len==0 - if (len > 0) { - pushOpUndo(new Op("delete " + len) { - @Override - public Op run() { - mInputConnection.beginBatchEdit(); - boolean success = deleteSurrounding(len, 0); - if (et != null && selectedText.length() > 0) { - success = mInputConnection.commitText(selectedText, 1) && - mInputConnection.setSelection(et.selectionStart, et.selectionEnd); - } - mInputConnection.endBatchEdit(); - if (success) { - return NO_OP; - } - return null; - } - }); - } - boolean success = false; - if (rewrite.isCommand()) { - CommandEditorManager.EditorCommand ec = CommandEditorManager.get(rewrite.mId); - if (ec != null) { - // TODO: dont call runOp from here - success = runOp(ec.getOp(this, rewrite.mArgs)); - } - } else { - mCommandPrefix.add(textRewritten); - } - result = new CommandEditorResult(success, rewrite); - } - return result; - } - - /** - * Sets the text as "composing text" unless there is a selection. - */ - @Override - public boolean commitPartialResult(String text) { - CharSequence cs = mInputConnection.getSelectedText(0); - if (cs != null && cs.length() > 0) { - return false; - } - - String newText = text; - if (mRewriters != null && !mRewriters.isEmpty()) { - for (UtteranceRewriter ur : mRewriters) { - if (ur == null) { - continue; - } - UtteranceRewriter.Rewrite rewrite = ur.getRewrite(newText); - newText = rewrite.mStr; - if (rewrite.isCommand()) { - break; - } - } - } - commitWithOverwrite(newText, false); - return true; - } - - @Override - public CharSequence getText() { - ExtractedText et = getExtractedText(); - if (et == null) { - return null; - } - return et.text; - } - - @Override - public void reset() { - mCommandPrefix.clear(); - mTextBeforeCursor = getTextBeforeCursor(); - } - - @Override - public Op activity(final String json) { - return new Op("activity") { - @Override - public Op run() { - Op undo = null; - try { - if (IntentUtils.startActivityIfAvailable(mContext, JsonUtils.createIntent(json.replace(F_SELECTION, getSelectedText())))) { - undo = NO_OP; - } - } catch (JSONException e) { - Log.i("startSearchActivity: JSON: " + e.getMessage()); - } - return undo; - } - }; - } - - @Override - public Op getUrl(final String url, final String arg) { - return new Op("getUrl") { - @Override - public Op run() { - String selectedText = getSelectedText(); - final String url1; - if (arg != null && !arg.isEmpty()) { - url1 = url.replace(F_SELECTION, selectedText) + HttpUtils.encode(arg.replace(F_SELECTION, selectedText)); - } else { - url1 = url.replace(F_SELECTION, selectedText); - } - new AsyncTask() { - - @Override - protected String doInBackground(String... urls) { - try { - return HttpUtils.getUrl(urls[0]); - } catch (IOException e) { - return "[ERROR: Unable to retrieve " + urls[0] + ": " + e.getLocalizedMessage() + "]"; - } - } - - @Override - protected void onPostExecute(String result) { - runOp(replaceSel(result)); - } - }.execute(url1); - return Op.NO_OP; - } - }; - } - - @Override - public Op keyUp() { - return new Op("goUp") { - @Override - public Op run() { - if (mInputConnection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_UP))) { - return keyDown(); - } - return null; - } - }; - } - - @Override - public Op keyDown() { - return new Op("goDown") { - @Override - public Op run() { - if (mInputConnection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_DOWN))) { - return keyUp(); - } - return null; - } - }; - } - - @Override - public Op keyLeft() { - return new Op("goLeft") { - @Override - public Op run() { - if (mInputConnection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT))) { - return keyRight(); - } - return null; - } - }; - } - - @Override - public Op keyRight() { - return new Op("goRight") { - @Override - public Op run() { - if (mInputConnection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT))) { - return keyLeft(); - } - return null; - } - }; - } - - /** - * Returns an operation that pops the undo stack the given number of times, - * executing each popped op. If the stack does not contain the given number of elements, or - * one of the ops fails, then returns null. Otherwise returns NO_OP. - */ - @Override - public Op undo(final int steps) { - return new Op("undo") { - @Override - public Op run() { - Op undo = Op.NO_OP; - mInputConnection.beginBatchEdit(); - for (int i = 0; i < steps; i++) { - try { - Op op = mUndoStack.pop().run(); - if (op == null) { - undo = null; - break; - } - } catch (NoSuchElementException ex) { - undo = null; - break; - } - } - mInputConnection.endBatchEdit(); - return undo; - } - }; - } - - /** - * The combine operation modifies the stack by removing the top n elements and adding - * their combination instead. - * TODO: implement undo - */ - @Override - public Op combine(final int steps) { - return new Op("combine " + steps) { - @Override - public Op run() { - final Deque combination = new ArrayDeque<>(); - try { - for (int i = 0; i < steps; i++) { - combination.push(mOpStack.pop()); - } - } catch (NoSuchElementException e) { - return null; - } - mOpStack.push(combineOps(combination)); - return NO_OP; - } - }; - } - - @Override - public Op apply(final int steps) { - return new Op("apply") { - @Override - public Op run() { - Op op = mOpStack.peek(); - if (op == null) { - return null; - } - final Deque combination = new ArrayDeque<>(); - mInputConnection.beginBatchEdit(); - for (int i = 0; i < steps; i++) { - Op undo = op.run(); - if (undo == null) { - break; - } - combination.push(undo); - } - mInputConnection.endBatchEdit(); - return new Op("undo apply", combination.size()) { - - @Override - public Op run() { - mInputConnection.beginBatchEdit(); - while (!combination.isEmpty()) { - combination.pop().run(); - } - mInputConnection.endBatchEdit(); - return NO_OP; - } - }; - } - }; - } - - @Override - public Op moveAbs(final int pos) { - return new Op("moveAbs " + pos) { - @Override - public Op run() { - Op undo = null; - mInputConnection.beginBatchEdit(); - ExtractedText et = getExtractedText(); - if (et != null) { - int charPos = pos; - if (pos < 0) { - //-1 == end of text - charPos = et.text.length() + pos + 1; - } - undo = getOpSetSelection(charPos, charPos, et.selectionStart, et.selectionEnd).run(); - } - mInputConnection.endBatchEdit(); - return undo; - } - }; - } - - @Override - public Op cut() { - return getContextMenuActionOp(android.R.id.cut); - } - - @Override - public Op copy() { - return getContextMenuActionOp(android.R.id.copy); - } - - @Override - public Op paste() { - return getContextMenuActionOp(android.R.id.paste); - } - - /** - * mInputConnection.performContextMenuAction(android.R.id.selectAll) does not create a selection - */ - @Override - public Op selectAll() { - return new Op("selectAll") { - @Override - public Op run() { - Op undo = null; - mInputConnection.beginBatchEdit(); - final ExtractedText et = getExtractedText(); - if (et != null) { - undo = getOpSetSelection(0, et.text.length(), et.selectionStart, et.selectionEnd).run(); - } - mInputConnection.endBatchEdit(); - return undo; - } - }; - } - - // Not undoable - @Override - public Op cutAll() { - Collection collection = new ArrayList<>(); - collection.add(selectAll()); - collection.add(cut()); - return combineOps(collection); - } - - // Not undoable - @Override - public Op deleteAll() { - return new Op("deleteAll") { - @Override - public Op run() { - Op undo = null; - mInputConnection.beginBatchEdit(); - Op op = selectAll().run(); - if (op != null) { - if (mInputConnection.commitText("", 0)) { - undo = NO_OP; - } - } - mInputConnection.endBatchEdit(); - return undo; - } - }; - } - - // TODO: test with failing ops - @Override - public Op combineOps(final Collection ops) { - return new Op(ops.toString(), ops.size()) { - @Override - public Op run() { - final Deque combination = new ArrayDeque<>(); - mInputConnection.beginBatchEdit(); - for (Op op : ops) { - if (op != null && !op.isNoOp()) { - Op undo = op.run(); - if (undo == null) { - break; - } - combination.push(undo); - } - } - mInputConnection.endBatchEdit(); - return new Op(combination.toString(), combination.size()) { - @Override - public Op run() { - mInputConnection.beginBatchEdit(); - while (!combination.isEmpty()) { - Op undo1 = combination.pop().run(); - if (undo1 == null) { - break; - } - } - mInputConnection.endBatchEdit(); - return combineOps(ops); - } - }; - } - }; - } - - // Not undoable - @Override - public Op copyAll() { - Collection collection = new ArrayList<>(); - collection.add(selectAll()); - collection.add(copy()); - return combineOps(collection); - } - - /** - * Deletes all characters up to the leftmost whitespace from the cursor (including the whitespace). - * If something is selected then delete the selection. - */ - @Override - public Op deleteLeftWord() { - return new Op("deleteLeftWord") { - @Override - public Op run() { - Op undo = null; - boolean success = false; - mInputConnection.beginBatchEdit(); - // If something is selected then delete the selection and return - final String oldText = getSelectedText(); - if (oldText.length() > 0) { - undo = getCommitTextOp(oldText, "").run(); - } else { - final CharSequence beforeCursor = mInputConnection.getTextBeforeCursor(MAX_DELETABLE_CONTEXT, 0); - if (beforeCursor != null) { - final int beforeCursorLength = beforeCursor.length(); - Matcher m = WHITESPACE_AND_TOKEN.matcher(beforeCursor); - int lastIndex = 0; - while (m.find()) { - // If the cursor is immediately left from WHITESPACE_AND_TOKEN, then - // delete the WHITESPACE_AND_TOKEN, otherwise delete whatever is in between. - lastIndex = beforeCursorLength == m.end() ? m.start() : m.end(); - } - if (lastIndex > 0) { - success = deleteSurrounding(beforeCursorLength - lastIndex, 0); - } else if (beforeCursorLength < MAX_DELETABLE_CONTEXT) { - success = deleteSurrounding(beforeCursorLength, 0); - } - if (success) { - mInputConnection.endBatchEdit(); - final CharSequence cs = lastIndex > 0 ? beforeCursor.subSequence(lastIndex, beforeCursorLength) : beforeCursor; - undo = new Op("commitText: " + cs) { - @Override - public Op run() { - if (mInputConnection.commitText(cs, 1)) { - return NO_OP; - } - return null; - } - }; - } - } - } - mInputConnection.endBatchEdit(); - return undo; - } - }; - } - - @Override - public Op select(final String query) { - return new Op("select " + query) { - @Override - public Op run() { - Op undo = null; - mInputConnection.beginBatchEdit(); - ExtractedText et = getExtractedText(); - if (et != null) { - Pair queryResult = lastIndexOf(query.replace(F_SELECTION, getSelectedText()), et); - if (queryResult.first >= 0) { - undo = getOpSetSelection(queryResult.first, queryResult.first + queryResult.second.length(), et.selectionStart, et.selectionEnd).run(); - } - } - mInputConnection.endBatchEdit(); - return undo; - } - }; - } - - /** - * Returns the current selection wrapped in regex quotation. - */ - private CharSequence getSelectionAsRe(ExtractedText et) { - if (et.selectionStart == et.selectionEnd) { - return ""; - } - return "\\Q" + et.text.subSequence(et.selectionStart, et.selectionEnd) + "\\E"; - } - - @Override - public Op selectReBefore(final String regex) { - return new Op("selectReBefore") { - @Override - public Op run() { - Op undo = null; - mInputConnection.beginBatchEdit(); - final ExtractedText et = getExtractedText(); - if (et != null) { - CharSequence input = et.text.subSequence(0, et.selectionStart); - // 0 == last match - Pair pos = matchNth(Pattern.compile(regex.replace(F_SELECTION, getSelectionAsRe(et))), input, 0); - if (pos != null) { - undo = getOpSetSelection(pos.first, pos.second, et.selectionStart, et.selectionEnd).run(); - } - } - mInputConnection.endBatchEdit(); - return undo; - } - }; - } - - @Override - public Op selectReAfter(final String regex, final int n) { - return new Op("selectReAfter") { - @Override - public Op run() { - Op undo = null; - mInputConnection.beginBatchEdit(); - final ExtractedText et = getExtractedText(); - if (et != null) { - CharSequence input = et.text.subSequence(et.selectionEnd, et.text.length()); - // TODO: sometimes crashes with: - // StringIndexOutOfBoundsException: String index out of range: -4 - Pair pos = matchNth(Pattern.compile(regex.replace(F_SELECTION, getSelectionAsRe(et))), input, n); - if (pos != null) { - undo = getOpSetSelection(et.selectionEnd + pos.first, et.selectionEnd + pos.second, et.selectionStart, et.selectionEnd).run(); - } - } - mInputConnection.endBatchEdit(); - return undo; - } - }; - } - - @Override - public Op selectRe(final String regex, final boolean applyToSelection) { - return new Op("selectRe") { - @Override - public Op run() { - Op undo = null; - mInputConnection.beginBatchEdit(); - final ExtractedText et = getExtractedText(); - if (et != null) { - if (applyToSelection || et.selectionStart == et.selectionEnd) { - Pair pos = matchAtPos(Pattern.compile(regex), et.text, et.selectionStart, et.selectionEnd); - if (pos != null) { - undo = getOpSetSelection(pos.first, pos.second, et.selectionStart, et.selectionEnd).run(); - } - } - } - mInputConnection.endBatchEdit(); - return undo; - } - }; - } - - @Override - public Op replace(final String query, final String replacement) { - return new Op("replace") { - @Override - public Op run() { - Op undo = null; - mInputConnection.beginBatchEdit(); - final ExtractedText et = getExtractedText(); - if (et != null) { - Pair queryResult = lastIndexOf(query, et); - final CharSequence match = queryResult.second; - if (queryResult.first >= 0) { - boolean success = mInputConnection.setSelection(queryResult.first, queryResult.first); - if (success) { - // Delete existing text - success = deleteSurrounding(0, match.length()); - if (replacement.isEmpty()) { - if (success) { - undo = new Op("undo replace1") { - @Override - public Op run() { - mInputConnection.beginBatchEdit(); - boolean success2 = mInputConnection.commitText(match, 1) && - mInputConnection.setSelection(et.selectionStart, et.selectionEnd); - mInputConnection.endBatchEdit(); - if (success2) { - return NO_OP; - } - return null; - } - }; - } - } else { - success = mInputConnection.commitText(replacement, 1); - if (success) { - undo = new Op("undo replace2") { - @Override - public Op run() { - mInputConnection.beginBatchEdit(); - boolean success2 = deleteSurrounding(replacement.length(), 0) && - mInputConnection.commitText(match, 1) && - mInputConnection.setSelection(et.selectionStart, et.selectionEnd); - mInputConnection.endBatchEdit(); - if (success2) { - return NO_OP; - } - return null; - } - }; - } - } - } - } - } - mInputConnection.endBatchEdit(); - return undo; - } - }; - } - - public Op replaceSel(final String str) { - return replaceSel(str, null); - } - - /** - * Commits texts and creates a new selection (within the commited text). - * TODO: fix undo - */ - @Override - public Op replaceSel(final String str, final String regex) { - return new Op("replaceSel") { - @Override - public Op run() { - // Replace mentions of selection with a back-reference - mInputConnection.beginBatchEdit(); - // Change the current selection with the input argument, possibly embedding the selection. - String selectedText = getSelectedText(); - String newText; - if (str == null || str.isEmpty()) { - newText = ""; - } else { - newText = str.replace(F_SELECTION, selectedText); - } - Op op = null; - if (regex != null) { - Pair pair = matchNth(Pattern.compile(regex), newText, 1); - if (pair != null) { - final ExtractedText et = getExtractedText(); - // TODO: shift by the offset whenever we use getExtractedText - int oldStart = et.startOffset + et.selectionStart; - int oldEnd = et.startOffset + et.selectionEnd; - Collection collection = new ArrayList<>(); - collection.add(getCommitTextOp(selectedText, newText)); - collection.add(getOpSetSelection(oldStart + pair.first, oldStart + pair.second, oldStart, oldEnd)); - op = combineOps(collection); - } - } - // If no regex was provided or no match was found then just commit the replacement. - if (op == null) { - op = getCommitTextOp(selectedText, newText); - } - Op undo = op.run(); - mInputConnection.endBatchEdit(); - return undo; - } - }; - } - - @Override - public Op replaceSelRe(final String regex, final String repl) { - return new Op("replaceSelRe") { - @Override - public Op run() { - mInputConnection.beginBatchEdit(); - String selectedText = getSelectedText(); - String newText = selectedText.replaceAll(regex, repl); - Op undo = getCommitTextOp(selectedText, newText).run(); - mInputConnection.endBatchEdit(); - return undo; - } - }; - } - - @Override - public Op ucSel() { - return new Op("ucSel") { - @Override - public Op run() { - mInputConnection.beginBatchEdit(); - String oldText = getSelectedText(); - Op undo = getCommitTextOp(oldText, oldText.toUpperCase()).run(); - mInputConnection.endBatchEdit(); - return undo; - } - }; - } - - @Override - public Op lcSel() { - return new Op("lcSel") { - @Override - public Op run() { - mInputConnection.beginBatchEdit(); - String oldText = getSelectedText(); - Op undo = getCommitTextOp(oldText, oldText.toLowerCase()).run(); - mInputConnection.endBatchEdit(); - return undo; - } - }; - } - - @Override - public Op incSel() { - return new Op("incSel") { - @Override - public Op run() { - Op undo = null; - mInputConnection.beginBatchEdit(); - String oldText = getSelectedText(); - try { - undo = getCommitTextOp(oldText, String.valueOf(Integer.parseInt(oldText) + 1)).run(); - } catch (NumberFormatException e) { - // Intentional - } - mInputConnection.endBatchEdit(); - return undo; - } - }; - } - - // TODO: share code with deleteLeftWord - @Override - public Op deleteChars(final int numOfChars) { - return new Op("deleteChars") { - @Override - public Op run() { - Op undo = null; - boolean success; - mInputConnection.beginBatchEdit(); - // If something is selected then delete the selection and return - final String oldText = getSelectedText(); - if (oldText.length() > 0) { - undo = getCommitTextOp(oldText, "").run(); - } else { - if (numOfChars != 0) { - final int num; - final CharSequence cs; - if (numOfChars < 0) { - num = -1 * numOfChars; - cs = mInputConnection.getTextBeforeCursor(num, 0); - } else { - num = numOfChars; - cs = mInputConnection.getTextAfterCursor(num, 0); - } - if (cs != null && cs.length() == num) { - final int newCursorPos; - if (numOfChars < 0) { - success = deleteSurrounding(num, 0); - newCursorPos = 1; - } else { - success = deleteSurrounding(0, num); - newCursorPos = 0; - } - if (success) { - mInputConnection.endBatchEdit(); - undo = new Op("commitText: " + cs) { - @Override - public Op run() { - if (mInputConnection.commitText(cs, newCursorPos)) { - return NO_OP; - } - return null; - } - }; - } - } - } - } - mInputConnection.endBatchEdit(); - return undo; - } - }; - } - - /** - * Not undoable - * - * @param code key code - * @return op that sends the given code - */ - @Override - public Op keyCode(final int code) { - return new Op("keyCode") { - @Override - public Op run() { - if (mInputConnection.sendKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, code))) { - return Op.NO_OP; - } - return null; - } - }; - } - - /** - * Not undoable - * - * @param symbolicName key code's symbolic name - * @return op that sends the given code - */ - @Override - public Op keyCodeStr(String symbolicName) { - int code = KeyEvent.keyCodeFromString("KEYCODE_" + symbolicName); - if (code != KeyEvent.KEYCODE_UNKNOWN) { - return keyCode(code); - } - return null; - } - - /** - * There is no undo. - */ - @Override - public Op imeAction(int editorAction) { - // TODO: map the given Id to a label that uses the symbolic name like NEXT - return getEditorActionOp(editorAction, "IME_ACTION_" + editorAction); - } - - /** - * There is no undo, because the undo-stack does not survive the jump to another field. - */ - @Override - public Op imeActionPrevious() { - return getEditorActionOp(EditorInfo.IME_ACTION_PREVIOUS, "IME_ACTION_PREVIOUS"); - } - - /** - * There is no undo, because the undo-stack does not survive the jump to another field. - */ - @Override - public Op imeActionNext() { - return getEditorActionOp(EditorInfo.IME_ACTION_NEXT, "IME_ACTION_NEXT"); - } - - @Override - public Op imeActionDone() { - // Does not work on Google Searchbar - return getEditorActionOp(EditorInfo.IME_ACTION_DONE, "IME_ACTION_DONE"); - } - - @Override - public Op imeActionGo() { - // Works in Google Searchbar, GF Translator, but NOT in the Firefox search widget - return getEditorActionOp(EditorInfo.IME_ACTION_GO, "IME_ACTION_GO"); - } - - @Override - public Op imeActionSearch() { - return getEditorActionOp(EditorInfo.IME_ACTION_SEARCH, "IME_ACTION_SEARCH"); - } - - @Override - public Op imeActionSend() { - return getEditorActionOp(EditorInfo.IME_ACTION_SEND, "IME_ACTION_SEND"); - } - - @Override - public Deque getOpStack() { - return mOpStack; - } - - @Override - public Deque getUndoStack() { - return mUndoStack; - } - - @Override - public ExtractedText getExtractedText() { - return mInputConnection.getExtractedText(new ExtractedTextRequest(), 0); - } - - private void pushOp(Op op) { - mOpStack.push(op); - Log.i("undo: push op: " + mOpStack.toString()); - } - - private void pushOpUndo(Op op) { - mUndoStack.push(op); - Log.i("undo: push undo: " + mUndoStack.toString()); - } - - private Op getEditorActionOp(final int editorAction, String label) { - return new Op(label) { - @Override - public Op run() { - if (mInputConnection.performEditorAction(editorAction)) { - return Op.NO_OP; - } - return null; - } - }; - } - - private Op getContextMenuActionOp(final int contextMenuAction) { - return new Op("contextMenuAction " + contextMenuAction) { - @Override - public Op run() { - if (mInputConnection.performContextMenuAction(contextMenuAction)) { - return Op.NO_OP; - } - return null; - } - }; - } - - /** - * Updates the text field, modifying only the parts that have changed. - * Adds text at the cursor, possibly overwriting a selection. - * Returns the number of characters added. - */ - private int commitWithOverwrite(String text, boolean isCommitText) { - mInputConnection.beginBatchEdit(); - text = getGlue(text, mTextBeforeCursor) + capitalizeIfNeeded(text, mTextBeforeCursor); - if (isCommitText) { - mInputConnection.commitText(text, 1); - if (!text.isEmpty()) { - // This seems to work correctly, regardless of the length of the added text, - // i.e. we do not need to call (the expensive) getTextBeforeCursor() here. - mTextBeforeCursor = text; - } - } else { - // We let the editor define the style of the composing text. - //Spannable ss = new SpannableString(text); - //ss.setSpan(SPAN_PARTIAL_RESULTS, 0, text.length(), Spanned.SPAN_COMPOSING); - mInputConnection.setComposingText(text, 1); - } - mInputConnection.endBatchEdit(); - return text.length(); - } - - /** - * We look at the left context of the cursor - * to decide which glue symbol to use and whether to capitalize the text. - * Note that also the composing text (set by partial results) moves the cursor. - */ - private CharSequence getTextBeforeCursor() { - return mInputConnection.getTextBeforeCursor(MAX_DELETABLE_CONTEXT, 0); - } - - /** - * TODO: review - * we should be able to review the last N ops and undo them if they can be interpreted as - * a combined op. - */ - private Op getCommitWithOverwriteOp(final String text, final boolean isCommand) { - return new Op("add " + text) { - @Override - public Op run() { - mInputConnection.beginBatchEdit(); - final ExtractedText et = getExtractedText(); - final String selectedText = getSelectedText(); - if (text.isEmpty() && !selectedText.isEmpty() && isCommand) { - mInputConnection.endBatchEdit(); - return null; - } - final int addedLength = commitWithOverwrite(text, true); - mInputConnection.endBatchEdit(); - return new Op("delete " + addedLength) { - @Override - public Op run() { - mInputConnection.beginBatchEdit(); - boolean success = deleteSurrounding(addedLength, 0); - if (et != null && selectedText.length() > 0) { - success = mInputConnection.commitText(selectedText, 1) && - mInputConnection.setSelection(et.selectionStart, et.selectionEnd); - } - mInputConnection.endBatchEdit(); - if (success) { - return NO_OP; - } - return null; - } - }; - } - }; - } - - /** - * If there is a selection, and the input is a command with no replacement text - * then do not replace the selection with an empty string, because the command - * needs to apply to the selection, e.g. uppercase it. - * - * @param text Text to be commited - * @param condition Condition in addition to text begin empty to block the commit - * @return Length of the added text - */ - private int maybeCommit(String text, boolean condition) { - if (text.isEmpty() && condition) { - return 0; - } - return commitWithOverwrite(text, true); - } - - /** - * Op that commits a text at the cursor. If successful then an undo is returned which deletes - * the text and restores the old selection. - */ - private Op getCommitTextOp(final CharSequence oldText, final CharSequence newText) { - return new Op("commitText") { - @Override - public Op run() { - Op undo = null; - // TODO: use getSelection - final ExtractedText et = getExtractedText(); - if (mInputConnection.commitText(newText, 1)) { - undo = new Op("deleteSurroundingText+commitText") { - @Override - public Op run() { - mInputConnection.beginBatchEdit(); - boolean success = deleteSurrounding(newText.length(), 0); - if (success && oldText != null) { - success = mInputConnection.commitText(oldText, 1); - } - if (et != null && success) { - success = mInputConnection.setSelection(et.selectionStart, et.selectionEnd); - } - mInputConnection.endBatchEdit(); - if (success) { - return NO_OP; - } - return null; - } - }; - } - return undo; - } - }; - } - - private Op getOpSetSelection(final int i, final int j, final int oldSelectionStart, final int oldSelectionEnd) { - return new Op("setSelection") { - @Override - public Op run() { - Op undo = null; - if (mInputConnection.setSelection(i, j)) { - undo = new Op("setSelection") { - @Override - public Op run() { - if (mInputConnection.setSelection(oldSelectionStart, oldSelectionEnd)) { - return NO_OP; - } - return null; - } - }; - } - return undo; - } - }; - } - - /** - * Tries to match a substring before the cursor, using case-insensitive matching. - * TODO: this might not work with some Unicode characters - * - * @param query search string - * @param et text to search from - * @return pair index of the last occurrence of the match, and the matched string - */ - private Pair lastIndexOf(String query, ExtractedText et) { - int start = et.selectionStart; - query = query.toLowerCase(); - CharSequence input = et.text.subSequence(0, start); - CharSequence match = null; - int index = input.toString().toLowerCase().lastIndexOf(query); - if (index >= 0) { - match = input.subSequence(index, index + query.length()); - } - return new Pair<>(index, match); - } - - /** - * Go to the Nth match and return the indices of the 1st group in the match if available. - * If not then return the indices of the whole match. - * If the match could not be made N times the return the last match (e.g. if n == 0). - * If no match was found then return {@code null}. - * TODO: if possible, support negative N to match from end to beginning. - */ - private Pair matchNth(Pattern pattern, CharSequence input, int n) { - Matcher matcher = pattern.matcher(input); - Pair pos = null; - int end = 0; - int counter = 0; - while (matcher.find(end)) { - counter++; - int group = 0; - if (matcher.groupCount() > 0) { - group = 1; - } - pos = new Pair<>(matcher.start(group), matcher.end(group)); - if (counter == n) { - break; - } - int newEnd = matcher.end(group); - // We require the end position to increase to avoid infinite loop when matching ^. - if (newEnd <= end) { - end++; - if (end >= input.length()) { - break; - } - } else { - end = newEnd; - } - } - return pos; - } - - private Pair matchAtPos(Pattern pattern, CharSequence input, int posStart, int posEnd) { - Matcher matcher = pattern.matcher(input); - Pair pos = null; - while (matcher.find()) { - int group = 0; - if (matcher.groupCount() > 0) { - group = 1; - } - if (matcher.start(group) <= posStart) { - if (posEnd <= matcher.end(group)) { - return new Pair<>(matcher.start(group), matcher.end(group)); - } - } else { - // Stop searching if the match start only after the cursor. - return null; - } - } - return pos; - } - - private String getSelectedText() { - CharSequence cs = mInputConnection.getSelectedText(0); - if (cs == null || cs.length() == 0) { - return ""; - } - return cs.toString(); - } - - @Override - public Op moveRel(final int numOfChars) { - return move(numOfChars, 2); - } - - - @Override - public Op moveRelSel(final int numOfChars, final int type) { - return move(numOfChars, type); - } - - private Op move(final int numberOfChars, final int type) { - return new Op("moveRel") { - @Override - public Op run() { - Op undo = null; - mInputConnection.beginBatchEdit(); - ExtractedText extractedText = getExtractedText(); - if (extractedText != null) { - int newStart = extractedText.selectionStart; - int newEnd = extractedText.selectionEnd; - if (type == 0) { - newStart += numberOfChars; - } else if (type == 1) { - newEnd += numberOfChars; - } else { - if (numberOfChars < 0) { - newStart += numberOfChars; - newEnd = newStart; - } else { - newEnd += numberOfChars; - newStart = newEnd; - } - } - undo = getOpSetSelection(newStart, newEnd, extractedText.selectionStart, extractedText.selectionEnd).run(); - } - mInputConnection.endBatchEdit(); - return undo; - } - }; - } - - /** - * Check the last committed texts if they can be combined into a command. If so, then undo - * these commits and return the constructed command. - */ - private UtteranceRewriter.Rewrite applyCommand(String text) { - int len = mCommandPrefix.size(); - for (int i = Math.min(MAX_UTT_IN_COMMAND, len); i > 0; i--) { - List sublist = mCommandPrefix.subList(len - i, len); - // TODO: sometimes sublist is empty? - String possibleCommand = TextUtils.join(" ", sublist); - if (possibleCommand.isEmpty()) { - possibleCommand = text; - } else { - possibleCommand += " " + text; - } - Log.i("applyCommand: testing: <" + possibleCommand + ">"); - for (UtteranceRewriter ur : mRewriters) { - if (ur == null) { - continue; - } - UtteranceRewriter.Rewrite rewrite = ur.getRewrite(possibleCommand); - if (rewrite.isCommand()) { - Log.i("applyCommand: isCommand: " + possibleCommand); - undo(i).run(); - return rewrite; - } - } - } - // TODO: review this - String newText = text; - UtteranceRewriter.Rewrite rewrite = null; - for (UtteranceRewriter ur : mRewriters) { - if (ur == null) { - continue; - } - rewrite = ur.getRewrite(newText); - if (rewrite.isCommand()) { - return rewrite; - } - newText = rewrite.mStr; - } - if (rewrite == null) { - rewrite = new UtteranceRewriter.Rewrite(newText); - } - return rewrite; - } - - private boolean deleteSurrounding(int beforeLength, int afterLength) { - if (VERSION.SDK_INT >= VERSION_CODES.N) { - return mInputConnection.deleteSurroundingTextInCodePoints(beforeLength, afterLength); - } - return mInputConnection.deleteSurroundingText(beforeLength, afterLength); - } - - /** - * Return capitalized text if - * the last character of the trimmed left context is end-of-sentence marker. - */ - private static String capitalizeIfNeeded(String text, CharSequence leftContext) { - if (text.isEmpty()) { - return text; - } - if (requiresCap(leftContext)) { - // Since the text can start with whitespace (newline), - // we capitalize the first non-whitespace character. - int firstNonWhitespaceIndex = -1; - for (int i = 0; i < text.length(); i++) { - if (!Constants.isTransparent(text.charAt(i))) { - firstNonWhitespaceIndex = i; - break; - } - } - if (firstNonWhitespaceIndex > -1) { - String newText = text.substring(0, firstNonWhitespaceIndex) - + Character.toUpperCase(text.charAt(firstNonWhitespaceIndex)); - if (firstNonWhitespaceIndex < text.length() - 1) { - newText += text.substring(firstNonWhitespaceIndex + 1); - } - return newText; - } - } - return text; - } - - private static boolean requiresCap(CharSequence leftContext) { - if (leftContext != null) { - for (int i = leftContext.length() - 1; i >= 0; i--) { - char c = leftContext.charAt(i); - if (Constants.CHARACTERS_EOS.contains(c)) { - return true; - } - if (!Constants.isTransparent(c)) { - return false; - } - } - } - return true; - } - - /** - * Return a whitespace if - * - the 1st character of the text is not punctuation, or whitespace, etc. - * - or the previous character (last character of the left context) is not opening bracket, etc. - */ - private static String getGlue(String text, CharSequence leftContext) { - if (leftContext == null || text.isEmpty()) { - return ""; - } - char firstChar = text.charAt(0); - - // TODO: experimental: glue all 1-character strings (somewhat Estonian-specific) - if (text.length() == 1 && Character.isLetter(firstChar)) { - return ""; - } - - // Glue whitespace and punctuation - if (leftContext.length() == 0 - || Character.isWhitespace(firstChar) - || Constants.CHARACTERS_STICKY_LEFT.contains(firstChar)) { - return ""; - } - - // Glue if the previous character is "sticky", e.g. opening bracket. - char prevChar = leftContext.charAt(leftContext.length() - 1); - if (Character.isWhitespace(prevChar) - || Constants.CHARACTERS_STICKY_RIGHT.contains(prevChar)) { - return ""; - } - return " "; - } -} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/Op.java b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/Op.java deleted file mode 100644 index c4ede1198cb6bade21f46fde7a327805512735eb..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/Op.java +++ /dev/null @@ -1,36 +0,0 @@ -package ee.ioc.phon.android.speechutils.editor; - -public abstract class Op { - public static final Op NO_OP = new Op("NO_OP") { - - @Override - public Op run() { - return null; - } - }; - - private final String mId; - private final int mCount; - - public Op(String id) { - this(id, 1); - } - - public Op(String id, int count) { - mId = id; - mCount = count; - } - - public String toString() { - if (mCount == 1) { - return mId; - } - return mId + " " + mCount; - } - - public boolean isNoOp() { - return "NO_OP".equals(mId); - } - - public abstract Op run(); -} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/RuleManager.java b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/RuleManager.java deleted file mode 100644 index 464f2400d2625539f0b6635d3c8eaa00b6e5b28b..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/RuleManager.java +++ /dev/null @@ -1,221 +0,0 @@ -package ee.ioc.phon.android.speechutils.editor; - -import android.content.ComponentName; - -import androidx.annotation.NonNull; - -import java.text.SimpleDateFormat; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collections; -import java.util.List; -import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -public class RuleManager { - - public static final SimpleDateFormat DATE_FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.US); - - private Pattern mLocalePattern; - private Pattern mServicePattern; - private Pattern mAppPattern; - - private CommandMatcher mCommandMatcher; - - public RuleManager() { - } - - public Pattern getLocalePattern() { - return mLocalePattern; - } - - public Pattern getServicePattern() { - return mServicePattern; - } - - public Pattern getAppPattern() { - return mAppPattern; - } - - public void setMatchers(String locale, ComponentName service, ComponentName app) { - if (locale == null) { - mLocalePattern = null; - } else { - mLocalePattern = Pattern.compile(Pattern.quote(locale)); - } - if (service == null) { - mServicePattern = null; - } else { - mServicePattern = Pattern.compile(Pattern.quote(service.getClassName())); - } - if (app == null) { - mAppPattern = null; - } else { - mAppPattern = Pattern.compile(Pattern.quote(app.getClassName())); - } - - mCommandMatcher = CommandMatcherFactory.createCommandFilter(locale, service, app); - } - - public CommandMatcher getCommandMatcher() { - return mCommandMatcher; - } - - /** - * Adds the given editor result to the top of the given rewrite rules (as the most recent rule). - */ - public UtteranceRewriter addRecent(CommandEditorResult editorResult, String rewrites) { - UtteranceRewriter.Rewrite rewrite = editorResult.getRewrite(); - Calendar cal = Calendar.getInstance(); - String comment = DATE_FORMAT.format(cal.getTime()); - Command newCommand = makeCommand(rewrite, makeUtt(cal), comment); - List commands = addToTop(newCommand, rewrites); - return new UtteranceRewriter(commands, UtteranceRewriter.DEFAULT_HEADER_COMMAND); - } - - /** - * Adds the selection replacement command with the given text as an argument to the top of the rule list. - *

- * TODO: another option is to add it as a simple text replacement, which means that spacing and capitalization rules - * would apply to it, and follow-up rules can rewrite it: - * new Command(text, comment, mLocalePattern, mServicePattern, mAppPattern, makeUtt(cal), text, null); - * TODO: we could also have a dedicated command (instead of replaceSel) that is optimized for inserting strings - * without any support for @sel or regex replacements (and thus needs no escaping). - */ - public UtteranceRewriter addRecent(String text, String rewrites) { - Calendar cal = Calendar.getInstance(); - String comment = DATE_FORMAT.format(cal.getTime()); - return addRecent(text, makeUtt(cal), comment, rewrites); - } - - public UtteranceRewriter addRecent(String text, Pattern utt, String comment, String rewrites) { - String textEscaped = Matcher.quoteReplacement(text); - Command newCommand = new Command(text, comment, mLocalePattern, mServicePattern, mAppPattern, utt, "", CommandEditorManager.REPLACE_SEL, new String[]{textEscaped}); - List commands = addToTop(newCommand, rewrites); - return new UtteranceRewriter(commands, UtteranceRewriter.DEFAULT_HEADER_REPLACE_SEL); - } - - /** - * Adds the given editor result to the given rewrite rules. And sorts them by frequency. - * The frequency info is stored in the comment-field. - */ - public UtteranceRewriter addFrequent(CommandEditorResult editorResult, String rewritesAsStr) { - UtteranceRewriter.Rewrite rewrite = editorResult.getRewrite(); - List commands = addRuleFreq(rewrite, rewritesAsStr); - return new UtteranceRewriter(commands, UtteranceRewriter.DEFAULT_HEADER_COMMAND); - } - - /** - * Adds the given rewrite to the rule list and sorts this by frequency. - */ - private List addRuleFreq(UtteranceRewriter.Rewrite rewrite, @NonNull String rewrites) { - Command newCommand = makeCommand(rewrite, makeUtt(Calendar.getInstance()), "1"); - List oldList = new UtteranceRewriter(rewrites).getCommands(); - List newList = new ArrayList<>(); - boolean isNewCommand = true; - for (Command c : oldList) { - // TODO: do not require command to exist yet, compare directly the relevant attributes - if (isNewCommand && newCommand.equalsCommand(c) && matches(c)) { - int count = getCount(c) + 1; - newList.add(makeCommand(rewrite, c.getUtterance(), "" + count)); - isNewCommand = false; - } else { - newList.add(c); - } - } - if (isNewCommand) { - newList.add(newCommand); - } - - Collections.sort(newList, (c1, c2) -> { - return Integer.compare(getCount(c2), getCount(c1)); - }); - - return newList; - } - - private List addToTop(Command newCommand, String rewrites) { - List oldList = new UtteranceRewriter(rewrites).getCommands(); - List newList = new ArrayList<>(); - - Command oldCommand = null; - for (Command c : oldList) { - if (oldCommand == null && newCommand.equalsCommand(c) && matches(c)) { - oldCommand = c; - } else { - newList.add(c); - } - } - if (oldCommand == null) { - newList.add(0, newCommand); - } else { - // TODO: update the timestamp (comment field) - newList.add(0, oldCommand); - } - return newList; - } - - private boolean matches(Command command) { - if (mCommandMatcher == null) { - return true; - } - return mCommandMatcher.matches(command.getLocale(), command.getService(), command.getApp()); - } - - /** - * Converts a rewrite (i.e. a result of an application of a command) into a command. The new command - * uses the given utterance pattern and comment. The other parts are reused from the rewrite. - * The replacement is escaped: slashes ('\') and dollar signs ('$') will be given no special meaning. - */ - public Command makeCommand(UtteranceRewriter.Rewrite rewrite, Pattern utt, String comment) { - String repl = Matcher.quoteReplacement(rewrite.mStr); - if (rewrite.isCommand()) { - // We store the matched command, but change the utterance, comment, and the command matcher. - // TODO: review this: use the (resolved) utterance as the label instead? - String label = rewrite.getCommand().getLabel(); - if (label == null) { - label = rewrite.ppCommand(); - } - // Rewrite args is the output of command.parse, i.e. the evaluated args, but with escaping of "\" and "$". - String[] args = new String[rewrite.mArgs.length]; - for (int i = 0; i < args.length; i++) { - args[i] = Matcher.quoteReplacement(rewrite.mArgs[i]); - } - return new Command(label, comment, mLocalePattern, mServicePattern, mAppPattern, utt, repl, rewrite.mId, args); - } else { - return new Command(rewrite.mStr, comment, mLocalePattern, mServicePattern, mAppPattern, utt, repl, null); - } - } - - /** - * Extracts an integer from the comment field, assuming that the "#f" table - * stores the count in that field. - *

- * TODO: do not assume this - * - * @param command command - * @return Frequency of the command - */ - private static int getCount(Command command) { - try { - return Integer.parseInt(command.getComment()); - } catch (NumberFormatException e) { - return 0; - } - } - - /** - * Converts the given timestamp into an utterance. - * TODO: maybe generate a pattern that would contain frequency, recency etc. info, and is easier to speak - * TODO: maybe leave the utterance empty - * - * @param cal timestamp - * @return Pattern that corresponds to the timestamp - */ - public static Pattern makeUtt(Calendar cal) { - long uttId = cal.getTimeInMillis(); - String uttAsStr = "^<" + uttId + ">$"; - return Pattern.compile(uttAsStr, Constants.REWRITE_PATTERN_FLAGS); - } -} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/UtteranceRewriter.java b/app/src/main/java/ee/ioc/phon/android/speechutils/editor/UtteranceRewriter.java deleted file mode 100644 index 9c28af789c42771d2718a8f3e735cd0636aa08d8..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/editor/UtteranceRewriter.java +++ /dev/null @@ -1,560 +0,0 @@ -package ee.ioc.phon.android.speechutils.editor; - -import android.content.ContentResolver; -import android.net.Uri; -import android.text.TextUtils; -import android.util.Pair; - -import androidx.annotation.NonNull; - -import java.io.BufferedReader; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.SortedMap; -import java.util.TreeMap; -import java.util.regex.Pattern; -import java.util.regex.PatternSyntaxException; - -/** - * utterance = "go to position 1" - * pattern = ("go to position (\d+)", "moveAbs", "$1") - * command = moveAbs($1) - *

- * TODO: do not use Java split - */ -public class UtteranceRewriter { - - public static final String HEADER_LABEL = "Label"; - public static final String HEADER_COMMENT = "Comment"; - public static final String HEADER_LOCALE = "Locale"; - public static final String HEADER_SERVICE = "Service"; - public static final String HEADER_APP = "App"; - public static final String HEADER_UTTERANCE = "Utterance"; - public static final String HEADER_REPLACEMENT = "Replacement"; - public static final String HEADER_COMMAND = "Command"; - public static final String HEADER_ARG1 = "Arg1"; - public static final String HEADER_ARG2 = "Arg2"; - - private static final Set COLUMNS; - - // Support Windows and Mac line endings and consume empty lines. - private static final String RE_LINE_SEPARATOR = "[\\r\\n]+"; - - static { - Set aSet = new HashSet<>(); - aSet.add(HEADER_LABEL); - aSet.add(HEADER_COMMENT); - aSet.add(HEADER_LOCALE); - aSet.add(HEADER_SERVICE); - aSet.add(HEADER_APP); - aSet.add(HEADER_UTTERANCE); - aSet.add(HEADER_REPLACEMENT); - aSet.add(HEADER_COMMAND); - aSet.add(HEADER_ARG1); - aSet.add(HEADER_ARG2); - COLUMNS = Collections.unmodifiableSet(aSet); - } - - // All fields - public static final SortedMap DEFAULT_HEADER; - - static { - SortedMap aMap0 = new TreeMap<>(); - aMap0.put(0, HEADER_LABEL); - aMap0.put(1, HEADER_COMMENT); - aMap0.put(2, HEADER_LOCALE); - aMap0.put(3, HEADER_SERVICE); - aMap0.put(4, HEADER_APP); - aMap0.put(5, HEADER_UTTERANCE); - aMap0.put(6, HEADER_REPLACEMENT); - aMap0.put(7, HEADER_COMMAND); - aMap0.put(8, HEADER_ARG1); - aMap0.put(9, HEADER_ARG2); - - DEFAULT_HEADER = Collections.unmodifiableSortedMap(aMap0); - } - - // All fields, but every row starts with Command. - // This makes sure that rows do not start with "#" (which might be e.g. the 1st char of a Label, - // but cannot be part of a Command). As a result the parser will not - // drop any rows by treating them as comments. - public static final SortedMap DEFAULT_HEADER_COMMAND; - - static { - SortedMap aMapHeaderCommand = new TreeMap<>(); - aMapHeaderCommand.put(0, HEADER_COMMAND); - aMapHeaderCommand.put(1, HEADER_COMMENT); - aMapHeaderCommand.put(2, HEADER_LOCALE); - aMapHeaderCommand.put(3, HEADER_SERVICE); - aMapHeaderCommand.put(4, HEADER_APP); - aMapHeaderCommand.put(5, HEADER_UTTERANCE); - aMapHeaderCommand.put(6, HEADER_REPLACEMENT); - aMapHeaderCommand.put(7, HEADER_LABEL); - aMapHeaderCommand.put(8, HEADER_ARG1); - aMapHeaderCommand.put(9, HEADER_ARG2); - - DEFAULT_HEADER_COMMAND = Collections.unmodifiableSortedMap(aMapHeaderCommand); - } - - // Same as DEFAULT_HEADER_COMMAND but without REPLACEMENT and ARG2. - // This is optimized for replaceSel commands used to store clipboard items. - public static final SortedMap DEFAULT_HEADER_REPLACE_SEL; - - static { - SortedMap aMapHeaderReplaceSel = new TreeMap<>(); - aMapHeaderReplaceSel.put(0, HEADER_COMMAND); - aMapHeaderReplaceSel.put(1, HEADER_COMMENT); - aMapHeaderReplaceSel.put(2, HEADER_LOCALE); - aMapHeaderReplaceSel.put(3, HEADER_SERVICE); - aMapHeaderReplaceSel.put(4, HEADER_APP); - aMapHeaderReplaceSel.put(5, HEADER_UTTERANCE); - aMapHeaderReplaceSel.put(6, HEADER_LABEL); - aMapHeaderReplaceSel.put(7, HEADER_ARG1); - - DEFAULT_HEADER_REPLACE_SEL = Collections.unmodifiableSortedMap(aMapHeaderReplaceSel); - } - - // Only utterance - private static final SortedMap DEFAULT_HEADER_1; - - static { - SortedMap aMap1 = new TreeMap<>(); - aMap1.put(0, HEADER_UTTERANCE); - DEFAULT_HEADER_1 = Collections.unmodifiableSortedMap(aMap1); - } - - // Utterance and replacement - private static final SortedMap DEFAULT_HEADER_2; - - static { - SortedMap aMap2 = new TreeMap<>(); - aMap2.put(0, HEADER_UTTERANCE); - aMap2.put(1, HEADER_REPLACEMENT); - DEFAULT_HEADER_2 = Collections.unmodifiableSortedMap(aMap2); - } - - /** - * A class that holds a text (mStr) and its possible interpretation as a command - * with a name (mId) and a list of arguments (mArgs). - */ - public static class Rewrite { - public final String mId; - public final String mStr; - public final String[] mArgs; - public final Command mCommand; - - public Rewrite(String str) { - this(null, str, null, null); - } - - public Rewrite(String id, String str, String[] args, Command command) { - mId = id; - mStr = str; - mArgs = args; - mCommand = command; - } - - public boolean isCommand() { - return mId != null; - } - - public String getStr() { - return mStr; - } - - public Command getCommand() { - return mCommand; - } - - /** - * Returns the pretty-printed command, e.g. - * name (arg1) (arg2) - * Empty trailing arguments are dropped. - * TODO: do not drop the arguments (decide at parse time how many arguments a command has) - * - * @return pretty-printed command - */ - public String ppCommand() { - if (mArgs == null) { - return mId; - } - int len = mArgs.length; - int last = len - 1; - // Search for the last non-empty argument position - for (; last >= 0 && mArgs[last].isEmpty(); last--) ; - String pp = mId; - for (int i = 0; i <= last; i++) { - pp += " (" + mArgs[i] + ")"; - } - return pp; - } - } - - public static class CommandHolder { - // Line that starts with "#" or consists entirely of 0 or more tabs. - // Must be used with "lookingAt()". - private static final Pattern PATTERN_EMPTY_ROW = Pattern.compile("#|\t*$"); - - private final List mCommands; - private final SortedMap mHeader; - private final SortedMap mErrors = new TreeMap<>(); - - public CommandHolder() { - this(DEFAULT_HEADER_1, new ArrayList<>()); - } - - public CommandHolder(String inputHeader) { - this(inputHeader, new ArrayList<>()); - } - - public CommandHolder(SortedMap header, List commands) { - mCommands = commands; - mHeader = header; - } - - public CommandHolder(String inputHeader, List commands) { - boolean hasColumnUtterance = false; - SortedMap header = new TreeMap<>(); - List fields = new ArrayList<>(); - if (inputHeader != null && !inputHeader.isEmpty()) { - final TextUtils.StringSplitter COLUMN_SPLITTER = new TextUtils.SimpleStringSplitter('\t'); - COLUMN_SPLITTER.setString(inputHeader); - int fieldCounter = 0; - for (String columnName : COLUMN_SPLITTER) { - fields.add(columnName); - if (COLUMNS.contains(columnName)) { - header.put(fieldCounter, columnName); - if (HEADER_UTTERANCE.equals(columnName)) { - hasColumnUtterance = true; - } - } - fieldCounter++; - } - } - mCommands = commands; - // If the Utterance column is missing then assume that the - // input was without a header and interpret it as a one or two column table. - if (!hasColumnUtterance) { - if (fields.size() > 1) { - mHeader = DEFAULT_HEADER_2; - mCommands.add(0, new Command(fields.get(0), fields.get(1))); - } else if (fields.size() > 0) { - mHeader = DEFAULT_HEADER_1; - mCommands.add(0, new Command(fields.get(0), "")); - } else { - mHeader = DEFAULT_HEADER_1; - } - } else { - mHeader = header; - } - } - - public SortedMap getHeader() { - return mHeader; - } - - public String getHeaderAsStr() { - return TextUtils.join("\t", mHeader.values()); - } - - public List getCommands() { - return mCommands; - } - - public SortedMap getErrors() { - return mErrors; - } - - private boolean addCommand(Command command) { - return mCommands.add(command); - } - - private String addError(int lineNumber, String message) { - return mErrors.put(lineNumber, message); - } - - public int size() { - return mCommands.size(); - } - - /** - * Adds a line unless it consists entirely of 0 or more tabs, or starts with "#". - */ - public void addLine(String line, int lineCounter, CommandMatcher commandMatcher) { - if (PATTERN_EMPTY_ROW.matcher(line).lookingAt()) { - return; - } - try { - Command command = getCommand(getHeader(), line, commandMatcher); - if (command != null) { - addCommand(command); - } - } catch (PatternSyntaxException e) { - addError(lineCounter, e.getLocalizedMessage()); - } catch (IllegalArgumentException e) { - addError(lineCounter, e.getLocalizedMessage()); - } - } - } - - private final CommandHolder mCommandHolder; - - public UtteranceRewriter(List commands) { - mCommandHolder = new CommandHolder(DEFAULT_HEADER_2, commands); - } - - public UtteranceRewriter(List commands, String header) { - mCommandHolder = new CommandHolder(header, commands); - } - - public UtteranceRewriter(List commands, SortedMap header) { - mCommandHolder = new CommandHolder(header, commands); - } - - public UtteranceRewriter(String str, CommandMatcher commandMatcher) { - mCommandHolder = loadRewrites(str, commandMatcher); - } - - public UtteranceRewriter(String str, String header, CommandMatcher commandMatcher) { - mCommandHolder = loadRewrites(str, header, commandMatcher); - } - - public UtteranceRewriter(String str, String header) { - this(str, header, null); - } - - /** - * Loads the rewrites table from the given string. The header is detected automatically. - * Command matching is not performed. - * - * @param str - */ - public UtteranceRewriter(String str) { - this(str, (CommandMatcher) null); - } - - public UtteranceRewriter(ContentResolver contentResolver, Uri uri) throws IOException { - mCommandHolder = loadRewrites(contentResolver, uri); - } - - /** - * Rewrites and returns the given string, - * and the first matching command. - */ - public Rewrite getRewrite(String str) { - for (Command command : mCommandHolder.getCommands()) { - Pair pair = command.parse(str); - String commandId = command.getId(); - if (commandId == null || commandId.isEmpty()) { - str = pair.first; - } else if (pair.second != null) { - // If there is a full match (pair.second != null) and there is a command (commandId != null) - // then stop the search and return the command. - str = pair.first; - return new Rewrite(commandId, str, pair.second, command); - } - } - return new Rewrite(str); - } - - @NonNull - public List getCommands() { - return mCommandHolder.getCommands(); - } - - /** - * Rewrites and returns the given results. - */ - public List rewrite(List results) { - List rewrittenResults = new ArrayList<>(); - for (String result : results) { - rewrittenResults.add(getRewrite(result).mStr); - } - return rewrittenResults; - } - - /** - * Serializes the rewrites as tab-separated-values. - */ - public String toTsv() { - StringBuilder stringBuilder = new StringBuilder(); - SortedMap header = mCommandHolder.getHeader(); - stringBuilder.append(mCommandHolder.getHeaderAsStr()); - for (Command command : mCommandHolder.getCommands()) { - stringBuilder.append('\n'); - stringBuilder.append(command.toTsv(header)); - } - return stringBuilder.toString(); - } - - public CommandHolder getCommandHolder() { - return mCommandHolder; - } - - public String[] getErrorsAsStringArray() { - SortedMap errors = mCommandHolder.getErrors(); - String[] array = new String[errors.size()]; - int i = 0; - for (SortedMap.Entry entry : errors.entrySet()) { - array[i++] = "#" + entry.getKey() + ": " + entry.getValue(); - } - return array; - } - - /** - * Loads the rewrites from a string of tab-separated values, - * guessing the header from the string itself. - */ - private static CommandHolder loadRewrites(String str, CommandMatcher commandMatcher) { - if (str == null) { - return new CommandHolder(); - } - String[] rows = str.split(RE_LINE_SEPARATOR); - int length = rows.length; - if (length == 0) { - return new CommandHolder(); - } - CommandHolder commandHolder = new CommandHolder(rows[0]); - if (length > 1) { - for (int i = 1; i < length; i++) { - commandHolder.addLine(rows[i], i, commandMatcher); - } - } - return commandHolder; - } - - /** - * Loads the rewrites from a string of tab-separated values. - * The header is given by a separate argument, the table must not - * contain the header. - */ - private static CommandHolder loadRewrites(String str, String header, CommandMatcher commandMatcher) { - CommandHolder commandHolder = new CommandHolder(header); - String[] rows = str.split(RE_LINE_SEPARATOR); - if (rows.length > 0) { - for (int i = 0; i < rows.length; i++) { - commandHolder.addLine(rows[i], i, commandMatcher); - } - } - return commandHolder; - } - - /** - * Loads the rewrites from an URI using a ContentResolver. - * The first line is a header. - * Non-header lines are ignored if they start with '#'. - */ - private static CommandHolder loadRewrites(ContentResolver contentResolver, Uri uri) throws IOException { - CommandHolder commandHolder = null; - InputStream inputStream = contentResolver.openInputStream(uri); - if (inputStream != null) { - BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream)); - String line = reader.readLine(); - if (line != null) { - int lineCounter = 0; - commandHolder = new CommandHolder(line); - while ((line = reader.readLine()) != null) { - lineCounter++; - commandHolder.addLine(line, lineCounter, null); - } - } - inputStream.close(); - } - if (commandHolder == null) { - return new CommandHolder(); - } - return commandHolder; - } - - /** - * Creates a command based on the given fields. - * For some fields the whitespace is trimmed from the beginning and end. - * - * @param header parsed header - * @param line single row - * @param commandMatcher command matcher - * @return command or null if commandMatcher rejects the command - */ - private static Command getCommand(SortedMap header, String line, CommandMatcher commandMatcher) { - String label = null; - String comment = null; - Pattern locale = null; - Pattern service = null; - Pattern app = null; - Pattern utterance = null; - String command = null; - String replacement = null; - String arg1 = null; - String arg2 = null; - - final TextUtils.StringSplitter columnSplitter = new TextUtils.SimpleStringSplitter('\t'); - columnSplitter.setString(line); - - int i = 0; - for (String split : columnSplitter) { - String colName = header.get(i++); - if (colName == null) { - continue; - } - switch (colName) { - case HEADER_LABEL: - label = split; - break; - case HEADER_COMMENT: - comment = split.trim(); - break; - case HEADER_LOCALE: - locale = Pattern.compile(split.trim()); - break; - case HEADER_SERVICE: - service = Pattern.compile(split.trim()); - break; - case HEADER_APP: - app = Pattern.compile(split.trim()); - break; - case HEADER_UTTERANCE: - split = split.trim(); - if (split.isEmpty()) { - throw new IllegalArgumentException("Empty Utterance"); - } - utterance = Pattern.compile(split, Constants.REWRITE_PATTERN_FLAGS); - break; - case HEADER_REPLACEMENT: - replacement = Command.unescape(split); - break; - case HEADER_COMMAND: - command = Command.unescape(split.trim()); - break; - case HEADER_ARG1: - arg1 = Command.unescape(split); - break; - case HEADER_ARG2: - arg2 = Command.unescape(split); - break; - default: - // Columns with undefined names are ignored - break; - } - } - - if (commandMatcher != null && !commandMatcher.matches(locale, service, app)) { - return null; - } - - if (arg1 == null) { - return new Command(label, comment, locale, service, app, utterance, replacement, command); - } - - if (arg2 == null) { - return new Command(label, comment, locale, service, app, utterance, replacement, command, new String[]{arg1}); - } - - return new Command(label, comment, locale, service, app, utterance, replacement, command, new String[]{arg1, arg2}); - } -} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/service/AbstractRecognitionService.java b/app/src/main/java/ee/ioc/phon/android/speechutils/service/AbstractRecognitionService.java deleted file mode 100644 index bc6eff834022c9b398a66a83e81b31d1aec39c3d..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/service/AbstractRecognitionService.java +++ /dev/null @@ -1,450 +0,0 @@ -package ee.ioc.phon.android.speechutils.service; - -import android.content.Intent; -import android.content.SharedPreferences; -import android.os.Build; -import android.os.Bundle; -import android.os.Handler; -import android.os.RemoteException; -import android.os.SystemClock; -import android.preference.PreferenceManager; -import android.speech.RecognitionService; -import android.speech.SpeechRecognizer; - -import java.io.IOException; -import java.util.ArrayList; - -import ee.ioc.phon.android.speechutils.AudioCue; -import ee.ioc.phon.android.speechutils.AudioPauser; -import ee.ioc.phon.android.speechutils.AudioRecorder; -import ee.ioc.phon.android.speechutils.EncodedAudioRecorder; -import ee.ioc.phon.android.speechutils.Extras; -import ee.ioc.phon.android.speechutils.Log; -import ee.ioc.phon.android.speechutils.RawAudioRecorder; -import ee.ioc.phon.android.speechutils.utils.PreferenceUtils; - -/** - * Performs audio recording and is meant for cloud services. - * About RemoteException see - * http://stackoverflow.com/questions/3156389/android-remoteexceptions-and-services - */ -public abstract class AbstractRecognitionService extends RecognitionService { - - // Check the volume 10 times a second - private static final int TASK_INTERVAL_VOL = 100; - // Wait for 1/2 sec before starting to measure the volume - private static final int TASK_DELAY_VOL = 500; - - private static final int TASK_INTERVAL_STOP = 1000; - private static final int TASK_DELAY_STOP = 1000; - - private AudioCue mAudioCue; - private AudioPauser mAudioPauser; - private RecognitionService.Callback mListener; - - private AudioRecorder mRecorder; - - private Handler mVolumeHandler = new Handler(); - private Runnable mShowVolumeTask; - - private Handler mStopHandler = new Handler(); - private Runnable mStopTask; - - private Bundle mExtras; - - protected static Bundle toResultsBundle(String hypothesis) { - ArrayList hypotheses = new ArrayList<>(); - hypotheses.add(hypothesis); - Bundle bundle = new Bundle(); - bundle.putStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION, hypotheses); - return bundle; - } - - protected static Bundle toResultsBundle(ArrayList hypotheses, boolean isFinal) { - Bundle bundle = new Bundle(); - bundle.putStringArrayList(SpeechRecognizer.RESULTS_RECOGNITION, hypotheses); - bundle.putBoolean(Extras.EXTRA_SEMI_FINAL, isFinal); - return bundle; - } - - /** - * Configures the service based on the given intent extras. - * Can result in an IOException, e.g. if building the server URL fails - * (UnsupportedEncodingException, MalformedURLException). - * TODO: generalize the exception - */ - protected abstract void configure(Intent recognizerIntent) throws IOException; - - /** - * Start sending audio to the server. - */ - protected abstract void connect(); - - /** - * Stop sending audio to the server. - */ - protected abstract void disconnect(); - - /** - * Returns the type of encoder to use. Subclasses must override this method if they want to - * record in a non-raw format. - * - * @return type of encoder as string (e.g. "audio/x-flac") - */ - protected String getEncoderType() { - return null; - } - - /** - * @return Audio recorder - */ - protected AudioRecorder getAudioRecorder() throws IOException { - if (mRecorder == null) { - mRecorder = createAudioRecorder(getEncoderType(), getSampleRate()); - } - return mRecorder; - } - - /** - * Queries the preferences to find out if audio cues are switched on. - * Different services can have different preferences. - */ - protected boolean isAudioCues() { - return false; - } - - /** - * Gets the sample rate used in the recorder. - * Different services can use a different sample rate. - */ - protected int getSampleRate() { - return 16000; - } - - /** - * Gets the max number of milliseconds to record. - */ - protected int getAutoStopAfterMillis() { - return 1000 * 10000; // We record as long as the server allows - } - - /** - * Stop after a pause is detected. - * This can be implemented either in the server or in the app. - */ - protected boolean isAutoStopAfterPause() { - return false; - } - - /** - * Tasks done after the recording has finished and the audio has been obtained. - */ - protected void afterRecording(byte[] recording) { - // Nothing to do, e.g. if the audio has already been sent to the server during recording - } - - // TODO: remove this, we have already getAudioRecorder - protected AudioRecorder getRecorder() { - return mRecorder; - } - - protected SharedPreferences getSharedPreferences() { - return PreferenceManager.getDefaultSharedPreferences(this); - } - - public void onDestroy() { - super.onDestroy(); - disconnectAndStopRecording(); - } - - /** - * Starts recording and opens the connection to the server to start sending the recorded packages. - */ - @Override - protected void onStartListening(final Intent recognizerIntent, RecognitionService.Callback listener) { - mListener = listener; - Log.i("onStartListening"); - - mExtras = recognizerIntent.getExtras(); - if (mExtras == null) { - mExtras = new Bundle(); - } - - if (mExtras.containsKey(Extras.EXTRA_AUDIO_CUES)) { - setAudioCuesEnabled(mExtras.getBoolean(Extras.EXTRA_AUDIO_CUES)); - } else { - setAudioCuesEnabled(isAudioCues()); - } - - try { - configure(recognizerIntent); - } catch (IOException e) { - onError(SpeechRecognizer.ERROR_CLIENT); - return; - } - - mAudioPauser = AudioPauser.createAudioPauser(this, true); - Log.i("AudioPauser can mute stream: " + mAudioPauser.isMuteStream()); - mAudioPauser.pause(); - - try { - onReadyForSpeech(new Bundle()); - startRecord(); - } catch (IOException e) { - onError(SpeechRecognizer.ERROR_AUDIO); - return; - } - - onBeginningOfSpeech(); - connect(); - } - - /** - * Stops the recording and informs the server that no more packages are coming. - */ - @Override - protected void onStopListening(RecognitionService.Callback listener) { - Log.i("onStopListening"); - onEndOfSpeech(); - } - - /** - * Stops the recording and closes the connection to the server. - */ - @Override - protected void onCancel(RecognitionService.Callback listener) { - Log.i("onCancel"); - disconnectAndStopRecording(); - // Send empty results if recognition is cancelled - // TEST: if it works with Google Translate and Slide IT - onResults(new Bundle()); - } - - - /** - * Calls onError(SpeechRecognizer.ERROR_SPEECH_TIMEOUT) if server initiates close - * without having received EOS. Otherwise simply shuts down the recorder and recognizer service. - * - * @param isEosSent true iff EOS was sent - */ - public void handleFinish(boolean isEosSent) { - if (isEosSent) { - onCancel(mListener); - } else { - onError(SpeechRecognizer.ERROR_SPEECH_TIMEOUT); - } - } - - protected Bundle getExtras() { - return mExtras; - } - - protected void onReadyForSpeech(Bundle bundle) { - if (mAudioCue != null) mAudioCue.playStartSoundAndSleep(); - try { - mListener.readyForSpeech(bundle); - } catch (RemoteException e) { - // Ignored - } - } - - protected void onRmsChanged(float rms) { - try { - mListener.rmsChanged(rms); - } catch (RemoteException e) { - // Ignored - } - } - - protected void onError(int errorCode) { - disconnectAndStopRecording(); - if (mAudioCue != null) mAudioCue.playErrorSound(); - try { - mListener.error(errorCode); - } catch (RemoteException e) { - // Ignored - } - } - - protected void onResults(Bundle bundle) { - disconnectAndStopRecording(); - try { - mListener.results(bundle); - } catch (RemoteException e) { - // Ignored - } - } - - protected void onPartialResults(Bundle bundle) { - try { - mListener.partialResults(bundle); - } catch (RemoteException e) { - // Ignored - } - } - - protected void onBeginningOfSpeech() { - try { - mListener.beginningOfSpeech(); - } catch (RemoteException e) { - // Ignored - } - } - - /** - * Fires the endOfSpeech callback, provided that the recorder is currently recording. - */ - protected void onEndOfSpeech() { - if (mRecorder == null || mRecorder.getState() != AudioRecorder.State.RECORDING) { - return; - } - byte[] recording; - - // TODO: make sure this call does not do too much work in the case of the - // WebSocket-service which does not use the bytes in the end - if (mRecorder instanceof EncodedAudioRecorder) { - recording = ((EncodedAudioRecorder) mRecorder).consumeRecordingEnc(); - } else { - recording = mRecorder.consumeRecording(); - } - - stopRecording0(); - if (mAudioCue != null) { - mAudioCue.playStopSound(); - } - try { - mListener.endOfSpeech(); - } catch (RemoteException e) { - // Ignored - } - afterRecording(recording); - } - - protected void onBufferReceived(byte[] buffer) { - try { - mListener.bufferReceived(buffer); - } catch (RemoteException e) { - // Ignored - } - } - - /** - * Return the server URL specified by the caller, or if this is missing then the URL - * stored in the preferences, or if this is missing then the default URL. - * - * @param key preference key to the server URL - * @param defaultValue default URL to use if no URL is stored at the given key - * @return server URL as string - */ - protected String getServerUrl(int key, int defaultValue) { - String url = getExtras().getString(Extras.EXTRA_SERVER_URL); - if (url == null) { - return PreferenceUtils.getPrefString( - getSharedPreferences(), - getResources(), - key, - defaultValue); - } - return url; - } - - /** - * Constructs a recorder based on the encoder type and sample rate. By default returns the raw - * audio recorder. If an unsupported encoder is specified then throws an exception. - */ - protected static AudioRecorder createAudioRecorder(String encoderType, int sampleRate) throws IOException { - // TODO: take from an enum - if ("audio/x-flac".equals(encoderType)) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - return new EncodedAudioRecorder(sampleRate); - } - throw new IOException(encoderType + " not supported"); - } - return new RawAudioRecorder(sampleRate); - } - - - /** - * Starts recording. - * - * @throws IOException if there was an error, e.g. another app is currently recording - */ - private void startRecord() throws IOException { - mRecorder = getAudioRecorder(); - if (mRecorder.getState() == AudioRecorder.State.ERROR) { - throw new IOException(); - } - - if (mRecorder.getState() != AudioRecorder.State.READY) { - throw new IOException(); - } - - mRecorder.start(); - - if (mRecorder.getState() != AudioRecorder.State.RECORDING) { - throw new IOException(); - } - - // Monitor the volume level - mShowVolumeTask = new Runnable() { - public void run() { - if (mRecorder != null) { - onRmsChanged(mRecorder.getRmsdb()); - mVolumeHandler.postDelayed(this, TASK_INTERVAL_VOL); - } - } - }; - - mVolumeHandler.postDelayed(mShowVolumeTask, TASK_DELAY_VOL); - - // Time (in milliseconds since the boot) when the recording is going to be stopped - final long timeToFinish = SystemClock.uptimeMillis() + getAutoStopAfterMillis(); - final boolean isAutoStopAfterPause = isAutoStopAfterPause(); - - // Check if we should stop recording - mStopTask = new Runnable() { - public void run() { - if (mRecorder != null) { - if (timeToFinish < SystemClock.uptimeMillis() || isAutoStopAfterPause && mRecorder.isPausing()) { - onEndOfSpeech(); - } else { - mStopHandler.postDelayed(this, TASK_INTERVAL_STOP); - } - } - } - }; - - mStopHandler.postDelayed(mStopTask, TASK_DELAY_STOP); - } - - - private void stopRecording0() { - releaseRecorder(); - if (mVolumeHandler != null) mVolumeHandler.removeCallbacks(mShowVolumeTask); - if (mStopHandler != null) mStopHandler.removeCallbacks(mStopTask); - if (mAudioPauser != null) mAudioPauser.resume(); - } - - - private void releaseRecorder() { - if (mRecorder != null) { - mRecorder.release(); - mRecorder = null; - } - } - - - private void setAudioCuesEnabled(boolean enabled) { - if (enabled) { - mAudioCue = new AudioCue(this); - } else { - mAudioCue = null; - } - } - - - private void disconnectAndStopRecording() { - disconnect(); - stopRecording0(); - } -} diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/utils/AudioUtils.java b/app/src/main/java/ee/ioc/phon/android/speechutils/utils/AudioUtils.java deleted file mode 100644 index 78e8a4f608301a0f00bade2b1c3a932de2796bdc..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/utils/AudioUtils.java +++ /dev/null @@ -1,233 +0,0 @@ -package ee.ioc.phon.android.speechutils.utils; - -import android.annotation.TargetApi; -import android.media.MediaCodec; -import android.media.MediaCodecInfo; -import android.media.MediaCodecList; -import android.media.MediaFormat; -import android.os.Build; -import android.text.TextUtils; - -import org.apache.commons.io.FileUtils; - -import java.io.File; -import java.io.IOException; -import java.io.RandomAccessFile; -import java.util.ArrayList; -import java.util.Collections; -import java.util.LinkedList; -import java.util.List; - -import ee.ioc.phon.android.speechutils.Log; -import ee.ioc.phon.android.speechutils.MediaFormatFactory; - -public class AudioUtils { - - private AudioUtils() { - } - - public static void saveWavToFile(String wavFileFullPath, byte[] wav, boolean append) { - try { - FileUtils.writeByteArrayToFile(new File(wavFileFullPath), wav, append); - } catch (IOException e) { - Log.e("Could not save a recording to " + wavFileFullPath + " due to: " + e.getMessage()); - } - } - - public static void saveWavHeaderToFile(String wavFileFullPath, byte[] wavHeader) { - try { - RandomAccessFile file = new RandomAccessFile(wavFileFullPath, "rw"); - file.seek(0L); - file.write(wavHeader); - file.close(); - } catch (Throwable t) { - Log.e("Could not write/rewrite the wav header to " + wavFileFullPath + " due to: " + t.getMessage()); - } - } - - public static byte[] getWavHeader(int totalAudioLen, int sampleRate, short resolutionInBytes, short channels) { - int headerLen = 44; - int byteRate = sampleRate * resolutionInBytes; // sampleRate*(16/8)*1 ??? - int totalDataLen = totalAudioLen + headerLen; - - byte[] header = new byte[headerLen]; - - header[0] = 'R'; // RIFF/WAVE header - header[1] = 'I'; - header[2] = 'F'; - header[3] = 'F'; - header[4] = (byte) (totalDataLen & 0xff); - header[5] = (byte) ((totalDataLen >> 8) & 0xff); - header[6] = (byte) ((totalDataLen >> 16) & 0xff); - header[7] = (byte) ((totalDataLen >> 24) & 0xff); - header[8] = 'W'; - header[9] = 'A'; - header[10] = 'V'; - header[11] = 'E'; - header[12] = 'f'; // 'fmt ' chunk - header[13] = 'm'; - header[14] = 't'; - header[15] = ' '; - header[16] = 16; // 4 bytes: size of 'fmt ' chunk - header[17] = 0; - header[18] = 0; - header[19] = 0; - header[20] = 1; // format = 1 - header[21] = 0; - header[22] = (byte) channels; - header[23] = 0; - header[24] = (byte) (sampleRate & 0xff); - header[25] = (byte) ((sampleRate >> 8) & 0xff); - header[26] = (byte) ((sampleRate >> 16) & 0xff); - header[27] = (byte) ((sampleRate >> 24) & 0xff); - header[28] = (byte) (byteRate & 0xff); - header[29] = (byte) ((byteRate >> 8) & 0xff); - header[30] = (byte) ((byteRate >> 16) & 0xff); - header[31] = (byte) ((byteRate >> 24) & 0xff); - header[32] = (byte) (2 * 16 / 8); // block align - header[33] = 0; - header[34] = (byte) (8 * resolutionInBytes); // bits per sample - header[35] = 0; - header[36] = 'd'; - header[37] = 'a'; - header[38] = 't'; - header[39] = 'a'; - header[40] = (byte) (totalAudioLen & 0xff); - header[41] = (byte) ((totalAudioLen >> 8) & 0xff); - header[42] = (byte) ((totalAudioLen >> 16) & 0xff); - header[43] = (byte) ((totalAudioLen >> 24) & 0xff); - - return header; - } - - public static byte[] getRecordingAsWav(byte[] pcm, int sampleRate, short resolutionInBytes, short channels) { - byte[] header = getWavHeader(pcm.length, sampleRate, resolutionInBytes, channels); - byte[] wav = new byte[header.length + pcm.length]; - System.arraycopy(header, 0, wav, 0, header.length); - System.arraycopy(pcm, 0, wav, header.length, pcm.length); - return wav; - } - - // TODO: use MediaFormat.MIMETYPE_AUDIO_FLAC) on API>=21 - @TargetApi(Build.VERSION_CODES.LOLLIPOP) - public static List getAvailableEncoders(String mime, int sampleRate) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - MediaFormat format = MediaFormatFactory.createMediaFormat(mime, sampleRate); - MediaCodecList mcl = new MediaCodecList(MediaCodecList.REGULAR_CODECS); - String encoderAsStr = mcl.findEncoderForFormat(format); - List encoders = new ArrayList<>(); - for (MediaCodecInfo info : mcl.getCodecInfos()) { - if (info.isEncoder()) { - String name = info.getName(); - String infoAsStr = name + ": " + TextUtils.join(", ", info.getSupportedTypes()); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - infoAsStr += String.format(": %s/%s/%s/%s", info.isHardwareAccelerated(), info.isSoftwareOnly(), info.isAlias(), info.isVendor()); - } - if (name.equals(encoderAsStr)) { - infoAsStr = '#' + infoAsStr; - } - encoders.add(infoAsStr); - } - } - return encoders; - } - return Collections.emptyList(); - } - - /** - * Maps the given mime type to a list of names of suitable codecs. - * Only OMX-codecs are considered. - */ - @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - public static List getEncoderNamesForType(String mime) { - LinkedList names = new LinkedList<>(); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - int n = MediaCodecList.getCodecCount(); - for (int i = 0; i < n; ++i) { - MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i); - if (!info.isEncoder()) { - continue; - } - // TODO: do we still need this? - if (!info.getName().startsWith("OMX.")) { - // Unfortunately for legacy reasons, "AACEncoder", a - // non OMX component had to be in this list for the video - // editor code to work... but it cannot actually be instantiated - // using MediaCodec. - Log.i("skipping '" + info.getName() + "'."); - continue; - } - String[] supportedTypes = info.getSupportedTypes(); - for (String type : supportedTypes) { - if (type.equalsIgnoreCase(mime)) { - names.push(info.getName()); - break; - } - } - } - } - // Return an empty list if API is too old - // TODO: maybe return null or throw exception - return names; - } - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - public static MediaCodec createCodec(String componentName, MediaFormat format) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - try { - MediaCodec codec = MediaCodec.createByCodecName(componentName); - codec.configure(format, null, null, MediaCodec.CONFIGURE_FLAG_ENCODE); - return codec; - } catch (IllegalStateException e) { - Log.e("codec '" + componentName + "' failed configuration."); - } catch (IOException e) { - Log.e("codec '" + componentName + "' failed configuration."); - } - } - return null; - } - - @TargetApi(Build.VERSION_CODES.JELLY_BEAN) - public static void showMetrics(MediaFormat format, int numBytesSubmitted, int numBytesDequeued) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - Log.i("queued a total of " + numBytesSubmitted + " bytes, " + "dequeued " + numBytesDequeued + " bytes."); - int sampleRate = format.getInteger(MediaFormat.KEY_SAMPLE_RATE); - int channelCount = format.getInteger(MediaFormat.KEY_CHANNEL_COUNT); - int inBitrate = sampleRate * channelCount * 16; // bit/sec - int outBitrate = format.getInteger(MediaFormat.KEY_BIT_RATE); - float desiredRatio = (float) outBitrate / (float) inBitrate; - float actualRatio = (float) numBytesDequeued / (float) numBytesSubmitted; - Log.i("desiredRatio = " + desiredRatio + ", actualRatio = " + actualRatio); - } - } - - public static byte[] concatenateBuffers(List buffers) { - byte[] buffersConcatenated; - int sum = 0; - for (byte[] ba : buffers) { - sum = sum + ba.length; - } - buffersConcatenated = new byte[sum]; - int pos = 0; - for (byte[] ba : buffers) { - System.arraycopy(ba, 0, buffersConcatenated, pos, ba.length); - pos = pos + ba.length; - } - return buffersConcatenated; - } - - /** - * Just for testing... - */ - public static void showSomeBytes(String tag, byte[] bytes) { - Log.i("enc: " + tag + ": length: " + bytes.length); - String str = ""; - int len = bytes.length; - if (len > 0) { - for (int i = 0; i < len && i < 5; i++) { - str += Integer.toHexString(bytes[i]) + " "; - } - Log.i("enc: " + tag + ": hex: " + str); - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/utils/BundleUtils.java b/app/src/main/java/ee/ioc/phon/android/speechutils/utils/BundleUtils.java deleted file mode 100644 index f2e31a665fdccd91b2ff5b278428ff00b7265471..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/utils/BundleUtils.java +++ /dev/null @@ -1,95 +0,0 @@ -package ee.ioc.phon.android.speechutils.utils; - -import android.os.Bundle; - -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class BundleUtils { - - private BundleUtils() { - } - - public static List ppBundle(Bundle bundle) { - return ppBundle("/", bundle); - } - - - private static List ppBundle(String bundleName, Bundle bundle) { - List strings = new ArrayList<>(); - if (bundle == null) { - return strings; - } - for (String key : bundle.keySet()) { - Object value = bundle.get(key); - String name = bundleName + key; - if (value instanceof Bundle) { - strings.addAll(ppBundle(name + "/", (Bundle) value)); - } else { - if (value instanceof Object[]) { - strings.add(name + ": " + Arrays.toString((Object[]) value)); - } else if (value instanceof float[]) { - strings.add(name + ": " + Arrays.toString((float[]) value)); - } else { - strings.add(name + ": " + value); - } - } - } - return strings; - } - - - /** - *

Traverses the given bundle looking for the given key. The search also - * looks into embedded bundles and thus differs from {@code Bundle.get(String)}. - * Returns the first found entry as an object. If the given bundle does not - * contain the given key then returns {@code null}.

- * - * @param bundle bundle (e.g. intent extras) - * @param key key of a bundle entry (possibly in an embedded bundle) - * @return first matching key's value - */ - public static Object getBundleValue(Bundle bundle, String key) { - if (bundle == null) { - return null; - } - for (String k : bundle.keySet()) { - Object value = bundle.get(k); - if (value instanceof Bundle) { - Object deepValue = getBundleValue((Bundle) value, key); - if (deepValue != null) { - return deepValue; - } - } else if (key.equals(k)) { - return value; - } - } - return null; - } - - /** - * @param bundle bundle that is assumed to contain just strings - * @return map of strings generated from the given bundle - */ - public static Map getBundleAsMapOfString(Bundle bundle) { - if (bundle == null) { - return null; - } - Map map = new HashMap<>(); - for (String key : bundle.keySet()) { - map.put(key, bundle.getString(key)); - } - return map; - } - - public static Bundle createResultsBundle(String hypothesis) { - ArrayList hypotheses = new ArrayList<>(); - hypotheses.add(hypothesis); - Bundle bundle = new Bundle(); - bundle.putStringArrayList(android.speech.SpeechRecognizer.RESULTS_RECOGNITION, hypotheses); - return bundle; - } -} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/utils/HttpUtils.java b/app/src/main/java/ee/ioc/phon/android/speechutils/utils/HttpUtils.java deleted file mode 100644 index ed0e93ca4b1ea1fbe4d6c4b65c12e636b0b85d82..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/utils/HttpUtils.java +++ /dev/null @@ -1,110 +0,0 @@ -package ee.ioc.phon.android.speechutils.utils; - -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStream; -import java.io.Reader; -import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLEncoder; -import java.util.Map; - -import ee.ioc.phon.android.speechutils.Log; - -public class HttpUtils { - - // Timeouts in milliseconds - // TODO: make settable - private static final int DEFAULT_READ_TIMEOUT = 3000; - private static final int DEFAULT_CONNECT_TIMEOUT = 4000; - - private HttpUtils() { - } - - public static String getUrl(String url) throws IOException { - return fetchUrl(url, "GET", null); - } - - public static String fetchUrl(String myurl, String method, String body) throws IOException { - return fetchUrl(myurl, method, body, null); - } - - public static String fetchUrl(String myurl, String method, String body, Map properties) - throws IOException { - byte[] outputInBytes = null; - - if (body != null) { - outputInBytes = body.getBytes("UTF-8"); - } - - InputStream is = null; - - try { - URL url = new URL(myurl); - HttpURLConnection conn = (HttpURLConnection) url.openConnection(); - conn.setReadTimeout(DEFAULT_READ_TIMEOUT); - conn.setConnectTimeout(DEFAULT_CONNECT_TIMEOUT); - conn.setRequestMethod(method); - if (properties != null) { - for (Map.Entry entry : properties.entrySet()) { - String value = entry.getValue(); - if (value != null) { - conn.setRequestProperty(entry.getKey(), value); - } - } - } - conn.setDoInput(true); - - if (outputInBytes == null) { - conn.connect(); - } else { - conn.setDoOutput(true); - OutputStream os = conn.getOutputStream(); - os.write(outputInBytes); - os.close(); - } - - // TODO: improve handling of response code - //int response = conn.getResponseCode(); - is = conn.getInputStream(); - return inputStreamToString(is, 1024); - } finally { - closeQuietly(is); - } - } - - public static String encode(String text) { - try { - return URLEncoder.encode(text, "UTF-8"); - } catch (UnsupportedEncodingException e) { - // This should never happen - return ""; - } - } - - private static void closeQuietly(InputStream is) { - try { - if (is != null) { - is.close(); - } - } catch (Exception e) { - Log.i(e.getMessage()); - } - } - - private static String inputStreamToString(final InputStream is, final int bufferSize) throws IOException { - final char[] buffer = new char[bufferSize]; - final StringBuilder out = new StringBuilder(); - Reader in = new InputStreamReader(is, "UTF-8"); - while (true) { - int rsz = in.read(buffer, 0, buffer.length); - if (rsz < 0) { - break; - } - out.append(buffer, 0, rsz); - } - return out.toString(); - } -} diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/utils/IntentUtils.java b/app/src/main/java/ee/ioc/phon/android/speechutils/utils/IntentUtils.java deleted file mode 100644 index ccce89c25681269150221877f9ad1a72b51083e8..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/utils/IntentUtils.java +++ /dev/null @@ -1,319 +0,0 @@ -package ee.ioc.phon.android.speechutils.utils; - -import android.app.Activity; -import android.app.PendingIntent; -import android.app.SearchManager; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ResolveInfo; -import android.os.Build; -import android.os.Bundle; -import android.os.Parcelable; -import android.speech.RecognitionService; -import android.speech.RecognizerIntent; -import android.speech.SpeechRecognizer; -import android.util.SparseIntArray; -import android.widget.Toast; - -import androidx.annotation.NonNull; - -import org.json.JSONException; - -import java.util.ArrayList; -import java.util.List; - -import ee.ioc.phon.android.speechutils.Extras; -import ee.ioc.phon.android.speechutils.Log; -import ee.ioc.phon.android.speechutils.R; -import ee.ioc.phon.android.speechutils.editor.Command; -import ee.ioc.phon.android.speechutils.editor.UtteranceRewriter; - -public final class IntentUtils { - - private IntentUtils() { - } - - /** - * @return table that maps SpeechRecognizer error codes to RecognizerIntent error codes - */ - public static SparseIntArray createErrorCodesServiceToIntent() { - SparseIntArray errorCodes = new SparseIntArray(); - errorCodes.put(SpeechRecognizer.ERROR_AUDIO, RecognizerIntent.RESULT_AUDIO_ERROR); - errorCodes.put(SpeechRecognizer.ERROR_CLIENT, RecognizerIntent.RESULT_CLIENT_ERROR); - errorCodes.put(SpeechRecognizer.ERROR_INSUFFICIENT_PERMISSIONS, RecognizerIntent.RESULT_CLIENT_ERROR); - errorCodes.put(SpeechRecognizer.ERROR_NETWORK, RecognizerIntent.RESULT_NETWORK_ERROR); - errorCodes.put(SpeechRecognizer.ERROR_NETWORK_TIMEOUT, RecognizerIntent.RESULT_NETWORK_ERROR); - errorCodes.put(SpeechRecognizer.ERROR_NO_MATCH, RecognizerIntent.RESULT_NO_MATCH); - errorCodes.put(SpeechRecognizer.ERROR_RECOGNIZER_BUSY, RecognizerIntent.RESULT_SERVER_ERROR); - errorCodes.put(SpeechRecognizer.ERROR_SERVER, RecognizerIntent.RESULT_SERVER_ERROR); - errorCodes.put(SpeechRecognizer.ERROR_SPEECH_TIMEOUT, RecognizerIntent.RESULT_NO_MATCH); - return errorCodes; - } - - public static PendingIntent getPendingIntent(Bundle extras) { - Parcelable extraResultsPendingIntentAsParceable = extras.getParcelable(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT); - if (extraResultsPendingIntentAsParceable != null) { - //PendingIntent.readPendingIntentOrNullFromParcel(mExtraResultsPendingIntent); - if (extraResultsPendingIntentAsParceable instanceof PendingIntent) { - return (PendingIntent) extraResultsPendingIntentAsParceable; - } - } - return null; - } - - public static Intent getAppIntent(Context c, String packageName) { - PackageManager pm = c.getPackageManager(); - return pm.getLaunchIntentForPackage(packageName); - } - - /** - * Constructs a list of search intents. - * The first one that can be handled by the device is launched. - * In split-screen mode, launch the activity into the other screen. Test this by: - * 1. Launch Kõnele, 2. Start split-screen, 3. Press Kõnele mic button and speak, - * 4. The results should be loaded into the other window. - * - * @param activity activity - * @param query search query - */ - public static void startActivitySearch(Activity activity, CharSequence query) { - // TODO: how to pass the search query to ACTION_ASSIST - // TODO: maybe use SearchManager instead - //Intent intent0 = new Intent(Intent.ACTION_ASSIST); - //intent0.putExtra(Intent.EXTRA_ASSIST_CONTEXT, new Bundle()); - //intent0.putExtra(SearchManager.QUERY, query); - //intent0.putExtra(Intent.EXTRA_ASSIST_INPUT_HINT_KEYBOARD, false); - //intent0.putExtra(Intent.EXTRA_ASSIST_PACKAGE, context.getPackageName()); - startActivityIfAvailable(activity, - getSearchIntent(Intent.ACTION_WEB_SEARCH, query), - getSearchIntent(Intent.ACTION_SEARCH, query)); - } - - public static boolean startActivityIfAvailable(@NonNull Context context, Intent... intents) { - PackageManager mgr = context.getPackageManager(); - try { - for (Intent intent : intents) { - if (isActivityAvailable(mgr, intent)) { - // TODO: is it sensible to always start activity for result, - // even if the activity is not designed to return a result - if (context instanceof Activity) { - context.startActivity(intent); - } else { - // Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | intent.getFlags()); - context.startActivity(intent); - } - //activity.startActivityForResult(intent, 2); - return true; - } else { - Log.i("startActivityIfAvailable: not available: " + intent); - } - } - showMessage(context, R.string.errorFailedLaunchIntent); - } catch (SecurityException e) { - // This happens if the user constructs an intent for which we do not have a - // permission, e.g. the CALL intent. - Log.i("startActivityIfAvailable: " + e.getMessage()); - showMessage(context, e.getLocalizedMessage()); - } - return false; - } - - /** - * Starts an activity from the given context and catches a possible security exception. - */ - public static void startActivityWithCatch(@NonNull Context context, Intent intent) { - try { - if (context instanceof Activity) { - context.startActivity(intent); - } else { - // Calling startActivity() from outside of an Activity context requires the FLAG_ACTIVITY_NEW_TASK flag - intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | intent.getFlags()); - context.startActivity(intent); - } - } catch (SecurityException e) { - // This happens if the user constructs an intent for which we do not have a - // permission, e.g. the CALL intent, - // or according to Google Play, com.vlingo.midas/.settings.SettingsScreen - showMessage(context, e.getLocalizedMessage()); - } - } - - public static String rewriteResultWithExtras(Context context, Bundle extras, String result) { - String defaultResultUtterance = null; - String defaultResultCommand = null; - String defaultResultArg1 = null; - if (extras.getBoolean(Extras.EXTRA_RESULT_LAUNCH_AS_ACTIVITY)) { - defaultResultUtterance = "(.+)"; - defaultResultCommand = "activity"; - defaultResultArg1 = "$1"; - } - String resultUtterance = extras.getString(Extras.EXTRA_RESULT_UTTERANCE, defaultResultUtterance); - if (resultUtterance != null) { - String resultReplacement = extras.getString(Extras.EXTRA_RESULT_REPLACEMENT, null); - String resultCommand = extras.getString(Extras.EXTRA_RESULT_COMMAND, defaultResultCommand); - String resultArg1 = extras.getString(Extras.EXTRA_RESULT_ARG1, defaultResultArg1); - String resultArg2 = extras.getString(Extras.EXTRA_RESULT_ARG2, null); - - String[] resultArgs; - - if (resultArg1 == null) { - resultArgs = null; - } else if (resultArg2 == null) { - resultArgs = new String[]{resultArg1}; - } else { - resultArgs = new String[]{resultArg1, resultArg2}; - } - - List commands = new ArrayList<>(); - commands.add(new Command(resultUtterance, resultReplacement, resultCommand, resultArgs)); - result = launchIfIntent(context, new UtteranceRewriter(commands), result); - } - if (result != null) { - String rewritesAsStr = extras.getString(Extras.EXTRA_RESULT_REWRITES_AS_STR, null); - if (rewritesAsStr != null) { - result = launchIfIntent(context, new UtteranceRewriter(rewritesAsStr), result); - } - } - return result; - } - - /** - * Rewrites the text. If the result is a command then executes it (only "activity" is currently - * supported). Otherwise returns the rewritten string. - * Errors that occur during the execution of "activity" are communicated via toasts. - * The possible errors are: syntax error in JSON, nobody responded to the intent, no permission to launch - * the intent. - */ - public static String launchIfIntent(Context context, Iterable urs, String text) { - String newText = text; - for (UtteranceRewriter ur : urs) { - // Skip null, i.e. a case where a rewrites name did not resolve to a table. - if (ur == null) { - continue; - } - UtteranceRewriter.Rewrite rewrite = ur.getRewrite(newText); - if (rewrite.isCommand() && rewrite.mArgs != null && rewrite.mArgs.length > 0) { - // Commands that interpret their 1st arg as an intent in JSON. - // There can be other commands in the future. - try { - Intent intent = JsonUtils.createIntent(rewrite.mArgs[0]); - switch (rewrite.mId) { - case "activity": - startActivityIfAvailable(context, intent); - break; - case "service": - // TODO - break; - case "broadcast": - // TODO - break; - default: - break; - } - } catch (JSONException e) { - Log.i("launchIfIntent: JSON: " + e.getMessage()); - showMessage(context, e.getLocalizedMessage()); - } - return null; - } - newText = rewrite.mStr; - } - return newText; - } - - public static String launchIfIntent(Context context, UtteranceRewriter ur, String text) { - UtteranceRewriter.Rewrite rewrite = ur.getRewrite(text); - if (rewrite.isCommand() && rewrite.mArgs != null && rewrite.mArgs.length > 0) { - // Commands that interpret their 1st arg as an intent in JSON. - // There can be other commands in the future. - try { - Intent intent = JsonUtils.createIntent(rewrite.mArgs[0]); - switch (rewrite.mId) { - case "activity": - startActivityIfAvailable(context, intent); - break; - case "service": - // TODO - break; - case "broadcast": - // TODO - break; - default: - break; - } - } catch (JSONException e) { - Log.i("launchIfIntent: JSON: " + e.getMessage()); - showMessage(context, e.getLocalizedMessage()); - } - return null; - } - return rewrite.mStr; - } - - /** - * Checks whether a speech recognition service is available on the system. If this method - * returns {@code false}, {@link SpeechRecognizer#createSpeechRecognizer(Context, ComponentName)} - * will fail. - * Similar to {@link SpeechRecognizer#isRecognitionAvailable(Context)} but supports - * restricting the intent query by component name. - *

- * TODO: propose to add this to SpeechRecognizer - * TODO: clarify what does "will fail" mean - * - * @param context with which {@code SpeechRecognizer} will be created - * @param componentName of the recognition service - * @return {@code true} if recognition is available, {@code false} otherwise - */ - public static boolean isRecognitionAvailable(final Context context, ComponentName componentName) { - Intent intent = new Intent(RecognitionService.SERVICE_INTERFACE); - intent.setComponent(componentName); - final List list = context.getPackageManager().queryIntentServices(intent, 0); - return list.size() != 0; - } - - /** - * Checks if there are any activities that can service this intent. - * Note that we search for activities using the MATCH_DEFAULT_ONLY flag, but this - * can also return a non-exported activity (for some reason). - * This can only (?) happen if the intent references the activity by its class name and the - * activity belongs to the app that calls this method. - * We assume that activities are not exported for a reason, and thus will declare the - * intent unserviceable if a non-exported activity matches the intent. - * - * @param mgr PackageManager - * @param intent Intent - * @return true iff an exported activity exists to service this intent - */ - private static boolean isActivityAvailable(PackageManager mgr, Intent intent) { - List list = mgr.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); - for (ResolveInfo ri : list) { - if (ri.activityInfo != null) { - if (!ri.activityInfo.exported) { - Log.i("Query returned non-exported activity, declaring it unavailable."); - return false; - } - } - } - return list.size() > 0; - } - - private static Intent getSearchIntent(String action, CharSequence query) { - Intent intent = new Intent(action); - intent.putExtra(SearchManager.QUERY, query); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - intent.addFlags(Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT); - } - return intent; - } - - private static void showMessage(Context context, String message) { - Toast.makeText(context, message, Toast.LENGTH_LONG).show(); - } - - private static void showMessage(Context context, int message) { - Toast.makeText(context, message, Toast.LENGTH_LONG).show(); - } -} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/utils/JsonUtils.java b/app/src/main/java/ee/ioc/phon/android/speechutils/utils/JsonUtils.java deleted file mode 100644 index 274708d709a661bc38dfc29ce58e7e3121b9a7bb..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/utils/JsonUtils.java +++ /dev/null @@ -1,130 +0,0 @@ -package ee.ioc.phon.android.speechutils.utils; - -import android.content.ComponentName; -import android.content.Intent; -import android.net.Uri; -import android.os.Bundle; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.List; - - -public final class JsonUtils { - - private JsonUtils() { - } - - /** - * Parse JSON. - * - * @param chars CharSequence that corresponds to serialized JSON. - * @return JSONObject - * @throws JSONException if parsing fails - */ - private static JSONObject parseJson(CharSequence chars) throws JSONException { - if (chars == null) { - throw new JSONException("input is NULL"); - } - return new JSONObject(chars.toString()); - } - - /** - * TODO: support: broadcast intent, voice interaction launch mode, etc. - * - * @param json Intent as a JSON object - * @return Intent - * @throws JSONException if parsing fails - */ - public static Intent createIntent(JSONObject json) throws JSONException { - Intent intent = new Intent(); - String action = json.optString("action"); - if (!action.isEmpty()) { - intent.setAction(action); - } - String component = json.optString("component"); - if (!component.isEmpty()) { - intent.setComponent(ComponentName.unflattenFromString(component)); - } - String packageName = json.optString("package"); - if (!packageName.isEmpty()) { - intent.setPackage(packageName); - } - String data = json.optString("data"); - if (!data.isEmpty()) { - intent.setData(Uri.parse(data)); - } - String type = json.optString("type"); - if (!type.isEmpty()) { - intent.setType(type); - } - JSONObject extras = json.optJSONObject("extras"); - if (extras != null) { - Iterator iter = extras.keys(); - while (iter.hasNext()) { - String key = iter.next(); - Object val = extras.get(key); - if (val instanceof Long) { - intent.putExtra(key, (Long) val); - } else if (val instanceof Integer) { - intent.putExtra(key, (Integer) val); - } else if (val instanceof Boolean) { - intent.putExtra(key, (Boolean) val); - } else if (val instanceof Double) { - intent.putExtra(key, (Double) val); - } else if (val instanceof String) { - intent.putExtra(key, (String) val); - } else if (val instanceof JSONArray) { - // TODO: improve this, currently assumes that array is string array - JSONArray array = (JSONArray) val; - int length = array.length(); - List vals = new ArrayList<>(); - for (int i = 0; i < length; i++) { - vals.add(array.optString(i, "")); - } - intent.putExtra(key, vals.toArray(new String[0])); - } else if (val instanceof JSONObject) { - JSONObject innerObject = (JSONObject) val; - if (Intent.EXTRA_INTENT.equals(key)) { - intent.putExtra(key, createIntent(innerObject)); - } else { - // TODO: improve this, currently assumes that object is a mapping - Bundle bundle = new Bundle(); - Iterator innerIter = innerObject.keys(); - while (innerIter.hasNext()) { - String innerKey = innerIter.next(); - Object innerVal = innerObject.get(innerKey); - if (innerVal instanceof String) { - bundle.putString(innerKey, (String) innerVal); - } - } - intent.putExtra(key, bundle); - } - } - } - } - JSONArray categories = json.optJSONArray("categories"); - if (categories != null) { - int length = categories.length(); - for (int i = 0; i < length; i++) { - intent.addCategory(categories.getString(i)); - } - } - JSONArray flags = json.optJSONArray("flags"); - if (flags != null) { - int length = flags.length(); - for (int i = 0; i < length; i++) { - intent.addFlags(flags.getInt(i)); - } - } - return intent; - } - - public static Intent createIntent(CharSequence query) throws JSONException { - return createIntent(parseJson(query)); - } -} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/utils/PreferenceUtils.java b/app/src/main/java/ee/ioc/phon/android/speechutils/utils/PreferenceUtils.java deleted file mode 100644 index 84bd55b748b0bbb748c586c4f0d3b34df7446109..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/utils/PreferenceUtils.java +++ /dev/null @@ -1,191 +0,0 @@ -package ee.ioc.phon.android.speechutils.utils; - -import android.content.SharedPreferences; -import android.content.res.Resources; - -import androidx.annotation.NonNull; - -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.UUID; - -public class PreferenceUtils { - - private static final String SEP = "/"; - - private PreferenceUtils() { - } - - public static String getPrefString(SharedPreferences prefs, String key, String defaultValue) { - return prefs.getString(key, defaultValue); - } - - public static String getPrefString(SharedPreferences prefs, Resources res, int key, int defaultValue) { - return getPrefString(prefs, res.getString(key), res.getString(defaultValue)); - } - - public static String getPrefString(SharedPreferences prefs, Resources res, int key) { - return prefs.getString(res.getString(key), null); - } - - public static Set getPrefStringSet(SharedPreferences prefs, Resources res, int key) { - try { - return prefs.getStringSet(res.getString(key), Collections.emptySet()); - } catch (ClassCastException e) { - return Collections.emptySet(); - } - } - - public static Set getPrefStringSet(SharedPreferences prefs, Resources res, int key, int defaultValue) { - return prefs.getStringSet(res.getString(key), getStringSetFromStringArray(res, defaultValue)); - } - - public static boolean getPrefBoolean(SharedPreferences prefs, Resources res, int key, int defaultValue) { - try { - return prefs.getBoolean(res.getString(key), res.getBoolean(defaultValue)); - } catch (ClassCastException e) { - // This can happen if the key is reused for a different purpose and the value has now a different type - // than stored (by an earlier version of the app). - return false; - } - } - - public static int getPrefInt(SharedPreferences prefs, Resources res, int key, int defaultValue) { - return Integer.parseInt(getPrefString(prefs, res, key, defaultValue)); - } - - public static String getUniqueId(SharedPreferences settings) { - String id = settings.getString("id", null); - if (id == null) { - id = UUID.randomUUID().toString(); - SharedPreferences.Editor editor = settings.edit(); - editor.putString("id", id); - editor.apply(); - } - return id; - } - - public static Set getStringSetFromStringArray(Resources res, int key) { - return new HashSet<>(Arrays.asList(res.getStringArray(key))); - } - - public static List getStringListFromStringArray(Resources res, int key) { - return Arrays.asList(res.getStringArray(key)); - } - - public static void putPrefString(SharedPreferences prefs, String key, String value) { - SharedPreferences.Editor editor = prefs.edit(); - editor.putString(key, value); - editor.apply(); - } - - public static void putPrefBoolean(SharedPreferences prefs, Resources res, int key, boolean value) { - SharedPreferences.Editor editor = prefs.edit(); - editor.putBoolean(res.getString(key), value); - editor.apply(); - } - - public static void putPrefString(SharedPreferences prefs, Resources res, int key, String value) { - putPrefString(prefs, res.getString(key), value); - } - - public static void putPrefStringSet(SharedPreferences prefs, Resources res, int key, Set value) { - SharedPreferences.Editor editor = prefs.edit(); - editor.putStringSet(res.getString(key), value); - editor.apply(); - } - - public static boolean togglePrefStringSetEntry(SharedPreferences prefs, Resources res, int key, String value) { - Set set; - try { - set = prefs.getStringSet(res.getString(key), new HashSet<>()); - } catch (ClassCastException e) { - set = new HashSet<>(); - } - boolean b = set.contains(value); - if (b) { - set.remove(value); - } else { - set.add(value); - } - putPrefStringSet(prefs, res, key, set); - return !b; - } - - /** - * Stores the given key-value pair into a map with the given name. - * If value is null, then delete the entry from the preferences. - */ - public static void putPrefMapEntry(SharedPreferences prefs, Resources res, int nameId, String key, String value) { - String name = res.getString(nameId); - Set keys = prefs.getStringSet(name, new HashSet<>()); - SharedPreferences.Editor editor = prefs.edit(); - String nameKey = name + SEP + key; - if (value == null) { - editor.remove(nameKey); - if (keys.contains(key)) { - keys.remove(key); - editor.putStringSet(name, keys); - } - } else { - editor.putString(nameKey, value); - if (!keys.contains(key)) { - keys.add(key); - editor.putStringSet(name, keys); - } - } - editor.apply(); - } - - public static String getPrefMapEntry(SharedPreferences prefs, Resources res, int nameId, String key) { - return prefs.getString(res.getString(nameId) + SEP + key, null); - } - - public static Set getPrefMapKeys(SharedPreferences prefs, Resources res, int nameId) { - return prefs.getStringSet(res.getString(nameId), Collections.emptySet()); - } - - @NonNull - public static Map getPrefMap(SharedPreferences prefs, Resources res, int nameId) { - String name = res.getString(nameId); - Set keys = prefs.getStringSet(name, Collections.emptySet()); - Map map = new HashMap<>(); - for (String key : keys) { - map.put(key, prefs.getString(name + SEP + key, null)); - } - return map; - } - - public static void clearPrefMap(SharedPreferences prefs, Resources res, int nameId) { - String name = res.getString(nameId); - Set keys = prefs.getStringSet(name, null); - if (keys != null) { - SharedPreferences.Editor editor = prefs.edit(); - for (String key : keys) { - editor.remove(name + SEP + key); - } - editor.remove(name); - editor.apply(); - } - } - - public static void clearPrefMap(SharedPreferences prefs, Resources res, int nameId, Set deleteKeys) { - String name = res.getString(nameId); - Set keys = prefs.getStringSet(name, null); - if (keys != null) { - SharedPreferences.Editor editor = prefs.edit(); - for (String key : deleteKeys) { - editor.remove(name + SEP + key); - } - Set newKeys = new HashSet<>(keys); - newKeys.removeAll(deleteKeys); - editor.putStringSet(name, newKeys); - editor.apply(); - } - } -} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/utils/TextUtils.java b/app/src/main/java/ee/ioc/phon/android/speechutils/utils/TextUtils.java deleted file mode 100644 index 0d5a241aaa73c299a5c7dbafcd00f7a820bd1e3e..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/utils/TextUtils.java +++ /dev/null @@ -1,62 +0,0 @@ -package ee.ioc.phon.android.speechutils.utils; - -import ee.ioc.phon.android.speechutils.editor.Constants; - -public class TextUtils { - - private TextUtils() { - } - - /** - * Pretty-prints the string returned by the server to be orthographically correct (Estonian), - * assuming that the string represents a sequence of tokens separated by a single space character. - * Note that a text editor (which has additional information about the context of the cursor) - * will need to do additional pretty-printing, e.g. capitalization if the cursor follows a - * sentence end marker. - * - * @param str String to be pretty-printed - * @return Pretty-printed string (never null) - */ - public static String prettyPrint(String str) { - boolean isSentenceStart = false; - boolean isWhitespaceBefore = false; - String text = ""; - for (String tok : str.split(" ")) { - if (tok.length() == 0) { - continue; - } - String glue = " "; - char firstChar = tok.charAt(0); - if (isWhitespaceBefore - || Character.isWhitespace(firstChar) - || Constants.CHARACTERS_STICKY_LEFT.contains(firstChar)) { - glue = ""; - } - - if (isSentenceStart) { - tok = Character.toUpperCase(firstChar) + tok.substring(1); - } - - if (text.length() == 0) { - text = tok; - } else { - text += glue + tok; - } - - isWhitespaceBefore = Character.isWhitespace(firstChar); - - // If the token is not a character then we are in the middle of the sentence. - // If the token is an EOS character then a new sentences has started. - // If the token is some other character other than whitespace (then we are in the - // middle of the sentences. (The whitespace characters are transparent.) - if (tok.length() > 1) { - isSentenceStart = false; - } else if (Constants.CHARACTERS_EOS.contains(firstChar)) { - isSentenceStart = true; - } else if (!isWhitespaceBefore) { - isSentenceStart = false; - } - } - return text; - } -} \ No newline at end of file diff --git a/app/src/main/java/ee/ioc/phon/android/speechutils/view/MicButton.java b/app/src/main/java/ee/ioc/phon/android/speechutils/view/MicButton.java deleted file mode 100644 index 6f0e3682c9d6dd8ac81e8c0bc7e4d86ad9ebe298..0000000000000000000000000000000000000000 --- a/app/src/main/java/ee/ioc/phon/android/speechutils/view/MicButton.java +++ /dev/null @@ -1,158 +0,0 @@ -package ee.ioc.phon.android.speechutils.view; - -import android.content.Context; -import android.content.res.Resources; -import android.graphics.Color; -import android.graphics.drawable.Drawable; -import android.util.AttributeSet; -import android.view.HapticFeedbackConstants; -import android.view.MotionEvent; -import android.view.animation.Animation; -import android.view.animation.AnimationUtils; - -import androidx.core.content.res.ResourcesCompat; - -import java.util.ArrayList; -import java.util.List; - -import ee.ioc.phon.android.speechutils.R; - -// TODO: rather use com.google.android.material.floatingactionbutton.FloatingActionButton -public class MicButton extends androidx.appcompat.widget.AppCompatImageButton { - - public enum State { - // Initial state - INIT, - // An attempt was made to start the recording/transcription, but unclear if it succeeded - WAITING, - // We are listening (if there is human speech) - LISTENING, - // We are recording and sending the (speech) audio for transcription - RECORDING, - // Recording has stopped but not all the recorded audio has been processed yet - TRANSCRIBING, - // An error has occurred - ERROR - } - - // TODO: rename to COLOR_RECORDING - public static final int COLOR_LISTENING = Color.argb(255, 198, 40, 40); - public static final int COLOR_TRANSCRIBING = Color.argb(255, 153, 51, 204); - - // Must equal to the last index of mVolumeLevels - private static final int MAX_LEVEL = 3; - - private float mMinRmsDb; - private float mMaxRmsDb; - - private Drawable mDrawableMic; - private Drawable mDrawableMicWaiting; - private Drawable mDrawableMicListening; - private Drawable mDrawableMicTranscribing; - - private List mVolumeLevels; - - private Animation mAnimFadeInOutInf; - - private int mVolumeLevel = 0; - - public MicButton(Context context, AttributeSet attrs, int defStyle) { - super(context, attrs, defStyle); - if (!isInEditMode()) { - init(context); - } - } - - public MicButton(Context context, AttributeSet attrs) { - super(context, attrs); - if (!isInEditMode()) { - init(context); - } - } - - public MicButton(Context context) { - super(context); - if (!isInEditMode()) { - init(context); - } - } - - public void setState(State state) { - switch (state) { - case INIT: - case ERROR: - mMinRmsDb = mMaxRmsDb = 0f; - setEnabled(true); - clearAnimation(); - setBackgroundDrawable(mDrawableMic); - break; - case WAITING: - setEnabled(false); - setBackgroundDrawable(mDrawableMicWaiting); - break; - case RECORDING: - setEnabled(true); - setBackgroundDrawable(mVolumeLevels.get(0)); - break; - case LISTENING: - setEnabled(true); - setBackgroundDrawable(mDrawableMicListening); - break; - case TRANSCRIBING: - setEnabled(true); - setBackgroundDrawable(mDrawableMicTranscribing); - startAnimation(mAnimFadeInOutInf); - break; - default: - break; - } - } - - - public void setVolumeLevel(float rmsdB) { - if (mMinRmsDb == mMaxRmsDb) { - mMinRmsDb = rmsdB; - mMaxRmsDb = mMinRmsDb + 1; - } else if (rmsdB < mMinRmsDb) { - mMinRmsDb = rmsdB; - } else if (rmsdB > mMaxRmsDb) { - mMaxRmsDb = rmsdB; - } - int index = (int) (MAX_LEVEL * ((rmsdB - mMinRmsDb) / (mMaxRmsDb - mMinRmsDb))); - int level = Math.min(Math.max(0, index), MAX_LEVEL); - if (level != mVolumeLevel) { - mVolumeLevel = level; - setBackgroundDrawable(mVolumeLevels.get(level)); - } - } - - private void initAnimations(Context context) { - Resources res = getResources(); - mDrawableMic = ResourcesCompat.getDrawable(res, R.drawable.button_mic, null); - mDrawableMicWaiting = ResourcesCompat.getDrawable(res, R.drawable.button_mic_waiting, null); - mDrawableMicListening = ResourcesCompat.getDrawable(res, R.drawable.button_mic_listening, null); - mDrawableMicTranscribing = ResourcesCompat.getDrawable(res, R.drawable.button_mic_transcribing, null); - - mVolumeLevels = new ArrayList<>(); - mVolumeLevels.add(ResourcesCompat.getDrawable(res, R.drawable.button_mic_recording_0, null)); - mVolumeLevels.add(ResourcesCompat.getDrawable(res, R.drawable.button_mic_recording_1, null)); - mVolumeLevels.add(ResourcesCompat.getDrawable(res, R.drawable.button_mic_recording_2, null)); - mVolumeLevels.add(ResourcesCompat.getDrawable(res, R.drawable.button_mic_recording_3, null)); - - mAnimFadeInOutInf = AnimationUtils.loadAnimation(context, R.anim.fade_inout_inf); - } - - private void init(Context context) { - initAnimations(context); - - // Vibrate when the microphone key is pressed down - setOnTouchListener((v, event) -> { - if (event.getAction() == MotionEvent.ACTION_DOWN) { - // TODO: what is the diff between KEYBOARD_TAP and the other constants? - // TODO: does not seem to work on Android 7.1 - v.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP); - } - return false; - }); - } -} \ No newline at end of file diff --git a/app/src/main/res/anim/fade_inout_inf.xml b/app/src/main/res/anim/fade_inout_inf.xml deleted file mode 100644 index 273fda91d587cca3c6ff4e596035ef6504eaf1ea..0000000000000000000000000000000000000000 --- a/app/src/main/res/anim/fade_inout_inf.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/button_mic.xml b/app/src/main/res/drawable/button_mic.xml deleted file mode 100644 index 27975e0b400639bec14ce9fb43b69f20e6c43654..0000000000000000000000000000000000000000 --- a/app/src/main/res/drawable/button_mic.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/button_mic_listening.xml b/app/src/main/res/drawable/button_mic_listening.xml deleted file mode 100644 index 9b62712efafd9379045e0efd0ab41ac0381b43ea..0000000000000000000000000000000000000000 --- a/app/src/main/res/drawable/button_mic_listening.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/button_mic_recording_0.xml b/app/src/main/res/drawable/button_mic_recording_0.xml deleted file mode 100644 index d069b9857ba83ebe6a9c8ef5eb8198db1fc1608b..0000000000000000000000000000000000000000 --- a/app/src/main/res/drawable/button_mic_recording_0.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/button_mic_recording_1.xml b/app/src/main/res/drawable/button_mic_recording_1.xml deleted file mode 100644 index 838b212210bb12e8753adecb0c01c488b11e692b..0000000000000000000000000000000000000000 --- a/app/src/main/res/drawable/button_mic_recording_1.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/button_mic_recording_2.xml b/app/src/main/res/drawable/button_mic_recording_2.xml deleted file mode 100644 index 539f9612b1ac064caf7f35b9b51efdcb31fd742d..0000000000000000000000000000000000000000 --- a/app/src/main/res/drawable/button_mic_recording_2.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/button_mic_recording_3.xml b/app/src/main/res/drawable/button_mic_recording_3.xml deleted file mode 100644 index 5b5124e03ac728225d3e67b9929851dc352c39eb..0000000000000000000000000000000000000000 --- a/app/src/main/res/drawable/button_mic_recording_3.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/button_mic_transcribing.xml b/app/src/main/res/drawable/button_mic_transcribing.xml deleted file mode 100644 index 4882ad2fbdc410761b5eb4caff08575c3d2adab8..0000000000000000000000000000000000000000 --- a/app/src/main/res/drawable/button_mic_transcribing.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/button_mic_waiting.xml b/app/src/main/res/drawable/button_mic_waiting.xml deleted file mode 100644 index f314ad120f991027341bca23334c3d75b437e197..0000000000000000000000000000000000000000 --- a/app/src/main/res/drawable/button_mic_waiting.xml +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_service.xml b/app/src/main/res/drawable/ic_service.xml deleted file mode 100644 index 6eea65e295742bdd6a5cc7ced806d4b038097513..0000000000000000000000000000000000000000 --- a/app/src/main/res/drawable/ic_service.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/ic_voice_search_api_material.xml b/app/src/main/res/drawable/ic_voice_search_api_material.xml deleted file mode 100644 index f48d8f65bce06883b32be06a6a2d702334657388..0000000000000000000000000000000000000000 --- a/app/src/main/res/drawable/ic_voice_search_api_material.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/raw/audiocue1_start_listening.ogg b/app/src/main/res/raw/audiocue1_start_listening.ogg deleted file mode 100644 index 18b69d98b355ea235e3f96b34b15bf0b332710db..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/raw/audiocue1_start_listening.ogg and /dev/null differ diff --git a/app/src/main/res/raw/audiocue1_stop_listening.ogg b/app/src/main/res/raw/audiocue1_stop_listening.ogg deleted file mode 100644 index 15b59fbbbda71c76e3298db53e386c5d9d355bd7..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/raw/audiocue1_stop_listening.ogg and /dev/null differ diff --git a/app/src/main/res/raw/error.wav b/app/src/main/res/raw/error.wav deleted file mode 100644 index a3557d8020205a2a5fc2cec32b614a4608399235..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/raw/error.wav and /dev/null differ diff --git a/app/src/main/res/raw/explore_begin.ogg b/app/src/main/res/raw/explore_begin.ogg deleted file mode 100644 index db4393762fa7232d8904c1b744f6ba44c6d085e0..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/raw/explore_begin.ogg and /dev/null differ diff --git a/app/src/main/res/raw/explore_end.ogg b/app/src/main/res/raw/explore_end.ogg deleted file mode 100644 index 98e6a16d5afa10978f44432564c0bc92cb6b7a0b..0000000000000000000000000000000000000000 Binary files a/app/src/main/res/raw/explore_end.ogg and /dev/null differ diff --git a/app/src/main/res/values-et/strings.xml b/app/src/main/res/values-et/strings.xml deleted file mode 100644 index 6c236f1f581afc8b5d685458369c521f47fab095..0000000000000000000000000000000000000000 --- a/app/src/main/res/values-et/strings.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - Lindistamine luhtus. Võibolla mõni teine rakendus parasjagu lindistab. - Puudub ühendus kõnetuvastusserveriga. Võrguühendus on välja lülitatud või server ei tööta. - Rakenduse viga - Kõnetuvastusserveri viga - Sisendkõne transkribeerimine luhtus. Proovige uuesti! - Katkestatud - - VIGA: kõneväljund ei ole keele \"%1$s\" jaoks installeeritud - VIGA: kõneväljundi konfigureerimine luhtus - VIGA: Intent-päringule vastavat rakendust ei leitud - diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml deleted file mode 100644 index f5250fbbaa0fca45f0315b1a2a71bc58755e2603..0000000000000000000000000000000000000000 --- a/app/src/main/res/values/colors.xml +++ /dev/null @@ -1,24 +0,0 @@ - - - #ffffffff - - - #11404040 - #ffcc00 - #996600 - #996600 - #ffcc00 - #ffcc00 - #ffcc00 - - #c62828 - #ff5f52 - #8e0000 - - #e57373 - - #c58be2 - - #9933cc - #424242 - \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml deleted file mode 100644 index f302cd4c2b6d38e506fc8ae488799867de3b25b4..0000000000000000000000000000000000000000 --- a/app/src/main/res/values/strings.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - Audio recording failed. Maybe another app is currently recording. - Server not reachable. The internet connection is broken or the server is down. - General client error - Recognizer server error - Transcription was not found for the recorded speech. Please try again! - Canceled - - ERROR: Spoken output not available for language \"%1$s\" - ERROR: Spoken output initialization failed - ERROR: Intent is not serviced by any app - diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..e463f147c46e9b38e8fe2fb8e3f3059b899a057d --- /dev/null +++ b/build.gradle @@ -0,0 +1,37 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. +apply plugin: 'com.huawei.ohos.app' + +ohos { + compileSdkVersion 5 + defaultConfig { + compatibleSdkVersion 5 + } +} + +buildscript { + repositories { + maven { + url 'https://repo.huaweicloud.com/repository/maven/' + } + maven { + url 'https://developer.huawei.com/repo/' + } + jcenter() + } + dependencies { + classpath 'com.huawei.ohos:hap:2.4.2.7' + classpath 'com.huawei.ohos:decctest:1.0.0.6' + } +} + +allprojects { + repositories { + maven { + url 'https://repo.huaweicloud.com/repository/maven/' + } + maven { + url 'https://developer.huawei.com/repo/' + } + jcenter() + } +} diff --git a/entry/.gitignore b/entry/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..796b96d1c402326528b4ba3c12ee9d92d0e212e9 --- /dev/null +++ b/entry/.gitignore @@ -0,0 +1 @@ +/build diff --git a/entry/build.gradle b/entry/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..658a4b42f4f2a825db48b73f8d79fe938cabf0be --- /dev/null +++ b/entry/build.gradle @@ -0,0 +1,27 @@ +apply plugin: 'com.huawei.ohos.hap' +apply plugin: 'com.huawei.ohos.decctest' +ohos { + compileSdkVersion 5 + defaultConfig { + compatibleSdkVersion 5 + } + buildTypes { + release { + proguardOpt { + proguardEnabled false + rulesFiles 'proguard-rules.pro' + } + } + } + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar', '*.har']) + implementation project(path: ':speechutils') + testImplementation 'junit:junit:4.13' + ohosTestImplementation 'com.huawei.ohos.testkit:runner:1.0.0.100' +} +decc { + supportType = ['html','xml'] +} diff --git a/entry/proguard-rules.pro b/entry/proguard-rules.pro new file mode 100644 index 0000000000000000000000000000000000000000..f7666e47561d514b2a76d5a7dfbb43ede86da92a --- /dev/null +++ b/entry/proguard-rules.pro @@ -0,0 +1 @@ +# config module specific ProGuard rules here. \ No newline at end of file diff --git a/entry/src/main/config.json b/entry/src/main/config.json new file mode 100644 index 0000000000000000000000000000000000000000..be719261bc55ee2fe97b4538a4c12e94a46d2c2f --- /dev/null +++ b/entry/src/main/config.json @@ -0,0 +1,87 @@ +{ + "app": { + "bundleName": "com.huawei.hm_speechutilsdemo", + "vendor": "huawei", + "version": { + "code": 1, + "name": "1.0" + }, + "apiVersion": { + "compatible": 5, + "target": 5, + "releaseType": "Release" + } + }, + "deviceConfig": {}, + "module": { + "reqPermissions": [{ + "name": "ohos.permission.INTERNET", + "reason": "允许使用网络socket", + "usedScene": { + "ability": [ + "com.chinasoft.ttsdemo.MainAbility" + ], + "when": "always" + } + }, { + "name": "ohos.permission.MICROPHONE", + "reason": "需要马克风录音", + "usedScene": { + "ability": [ + "com.chinasoft.ttsdemo.MainAbility" + ], + "when": "always" + } + }, + { + "name": "ohos.permission.WRITE_MEDIA", + "reason": "允许应用读写用户外部存储中的媒体文件信息", + "usedScene": { + "ability": [ + "com.huawei.hm_speechutilsdemo.MainAbility" + ], + "when": "always" + } + } + ], + "package": "com.huawei.hm_speechutilsdemo", + "name": ".MyApplication", + "deviceType": [ + "phone" + ], + "distro": { + "deliveryWithInstall": true, + "moduleName": "entry", + "moduleType": "entry" + }, + "abilities": [ + { + "metaData": { + "customizeData": [ + { + "name": "hwc-theme", + "value": "androidhwext:style/Theme.Emui.NoTitleBar" + } + ] + }, + "skills": [ + { + "entities": [ + "entity.system.home" + ], + "actions": [ + "action.system.home" + ] + } + ], + "orientation": "unspecified", + "name": "com.huawei.hm_speechutilsdemo.MainAbility", + "icon": "$media:icon", + "description": "$string:mainability_description", + "label": "$string:app_name", + "type": "page", + "launchType": "standard" + } + ] + } +} \ No newline at end of file diff --git a/entry/src/main/java/com/huawei/hm_speechutilsdemo/AttrValue.java b/entry/src/main/java/com/huawei/hm_speechutilsdemo/AttrValue.java new file mode 100644 index 0000000000000000000000000000000000000000..9dfec8ba7fccede8880c9309214f65ac99661116 --- /dev/null +++ b/entry/src/main/java/com/huawei/hm_speechutilsdemo/AttrValue.java @@ -0,0 +1,122 @@ +/* + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.huawei.hm_speechutilsdemo; + +import ohos.agp.components.Attr; +import ohos.agp.components.AttrSet; +import ohos.agp.components.element.Element; +import ohos.agp.utils.Color; + +/** + * 自定义属性工具类 + * + * @author:wjt + * @since 2021-04-06 + */ +public class AttrValue { + /** + * 构造 + */ + private AttrValue() { + } + + /** + * 获取自定义属性值 + * + * @param attrSet attrSet + * @param key key + * @param defValue defValue + * @param Value + * @return Value + */ + public static T get(AttrSet attrSet, String key, T defValue) { + if (attrSet == null || !attrSet.getAttr(key).isPresent()) { + return (T) defValue; + } + + Attr attr = attrSet.getAttr(key).get(); + if (defValue instanceof String) { + return (T) attr.getStringValue(); + } else if (defValue instanceof Long) { + return (T) (Long) (attr.getLongValue()); + } else if (defValue instanceof Float) { + return (T) (Float) (attr.getFloatValue()); + } else if (defValue instanceof Integer) { + return (T) (Integer) (attr.getIntegerValue()); + } else if (defValue instanceof Boolean) { + return (T) (Boolean) (attr.getBoolValue()); + } else if (defValue instanceof Color) { + return (T) (attr.getColorValue()); + } else if (defValue instanceof Element) { + return (T) (attr.getElement()); + } else { + return (T) defValue; + } + } + + /** + * 获取Element + * + * @param attrSet 属性 + * @param key 键 + * @return Element + */ + public static Element getElement(AttrSet attrSet, String key) { + Element element = null; + if (attrSet.getAttr(key).isPresent()) { + element = attrSet.getAttr(key).get().getElement(); + } + return element; + } + + /** + * 获取layout + * + * @param attrSet 属性 + * @param key 键 + * @param def 默认值 + * @return 值 + */ + public static int getLayout(AttrSet attrSet, String key, int def) { + if (attrSet.getAttr(key).isPresent()) { + int layoutId = def; + String value = attrSet.getAttr(key).get().getStringValue(); + if (value != null) { + String subLayoutId = value.substring(value.indexOf(":")); + layoutId = Integer.parseInt(subLayoutId); + } + return layoutId; + } + return def; + } + + /** + * 获得Dimension值 + * + * @param attrSet 属性 + * @param key 键 + * @param defDimensionValue 默认值 + * @return 值 + */ + public static int getDimension(AttrSet attrSet, String key, int defDimensionValue) { + if (!attrSet.getAttr(key).isPresent()) { + return defDimensionValue; + } + + Attr attr = attrSet.getAttr(key).get(); + return attr.getDimensionValue(); + } +} diff --git a/entry/src/main/java/com/huawei/hm_speechutilsdemo/ListProvider.java b/entry/src/main/java/com/huawei/hm_speechutilsdemo/ListProvider.java new file mode 100644 index 0000000000000000000000000000000000000000..5cc1ec891552c11a45e0eace54fd40c7122b3c89 --- /dev/null +++ b/entry/src/main/java/com/huawei/hm_speechutilsdemo/ListProvider.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.huawei.hm_speechutilsdemo; + +import com.huawei.hm_speechutilsdemo.slice.MainAbilitySlice; +import ohos.agp.components.BaseItemProvider; +import ohos.agp.components.Component; +import ohos.agp.components.ComponentContainer; +import ohos.agp.components.LayoutScatter; +import ohos.agp.components.Text; + +import java.util.List; + +/** + * provider + * + * @author:wjt + * @since 2021-03-20 + */ +public class ListProvider extends BaseItemProvider { + private List data; + private MainAbilitySlice mainAbilitySlice; + + /** + * 构造 + * + * @param mainAbilitySlice + * @param data + */ + public ListProvider(MainAbilitySlice mainAbilitySlice, List data) { + this.data = data; + this.mainAbilitySlice = mainAbilitySlice; + } + + @Override + public int getCount() { + return data == null ? 0 : data.size(); + } + + @Override + public Object getItem(int position) { + return data.get(position); + } + + @Override + public long getItemId(int position) { + return position; + } + + @Override + public Component getComponent(int position, Component component, ComponentContainer componentContainer) { + final Component cpt; + if (component == null) { + cpt = LayoutScatter.getInstance(mainAbilitySlice).parse(ResourceTable.Layout_item_sample, null, false); + } else { + cpt = component; + } + String sampleItem = data.get(position); + Text text = (Text) cpt.findComponentById(ResourceTable.Id_item_index); + text.setText(sampleItem); + return cpt; + } +} diff --git a/entry/src/main/java/com/huawei/hm_speechutilsdemo/MainAbility.java b/entry/src/main/java/com/huawei/hm_speechutilsdemo/MainAbility.java new file mode 100644 index 0000000000000000000000000000000000000000..4df09ea2a7373ae3f53f571b4fd22e327f084197 --- /dev/null +++ b/entry/src/main/java/com/huawei/hm_speechutilsdemo/MainAbility.java @@ -0,0 +1,69 @@ +/* + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.huawei.hm_speechutilsdemo; + +import com.huawei.hm_speechutilsdemo.slice.MainAbilitySlice; +import ee.ioc.phon.android.speechutils.utils.Define; +import ohos.aafwk.ability.Ability; +import ohos.aafwk.content.Intent; +import ohos.agp.window.dialog.ToastDialog; +import ohos.agp.window.service.WindowManager; +import ohos.bundle.IBundleManager; + +/** + * 主页面 + * + * @author:wjt + * @since 2021-04-06 + */ +public class MainAbility extends Ability { + @Override + public void onStart(Intent intent) { + /** + * 隐藏状态栏 ,沉浸式状态栏 + */ + getWindow().addFlags(WindowManager.LayoutConfig.MARK_FULL_SCREEN); + getWindow().addFlags(WindowManager.LayoutConfig.MARK_FULL_SCREEN); + getWindow().addFlags(WindowManager.LayoutConfig.MARK_TRANSLUCENT_STATUS); + super.onStart(intent); + super.setMainRoute(MainAbilitySlice.class.getName()); + requestPermissionsFromUser(new String[]{Define.MICROPHONE, Define.SHORTCUTS}, 0); + if (verifySelfPermission(Define.SHORTCUTS) != IBundleManager.PERMISSION_DENIED) { + // 应用权限未被授予 + if (canRequestPermission(Define.SHORTCUTS)) { + // 验证是否可以申请弹窗授权(首次申请或者用户未选择禁止且不再提示) + requestPermissionsFromUser(new String[]{Define.SHORTCUTS}, Define.FUYI); + } else { + // 显示应用权限需要权限的理由,提示用户进入设置授权 + new ToastDialog(this).setText("请前往设置授予录音权限").setDuration(Define.ERQIAN).show(); + } + } else { + new ToastDialog(this).setText("录音权限已被授予").setDuration(Define.ERQIAN).show(); + } + if (verifySelfPermission(Define.MICROPHONE) != IBundleManager.PERMISSION_DENIED) { + // 应用权限未被授予 + if (canRequestPermission(Define.MICROPHONE)) { + // 验证是否可以申请弹窗授权(首次申请或者用户未选择禁止且不再提示) + requestPermissionsFromUser(new String[]{Define.MICROPHONE}, Define.FUYI); + } else { + // 显示应用权限需要权限的理由,提示用户进入设置授权 + new ToastDialog(this).setText("请前往设置授予麦克风权限").setDuration(Define.ERQIAN).show(); + } + } else { + new ToastDialog(this).setText("麦克风权限已被授予").setDuration(Define.ERQIAN).show(); + } + } +} diff --git a/entry/src/main/java/com/huawei/hm_speechutilsdemo/MyApplication.java b/entry/src/main/java/com/huawei/hm_speechutilsdemo/MyApplication.java new file mode 100644 index 0000000000000000000000000000000000000000..7fe74b1caf14afd102cb2350588cc24ab333c220 --- /dev/null +++ b/entry/src/main/java/com/huawei/hm_speechutilsdemo/MyApplication.java @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.huawei.hm_speechutilsdemo; + +import ohos.aafwk.ability.AbilityPackage; + +public class MyApplication extends AbilityPackage { + @Override + public void onInitialize() { + super.onInitialize(); + } +} diff --git a/entry/src/main/java/com/huawei/hm_speechutilsdemo/SuperButton.java b/entry/src/main/java/com/huawei/hm_speechutilsdemo/SuperButton.java new file mode 100644 index 0000000000000000000000000000000000000000..16b09d7c28bd4546326831091532b504fbc8a729 --- /dev/null +++ b/entry/src/main/java/com/huawei/hm_speechutilsdemo/SuperButton.java @@ -0,0 +1,384 @@ +/* + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain an copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.huawei.hm_speechutilsdemo; + +import ohos.agp.colors.RgbColor; +import ohos.agp.components.AttrHelper; +import ohos.agp.components.AttrSet; +import ohos.agp.components.Button; +import ohos.agp.components.Component; +import ohos.agp.components.ComponentState; +import ohos.agp.components.element.ShapeElement; +import ohos.agp.components.element.StateElement; +import ohos.agp.render.Canvas; +import ohos.agp.utils.Color; +import ohos.app.Context; +import ohos.eventhandler.EventHandler; +import ohos.eventhandler.EventRunner; +import ohos.multimodalinput.event.TouchEvent; + +/** + * button + * + * @author:wjt + * @since 2021-04-22 + */ +public class SuperButton extends Button implements Component.DrawTask, + Component.TouchEventListener, Component.ComponentStateChangedListener { + private final int num255 = 255; + private final int down = 18432; + private final int up = 2048; + private final float numldq = 0.7F; + private float mPhase; + private float[] mDashPath; + + private int strokeWidth; + private float cornersRadius; + private float[] cornersRadius2; + + private RgbColor strokeColor; + private RgbColor normalColor; + private RgbColor selectColor; + private RgbColor[] gradientColors; + private RgbColor disabledColor; + + private StateElement mBackground; + + private EventHandler mHandle = new EventHandler(EventRunner.getMainEventRunner()); + + private int shape = ShapeElement.RECTANGLE; + private ShapeElement.Orientation gorientation5; + + /** + * 构造函数 + * + * @param context context + */ + public SuperButton(Context context) { + this(context, null); + } + + /** + * 构造 + * + * @param context + * @param attrSet + */ + public SuperButton(Context context, AttrSet attrSet) { + this(context, attrSet, null); + } + + /** + * 构造 + * + * @param context + * @param attrSet + * @param styleName + */ + public SuperButton(Context context, AttrSet attrSet, String styleName) { + super(context, attrSet, styleName); + parseAttrSet(attrSet); + addDrawTask(this); + if (isClickable() && isEnabled()) { + setComponentStateChangedListener(this); + } + } + + private void parseAttrSet(AttrSet attrSet) { + Color defaultColor = new Color(); + shape = AttrValue.get(attrSet, "super_shape", ShapeElement.RECTANGLE); + Color normalColor2 = AttrValue.get(attrSet, "super_normal_color", defaultColor); + if (normalColor2.getValue() != 0) { + normalColor = RgbColor.fromArgbInt(normalColor2.getValue()); + } + Color selectColor2 = AttrValue.get(attrSet, "super_select_color", defaultColor); + if (selectColor2.getValue() != 0) { + selectColor = RgbColor.fromArgbInt(selectColor2.getValue()); + } + int strokeWidth2 = AttrValue.get(attrSet, "super_stroke_width", 0); + if (strokeWidth2 != 0) { + strokeWidth = AttrHelper.vp2px(strokeWidth2, getContext()); + } + Color strokeColor2 = AttrValue.get(attrSet, "super_stroke_color", defaultColor); + if (strokeColor2.getValue() != 0) { + strokeColor = RgbColor.fromArgbInt(strokeColor2.getValue()); + } + int cornersRadius3 = AttrValue.get(attrSet, "super_corners_radius", 0); + if (cornersRadius3 != 0) { + cornersRadius = AttrHelper.vp2px(cornersRadius3, getContext()); + } + if (shape == ShapeElement.RECTANGLE) { + int leftTop = AttrValue.get(attrSet, "super_lef_top_radius", 0); + int leftBottom = AttrValue.get(attrSet, "super_lef_bottom_radius", 0); + int rightTop = AttrValue.get(attrSet, "super_right_top_radius", 0); + int rightBottom = AttrValue.get(attrSet, "super_right_bottom_radius", 0); + cornersRadius2 = new float[]{leftTop, leftBottom, rightTop, rightBottom}; + } + Color notClickColor3 = AttrValue.get(attrSet, "super_disabled_color", defaultColor); + if (notClickColor3.getValue() != 0) { + disabledColor = RgbColor.fromArgbInt(notClickColor3.getValue()); + } + Color startColor3 = AttrValue.get(attrSet, "super_g_start_color", defaultColor); + Color endColor3 = AttrValue.get(attrSet, "super_g_end_color", defaultColor); + if (startColor3.getValue() != 0 && endColor3.getValue() != 0) { + RgbColor startColor = RgbColor.fromArgbInt(startColor3.getValue()); + RgbColor endColor = RgbColor.fromArgbInt(endColor3.getValue()); + gradientColors = new RgbColor[]{startColor, endColor}; + } + } + + @Override + public void onDraw(Component component, Canvas canvas) { + init(); + setBackground(mBackground); + } + + private void init() { + mBackground = new StateElement(); + apply(); + } + + private void apply() { + if (selectColor != null) { + applyPressedElement(); + } + if (disabledColor != null) { + applyDisabledElement(); + } + applyEmptyElement(); + } + + private void applyEmptyElement() { + ShapeElement mStateEmptyElement = new ShapeElement(); + mStateEmptyElement.setShape(shape); + if (normalColor != null) { + mStateEmptyElement.setRgbColor(normalColor); + } else { + normalColor = new RgbColor(num255, num255, num255, num255); + mStateEmptyElement.setRgbColor(normalColor); + } + if (strokeWidth != 0 && strokeColor != null) { + mStateEmptyElement.setStroke(strokeWidth, strokeColor); + } + if (cornersRadius != 0) { + mStateEmptyElement.setCornerRadius(cornersRadius); + } + if (cornersRadius2 != null) { + mStateEmptyElement.setCornerRadiiArray(cornersRadius2); + } + if (gradientColors != null) { + mStateEmptyElement.setRgbColors(gradientColors); + } + if (gorientation5 != null) { + mStateEmptyElement.setGradientOrientation(gorientation5); + } + if (mDashPath != null) { + mStateEmptyElement.setDashPathEffectValues(mDashPath, mPhase); + } + mBackground.addState(new int[]{ComponentState.COMPONENT_STATE_EMPTY}, mStateEmptyElement); + } + + private void applyDisabledElement() { + ShapeElement mDisabledElement = new ShapeElement(); + mDisabledElement.setShape(shape); + mDisabledElement.setRgbColor(disabledColor); + if (cornersRadius != 0) { + mDisabledElement.setCornerRadius(cornersRadius); + } + if (cornersRadius2 != null) { + mDisabledElement.setCornerRadiiArray(cornersRadius2); + } + mBackground.addState(new int[]{ComponentState.COMPONENT_STATE_DISABLED}, mDisabledElement); + } + + private void applyPressedElement() { + ShapeElement mStatePressedElement = new ShapeElement(); + mStatePressedElement.setShape(shape); + mStatePressedElement.setRgbColor(selectColor); + if (cornersRadius != 0) { + mStatePressedElement.setCornerRadius(cornersRadius); + } + if (cornersRadius2 != null) { + mStatePressedElement.setCornerRadiiArray(cornersRadius2); + } + mBackground.addState(new int[]{ComponentState.COMPONENT_STATE_PRESSED}, mStatePressedElement); + } + + /** + * 设置形状 + * + * @param shape + * @return button + */ + public SuperButton setShape(int shape) { + this.shape = shape; + invalidate(); + return this; + } + + /** + * 设置状态选择器颜色 + * + * @param selectColor3 + * @return button + */ + public SuperButton setShapeSelectorColor(RgbColor selectColor3) { + this.selectColor = selectColor3; + invalidate(); + return this; + } + + /** + * 设置填充的颜色 + * + * @param color 颜色 + * @return 对象 + */ + public SuperButton setShapeColor(RgbColor color) { + this.normalColor = color; + invalidate(); + return this; + } + + /** + * 设置边框宽度 + * + * @param strokeWidth3 边框宽度值 + * @param strokeColor3 设置边框颜色 + * @return 对象 + */ + public SuperButton setShapeStrokeWidthColor(int strokeWidth3, RgbColor strokeColor3) { + this.strokeWidth = AttrHelper.vp2px(strokeWidth3, getContext()); + this.strokeColor = strokeColor3; + invalidate(); + return this; + } + + /** + * 设置圆角半径 + * + * @param radius 半径 + * @return 对象 + */ + public SuperButton setShapeCornersRadius(float radius) { + this.cornersRadius = radius; + invalidate(); + return this; + } + + /** + * 分别设置四个角半径 + * + * @param radius + * @return button + */ + public SuperButton setShapeCornersRadius(float[] radius) { + this.cornersRadius2 = radius; + invalidate(); + return this; + } + + /** + * 设置背景渐变方向 + * + * @param orientation + * @return button + */ + public SuperButton setShapeGradientOrientation(ShapeElement.Orientation orientation) { + this.gorientation5 = orientation; + invalidate(); + return this; + } + + /** + * 设置渐变颜色 + * + * @param gradientColors2 + * @return button + */ + public SuperButton setShapeGradientColor(RgbColor[] gradientColors2) { + this.gradientColors = gradientColors2; + invalidate(); + return this; + } + + /** + * 设置不能点击状态的颜色 + * + * @param color + * @return button + */ + public SuperButton setDisabledColor(RgbColor color) { + this.disabledColor = color; + invalidate(); + return this; + } + + /** + * new float[]{10, 21, 32, 43, 54, 65}, 1 + * + * @param dashPath dashPath + * @param phase phase + * @return button + */ + public SuperButton setShapeDashPathEffectValues(float[] dashPath, float phase) { + this.mDashPath = dashPath; + this.mPhase = phase; + invalidate(); + return this; + } + + @Override + public boolean onTouchEvent(Component component, TouchEvent touchEvent) { + if (!isClickable() || !isEnabled()) { + return false; + } + + switch (touchEvent.getAction()) { + case TouchEvent.PRIMARY_POINT_DOWN: + case TouchEvent.POINT_MOVE: + + break; + case TouchEvent.PRIMARY_POINT_UP: + // 更新API5 不起作用了 + this.setAlpha(1F); + break; + default: + break; + } + return true; + } + + @Override + public void setEnabled(boolean isEnabled) { + super.setEnabled(isEnabled); + this.setAlpha(1F); + } + + @Override + public void onComponentStateChanged(Component component, int state) { + switch (state) { + case down: + if (selectColor == null) { + this.setAlpha(numldq); + } + break; + case up: + default: + setAlpha(1); + } + } +} diff --git a/entry/src/main/java/com/huawei/hm_speechutilsdemo/slice/AsrAbilitySlice.java b/entry/src/main/java/com/huawei/hm_speechutilsdemo/slice/AsrAbilitySlice.java new file mode 100644 index 0000000000000000000000000000000000000000..6a4c7cd08eadf34844f7b62a488948d7bab9eeb7 --- /dev/null +++ b/entry/src/main/java/com/huawei/hm_speechutilsdemo/slice/AsrAbilitySlice.java @@ -0,0 +1,72 @@ +/* + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.huawei.hm_speechutilsdemo.slice; + +import com.huawei.hm_speechutilsdemo.ResourceTable; +import ee.ioc.phon.android.speechutils.abilityslice.AsrBaseAbilitySlice; +import ee.ioc.phon.android.speechutils.view.MicButton; +import ohos.agp.components.Text; +import ohos.hiviewdfx.HiLog; +import ohos.hiviewdfx.HiLogLabel; +import ohos.multimodalinput.event.TouchEvent; + +/** + * 语音转文字 + * + * @author:wjt + * @since 2021-03-20 + */ +public class AsrAbilitySlice extends AsrBaseAbilitySlice { + private static final HiLogLabel LABEL_LOG = new HiLogLabel(3, 0xD001100, "AsrAbilitySlice"); + private MicButton recorderView; + private Text text; + + @Override + public int setLayout() { + return ResourceTable.Layout_ability_asr; + } + + @Override + public void initView() { + recorderView = (MicButton) findComponentById(ResourceTable.Id_recorder); + text = (Text) findComponentById(ResourceTable.Id_text); + recorderView.setTouchEventListener((component, touchEvent) -> { + switch (touchEvent.getAction()) { + case TouchEvent.PRIMARY_POINT_DOWN: + HiLog.info(LABEL_LOG, "按钮按下"); + startRecoding(); + break; + case TouchEvent.PRIMARY_POINT_UP: + HiLog.info(LABEL_LOG, "按钮松开"); + stopRecoding(); + break; + default: + break; + } + return false; + }); + } + + @Override + public void showRmsChanged(float chage) { + recorderView.setRmsdbLevel(chage); + } + + @Override + public void showAsrResult(String info) { + text.setText(info); + } +} diff --git a/entry/src/main/java/com/huawei/hm_speechutilsdemo/slice/MainAbilitySlice.java b/entry/src/main/java/com/huawei/hm_speechutilsdemo/slice/MainAbilitySlice.java new file mode 100644 index 0000000000000000000000000000000000000000..3971da364825cca5f0d69f67b87321b9e3733185 --- /dev/null +++ b/entry/src/main/java/com/huawei/hm_speechutilsdemo/slice/MainAbilitySlice.java @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.huawei.hm_speechutilsdemo.slice; + +import com.huawei.hm_speechutilsdemo.ListProvider; +import com.huawei.hm_speechutilsdemo.ResourceTable; +import ee.ioc.phon.android.speechutils.utils.LogUtils; +import ohos.aafwk.ability.AbilitySlice; +import ohos.aafwk.content.Intent; +import ohos.agp.components.Component; +import ohos.agp.components.ListContainer; +import ohos.agp.window.service.WindowManager; +import ohos.global.resource.NotExistException; +import ohos.global.resource.ResourceManager; +import ohos.global.resource.WrongTypeException; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; + +/** + * MainAbilitySlice + * + * @author:wjt + * @since 2021-03-20 + */ +public class MainAbilitySlice extends AbilitySlice { + private final String mainAbilitySliceTag = "test"; + + @Override + public void onStart(Intent intent) { + super.onStart(intent); + getWindow().addFlags(WindowManager.LayoutConfig.MARK_TRANSLUCENT_STATUS); + getWindow().addFlags(WindowManager.LayoutConfig.MARK_FULL_SCREEN); + super.setUIContent(ResourceTable.Layout_ability_main); + + try { + initListContainer(); + } catch (NotExistException e) { + LogUtils.log(LogUtils.DEBUG, mainAbilitySliceTag, e.getMessage()); + } catch (WrongTypeException e) { + LogUtils.log(LogUtils.DEBUG, mainAbilitySliceTag, e.getMessage()); + } catch (IOException e) { + LogUtils.log(LogUtils.DEBUG, mainAbilitySliceTag, e.getMessage()); + } + } + + private void initListContainer() throws NotExistException, WrongTypeException, IOException { + ListContainer listContainer = (ListContainer) findComponentById(ResourceTable.Id_listText); + List list = new ArrayList(); + ResourceManager resManager = getContext().getResourceManager(); + String speechToText = resManager.getElement(ResourceTable.String_SpeechToText).getString(); + String textToSpeech = resManager.getElement(ResourceTable.String_TextToSpeech).getString(); + list.add(speechToText); + list.add(textToSpeech); + ListProvider listProvider = new ListProvider(this, list); + listContainer.setItemProvider(listProvider); + listContainer.setItemClickedListener(new ListContainer.ItemClickedListener() { + @Override + public void onItemClicked(ListContainer listContainer, Component component, int i, long l) { + String item = (String) listContainer.getItemProvider().getItem(i); + if (item.equals(getAbility().getString(ResourceTable.String_SpeechToText))) { + /** + * 语音转文字 + */ + AbilitySlice abilitySlice = new AsrAbilitySlice(); + Intent intent1 = new Intent(); + present(abilitySlice, intent1); + } else { + /** + * 文字转语音 + */ + AbilitySlice abilitySlice = new TtsAbilitySlice(); + Intent intent1 = new Intent(); + present(abilitySlice, intent1); + } + } + }); + } + + @Override + public void onActive() { + super.onActive(); + } + + @Override + public void onForeground(Intent intent) { + super.onForeground(intent); + } +} diff --git a/entry/src/main/java/com/huawei/hm_speechutilsdemo/slice/TtsAbilitySlice.java b/entry/src/main/java/com/huawei/hm_speechutilsdemo/slice/TtsAbilitySlice.java new file mode 100644 index 0000000000000000000000000000000000000000..574009e4fb03bd6d4803ad90228fdffe8e9f8da7 --- /dev/null +++ b/entry/src/main/java/com/huawei/hm_speechutilsdemo/slice/TtsAbilitySlice.java @@ -0,0 +1,93 @@ +/* + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.huawei.hm_speechutilsdemo.slice; + +import com.huawei.hm_speechutilsdemo.ResourceTable; +import ee.ioc.phon.android.speechutils.abilityslice.TtsBaseAbilitySlice; +import ohos.agp.components.Button; +import ohos.agp.components.Component; +import ohos.agp.components.Text; +import ohos.hiviewdfx.HiLog; +import ohos.hiviewdfx.HiLogLabel; + +/** + * 文字转语音 + * + * @author:wjt + * @since 2021-03-20 + */ +public class TtsAbilitySlice extends TtsBaseAbilitySlice { + private static final HiLogLabel LABEL_LOG = new HiLogLabel(3, 0xD001100, "TTSAbilitySlice"); + /** + * 需要转换为语音的文字 + */ + private Text textPhonetic; + /** + * 开始转换按钮 + */ + private Button startPlay; + /** + * 开始播放按钮 + */ + private Button stopPlay; + /** + * 转换耗时 + */ + private Text textTimes; + + @Override + public int setLayout() { + return ResourceTable.Layout_ability_tts; + } + + @Override + public void initView() { + textPhonetic = (Text) findComponentById(ResourceTable.Id_text_phonetic); + textTimes = (Text) findComponentById(ResourceTable.Id_text_times); + startPlay = (Button) findComponentById(ResourceTable.Id_bt_tts_play); + stopPlay = (Button) findComponentById(ResourceTable.Id_bt_tts_stop); + startPlay.setClickedListener(new Component.ClickedListener() { + @Override + public void onClick(Component component) { + if (isInitItsResult) { + startTts(textPhonetic.getText()); + } else { + HiLog.error(LABEL_LOG, "initItsResult is false"); + } + } + }); + stopPlay.setClickedListener(new Component.ClickedListener() { + @Override + public void onClick(Component component) { + if (isSpeaking()) { + stopSpeaking(); + } else { + HiLog.error(LABEL_LOG, "已经播报完毕或者tts客户端初始化未成功!"); + } + } + }); + } + + /** + * 耗费时间 + * + * @param info + */ + @Override + public void showInfo(String info) { + textTimes.setText(info); + } +} diff --git a/entry/src/main/resources/base/element/string.json b/entry/src/main/resources/base/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..32ac3475ad25b1e05f8cf290a9774721b09c4102 --- /dev/null +++ b/entry/src/main/resources/base/element/string.json @@ -0,0 +1,25 @@ +{ + "string": [ + { + "name": "app_name", + "value": "HM_speechutilsDemo" + }, + { + "name": "mainability_description", + "value": "Java_Phone_Empty Feature Ability" + }, + { + "name": "HelloWorld", + "value": "Hello World" + }, + { + "name": "SpeechToText", + "value": "语音转文字" + } + , + { + "name": "TextToSpeech", + "value": "文字转语音" + } + ] +} \ No newline at end of file diff --git a/entry/src/main/resources/base/graphic/background_ability_main.xml b/entry/src/main/resources/base/graphic/background_ability_main.xml new file mode 100644 index 0000000000000000000000000000000000000000..c0c0a3df480fa387a452b9c40ca191cc918a3fc0 --- /dev/null +++ b/entry/src/main/resources/base/graphic/background_ability_main.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/entry/src/main/resources/base/graphic/background_ability_tst.xml b/entry/src/main/resources/base/graphic/background_ability_tst.xml new file mode 100644 index 0000000000000000000000000000000000000000..b0d30736cffa5d627378f39968f4ebcca78ae55f --- /dev/null +++ b/entry/src/main/resources/base/graphic/background_ability_tst.xml @@ -0,0 +1,10 @@ + + + + + \ No newline at end of file diff --git a/entry/src/main/resources/base/graphic/background_ability_tst_button.xml b/entry/src/main/resources/base/graphic/background_ability_tst_button.xml new file mode 100644 index 0000000000000000000000000000000000000000..b575e98190834c87774a320cc43643186f336f5e --- /dev/null +++ b/entry/src/main/resources/base/graphic/background_ability_tst_button.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/entry/src/main/resources/base/layout/ability_asr.xml b/entry/src/main/resources/base/layout/ability_asr.xml new file mode 100644 index 0000000000000000000000000000000000000000..b654e143ad658e06a9e06865171fe52015c48149 --- /dev/null +++ b/entry/src/main/resources/base/layout/ability_asr.xml @@ -0,0 +1,28 @@ + + + + + + + diff --git a/entry/src/main/resources/base/layout/ability_main.xml b/entry/src/main/resources/base/layout/ability_main.xml new file mode 100644 index 0000000000000000000000000000000000000000..d8ab66d4229bbf43d7767f133681ba8b1b80d51b --- /dev/null +++ b/entry/src/main/resources/base/layout/ability_main.xml @@ -0,0 +1,17 @@ + + + + + diff --git a/entry/src/main/resources/base/layout/ability_speek.xml b/entry/src/main/resources/base/layout/ability_speek.xml new file mode 100644 index 0000000000000000000000000000000000000000..e120c9a67f2f0f4e27ad4c106adb0138142a8113 --- /dev/null +++ b/entry/src/main/resources/base/layout/ability_speek.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/entry/src/main/resources/base/layout/ability_tts.xml b/entry/src/main/resources/base/layout/ability_tts.xml new file mode 100644 index 0000000000000000000000000000000000000000..cb4ccd360bc5b490a558bf8c3b02ed595a5c511a --- /dev/null +++ b/entry/src/main/resources/base/layout/ability_tts.xml @@ -0,0 +1,80 @@ + + + + + + + + + + + diff --git a/entry/src/main/resources/base/layout/item_sample.xml b/entry/src/main/resources/base/layout/item_sample.xml new file mode 100644 index 0000000000000000000000000000000000000000..22647ccc4afbc21f4cb4e0711e45988d1fb16168 --- /dev/null +++ b/entry/src/main/resources/base/layout/item_sample.xml @@ -0,0 +1,26 @@ + + + + + + + \ No newline at end of file diff --git a/entry/src/main/resources/base/media/icon.png b/entry/src/main/resources/base/media/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..ce307a8827bd75456441ceb57d530e4c8d45d36c Binary files /dev/null and b/entry/src/main/resources/base/media/icon.png differ diff --git a/entry/src/ohosTest/config.json b/entry/src/ohosTest/config.json new file mode 100644 index 0000000000000000000000000000000000000000..04b3930aead2b0d86479ceb7b5898eadc2ee6cb3 --- /dev/null +++ b/entry/src/ohosTest/config.json @@ -0,0 +1,41 @@ +{ + "app": { + "bundleName": "com.huawei.hm_speechutilsdemo", + "vendor": "huawei", + "version": { + "code": 1, + "name": "1.0" + }, + "apiVersion": { + "compatible": 5, + "target": 5, + "releaseType": "Beta1" + } + }, + "deviceConfig": {}, + "module": { + "package": "com.huawei.hm_speechutilsdemo", + "name": "testModule", + "deviceType": [ + "phone" + ], + "distro": { + "deliveryWithInstall": true, + "moduleName": "entry_test", + "moduleType": "feature", + "installationFree": true + }, + "abilities": [ + { + "name": "decc.testkit.runner.EntryAbility", + "description": "Test Entry Ability", + "icon": "$media:icon", + "label": "$string:app_name", + "launchType": "standard", + "orientation": "landscape", + "visible": true, + "type": "page" + } + ] + } +} \ No newline at end of file diff --git a/entry/src/ohosTest/java/com/huawei/hm_speechutilsdemo/ExampleOhosTest.java b/entry/src/ohosTest/java/com/huawei/hm_speechutilsdemo/ExampleOhosTest.java new file mode 100644 index 0000000000000000000000000000000000000000..a324f1cd6da91b2e087d8e8abb9a087846d91201 --- /dev/null +++ b/entry/src/ohosTest/java/com/huawei/hm_speechutilsdemo/ExampleOhosTest.java @@ -0,0 +1,14 @@ +package com.huawei.hm_speechutilsdemo; + +import ohos.aafwk.ability.delegation.AbilityDelegatorRegistry; +import org.junit.Test; + +import static org.junit.Assert.assertEquals; + +public class ExampleOhosTest { + @Test + public void testBundleName() { + final String actualBundleName = AbilityDelegatorRegistry.getArguments().getTestBundleName(); + assertEquals("com.huawei.hm_speechutilsdemo", actualBundleName); + } +} \ No newline at end of file diff --git a/entry/src/test/java/com/huawei/hm_speechutilsdemo/ExampleTest.java b/entry/src/test/java/com/huawei/hm_speechutilsdemo/ExampleTest.java new file mode 100644 index 0000000000000000000000000000000000000000..fd60bc8dac85945be75c1be37d59169da68facae --- /dev/null +++ b/entry/src/test/java/com/huawei/hm_speechutilsdemo/ExampleTest.java @@ -0,0 +1,9 @@ +package com.huawei.hm_speechutilsdemo; + +import org.junit.Test; + +public class ExampleTest { + @Test + public void onStart() { + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000000000000000000000000000000000000..0daf1830fbdef07e50a44d74210c8c82f1b66278 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,10 @@ +# Project-wide Gradle settings. +# IDE (e.g. DevEco Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +# If the Chinese output is garbled, please configure the following parameter. +# org.gradle.jvmargs=-Dfile.encoding=GBK diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..490fda8577df6c95960ba7077c43220e5bb2c0d9 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000000000000000000000000000000000..f59159e865d4b59feb1b8c44b001f62fc5d58df4 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://repo.huaweicloud.com/gradle/gradle-6.3-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000000000000000000000000000000000000..2fe81a7d95e4f9ad2c9b2a046707d36ceb3980b3 --- /dev/null +++ b/gradlew @@ -0,0 +1,183 @@ +#!/usr/bin/env sh + +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +## +## Gradle start up script for UN*X +## +############################################################################## + +# Attempt to set APP_HOME +# Resolve links: $0 may be a link +PRG="$0" +# Need this for relative symlinks. +while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=`expr $i + 1` + done + case $i in + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=`save "$@"` + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000000000000000000000000000000000000..62bd9b9ccefea2b65ae41e5d9a545e2021b90a1d --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,103 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/images/ic_service.svg b/images/ic_service.svg deleted file mode 100644 index 324689a7662a99950809d58a66da8b7181cabab9..0000000000000000000000000000000000000000 --- a/images/ic_service.svg +++ /dev/null @@ -1,89 +0,0 @@ - - - - - - image/svg+xml - - - - - - - - - - - - - - - - diff --git a/img/asr.gif b/img/asr.gif new file mode 100644 index 0000000000000000000000000000000000000000..f5aab4d795cc36e1387f708449cf6e931d2583a3 Binary files /dev/null and b/img/asr.gif differ diff --git a/img/tts.gif b/img/tts.gif new file mode 100644 index 0000000000000000000000000000000000000000..5a805cb9a30b4400895e6e1612bf2f7137c335a5 Binary files /dev/null and b/img/tts.gif differ diff --git a/settings.gradle b/settings.gradle index e7b4def49cb53d9aa04228dd3edb14c9e635e003..46528264f83eca9cbd86c397300ef533a7e4eeba 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':app' +include ':entry', ':speechutils' diff --git a/speechutils/.gitignore b/speechutils/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..796b96d1c402326528b4ba3c12ee9d92d0e212e9 --- /dev/null +++ b/speechutils/.gitignore @@ -0,0 +1 @@ +/build diff --git a/speechutils/build.gradle b/speechutils/build.gradle new file mode 100644 index 0000000000000000000000000000000000000000..bebbce584330e96eced0da9962a4c6b1cfd76fc8 --- /dev/null +++ b/speechutils/build.gradle @@ -0,0 +1,22 @@ +apply plugin: 'com.huawei.ohos.library' +ohos { + compileSdkVersion 5 + defaultConfig { + compatibleSdkVersion 5 + } + buildTypes { + release { + proguardOpt { + proguardEnabled false + rulesFiles 'proguard-rules.pro' + } + } + } + +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + testImplementation 'junit:junit:4.13' + implementation 'com.google.code.gson:gson:2.8.4' +} diff --git a/speechutils/proguard-rules.pro b/speechutils/proguard-rules.pro new file mode 100644 index 0000000000000000000000000000000000000000..f7666e47561d514b2a76d5a7dfbb43ede86da92a --- /dev/null +++ b/speechutils/proguard-rules.pro @@ -0,0 +1 @@ +# config module specific ProGuard rules here. \ No newline at end of file diff --git a/speechutils/src/main/config.json b/speechutils/src/main/config.json new file mode 100644 index 0000000000000000000000000000000000000000..86644bd4db671039e55677b95de443ede1f48115 --- /dev/null +++ b/speechutils/src/main/config.json @@ -0,0 +1,28 @@ +{ + "app": { + "bundleName": "com.huawei.hm_speechutilsdemo", + "vendor": "ioc", + "version": { + "code": 1, + "name": "1.0" + }, + "apiVersion": { + "compatible": 5, + "target": 5, + "releaseType": "Beta1" + } + }, + "deviceConfig": { + }, + "module": { + "package": "ee.ioc.phon.android.speechutils", + "deviceType": [ + "phone" + ], + "distro": { + "deliveryWithInstall": true, + "moduleName": "speechutils", + "moduleType": "har" + } + } +} \ No newline at end of file diff --git a/speechutils/src/main/java/ee/ioc/phon/android/speechutils/abilityslice/AsrBaseAbilitySlice.java b/speechutils/src/main/java/ee/ioc/phon/android/speechutils/abilityslice/AsrBaseAbilitySlice.java new file mode 100644 index 0000000000000000000000000000000000000000..488e435ae2d47f01aa4aceb44e144eae419319e4 --- /dev/null +++ b/speechutils/src/main/java/ee/ioc/phon/android/speechutils/abilityslice/AsrBaseAbilitySlice.java @@ -0,0 +1,275 @@ +/* + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain an copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ee.ioc.phon.android.speechutils.abilityslice; + +import com.google.gson.Gson; +import ee.ioc.phon.android.speechutils.utils.Define; +import ee.ioc.phon.android.speechutils.utils.LogUtils; +import ee.ioc.phon.android.speechutils.utils.ResultInfo; +import ohos.aafwk.ability.AbilitySlice; +import ohos.aafwk.content.Intent; +import ohos.ai.asr.AsrClient; +import ohos.ai.asr.AsrIntent; +import ohos.ai.asr.AsrListener; +import ohos.eventhandler.EventHandler; +import ohos.eventhandler.EventRunner; +import ohos.hiviewdfx.HiLog; +import ohos.hiviewdfx.HiLogLabel; +import ohos.media.audio.AudioCapturer; +import ohos.media.audio.AudioCapturerInfo; +import ohos.media.audio.AudioStreamInfo; +import ohos.utils.PacMap; + +/** + * 语音转文字功能类 + * + * @author:wjt + * @since 2021-03-20 + */ +public abstract class AsrBaseAbilitySlice extends AbilitySlice implements RmsChangedInterface, ShowAsrResult { + private static final HiLogLabel LABEL_LOG = new HiLogLabel(3, 0xD001100, "AsrAbilitySlice"); + private AsrClient asrClient = null; + private AudioCapturer mAudioCapturer = null; + private AsrIntent initIntent = null; + private boolean isFlag = false; + private AsrListener mMyAsrListener = new AsrListener() { + @Override + public void onInit(PacMap pacMap) { + HiLog.info(LABEL_LOG, "onInit"); + } + + @Override + public void onBeginningOfSpeech() { + HiLog.info(LABEL_LOG, "onBeginningOfSpeech"); + } + + @Override + public void onRmsChanged(float energyV) { + rmsChanged(energyV); + } + + @Override + public void onBufferReceived(byte[] bytes) { + HiLog.info(LABEL_LOG, "onBufferReceived"); + } + + @Override + public void onEndOfSpeech() { + HiLog.info(LABEL_LOG, "onEndOfSpeech"); + } + + @Override + public void onError(int i) { + HiLog.info(LABEL_LOG, "onError = " + i); + } + + @Override + public void onResults(PacMap pacMap) { + } + + @Override + public void onIntermediateResults(PacMap pacMap) { + results2(pacMap); + HiLog.info(LABEL_LOG, "onIntermediateResults"); + } + + @Override + public void onEnd() { + HiLog.info(LABEL_LOG, "onEnd"); + } + + @Override + public void onEvent(int i, PacMap pacMap) { + HiLog.info(LABEL_LOG, "onEvent"); + } + + @Override + public void onAudioStart() { + } + + @Override + public void onAudioEnd() { + HiLog.info(LABEL_LOG, "onAudioEnd"); + } + }; + + private EventHandler handler = new EventHandler(EventRunner.create()); + private Runnable runnable = new Runnable() { + @Override + public void run() { + HiLog.info(LABEL_LOG, "监听线程启动 = " + Thread.currentThread().getName()); + while (isFlag) { + // buffer需要替换为真实的音频数据 + byte[] buffer = new byte[Define.BYTE_BUFFER]; + /** + * 对于长度大于1280的音频,需要多次调用writePcm分段传输 + */ + mAudioCapturer.read(buffer, 0, buffer.length); + asrClient.writePcm(buffer, buffer.length); + try { + Thread.sleep(Define.SLEEP_INTERVAL); + } catch (InterruptedException e) { + LogUtils.log(LogUtils.DEBUG, "test", e.getMessage()); + } + } + } + }; + + /** + * 监听事件 + */ + + @Override + protected void onStart(Intent intent) { + super.onStart(intent); + super.setUIContent(setLayout()); + initView(); + initAsrClient(); + } + + private void initAsrClient() { + initAudioStream(); + asrClient = AsrClient.createAsrClient(this).orElse(null); + initIntent = new AsrIntent(); + initIntent.setVadFrontWaitMs(Define.YIQIAN); + initIntent.setVadEndWaitMs(Define.YIQIAN); + initIntent.setAudioSourceType(AsrIntent.AsrAudioSrcType.ASR_SRC_TYPE_PCM); + asrClient.init(initIntent, mMyAsrListener); + } + + /** + * 初始化录音 + */ + private void initAudioStream() { + AudioStreamInfo audioStreamInfo = new AudioStreamInfo.Builder() + .encodingFormat(AudioStreamInfo.EncodingFormat.ENCODING_PCM_16BIT) + .channelMask(AudioStreamInfo.ChannelMask.CHANNEL_IN_MONO) + .sampleRate(Define.SAMPLE_RATE) // 采样率 + .build(); + AudioCapturerInfo audioCapturerInfo = new AudioCapturerInfo.Builder().audioStreamInfo(audioStreamInfo) + .build(); + mAudioCapturer = new AudioCapturer(audioCapturerInfo); + } + + /** + * 停止监听 + */ + protected void stopRecoding() { + isFlag = false; + handler.removeTask(runnable); + mAudioCapturer.stop(); + asrClient.stopListening(); + asrClient.cancel(); + HiLog.info(LABEL_LOG, "停止监听"); + } + + /** + * 开始监听 + */ + protected void startRecoding() { + HiLog.info(LABEL_LOG, "开始监听"); + asrClient.startListening(initIntent); + mAudioCapturer.start(); + isFlag = true; + handler.postTask(runnable); + } + + /** + * 能力变化 + * + * @param energyV + */ + private void rmsChanged(float energyV) { + HiLog.info(LABEL_LOG, "onRmsChanged = " + energyV); + getUITaskDispatcher().delayDispatch(new Runnable() { + @Override + public void run() { + int asd = 1; + float energy = energyV; + if (energy > Define.ENERGYYIQN) { + asd = (int) (energy / Define.ENERGSIQIAN); + } else { + asd = (int) (energy / Define.ENERGSANSHI); + } + if (asd < 1) { + asd = 1; + } + showRmsChanged(asd); + HiLog.info(LABEL_LOG, "能量值 = " + asd); + } + }, 0); + } + + /** + * 识别结果 + * + * @param pacMap 数据 + */ + private void results(PacMap pacMap) { + HiLog.info(LABEL_LOG, "onResults"); + asrClient.stopListening(); // 停止识别 + asrClient.cancel(); // 取消识别 + Gson gson = new Gson(); + ResultInfo resultInfo = gson.fromJson(pacMap.getString("results_recognition"), ResultInfo.class); + if (resultInfo != null) { + getUITaskDispatcher().delayDispatch(new Runnable() { + @Override + public void run() { + showAsrResult(resultInfo.getResult().get(0).getWord()); + } + }, 0); + } + } + + /** + * 片段识别 + * + * @param pacMap 数据 + */ + protected void results2(PacMap pacMap) { + HiLog.info(LABEL_LOG, "onResults"); + Gson gson = new Gson(); + ResultInfo resultInfo = gson.fromJson(pacMap.getString("results_partial"), ResultInfo.class); + if (resultInfo != null) { + getUITaskDispatcher().delayDispatch(new Runnable() { + @Override + public void run() { + showAsrResult(resultInfo.getResult().get(0).getWord()); + } + }, 0); + } + } + + /** + * 布局 + * + * @return 布局id + */ + public abstract int setLayout(); + + /** + * 布局 + * + * @return + */ + public abstract void initView(); + + @Override + protected void onStop() { + super.onStop(); + asrClient.destroy(); + } +} diff --git a/speechutils/src/main/java/ee/ioc/phon/android/speechutils/abilityslice/RmsChangedInterface.java b/speechutils/src/main/java/ee/ioc/phon/android/speechutils/abilityslice/RmsChangedInterface.java new file mode 100644 index 0000000000000000000000000000000000000000..0f709763206185da459de7d59aa639a9a116d620 --- /dev/null +++ b/speechutils/src/main/java/ee/ioc/phon/android/speechutils/abilityslice/RmsChangedInterface.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain an copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ee.ioc.phon.android.speechutils.abilityslice; + +/** + * 显示能量波动 + * + * @author:wjt + * @since 2021-03-20 + */ +public interface RmsChangedInterface { + /** + * 能量变化 + * + * @param chage + */ + void showRmsChanged(float chage); +} diff --git a/speechutils/src/main/java/ee/ioc/phon/android/speechutils/abilityslice/ShowAsrResult.java b/speechutils/src/main/java/ee/ioc/phon/android/speechutils/abilityslice/ShowAsrResult.java new file mode 100644 index 0000000000000000000000000000000000000000..16a337effefe88f00585f1bf4e4417cbfc8eca4d --- /dev/null +++ b/speechutils/src/main/java/ee/ioc/phon/android/speechutils/abilityslice/ShowAsrResult.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain an copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ee.ioc.phon.android.speechutils.abilityslice; + +/** + * 显示识别结果 + * + * @author:wjt + * @since 2021-03-20 + */ +public interface ShowAsrResult { + /** + * 识别结果 + * + * @param info + */ + void showAsrResult(String info); +} diff --git a/speechutils/src/main/java/ee/ioc/phon/android/speechutils/abilityslice/TtsBaseAbilitySlice.java b/speechutils/src/main/java/ee/ioc/phon/android/speechutils/abilityslice/TtsBaseAbilitySlice.java new file mode 100644 index 0000000000000000000000000000000000000000..508ce271741edae589f57721ddc4a85673ada74a --- /dev/null +++ b/speechutils/src/main/java/ee/ioc/phon/android/speechutils/abilityslice/TtsBaseAbilitySlice.java @@ -0,0 +1,211 @@ +/* + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain an copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ee.ioc.phon.android.speechutils.abilityslice; + +import ee.ioc.phon.android.speechutils.utils.Define; +import ee.ioc.phon.android.speechutils.utils.DialogUtils; +import ohos.aafwk.ability.AbilitySlice; +import ohos.aafwk.content.Intent; +import ohos.ai.tts.TtsClient; +import ohos.ai.tts.TtsListener; +import ohos.ai.tts.TtsParams; +import ohos.ai.tts.constants.TtsEvent; +import ohos.eventhandler.EventHandler; +import ohos.eventhandler.EventRunner; +import ohos.eventhandler.InnerEvent; +import ohos.hiviewdfx.HiLog; +import ohos.hiviewdfx.HiLogLabel; +import ohos.utils.PacMap; + +import java.util.Timer; +import java.util.TimerTask; +import java.util.UUID; + +/** + * 文字转语音功能类 + * + * @author:wjt + * @since 2021-03-20 + */ +public abstract class TtsBaseAbilitySlice extends AbilitySlice implements TtsShowInfoInterface { + private static final int EVENT_MSG_INIT = 0x1000001; + private static final int EVENT_MSG_TIME_COUNT = 0x1000002; + private static final HiLogLabel LABEL_LOG = new HiLogLabel(3, 0xD001100, "TTSAbilitySlice"); + protected boolean isInitItsResult; + protected String info = ""; + private int time = 0; + private Timer timer = null; + private TimerTask timerTask = null; + + private EventHandler handler = new EventHandler(EventRunner.current()) { + @Override + protected void processEvent(InnerEvent event) { + super.processEvent(event); + switch (event.eventId) { + case EVENT_MSG_TIME_COUNT: + getUITaskDispatcher().delayDispatch(new Runnable() { + @Override + public void run() { + time = time + 1; + HiLog.info(LABEL_LOG, "播报耗时:" + Integer.toString(time) + " s"); + info = "播报耗时:" + Integer.toString(time) + " s"; + showInfo(info); + } + }, 0); + break; + default: + break; + } + } + }; + + /** + * 监听事件 + */ + private TtsListener ttsListener = new TtsListener() { + @Override + public void onEvent(int eventType, PacMap pacMap) { + HiLog.info(LABEL_LOG, "onEvent..."); + if (eventType == TtsEvent.CREATE_TTS_CLIENT_SUCCESS) { + TtsParams ttsParams = new TtsParams(); + ttsParams.setDeviceId(UUID.randomUUID().toString()); + isInitItsResult = TtsClient.getInstance().init(ttsParams); + } + } + + @Override + public void onStart(String utteranceId) { + HiLog.info(LABEL_LOG, "onStart..."); + } + + @Override + public void onProgress(String utteranceId, byte[] audioData, int progress) { + } + + @Override + public void onFinish(String utteranceId) { + HiLog.info(LABEL_LOG, "onFinish..."); + } + + @Override + public void onError(String s, String s1) { + HiLog.info(LABEL_LOG, "onError..."); + } + + @Override + public void onSpeechStart(String utteranceId) { + // 开始计时 + HiLog.info(LABEL_LOG, "onSpeechStart..."); + if (timer == null && timerTask == null) { + timer = new Timer(); + timerTask = new TimerTask() { + public void run() { + handler.sendEvent(EVENT_MSG_TIME_COUNT); + } + }; + timer.schedule(timerTask, 0, Define.YIQIAN); + } + } + + @Override + public void onSpeechProgressChanged(String utteranceId, int progress) { + } + + @Override + public void onSpeechFinish(String utteranceId) { + // 结束计时 + HiLog.info(LABEL_LOG, "onSpeechFinish..."); + timer.cancel(); + time = 0; + timer = null; + timerTask = null; + } + }; + + @Override + protected void onStart(Intent intent) { + super.onStart(intent); + super.setUIContent(setLayout()); + initView(); + initTtsEngine(); + } + + private void initTtsEngine() { + TtsClient.getInstance().release(); + TtsClient.getInstance().create(this, ttsListener); + } + + /** + * 布局 + * + * @return 布局id + */ + public abstract int setLayout(); + + /** + * 初始化view + */ + public abstract void initView(); + + /** + * 开始文字转语音 + * + * @param message 要转化的文字 + */ + protected void startTts(String message) { + HiLog.info(LABEL_LOG, "initItsResult is true, speakText"); + if (isInitItsResult) { + TtsClient.getInstance().stopSpeak(); + TtsClient.getInstance().speakLongText(message, null); + } else { + DialogUtils.showToast("初始化失败!无法播放!", getAbility()); + } + } + + /** + * 判断是否正在进行TTS播报。如果正在进行TTS播报,返回true;否则,返回false。 + * + * @return 是否正在进行TTS播报 + */ + protected boolean isSpeaking() { + boolean isFlag = false; + if (isInitItsResult) { + isFlag = TtsClient.getInstance().isSpeaking(); + } + return isFlag; + } + + /** + * 停止播放 + */ + protected void stopSpeaking() { + if (isInitItsResult) { + if (isSpeaking()) { + TtsClient.getInstance().stopSpeak(); + timer.cancel(); + time = 0; + timer = null; + timerTask = null; + } + } + } + + @Override + protected void onStop() { + super.onStop(); + TtsClient.getInstance().destroy(); + } +} diff --git a/speechutils/src/main/java/ee/ioc/phon/android/speechutils/abilityslice/TtsShowInfoInterface.java b/speechutils/src/main/java/ee/ioc/phon/android/speechutils/abilityslice/TtsShowInfoInterface.java new file mode 100644 index 0000000000000000000000000000000000000000..80d0d56e8198806cd9fa12d1df155dee2cd908fc --- /dev/null +++ b/speechutils/src/main/java/ee/ioc/phon/android/speechutils/abilityslice/TtsShowInfoInterface.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain an copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ee.ioc.phon.android.speechutils.abilityslice; + +/** + * 显示耗费时间 + * + * @author:wjt + * @since 2021-03-20 + */ +public interface TtsShowInfoInterface { + /** + * 显示时间 + * + * @param info + */ + void showInfo(String info); +} diff --git a/speechutils/src/main/java/ee/ioc/phon/android/speechutils/utils/Define.java b/speechutils/src/main/java/ee/ioc/phon/android/speechutils/utils/Define.java new file mode 100644 index 0000000000000000000000000000000000000000..941e5e481ef4801c70e48658cfa7936bfacf92d0 --- /dev/null +++ b/speechutils/src/main/java/ee/ioc/phon/android/speechutils/utils/Define.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain an copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ee.ioc.phon.android.speechutils.utils; + +/** + * 静态常量 + * + * @author:wjt + * @since 2021-03-20 + */ +public class Define { + /** + * 音频权限 + */ + public static final String AUDIO = "android.permission.RECORD_AUDIO"; + /** + * 麦克风权限 + */ + public static final String MICROPHONE = "ohos.permission.MICROPHONE"; + /** + * 录音 + */ + public static final String SHORTCUTS = "ohos.permission.MANAGE_SHORTCUTS"; + /** + * 1000 + */ + public static final int YIQIAN = 1000; + /** + * 2000 + */ + public static final int ERQIAN = 2000; + /** + * -1 + */ + public static final int FUYI = -1; + /** + * 10000000 + */ + public static final int ENERGYYIQN = 10000000; + /** + * 4000000 + */ + public static final int ENERGSIQIAN = 4000000; + /** + * 300000 + */ + public static final int ENERGSANSHI = 300000; + /** + * 比特率 + */ + public static final int SAMPLE_RATE = 16000; + /** + * 1280 + */ + public static final int BYTE_BUFFER = 1280; + /** + * 50 + */ + public static final int SLEEP_INTERVAL = 50; + + /** + * 构造函数 + */ + private Define() { + } +} diff --git a/speechutils/src/main/java/ee/ioc/phon/android/speechutils/utils/DialogUtils.java b/speechutils/src/main/java/ee/ioc/phon/android/speechutils/utils/DialogUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..e38844a6ec4f6cb6503b36e5424773287e0db2e1 --- /dev/null +++ b/speechutils/src/main/java/ee/ioc/phon/android/speechutils/utils/DialogUtils.java @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain an copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ee.ioc.phon.android.speechutils.utils; + +import ee.ioc.phon.android.speechutils.ResourceTable; +import ohos.aafwk.ability.Ability; +import ohos.agp.components.DirectionalLayout; +import ohos.agp.components.LayoutScatter; +import ohos.agp.components.Text; +import ohos.agp.utils.LayoutAlignment; +import ohos.agp.window.dialog.ToastDialog; + +/** + * 描述 + * + * @author name + * @since 2021-04-16 + */ +public class DialogUtils { + private static final int NUM50 = -50; + private static final int NUM3000 = 3000; + + /** + * 构造 + */ + private DialogUtils() { + } + + /** + * 显示Toast + * + * @param msg + * @param ability + */ + public static void showToast(String msg, Ability ability) { + DirectionalLayout layout = (DirectionalLayout) LayoutScatter.getInstance(ability) + .parse(ResourceTable.Layout_layout_toast_and_image, null, false); + Text text = (Text) layout.findComponentById(ResourceTable.Id_msg_toast); + text.setText(msg); + new ToastDialog(ability) + .setComponent(layout) + .setSize(DirectionalLayout.LayoutConfig.MATCH_PARENT, DirectionalLayout.LayoutConfig.MATCH_CONTENT) + .setAlignment(LayoutAlignment.BOTTOM) + .setOffset(0, NUM50) + .setDuration(NUM3000) + .show(); + } +} diff --git a/speechutils/src/main/java/ee/ioc/phon/android/speechutils/utils/LogUtils.java b/speechutils/src/main/java/ee/ioc/phon/android/speechutils/utils/LogUtils.java new file mode 100644 index 0000000000000000000000000000000000000000..be028ca9d2e042885d464a12837ad095370f84f7 --- /dev/null +++ b/speechutils/src/main/java/ee/ioc/phon/android/speechutils/utils/LogUtils.java @@ -0,0 +1,94 @@ +/* + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain an copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ee.ioc.phon.android.speechutils.utils; + +import ohos.hiviewdfx.HiLog; +import ohos.hiviewdfx.HiLogLabel; + +/** + * description 日志类 + * + * @author wjt + * @since 2021-03-20 + */ +public class LogUtils { + /** + * 持续时间 + */ + public static final int DURATION = 2000; + + /** + * info + */ + public static final int INFO = 0; + /** + * error + */ + public static final int ERROR = 1; + /** + * debug + */ + public static final int DEBUG = 2; + /** + * warning + */ + public static final int WARN = 3; + /** + * 返回码 + */ + public static final int REQUEST_CODE = 300; + /** + * System + */ + public static final int SYETEM = 6; + /** + * 持续时间 + */ + public static final String TAG = "wjtt"; + + /** + * 构造函数 + */ + private LogUtils() { + } + + /** + * Log + * + * @param logType LogUtils.INFO || LogUtils.ERROR || LogUtils.DEBUG|| LogUtils.WARN + * @param tag 日志标识 根据喜好,自定义 + * @param message 需要打印的日志信息 + */ + public static void log(int logType, String tag, String message) { + HiLogLabel lable = new HiLogLabel(HiLog.LOG_APP, 0x0, tag); + switch (logType) { + case INFO: + HiLog.info(lable, message); + break; + case ERROR: + HiLog.error(lable, message); + break; + case DEBUG: + HiLog.debug(lable, message); + break; + case WARN: + HiLog.warn(lable, message); + break; + default: + break; + } + } +} diff --git a/speechutils/src/main/java/ee/ioc/phon/android/speechutils/utils/ResultInfo.java b/speechutils/src/main/java/ee/ioc/phon/android/speechutils/utils/ResultInfo.java new file mode 100644 index 0000000000000000000000000000000000000000..3c89f61f2c98343e55305bbd103539344f860e82 --- /dev/null +++ b/speechutils/src/main/java/ee/ioc/phon/android/speechutils/utils/ResultInfo.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain an copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ee.ioc.phon.android.speechutils.utils; + +import java.util.List; + +/** + * 转换成功后的信息 + * + * @author: wjt + * @since 2021-03-30 + */ +public class ResultInfo { + private String resultType; + private List result; + + public String getResultType() { + return resultType; + } + + public void setResultType(String resultType) { + this.resultType = resultType; + } + + public List getResult() { + return result; + } + + public void setResult(List result) { + this.result = result; + } + + /** + * 转换成功后的具体信息 + * + * @author: wjt + * @since 2021-03-30 + */ + public static class ResultBean { + /** + * confidence : 0.9800869226455688 + * end_time : 1720 + * ori_word : 你 好 你 好 你 好 你 好 + * pinyin : ni3 hao3 ni3 hao3 ni3 hao3 ni3 hao3 + * start_time : 440 + * word : 你好你好你好你好。 + */ + + private double confidence; + private int endTime; + private String oriWord; + private String pinyin; + private int startTime; + private String word; + + public double getConfidence() { + return confidence; + } + + public void setConfidence(double confidence) { + this.confidence = confidence; + } + + public int getEndTime() { + return endTime; + } + + public void setEndTime(int endTime) { + this.endTime = endTime; + } + + public String getOriWord() { + return oriWord; + } + + public void setOriWord(String oriWord) { + this.oriWord = oriWord; + } + + public String getPinyin() { + return pinyin; + } + + public void setPinyin(String pinyin) { + this.pinyin = pinyin; + } + + public int getStartTime() { + return startTime; + } + + public void setStartTime(int startTime) { + this.startTime = startTime; + } + + public String getWord() { + return word; + } + + public void setWord(String word) { + this.word = word; + } + } +} diff --git a/speechutils/src/main/java/ee/ioc/phon/android/speechutils/view/MicButton.java b/speechutils/src/main/java/ee/ioc/phon/android/speechutils/view/MicButton.java new file mode 100644 index 0000000000000000000000000000000000000000..c8180aa93bb22b581fed45f6452f1b75a65cd4e1 --- /dev/null +++ b/speechutils/src/main/java/ee/ioc/phon/android/speechutils/view/MicButton.java @@ -0,0 +1,259 @@ +/* + * Copyright (C) 2021 Huawei Device Co., Ltd. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain an copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package ee.ioc.phon.android.speechutils.view; + +import ee.ioc.phon.android.speechutils.ResourceTable; +import ee.ioc.phon.android.speechutils.utils.LogUtils; +import ohos.agp.components.AttrSet; +import ohos.agp.components.Component; +import ohos.agp.render.Canvas; +import ohos.agp.render.Paint; +import ohos.agp.render.PixelMapHolder; +import ohos.agp.utils.Color; +import ohos.agp.utils.RectFloat; +import ohos.app.Context; +import ohos.global.resource.NotExistException; +import ohos.media.image.ImageSource; +import ohos.media.image.PixelMap; +import ohos.media.image.common.PixelFormat; +import ohos.media.image.common.Rect; +import ohos.media.image.common.Size; + +import java.io.IOException; +import java.io.InputStream; + +/** + * 话筒 + * + * @author:wjt + * @since 2021-03-20 + */ +public class MicButton extends Component implements Component.EstimateSizeListener, Component.DrawTask { + /** + * more线条颜色 + */ + public static final int COLOR_INDICATOR_DEFAULT = 0xff3F51B5; + /** + * COLOR_INDICATOR_GONE + */ + public static final int COLOR_INDICATOR_GONE = 0x00000000; + private static final int ROTATION_SPEED = 1; + /** + * ER + */ + private static final int ER = 2; + /** + * RGB + */ + private static final int RGB = 0xff0000; + /** + * RGB2 + */ + private static final int RGB2 = 0x00ff00; + /** + * RGB3 + */ + private static final int RGB3 = 0x0000ff; + /** + * 2f + */ + private static final float ERF = 2f; + /** + * 1f + */ + private static final float YIF = 1f; + /** + * 0.45 + */ + private static final float NUM_045 = 0.45f; + /** + * 0.55f + */ + private static final float NUM_055 = 0.55f; + /** + * 0.01f + */ + private static final float NUM_001 = 0.01f; + private static final String MICBUTTONTAG = "MICTAG"; + private final float num20 = 20f; + private float rmsdbLevel = 0; + private float oldRadius; + private PixelMapHolder pixelMapHolder; + private RectFloat rectSrc; + + private int width = 0; + private int height = 0; + private int min = 0; + private int imageSize; + private Paint backgroundPaint; + private Paint wavePaint; + + /** + * 构造参数 + * + * @param context 上下文 + */ + public MicButton(Context context) { + super(context); + init(); + } + + /** + * 构造参数 + * + * @param context 上下文 + * @param attrs 属性参数 + */ + public MicButton(Context context, AttrSet attrs) { + super(context, attrs); + init(); + } + + /** + * 构造参数 + * + * @param context 上下文 + * @param attrs 属性参数 + * @param defStyleAttr 属性样式 + */ + public MicButton(Context context, AttrSet attrs, int defStyleAttr) { + super(context, attrs, defStyleAttr); + init(); + } + + /** + * 测量方法与Android中onMeasure相似 + * + * @param widthMeasureSpec + * @param heightMeasureSpec + * @return + */ + @Override + public boolean onEstimateSize(int widthMeasureSpec, int heightMeasureSpec) { + width = EstimateSpec.getSize(widthMeasureSpec); + height = EstimateSpec.getSize(heightMeasureSpec); + min = Math.min(width, height); + imageSize = (int) (min * NUM_045); + setRmsdbLevel(1); + return false; + } + + private void init() { + backgroundPaint = new Paint(); + backgroundPaint.setColor(new Color(Color.getIntColor("#0080ff"))); + backgroundPaint.setStyle(Paint.Style.FILLANDSTROKE_STYLE); + wavePaint = new Paint(); + Color color = new Color(COLOR_INDICATOR_DEFAULT); + wavePaint.setColor(color); + wavePaint.setAntiAlias(true); + wavePaint.setStyle(Paint.Style.STROKE_STYLE); + wavePaint.setStrokeWidth(num20); + addDrawTask(this); + setEstimateSizeListener(this); + oldRadius = defaultRadius(); + } + + /** + * 1-30 + * + * @param level + */ + public void setRmsdbLevel(float level) { + rmsdbLevel = level; + invalidate(); + } + + /** + * 设置颜色 + * + * @param color + */ + public void setIndicatorColor(int color) { + wavePaint.setColor(new Color(color)); + invalidate(); + } + + @Override + public void invalidate() { + super.invalidate(); + addDrawTask(this::onDraw); + } + + @Override + public void onDraw(Component component, Canvas canvas) { + canvas.drawCircle(width / ER, height / ER, getRadius(), wavePaint); + canvas.drawCircle(width / ER, height / ER, getRadius(), backgroundPaint); + PixelMap pixelMap = getPixelMap(ResourceTable.Media_microphone); + rectSrc = new RectFloat((width - imageSize) / ER, + (height - imageSize) / ER, + width - ((width - imageSize) / ER), height - ((height - imageSize) / ER)); + pixelMapHolder = new PixelMapHolder(pixelMap); + canvas.drawPixelMapHolderRect(pixelMapHolder, rectSrc, wavePaint); + } + + private float getRadius() { + float percent = (float) (rmsdbLevel * Math.log(rmsdbLevel)) * NUM_001; + percent = Math.min(Math.max(percent, 0f), YIF); + percent = NUM_055 + NUM_045 * percent; + return percent * ((float) min) / ERF; + } + + /** + * 默认圆 + * + * @return + */ + private float defaultRadius() { + float percent = (float) (1 * Math.log(0)) * NUM_001; + percent = Math.min(Math.max(percent, 0f), YIF); + percent = NUM_055 + NUM_045 * percent; + return percent * ((float) min) / ERF; + } + + /** + * 通过资源ID获取位图对象 + **/ + private PixelMap getPixelMap(int resId) { + InputStream drawableInputStream = null; + PixelMap pixelMap = null; + ImageSource imageSource = null; + try { + drawableInputStream = getResourceManager().getResource(resId); + ImageSource.SourceOptions sourceOptions = new ImageSource.SourceOptions(); + sourceOptions.formatHint = "image/png"; + imageSource = ImageSource.create(drawableInputStream, null); + ImageSource.DecodingOptions decodingOptions = new ImageSource.DecodingOptions(); + decodingOptions.desiredSize = new Size(0, 0); + decodingOptions.desiredRegion = new Rect(0, 0, 0, 0); + decodingOptions.desiredPixelFormat = PixelFormat.ARGB_8888; + pixelMap = imageSource.createPixelmap(decodingOptions); + return pixelMap; + } catch (IOException ioException) { + LogUtils.log(LogUtils.DEBUG, MICBUTTONTAG, ioException.getMessage()); + } catch (NotExistException notExistException) { + LogUtils.log(LogUtils.DEBUG, MICBUTTONTAG, "notExistException"); + } finally { + if (drawableInputStream != null) { + try { + drawableInputStream.close(); + } catch (IOException e) { + LogUtils.log(LogUtils.DEBUG, MICBUTTONTAG, e.getMessage()); + } + } + } + return pixelMap; + } +} diff --git a/speechutils/src/main/resources/base/element/string.json b/speechutils/src/main/resources/base/element/string.json new file mode 100644 index 0000000000000000000000000000000000000000..b1712549d330191fd3b43d4eafbd65e901c4cb4a --- /dev/null +++ b/speechutils/src/main/resources/base/element/string.json @@ -0,0 +1,8 @@ +{ + "string": [ + { + "name": "app_name", + "value": "speechutils" + } + ] +} diff --git a/speechutils/src/main/resources/base/graphic/background_toast_element.xml b/speechutils/src/main/resources/base/graphic/background_toast_element.xml new file mode 100644 index 0000000000000000000000000000000000000000..f51698fdb009b1b92595fc034aaed2a9270e24a3 --- /dev/null +++ b/speechutils/src/main/resources/base/graphic/background_toast_element.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/speechutils/src/main/resources/base/layout/layout_toast_and_image.xml b/speechutils/src/main/resources/base/layout/layout_toast_and_image.xml new file mode 100644 index 0000000000000000000000000000000000000000..bca763a283bf344a300955bdb07e950568994c90 --- /dev/null +++ b/speechutils/src/main/resources/base/layout/layout_toast_and_image.xml @@ -0,0 +1,20 @@ + + + + \ No newline at end of file diff --git a/speechutils/src/main/resources/base/media/microphone.png b/speechutils/src/main/resources/base/media/microphone.png new file mode 100644 index 0000000000000000000000000000000000000000..d859c6ff1f3311afe975a046bb25a38ac0614005 Binary files /dev/null and b/speechutils/src/main/resources/base/media/microphone.png differ diff --git a/speechutils/src/test/java/ee/ioc/phon/android/speechutils/ExampleTest.java b/speechutils/src/test/java/ee/ioc/phon/android/speechutils/ExampleTest.java new file mode 100644 index 0000000000000000000000000000000000000000..66e3cb0a57aa63173916a858004654a24b076328 --- /dev/null +++ b/speechutils/src/test/java/ee/ioc/phon/android/speechutils/ExampleTest.java @@ -0,0 +1,9 @@ +package ee.ioc.phon.android.speechutils; + +import org.junit.Test; + +public class ExampleTest { + @Test + public void onStart() { + } +}