Sorry you have no rights to view this post!
안드로이드 앱 개발 일지, 끝
안드로이드 앱 개발 일지, 끝
안드로이드 6.0 권한 설정
안드로이드 6.0으로 넘어가면서 구글이 앱을 실행할 때 권한을 요청하도록 정책을 변경했다. 사용자들은 앱이 어떤 권한을 사용하는지 확실히 알지만, 그만큼의 귀찮음이 개발자에게 전가 되었다. 사용자는 많고, 개발자는 적으니 보통 개발자가 불편함을 갖는게 최대 다수의 최대 행복의 기준에 맞아 보인다. 그러나 사용자와 개발자의 숫자가 비슷할 경우는 그냥 일만 늘어난다. 개발자들이 쉽게 사용할 수 있게 구글이 관련 기능을 패키지 형식으로 제공하면 좋았겠지만, 그렇지는 않았다. 위 기준을 적용하면 구글의 갑질이라 볼 수 있다. 인터넷에 안드로이드 6.0 권한으로 쳐보면 많은 불만들이 보인다. 다행히 누군가 인터넷에 쉽게 구현할 수 있도록 코드를 공개했다.
[안드로이드]유용한 라이브러리 – TedPermission(마시멜로우 권한체크)
안드로이드 스튜디오에서 gradle에 주소를 넣어주면 소스를 다운받아 컴파일 해주나보다. 몇 줄 추가하여 바로 사용했다. 구현까지 10분도 안걸렸다!!!
완료 동영상 및 소감
-를 입력하기 어려워 보이는데, 나중에 좋은 생각이 나면 보완하기로 하고, 일단 여기에서 끝낸다.
내가 java를 잘 알지 못했지만 끝까지 개발해보니 보람 있다. 직접 쓰고, 인터넷에서 가져다 쓴 코드가 완벽하지 않다. 내가 개념을 제대로 모르고 쓴 부분도 있고, 버그가 생길 느슨한 부분도 있다. 나는 이런 생각으로 무엇인가 새롭게 시도하는데 주저하고, 시간을 오래 사용한다. 그러나, 이런 상황은 소비자가 시장에서 제품을 사도 똑같다.
자동차에 사용되는 기술을 예를 들어보자. 제조사는 새로운 기술이 적용된 제품을 소비자에게 팔 때, 자기네 기술이 완벽하다고 홍보한다.(뻥을 친다.) 그러나 제조사가 새로운 기술을 구현할 때 그것이 완벽할 수 없다. 제조사가 이런 사실을 공개하지 않지만 다수의 프로그램 회사들이 제품 출시 후 패치를 하는데 이를 보면 확실하다. 제조사는 다른 회사들보다 잘 만들거나, 동등하게 만든다. 제조사가 완전하지 않은 기술로 만든 제품을 소비자에게 팔아 이익을 남긴다고 비난할 수 있으나, 과거 그래왔고 그러면서 기술이 발전했다. 이렇게 생각하니 속편하다.
내가 전문 개발자는 아니지만 이 개발로 필요한 도구를 어느 정도 만들어 사용할 수 있게 되었다. 특히 휴대폰은 여러 목적사용이 가능하여 많은 이점이 있다. 좋은 시대 태어나 시대가 주는 이점을 잘 받아먹고 있다.
프로그램을 개발하는데 여러 방법이 있지만, fsm가 가장 합리적으로 보인다. fsm의 신도?로, 믿음을 실천할 수 있어 보람 되다. 정작 프로 개발자들은 fsm을 잘 사용하지 않아 보인다. 퀀텀 프로그램의 창시자? Miro Samek과 C++을 자바로 포팅한 Alexei Krasnopolski가 특히 고맙다. 내 취미생활에 신념을 넣어주신 분들이다. 음성인식을 자유롭게 사용하도록 공개???했고, 장난감을 만들어준 구글도 고맙다. 인터넷의 여러 오픈소스 개발자들에게도 고맙다.
안드로이드 앱 개발 일지, 7차
안드로이드 앱 개발 일지, 7차
입력된 문자를 숫자로
구글 음성인식은 내가 말하는 내용을 문맥에 맞춰 텍스트로 바꿔준다. 숫자로만 바꿔주면 좋겠지만, 문맥에 맞춰 숫자, 한들로 변경한다. 나는 숫자에만 관심이 있으므로 숫자로 말하기로 정했다. 말하는 내용이 길어질 경우, 문자가 칸을 넘어가 잘 보이지 않는다. 숫자라도 소수점을 넣을때 방해가 될것이므로 총 4자리를 사용하기로 결정했다.
전체적인 flow는 아래와 같다.
1. 구글의 음성인식분을 받아들임
2. 음성의 한글로 표시된 부분을 숫자로 변경
3. 중간의 공백 제거
4. 앞뒤의 공백 제거
5. 글자가 4자리를 넘어가면 앞에서부터 4자리를 자름
6. 글자가 4자리를 안넘어가면, 뒤쪽은 부족분만큼 0으로 채움
7. 앞의 2개, 뒤의 2개로 나누어서, 중간에 소수점 삽입
다음의 코드로 구현했다.
public class DataGapFlush {
private String[] gap;
private String[] flush;
private int gapIndex = 0;
private int flushIndex = 0;
private String VIN = "";
....
public void setTmpWord(String words) {
String tmp1, tmp2;
//한글을 숫자로 변경..
tmp1 = words;
for (int i = 0; i < words.length(); i++) {
tmp2 = tmp1.replace("영", "0");
tmp1 = tmp2;
tmp2 = tmp1.replace("일", "1");
tmp1 = tmp2;
tmp2 = tmp1.replace("이", "2");
tmp1 = tmp2;
tmp2 = tmp1.replace("리", "2");
tmp1 = tmp2;
tmp2 = tmp1.replace("삼", "3");
tmp1 = tmp2;
tmp2 = tmp1.replace("셋", "3");
tmp1 = tmp2;
tmp2 = tmp1.replace("사", "4");
tmp1 = tmp2;
tmp2 = tmp1.replace("오", "5");
tmp1 = tmp2;
tmp2 = tmp1.replace("육", "6");
tmp1 = tmp2;
tmp2 = tmp1.replace("칠", "7");
tmp1 = tmp2;
tmp2 = tmp1.replace("팔", "8");
tmp1 = tmp2;
tmp2 = tmp1.replace("구", "9");
tmp1 = tmp2;
tmp2 = tmp1.replace("십", "0");
tmp1 = tmp2;
}
//공백 제거..
String tmpSpokenWordNospace = tmp1.replace(" ", "");
// 앞뒤 공백 제거.
String tmpSpokenWordTrimmed = tmpSpokenWordNospace.trim();
//앞에서 4자리 자른 문자.
int tmpLength = tmpSpokenWordTrimmed.length();
String tmpSpokenWord4char;
if (tmpLength < 4) {
tmpSpokenWord4char = tmpSpokenWordTrimmed.substring(0, tmpLength);
//앞에서 자른 수만큼 뒤로 0을 채워 넣음..
//소수점을 일정하게 넣기 위해서..
for (int tmpi = 0; tmpi < 4 - tmpLength; tmpi++) {
tmpSpokenWord4char = tmpSpokenWord4char + "0";
}
} else
tmpSpokenWord4char = tmpSpokenWordTrimmed.substring(0, 4);
//4자리 숫자에서 소수점을 2개 추가..
String FirstString, SecondString;
String FirstPlusSecond;
FirstString = tmpSpokenWord4char.substring(0,2);
SecondString = tmpSpokenWord4char.substring(2,4);
FirstPlusSecond = FirstString+"."+SecondString;
this.tmpSpokenWord = FirstPlusSecond;
}
사용자에게 입력 대기 표시
지금은 버튼을 누를경우, 음성인식 대기 상태가 된다. 이 때 삐 소리가 나는데, 이 소리로 언제 말해야 할지를 알았다.그러나 현장에서 사용할 경우, 시끄러운 소리로 잘 들리지 않을 것이다. 그래서 화면의 색으로 사용자가 언제 말할지 알려 주기로 했다. 배경 화면 색 변경은 MainActivity에서 함수로 가능하다. 처음에 전이가 일어날 경우 fsm 클래스에서 MainActivity로 message를 전달했다. 그러나 시간이 부족한지 먼가 메세지는 전달되는데, 색이 변하는게 보이지 않았다. 그래서 MainActivity에서 음성인식이 종료될 경우 색부터 바꿨더니, 사용자가 잘 알 수 있도록 표시되었다.
1. 흰색 : 음성이 대기중이 아닐 경우
2. aqua색 : 음성이 대기중일 경우
3. 전이가 일어날 경우, message로 음성인식 시작
4. 음성인식 시작과 동시에 배경색을 aqua로 변경
5. onResult, onError 등 이벤트 발생시 MainActvity에서 바로 색을 흰색으로 변경
public class MainActivity extends AppCompatActivity {
Intent i;
Intent subActivity;
SpeechRecognizer mRecognizer;
int TvIndex = 0;
TextView[] TvGap = new TextView[10];
TextView[] TvFlush = new TextView[10];
//hsm에서 SpeechListener를 제어하기 위해서.
String dataToFile = "hello?!";
FileOutputStream outputStream;
Context context;
Handler mHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
//음성인식 시작부분..
...
if (msg.what == 100){
Toast.makeText(context, "음성인식 대기", Toast.LENGTH_SHORT).show();
setActivityBackgroundColor(Color.parseColor("#00FFFF"));
}
if (msg.what == 101){
setActivityBackgroundColor(Color.WHITE);
}
}
};
//background color 변경..
public void setActivityBackgroundColor(int color){
View view = this.getWindow().getDecorView();
view.setBackgroundColor(color);
}
이렇게 현재 어느상태인지를 확실하게 표시되니 마지막의 버그를 하나 찾았다. stateBarchRec_Flush에서 마지막 데이터를 입력 할 경우, stateDecision으로 넘어가는 부분이 잇는데, 데이터가 차 있어도 startListen을 실행한다. 이 때문에 색이 바뀌지 않았다.

