오늘은 Android 에서 User Data를 저장하는 방법에 대해 이야기 해보도록 하겠습니다.

사용자가 가장 많이 접할 수 있는 데이터 저장 방법은 Internal Storage 와 External Storage 를 이용하는 것입니다.

External Storage 라고 해서 SDCard 같은 외부 저장장치를 연결 해야만 생기는 것이라고 오해할 수 있지만 개념은 살짝 다릅니다.

Android 에서 이야기하는 Internal Storage 는 애플리케이션 데이터가 저장되는 영역을 말하고, 사진이나 동영상 등이 저장될 수 있는 사용자 영역을 External Storage 라고 이야기 합니다.

Internal/External Storage 외에도 데이터를 저장하는 방법은 여러가지가 있겠지만, 이번 포스팅에서는 그 중 가장 간단한 방법인 Internal Storage 와 External Storage 를 사용하는 방법에 대해 알아 보겠습니다.

 

1. Internal Storage

Internal Storage 는 External Storage 와는 다르게 별도의 Permission 추가 없이 사용할 수 있는 저장장치 입니다.  

Internal Storage 에 저장된 파일은 자신의 앱에서만 액세스 가능하며, 사용자가 앱을 삭제할 경우 시스템이 Internal Storage 에서 앱의 모든 파일을 제거하게 됩니다.

=> 즉, 사용자와 다른 앱이 자신의 파일에 액세스 하는것을 원하지 않을 경우 가장 적합하게 사용될 수 있습니다.

Internal Storage 에 데이터를 저장할 때에는 openFileOutput() 함수를 사용 합니다.

이 함수는 내부 디렉터리의 파일에 데이터를 쓰는 FileOutputStream 을 retun 하여, 내부 저장소에 파일을 쓸 수 있도록 합니다.

 

2. External Storage

External Storage 를 사용하기 위해서는 Internal Stroage 와는 다르게 파일을 읽고 쓰기 위한 Permission 이 필요합니다.

    <USES-PERMISSION android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <USES-PERMISSION android:name="android.permission.READ_EXTERNAL_STORAGE" />

외부 저장소는 항상 사용 가능한 것은 아니고, 사용자가 sdcard 등의 외부 저장소를 mount 했을 경우에만 사용 가능 합니다.

그렇기 때문에 외부저장소를 사용하기 전에, getExternalStorageState() 함수를 호출하여 외부 저장소가 사용 가능한지에 대해 확인하는 것이 좋습니다.

이 함수가 return 하는 값이 MEDIA_MOUNTED 일 경우에, External Stroage 에 read/write 가 가능한 상태입니다.

외부저장소는 내부저장소와는 다르게 모든 앱에서 읽을 수 있기 때문에, 다른 앱과 공유하기 원하는 파일들을 저장하기 적합합니다.

 

그러면 이제부터 Internal Storage 와 External Strorage 에 파일을 저장하는 예제를 다뤄보도록 하겠습니다.

1. Layout 구성

Layout은 EditText, Button 3개, TextView 를 두어, EditText 에 입력된 String 을 Internal 혹은 External Storage 에 저장하고 TextView 에 출력할수 있도록 구성하겠습니다.

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.codetravel.storeuserdata.MainActivity">

<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="Enter your message"/>

<Button
android:id="@+id/bt_internal"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Internal"/>


<Button
android:id="@+id/bt_external"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="External"/>

<TextView
android:id="@+id/tv_output"
android:layout_width="match_parent"
android:layout_height="wrap_content"
/>

<Button
android:id="@+id/bt_print"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Print message" />
</LinearLayout>

 

2. Internal Stroage 에 저장하기

위에서 언급했다시피, Internal Storage 를 사용하기 위해서는 openFileOutput(..) 을 이용합니다.

이 함수는 자신의 앱에서만 사용할 수 있는 private 한 파일을 open 합니다.

첫번째 parameter 로 open 할 파일의 이름을 전달 해주고, 두번째 parameter 로 operation mode 를 전달해 줍니다.

file open mode 에는 MODE_PRIVATE, MODE_WORLD_READABLE, MODE_WORLD_WRITEBLE, MODE_APPEND 4가지가 있습니다.

MODE_PRIVATE 는 default mode 로 파일을 생성한 어플리케이션에서만 이 파일에 접근 할 수 있게 하는 모드입니다.

MODE_APPEND 도 MODE_PRIVATE 와 비슷하지만, 파일이 이미 있을 경우에 내용을 그 뒤에 이어 붙이게 됩니다.

MODE_WORLD_READBLE 과 MODE_WORLD_WRITEBLE 은 보안 문제로 API level 17에서 부터 지워진 모드라고 하네요. ^^

 

이 예제에서는 MODE_PRIVATE 를 이용해 파일을 생성해 보았습니다.

