기존에는 그냥 연습용 앱이어서 해상도 별로 이미지를 생각하지 않고 drawable 에 다 때려 넣었다.

그러다보니 여러명에서 같이 하는 앱개발의 경우에, 각기 다른 Android Device 를 가지고 있어서, OutOfMemory  error 가 나는 경우가 많았다.

그래서 찾아봤는데 아래 사이트에서 각 해상도별 이미지를 생성 해준다.

https://romannurik.github.io/AndroidAssetStudio/nine-patches.html#&sourceDensity=320&name=example

 

Android Asset Studio - Simple nine-patch generator

Drag or select a source graphic to get started.

romannurik.github.io

 

생성된 이미지를 아래와 같이 적용해주기만 하면 된다.

splash_logo.png

 

그리고 불러다 쓸때는 기존에 @drawble/splash_logo 가 아닌 @mipmap/splash_logo 라고 해주면, 알아서 돌아가는 device 에 맞게 이미지를 로드 해준다. 

<ImageView
    android:id="@+id/splash_img"
    android:layout_width="0dp"
    android:layout_height="290dp"
    android:scaleType="fitCenter"
    android:contentDescription="@string/contentDescription_img"
    app:layout_constraintWidth_percent="0.7"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintHorizontal_bias="0.5"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintVertical_bias="0.276"
    app:srcCompat="@mipmap/splash_logo" />

 

굿굿 

오늘은 근거리 무선 통신기술인 NFC(Near Field Communication) 에 대해 알아보겠습니다.

NFC 는 13.56MHz 주파수 대역을 사용해 데이터를 주고 받는 근거리 무선 통신기술 중의 한 종류입니다.

이렇게 이야기하면 어려운 개념 같지만, 실제로 우리는 스마트폰에서 NFC 기술을 자주 접하고 있습니다.

NFC 를 이용해서 스마트폰끼리 간단한 Text 나 URL 정보등을 교환할 수 있고, NFC 태그에 스마트폰을 접촉하면 저장되어 있는 정보들을 읽어들일 수 있습니다.

그래서 요즘에는 명함에도 NFC 기능이 탑재되어, 간단히 태그만 하더라도 스마트폰으로 그 명함에 적혀있는 이름이나 핸드폰 번호를 간단하게 읽어들일 수 있습니다.

무선통신 종류 별로 Speed 와 Range 를 살펴보면, NFC 의 경우 10cm 이하의 거리에서 굉장히 느린 속도로 데이터를 주고 받을 수 있는 프로토콜이라고 볼 수 있습니다.

그렇기 때문에 간단한 Text 만을 전송하는데 주로 사용되는데,  예를들어 카드번호/전화번호/특정 URL Link 등을 전송하는데 주로 사용 됩니다.

그에 대비해서 Bluetooth 의 경우는 30m 이하. 즉, NFC 보다는 비교적 먼 거리에서의 통신이 가능 합니다.

데이터 전송 속도도 느린 편에 속하고 있지만, NFC 보다는 2배 가까이 빠른 속도를 가집니다.  저화질의 사진 정도는 전송할 수 있는 수준입니다.

우리가 비교적 많이 사용하고 있는 Wi-Fi 의 경우 최대 100Mbs 정도의 속도로 Multimedia 컨텐츠도 전송 할 수 있는 수준입니다.  위 표에 표기된 무선통신 프로토콜 중에는 WiFi 의 속도가 가장 빠른 것으로 확인 되네요.

 

NFC 는 굉장히 다양한 분야에 활용되고 있습니다. 제일 많이 사용되는 분야가 '결제' 시스템인데,

스마트폰에 기본으로 탑재되어있는 NFC 기능을 이용하여 교통카드/신용카드/각종 페이 간편결제 등으로 활용할 수 있습니다.

 

LG전자에서 2013년에 출시한 '트롬' 세탁기에도 NFC 기능이 탑재되었는데,

예를들어 사용자가 스마트폰 앱에서 원하는 세탁 코스를 선택한 후 스마트폰을 세탁기의 NFC 태그에 접촉 하게 되면

