Android Support Annotations 라이브러리를 활용한 결함 탐지방법

1. Annotations 이란 ?

 - Annotation 은 Java 코드에서 추가할 수 있는 메타데이터로, @ 기호로 시작한다.

 - 상위 클래스의 메서드를 오버라이드 한다는 정보를 표현하는 @Override 애너테이션이 대표적이다.

 - 추가 정보를 문법적으로 표현할 수 있기 때문에 Java 에서는 API의 의도를 애너테이션으로 명시하고 결함 탐지에 활용하는 기법이 발달 했다.

 

2. Java 에서 제공되는 Annotations

 - @Override : 함수 Override 일 경우에 사용되는 Annotations

 - @Deprecated : 해당 변수, 함수 명이 삭제될 수 있음을 나타낼 때 사용

 - @SuppressWarnings : 권장하지 않는 구문에 대하여 노랑색으로 경고를 표시해 주는 경우

 

3. Android Support Library 의 Annotations

build.gradle 에 아래의 코드를 추가하면 사용 가능하다.

dependencies {

   compile 'com.android.support:support-annotations:20.0.0'

}

 

3.1 @StringRes, @DrawableRes, @ColorRes

위 세가지의 Annotations 는 리소스 아이디 관련 애너테이션이다. 요소의 값이 이 애너테이션들이 의미하는 리소스 타입에 해당하는 리소스 아이디임을 의미한다.

Android 의 모든 리소스는 R 클래스에 의해 int 타입의 아이디로 관리된다. 그래서 drawable 타입 리소스의 아이디를 넣어야 할 곳에 문자열 리소스의 아이디나 정수 값을 넣는 등 실수를 범할 수 있는데, 이 애너테이션을 이용하면 실수를 방지할 수 있다.

void setMessage(@StringRes int resId) {  
    mMessage = mContext.getText(resId);
}

// ...

setMessage(R.string.error_retry);    // OK

int stringId = R.string.error_retry;  
setMessage(stringId);                // OK

setMessage(R.color.white);           // ERROR: Expected resource of type string  
setMessage(1);                       // ERROR: Expected resource of type string 

 

3.2 @ColorInt

@ColorInt 이너테이션은 해당 값이 0xff99ff99와 같은 ARGB 컬러 정수임을 나타낸다. 글자가 비슷한 @ColorRes 애너테이션은 R.color.divider 와 같은 Color Drawable 타입 리소스의 아이디를 나타내므로 혼동하지 않도록 주의한다.

다음 코드는 int 타입 파라미터의 값을 @ColorInt 애너테이션과 @ColorRes 애너테이션으로 명확하게 구분한 예이다.

void setColor(@ColorInt int color) {  
    mColor = color;
}

void setColorRes(@ColorRes int resId) {  
    setColor(mContext.getResources().getColor(resId));
}

// ...

setColor(0xff99ff99);         // OK  
setColor(R.color.divider);    // ERROR: Should pass resolved color instead of resource id  
                              //        here: `getResources().getColor(R.color.divider)`

setColorRes(0xff99ff99);      // ERROR: Expected resource of type color  
setColorRes(R.color.divider); // OK  

 

3.3 @IntRange, @FloatRange

@IntRange 애너테이션 @FloatRange 애너테이션은 숫자형 값의 범위를 한정한다. from 파라미터와 to 파라미터를 지니며, 값의 범위는 from 파라미터의 값부터 to 파라미터의 값 사이다. 투명도에서 0~1 범위의 소숫값(decimal)을 사용하는지, 색상 표현에서 0~255 범위의 정숫값을 사용하는지 혼동하기 쉬운데, 이런 경우에 유용하게 쓰일 수 있다. @IntRange 애너테이션은 int 타입뿐만 아니라 long 타입에도 사용할 수 있다. 마찬가지로 @FloatRange 애너테이션은 float 타입뿐만 아니라 double 타입에도 사용할 수 있다.

다음 코드는 @FloatRange 애너테이션으로 alpha 파라미터의 값이 0.0~1.0이어야 함을 나타내는 예다.

void setAlpha(@FloatRange(from=0.0, to=1.0) float alpha) {  
    // if (alpha < 0 || alpha > 1.0f) {
    // throw new IllegalArgumentException();
    // }
}