View.OnClickListener listener = new View.OnClickListener() { @Override public void onClick(View view) { String inputData = mEtInput.getText().toString(); switch(view.getId()) { case R.id.bt_internal: FileOutputStream fos = null; try { fos = openFileOutput("internal.txt", Context.MODE_PRIVATE); fos.write(inputData.getBytes()); fos.close();; } catch (FileNotFoundException e) { e.printStackTrace(); } catch (IOException e) { e.printStackTrace(); } break;

 

Button Click Listener 를 하나 생성 한 후, Internal 버튼이 눌렸을 경우 Internal Storage 에 저장하기 위해서 openFileOuput 함수를 호출해 FileOutputStream 을 가져왔습니다.

이 FileOutputStream 을 이용해 EditText 로 부터 입력받은 String 을 파일에 write 해 해줍니다.

openFileOutput 을 통해 생성된 FileOutputStream 은 /data/data/[project명]/ 아래에 파일을 저장하게 됩니다.

 

3. External Storage 에 저장하기

external 버튼이 눌렸을 경우에는 먼저 getExternalStorageState() 함수를 통해 외부저장장치가 Mount 되어 있는지를 확인 합니다.

Mounted 되어 있는 경우에만 File 을 하나 생성하고, FileWriter 를 이용해 EditText 의 내용을 저장해 주었습니다.

getExternalStorageDirectory() 를 통해 가져온 경로는 /storage/external/0/ 의 위치에 파일을 저장하게 됩니다.

    case R.id.bt_external:
         if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) {
             File file = new File(Environment.getExternalStorageDirectory(), "External.txt");
             try {
                 FileWriter fw = new FileWriter(file, false);
                 fw.write(inputData);
                 fw.close();
             } catch(IOException e) {
                 e.printStackTrace();
             }
         }
         else {
             Log.d(TAG, "External Storage is not ready");
         }

 

 

4. 데이터 출력하기

위에서 저장한 데이터를 출력하는 코드입니다.

출력 버튼이 눌리면, StringBuffer 를 하나 생성해 줍니다. 여기에 쓰여지는 모든 데이터를 TextView 에 뿌려주게 될 것입니다.

Internal Storage 의 데이터를 읽어올 땐 openFileInput 함수를 이용해서 FileInputStream 을 가지고 오고, ExternalStorage 의 파일을 읽어올 때는 FileReader 를 이용해 읽어오면 됩니다.

case R.id.bt_print:
     StringBuffer buffer = new StringBuffer();
     String data = null;
     FileInputStream fis = null;
     try {
         fis = openFileInput("internal.txt");
         BufferedReader iReader = new BufferedReader(new InputStreamReader((fis)));

         data = iReader.readLine();
         while(data != null)
         {
             buffer.append(data);
             data = iReader.readLine();
         }
         buffer.append("\n");
         iReader.close();
     } catch (FileNotFoundException e) {
         e.printStackTrace();
     } catch (IOException e) {
         e.printStackTrace();
     }

     String path = Environment.getExternalStorageDirectory() + "/External.txt";

     try {
         BufferedReader eReader = new BufferedReader(new FileReader(path));
         data = eReader.readLine();
         while(data != null)
         {
             buffer.append(data);
             data = eReader.readLine();
         }
         mTvOutput.setText(buffer.toString()+"\n");
         eReader.close();

     } catch (FileNotFoundException e) {
         e.printStackTrace();
     } catch (IOException e) {
         e.printStackTrace();
     }

     break;

 

아래와 같이 간단하게 완성이 되었습니다. 각 버튼을 눌렀을 때 Internal/External Storage 에 저장하고, Print 버튼을 눌렀을 때 저장된 내용이 출력되도록 한 예제입니다. 작성된 코드는 https://github.com/bettercho/MyGithub/tree/master/storeuserdata 를 참고하세요.

 

 

 

 

 

Kotlin에서는 Range expression 을 통해서 개발자들이 편하게 사용할 수 있도록 해줍니다.

 

가장 흔하게 if 문이나 for문에서 사용될 수 있습니다.

".." operator 를 갖는 rangeTo 함수로 표현합니다.

아래는 C / JAVA 언어의 if (i>=1 && i<= 10) 조건문과 같은 의미를 가집니다.

if (i in 1..10) { // equivalent of 1 <= i && i <= 10
    println(i)
}

아래와 같이 반복문에서도 사용합니다

이때 1..4 와 같이 커지는 순서로 for문을 동작 시키게 되는데요

아래와 같이 4..1 이 되면 아무것도 수행되지 않게 됩니다.

for (i in 1..4) print(i) // prints "1234"

for (i in 4..1) print(i) // prints nothing

그렇다면 C언어에서처럼 i값을 점점 작아지게 하고 싶으면 어떻게 하면 될까요?

C언어에서 for (int i = 4 ; i > 0 ; i --) 와 같이 사용되는 반복문을

Kotlin에서는 downTo() 함수를 통해 아래와 같이 표현할 수 있습니다.

for (i in 4 downTo 1) print(i) // prints "4321"

 

또한 step을 사용하여 일정한 간격으로 i를 증가시킬 수도 있고,

downTo와 step을 함께 사용하여 역순으로 일정간격으로 i를 감소시킬 수도 있습니다.

첫번째 for문은 1부터 4까지 2씩 건너뛰므로, 1과 3이 출력됩니다.

두번째 for문은 4부터 1까지 2씩 건너뛰므로, 4와 2가 출력됩니다.

for (i in 1..4 step 2) print(i) // prints "13"

for (i in 4 downTo 1 step 2) print(i) // prints "42"

 

until 함수를 사용하여 범위를 결정할 수도 있습니다.

(아래의 식에서 i는 1에서 9까지만 수행합니다. 10은 포함되지 않습니다.)

C언어에서보다는 쉽고 다양하게 표현할 수 있지만,

처음 접한다면 익숙한 C언어의 표현을 바꿔서 하기에 좀 불편하기도 할 것 같아요.

for (i in 1 until 10) { // i in [1, 10), 10 is excluded
     println(i)
}

 

마지막으로 last 함수를 사용하여 마지막 i 값을 확인할 수 있습니다.

(1..12 step 2).last == 11  // progression with values [1, 3, 5, 7, 9, 11]
(1..12 step 3).last == 10  // progression with values [1, 4, 7, 10]
(1..12 step 4).last == 9   // progression with values [1, 5, 9]

 

 

 

프로세스를 생성하고자 할 때 fork 함수를 사용하면 됩니다.

fork 함수를 호출하는 프로세스는 부모 프로세스가 되고 새롭게 생성되는 프로세스는 자식 프로세스가 됩니다.

fork 함수에 의해 생성된 자식 프로세스는 부모 프로세스의 메모리를 그대로 복사하여 가지게 됩니다.

그리고 fork 함수 호출 이후 코드부터 각자의 메모리를 사용하여 실행됩니다


fork 함수를 사용하기 위해서는 unistd.h를 include하면 됩니다.

fork 함수는 unistd.h 파일에 system call로 정의되어 있습니다.

// unistd.h header file

pid_t fork(void); // 성공 시 : 부모 프로세스에서는 자식 프로세스의 PID값을 반환 받음

// 자식 프로세스에서는 0 값을 반환 받음

// 실패 시 : 음수 값(-1) 반환


간단한 예제 코드를 살펴 보겠습니다.

#include <stdio.h>
#include <unistd.h>

int main() {
    int x;
    x = 0;
    
    fork();
    
    x = 1;
    printf("PID : %ld,  x : %d\n",getpid(), x);
    
    return 0;
}


실행 결과는 다음과 같습니다.

fork 함수 코드 이후부터는 부모 프로세스와 자식 프로세스가 각자의 x = 1, printf() 코드를 실행하였습니다.

그렇기 때문에 PID 43889(부모 프로세스)에서 x 값은 1을 출력하였고 PID 43895(자식 프로세스)에서도 x 값은 1이라고 출력하였습니다.

PID : 43889,  x : 1

PID : 43895,  x : 1 


그림을 통하여 다시 한번 보도록 하겠습니다.

fork 함수가 실행 된 직후에는 자식 프로세스 부모 프로레스와 동일한 주소 공간의 복사본을 가지게 됩니다.

fork 함수 실행 이후 , 부모와 자식 프로세스는 동일한 코드 (x = 1, printf())를 각자 메모리상에서 실행하고 있습니다.


이번에는 fork 함수의 리턴값을 검사하여 부모와 자식 코드를 각각 분리하도록 하겠습니다.

fork 함수는 부모 프로세스에게는 자식프로세스의 PID를 반환하며 자식 프로세스에게는 0을 반환합니다.

#include <stdio.h>
#include <unistd.h>
#include <sys/types.h>

int main() {
    
    pid_t pid;
    
    int x;
    x = 0;
    
    pid = fork();
    
    if(pid > 0) {  // 부모 코드
        x = 1;
        printf("부모 PID : %ld,  x : %d , pid : %d\n",(long)getpid(), x, pid);
    }
    else if(pid == 0){  // 자식 코드
        x = 2;
        printf("자식 PID : %ld,  x : %d\n",(long)getpid(), x);
    }
    else {  // fork 실패
        printf("fork Fail! \n");
        return -1;
    }
    
    return 0;

}


실행 결과는 다음과 같습니다. 

pid 반환값을 검사하여 부모 프로세스와 자식 프로세스에서 실행될 코드를 별도로 작성할 수 있는것을 확인 하였습니다.

부모 PID : 46834,  x : 1 , pid : 46838

자식 PID : 46838,  x : 2 


fork 함수가 실행 된 직후에는 자식 프로세스 부모 프로레스와 동일한 주소 공간의 복사본을 가지게 됩니다.

하지만 fork 함수 이후의 코드가 pid값을 기준으로 분리되어 있습니다.

따라서 각 프로세스의 메모리 공간의 x값은 서로 달라지게 됩니다. 


지금까지 fork 함수 사용에 대해서 살펴 보았습니다.

BeautifulSoup 은 HTML 및 XML 파일에서 원하는 데이터를 손쉽게 Parsing 할 수 있는 Python 라이브러리 입니다.

오늘은 Beautiful Soup 라이브러리를 활용하여 HTML 코드 안에서 원하는 Data 를 뽑아내는 예제를 다뤄보려고 합니다.

 

1. Beautiful Soup 설치하기

Beautiful Soup 라이브러리는 Python 에서 기본적으로 제공하는 라이브러리에 해당하지 않기 때문에 별도의 설치가 필요합니다.

Windows 에서는 cmd 창에서 간단하게 pip 명령으로 설치할 수 있습니다.

- pip install beautifulsoup4


pip 는 Python 설치 디렉토리의 Scripts 폴더 밑에 있기 때문에, 해당 위치로 이동하여 명령어를 실행해 주어야 합니다.

설치가 완료되면 Python 설치 디렉토리 밑의 Lib/site-packages/ 위치에 bs4 디렉토리가 생성된 것을 확인 할 수 있습니다.

 

2. HTML 에서 원하는 정보 추출하기 예제

먼저 BeautifulSoup 라이브러리 사용을 위해 bs4 를 import 합니다.

from bs4 import BeautifulSoup

 예제에 사용될 HTML 문서는 다음과 같습니다.

html_doc = """
<html><head><title>The Dormouse's story</title></head>
<body>
<p class="title"><b>The Dormouse's story</b></p>

<p class="story">Once upon a time there were three little sisters; and their names were
<a href="http://example.com/elsie" class="sister" id="link1">Elsie</a>,
<a href="http://example.com/lacie" class="sister" id="link2">Lacie</a> and
<a href="http://example.com/tillie" class="sister" id="link3">Tillie</a>;
and they lived at the bottom of a well.</p>

<p class="story">...</p>
"""

문서를 Parsing 하기 위해서 BeautifulSoup 생성자에 해당 문서를 인자로 전달 해주어야 합니다.

아래와 같이 문자 열을 전달할 수도 있고, file handle 을 전달 할 수도 있습니다.

from bs4 import BeautifulSoup

with open("index.html") as fp:
    soup = BeautifulSoup(fp)

soup = BeautifulSoup("<html>data</html>")

위의 html_doc 문자열을 전달하여 BeautifulSoup 객체를 생성 합니다.

그렇게 생성한 soup 객체를 통해 HTML Parsing 을 위해 제공되는 BeautifulSoup 라이브러리의 여러가지 API 들을 사용해 보겠습니다.

if __name__  == "__main__":
    soup = BeautifulSoup(html_doc, 'html.parser')
    print('1. ', soup.title)
    print('2. ', soup.title.name)
    print('3. ', soup.title.string)
    print('4. ', soup.title.parent.name)
    print('5. ', soup.p)
    print('6. ', soup.p['class'])
    print('7. ', soup.a)
    print('8. ', soup.find_all('a'))
    print('9. ', soup.find(id="link3"))

 

1번 부터 9번까지 파싱 결과를 살펴보겠습니다.

원하는 tag 를 가지고 오고 싶을 때 1번과 5번 경우처럼 간단하게 soup.(원하는tag 명) 을 사용할 수 있습니다.

특정 tag 로 감싸진 내용만 가져오고 싶다면 3번과 같이 soup.title.string 을 사용하면 됩니다.

.(tag 명)의 경우 해당 태그를 포함하여 그 태그가 끝날 까지의 문장을 가지고 오고, .name, .string, .parent.name 등을 통해 더 자세한 정보들을 얻어 올 수 있습니다.

6번의 경우 soup.p['class'] 라고 하면 tag 가 p 인 것들 중 속성이 class 인 부분을 파싱 합니다.

7번과 8번의 다른점은 soup.a 의 경우 html 중 tag 가 a 인 첫번째 항목을 뽑아내지만,  find_all 을 이용하면 tag 가 a 인 것들을 모두 리스트의 형태로 뽑아 낼 수 있습니다.

 

3. 네이버 영화 랭킹 페이지에서 영화 목록 가져오기

이번에는 자주 들어가는 Naver 포탈 사이트에서 제공하는 영화 목록을 가져와 보도록 하겠습니다.

먼저 Parsing 할 주소는 http://movie.naver.com/movie/sdb/rank/rmovie.nhn?sel=pnt&date=20180117 입니다.

페이지에 들어가 보면 평점이 가장 높은 영화부터 쭉 리스팅이 되어있습니다.  많은 정보들 중에 영화의 제목만 파싱해 오도록 하겠습니다.

 

먼저 BeautifulSoup 을 하나 생성하고, 인자로 네이버 영화 랭킹 페이지를 전달 합니다.

soup = BeautifulSoup(urllib.request.urlopen('http://movie.naver.com/movie/sdb/rank/rmovie.nhn?sel=pnt&date=20161120').read(), 'html.parser')

 

해당 페이지가 어떤 tag 들로 어떻게 구성되어있는지 보고싶다면,

print(soup.prettify())

를 이용할 수도 있지만, Chrome 에서 제공하는 개발자도구를 이용하면 더 편리합니다. (도구더보기 -> 개발자도구)

html 코드로 보면 아래와 같이 영화 제목은 'div' tag 의 'tit5'를 속성으로 가진 항목들에 포함되어 있습니다.

<div class="tit5"> <a href="/movie/bi/mi/basic.nhn?code=17421" title="쇼생크 탈출"> 쇼생크 탈출 </a> </div>

그래서 생성한 BeautifulSoup 객체의 find_all 함수를 이용해서 영화 제목이 담긴 div tag 를 가진 항목을 list 로 뽑아 내었습니다.

if __name__  == "__main__":
    soup = BeautifulSoup(urllib.request.urlopen('http://movie.naver.com/movie/sdb/rank/rmovie.nhn?sel=pnt&date=20161120').read(), 'html.parser')
    res = soup.find_all('div', 'tit5')
    print(res)

 

위 코드에서 res 를 출력해보면, 의도했던대로 모든 div tag 항목이 저장되어 있는 것을 확인 할 수 있습니다.

위와 같이 1차적으로 Parsing 이 되었고, 완벽하게 영화 제목만 가져오기 위해 다시한번 파싱을 해줍니다.

동일 Tag 로 잘 분리가 되어있다면, get_text() 를 이용해 해당 tag 에서 text 정보면 가져올 수 있습니다.

그래서 최종적으로 완성된 코드는 아래와 같이 간단합니다.

if __name__  == "__main__":
    soup = BeautifulSoup(urllib.request.urlopen('http://movie.naver.com/movie/sdb/rank/rmovie.nhn?sel=pnt&date=20161120').read(), 'html.parser')
    res = soup.find_all('div', 'tit5')

    for n in res:
        print(n.get_text())

결과를 출력해 보면 원하는 데로 제목만 잘 출력이 되었습니다. ^^

결과적으로, 네이버 페이지에 있는 많은 영화 제목들을 단 몇줄로 간단하게 출력해본 예제입니다.

예제는 네이버 페이지이지만, 다양한 사이트에서 원하는 정보를 뽑아낼 때 활용할 수 있겠습니다.

 

 

지난 포스팅에서 'MediaRecorder 를 이용한 오디오 레코딩 예제' 를 살펴보았습니다.

이번에는 지난번에 생성한 Project에 MediaRecorder API 를 이용한 캠코딩 예제를 추가해 보도록 하겠습니다.

즉 지난번에는 음성만 녹음하였다면, 이번에는 카메라로 들어오는 화면도 함께 캠코딩하는 것입니다.

 

1. Permission

우선 캠코딩을 하기 위해서 Camera 를 사용해야 하기 때문에, AndroidManifest.xml 에 Camera 관련 Permission 을 추가해 줍니다.

총 3개의 Permission 입니다. 음성을 녹음할 수 있는 권한과, 캠코딩된 파일을 저장해야 하기 때문에 External Storage 에 쓸수 있는 권한, 그리고 카메라로 들어오는 영상을 캠코딩해야 하므로 Camera 사용 권한도 함께 줄 수 있도록 합니다.

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

 

2. Layout 구성하기

Layout은 지난번 구현하였던 오디오 레코딩 예제에 버튼을 하나 추가해서 캠코딩을 시작하고 정지할 수 있게 합니다.

카메라 프리뷰 화면과 녹화된 영상을 재생할 때 Video 를 뿌려줄 SurfaceView 를 하나 구성합니다.  

<Button
android:id="@+id/bt_camcording"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start Camcording"/>



<SurfaceView
android:id="@+id/sv"
android:layout_width="match_parent"
android:layout_height="wrap_content" />

캠코딩 시작 버튼이 눌렸을 때 캠코더를 시작하고, 다시 눌렸을 때 종료할 수 있도록 아래와 같이 OnClickListener 를 구현 해 줍니다.

mBtCamcording = (Button)findViewById(R.id.bt_camcording);
mBtCamcording.setOnClickListener(new View.OnClickListener(){
@Override
public void onClick(View v) {
hasVideo = true;
initVideoRecorder();
startVideoRecorder();
}
});

 

3. Camera 와 Surface 연결 및 레코딩 시작 하기

위에서 버튼이 눌리면 가장 먼저 실행되는 initVideoRecorder() 함수에서는 Camera Device 를 open 하고, SurfaceHolder 를 초기화 하는 일을 수행 합니다.

Camera.open() 함수는 카메라 인스턴스를 리턴하고, 사용자는 이 객체를 통해 카메라 Device 에 접근할 수 있습니다.

Camera.open(int) 함수를 이용하면 Device 에 장착되어 있는 여러개의 카메라 중, 원하는 카메라에 접근하여 사용할 수 있습니다.

void initVideoRecorder() {
mCamera = Camera.open();
mCamera.setDisplayOrientation(90);
mSurfaceHolder = mSurface.getHolder();
mSurfaceHolder.addCallback(this);
mSurfaceHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
}

mSurfaceHolder 에는 SurfaceView 의 getHolder 메소드를 통해 SurfaceHolder 의 인스턴스를 연결해 주고, Surface 의 변화가 있을 때 처리를 위해 Callback 을 등록해 줍니다.

두번째로 불리는 startVideoRecorder 함수에서는 MediaRecorder 를 초기화 하고, 실제 레코딩을 수행 합니다.

void startVideoRecorder() {
if(isRecording) {
mRecorder.stop();
mRecorder.release();
mRecorder = null;

mCamera.lock();
isRecording = false;

mBtCamcording.setText("Start Camcording");
}
else {
runOnUiThread(new Runnable() {
@Override
public void run() {
mRecorder = new MediaRecorder();
mCamera.unlock();
mRecorder.setCamera(mCamera);
mRecorder.setAudioSource(MediaRecorder.AudioSource.CAMCORDER);
mRecorder.setVideoSource(MediaRecorder.VideoSource.CAMERA);
mRecorder.setOutputFormat(MediaRecorder.OutputFormat.MPEG_4);
mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);
mRecorder.setVideoEncoder(MediaRecorder.VideoEncoder.MPEG_4_SP);
mRecorder.setOrientationHint(90);

mPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/record.mp4";
Log.d(TAG, "file path is " + mPath);
mRecorder.setOutputFile(mPath);

mRecorder.setPreviewDisplay(mSurfaceHolder.getSurface());
try {
mRecorder.prepare();
}catch(Exception e){
e.printStackTrace();
}
mRecorder.start();
isRecording = true;

mBtCamcording.setText("Stop Camcording");
}
});
}
}

음성 녹음만 할 때는 Audio 에 관련된 설정들만 해주면 되었지만, 캠코딩인 경우에는 Video Source 에 대한 설정도 해주어야 합니다.

setCamera(..) :  비디오 레코딩 시 사용할 카메라를 설정해 줍니다.

setVideoSource(..) : 레코딩 시 비디오 소스를 설정합니다. DEFAULT, CAMERA, SURFACE 세개의 값 중 하나를 선택하면 됩니다.

setVideoEncoder(..) : 비디오 코덱을 설정합니다. Video Encoder 의 경우 DEFAULT, H263, H264, MPEG_4_SP, VP8, HEVC 등을 제공하고 있습니다.

Android 에서 제공하는 Video/Audio Encoder/Decoder 의 정보는 하기 사이트에서 더 자세하게 볼 수 있습니다.

https://developer.android.com/guide/topics/media/media-formats.html

 

각종 설정들을 마무리 하고 mRecorder.start() 를 호출하면 드디어 캠코딩이 시작됩니다.

캠코딩 시 Preview 화면은 MediaRecorder 의 setPreviewDisplay(..) 함수를 통해 설정해 준 SurfaceView 에 뿌려지게 됩니다.

 

4. 레코딩 한 파일 재생하기

재생의 경우에는 Audio 파일 재생과 완전히 동일하지만, Video 의 경우 화면을 뿌려주기 위한 Surface View 만 추가로 지정해 주면 됩니다.

기존에 만들어 두었던 Player Button 의 OnClickListener 에 아래와 같이 Video 를 가질 경우, 만들어 놓았던 SurfaceHolder 를 지정 해 주고,

if(hasVideo == true) {
mPlayer.setDisplay(mSurfaceHolder);
mPlayer.setOnCompletionListener(mListener);
}

Video 재생이 끝났을 경우 Button 의 Text 를 변경해 주기 위한 OnCompletionListener 를 등록해 줍니다.

MediaPlayer.OnCompletionListener mListener = new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
mBtPlay.setText("Start Playing");
}
};

