안드로이드 앱 개발기 1
구글 음성인식 API를 활용한 기록기
일하는 주변에 필요한 도구를 만들어 쓰기로 했다. 휴대폰에 사용되는 안드로이드 os가 휴대용 PC인데 그 기능을 잘 활용하면 현장의 많은 삽질을 줄일 수 있다. 나의 코딩 등급 및 JAVA에 대한 이해도는 미미하지만, stack overflow와 구글신을 믿어 해보기로 했다. 이 개발이 끝나면 JAVA 사용 방법, HSM(fsm) 적용, 파일 저장 등의 기술이 늘 것 같다.
옛날에 fsm을 공부하기 위해 퀀텀 프로그래밍을 사서 읽었다. 이 책에 HSM에 대한 소개가 있었다. fsm을 확장하여 hierarchy로 정의한 fsm인데, 그런 방법으로 개발하면 쉽다고 했다. 문제는 이 사람이 C로 코드를 소개를 했다. 이를 JAVA로 옮겨야 되는데, 내 능력으로는 불가능 했다. 3년전에 인터넷을 열심히 뒤진 결과 Alexei Krasnopolski가 자바로 변경한 결과를 찾았다. fsm을 잘 사용하려면 uml을 정확하게 알아야 되는데, 대충 알고 있다. 나중에 문제되는 부분을 따로 찾아 보기로 했다.
내가 원하는 기능
아래의 기능을 갖는 앱을 개발하려고 한다.
- 구글의 음성인식을 활용하여, 말하는 내용을 텍스트로 변환
- layout에 번호를 할당하여 처음부터 끝까지 연속적인 인식
- 사용자 입력에 대한 android 판단은 소리로 표시
- 연속 작업 도중 틀린 부분은 버튼을 눌러 수정
- 수정 작업이 완료되면 다음 연속 작업으로 다시 복귀
- 모든 작업이 끝나면 파일로 저장
아래의 방향으로 접근 하기로 했다.
- 구글 음성인식 API 사용
- hsm의 구성 및 설정
- event 전달 방법
- thread 사용 방법
- handler
- 파일 저장
- android studio 사용
구글 음성 인식은 speech to text라고 하는데, 과거 text to speech의 역버전이다. 한국의 네이버도 이런 기능을 지원한다. 그러나 안드로이드에서 적절하게 사용이 가능한지 모르겠고, 별도 API를 알아야 된다. 게다가 전세계 개발자들이 구글을 많이 사용하여 내가 원하는 답이 많다. 이런 저런 이유로 구글을 사용했다. 안드로이드 내부에서 바로 구현이 가능하다. 샘플 코드는 인터넷에 구했다.
전에는 eclipse를 안드로이드 개발이 가능하도록 세팅했는데, 요새 무슨 변화가 있었는지 최신 버전은 지원하지 않았다. 테마 관련 에러도 많이 뜨고..구글이 공식앱 개발툴이라고 밀고 있는 android studio를 사용했다. 인터넷에서 다운로드 하여 사용 하면 된다.
Hierarchy-finite State Machine 정의
일단 아래 그림과 같이 접근 하기로 했다.
이 그림이 정확한지 해보지 않고는 정확한 판단을 할 수 없어, 일단 해보고 나중에 다시 보완하기로 했다.
package com.example.now0930.myapplication; /** * Created by now0930 on 17. 2. 26. */ import android.app.Activity; import android.os.Handler; import android.os.Message; import android.provider.ContactsContract; import android.speech.SpeechRecognizer; import android.util.Log; import com.example.now0930.myapplication.hsm.Event; import com.example.now0930.myapplication.hsm.Hsm; import com.example.now0930.myapplication.hsm.State; /** * This class represents a test HSM implementation. * * @author Alexei Krasnopolski (krasnop@bellsouth.net) */ public class myHsm extends Hsm { int myFoo; DataGapFlush myGapFlush; Handler myHandle; public myHsm(DataGapFlush myDataInst, Handler handle) { this.myGapFlush = myDataInst; this.myHandle = handle; } public void init() { //System.out.print("Top-INIT;"); Log.d("FSM", "Top-INIT;"); myFoo = 0; super.init(); } ; public State fireInit() { Log.d("FSM", "INIT>>s0;"); return stateStandby; } ; public State fireEvent(Event e) { return getParent(); } ; public void enter() { Log.d("FSM", "ENTRY>>s0;"); } ; public void exit() { Log.d("FSM", "EXIT<<s0;"); } Hsm s0 = this; // This is just alias of current instance State stateStandby = new State(s0) { @Override public State fireInit() { Log.d("FSM", "INIT>>stateStandby;"); return null; } @Override public void enter() { Log.d("FSM", "ENTRY>>stateStandby;"); } @Override public State fireEvent(Event e) { switch (e.getID()) { case 5: printMessage(e, "stateBatchRec"); //myHandle.sendMessage(myHandle.obtainMessage()); myHandle.sendEmptyMessage(1); myHsm.this.transition(stateBatchRec); return null; default: break; } return getParent(); } @Override public void exit() { Log.d("FSM", "EXIT<<stateStandby;"); } }; State stateBatchRec = new State(s0) { @Override public State fireInit() { Log.d("FSM", "Init>>stateBatchRec;"); return stateBatchRec_Gap; } @Override public void enter() { Log.d("FSM", "Entry>>stateBatchRec;"); } @Override public State fireEvent(Event e) { switch (e.getID()) { case 6: printMessage(e, "stateStandby"); myHsm.this.transition(stateStandby); return null; case 11: printMessage(e, "stateModify"); myHsm.this.transition(stateModify); return null; default: break; } return getParent(); } @Override public void exit() { Log.d("FSM", "EXIT<<stateBatchRec;"); } }; State stateBatchRec_Gap = new State(stateBatchRec) { @Override public State fireInit() { Log.d("FSM", "Init>>stateBatchRec_Gap;"); return null; } @Override public void enter() { Log.d("FSM", "Entry>>stateBatchRec_Gap;"); //myHandle.sendEmptyMessage(1); } @Override public State fireEvent(Event e) { //아래 e.getID를 실행하면 //이벤트 정의시 내부 데이터에 의한 기준으로 하면 //일정 시점 이후로는 그 동작만 계속됨.. //내부 이벤트, 이부 이벤트로 분리. //내부 이벤트는 그 state에서만 실행되도록 정의 boolean gapFlag = myGapFlush.checkGapFulled(); if(gapFlag == true){ myHsm.this.transition(stateBatchRec_Flush); return null; } switch (e.getID()) { case 1: printMessage(e, "stateBatchRec_Gap"); int i = myGapFlush.getGapIndex(); myGapFlush.setGapIthwithN(i, myGapFlush.getTmpWord()); myGapFlush.emptyTmpWord(); myHsm.this.transition(stateBatchRec_Gap); myHandle.sendEmptyMessage(1); return null; /*** case 2: myHsm.this.transition(stateBatchRec_Flush); return null; ***/ default: break; } return getParent(); } ; @Override public void exit() { Log.d("FSM", "Exit<<stateBatchRec_Gap;"); } State stateBatchRec_Flush = new State(stateBatchRec) { @Override public State fireInit() { Log.d("FSM", "Init>>stateBatchRec_Flush;"); return null; } @Override public void enter() { Log.d("FSM", "Entry>>stateBatchRec_Flush;"); } @Override public State fireEvent(Event e) { switch (e.getID()) { case 3: printMessage(e, "stateBatchRec_Flush"); int i = myGapFlush.getFlushIndex(); myGapFlush.setFlushIthwithN(i, myGapFlush.getTmpWord()); myGapFlush.emptyTmpWord(); myHsm.this.transition(stateBatchRec_Flush); myHandle.sendEmptyMessage(1); return null; default: break; } return getParent(); } @Override public void exit() { Log.d("FSM", "Exit<<stateBatchRec_Flush;"); } }; }; State stateModify = new State(s0) { @Override public State fireInit() { Log.d("FSM", "Init>>stateModify;"); return null; } @Override public void enter() { Log.d("FSM", "Entry>>stateModify;"); } @Override public State fireEvent(Event e) { return null; } @Override public void exit() { Log.d("FSM", "Exit<<stateModify;"); } }; public static void printMessage(Event e, String stateName) { Log.d("FSM", "Event-" + e.getID() + ">>" + stateName + ";"); } };
HSM 내부에 gap과 flush를 같이 관리가 되도록 데이터 형식으로 같이 정의 했다. 핸들러는 MainActivity의 startListener를 제어하기 위한 핸들러이다.
MainActivity에서 생성자를 만들경우 자동으로 관련 주소를 넘겨오도록 했다.
State간의 Action은 전이가 있을 때 실행된다. transition과 별개의 action을 실행하려면, state 내부에 넣어주면 될것 같다.
DataGapFlush Class 정의
DataGapFlush는 다음과 같이 정의 했다.
public class DataGapFlush { private String[] gap; private String[] flush; private int gapIndex = 0; private int flushIndex = 0; private String VIN = ""; private String tmpSpokenWord = ""; public DataGapFlush() { gap = new String[10]; flush = new String[10]; //0부터 10까지 "Empty"를 입력.. for (int i = 0; i < 10; i++) { gap[i] = "Empty"; flush[i] = "Empty"; } //DEBUG for (int i = 0; i < 8; i++) { gap[i] = "가"; } //DEBUG } //gap을 0에서 10까지 "Empty"로 들어가 있는지 확인 //모두 차있으면 true을 return.. //중간에 비어 있으면 flase를 return public boolean checkGapFulled() { int i; for (i = 0; i < 10; i++) { if (gap[i] != "Empty") { this.gapIndex = i; } else break; } setGapIndex(i); if (this.gapIndex == 10) return true; else return false; } public boolean checkFlushFulled() { int i; for (i = 0; i < 10; i++) { if (flush[i] != "Empty") { this.flushIndex = i; } else break; } setFlushIndex(i); if (this.flushIndex == 10) return true; else return false; } public int getGapIndex() { checkGapFulled(); return this.gapIndex; } public int getFlushIndex() { checkFlushFulled(); return this.flushIndex; } public void setGapIndex(int i) { this.gapIndex = i; } public void setFlushIndex(int i) { this.flushIndex = i; } public boolean setGapIthwithN(int i, String value) { if (i >= 10) return false; //failed.. else { gap[i] = value; } return true; //success } public boolean setFlushIthwithN(int i, String value) { if (i >= 10) return false; //failed.. else { flush[i] = value; } return true; //success } public void setTmpWord(String words) { this.tmpSpokenWord = words; } public String getTmpWord() { return this.tmpSpokenWord; } public void emptyTmpWord() { this.tmpSpokenWord = ""; } public String getData(boolean GAPFLUSH_FLAG, int NthData) { String tmp = ""; if (NthData <= 9) { if (GAPFLUSH_FLAG == true) { //GAP을 가져옴.. tmp = this.gap[NthData]; } else //Flush를 가져옴.. tmp = this.flush[NthData]; } return tmp; } }
구글 음성인식이 숫자로 자동으로 변환되면 좋은데, 텍스트로 입력이 들어온다. 10개까지 저장할 수 있도록 String으로 배열을 잡았다. gap, flush index는 어느 부분에 데이터를 넣을지 결정한는 부분이다. Hsm에서 DataGapFlush를 만들 때, 내부에 Empty로 10개를 만든다. Empty 문자열을 확인 후, 0번부터 입력을 한다.
Event 정의
package com.example.now0930.myapplication.hsm; /** * Created by now0930 on 17. 2. 26. */ import com.example.now0930.myapplication.DataGapFlush; import java.util.EventObject; import java.util.Objects; import java.util.logging.Handler; public class Event extends EventObject { private int id; private int gapIndex = 0; private int flushIndex = 0; private DataGapFlush thisData; boolean SPEECHFLAG = false; private enum ID {JUSTONRESULT, GAPISFULLED, FLUSHISFULLED} ; public Event(Object o, DataGapFlush myDataIns, boolean flag) { super(o); this.thisData = myDataIns; this.SPEECHFLAG = flag; //false이면 음성 입력이 없음.. } //버튼을 눌렀을 경우, 이벤트 정의 함수.. public Event(Object o, DataGapFlush myDataIns, int eventInstance) { super(o); this.thisData = myDataIns; this.id = eventInstance; } public Event(Object o, int index) { super(o); this.id = index; } //event 정의.. //gap부터 채우고 flush를 채움.. //gap not full, flush not full,onResult : id 1 //gap full, flush not full : id 2 //gap full, flush not full, onResult : id 3 //gap not full, flush full : id not defined //gap full, flush full : id 4 //음성인식 버튼을 누를 경우 : id 5 //startListening 후, 입력이 없을 경우.: id6 //startListening 후, 에러 발생. : id 6 //버튼 눌름 정의.. //1번을 눌렀을 때, Gap을 수정할 때. : 11 //2번을 눌렀을 때, Gap을 수정할 때. : 12 //3번을 눌렀을 때, Gap을 수정할 때. : 13 //4번을 눌렀을 때, Gap을 수정할 때. : 14 //5번을 눌렀을 때, Gap을 수정할 때. : 15 //6번을 눌렀을 때, Gap을 수정할 때. : 16 //7번을 눌렀을 때, Gap을 수정할 때. : 17 //8번을 눌렀을 때, Gap을 수정할 때. : 18 //9번을 눌렀을 때, Gap을 수정할 때. : 19 //10번을 눌렀을 때, Gap을 수정할 때. : 20 //1번을 눌렀을 때, Flush을 수정할 때. : 21 //2번을 눌렀을 때, Flush을 수정할 때. : 22 //3번을 눌렀을 때, Flush을 수정할 때. : 23 //4번을 눌렀을 때, Flush을 수정할 때. : 24 //5번을 눌렀을 때, Flush을 수정할 때. : 25 //6번을 눌렀을 때, Flush을 수정할 때. : 26 //7번을 눌렀을 때, Flush을 수정할 때. : 27 //8번을 눌렀을 때, Flush을 수정할 때. : 28 //9번을 눌렀을 때, Flush을 수정할 때. : 29 //10번을 눌렀을 때, Flush을수정할 때. : 30 public int getID() { //외부에서 미리 설정한 event id를 아래 부분에서 바꾸지 않도록 설정.. if(this.id == 0) { if (SPEECHFLAG == true) { //gap이 모두 차있음.. if (thisData.checkGapFulled() == false && thisData.checkFlushFulled() == false) this.id = 1; else if (thisData.checkGapFulled() == true && thisData.checkFlushFulled() == false) this.id = 3; } /*** * 아래 부분은 데이터 기준으로 이벤트가 설정됨.. else if (SPEECHFLAG == false) { if (thisData.checkGapFulled() == true && thisData.checkFlushFulled() == false) this.id = 2; else if (thisData.checkGapFulled() == true && thisData.checkFlushFulled() == true) this.id = 4; } */ } return this.id; } }
이벤트는 위와 같이 했다. 책에서는 이벤트를 판단하는 함수가 간략해야 된다고 했는데, 내 능력으로는 이런 구현이 어려울 듯 하다. 사용할 모든 이벤트를 넣어 버렸다. 이벤트는 Hsm에서 e.getID() 함수를 call할 경우, 판단이 된다.
내가 원하는 Event 처리는 다음과 같다.
- 음성인식 버튼을 눌렀을 경우 : e.id = 5
- 음성인식 후, 결과가 제대로 나왔을 경우 : e.id = 1, 3
- 음성인식 후, 결과가 에러가 발생할 경우 : e.id = 6
- gap의 데이터 10개를 모두 입력했을 경우 : e.id = 2
- flush의 데이터 10개를 모두 입력했을 경우 : e.id = 4
이벤트는 MainActivity에서 thread로 실행을 했다. 위에서 문데되는 부분이 e.id가 2, 4일 경우이다. gap의 모든 데이터를 채워 버리면 다른 이벤트는 e.getID()를 실행할 때, 인식이 되지 않는다. 위 부분을 처리하기 위해서, 특정 state에서 한번만 일어나도록 했다.
public State fireEvent(Event e) { //아래 e.getID를 실행하면 //이벤트 정의시 내부 데이터에 의한 기준으로 하면 //일정 시점 이후로는 그 동작만 계속됨.. //내부 이벤트, 이부 이벤트로 분리. //내부 이벤트는 그 state에서만 실행되도록 정의 boolean gapFlag = myGapFlush.checkGapFulled(); if(gapFlag == true){ myHsm.this.transition(stateBatchRec_Flush); return null; } switch (e.getID()) { case 1: printMessage(e, "stateBatchRec_Gap"); int i = myGapFlush.getGapIndex(); myGapFlush.setGapIthwithN(i, myGapFlush.getTmpWord()); myGapFlush.emptyTmpWord(); myHsm.this.transition(stateBatchRec_Gap); myHandle.sendEmptyMessage(1); return null; /*** case 2: myHsm.this.transition(stateBatchRec_Flush); return null; ***/ default: break; } return getParent(); }
HSM 내부의 이벤트와 외부의 이벤트를 구분해야 할 듯 한데, 딱히 어떻게 할지를 모르겠다.
Handler 정의 및 Message 전송
JAVA는 어떤지 모르겠는데, android는 MainActivity내부에 handler를 정의하여 외부에서 메세지를 보내는 구조로 되어 있다. HSM에서 일정한 전이가 일어나면 MainActivity에 있는 SpeechRecognizer로 정의 되어잇는 startListening을 실행하도록 했다.
MainActivity handler 정의
public class MainActivity extends AppCompatActivity { Intent i; SpeechRecognizer mRecognizer; int TvIndex = 0; TextView[] TvGap = new TextView[10]; TextView[] TvFlush = new TextView[10]; //hsm에서 SpeechListener를 제어하기 위해서. Handler mHandler = new Handler() { @Override public void handleMessage(Message msg) { super.handleMessage(msg); //음성인식 시작부분.. if (msg.what == 1) mRecognizer.startListening(i); } };
Hsm에서 handler 초기화 및 message 전송
HSM을 만들 떄, MainActvity에서 만든 handler로 초기화 했다. 메세지는 구조는 잘 모르겠으나, 간단한 메세지를 sendEmptyMessage로 보냈다.
public class myHsm extends Hsm { int myFoo; DataGapFlush myGapFlush; Handler myHandle; public myHsm(DataGapFlush myDataInst, Handler handle) { this.myGapFlush = myDataInst; this.myHandle = handle; } public State fireEvent(Event e) { switch (e.getID()) { case 5: printMessage(e, "stateBatchRec"); //myHandle.sendMessage(myHandle.obtainMessage()); myHandle.sendEmptyMessage(1); myHsm.this.transition(stateBatchRec); return null; default: break; } return getParent(); }