즉시 해당 코스가 세탁기에 저장되어 제품에 탑재된 기본 12가지 세탁코스 외에도 다양한 세탁코스를 사용할 수 있습니다.


삼성전자에서 나온 프린터에도 NFC 기능이 탑재되어

별도로 컴퓨터를 킬 필요 없이, 스마트폰에 저장된 문서를 프린터에 부탁된 NFC 태그에 터치만 해도 바로 프린트가 된다고 합니다.


최근 평창동계올림픽에서도 NFC 방식의 결제 시스템이 도입되었는데,

스티커나 배지, 장갑의 형태로 카드나 지갑 없이도 간단히 몸에 부착해 결제가 가능한 것이 특징입니다.

스티커의 경우 3,5,10,20 만원이 충전된 형태로, 배지와 장갑은 3,5 만원이 충전된 형태로 구매할 수 있었으며,

간단한 Tag 만으로 결제가 가능 하도록 NFC 기능이 도입되었습니다.


NFC 스티커는 가격도 저렴하여 400원 정도로 인터넷에서 쉽게 구매할 수 있기 때문에

여러 기능들을 실현해 보기 쉬운 방법 중 하나인 것 같습니다.

 

다음 포스팅에서는 실제 Android 에서 NFC Tag 를 Read 하고 Write 하는 방법에 대해 알아보도록 하겠습니다.

 

앞선 포스팅에서는 Internal/External 저장소에 사용자의 데이터를 저장하는 방법에 대해 살펴 보았습니다.

오늘은 데이터베이스를 활용하여 원하는 데이터를 저장하는 방법에 대해 알아보겠습니다.

Intenrnal/External 저장소에 저장하는 것 보다 데이터베이스를 이용하여 저장할 때는, 동일한 형태의 데이터를 저장할 수 있다는 점에서 더 편리합니다.

회원정보를 저장할 경우가 대표적인 예인데, 이 경우 회원의 이름, 성별, 전화번호, 주소 등등이 각각 DB의 속성(Attribute) 가 될 것이고,

실제 들어가는 데이터 (김태희, 여, 010-0000-0000, 경기도.. ) 들이 값(Value) 이 될 것입니다.

 

1. SQLiteOpenHelper 를 이용한 DB 생성

SQLiteOpenHelper Class 는 Database 를 생성하고 해당 Database 의 Version 을 관리합니다.

해당 Class 를 사용하기 위해서 Database가 처음 생성될 때 불리는 onCreate(..) 와 Database 가 Upgrade 될 경우에 불리는 onUpgrade(..) callback method 를 구현해 주어야 하며, 필요에 따라 onOpen(..) 함수와 onDowngrade(..) 함수도 구현하여 사용할 수 있습니다.

저는 SQLiteOpenHelper를 상속 받은 MySQLiteOpenHelper 를 생성해서 간단하게 onCreate/onUpgrade 만 구현해 주었습니다.

 

public class MySQLiteOpenHelper extends SQLiteOpenHelper {

    private final String TAG = "MySQLiteOpenHelper";

    public MySQLiteOpenHelper(Context context, String name, SQLiteDatabase.CursorFactory factory, int version) {
        super(context, name, factory, version);
    }

    @Override
    public void onCreate(SQLiteDatabase sqLiteDatabase) {
        String sql = "create table student (_id integer primary key autoincrement, name text, age integer, address text)";
        sqLiteDatabase.execSQL(sql);
    }

    @Override
    public void onUpgrade(SQLiteDatabase sqLiteDatabase, int oldVersion, int newVersion) {
        String sql="drop table if exists student";
        sqLiteDatabase.execSQL(sql);
        onCreate(sqLiteDatabase);
    }
}

 

onCreate 에서는 Database를 생성해 주었고, 각 Field 는 name, age, address 로 구성될 수 있도록 하였습니다.

"create table student (_id integer primary key autoincrement, name text, age integer, address text)";