등록된 Listener 에서는 간단하게 버튼의 Text 만 변경해 주었습니다.

 

여기까지 아직 많은 예외처리들이 필요하지만, 기본적인 오디오, 비디오 레코더를 완성 하였습니다.

MediaRecorder API와 Camera Class 에서 제공하는 API를 사용하면 이렇게 간단하게 레코딩 기능을 구현할 수 있습니다.

전체 소스는 https://github.com/bettercho/MyGithub/tree/master/MediaRecorder/app/src 를 참고해 주세요.

 

 

getMainLooper() 함수 


getMainLooper() 함수는 Main Thread(UI Thread)가 사용하는 Looper 즉 Main Looper를 반환합니다.

이 함수는 호출하는 스레드가 메인 스레드이거나 메인 스레드가 아니어도 언제든지 Main Looper를 반환합니다.

참고 1) Main thread의 Main Looper와 Handler는 ActivityThread에서 자동으로 생성하기 때문에 개발자가 명시적으로 생성하지 않습니다.

참고 2) Looper.myLooper() 함수는 호출한 스레드의 Looper를 반환합니다.


getMainLooper() 함수는 어떤 경우에 사용하면 될까요?

크게 3가지의 경우에 사용합니다.


1. Handler를 생성할 때 Main Looper를 생성자의 인자로 전달하고 싶을 경우