// ...

setAlpha(0.5f); // OK  
setAlpha(127);  // ERROR: Value must be ≥ 0.0 and ≤ 1.0 (was 127)  

 

3.4 @UiThread, @WorkerThrea

@UiThread 애너테이션 @WorkerThread 애너테이션, 은 스레드 관련 애너테이션이다. 이 애너테이션들이 의미하는 스레드와 동일한 유형의 스레드에서만 해당 메서드를 호출할 수 있도록 제약한다.

@WorkerThread
void backgroundJob() {  
}

@UiThread
void uiJob() {  
}

// ...

@WorkerThread
void backgroundJob2() {  
    backgroundJob();                  // OK
    uiJob();                          // ERROR: Method uiJob must be called from the UI
                                      // thread, currently inferred thread is worker
    view.setVisibility(View.VISIBLE); // ERROR: Method setVisibility must be called from
                                      // the UI thread, currently inferred thread is
                                      // worker
}

@UiThread
void uiJob2() {  
    backgroundJob();                  // ERROR: Method backgroundJob must be called from
                                      // the worker thread, currently inferred thread is
                                      // UI
    uiJob();                          // OK
    view.setVisibility(View.VISIBLE); // OK
}

 

3.5 @CallSuper

@CallSuper 애너테이션은 이 애너테이션이 붙은 메서드를 하위 클래스에서 오버라이드할 때는 반드시 상위 클래스의 메서드를 호출하도록 강제한다. 액티비티의 라이프사이클 메서드에도 사용된다.

다음 코드는 foo() 메서드에 @CallSuper 애너테이션을 붙여 foo() 메서드를 오버라이드한 하위 클래스에서 반드시 super.foo() 메서드를 호출하도록 강제하는 예다.

 

class Super {  
    @CallSuper
    void foo() {
    }

    void bar() {
    }
}

// ...

class Example1 extends Super {  
    @Override
    void foo() {                // ERROR: Overriding method should call 'super.foo'
    }

    @Override
    void bar() {                // OK
    }
}

class Example2 extends Super {  
    @Override
    void foo() {                // OK
        super.foo();
    }

    @Override
    void bar() {                // OK
        super.bar();
    }
}

class ExampleActivity extends Activity {  
    protected void onCreate(Bundle saved) { // ERROR: Overriding method should call
                                            //        'super.onCreate'
    }
}

 

3.6 @NonNull, @Nullable

 

@NonNull 애너테이션 @Nullable 애너테이션은 null 값 처리에 관련된 애너테이션이다.

@NonNull 애너테이션은 값이 'null'이 아니라는 것을 나타낸다. 예를 들어 @NonNull 애너테이션이 붙은 변수에 null 값을 대입하면 경고가 나타난다.

@Nullable 애너테이션은 값이 'null'일 수 있다는 것을 나타낸다. 예를 들어 @Nullable 애너테이션이 붙은 변수를 null 검사 없이 사용하면 경고가 나타난다.

다음 코드는 필드, 파라미터, 메서드에 @NonNull 애너테이션과 @Nullable 애너테이션을 사용한 예다.

 

class Example {  
    @NonNull
    final Context mContext;
    @Nullable
    View mView;

    Example(@NonNull Context context) {
        // context 파라미터에 @NonNull 애너테이션이 붙어 있으므로
        // 파라미터의 값이 null이 아닐 것이라 가정한다.
        // 그래서 null 검사가 필요 없다.
        // if (context == null) {
        // throw new NullPointerException("context");
        // }
        mContext = context;
    }

    @NonNull
    Context getContext() {
        return mContext;
    }

    void setView(@Nullable View view) {
        mView = view;
    }

    @Nullable
    View getView() {
        return mView;
    }
}

// ...

Context context = null;  
new Example(context);        // WARNING: Argument 'context' might be null

new Example(null);           // WARNING: Passing 'null' argument to parameter annotated as @NonNull

new Example(nonNullContext); // OK

View view = getView();  
view.getTag();               // WARNING: Method invocation 'view.getTag()' may produce  
                             // 'java.lang.NullPointerException'