위 Query 로 DB생성 후 실제 만들어진 DB 를 열어보면 다음과 같이 data 가 저장됩니다.

 

그리고 onUpgrade 함수에는 현재 생성되어있는 Database 를 지우고 onCreate 를 통해 다시 생성될 수 있도록만 구현을 해 두었습니다.

 

2. DB에 데이터 삽입 및 삭제

DB에 데이터 삽입/삭제를 위해 MyDBHandler 클래스를 생성하였습니다.

MyDBHandler 클래스에서는 DBActivity 로 부터 사용자 정보를 받아 MySQLiteOpenHelper 로 전달 합니다.

데이터를 저장 할 때 ContentValues 를 이용했는데, ContentValues 는 Data 를 Key와 Value의 Set 으로 저장할 수 있습니다.

그래서 Database 에 값을 줄때 "name, 홍길동" ,"age, 24" 등과 같이 Key 와 Value 의 형태로 쉽게 전달 가능합니다.

 

public class MyDBHandler {

    private final String TAG = "MyDBHandler";

    SQLiteOpenHelper mHelper = null;
    SQLiteDatabase mDB = null;

    public MyDBHandler(Context context, String name) {
        mHelper = new MySQLiteOpenHelper(context, name, null, 1);
    }

    public static MyDBHandler open(Context context, String name) {
        return new MyDBHandler(context, name);
    }

    public Cursor select()
    {
        mDB = mHelper.getReadableDatabase();
        Cursor c = mDB.query("student", null, null, null, null, null, null);
        return c;
    }

    public void insert(String name, int age, String address) {

        Log.d(TAG, "insert");

        mDB = mHelper.getWritableDatabase();

        ContentValues value = new ContentValues();
        value.put("name", name);
        value.put("age", age);
        value.put("address", address);

        mDB.insert("student", null, value);

    }

    public void delete(String name)
    {
        Log.d(TAG, "delete");
        mDB = mHelper.getWritableDatabase();
        mDB.delete("student", "name=?", new String[]{name});
    }

    public void close() {
        mHelper.close();
    }
}

select 함수는 Database 의 전체 내용을 return 해주고, insert/delete 함수는 각각 Database에 데이터를 추가/삭제 시 호출 됩니다.

delete 의 경우 name 을 인자로 받아서 이름이 동일 하면 해당 row 를 모두 삭제하도록 하였습니다.

 

3. 동작 확인

기존에 작성하였던 MainActivity 에 Database 버튼을 하나 추가한다음에, 이 버튼을 눌렀을 때 Activity 를 전환하여 Database 의 내용을 뿌려주도록 하였습니다.

가장 상단에는 Database 에 내용을 추가할 수 있도록 Editbox 를 3개 두었고, 각각 이름/나이/주소 정보를 저장할 수 있도록 하였습니다.

Update Database 를 누르게 되면 Editbox 에 추가한 내용이 Database 에 업데이트 되고 리스트의 내용이 업데이트 됩니다.

리스트의 각 항목을 롱클릭 하게 되면 해당 Field 가 삭제 됩니다. ^^

    

여기까지 Database 를 이용한 아주 간단한 사용자 데이터 저장에 대해서 살펴보았습니다.

모든 소스는 https://github.com/bettercho/MyGithub/tree/master/storeuserdata 를 참고하시면 됩니다. 

 

오늘은 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 를 참고하세요.

 

 

 

 

지난 포스팅에서 '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

Android 에서 제공하는 Recording API 중, 지난번에 보았던 AudioRecorder 말고 더 편리한 MediaRecorder 가 있습니다.

AudioRecorder 는 오디오만 레코딩 가능 하지만, MediaRecorder 의 경우 Audio 및 Video 컨텐츠의 레코딩이 가능 합니다.

다만 인코딩 된 파일을 받기 때문에 AudioRecorder 처럼 PCM Data 를 바로 받아올 수는 없는 단점이 있습니다.