즉, 작업자 스레드(UI thread가 아닌 스레드)에서 UI thread에게 "Runnabel 작업" 또는 "메시지"을 보내고 싶을 때 사용할 수 있는 방법입니다.

View.post() 또는 runOnUiThread() API를 사용하는 것도 작업자 스레드에서 UI thread로 Runnable객체를 전달하는 용도로 사용됩니다.


public class MainActivity extends AppCompatActivity {
    private String TAG = "MainActivity";
    private TestThread mTestThread;
    private Button mButton;
    static int count = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mButton = (Button)findViewById(R.id.button);
    }

    public void onClickButton(View view) {
        Log.d(TAG, "onClickButton");

        mTestThread = new TestThread();
        mTestThread.start();
    }

    class TestThread extends Thread {
        private Handler mHandler;

        TestThread() {
            mHandler = new Handler(Looper.getMainLooper()) {  // 핸들러에 Main Looper를 인자로 전달
                @Override
                public void handleMessage(Message msg) {  // 메인 스레드에서 호출
                    Log.d(TAG,"handleMessage : " + msg.what);

                    switch(msg.what) {
                        case 0:
                            mButton.setText("Button 0");
                            break;
                        case 1:
                            mButton.setText("Button 1");
                            break;
                        case 2:
                            mButton.setText("Button 2");
                            count = 0;
                            break;
                        default:
                            break;
                    }
                }
            };
        }

        @Override
        public void run() {
            Log.d(TAG, "Start TestThread");

            Message msg = mHandler.obtainMessage(count++);
            mHandler.sendMessage(msg);  // 메인 스레드로 메시지를 보냄
        }
    }
}

Handler를 생성할 때 인자로 Looper를 전달하면 어떤 과정을 거치는지 보도록 하겠습니다. (xref : Handler.java)


Main Looper를 인자로 넣으면 Main Looper의 Queue를 mQueue로 설정합니다. 이 부분이 중요합니다.

결론적으로 sendMessage() 또는 post()를 하게 되면 mQueue에 메시지나 작업이 들어가게됩니다. 즉 Main thread의 큐에 들어갑니다.

따라서 dequeue가 될때 Main thread에서 실행되는 것입니다.


실행 로그를 확인해 보겠습니다.

TestThread 스레드는(TID 12310) sendMessage() 함수를 호출하였고, 메인 스레드(TID 12235)에서 handleMessage() 함수가 호출되었습니다.

이 함수는 버튼을 터치할 때 전달 받은 msg.what 값에 setText("Button 0"), setText("Button 1"), setText("Button 2") 반복적으로 변경합니다. 

01-06 14:05:30.519 12235-12235/com.example.codetravel.getmainlooper D/MainActivity: onClickButton

01-06 14:05:30.562 12235-12310/com.example.codetravel.getmainlooper D/MainActivity: Start TestThread

01-06 14:05:30.586 12235-12235/com.example.codetravel.getmainlooper D/MainActivity: handleMessage : 0

01-06 14:05:32.511 12235-12235/com.example.codetravel.getmainlooper D/MainActivity: onClickButton

01-06 14:05:32.537 12235-12335/com.example.codetravel.getmainlooper D/MainActivity: Start TestThread

01-06 14:05:32.571 12235-12235/com.example.codetravel.getmainlooper D/MainActivity: handleMessage : 1

01-06 14:05:34.660 12235-12235/com.example.codetravel.getmainlooper D/MainActivity: onClickButton

01-06 14:05:34.698 12235-12366/com.example.codetravel.getmainlooper D/MainActivity: Start TestThread

01-06 14:05:34.717 12235-12235/com.example.codetravel.getmainlooper D/MainActivity: handleMessage : 2


같은 원리로 다음 예제는 Runnable 작업을 메인 스레드로 전달하는 코드입니다.


public class MainActivity extends AppCompatActivity {
    private String TAG = "MainActivity";
    private TestThread mTestThread;
    private Button mButton;
    static int count = 0;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mButton = (Button)findViewById(R.id.button);
    }

    public void onClickButton(View view) {
        Log.d(TAG, "onClickButton");

        mTestThread = new TestThread();
        mTestThread.start();
    }

    class TestThread extends Thread {

        @Override
        public void run() {
            Log.d(TAG, "Start TestThread");

            Handler handler = new Handler(Looper.getMainLooper());  // 핸들러에 메인 루퍼를 인자로 전달
            handler.post(new Runnable() {  // 메인 스레드로 Runnable 객체를 보냄, runOnUiThread()함수 사용과 유사

                @Override
                public void run() {  // run()함수는 메인 스레드에서 실행 됨
                    Log.d(TAG, "Change Button text");
                    mButton.setText("Button changed");
                }
            });
        }
    }
}


2. 현재 스레드의 루퍼가 Main Looper인지 아닌지 검사하고 싶을 경우

아래 예제에는 코드와 같이 버튼 UI 텍스트를 변경하는 changeButtonText() 함수가 있습니다.

이 함수를 호출하는 스레드의 Looper가 Main Looper 인지 아닌지에 따라서 버튼 UI 텍스트를 변경하는 방법을 다르게 하고 있습니다.

UI 변경은 UI thread에서만 허용하기 때문입니다.

만약 Main Looper라면 이것은 스레드가 Main 스레드를 의미하기 때문에 바로 setText() 함수를 호출 할 수 있습니다.

하지만 Main Looper가 아니라면 runOnUiThread() 또는 View.post() 함수 등을 사용해야 합니다.


public class MainActivity extends AppCompatActivity {
    private String TAG = "MainActivity";
    private TestThread mTestThread;
    private Button mButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mButton = (Button)findViewById(R.id.button);
    }

    public void onClickButton(View view) {
        Log.d(TAG, "onClickButton");
        changeButtonText();

        mTestThread = new TestThread();
        mTestThread.start();
    }

    class TestThread extends Thread {
        @Override
        public void run() {
            Log.d(TAG, "Start TestThread");
            changeButtonText();
        }
    }

    public void changeButtonText() {
        Log.d(TAG, "changeButtonText myLooper() " + Looper.myLooper());

        if (Looper.getMainLooper() == Looper.myLooper()) { // 현재 스레드의 루퍼와 메인 루퍼가 같은지 비교
            mButton.setText("Button 1");
            Log.d(TAG, "changeButtonText method is called from main thread");
        } else {
            (MainActivity.this).runOnUiThread(new Runnable() {

                @Override
                public void run() {
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    mButton.setText("Button 2");
                    Log.d(TAG, "changeButtonText method is called from non-main thread");
                }
            });
        }
    }
}



실행 로그 입니다.

onClickButton에서 실행된 changeButtonText() 함수는 Main Looper를 사용하는 스레드에서 호출되었습니다.

그리고 별도로 생성한 스레드에서 호출된 changeButtonText() 함수는 당연히 Looper를 생성한 적이 없으므로 Looper.myLooper() 함수를 호출하면 null을 리턴합니다.

13:03:11.005 11277-11277/com.example.codetravel.getmainlooper D/MainActivity: onClickButton

01-06 13:03:11.005 11277-11277/com.example.codetravel.getmainlooper D/MainActivity: changeButtonText myLooper() Looper (main, tid 1) {f383015}

01-06 13:03:11.006 11277-11277/com.example.codetravel.getmainlooper D/MainActivity: changeButtonText method is called from main thread

01-06 13:03:11.008 11277-11364/com.example.codetravel.getmainlooper D/MainActivity: Start TestThread