@NonNull View mView;
mView = getView();           // WARNING: Expression 'getView()' might evaluate to null but  
                             // is assigned to a variable that is annotated with @NonNull

 

http://d2.naver.com/helloworld/8725603

http://thdev.net/616

'Android' 카테고리의 다른 글

[AndroidStudio] git clone 방법  (0) 2023.05.27

Android PowerManager.WakeLock

 

일반적으로 Android 는 애플리케이션이나 서비스에 전력이 필요하지 않으면 CPU도 전력을 소모하지 않게 설계되어 있습니다.

따라서 화면이 꺼진 뒤 일정 시간이 지나면 CPU 가 딥 슬립(deep sleep) 상태로 전환됩니다.

딥 슬립 상태로 전환되면 백그라운드에서 동작하는 서비스도 CPU를 사용하지 못하기 때문에, 아래와 같은 일이 발생할 수 있습니다.

1. BroadcastReceiver 클래스의 onReceive() 메서드가 호출 되었지만, 관련 로직을 수행하지 않음.

2. AlarmManager 클래스를 사용하여 트리거가 발생 하였지만, CPU 가 실행상태가 아니므로 이후 로직을 사용하지 않음.

 

이를 방지하기 위해 Android 에서는 PowerManager.WakeLock 클래스를 제공하여 CPU가 딥 슬립상태에서 실행 상태로 전환되어 유지될 수 있도록 합니다. 다만, Device 의 베터리 수명은 이 API 를 어떻게 사용하느냐에 따라 크게 달라지기 때문에, PowerManager.WakeLock 은 정말 필요한 경우가 아니면 사용하지 말아야 하고, 만약 사용 하더라도 가능한 최소 수준을 사용하고, 최대한 빨리 release 해주어야 합니다.

WakeLock 을 사용하기 위한 API 는 newWakeLock() 입니다. 이 함수는 PowerManager.WakeLock object 를 생성 합니다. 이 객체를 사용하여 Device 의 Power 를 제어할 수 있습니다.

Application 에서 이를 사용하는 방법은 아주 간단 합니다.

PowerManager pm = (PowerManager) getSystemService(Context.POWER_SERVICE);
 PowerManager.WakeLock wl = pm.newWakeLock(PowerManager.SCREEN_DIM_WAKE_LOCK, "My Tag");
 wl.acquire();
   ..screen will stay on during this section..
 wl.release();

newWakeLock 에 사용되는 Flag 의 종류는 아래와 같이 4가지가 있습니다.

PARTIAL_WAKE_LOCK 을 사용하면 스크린 상태에 관계 없이, 사용자가 전원 버튼을 눌러 단말을 sleep 시켜도 계속 CPU는 실행상태에 있습니다.

다른 PARTIAL_WAKE_LOCK 을 제외한 다른 Flag 를 사용하면 사용자가 전원버튼을 눌렀을 때 CPU는 sleep 상태로 들어갈 수 있습니다.

 

WakeLock 클래스의 로직은 IOS 미만 버전과 이상 버전의 로직이 다릅니다.

IOS 이전에는 WakeLock.acquire(timeout) 메서드에 치명적인 문제점이 존재하는데, 이 메서드는 timeout 시간만큼 CPU 를 사용하고 나면 자동으로 WakeLock.release() 메서드가 호출되어 CPU 사용을 마칩니다. 그런데 timeout 시간보다 먼저 작업이 끝나서 명시적으로 WakeLock.release() 메서드를 호출 하더라도 timeout 시간이 지나면 다시한번 WakeLock.release() 메서드가 호출되는데, 그러면 이미 CPU 사용을 마쳤는데도 다시한번 CPU 사용을 마치려 시도하면서 충돌이 발생합니다.

 

FATAL EXCEPTION: main  
java.lang.RuntimeException: WakeLock under-locked  
    at android.os.PowerManager$WakeLock.release(PowerManager.java:307)
    at android.os.PowerManager$WakeLock.release(PowerManager.java:282)
    at android.os.PowerManager$WakeLock$1.run(PowerManager.java:202)
    at android.os.Handler.handleCallback(Handler.java:587)
    at android.os.Handler.dispatchMessage(Handler.java:92)
    at android.os.Looper.loop(Looper.java:130)
    at android.app.ActivityThread.main(ActivityThread.java:3691)
    at java.lang.reflect.Method.invokeNative(Native Method)
    at java.lang.reflect.Method.invoke(Method.java:507)
    at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:847)
    at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:605)
    at dalvik.system.NativeStart.main(Native Method)