본인이 구현하려는 레코더의 사용 용도에 맞게 AudioRecorder/MediaRecorder 를 선택하여 사용하시면 되겠습니다.

 

Android Developer 의 MediaRecorder API Guide 를 참고하면, MediaRecorder 는 아래와 같은 state machine 을 갖습니다.

따라서 MediaRecorder API 를 사용할 때는 아래의 State 를 잘 따라서 코딩해야 합니다. 예를들어 Initail 상태에서는 바로 Prepared 상태로 갈 수 없고, Initailized 와 DataConfigured 상태를 거쳐야지 Prepared 상태가 될 수 있습니다.

이를 어기게 되면 StateIllegalException 이 발생하고, 원하는 동작을 얻을 수 없으니 조심해야 합니다.

 

 

Android Developer 사이트에서 설명하는 일반적인 Recording Flow 는 아래 코드와 같습니다.

각 단계별로 실제 레코딩시 일어나는 동작들에 대해 살펴보도록 하겠습니다.

MediaRecorder recorder = new MediaRecorder();
 recorder
.setAudioSource(MediaRecorder.AudioSource.MIC); //----------- (1)
 recorder
.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); // -- (2)
 recorder
.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); // ------(3)
 recorder
.setOutputFile(PATH_NAME);
 recorder
.prepare(); // -----------------------------------------------(4)
 recorder
.start();   // Recording is now started ----------------------(5)
 
...
 recorder
.stop();
 recorder
.reset();   // You can reuse the object by going back to setAudioSource() step
 recorder
.release(); // Now the object cannot be reused

 

(1) AudioSource 설정

AudioSource 의 종류는 아래 표에 정리되어 있습니다.

DEFAULT 

 0

 Default audio source

MIC 

 1

 Microphone audio source

VOICE_UPLINK  2  Voice call uplink (TX) audio source

VOICE_DOWNLINK 

 3

 Voice call downlink (RX) audio source 

VOICE_CALL

 4

 Voice call uplink + downlink audio source

CAMCORDER   5

 Microphone audio source tuned for video recording. wit the same orientation as the camera if available

VOICE_RECOGNITION

 6  Microphone audio source tuned for voice recognition

VOICE_COMMUNICATION

 7

 Microphone audio source tuned for voice communications such as VoIP

REMOTE_SUBMIX 

 8

 Audio source for a submix for audio streams to be presented remotely. An application can use this audio source to capture a mix of audio streams that should be transmitted to a remote receiver such as a Wifi display

UNPROCESSED

 9

 Microphone audio source tuned for unprocessed sound if available.

RADIO_TUNER

 1998

 Audio source for capturing broadcast radio tuner output

HOTWORD

 1999

 Audio source for preemptible, low-priority software hotword detection

보통은 마이크로 들어오는 음성을 많이 녹음하기 때문에 MIC 값을 많이 사용하고, 용도에 맞게 그 외의 값들을 설정할 수 있습니다.

제일 아래 RADIO_TUNER 와 HOTWORD 는 System API 로 일반 Application 에서는 사용 불가 합니다.

setAudioSource 함수가 호출되지 않을 경우에는 output file 에 audio track 이 포함되지 않습니다. 즉, 오디오가 녹음되지 않는다는 이야기입니다.

오디오를 녹음하고 싶으면 setAudioSource 함수를 prepare 전에 꼭 호출해 주어야 합니다.

 

(2) Output Format 설정

레코딩 후 실제 파일로 저장할 때, 저장할 파일의 format 을 설정해 줄 수 있습니다. 

DEFAULT

 0

THREE_GPP

 1

MPEG_4

 2

AMR_NB

 3

AMR_WB

 4

AAC_ADIF

 5

AAC_ADTS

 6

OUTPUT_FORMAT_RTP_AVP

 7

MPEG_2_TS

 8

WEBM

 9

비디오 포맷으로 가장 많이 사용되는 THREE_GPP 혹은 MPEG_4 가 있고, 오디오만 녹음 할 경우에는 오디오 전용 파일포맷인 AMR 이나 AAC 가 많이 사용 됩니다.