01-06 13:03:11.008 11277-11364/com.example.codetravel.getmainlooper D/MainActivity: changeButtonText myLooper() null

01-06 13:03:14.034 11277-11277/com.example.codetravel.getmainlooper D/MainActivity: changeButtonText method is called from non-main thread 

Looper.myLooper() 함수를 로그로 출력하였을 때 내용은 아래와 같습니다.

Looper (main, tid 1)   <== 메인 스레드에서 호출

null   <== TestThread에서 호출

이것이 의미하는 것을 알기 위해서 Looper 클래스의 toString()함수를 보도록 하겠습니다. (xref : Looper.java)

Looper (" 스레드 이름", "스레드 ID") 임을 알 수 있습니다.



3. 현재 스레드가 Main thread(UI thread)인지 아닌지 검사하고 싶을 경우

public class MainActivity extends AppCompatActivity {
    private String TAG = "MainActivity";
    private TestThread mTestThread;
    private Button mButton;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        mButton = (Button)findViewById(R.id.button);
    }

    public void onClickButton(View view) {
        Log.d(TAG,"onClickButton()" + " " + Thread.currentThread() + " " + Looper.getMainLooper().getThread());

        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            Log.d(TAG,"onClickButton() : This thread is main thread!");
        }
        else {
            Log.d(TAG,"onClickButton() : This thread is not main thread!");
        }

        mTestThread = new TestThread();
        mTestThread.start();
    }

    class TestThread extends Thread {
        @Override
        public void run() {
            Log.d(TAG,"TestThread run()" + " " + Thread.currentThread() + " " + Looper.getMainLooper().getThread());
            if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
                Log.d(TAG,"TestThread run() : This thread is main thread!");
            }
            else {
                Log.d(TAG,"TestThread run() : This thread is not main thread!");
            }
            while(true) {  // ps 명령어로 스레드 ID를 보기 위해서 스레드가 종료되지 않도록 하기 위한 코드
                // 아무 동작도 안하고 그냥 살아 있는 스레드
            }
        }
    }
}


로그를 확인하기 전에 코드만으로 예측해보면 onClickButton() 함수는 UI thread에서 호출되기 때문에 "This thread is main thread!" 출력될 것이라는 것을 예측할 있습니다.

그리고 TestThread 스레드가 생성되어 run() 함수가 동작하는 스레드는 메인 스레드가 아닌 백그라운드 스레드입니다.

따라서 "This thread is not main thread!" 출력 것입니다.

실행 로그를 보도록 하겠습니다.


앞서 예측한 결과와 동일한 값이 출력 되었습니다.

01-06 06:10:49.537 32177-32177/com.example.codetravel.getmainlooper D/MainActivity: onClickButton()  Thread[main,5,main] Thread[main,5,main]

01-06 06:10:49.537 32177-32177/com.example.codetravel.getmainlooper D/MainActivity: onClickButton() : This thread is main thread!

01-06 06:10:49.620 32177-822/com.example.codetravel.getmainlooper D/MainActivity: TestThread run() Thread[Thread-175,5,main] Thread[main,5,main]

01-06 06:10:49.620 32177-822/com.example.codetravel.getmainlooper D/MainActivity: TestThread run() : This thread is not main thread!


ps 정보를 보면 아래와 같이 TestThread의 Name은 "Thread-175" 입니다.

root@generic_x86_64:/ # ps -t 32177 

USER      PID   PPID  VSIZE  RSS   WCHAN              PC  NAME

u0_a68    32177 1264  1293420 48356    ep_poll 7fc1ba06b8ca S com.example.codetravel.getmainlooper    // Main 스레드

u0_a68    32182 32177 1293420 48356 do_sigtime 7fc1ba06b7ca S Signal Catcher

u0_a68    32183 32177 1293420 48356 poll_sched 7fc1ba06b60a S JDWP

u0_a68    32184 32177 1293420 48356 futex_wait 7fc1ba000f68 S ReferenceQueueD

u0_a68    32185 32177 1293420 48356 futex_wait 7fc1ba000f68 S FinalizerDaemon

u0_a68    32186 32177 1293420 48356 futex_wait 7fc1ba000f68 S FinalizerWatchd

u0_a68    32187 32177 1293420 48356 futex_wait 7fc1ba000f68 S HeapTaskDaemon

u0_a68    32188 32177 1293420 48356 binder_thr 7fc1ba06bc67 S Binder_1

u0_a68    32189 32177 1293420 48356 binder_thr 7fc1ba06bc67 S Binder_2

u0_a68    32205 32177 1293420 48356 __skb_recv 7fc1ba06c30a S Thread-172

u0_a68    32214 32177 1293420 48356    ep_poll 7fc1ba06b8ca S RenderThread

u0_a68    32231 32177 1293420 48356 futex_wait 7fc1ba000f68 S hwuiTask1

u0_a68    822   32177 1293420 48356          0 7fc1b11ec6cc R Thread-175     // TestThread 스레드


Thread.currentThread()와 Looper.getMainLooper.getThread() 함수는 모두 Thread 클래스 객체를 반환합니다. 

출력 내용을 보면 아래와 같습니다.

Thread[main,5,main]   <== main thread

Thread[Thread-175,5,main]  <== TestThread


Thread 클래스 객체를 log로 출력했기 때문에 Thread 클래스의 toString() 함수가 호출되었습니다. 

Thread["현재 스레드 이름", "우선순위", "스레드 그룹"] 형식으로 출력됩니다.

main 스레드와 TestThread 스레드는 이름은 다르지만 우선순위와 스레드 그룹은 동일하게 출력되었습니다.

별도로 우선순위와 스레드 그룹을 설정하지 않았기 때문에 default 상태의 값이 설정되어 있습니다.


이상으로 getMainLooper() 함수의 사용에 대해서 알아 보았습니다.


이번 포스팅에서는 Kotline 의 기본 제어문(반복문/조건문) 사용법을 알아보도록 하겠습니다.

모든 언어들이 비슷하기 때문에 하나의 언어만 잘 알아두면 나머지 언어의 문법도 금방금방 익힐 수 있는 것 같습니다.

Kotline 의 제어문도 다른 언어들과 비슷 하지만, 함축적으로 사용되는 부분이 있어 다른 언어들과는 다르게 Kotline 의 문법을 모르면 잘 알아보기가 힘들게 되어 있는 부분도 있습니다.

익숙해 지면 다른 언어들보다 편할것이라는 생각도 드네요. ^^

 

1. If 문

Kotline 에서 if 문은 Expression 입니다. 즉, value 를 return 합니다. if 문 자체로 기존의 3항 연산자의 역할을 대체하기 때문에 더이상 삼항 연산자는 사용되지 않습니다. (조건 ? true : false)

기존 우리는 if 문을 아래와 같이 사용했습니다.

max 에 a 변수를 넣고, 만약 a 보다 b 가 크다면 max 변수에 b 의 값을 넣는 코드입니다.

// Traditional usage 
var max = a 
if (a < b) max = b

// With else 
var max: Int
if (a > b) {
    max = a
} else {
    max = b
}

 Kotline 에서는 위의 if 문을 아래와 같이 간단하게 사용할 수 있습니다.

// As expression 
val max = if (a > b) a else b

즉, " if (a>b) a else b " 구문 자체가 a 혹은 b 의 값을 return 하기 때문에, max 변수에 바로 이 값을 대입할 수 있습니다.

만약, if 문 자체에 블럭이 포함되어야 한다면 위 구문은 아래와 같이 사용할 수 있습니다.

val max = if (a > b) {
    print("Choose a")
    a
} else {
    print("Choose b")
    b
}

위의 경우 블럭의 끝에 쓰여진 값이 return 되는 값이 됩니다.

 

2. When

When 은 일반 언어에서 사용되던 Switch 문을 대체합니다.

when (x) {
    1 -> print("x == 1")
    2 -> print("x == 2")
    else -> { // Note the block
        print("x is neither 1 nor 2")
    }
}

When 옆에 쓰여진 x 에 값이, 블럭 안의 조건에 충족이 될 때 까지 모든 인수를 순차적으로 검사 합니다.

아무 조건에도 충족되지 않으면 else 분기문으로 들어가게 되고, Switch 문의 default 문은 필수가 아니지만, When 의 else 문은 필수로 들어가야 합니다.

많은 경우가 동일한 방식으로 처리되어야 하는 경우에는 . (콤마) 를 사용하여 조건을 추가할 수 있습니다.

when (x) {
    0, 1 -> print("x == 0 or x == 1")
    else -> print("otherwise")
}

각 조건의 경우에는 위와 같이 특정 상수 값 (0, 1) 이 올 수도 있지만, 함수가 올 수도 있습니다.

when (x) {
    parseInt(s) -> print("s encodes x")
    else -> print("s does not encode x")
}

위의 경우에는 x 와 parseInt() 에서 return 되는 값과 일치하게 되면 "s encodes x" 를 출력할 것입니다.

그리고 in 키워드를 사용하여 특정 값의 범위를 지정 할 수도 있습니다.

when (x) {
    in 1..10 -> print("x is in the range")
    in validNumbers -> print("x is valid")
    !in 10..20 -> print("x is outside the range")
    else -> print("none of the above")
}

in 1..10 은 1<= x <=10 의 범위를 의미 합니다. Range 에 대해서는 'https://kotlinlang.org/docs/reference/ranges.html' 를 참고해 보세요 .