transition의 마지막의 경우, 데이터가 차 있으면 startListen을 하지 않도록 수정했다.
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) {
//아래 e.getID를 실행하면
//이벤트 정의시 내부 데이터에 의한 기준으로 하면
//일정 시점 이후로는 그 동작만 계속됨..
//내부 이벤트, 이부 이벤트로 분리.
//내부 이벤트는 그 state에서만 실행되도록 정의
boolean flushFlag = myGapFlush.checkFlushFulled();
if (flushFlag == true) {
Log.d("FSM", "stateBatchRec_Gap >> stateDecision");
myHandle.sendEmptyMessage(101);
myHandle.sendEmptyMessage(2);
myHsm.this.transition(stateDecision);
return null;
}
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);
//Gap N Flush의 내용을 업데이트..
myHandle.sendEmptyMessage(4);
//입력을 위한 시간 지연..
try {
Thread.sleep(1000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
//마지막 실행전 데이터가 모두 차 있는지 확인 후, 전송..
if (myGapFlush.checkFlushFulled() == false) {
myHandle.sendEmptyMessage(1);
//Toast Message 표시..
myHandle.sendEmptyMessage(100);
}
return null;
default:
break;
}
return getParent();
}
@Override
public void exit() {
Log.d("FSM", "Exit<<stateBatchRec_Flush;");
}
};
Transition 시간 지연
테스트를 위해서는 시간 지연이 없는 환경이 좋으나, 현장에서 이렇게 쉽게 측정하지 못할 것이다. 갭자를 찔러보는 시간을 약 2초정도로 설정 가능하도록 수정 했다. 단순 타이머를 사용하는데 try catch를 왜 사용하는지 모르겠다.
다음 작업들…
이제 한번만 더하면 끝이다. 사용자가 입력한 SEQ와 측정 위치를 찾아 파일로 기록해 주면 끝이다. 각 설정 위치별로 layout도 그려야 되는데, 귀찮아서 패스하고 종료할 것이다. 마지막을 android 6.0에서는 권한에 대한 정책이 변경되었다. 이 분을 수정해야 대부분 휴대폰에서 동작이 가능해 보인다. 마지막 배열에 써야 되는데, 가끔 중간에 데이터가 들어간다. 왜그런지 모르겟으나…수정 버튼을 눌러야 겠다..
안드로이드 앱 개발 일지, 6차
안드로이드 앱 개발 일지, 6차
버튼을 눌렀을 겨우, 다른 activity 실행
안드로이드 스튜디오를 실행하면 기본으로 하나의 activity를 가지고 있다. 앱을 실행할 경우, 처음 보여주는 Activity는 번호만 나와있어 사용자가 어느 방향으로 측정할 지 모른다. 버튼을 하나 누르면 번호와 측정 부분을 보여줘야 한다. 이렇게 하기 위해 MainActivity에서 버튼을 하나 만들었고, 사용자가 이를 눌렀을 경우 subActivity가 실행되도록 했다. 여기에서 참조 했다.
아래는 구현한 코드이다.
public class MainActivity extends AppCompatActivity {
Intent subActivity;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
subActivity = new Intent(this, LayoutActivity.class);
Button BtnLayout = (Button)findViewById(R.id.Layout);
BtnLayout.setOnClickListener(mLayoutListener);
Button.OnClickListener mLayoutListener = new View.OnClickListener(){
@Override
public void onClick(View v) {
startActivity(subActivity);
}
};
}
subActivity라고 LayoutActivity.java로 아래와 같이 만들었다. setContentView의 layout_main은 res의 layout의 경로이다.
public class LayoutActivity extends Activity {
Intent LayoutIntent;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.layout_main);
}
}
layout을 아래와 같이 새로 만들어 주고, LayoutActivity에 알려 줬다.