setOutputFormat() 함수는 setAudioSource()/setVideoSource() 호출 이후 / prepare() 이전에 호출되어야 합니다.

 

(3) AudioEncoder 설정

Audio Recording 시 Audio Encoder 를 설정해 주는 함수 입니다. 이 함수를 호출하지 않을 경우에 녹음된 파일에 audido track 은 포함되지 않습니다.

즉, 이 함수도 setAudioSource 와 동일하게 오디오를 녹음하고 싶다면 setOutputFormat() 이후 / prepare() 이전에 꼭 호출해 주어야 합니다.

 DEFAULT

 0

 AMR_NB

 1

 AMR_WB

 2

 AAC

 3

 HE_AAC

 4

 AAC_ELD

 5

 VORBIS

 6

 

(4) prepare

prepare 단계는 앞서 설정한 설정값들로 recording 을 준비하는 단계 입니다.

때문에 이 함수는 audio/video source와 audio/video encoder 를 모두 설정하고 file format 을 확정 지은 후에 불려야 합니다.

이 함수가 불리면 위의 State Machine 에서 recorder 는 prepared 상태가 되어 start 나 reset 을 호출할 수 있습니다.

 

(5) start

start 함수를 호출하면 정해놓은 audio/video source 에서 실제 레코딩을 시작하게 되고, state 는 Recording 상태가 됩니다.

start 이후 원하는 시점에 stop 을 호출하면, 그 시점까지 레코딩이 진행 됩니다.

 

이 외에도 API Level 24 부터 제공되는 pause() / resume() 함수를 이용해 레코딩을 잠시 멈추는 것이 가능합니다.

stop() 과 다르게 pause() 함수로 레코딩을 멈추면 기존의 configuration 은 유지된 채로 MediaRecorder 가 잠시 멈추게 됩니다.

이 상태에서 resume() 함수로 레코딩을 다시 시작할 수 있습니다. 당연한 이야기지만 paused 상태에서 레코딩 되는 내용은 버려지게 됩니다.

 

developer 사이트를 참고하면 위의 간단한 API 이외에도 MediaRecorder 에서 제공하는 다양한 API 들을 볼 수 있으니 참고하시면 되겠습니다.

https://developer.android.com/reference/android/media/MediaRecorder.html

 

다음 포스팅에서는 MediaRecorder 를 이용한 실제 레코딩 예제를 살펴보도록 하겠습니다.

 

 

 

앞서 안드로이드 "안드로이드 메인 스레드 포스팅"을 통하여 메인스레드의 특징에 대해서 알아보았습니다. 

그 중에서 메인 스레드가 아닌 스레드에서 Button UI를 조작하다가 CalledFromWrongThreadException를 경험하기도 하였습니다.

이런 문제를 해결할 수 있는 방법에 대해서 알아보겠습니다.


Activity.runOnUiThread(Runnable) 사용

Developer Android 사이트에서 찾아보면 다음과 같이 설명이 있습니다.

설명 중 다음 부분이 우리가 처한 상황을 설명해 주고 있습니다.

"특정 동작을 UI 스레드에서 동작하도록 합니다. 만약 현재 스레드가 UI 스레드이면 그 동작은 즉시 수행됩니다."

하지만 "현재 스레드가 UI 스레드가 아니면, 필요한 동작을 UI 스레드의 이벤트 큐로 전달한다" <== 이부분 입니다.


우선 설명을 믿고 코드를 만들어 보겠습니다. 먼저 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;
        }
    }
}

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) {

                    (MainActivity.this).runOnUiThread(new Runnable(){

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

            isDownloading = false;
        }
    }
}


다음은 runOnUiThread 함수를 사용한 코드입니다.

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) {
                       (MainActivity.this).runOnUiThread(new Runnable(){

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

            isDownloading = false;
        }
    }
}


run()함수를 오버라이드하여 Runnable 인터페이스를 구현하였습니다. 