마지막으로 when 을 사용할 때 인수를 생략할 수 있습니다.

인수를 생략하게 되면 분기 조건은 단순히 bool 식이 되고, 해당 조건이 참일 때 분기문이 실행 됩니다.

아래의 경우에서는 x 가 홀수 일 경우 "x is odd", 짝수일 경우 'x is even" 둘다 아닐 경우에 "x is funny" 가 출력 됩니다.

when {
    x.isOdd() -> print("x is odd")
    x.isEven() -> print("x is even")
    else -> print("x is funny")
}

 

3. For Loops

for loop 는 비교적 기존에 사용하던 for 문과 크게 다르지 않습니다. iterator 를 제공하는 모든 것들을 반복할 수 있습니다.

아래 예제에서는 item 에 collection 에 있는 모든 값들이 하나씩 대입이 되면서 반복 하게 됩니다.

for (item in collection) print(item)

배열이나 리스트를 반복할 경우에 index 를 사용하고 싶다면 indices를 사용 합니다.

for (i in array.indices) {
    print(array[i])
}

혹은 withIndex' fun 을 사용해서 index 와 value 를 return 받아 for문을 돌릴 수도 있습니다.

for ((index, value) in array.withIndex()) {
    println("the element at $index is $value")
}

 

4. While Loops

while 문과 do.. while 문은 java 와 완전히 동일합니다.

아래의 예제에서 do 블록 안에 정의한 멤버변수 y 는 while 의 조건 식에서 참조할 수 있습니다.

while (x > 0) {
    x--
}

do {
    val y = retrieveData()
} while (y != null) // y is visible here!

 

여기까지 Kotline 의 if, when, for, while 제어문 사용법에 대해 간단히 알아보았습니다.

기본 문법이기 때문에 잘 익혀두면 Kotline 으로 코딩하는데 많은 도움이 될 것입니다.

'Language > Kotlin' 카테고리의 다른 글

[Kotlin] Range 사용하기  (0) 2018.01.25
[Kotlin] 함수 사용하기  (0) 2017.12.28
[Kotlin] Class 두번째 이야기  (0) 2017.12.22
[Kotlin] Class 사용하기  (0) 2017.12.15
안드로이드 공식 개발언어 Kotlin  (0) 2017.12.01

지난 "runOnUiThread(Runnable) 사용하기" 포스팅을 통하여 UI 도구 키트를 UI thread에서 동작시킬 수 있는 방법을 알아 보았습니다.

이번에는 유사한 방법인 View.post(Runnable) API를 사용하여 동일한 결과를 만들어 보도록 하겠습니다.


아래 붉은색 코드는 문제가 있는 코드입니다.

이유는 별도로 생성한 스레드에서 Button UI를 변경하려고 했기 때문입니다. 

안드로이드 시스템에서는 UI 조작 및 접근은 UI thread에서만 허용합니다. (자세한 내용은 "안드로이드 메인 스레드(Android Main Thread 또는 UI Thread"포스팅 참조)

어플리케이션을 실행하여 Download 버튼을 누르면 다운로드가 완료되는 시점에 CalledFromWrongThreadException  발생합니다.

public class MainActivity extends Activity {
    private String TAG = "AndroidThread";
    private Button mDownloadButton;
    private Button mCancelButton;
    private Download mDownload;
    private boolean isDownloading = false;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mDownloadButton = (Button)findViewById(R.id.button_download);
        mCancelButton = (Button)findViewById(R.id.button_cancel);
    }

    public void onDownload(View view) {
        Log.d(TAG, "Press Download button");
        isDownloading = true;
        mDownload = new Download();
        mDownload.start();
    }

    public void onCancel(View view) {
        Log.d(TAG, "Press Cancel button");
        isDownloading = false;
    }

    public class Download extends Thread {

        @Override
        public void run() {
            for(int i=1;i<=10;i++) {

                if (!isDownloading) {
                    break;
                }
                try {
                    Thread.sleep(1000);
                    Log.d(TAG, "Downloading ..." + i*10 + "%");

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                if(i == 10) {
                    mDownloadButton.setText("Downloaded"); // CalledFromWrongThreadException 유발 코드!!
                }
            }

            isDownloading = false;
        }
    }
}


로그는 아래와 같습니다.

안드로이드 시스템은 setText()함수를 실행하고 있는 스레드가 main thead인지 검사하였고

이를 위반 하였기 때문에 CalledFromWrongThreadException 런타임 에러를 발생시켰습니다.

12-06 15:31:12.849 18227-18227/com.example.codetravel.androidthread D/AndroidThread: Press Download button

12-06 15:31:13.863 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...10%

12-06 15:31:14.864 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...20%

12-06 15:31:15.865 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...30%

12-06 15:31:16.866 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...40%

12-06 15:31:17.869 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...50%

12-06 15:31:18.871 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...60%

12-06 15:31:19.873 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...70%

12-06 15:31:20.876 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...80%

12-06 15:31:21.879 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...90%

12-06 15:31:22.883 18227-18307/com.example.codetravel.androidthread D/AndroidThread: Downloading ...100%

12-06 15:31:22.883 18227-18307/com.example.codetravel.androidthread E/AndroidRuntime: FATAL EXCEPTION: Thread-204

                               Process: com.example.codetravel.androidthread, PID: 18227

                               android.view.ViewRootImpl$CalledFromWrongThreadException: Only the original thread that created a view hierarchy can touch its views.

                               at android.view.ViewRootImpl.checkThread(ViewRootImpl.java:6556)

                               at android.view.ViewRootImpl.invalidateChildInParent(ViewRootImpl.java:942)

                               at android.view.ViewGroup.invalidateChild(ViewGroup.java:5081)

                               at android.view.View.invalidateInternal(View.java:12713)

                               at android.view.View.invalidate(View.java:12677)

                               at android.view.View.invalidate(View.java:12661)

                               at android.widget.TextView.checkForRelayout(TextView.java:7159)

                               at android.widget.TextView.setText(TextView.java:4342)

                               at android.widget.TextView.setText(TextView.java:4199)

                               at android.widget.TextView.setText(TextView.java:4174)

                               at com.example.codetravel.androidthread.MainActivity$Download.run(MainActivity.java:53)  


View.post(Runnable) API를 사용해 보도록 하겠습니다.

    public class MainActivity extends Activity {
    private String TAG = "AndroidThread";
    private Button mDownloadButton;
    private Button mCancelButton;
    private Download mDownload;
    private boolean isDownloading = false;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mDownloadButton = (Button)findViewById(R.id.button_download);
        mCancelButton = (Button)findViewById(R.id.button_cancel);
    }

    public void onDownload(View view) {
        Log.d(TAG, "Press Download button");
        isDownloading = true;
        mDownload = new Download();
        mDownload.start();
    }

    public void onCancel(View view) {
        Log.d(TAG, "Press Cancel button");
        isDownloading = false;
    }

    public class Download extends Thread {

        @Override
        public void run() {
            for(int i=1;i<=10;i++) {

                if (!isDownloading) {
                    break;
                }
                try {
                    Thread.sleep(1000);
                    Log.d(TAG, "Downloading ..." + i*10 + "%");

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                if(i == 10) {
                        mDownloadButton.post(new Runnable() {

                        @Override
                        public void run() {
                            Log.d(TAG, "Change Button text");
                            mDownloadButton.setText("Downloaded");
                        }
                    });
               }
            }

            isDownloading = false;
        }
    }
}


실행 로그를 보겠습니다.

"Downloading ... %" 로그는 별도로 생성한 Download 스레드에서(TID 2886)에서 실행되고 있습니다.

다운로드가 100% 완료된 시점에 실행된 mDownloadButton.post(...) 의 인자로 전달한 Runnable action 객체의 run()함수 코드가 UI thread에서(TID 2436) 실행된 것을 확인할 수 있습니다.

12-26 13:29:28.433 2436-2436/com.example.codetravel.androidthread D/AndroidThread: Press Download button

12-26 13:29:29.448 2436-2886/com.example.codetravel.androidthread D/AndroidThread: Downloading ...10%

12-26 13:29:30.450 2436-2886/com.example.codetravel.androidthread D/AndroidThread: Downloading ...20%

12-26 13:29:31.453 2436-2886/com.example.codetravel.androidthread D/AndroidThread: Downloading ...30%

12-26 13:29:32.459 2436-2886/com.example.codetravel.androidthread D/AndroidThread: Downloading ...40%

12-26 13:29:33.463 2436-2886/com.example.codetravel.androidthread D/AndroidThread: Downloading ...50%

12-26 13:29:34.465 2436-2886/com.example.codetravel.androidthread D/AndroidThread: Downloading ...60%

12-26 13:29:35.467 2436-2886/com.example.codetravel.androidthread D/AndroidThread: Downloading ...70%

12-26 13:29:36.472 2436-2886/com.example.codetravel.androidthread D/AndroidThread: Downloading ...80%

12-26 13:29:37.473 2436-2886/com.example.codetravel.androidthread D/AndroidThread: Downloading ...90%

12-26 13:29:38.474 2436-2886/com.example.codetravel.androidthread D/AndroidThread: Downloading ...100%

12-26 13:29:38.476 2436-2436/com.example.codetravel.androidthread D/AndroidThread: Change Button text 



