[카테고리:] 생활코딩

  • 안드로이드 앱 개발 일지, 7차

    안드로이드 앱 개발 일지, 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차

    안드로이드 앱 개발 일지, 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이 어렵나보다.

  • 안드로이드 앱 개발 일지, 4차

    안드로이드 앱 개발 일지, 4차

    안드로이드 앱 개발기 4

    Filesave를 위한 FSM정의

    어제의 삽질을 시작으로 Gap과 Flush의 데이터를 file로 저장하는 state를 구현했다. fsm은 아래 그림이다.

    오른쪽 그림을 확대하면 다음과 같다.

    stateSave

    stateDecision에서 save 버튼을 누르면 stateSave로 들어가기로 했다. stateSave의 초기 상태는 stateSaveDecision이다.

    stateSaveDecision

    JAVA에서 바로 스테이트를 사용하기 위해, 좀 어색하지만 영문으로 사용하기로 했다. 이 상태는 하는런 작업을 하지 않는다. 데이터가 비어있는지 아닌지를 판단할 때까지 대기한다. data가 없으면 stateStandby로 아무런 작업없이 간다. data가 있으면 stateSveWrite 상태로 움직인다. 데이터가 비어있는지 아닌지는 외부 이벤트로 처리하지 않았다. 전에도 그랬듯이 외부 이벤트 처리를 하면, 한 개의 이벤트로만 잡혀, 아무런 작업이 되질 않는다.

    stateSaveWrite

    이 state는 데이터를 file로 저장한다. 내부 저장소에 파일을 만들 수 없어, 외부 저장소에 디렉토리를 만들고 파일을 만들었다. 파일명에 날자를 넣고 싶은데, stack overflow에서 찾아 아래와 같이 했다.

    SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
    Date now = new Date();
    String timestamp = dateFormat.format(now)+".txt";
    

    파일을 만든 다음에 루프를 10번 돌려 ,로 분리하여 기록했다.

    try {
    outputStream = new FileOutputStream(filename);
                            for(i=0;i<10;i++) {
                            outputStream.write("SEQ,".getBytes());
                            outputStream.write("Gap,".getBytes());
                            tmp = myGapFlush.getData(false,i);
                            outputStream.write(tmp.getBytes());
                            outputStream.write(",".getBytes());
                            outputStream.write("Flush,".getBytes());
                            tmp = myGapFlush.getData(true,i);
                            outputStream.write(tmp.getBytes());
                            outputStream.write("\n".getBytes());
                        }
                        outputStream.close();
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
    fsm.dispatchEvent(new Event(fsm, myGapFlush, 8));
    

    파일의 맨 위에 항목을 넣고 싶은데, 복잡하게 될것 같아 그냥 두기로 했다.
    이 상태로 들어가면 — enter() 실행시 — 파일로 저장을 한다. file을 만들고 저장하는 부분은 mainActivity에서 한다. fsm에서 할 수 있도록 핸들러로 처리했다. 파일 저장을 하던 못하던 외부 이벤트를 발생하여 stateStandby로 가도록 했다.
    stateSaveWrite에서 stateStandby로 전이가 일어날 경우, data의 내용을 초기화한다.

  • 안드로이드 앱 개발 일지, 3차

    안드로이드 앱 개발기 3

    파일 입출력

    수집된 데이터를 저장할 경우, 파일에 저장하면 된다. 전에 누가 개발한 코드를 보니 쉽게 사용하던데, android developer에서 보니 설명이 어렵게 되어 있다. stack overflow에서 찾아, 되는 코드를 만들 수 있었다.

    android developer에는 internal storage는 아무 제한이 없이 사용하는데, 코드로 만들면 permission 문제인지 파일과 폴더를 만들 수 없다. internal storage의 기본 위치는 /data/…인데, 안드로이드에서 직접 만들어보니 안되었다. 갤럭시 노트2의 경우 권한으로 막아놓은 듯 하다. 이런 설명이 없으니, 하루 삽질을 하게 되었다.

    결국 외부 저장소로 만들기로 했다. sd카드 슬롯이 없는데도 외부 저장소로 저장이 되었다. 꼭 sd카드일 필요는 없어 보인다. stack overflow에 있는 내용을 요약하면..
    First, get a file object

    You’ll need the storage path. For the internal storage, use:

    File path = context.getFilesDir();

    For the external storage (SD card), use:

    File path = context.getExternalFilesDir(null);

    위 부분을 아래와 같이 사용하면 앱별 폴더 이름을 특성화 할 수 있다.

     File directory = new File(context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS), "GapnFlush");
                if (!directory.mkdirs()) {
                    Log.d("file", "Directory was created");
                }

    이렇게 하면 갤럭시 노트의 경우,
    /storage/emulated/0/Android/data/app이름/files/Documents에 GapnFlush란 폴더를 만든다.

    Then create your file object:

    File file = new File(path, "my-file-name.txt");

    Write a string to the file

    FileOutputStream stream = new FileOutputStream(file);
    try {
        stream.write("text-to-write".getBytes());
    } finally {
        stream.close();
    }

    Or with Google Guava

    Files.write("text-to-write", file, "UTF-8");

    앱 실행 전 권한도 설정을 해줘야 된다.

        <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>

    이제 파일은 저장이 되니까, 입맛대로 쓰면 된다..