이제 run() 함수에 있는 작업은(여기서는 setText함수 실행이겠죠) main thread(UI thread) 에서 실행 됩니다. 

로그로 확인 해보겠습니다.   

12-08 16:05:22.968 3443-3443/com.example.codetravel.androidthread D/AndroidThread: Press Download button

12-08 16:05:23.986 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...10%

12-08 16:05:24.987 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...20%

12-08 16:05:25.990 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...30%

12-08 16:05:26.994 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...40%

12-08 16:05:27.998 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...50%

12-08 16:05:29.000 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...60%

12-08 16:05:30.003 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...70%

12-08 16:05:31.006 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...80%

12-08 16:05:32.011 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...90%

12-08 16:05:33.013 3443-3467/com.example.codetravel.androidthread D/AndroidThread: Downloading ...100%

12-08 16:05:33.014 3443-3443/com.example.codetravel.androidthread D/AndroidThread: Change Button text 

다운로드 진행 상태를 출력하는 로그는 TID3467 thread에서 동작합니다. 

의도한대로 다운로드 완료 후,  setText 함수를 수행하는 run 함수가 UI thread에서 실행된 것을 확인 할 수 있습니다.

다음 그림은 다운로드 버튼의 텍스트가 정상적으로 변경된 모습입니다.



runOnUiThread 함수는 어떻게 이런 일을 할까요? xref에서 runOnUiThread 함수를 살펴 보겠습니다.

"특정 동작을 UI 스레드에서 동작하도록 합니다. 만약 현재 스레드가 UI 스레드이면 그 동작은 즉시 수행됩니다."

하지만 "현재 스레드가 UI 스레드가 아니면, 필요한 동작을 UI 스레드의 이벤트 큐로 전달합니다 설명이 그대로 코드화 되어 있네요


runOnUiThread가 실행된 스레드는 TID3467이므로 즉 UI thread가 아니므로 mHandler.post(action) 코드가 실행됩니다.

mHandler.post(action)을 실행하면 아래와 같이 순서대로 함수가 호출됩니다.(1 -4)

결과적으로 Runnable r은 UI thread의 queue에 메시지 형식으로 enqueue가 됩니다. 

후에 queue에 있는 메세지를 꺼내서 UI thread에서 동작을 수행할 것입니다.

1. post(Runnable r)

2. sendMessageDelayed(Message msg, long delayMillis)

3. sendMessageAtTime(Message msg, long uptimeMillis)

 - MessageQueue queue = mQueue;

4. enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis)


간략하게 말씀드리면 처음 startActivity() 함수를 통해서 MainActivity가 생성되는 과정은 zygoteInit에 의해서 ActivityThread가 생성됩니다.

ActivityThread는 아래와 같이 Main Looper와 Handler를 생성합니다. 

 // 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. 루퍼 동작 시작

}

}


이것이 안드로이드 시스템에서 자동으로 생성되는 UI thread의  Looper와  Handler 입니다. 

그리고 ActivityThread는 ActivityManagerService 와 바인더 통신을 하여 MainActivity를 생성하게 됩니다. 

따라서 ActivityThread가 생성한 MainActivity는 ActivityThread에서 생성한 Looper와 Handler를 사용하게 됩니다.

즉 위의 3번에서 mQueue는 UI thread의 Looper가 관리하는 queue를 가리킵니다.

결국 queue에 메세지 형태로 들어간 Runnable 작업은 UI Thread에서 dequeue 되어 UI Thread상에서 수행됩니다.

 UI thread에서 Button의 Text 변경 작업을 했기 때문에 CalledFromWrongThreadException와 같은 문제가 발생하지 않는 것입니다.


아래 콜스택은 MainActivity 액티비티의 onCreate가 불리는 함수 호출 과정입니다.

"main@4450" prio=5 tid=0x1 nid=NA runnable
  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) 


ActivityThread의 main 함수에서 실행된 Looper.loop() 함수에 의해서 메세지가 처리되고 있는것을 볼 수 있습니다.

그리고 MainActivity가 생성되면서 onCreate가 호출됩니다.