Manifest에 등록하기
이렇게 하고 버튼을 누를 경우, 빈 화면이 보여야 되는데, 에러가 발생한다. Manifest에 아래와 같이 등록한다.
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.now0930.myapplication">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity
android:name=".MainActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".LayoutActivity">
</activity>
</application>
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.RECORD_AUDIO"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
</manifest>
사용자 바탕화면 설정하기
새로운 activity에는 바탕화면을 설정하고 싶다. 다음 사이트에서 참조 했다. 결론을 말하면, png 파일로 AndroidStudio 폴더에 넣어주면 된다. layout 설정하는 화면에 design과 text를 설정할 수 있는데, text로 다음과 같이 입력하면 된다. 설정 옵션에 방향을 변경할 수 있는데, 어떻게 하는지 몰라 그림을 90도 돌렸다.

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/carsideview"
android:orientation="vertical"
android:weightSum="1">
</LinearLayout>
이 사진에 번호를 붙이고 싶으나, 제공하는 TextView에서는 원하는 위치로 가져갈 수 없다. 사진을 수정하여 넣어야 될듯 보인다.
안드로이드 앱 개발 일지, 5차
안드로이드 앱 개발기 5
이제 거의 끝이 보인다. 마지막으로 만든 state는 잘못 입력한 부분을 수정한다. 임의로 잡은 10개의 항목 중, 수정할 버튼을 눌러 다시 입력을 받는다. 음성인식 대기를 하다 에러가 나오면 다시 입력할 수 있도록 대기를 한다. 결과를 받으면 어느 버튼을 눌렀는지 기억을 했다가 그 내용을 새로운 부분으로 업데이트 한다. 이후 stateDecision으로 다시 돌아오게 했다.
stateModify
딱히 무슨 기능을 하는 상태는 아니다. 그냥 대기 상태이다. 아래 그림과 같이 관련된 state의 전이 조건을 설정했다.