충돌이 발생하는 원인을 살펴보기 위해 WakeLock.acquire(timeout) 메서드의 코드를 살펴보면, 다음과 같이 postDelayed() 메서드를 사용하여 timeout 시간 뒤에 mReleaser 객체가 호출되게 됩니다. mReleaser 객체는 Runnable 객체로, WakeLock.release() 메서드를 호출합니다.

/**
 * Makes sure the device is on at the level you asked when you created
 * the wake lock. The lock will be released after the given timeout.
 * 
 * @param timeout Release the lock after the give timeout in milliseconds.
 */
public void acquire(long timeout) {
    synchronized (mToken) {
        acquireLocked();
        mHandler.postDelayed(mReleaser, timeout);
    }
}
private final Runnable mReleaser = new Runnable() { 1254 public void run() { 1255 release(); 1256 } 1257 };

왜 postDelated() 메서드에 의해서 충돌이 발생하는지 ICS 미만 버전과 ICS 이상 버전에 있는 PowerManager.java 의 소스코드를 살펴보면 알 수 있습니다.

ICS 미만 버전의 PowerManager.java 에서 release() 메서드의 코드는 다음과 같습니다.

public void release(int flags)  
{
    synchronized (mToken) {
        if (!mRefCounted || --mCount == 0) {
            try {
                mService.releaseWakeLock(mToken, flags);
            } catch (RemoteException e) {
            }
            mHeld = false;
        }
        if (mCount < 0) {
            throw new RuntimeException("WakeLock under-locked " + mTag);
        }
    }
}

 

위 두 소스 코드를 비교해 보면 ICS 이상 버전에 removeCallbacks(mRelease) 메서드가 추가된 것을 볼 수 있습니다 .

이처럼 timeout 시간보다 먼저 release() 메서드가 호출되었을 때에는 timeout 시간이 지난 뒤 다시한번 release() 메서드가 호출되지 않도록 하여 충돌이 발생하지 않습니다.

Handler.removeCallbacks() 메서드는 postDelayed() 메서드를 통해 메시지 큐에 등록된 Runnable 객체를 삭제합니다.

 

WakeLock  효율적으로 사용하기

WakeLock 은 배터리에 가장 영향을 많이 끼치는 기능 중 하나이므로, 사용할 수 밖에 없는 상황이라면 최대한 효율적으로 사용해야 합니다.

WakeLock 을 효율적으로 사용하는 방법은 다음과 같습니다.

 

1. 어떤 동작을 수행하려고 WakeLock 을 사용했다면 동작이 종료되었을 때 바로 release() 메서드를 호출한다.

2. 연산이 끝나면 항상 release() 메서드가 호출되고 Exception 등으로 인해 release() 메서드가 호출되지 못하는 일이 없도록 finally 키워드를 사용한다.

3. 패킷 발신, 수신에 사용한다면 발신할 때 한번 수신할때 다시한번 WakeLock을 호출 하여 두번의 호출이 생긴다. 만약 발신 후 수신까지 시간이 매우 짧다면 WakeLock.acquire(timeout) 메서드를 사용하여 호출 횟수를 줄일 수 있다.

4. 화면이 켜진 상태라면 CPU는 딥슬립 모드로 전환되지 않기 때문에 화면이 꺼진 상태에서만 WakeLock 을 사용하고, 화면이 켜진 상태에서는 WakeLock 을 사용하지 않는 것도 좋은 방법이다.

5. BroadcastReceiver 클래스를 사용하는 경우에도 onReceive(...) 메서드 이후의 로직이 언제나 정상적으로 수행되게 하기 위해서 WakeLock 을 사용한다. 하지만 Android Support Library 를 사용하면 WakefulBroadcastReceiver 클래스를 사용하여 직접 WakeLock 을 호출하지 않아도 된다.

 

 