여기서 부터는 View.post(Runnable) API를  사용하면 어떻게 UI thread에서 Runnable 코드가 실행되는지 알아 보도록 하겠습니다.

Button 클래스는 TextView를 상속받고 있습니다. TextView는 View 클래스를 상속 받고 있구요.

post() 함수는 View 클래스의 멤버 함수입니다. 

친절하게 주석을 통하여 설명되어 있습니다.

"Runnable 작업은  메세지큐에 추가될 것입니다. 그 작업은 UI thread에서 수행될 것입니다."

주석 내용에 근거하여 Runnable action을 attachInfo.mHandler 하여 post하면 이것이 UI thread의 Looper가 관리하는 Queue에 전달될 것임을 짐작할 수 있습니다.

이 짐작이 맞는지 코드를 따라가 보겠습니다.

먼저 위 예제 코드에서 Button 객체는 아래와 같이 Layout xml로 정의하였습니다.

Layout xml에는 화면 배치 정보가 들어 있는 파일입니다.

어플리케이션이 실행되는 시점에 Layout xml 파일은 로딩되어 각 배치 정보들이 메모리에서 객체화 됩니다.

이렇게 메모리에서 배치 정보들이 객체화 되는 과정을 Inflation이라고 합니다.

명시적으로 "new Button()" 코드를 사용하지 않아도 Button은 생성됩니다.


Button 클래스의 상속관계를 살펴 보았기 때문에 Button 객체 생성 시점에 View 클래스의 생성자가 가장 먼저 호출 될것입니다.

View 클래스의 post함수에서 사용하고 있는 attachInfo.mHandler 값이 어디서 채워지는지 보겠습니다. 


안드로이드 시스템에서 어플리케이션이 실행될때  MainActivity와 MainActivity에 배치될 Button과 같은 View 들이 생성됩니다.

이 과정은 ActivityThread에서 시작됩니다.

ActivityThread는 ZygoteInit에 의해서 실행되면서 아래와 같이 main함수에서 UI thread looper를 생성합니다.


 // frameworks/base/core/java/android/app/ActivityThread.java


public final class ActivityThread {

...

public static void main(String[] args) { 

...

Looper.prepareMainLooper(); // 1. UI thread의 루퍼를 생성 ActivityThread thread = new ActivityThread(); thread.attach();if(sMainThreadHandler == null) {     sMainThreadHandler = thread.getHandler(); // 2. 핸들러 설정 } Looper.loop(); // 3. 루퍼 동작 시작

}

}



아래 콜스택은 MainActivity의 onCreate 함수가 호출되기까지의 콜스택입니다.

"main@4450" prio=5 tid=0x1 nid=NA runnable   // main thread or ui thread
  java.lang.Thread.State: RUNNABLE
      at com.example.codetravel.androidthread.MainActivity.onCreate(MainActivity.java:17)
      at android.app.Activity.performCreate(Activity.java:6757)
      at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1119)
      at android.app.ActivityThread.performLaunchActivity(ActivityThread.java:2703)
      at android.app.ActivityThread.handleLaunchActivity(ActivityThread.java:2811)
      at android.app.ActivityThread.-wrap12(ActivityThread.java:-1)
      at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1528)
      at android.os.Handler.dispatchMessage(Handler.java:102)
      at android.os.Looper.loop(Looper.java:154)
      at android.app.ActivityThread.main(ActivityThread.java:6316)
      at java.lang.reflect.Method.invoke(Method.java:-1)
      at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:872)

      at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:762)  

위의 콜스택에서 파란색으로 표시된 handleLaunchActivity를 잠시 따라가 보겠습니다.


// frameworks/base/core/java/android/app/ActivityThread.java

 public void handleMessage(Message msg) {

    switch(msg.what) {

        case LAUNCH_ACTIVITY: {

           ...

            handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");

            ...

        }

    }

}


private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {

    ...

    Activity a = performLaunchActivity(r, customIntent);

    

    if (a != null) {

        ...

        handleResumeActivity(r.token, false, r.isForward, !r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);

        ...

    }

}


final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {

    ...

    ViewManager wm = a.getWindowManager();

    ...

    wm.addView(decor, l);

}


//frameworks/base/core/java/android/view/WindowManagerImpl.java

public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) { 

    applyDefaultToken(params);

    mGlobal.addView(view, params, mContext.getDisplay(), mParentWindow);

}


//frameworks/base/core/java/android/view/WindowManagerGlobal.java

public void addView(View view, ViewGroup.LayoutParams params, Display display, Window parentWindow) {

    ...

    root = new ViewRootImpl(view.getContext(), display);  // ActivityThread에서 Main Looper를 생성했던 스레드에서 ViewRootImpl 객체를 생성

    ...

}

위의 함수 호출과정을 통해 ActivityThread의 main 함수에서 시작된 스레드가 ViewRootImpl객체를 생성했다는 것을 알 수 있습니다.

바로 이 스레드가 앞서 보았듯이 Main Looper를 생성한 UI 스레드입니다.


다시 ViewRootImpl 객체 생성과정을 계속 따라 가겠습니다.

ViewRootImpl 객체가 생성되면서 ViewRootHandler를 생성하여 View.mAttachInfo 객체 생성시에 사용합니다.

// frameworks/base/core/java/android/view/ViewRootImpl.java

public ViewRootImpl(Context context, Display display) {   // 생성자

    ...

    // 나중에 View 객체에 넘겨줄 mAttachInfo 객체를 생성합니다

    mAttachInfo = new View.AttachInfo(mWindowSession, mWindow, display, this, mHandler, this, context)  

}


final ViewRootHandler mHandler = new ViewRootHandler()   // View.AttachInfo 생성자로 넘겨지는 mHandler


final class ViewRootHandler extends Handler {   // ViewRootHandler는 handler를 상속한 클래스입니다

    ...

}

 

//frameworks/base/core/java/android/view/View.java

AttachInfo(IWindowSession session, IWindow window, Display display, ViewRootImpl viewRootImpl, 

                                                                 Handler handler, Callbacks effectPlayer, Context context) { 

    mSession = session;

    mWindow = window;

    mWindowToken = window.asBinder();

    mDisplay = display; 

    mViewRootImpl = viewRootImpl;

    mHandler = handler;      

    mRootCallbacks = effectPlayer;       

    mTreeObserver = new ViewTreeObserver(context);

}


여기까지 코드를 간단히 보았습니다. 이제 View 클래스의 post 함수에서 본 attachInfo.mHandler 값이 어디서 채워지는지 확인 할 단계입니다.

 // frameworks/base/core/java/android/view/ViewRootImpl.java

private void performTraversals() {

    // cache mView since it is used so much below...

    final View host = mView; 

    ...

    host.dispatchAttachedToWindow(mAttachInfo, 0);  // mAttachInfo  = new View.AttachInfo( ..., this, mHandler, this, context) 

}


//frameworks/base/core/java/android/view/View.java

void dispatchAttachedToWindow(AttachInfo info, int visibility) { 

    mAttachInfo = info;   // info는 ViewRootImpl 생성자에서 생성한 View.AttachInfo 객체입니다

    ...

}

결과적으로 attachInfo.mHandler.post(action) 코드는 아래와같은 과정을 거칩니다.

action은 ViewRootImpl 객체에서 생성한 핸들러를 통해서 메세지 Queue에 삽입됩니다.

이 Queue는 ActivityThread가 생성한 Main Looper를 통해서 처리됩니다.

참고로 하나의 스레드에는 여러개의 Handler가 동작할 수 있습니다. 

그리고 하나의 스레드는 단 하나의 Looper만 가질수 있고 Looper는 하나의 Queue와 연결되어 있습니다.


요약하면 post 함수는 action을 ViewRootImpl객체가 생성한 Handler로 전달하고 이 Handler에 의해서 ActivityThread가 생성한 Main Looper에 연결된  Queue에 삽입됩니다.

그리고 Main Looper는 이 Queue에서 action을 꺼내서 UI thread상에서 코드를 실행하게 됩니다. 