위의 과정은 붉은색으로 표시된 "main"이라는 표시로 main thread 즉 UI thread의  콜스택임을 알 수 있습니다.  

이상으로 runOnUiThread 함수의 사용에 대해서 간략하게 보았습니다.






Kotlin 에서의 Class 사용에 대해서 알아보도록 하겠습니다.

Java 와의 호환성이 Kotlin 의 큰 장점으로 부곽되는 만큼 Java 와 비교하게 되는 부분이 있습니다.

 

Kotlin 에서 Class는 Java와 같이 아래처럼 사용합니다.

class Person{ }

그러나 Java 와 다르게 Body 가 없이 아래와 같이 사용될 수도 있습니다.

class Person

 

Kotlin에서는 primary constructor 과 하나 이상의 secondary constructor 를 가질 수 있습니다.

primary constructor 는 class 선언시 함께 가능합니다.

constructor 키워드는 생략 가능합니다.

annotation 이나 접근자(private, pubilc 등) 와 함께 사용되는 경우 생략할 수 없습니다.

class Person constructor(firstName: String) {

}
class Person(firstName: String) {

}

primary constructor

primary constructor 는 어떤 코드도 포함하지 않으므로 initializer block 을 사용하여 초기화 코드를 구현하면 됩니다.

constructor 가 불리면 객체 생성이 되면서 initializer block 이 불립니다.

primary constructor 의 parameter 는 class 내 어디서든(init 블럭이나 속성값 초기화) 사용 가능합니다.

class Person(name: String) {
    val nameInfo = "Name : $name".also(::println)

    init {
        println("First initializer block that prints ${name}")
    }

    val secondProperty = "Second property: ${name.length}".also(::println)

    init {
        println("Second initializer block that prints ${name.length}")
    }
}

 

secondary constructor

constructor 키워드가 반드시 사용되어야 합니다.

class Person {
    constructor(parent: Person) {
        parent.children.add(this)
    }
}

primary constructor 가 있는 경우 모든 second constructor는 primary constructor 에게 위임해야 합니다. 동일 클래스 내에서 다른 생성자로의 위임은 this 키워드를 사용합니다.

class Person(val name: String) {
    constructor(name: String, parent: Person) : this(name) {
        parent.children.add(this)
    }
}

initializer block의 코드는 primary constructor 의 일부가 된다. primary constructor 로의 위임은 secondary constructor 코드 실행 전에 실행되므로 모든 initializer block 의 코드는 가장 먼저 실행됩니다.

class Constructors {
    init {
        println("Init block")    // 먼저 실행됨
    }

    constructor(i: Int) {
        println("Constructor")   // 나중에 실행됨
    }
}

 

class의 instance 생성하기

class의 instance 를 생성하는데 JAVA에서는 new 키워드를 사용했다면, Kotlin 에서는 constructor 호출만 하면 됩니다.

val invoice = Invoice()

val customer = Customer("Joe Smith")

 

class 상속받기

Kotlin 에서 class 를 상속받기 위해서는 아래와 같이 사용할 수 있습니다.

C++과 같이 : 기호를 사용하는데 super class의 클래스명에 괄호기호가 붙는 점이 다르네요

물론 함수 override 도 아래와 같이 할 수 있습니다.

open annotation은 class로부터의 상속이 가능하다는 의미입니다. Kotlin에서는 모든 class가 default로 final 로 선언됩니다

open annotation을 사용하는 경우에만 상속이 가능합니다.

method 의 경우에도 open annotation이 있는 method 만 override 가능합니다.

open class Base {
    open fun v() {}
    fun nv() {}
}
class Derived() : Base() {
    override fun v() {}
}

변수의 경우 val 변수(Read only) 를 var 변수로 재정의 할 수 있지만, 반대는 안됩니다.

interface Foo {
    val count: Int
}

class Bar1(override val count: Int) : Foo

class Bar2 : Foo {
    override var count: Int = 0
}

 

 

+ Recent posts