WakefullBroadcastReceiver 클래스는 startWakefulService() 메서드로 서비스 (주로 IntentService) 에 처리해야 할 작업을 전달 해주고, 이러한 전달 과정에서 단말이 딥 슬립 상태로 돌아가지 않도록 내부적으로 PARTIAL_WAKE_LOCK 을 획득 합니다. 서비스에서 작업이 완료 되면 completeWakefullIntent() 메서드를 호출하여 WakeLock 사용을 종료하게 됩니다.

 

 

기존에는 그냥 연습용 앱이어서 해상도 별로 이미지를 생각하지 않고 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" />

 

굿굿 

1. File > New > Project from Version Control... 선택

 

2. Clone 하고자 하는 repository 의  URL 입력

'Android' 카테고리의 다른 글

Anotation  (0) 2024.11.10

오늘은 근거리 무선 통신기술인 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 를 참고하시면 됩니다. 

 

안드로이드 어플리케이션 개발 중, external 저장 공간에 있는 파일을 read 또는 write 하려고 할 때 아래와 같이 에러가 발생하는 경우가 있습니다.

02-10 14:20:45.310 30711-30711/com.example.codetravel.musicplayer W/System.err: java.io.FileNotFoundException: /storage/emulated/0/john.mp3: open failed: EACCES (Permission denied)
02-10 14:20:45.343 30711-30711/com.example.codetravel.musicplayer W/System.err:     at libcore.io.IoBridge.open(IoBridge.java:452)
02-10 14:20:45.343 30711-30711/com.example.codetravel.musicplayer W/System.err:     at java.io.FileInputStream.<init>(FileInputStream.java:76)
02-10 14:20:45.343 30711-30711/com.example.codetravel.musicplayer W/System.err:     at android.media.MediaPlayer.setDataSource(MediaPlayer.java:1095)
02-10 14:20:45.343 30711-30711/com.example.codetravel.musicplayer W/System.err:     at android.media.MediaPlayer.setDataSource(MediaPlayer.java:1046)
02-10 14:20:45.344 30711-30711/com.example.codetravel.musicplayer W/System.err:     at com.example.codetravel.musicplayer.MusicPlayerActivity.onPlayPause(MusicPlayerActivity.java:45)
...
02-10 14:20:45.344 30711-30711/com.example.codetravel.musicplayer W/System.err:     at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:616)
02-10 14:20:45.344 30711-30711/com.example.codetravel.musicplayer W/System.err: Caused by: android.system.ErrnoException: open failed: EACCES (Permission denied)
02-10 14:20:45.344 30711-30711/com.example.codetravel.musicplayer W/System.err:     at libcore.io.Posix.open(Native Method)
02-10 14:20:45.344 30711-30711/com.example.codetravel.musicplayer W/System.err:     at libcore.io.BlockGuardOs.open(BlockGuardOs.java:186)
02-10 14:20:45.344 30711-30711/com.example.codetravel.musicplayer W/System.err:     at libcore.io.IoBridge.open(IoBridge.java:438)

02-10 14:20:45.345 30711-30711/com.example.codetravel.musicplayer W/System.err:     ... 15 more 


open failed: EACCES (Permission denied) 에러가 발생하는 이유는 external 저장 공간의 파일에 대해서 read 또는 write 하기 위해서는 별도의 권한이 필요하기 때문입니다.

권한을 부여하는 방법은 AndroidManifest.xml 파일에 아래와 같이 read, write 권한을 기입하면 됩니다.

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

위치는 <manifest> 안에 그리고 <application> 밖에 선언하면 됩니다.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.codetravel.musicplayer">

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- 여기에 추가해 줍니다 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/> <!-- 여기에 추가해 줍니다 -->

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MusicPlayerActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>

</manifest> 


그리고 어플리케이션을 install 한 후, setting 메뉴의 해당 app으로 가서 storage 권한을 enable 해주면 에러는 사라지게 됩니다.

Internal 저장 공간과 External 저장 공간의 특징에 대해서는 Android 데이터 저장 방법 - Internal/External Storage 포스팅에 자세히 설명되어 있습니다.