public boolean post(Runnable action) {

    final AttachInfo attachInfo = mAttachInfo;

    if(attachInfo != null) {

        return attachInfo.mHandler.post(action);

    }

    // Postpone the runnable until we know on which thread it needs to run.

    // Assume that the runnable will be successfully placed after attach.

    getRunQueue().post(action);

    return true;


지금까지 View.post() 함수의 사용법과 동작 원리를 간단하게 알아보았습니다.






Kotlin 에서의 함수는 fun 키워드를 사용합니다.

fun double(x: Int):Int {
    return 2 * x
}

함수는 일반적으로 아래와 같이 부를 수 있습니다.

 

val result = double(2)

.(dot) 을 사용하여 멤버 함수를 부를 수도 있습니다.

 

Math().double()

 

Default Arguments

Parameter 의 전달은 name: type 과 같이 사용하며, default value 를 가질 수 있습니다. 인자가 전달되지 않으면 default value 가 값에 사용되며 불필요한 함수 오버로딩을 막을 수 있습니다.

checkCustomer("Gildong") 와 같이 필요한 인자만 사용하여 함수 호출이 가능합니다.

fun checkCustomer(name: String, phone: String = "NA", age: Int = 30){
    ...
}

checkCustomer("Gildong") // The default value phone = "NA" age = 30 is used

오버라이딩은 상위 메소드와 동일한 인자를 가지게 되므로, default value 를 사용하지 않을 때는 오버라이딩을 하여 default value 를 선언하지 않아야 합니다.

open class A {   open fun foo(i: Int = 10) {...}
}

class B: A(){
   override fun foo(i: Int) {...}   // no default value allowed
}

 

 

Name Arguments

함수를 호출할 때 parameter 의 이름을 사용하는데, 이를 Name Argument 라고 부릅니다.

fun reformat(str: String,
             normalizeCase: Boolean = true,
             upperCaseFirstLetter: Boolean = true,
             divideByCamelHumps: Boolean = false,
             wordSeparator: Char ' ') {
...
}

위와 같은 함수가 있을때 아래와 같이 호출 할 수 있습니다.(위에서 default value 에 대해 설명한 것을 참고하시면 됩니다.)

reformat(str)

default value 가 없다면 reformat(str, true, true, false, ' ') 라고 불러야 할 것입니다. Name Argument 는 좀더 읽기 쉽게 아래와 같이 표현할 수 있도록 해줍니다.

reformat(str,
        normalizeCase = true,
        upperCaseFirstLetter = true,
        divideByCamelHumps = false,
        wordSeparator = '_')

모든 parameter 를 전달하지 않고 아래와 같이 사용가능합니다.

reformat(str, wordSeparator = '_')

단, str 인자가 positional argument 라 부르는데, positional argument는 항상 Name argument 보다 앞에 있어야 합니다.

즉, f(1, y = 2)는 가능하지만 f(x = 1, 2) 는 불가능합니다.

 

Unit-returning Functions

함수에서 return 값이 불필요하다면 return type을 Unit 으로 선언할 수 있습니다. Unit 은 명시적으로 반환할 필요가 없습니다.

return Unit 이나 return 은 선택적으로 할 수 있습니다. 또한 Unit 은 생략 가능합니다.(반환값을 사용하지 않는다면 Unit 이 생략되어 있는것과 같습니다.)

fun printHello(name: String?): Unit {
    if (name != null)
        println("Hello ${name}")
    else
        println("Hi there")
}

 

Single-Expression functions

함수가 한줄로 표현 가능하다면 괄호는 생략 가능합니다. 또한 반환값도 컴파일러에 의해 유추될 수 있다면 생략 가능합니다.

fun double(x: Int): Int = x * 2

fun double(x: Int) = x * 2

 

Variable number of arguments(Varargs)

함수의 매개 변수는 vararg modifier로 표시될 수 있습니다.

fun <T> asList(vararg ts: T): List<T> {
    val result = ArrayList<T>()
    for(t in ts) // ts is an Array
        result.add(t)
    return result
}

val list = asList(1, 2, 3)

asList 함수에서 T형의 vararg 매개 변수가 T의 배열로 표시됩니다. 하나의 매개 변수만 vararg로 표시 될 수 있습니다.

 

 

Android 에서 음성을 녹음할 때 사용할 수 있는 API 는 여러가지가 있지만, 지난 포스팅에서 가장 대표적으로 사용되는 AudioRecord API 를 이용한 예제를 다루어 보았습니다.

이번 포스팅에서는 MediaRecorder API 를 이용한 음성 녹음 예제를 다루어 보겠습니다.

AudioRecord 를 이용한 예제처럼 간단하게 완성할 수 있습니다.

 

1. main layout 구성하기

layout 은 간단히 버튼을 두개 놓고, 하나는 녹음 시작/정지, 다른 하나는 녹음된 컨텐츠를 재생/정지 하도록 구현해 보겠습니다.

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="com.codetravel.mediarecorder.MainActivity">

<Button
android:id="@+id/bt_record"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start Recording"/>

<Button
android:id="@+id/bt_play"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="Start Playing"/>
</LinearLayout>

이를 위해 bt_record 와 bt_play 를 각각 layout 에 추가 하고, MainActivity 에서 각각의 버튼에 대한 OnClickListener() 를 구현 해 줍니다.

OnClickListener() 에서는 현재 레코딩 상태에 따라서 recorder 를 Start 하거나 Stop 하는 부분이 구현될 것입니다.

 

2. MediaRecorder 생성 후 레코딩 준비하기

recorder 를 start 하기에 앞서, MediaRecorder 객체를 하나 생성 하고 레코딩 할 파일의 포맷, 인코더 등등을 설정 해 줍니다.

    @Override                                                     
    protected void onCreate(Bundle savedInstanceState) {          
        super.onCreate(savedInstanceState);                       
        setContentView(R.layout.activity_main);    

        mRecorder = new MediaRecorder();                          
    }    

    // 레코더 기본 설정 
    void initAudioRecorder() {                                                                
        mRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);                              
        mRecorder.setOutputFormat(MediaRecorder.OutputFormat.AAC_ADTS);                       
        mRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AAC);                            
                                                                                              
        mPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/record.aac";  
        Log.d(TAG, "file path is " + mPath);                                                  
        mRecorder.setOutputFile(mPath);                                                       
        try {                                                                                 
            mRecorder.prepare();                                                              
        } catch (Exception e) {                                                               
            e.printStackTrace();                                                              
        }                                                                                     
    }                                                                                                                                                 

initAudioRecorder() 함수 안에다가 MediaRecorder prepare 단계 까지 모든 설정들을 구현 합니다.

MediaRecorder 의 State Diagram 을 참고하면, 한번 MediaRecorder 를 Stop 할 시에, 각종 설정들을 다시 해 주어야 하기 때문에 함수로 분리하여 구현하는 것이 추후 더 편리합니다.

 

3. 녹음 시작/ 정지

isRecording 이라는 flag 를 두어 현재 녹음 중인지 아닌지를 구분 할 수 있습니다.

현재 녹음중이 아니면 initAudioRecorder(), start() 를 수행 하여 녹음을 시작하고, 녹음중 일 경우에는 stop() 을 호출하여 녹음을 정지합니다.

isRecording flag 와 button 의 text 는 현재 상태에 맞게 update 합니다.

    boolean isRecording = false;                                                                      
    Button mBtRecord = null;                                       
                                                                  
    @Override                                                     
    protected void onCreate(Bundle savedInstanceState) {          
        ..
        mBtRecord = (Button) findViewById(R.id.bt_record);        
        mBtRecord.setOnClickListener(new View.OnClickListener() { 
            @Override                                             
            public void onClick(View v) {                         
                if (isRecording == false) {                       
                    initAudioRecorder();                          
                    mRecorder.start();                            
                                                                  
                    isRecording = true;                           
                    mBtRecord.setText("Stop Recording");          
                } else {                                          
                    mRecorder.stop();
                             
                    isRecording = false;                          
                    mBtRecord.setText("Start Recording");         
                }                                                 
            }                                                     
        });                                                       

 

4. 재생 시작하기

재생 버튼에 대한 onClickListener 를 생성하고 재생을 위한 MediaPlayer 객체를 하나 생성합니다.

버튼이 한번 클릭 되었을 때, setDataSource/prepare/start 과정을 진행 해줍니다.

'MediaPlayer' 도 MediaRecorder 와 같이 State Diagram 을 참고하여 각 State 에 맞는 함수를 호출해 주어야 합니다.

한번 Stop 을 호출 하게 되면 stopped 상태가 되어 다시 started 상태로 가기 위해서는 prepare/start 함수를 순차적으로 호출해야 합니다.

=> 만약 현재 상태에서 부를 수 없는 함수를 호출하는 경우 ( 예를들어 stopped 상태에서 prepare를 호출하지 않고 start 부터 호출하는 경우)에는 IllegalStateException 이 발생하니 주의 해야 합니다.

 
    MediaPlayer mPlayer = null;
    boolean isPlaying = false;
    Button mBtPlay = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        ..
        mBtPlay = (Button) findViewById(R.id.bt_play);
        mBtPlay.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (isPlaying == false) {
                    try {
                        mPlayer.setDataSource(mPath);
                        mPlayer.prepare();
                    }catch (Exception e) {
                        e.printStackTrace();
                    }
                    mPlayer.start();

                    isPlaying = true;
                    mBtPlay.setText("Stop Playing");
                }
                else {
                    mPlayer.stop();

                    isPlaying = false;
                    mBtPlay.setText("Start Playing");
                }
            }
        });

        mPlayer = new MediaPlayer();
        mPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
            @Override
            public void onCompletion(MediaPlayer mp) {
                isPlaying = false;
                mBtPlay.setText("Start Playing");
            }
        });
    }

MediaPlayer는 onCompletion callback method 를 제공해 주고 있어서, 파일 재생이 끝날 경우에 대한 처리를 편리하게 해 줄 수 있습니다.

파일 재생이 완료되면 isPlaying flag 를 false 로 설정 하고, 버튼의 Text 를 변경해 주는 일을 수행하도록 구현 하였습니다.

 

여기까지 진행하면 음성을 녹음하고 재생하는 코드가 완성 됩니다. 상세 코드는 'github 코드' 를 참고하세요.

현재는 음성 녹음 뿐이지만 다음 포스팅에서는 카메라로 들어오는 Video 컨텐츠도 함께 레코딩 하여 비디오 파일을 만들어 보도록 하겠습니다.

 

+ Recent posts