State stateModify = new State(s0) {
@Override
public State fireInit() {
Log.d("FSM", "Init>>stateModify;");
return stateModifyWhat;
}
@Override
public void enter() {
Log.d("FSM", "Entry>>stateModify;");
}
@Override
public State fireEvent(Event e) {
return getParent();
}
@Override
public void exit() {
Log.d("FSM", "Exit<<stateModify;");
}
};
State stateModifyWhat = new State(stateModify) {
@Override
public State fireInit() {
Log.d("FSM", "Init>>stateModifyWhat;");
return null;
}
@Override
public void enter() {
Log.d("FSM", "Entry>>stateModifyWhat;");
}
@Override
public State fireEvent(Event e) {
switch (e.getID()) {
case 5:
printMessage(e, "stateModifyListening");
myHandle.sendEmptyMessage(1);
myHsm.this.transition(stateModifyListening);
}
return getParent();
}
@Override
public void exit() {
Log.d("FSM", "Exit<<stateModifyWhat;");
}
};
State stateModifyUpdate = new State(stateModify) {
@Override
public State fireInit() {
Log.d("FSM", "Init>>stateModifyUpdate;");
return null;
}
@Override
public void enter() {
Log.d("FSM", "Entry>>stateModifyUpdate;");
int i = myGapFlush.getIndexForGapUpdate();
myGapFlush.setGapIthwithN(i, myGapFlush.getTmpWord());
myGapFlush.emptyTmpWord();
//화면 업데이트..
myHandle.sendEmptyMessage(4);
}
@Override
public State fireEvent(Event e) {
//완료되면 판단상태로 다시 돌아감..
myHsm.this.transition(stateDecision);
return getParent();
}
@Override
public void exit() {
Log.d("FSM", "Exit<<stateModifyUpdate;");
}
};
State stateModifyListening = new State(stateModify) {
@Override
public State fireInit() {
Log.d("FSM", "Init>>stateModifyListening;");
return null;
}
@Override
public void enter() {
Log.d("FSM", "Entry>>stateModifyListening;");
}
@Override
public State fireEvent(Event e) {
switch (e.getID()) {
case 1:
printMessage(e, "stateModifyUpdate");
myHsm.this.transition(stateModifyUpdate);
return null;
case 3:
printMessage(e, "stateModifyUpdate");
myHsm.this.transition(stateModifyUpdate);
return null;
case 6:
printMessage(e, "stateModifyWhat");
myHsm.this.transition(stateModifyWhat);
return null;
}
return getParent();
}
@Override
public void exit() {
Log.d("FSM", "Exit<<stateModifyListening;");
}
};
이 state에 들어가면 초기로 stateModifyWhat으로 들어가도록 했다.
이전에 만들었던 부분을 재활용했다.
stateModifyWhat
수정 버튼을 눌렀을 경우, 어느 버튼을 눌렀는지를 데이터로 써주는 부분이다. 나중에 이 변수를 읽어 그 영역을 수정하도록 했다. gap과 flush 두 가지 경우가 있어 변수를 쉽게 사용하기 위해서 두 개를 썼다. 이 state에서 음성인식 버튼을 누르면 stateModifyListening으로 들어가도록 했다.
stateModifyListening
여기도 딱히 무슨 기능을 하는 영역은 아니다. 버튼을 눌렀을 때, 에러 처리를 하기 위해 임시로 만든 영역이다. 에러가 나면 다시 stateModifyWhat 영역으로 들어가고, 결과가 정상이면 stateModifyUpdate로 가도록 했다.
stateModifyUpdate
입력된 정보를 업데이트 하는 부분이다. 배열에 기록을 하고, 화면 업데이트도 한다.
전체적인 윤곽 마무리
이렇게 전체적인 윤곽이 마무리 되었다. 수정버튼을 테스트하기 위해 하나만 만들었는데, 모두 수정하기 위해서는 20개의 버튼이 필요하다. 또한 음성인식 대기 중인지, 아닌지를 사용자가 잘 알 수 없다. 소리로 구분이 되는데, 잘 안들린다. 이 부분을 화면으로 표시를 해줘야 겠다.
입력할 경우, SEQ를 입력할 텐데, 이 부분에 대한 처리도 없다.
file이 정상적으로 save가 되었을 경우, 경로와 처리 여부를 알려줘야 사용자가 쉽게 찾는다.
수정 버튼을 눌렀을 경우, 어느 버튼을 눌렀는지 표시를 해줘야 쉽게 수정이 가능해 보인다.
구글 음성인식 엔진을 사용하다 보니, 오만가지 소리를 지 맘대로 해석한다. 숫자를 하나씩 읽어주면 그나마 String이 숫자로 들어오는데, 가끔 한글로 그대로 들어올 때도 있다. 입력된 부분을 읽어서 한글로 되어 있으면 숫자로 바꿔주는 부분도 필요하다.
특성상 마이너스는 제대로 인식이 안되는데, 이를 쉽게 입력할 수 있는 방법이 필요해 보인다.
차량의 전체적인 위치표시와 LH/RH의 구분이 필요해 보인다. 측정위치는 모든 차량에 일관적으로 하여 일정한 기준으로 정리가 되어야 할 것 같다.
측정 포인트가 많으면 글자가 작아서 보이지가 않을텐데, 화면을 스크롤하여 보여주는 장치도 필요해 보인다.
화면에 모두 표시가 불가능하면 연속입력이 들어갈 경우, 해당 위치로 옮겨줘서 보여줘야 할텐데, 이 부분을 구현해야 될것으로 보인다.
전체적으로 끝났다고 몇 개만 더 하면 끝날 것 같아 보였는데, 아직도 멀어 보인다. 역시 detail이 어렵나보다.