StrictMode는 Main Thread 에서 일어날 수 있는 일을 감지하고 문제를 해결할 수 있도록 해주는 개발자 도구입니다.

StrickMode는 Main Thread 에서 Disk 입출력 또는 Network 액세스 같은 속도가 느려질 수 있는 동작을 하는 것을 감지하는데 가장 일반적으로 사용됩니다.

오래 걸릴 수 있는 작업을 Main Thread 분리하여 ANR(Android Not Response)를 방지할 수  있도록 미리 탐지합니다.

 

* Main Thread 의 작업

  안드로이드의 Callback과 lifecycle 관련 이벤트들은 모두 Main Thread에서 처리됨

  Application의 경우 Main Thread 에서 Animation, Scroll 작업에 대한 Callback을 처리함.

  I/O 작업에 걸리는 시간만큼 UI 반응이 지연됨

 

* Disk I/O 작업

  파편화된 블록 모으기 작업을 수행하는 동안 지연 발생 가능성 있음

  다른 프로세스가 오랜 시간이 걸리는 삭제 작업을 진행하면 다른 프로세스의 I/O 작업은 지연됨

  메모리 여유 공간이 적을 수록 I/O 작업이 지연됨

 

* 네트워크는 예측 불가능하므로 절대 Main Thread 에서 처리되면 안됨(기본적으로 정책적용됨)

 

한 스레드에 적용된 규약은 이 스레드에 접근하는 다른 스레드에도 전파됩니다.

바인더를 통해 다른 스레드 또는 다른 프로세스의 메소드를 호출할 때도 동일한 규약이 적용됩니다.

 

 

public void onCreate() {
    if (DEVELOPER_MODE) {
        StrickMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                        .detectDiskReads()
                        .detectDiskWrites()
                        .detectNetwork()
                        .penaltyLog()
                        .build());
        StrickMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                        .detectLeakedSqlLiteObjects()
                        .detectLeakedClosableObjects()
                        .panaltyLog()
                        .penaltyDeath()
                        .build());
    }
}

 

penal...() 메소드를 통해 위반이 감지될 때 어떤 일이 발생할지 결정할 수 있습니다.

penaltyLog() : Log를 통해 위반을 감지

penaltyDropbox() : DropBoxManager 에 기록

                         adb shell dumpsys sropbox data_app_strictmode --print 를 통해 내용을 확인

panaltyDeath() : crash발생

 

 

해당 Thread의 동작이 의도된 동작일 경우 아래와 같이 정책변경을 할 수 있습니다.

여러 블로그에서 Nougat(Android N-OS)부터 Main Thread 에서 Network 액세스를 차단하기 때문에 아래와 같이 해결하라고 말하고 있는데요.

정책변경 보다는 Thread 분리가 우선입니다.

 

StrickMode.ThreadPolicy tp = new StrictMode.ThreadPolicy.Builder()
        .permitDiskReads()
        .permitDiskWrites()
        .build();
StrickMode.setThreadPolicy(tp);

 

 

StrickMode.ThreadPolicy tp = new StrictMode.ThreadPolicy.Builder()
        .permitNetwork()
        .build();
StrickMode.setThreadPolicy(tp);

 

아래와 같이 StrictMode 테스트를 위해 App에서 StrictMode 정책 설정을 하고 Main Thread에서 SharedPreferences를 설정하는 코드를 적용해보았습니다.

SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(activity);
SharedPreferences.Editor editor = sharedPreferences.edit();
editor.putString("venderinfo", "aaa");
editor.apply();

 

StrictMode 정책을 penaltyLog()로 설정하였을 때 위반로그를 확인 할 수 있습니다.

SharedPreferences 와 같이 Disk I/O 작업을 수반하는 동작은 Main Thread 에서 분리하여 개발해야겠습니다.

 

Google 에서 StrickMode 정책 적용은 실제 배포할 때는 적용하지 않도록 가이드 하고 있습니다.

OS upgrade 때마다 정책변경이 발생할 수 있으므로 기존 OS에서는 정상 동작했던 것도 OS Upgrade 이후에 오동작을 유발할 수 있기 때문이라고 합니다.

 

 

 

 

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

 

 

+ Recent posts