FFmpeg 참고자료

by Blogger 하얀쿠아
2012. 10. 3. 06:26 수행 프로젝트 이력/스마트폰을 이용한 감시시스템 [2011.09~ 2012.09]

* 스마트폰을 이용한 가정용 화상감시 장비 프로젝트 진행 과정에서 얻게된 지식을 공유하는 포스팅 입니다.


본 프로젝트에서는 FFmpeg와 x264 라이브러리를 안드로이드에 포팅하여 H.264 인코딩/디코딩을 사용하였다.

결국 인코딩/디코딩 모두 성공 했지만 불완전 하다. 

네트워크 전송시 Intra 코딩만 가능. Inter 코딩 시 수신 측에서 디코딩 불가 문제가 있다. 

이 문제를 해결 중에 있다.(2012. 10월)

아래 링크는 이 작업과정에서 참조했던 사이트의 링크들 이다.


- ffmpeg 사용시 참고했던 사이트 링크

ffmpeg는 레퍼런스 문서나 예제가 많지 않아서 상당히 애를 먹었던 기억이 있다.

doxygen 문서가 있긴 하지만, 필요한 걸 찾기가 개인적으로 불편했었다. 

그래서 각 섹션마다 링크를 걸어둔다.


전역함수 레퍼런스

http://ffmpeg.org/doxygen/trunk/globals_func.html


AVCodecContext 구조체 레퍼런스

http://ffmpeg.org/doxygen/trunk/structAVCodecContext.html#7abe7095de73df98df4895bf9e25fc6b


AVPacket 구조체 레퍼런스

http://ffmpeg.org/doxygen/trunk/structAVPacket.html


libavcodec/h264.h 파일 레퍼런스

http://www.ffmpeg.org/doxygen/trunk/h264_8h.html


ffmpeg 파일 목록

http://www.ffmpeg.org/doxygen/trunk/files.html


ffmpeg 와 SDL 튜토리얼

http://dranger.com/ffmpeg/tutorial01.html



- 그외에 참조했던 사이트 (인코딩/디코딩)


FFmpeg을 이용한 Android 동영상 플레이어 개발 

http://helloworld.naver.com/helloworld/8794


ffmpeg 비디오 디코더(decoder) 사용법 - how to use ffmpeg video decoder

http://greenday96.blogspot.kr/2011/07/ffmpeg-decoder.html


Using libavformat and libavcodec

http://www.inb.uni-luebeck.de/~boehme/using_libavcodec.html


x264 옵션 값

http://yg05123.blog.me/70042737774


FFmpeg에 x264 인코더 사용방법 

http://iamlow.tistory.com/entry/FFmpeg%EC%97%90-x264-%EC%9D%B8%EC%BD%94%EB%8D%94-%EC%82%AC%EC%9A%A9%EB%B0%A9%EB%B2%95


ffmpeg의 api-example을 기반으로 한 mpeg 인코딩 방법(인코딩 시나리오 파악용으로 참조했다)

http://blog.daum.net/thflfkaus/6


이 댓글을 비밀 댓글로
    • 박가람
    • 2012.11.13 16:38
    안녕하세요~

    저도 ffmpeg + x264로 스마트폰에서 서버로 동영상 전송을 구현하려고하고 있습니다.

    혹시 ffmpeg 과 x264 포팅을 어떻게 하셨는지 여쭈어 봐도 될까요?
    • 저는 halfninja 라는 프로젝트를 참조했습니다.
      다음 URL에서 그 프로젝트의 자료를 다운로드 받을 수 있습니다.

      https://github.com/halfninja/android-ffmpeg-x264

    • 박가람
    • 2012.11.15 22:07
    감사합니다.
    • 박가람
    • 2012.11.25 23:31
    혹시 성능은 어느정도 나오는지 알 수 있을까요?

    실시간으로 적용하려고 하는데 성능이 어느정도 나오는지 궁금해서요~

    싸이즈는 몇에 몇 프레임 정도 나왔나요?

    • 320 X 240 에 대략 10~15 프레임 정도 였던것 같습니다.

      프레임 수치는 측정치는 아니고 제 느낌이(;;) 그렇다는것이므로 참고만 해주시기 바랍니다.
    • 박가람
    • 2013.01.02 10:39
    안녕하세요~ 혹시 인코드 디코드 할때 컨텍스트랑 코덱 세팅 값을 어떻게 하셨는지 알수 있을까요..
    돌아가는거 보니깐 인코딩은 되는 것 같은데 디코딩에서 에러가 나네요ㅠ
    • 김철표
    • 2013.08.16 02:47
    안녕하세요!
    박가람님과 마찬가지 이유로 문의드립니다.
    네트워크로 오디오 패킷을 전송해서 디코딩해보려고 하는데요, 코덱컨텍스트 세팅을 어떻게 하면될까요? 마찬가지고 원격으로는 자꾸 오류가나네요

libavcodec을 사용한 H.264 인코딩/디코딩 - C code

by Blogger 하얀쿠아
2012. 10. 3. 05:58 수행 프로젝트 이력/스마트폰을 이용한 감시시스템 [2011.09~ 2012.09]


* 스마트폰을 이용한 가정용 화상감시 장비 프로젝트 진행 과정에서 얻게된 지식을 공유하는 포스팅 입니다.


본 프로젝트의 전체적인 시스템 구성도는 아래와 같다.



여기에서 코덱이 필요한 이유는, 감시 영상을 네트워크를 통해 전달하기 때문이다.

raw data를 그대로 전달 할 경우, 1프레임 영상의 크기가 1메가를 상회하게 된다. 

(지원 해상도는 QCIF, QVGA 만 허용함. 이는 각각 352 x 288과 320 x 240 이다.)


네트워크 대역폭 제한으로 인해 받아 보는 쪽에서 영상의 프레임이 낮을 것이다.

또한 3G/4G 를 사용한다면 데이터 요금도 상당할 것이다.

 이를 h.264 코덱으로 인코딩 하여 네트워크로 보내게 된다면, 이런 문제를 해결 가능 할 것이라 판단하여 코덱을 사용하였다. 물론 인코딩 하는 카메라 스마트폰의 CPU사용량이 상당히 증가할 것이지만, 뷰어 측의 원할한 영상 수신을 위해서 이정도는 감수하기로 하였다.


감시카메라 Application을 구동하는 스마트폰에서는 입력되는 카메라 영상을 H.264 코덱으로 인코딩 하여 중계서버로 전송한다.


사용자뷰어 Application을 구동하는 스마트폰에서는 중계서버로부터 전송받는 인코딩된 데이터를 디코딩하여 사용자에게 보여준다.


위 과정에서 이루어지는 인코딩/디코딩은 ffmpeg 를 android 용으로 포팅하여 사용했다.

1. 안드로이드에서 ffmpeg의 라이브러리 함수를 호출할 수 있도록 C로 인터페이스 함수를 만들었다.

2. 이 인터페이스 함수들만 모아 .so파일로 만들어 dynamic link 하여 사용하였다.


아래는 인터페이스 함수 제작 과정에 참고한 예제 코드이다.


출처 : http://natnoob.blogspot.kr/search?updated-min=2011-01-01T00:00:00%2B07:00&updated-max=2012-01-01T00:00:00%2B07:00&max-results=6


이 예제는 api-example을 기반으로 구현했다.

이것은 C코드를 이용하여 raw data 영상(QCIF)을 H264로 인코딩 하는 방법과 인코딩 된 영상을 yuv로 디코딩 하는 방법을 보여준다.



 - 아래는 main 함수 부분이다.

int main(int argc, char **argv)

{


/* must be called before using avcodec lib */

avcodec_init();


/* register all the codecs */

avcodec_register_all();


h264_encode_decode("Foreman.qcif","Decoded.yuv");


return 0;

}



- 시작 부분은 언제나 코덱의 등록과 초기화이다.

그 후 이 예제에서는 h264_encode_decode( ) 함수를 호출할 것인데, 이 함수는 Foreman.qcif 라는 파일로 입력되는 영상을 h.264로 인코딩 한 뒤, 다시 yuv 파일로 디코딩 하여 Decoded.yuv 로 저장 할 것이다.


총 5단계로 나눠서 보이겠다.

1. 인코딩/디코딩에 사용할 변수 선언

AVCodec *codecEncode, *codecDecode;

AVCodecContext *ctxEncode= NULL, *ctxDecode = NULL; 


FILE *fin, *fout;

AVFrame *pictureEncoded, *pictureDecoded;


uint8_t *encoderOut, *picEncodeBuf;

int encoderOutSize, decoderOutSize;

int pic_size;


AVPacket avpkt;

int got_picture, len;


const int clip_width = 176;

const int clip_height = 144;


int frame = 0;

uint8_t *decodedOut;



2. 코덱 초기화/디코더를 위한 picture 구조체

codecDecode = avcodec_find_decoder(CODEC_ID_H264);

if (!codecDecode) {

fprintf(stderr, "codec not found\n");

exit(1);

}


ctxDecode= avcodec_alloc_context();

avcodec_get_context_defaults(ctxDecode);

ctxDecode->flags2 |= CODEC_FLAG2_FAST;

ctxDecode->pix_fmt = PIX_FMT_YUV420P;

ctxDecode->width = clip_width;

ctxDecode->height = clip_height; 

ctxDecode->dsp_mask = (FF_MM_MMX | FF_MM_MMXEXT | FF_MM_SSE);


if (avcodec_open(ctxDecode, codecDecode) < 0) {

fprintf(stderr, "could not open codec\n");

exit(1);

}


pictureDecoded= avcodec_alloc_frame();

avcodec_get_frame_defaults(pictureDecoded);

pic_size = avpicture_get_size(PIX_FMT_YUV420P, clip_width, clip_height);


decodedOut = (uint8_t *)malloc(pic_size);

fout = fopen(fileout, "wb");

if (!fout) {

fprintf(stderr, "could not open %s\n", fileout);

exit(1);

}



3. 코덱 초기화/인코더를 위한 picture 구조체

codecEncode = avcodec_find_encoder(CODEC_ID_H264);

if (!codecEncode) {

printf("codec not found\n");

exit(1);

}


ctxEncode= avcodec_alloc_context();

ctxEncode->coder_type = 0; // coder = 1

ctxEncode->flags|=CODEC_FLAG_LOOP_FILTER; // flags=+loop

ctxEncode->me_cmp|= 1; // cmp=+chroma, where CHROMA = 1

ctxEncode->partitions|=X264_PART_I8X8+X264_PART_I4X4+X264_PART_P8X8+X264_PART_B8X8; // partitions=+parti8x8+parti4x4+partp8x8+partb8x8

ctxEncode->me_method=ME_HEX; // me_method=hex

ctxEncode->me_subpel_quality = 0; // subq=7

ctxEncode->me_range = 16; // me_range=16

ctxEncode->gop_size = 30*3; // g=250

ctxEncode->keyint_min = 30; // keyint_min=25

ctxEncode->scenechange_threshold = 40; // sc_threshold=40

ctxEncode->i_quant_factor = 0.71; // i_qfactor=0.71

ctxEncode->b_frame_strategy = 1; // b_strategy=1

ctxEncode->qcompress = 0.6; // qcomp=0.6

ctxEncode->qmin = 0; // qmin=10

ctxEncode->qmax = 69; // qmax=51

ctxEncode->max_qdiff = 4; // qdiff=4

ctxEncode->max_b_frames = 3; // bf=3

ctxEncode->refs = 3; // refs=3

ctxEncode->directpred = 1; // directpred=1

ctxEncode->trellis = 1; // trellis=1

ctxEncode->flags2|=CODEC_FLAG2_FASTPSKIP; // flags2=+bpyramid+mixed_refs+wpred+dct8x8+fastpskip

ctxEncode->weighted_p_pred = 0; // wpredp=2

ctxEncode->bit_rate = 32000;

ctxEncode->width = clip_width;

ctxEncode->height = clip_height;

ctxEncode->time_base.num = 1;

ctxEncode->time_base.den = 30;

ctxEncode->pix_fmt = PIX_FMT_YUV420P; 

ctxEncode->dsp_mask = (FF_MM_MMX | FF_MM_MMXEXT | FF_MM_SSE);

ctxEncode->rc_lookahead = 0;

ctxEncode->max_b_frames = 0;

ctxEncode->b_frame_strategy =1;

ctxEncode->chromaoffset = 0;

ctxEncode->thread_count =1;

ctxEncode->bit_rate = (int)(128000.f * 0.80f);

ctxEncode->bit_rate_tolerance = (int) (128000.f * 0.20f);

ctxEncode->gop_size = 30*3; // Each 3 seconds


/* open codec for encoder*/

if (avcodec_open(ctxEncode, codecEncode) < 0) {

printf("could not open codec\n");

exit(1);

}


//open file to read

fin = fopen(filein, "rb");

if (!fin) {

printf("could not open %s\n", filein);

exit(1);

}


/* alloc image and output buffer for encoder*/

pictureEncoded= avcodec_alloc_frame();

avcodec_get_frame_defaults(pictureEncoded);


//encoderOutSize = 100000;

encoderOut = (uint8_t *)malloc(100000);

//int size = ctxEncode->width * ctxEncode->height;

picEncodeBuf = (uint8_t *)malloc(3*pic_size/2); /* size for YUV 420 */

pictureEncoded->data[0] = picEncodeBuf;

pictureEncoded->data[1] = pictureEncoded->data[0] + pic_size;

pictureEncoded->data[2] = pictureEncoded->data[1] + pic_size / 4;

pictureEncoded->linesize[0] = ctxEncode->width;

pictureEncoded->linesize[1] = ctxEncode->width / 2;

pictureEncoded->linesize[2] = ctxEncode->width / 2; 



4. 입력 파일로 부터 데이터를 읽고, avcodec_encode_video 를 사용해서 인코딩 한다.

인코딩된 데이터는 디코더로 보내질 것인데, yuv 포맷으로 디코딩 된 결과가 나올 것이고, avcodec_decode_video2 를 사용할 것이다. 

디코딩 된 데이터는 decoded.yuv 라는 파일로 만들 것 이다.

//encode and decode loop

for(int i=0;i<30;i++) 

{

fflush(stdout);

//read qcif 1 frame to buufer

fread(pictureEncoded->data[0],ctxEncode->width * ctxEncode->height, 1, fin);

fread(pictureEncoded->data[1],ctxEncode->width * ctxEncode->height/4, 1, fin);

fread(pictureEncoded->data[2],ctxEncode->width * ctxEncode->height/4, 1, fin); 

pictureEncoded->pts = AV_NOPTS_VALUE;


/* encode frame */

encoderOutSize = avcodec_encode_video(ctxEncode, encoderOut, 100000, pictureEncoded);

printf("encoding frame %3d (size=%5d)\n", i, encoderOutSize);

if(encoderOutSize <= 0)

continue;


//send encoderOut to decoder

avpkt.size = encoderOutSize;

avpkt.data = encoderOut;

//decode frame

len = avcodec_decode_video2(ctxDecode, pictureDecoded, &got_picture, &avpkt);

if (len < 0) {

printf("Error while decoding frame %d\n", frame);

exit(1);

}

if (got_picture) {

printf("len = %d saving frame %3d\n", len, frame);

fflush(stdout);


avpicture_layout((AVPicture *)pictureDecoded, ctxDecode->pix_fmt

, clip_width, clip_height, decodedOut, pic_size);

fwrite(decodedOut, pic_size, 1, fout);

frame++;

}

}



5. 할당된 메모리를 해제하고, 파일 포인터를 닫아준다.

fclose(fout);

fclose(fin);


avcodec_close(ctxEncode);

avcodec_close(ctxDecode);

av_free(ctxEncode);

av_free(ctxDecode);

av_free(pictureEncoded); 

av_free(pictureDecoded); 



나는 이 구현코드가 libavcodec을 사용해서 C언어로 h264의 인코딩과 디코딩을 올바르게 하는 방법을 보여주는 적절한 예제라고 생각한다.

이 예제코드를 바로 사용하긴 힘들지만, 인코딩/디코딩 하는 시나리오를 이해하는데 도움이 될 것이다.



이 댓글을 비밀 댓글로
    • 밀레니안
    • 2012.10.12 17:42
    안녕하세요..? 저랑 비슷한 프로젝트를 진행중이셔서 질문이 있어 글 남깁니다..

    저도 ffmpeg를 이용해서 동영상 인코딩을 하는데

    안드로이드에서 영상을 받을때 mediarecoder를 이용해 raw data를 받아서 하신건가요?
    미디어레코더와 로컬 소켓을 이용해 받은 것을 어떻게 활용해야 할지 모르겠어서
    현재 카메라 프리뷰 콜백을 이용해 이미지를 영상으로 인코딩해서 하고 있는데

    최저 화질을 사용해도 15초 촬영에 인코딩 시간이 20초 걸려서 활용할 수가 없어 막혀있습니다....

    미디어레코더로 받은 것을 활용하신거라면...노하우좀 전수해주세요 ㅠㅠ
    • 저 역시 카메라 프리뷰 콜백을 이용했습니다. ^^;
      콜백의 파라미터로 들어오는 byte[ ]형태의 1프레임 짜리 이미지를 콜백 호출이 될 때마다 인코더에 넣어서 인코딩 하는 방식으로 구현했습니다.

      저같은 경우는 영상을 파일로 저장하는게 목적이 아니라 네트워크를 통해 Viewer 쪽에 최대한 빠르게 전송 + 1프레임 영상 하나하나에 대한 영상처리(차영상을 통한 detection & tracking)가 목적이었기 때문에 미디어레코더를 애초에 배제하고 raw image를 받을 수 있는 카메라 프리뷰를 이용했네요.
      제 포스팅이 도움이 되지는 못할 듯 해서 아쉽습니다 ^^;
    • 밀레니안
    • 2012.10.13 01:10
    아...감사합니다..

    Dev고양이님의 글이 저에게 충분히 도움이 되고있습니다...;;

    모든 것을 ffmpeg.c에 기대어 할 생각을 했는데 따로 인코딩 소스를 짜서 할 생각이 생기게 해주셨네요..

    한가지 질문 더 할게요 ㅠㅠ

    위에 적은것 처럼 저두 콜백 이용해서 하는 중인데 저의 경우에는 콜백 통해 받은 바이트를 한 파일에 이어 붙여 그걸 인코딩 하는 식으로 했는데 Yuvimage 객체 를 이용해 사이즈를 변환 해 보려구 했지만 jpeg 로 압축하지 않으면 800X480에서 변하질 않더군여;; 352X288 로는 어떻게 변환 하신건가요?
    사이즈가 줄어들면 인코딩 시간이 줄어들까 싶어서..
    • Camera 객체를 open 한 이후에 여러가지 세팅을 하고(set 계열 메소드들을 호출함으로써) 최종적으로 startPreview( ) 메소드를 호출 하여 카메라 콜백을 하셨을 텐데요.

      그 세팅 과정에서 다음과 같이 파라메터를 카메라에 넣음으로써 콜백함수의 byte[ ] 영상의 해상도를 설정 할 수 있습니다. (try catch 는 댓글의 간략화를 위해 생략했습니다)

      Camera mCamera = Camera.open();
      Camera.Parameters parameters = mCamera.getParameters();
      parameters.setPictureSize(width, height); // width 352, height 288 넣으면.
      mCamera.setParameters(parameters);

      ** 여기서 문제가 있습니다. **
      만약 setPictureSize 에 임의의 수치를 넣게 되면 예외가 발생할 수 있는데, 이 코드가 돌고 있는 안드로이드 폰이 지원하지 않는 해상도를 넣게 되면 예외가 발생 하게됩니다.

      넥서스S 는 352x288을 지원하지만(확실) 320x240을 지원하지 않고요.(확실)
      갤럭시S2는 320x240을 지원하지만(확실), 352x288을 지원 하지 않았던것 같습니다.(긴가민가)

      다음과 같이 현재 코드가 돌고 있는 폰이 지원하는 해상도를 List 형태로 얻을수 있습니다.

      // 카메라에서 찍을 수 있는 모든 사이즈를 List 형태로 얻음

      Camera mCamera = Camera.open();
      Camera.Parameters parameters = mCamera.getParameters();
      List<Size> sizes = parameters.getSupportedPictureSizes();
      Size optimalSize;
      optimalSize = getOptimalPreviewSize(sizes, IMAGE_WIDTH, IMAGE_HEIGHT);
      parameters.setPictureSize(optimalSize.width, optimalSize.height);
      mCamera.setParameters(parameters);

      getOptimalPreviewSize( ) 함수는 뭐.. 다음과 같이 구현 하면 될 것 같습니다.
      파라메터는 지원하는 해상도 목록 sizes, 원하는 해상도의 width와 height 를 넣고요.
      그러면, getOptimalPreviewSize(sizes, 352, 288 ) 이렇게 호출했을때 352x288 지원 불가 폰이라면 가장 인접한 해상도를 리턴해주던지 null을 리턴해 줄 겁니다. (아마도 말이죠)

      private Size getOptimalPreviewSize(List<Size> sizes, int width, int height) {
      final double ASPECT_TOLERANCE = 0.05;
      double targetRatio = (double) width / height;
      if (sizes == null) {
      return null;
      }

      Size optimalSize = null;
      double minDiff = Double.MAX_VALUE;

      int targetHeight = height;

      // Try to find an size match aspect ratio and size
      for (Size size : sizes) {
      double ratio = (double) size.width / size.height;
      if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) {
      continue;
      }
      if (Math.abs(size.height - targetHeight) < minDiff) {
      optimalSize = size;
      minDiff = Math.abs(size.height - targetHeight);
      }
      }

      // Cannot find the one match the aspect ratio, ignore the requirement
      if (optimalSize == null) {
      minDiff = Double.MAX_VALUE;
      for (Size size : sizes) {
      if (Math.abs(size.height - targetHeight) < minDiff) {
      optimalSize = size;
      minDiff = Math.abs(size.height - targetHeight);
      }
      }
      }
      Log.i("optimal size", ""+optimalSize.width+" x "+optimalSize.height); //for debugging
      return optimalSize;
      }

      그럼 좋은 결과 있기를 바랍니다 ^^
    • 밀레니안
    • 2012.10.13 01:44
    헐...완전 자세한 답변에 감동했네요.. 감사합니다 (__)

    이걸로 인코딩 속도는 무조건 줄어들겠죠

    그리고 찾아보니 저랑 같은 곳에 소속되어 있는 곳이 있더군요^^; 지역은 틀리지만...

    반갑습니다. ㅋㅋ

    이 프로젝트 진행중이시던데 좋은 결과 얻길 바라겠습니다.
    • 아! 어느 지역에 계신가요?
      저는 곧 이 프로젝트 마감이라 한창 준비하고 있습니다ㅎ.
      아무튼 무척 반갑습니다. ^^
      해상도를 줄이면 아무래도 인코딩에 소요되는 시간은 줄어들거 같네요. 화이팅 입니다 ~
    • 밀레니안
    • 2012.10.14 21:51
    부산이에요..ㅎ

    15초짜리 20초에서 15초짜리 14초 정도로 줄어들었네요 ㅎㅎ 폰을 좀더 좋은걸 쓴다면 사용 하능 할 것 같아요 ^^

    도움 감사합니다.
    Dev고양이님도 화이팅이요~

[안드로이드] 안드로이드 API GPS 상태체크

by Blogger 하얀쿠아
2011. 12. 4. 18:39 소프트웨어 Note/Android

안드로이드 API GPS 상태체크


지도를 비롯하여, 안드로이드 기기의 위치정보를 이용한 서비스 개발을 할 때, 일반적으로 기기의 현재 위도(latitude), 경도(longitude) 값이 필요하다.

기기의 현재 위,경도 값을 알기 위해서는, 사용자가 위치정보 사용을 동의해야 한다.
만약 동의하지 않은 상태에서 위치 획득 관련 API호출한다면,  exception을 던지게 되어있다.

만약 아래와 같은 UX flow를 구현하려 한다면, 아래 코드를 참고하자.

GPS 사용동의 여부를 체크 후  
    if 미동의 : 'GPS 사용 동의 설정 화면으로 이동 하겠는가?' 다이얼로그 출력. 
         if 다이얼로그로 부터 '이동한다' 입력받을 경우 : GPS 설정 화면으로 이동.

 아래는 그 기능의 예제코드이다.


@Override
 public void onCreate(Bundle savedInstanceState) {
	 ...
	 String context = Context.LOCATION_SERVICE;
	 locationManager = (LocationManager)getSystemService(context);
	 if(!locationManager.isProviderEnabled(LocationManager.GPS_PROVIDER)) {
		alertCheckGPS();
  }
	 ...
 }

private void alertCheckGPS() {
	 AlertDialog.Builder builder = new AlertDialog.Builder(this);
	 builder.setMessage("Your GPS is disabled! Would you like to enable it?")
			 .setCancelable(false)
			 .setPositiveButton("Enable GPS",
					 new DialogInterface.OnClickListener() {
						 public void onClick(DialogInterface dialog, int id) {
							 moveConfigGPS();
						 }
				 })
			 .setNegativeButton("Do nothing",
					 new DialogInterface.OnClickListener() {
						 public void onClick(DialogInterface dialog, int id) {
							 dialog.cancel();
						 }
				 });
	 AlertDialog alert = builder.create();
	 alert.show();
 }

// GPS 설정화면으로 이동
private void moveConfigGPS() {
	Intent gpsOptionsIntent = new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS);
	startActivity(gpsOptionsIntent);
}



이 댓글을 비밀 댓글로

[안드로이드] 이미 생성해둔(Sqlite) DB를 앱에서 Load하여 사용하기

by Blogger 하얀쿠아
2011. 8. 11. 00:27 소프트웨어 Note/Android

이미 생성해둔(Sqlite) DB파일을 앱에서 읽어들여 사용하기



약간 복잡한 기초 데이터를 위해 초기 실행시 Create Table 도 하고 insert도 할 수 있겠지만, 별도의 db 파일을 PC등에서 미리 만들고, 관리하며, 앱에서는 이 파일을 읽어들이기만 하여 (Load) 사용하고 싶다면 아래와 같은 방법을 쓰는 것도 한가지 해결책이 될 수 있다.

  1. db 파일을 assets에 넣는다.
  2. 아래 함수를 적당한 위치에 넣는다.


public static void initialize(Context ctx) {
	// check 
	File folder = new File(ROOT_DIR + "databases");
	folder.mkdirs();
	File outfile = new File(ROOT_DIR + "databases/" + DATABASE_NAME);
	if (outfile.length() <= 0) {
		AssetManager assetManager = ctx.getResources().getAssets();
		try {
			InputStream is = assetManager.open(DATABASE_NAME, AssetManager.ACCESS_BUFFER);
			long filesize = is.available();
			byte [] tempdata = new byte[(int)filesize];
			is.read(tempdata); 
			is.close();
			
			outfile.createNewFile();
			FileOutputStream fo = new FileOutputStream(outfile);
			fo.write(tempdata);
			fo.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}


완전한 코드는 아닌지라, copy & paste로 당장 본인의 앱에 적용을 하는건 불가능 할 수 있다. 하지만 '컨셉'을 이해하는 데는 도움이 될 것이라고 생각한다.

ROOT_DIR은 아래와 같이 선언되어 있다. 경로는 자신의 앱 패키지 경로로 수정하면 된다.

public static final String ROOT_DIR = "/data/data/com.gurucat.test/";


DATABASE_NAME은 assets에 복사해놓은 파일명이다. 이 경우 lecture.db가 되될것이다. 

내 경우에는 폴더를 따로 만들었다. 이를 위해 먼저 3,4번 라인에서 패키지 경로 밑에 database란 폴더를 생성합니다. mkdirs( ) 메서드는 이미 폴더가 존재한다면 실패한다.
5번 라인에서 파일을 읽어들인다.
6번 라인과 같이 파일의 길이가 0보다 작다는 것은 파일이 없다는 의미라고 가정한다. 따라서 파일을 복사하는 그 아래의 코드를 실행하게된다.
asset의 파일을 읽어오기 위해 7번 라인과 같이 assetManager를 이용해서 파일을 오픈한다.
나머지 내용들은 단순한 Java File I/O 코드이므로 더이상의 설명은 생략한다.


1MB 이상의 Database file을 asset에 넣을 때 몇가지 기억할 점

asset 폴더에 단일 파일의 크기가 1MB가 넘으면 안된다.
만약 1MB이상의 DB파일을 위와 같은 방법에 적용해야 한다면??

1MB 이상의 db 파일을, 여러개로 분할해서, assets 디렉토리에 넣어서 패키징하고, 배포한 후에, 런타임 시에 SD 카드로 복사하면서 파일을 합치면 된다.

근데 이런 식으로 하게되면 생기는 문제가 apk 파일 크기가 커지는 것이다. 이렇게 되면 사용자들로부터 '앱 용량이 크다'는 불만을 유발 할 수 있다. 이때는 큰 용량의 db 파일은 밖으로 빼서, 런타임시 인터넷에서 다운로드하여 SD 카드에 저장하는 방식을 적용하면 apk파일 크기가 커지는 문제를 회피할 수 있다. 이렇게 하려면, 웹서버가 있어야 하겠지만, 구글 프로젝트 호스팅이라는 서비스를 이용하면, 웹서버 없이도 다운로드 호스팅을 해결할 수 있을 것이다.


이 댓글을 비밀 댓글로

[안드로이드] 안드로이드에서 SQLite를 다뤄보자 (2)

by Blogger 하얀쿠아
2011. 8. 9. 05:11 소프트웨어 Note/Android



안드로이드에서 SQLite를 다뤄보자 (2)

안드로이드 앱 개발을 할 때, 없어서는 안될 존재. SQLite의 사용법에 대해 알아보겠다오늘은 아주 데이터베이스를 뿌리뽑아보도록 하자.

안드로이드는 모바일 환경에 알맞은 SQLite 데이터베이스를 채택하고 있다. 기본적으로 다른 데이터베이스와의 큰 차이는 없다. 물론, 완전히 같지는 않다.

다른 점 이라면, 일반적은 데이터베이스는 테이블 생성 시 각 속성에 대한 타입을 지정한다. 하지만 SQLite는 타입을 지정하는 것이 없다. 즉, int, string, text 등의 타입을 지정할 수가 없다는 말이다. 그리고 메모리와 속도면에서 소규모의 데이터베이스를 운영하는 데 있어서는 이점이 있다.

데이터베이스의 사용법은 기존의 데이터베이스, mySQL이나 Oracle 등 SQL을 사용하는 데이터베이스를 한번이라도 다루어 본적이 있다면  별다른 어려움 없이 사용할 수 있을 것이다. 처음 접하는 사람들 역시, 기존에 있는 샘플코드를 이용하여 조금만 수정해서 사용한다면, 별다른 어려움 없이 프로그램에 적용시키실 수 있을 것이다.

지금부터는 하나의 예제 코드를 가지고 구체적으로 안드로이드에서 SQLite를 사용하는 방법을 알아보겠다. 이 예제코드는 인터넷에 널리 퍼져 있는 코드를 정리한 것이다. 예제 코드를 따라가다보면 어느새 간단한 노트 기능의 앱을 만들게 될 것이며, 타이틀과 바디를 가지는 DB테이블, 그리고 이를 이용하여 데이터를 추가, 삭제, 업데이트 등을 수행할 수 있는 방법을 배우게 될 것이다.

   

Java Source Code 부분

 자바 코드는 크게 두가지로 나뉘어진다. 데이터베이스를 컨트롤 하는 객체와 이 객체를 사용하여 데이터베이스를 접근하는 엑티비티(Activity)이다.

 아래의 예제코드는 데이터베이스를 컨트롤 하는 객체, NotesDbAdapter, 이다. 이 클래스 내부에 DatabaseHelper라는 이름의 객체가 있고, 이 DatabaseHelper를 통해 데이터베이스를 관리한다. 이는 안드로이드에서 제공하는 SQLiteOpenHelper를 상속받아 간단히 만들 수 있다. DatabaseHelper 객체에는 크게 세 가지 메서드가 존재한다.

 생성자, onCreate, onUpdate 가 그것이다. 

이름 그대로 onCreate는 데이터베이스를 생성하는 시점에 호출되는 메서드로, 데이터베이스 이름과 버전 등을 설정할 수 있다. 이 메서드 안에서 쿼리문을 사용하여 데이터베이스의 테이블을 생성한다. 

onUpdate는 이름 그대로 업데이트가 필요할 시 수행이 된다. 현재의 데이터베이스 버전과 업데이트 하려는 데이터베이스의 버전을 비교하여, 낮은 버전일 경우 새롭게 테이블을 구성한다던가, 다른 조작 등을 취할 수 있다.


데이터베이스 관리 클래스

아래는 위에서 언급했던 데이터베이스를 컨트롤 하는 객체인 NotesDbAdapter의 Java코드이다.

  

import android.content.ContentValues; 
import android.content.Context; 
import android.database.Cursor; 
import android.database.SQLException; 
import android.database.sqlite.SQLiteDatabase; 
import android.database.sqlite.SQLiteOpenHelper; 
import android.util.Log; 

public class NotesDbAdapter {
    public static final String KEY_TITLE = "title"; 
    public static final String KEY_BODY = "body"; 
    public static final String KEY_ROWID = "_id"; 

    private static final String TAG = "NotesDbAdapter"; 
    private DatabaseHelper mDbHelper; 
    private SQLiteDatabase mDb;

    /**
    * Database creation sql statement
    */
    private static final String DATABASE_CREATE =
    "create table notes (_id integer primary key autoincrement, "
        + "title text not null, body text not null);";

    private static final String DATABASE_NAME = "data";
    private static final String DATABASE_TABLE = "notes";
    private static final int DATABASE_VERSION = 2;

    private final Context mCtx;

    private static class DatabaseHelper extends SQLiteOpenHelper {
        DatabaseHelper(Context context) {
            super(context, DATABASE_NAME, null, DATABASE_VERSION);
        }

        @Override
        public void onCreate(SQLiteDatabase db) {
            db.execSQL(DATABASE_CREATE);
        }

        @Override
        public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { 
            Log.w(TAG, "Upgrading database from version " + oldVersion + " to "
              + newVersion + ", which will destroy all old data");
            db.execSQL("DROP TABLE IF EXISTS notes");
            onCreate(db);
        }
    }

    public NotesDbAdapter(Context ctx) {
        this.mCtx = ctx
    }

    public NotesDbAdapter open() throws SQLException {
        mDbHelper = new DatabaseHelper(mCtx);
        mDb = mDbHelper.getWritableDatabase();
        return this;
    }

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

    public long createNote(String title, String body) {
        ContentValues initialValues = new ContentValues();
        initialValues.put(KEY_TITLE, title);
        initialValues.put(KEY_BODY, body);

        return mDb.insert(DATABASE_TABLE, null, initialValues);
    }

    public boolean deleteNote(long rowId) {
        Log.i("Delete called", "value__" + rowId);
        return mDb.delete(DATABASE_TABLE, KEY_ROWID + "=" + rowId, null) > 0;
    }

    public Cursor fetchAllNotes() {
        return mDb.query(DATABASE_TABLE, new String[] { KEY_ROWID, KEY_TITLE,
            KEY_BODY }, null, null, null, null, null);
    }

    public Cursor fetchNote(long rowId) throws SQLException {
        Cursor mCursor = mDb.query(true, DATABASE_TABLE, new String[] { KEY_ROWID, KEY_TITLE,
            KEY_BODY }, KEY_ROWID + "=" + rowId, null, null, null, null,
            null);
        if (mCursor != null) {
            mCursor.moveToFirst();
        }
        return mCursor;
    }

    public boolean updateNote(long rowId, String title, String body) {
        ContentValues args = new ContentValues();
        args.put(KEY_TITLE, title);
        args.put(KEY_BODY, body);
        return mDb.update(DATABASE_TABLE, args, KEY_ROWID + "=" + rowId, null) > 0;
    }
}


 그 밖에는 함수명에서도 볼 수 있듯이 데이터를 추가, 삭제, 업데이트하는 기능이 있다. 기존의 데이터베이스와 다른 점은 쿼리문을 이용하지 않고 데이터를 조작이 가능하다는 것이다. ContentValues 라는 type을 이용하여 기존에 있는 테이블의 속성명과 조작하려는 인스턴스를 넣어 한꺼번에 데이터베이스로 요청할 수 있다. Insert( ), Update( ) 와 같은 데이터베이스 객체 내에 있는 메서드를 이용하여 쿼리문 없이 데이터베이스 조작이 가능하다. 이러한 함수는 내부에서 직접 쿼리문을 만들어 데이터베이스를 조작한다.

자신이 사용하고 싶은 테이블을 구성한 뒤에, 이 코드를 자신에 맞는 테이블로 바꾸시면 큰 어려움 없이 데이터베이스를 이용하실 수 있을 것이다.

 

 Cursor의 메서드, 사용방법

moveToFirst

커서가 쿼리(질의) 결과 레코드들 중에서 가장 처음에 위치한 레코드를 가리키도록 한다.

moveToNext

다음 레코드로 커서를 이동한다.

moveToPrevious

이전 레코드로 커서를 이동한다.

getCount

질의 결과값(레코드)의 갯수를 반환한다.

getColumnIndexOrThrow

특정 필드의 인덱스값을 반환하며, 필드가 존재하지 않을경우 예외를 발생시킨다

getColumnName

특정 인덱스값에 해당하는 필드 이름을 반환한다.

getColumnNames

필드 이름들을 String 배열 형태로 반환한다.

moveToPosition

커서를 특정 레코드로 이동시킨다.

getPosition

커서가 현재 가리키고 있는 위치를 반환한다.

 

안드로이드 SQLite에서는 테이블에서 하나의 '레코드'를 읽어 오기 위해서 커서(cursor)라는 것이 필요하다. 조건에 맞는 레코드를 한꺼번에 모두 들고 올 수가 없기 때문에 커서를 조작해서 각각의 레코드에 접근 한다. 

커서라는 것을 이해할 때, 이름 그대로 '현재 레코드를 가리키고 있는 곳'이라고 생각하면 쉽다. 마우스 커서가 한 지점을 가리키는 것 처럼 말이다. 이 커서를 조작하여 각 레코드 사이를 이동하면서 데이터를 접근한다. 그리고 이 커서 객체를 이용하여 get( ) 메서드를 호출하여 컬럼 번호에 상응하는 데이터를 가져올 수 있다.

 


데이터베이스를 사용하는 사용자 화면 - 액티비티 소스코드

위에서 만든 데이터베이스를 관리하는 클래스의 인스턴스를 만들고 사용하는, 액티비티 소스코드를 살펴보겠다.

이 코드가 앱의 화면을 구성한다.


   

import android.app.Activity;
import android.database.Cursor;
import android.os.Bundle;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;

public class DatabaseTestActivity extends Activity {
    private NotesDbAdapter dbAdapter;
    private static final String TAG = "NotesDbAdapter";

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        Log.d(TAG, "DatabaseTest :: onCreate()");
        dbAdapter = new NotesDbAdapter(this);
        dbAdapter.open();

        Button bt = (Button)findViewById(R.id.inputButton);
        bt.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                dbAdapter.createNote("title", "body");
                TextView tv = (TextView)findViewById(R.id.textView1);
                tv.setText("데이터베이스에 넣었습니다.");
                TextView tv1 = (TextView)findViewById(R.id.textView2);
                tv1.setText("Title과 Body를 데이터베이스에 저장하였습니다.");
                Log.d(TAG, "First Button Click");
            }
        });

        Button bt1 = (Button)findViewById(R.id.outputButton);

        bt1.setOnClickListener(new View.OnClickListener() {
            public void onClick(View v) {
                Cursor result =    dbAdapter.fetchAllNotes();
                result.moveToFirst();
                while(!result.isAfterLast()){
                  String title = result.getString(1);
                  String body = result.getString(2);
                  TextView tv = (TextView)findViewById(R.id.textView1);
                  tv.setText(title);
                  TextView tv1 = (TextView)findViewById(R.id.textView2); 
                  tv1.setText(body);
                  result.moveToNext();
                }
                Log.d(TAG, "Second Button Click");
                result.close();
            }
        });
    }
}

  위의 코드는 실제 엑티비티에서 데이터베이스를 이용하는 것을 나타내는 소스코드이다. 위에서 살펴본 NotesDbAdapter의 인스턴스를 생성한 후, open( ) 메서드를 호출 한 뒤 사용하면 된다. 사용하는 법은 너무나도 직관적이라 생략하겠다. while문 내에서 커서를 순회하며 테이블에 있는 모든 레코드를 가져와서 화면에 뿌려주는 것을 수행하는 코드이다.  

   

XML Code

XML 로 레이아웃을 잡을 때, 간단히 테스트할 수 있는 TextView를 두 개만 생성하고 있다.  

   

AndroidManifest.xml

 데이터베이스를 사용하기 위해서 추가해야 하는 Permission은 없다. 즉, AndroidManifest.xml 의 수정은 필요하지 않다.

   

 

마무리 - SQLite 데이터베이스 사용하기

데이터베이스를 사용하기 위해서는 안드로이드에서 제공하는 SQLiteOpenHelper를 이용하여 간단히 데이터베이스를 조작할 수 있다. 예제에서는 이 것을 상속받아 객체를 만들고 이 객체를 자신의 데이터베이스에 맞게 조작하도록 클래스(NotesDbAdapter)를 만들고, 엑티비티에서는 이 NotesDbAdapter 클래스의 인스턴스를 하나 만들고, 이를 통해 데이터베이스를 직접 사용할 수 있게 하였다. 간단한 예제 코드를 이용하여 자신의 데이터베이스 테이블에 맞게 수정하여 사용하면 간단히 데이터를 저장할 수 있는 환경을 만들 수 있다.



이 댓글을 비밀 댓글로

[안드로이드] 안드로이드에서 SQLite를 다뤄보자 (1)

by Blogger 하얀쿠아
2011. 8. 9. 05:01 소프트웨어 Note/Android

안드로이드에서 SQLite를 다뤄보자 (1)



안드로이드 플랫폼은 Database를 위해서 플랫폼 자체적으로 SQLite를 기본 탑재하고 있다.

우선 SQLite의 특징을 살펴보자.


  • SQLite는 다른 프로그램에 임베딩하기 좋으면서도 깔끔한 SQL 인터페이스를 제공한다.
  • 메모리도 적게 사용하면서 속도도 빠르다.
  • 실행파일과 소스 코드가 무료이고 공개되어 있기 때문에 많이 사용된다.
  • 위와 같은 이유로, 안드로이드는 SQLite를 기본탑재하여 내장하고 있으며, 그결과 모든 안드로이드 애플리케이션은 간단하게 SQLite 데이터베이스를 생성해 활용할수 있게 됐다.
  • SQLite는 표준 SQL 인터페이스를 사용한다. -> SQLite는 SQL 문법에 맞는 명령을 통해 데이터를 가져오거나(SELECT) 데이터를 변경하고(INSERT 등) 데이터 구조를 정의하는(CREATE TABLE 등) 작업을 처리한다.
  • SQLite가 JDBC를 기본 API로 제공하지 않으며, 휴대폰과 같은 환경에서 JDBC와 같은 규모 있는 프레임워크는 무리가 되기 때문이다.


참고로 안드로이드의 액티비티는 일반적으로 컨텐트 프로바이더(Content Provider)나 서비스(Service) 등을 통해 데이터베이스에 접근한다.


또한 SQLite의 재미있는 특징은 실제 데이터를 추가할 때 '컬럼마다 데이터 타입에 상관없이 어떤 데이터라도 마음대로 넣을수 있다'는 것이다. 예를 들어 INTEGER로 정의된 컬럼에 문자열 값도 아무런 문제없이 넣을수 있다. 이와 같은 기능을 매니페스트 타입이라고 표현한다.

 매니페스트 타입 입장에서 보면 데이터 타입은 컬럼 자체가 아닌 개별값에 연결되는 속성이다. 따라서 SQLite는 애초에 해당 컬럼에 지정된 데이터 타입과 상관없이 어떤 데이터 타입의 어떤 값이라도 아무 컬럼에나 집어 넣을수 있는 컨셉이다.

그런데 표준 SQL 구문에 정의된 기능 중에 SQLite가 지원하지 않는 기능이 몇가지 있다. 기억해두자. 

FOREIGN KEY, 중첩 트랜잭션, RIGHT OUTER JOIN, FULL OUTER JOIN, ALTER TABLE 은 SQLite가 지원하지 않는다


코드에 적용하기 위한 기초

데이터베이스를 생성하고 오픈하려면 SQLiteOpenHelper 객체를 사용한다. SQLiteOpenHelper 클래스는 애플리케이션에서 요구하는 내용에 따라 데이터베이스를 생성하거나 업그레이드하는 기능을 제공한다. SQLiteOpenHelper 클래스를 상속받아 구현하려면 다음과 같은 세가지 기능을 준비해야한다


  1. 생성 메소드 : 상위 클래스의 생성 메소드를 호출, Activity 등의 Context 인스턴스와 데이터베이스의 이름, 커서 팩토리(보통 Null 지정) 등을 지정하고, 데이터베이스 스키마 버전을 알려주는 숫자값을 넘겨 준다.
  2. onCreate() 메소드 : SQLiteDatabase를 넘겨 받으며, 데이블을 생성하고 초기 데이터를 추가하기에 적당한 위치이다.
  3. onUpgrade() 메소드 : SQLiteDatabase 인스턴스를 넘겨 받으며, 현재 스키마 버전과 최신 스키마 버전 번호도 받는다.


 SQLiteOpenHelper를 상속받은 클래스를 사용하려면 먼저 인스턴스를 하나 생성한 다음, 하려는 작업이 읽기 전용인지 여부에 따라 getReadableDatabase()나 getWritealbleDatabese() 메소드를 호출 해야 한다.

아래 예제코드를 살펴보길 바란다.

db = (new DatabaseHelper(getContext())).getWritableDatabase();
return (db == null) ? false : ture;

결과적으로 db 변수에 SQLiteDatabase 인스턴스를 받아오게 되는데, SQLiteDatabase 인스턴스를 사용해 데이터를 호출하거나 내용을 변경 할수 있다. 그리고 액티비티가 종료되는 등 데이터베이스를 모두 사용하고 나면 SQLiteDatabase인스턴스의 close() 메소드를 호출해 연결을 해제하면 된다.


DB테이블 준비

본격적으로 SQLite로 DB를 다루기 위해서는 DB테이블 준비해야 한다. 

테이블과 색인 등을 생성하려면 생성하길 원하는 DB Scheme에 대한 DDL 구문을 준비해서 SQLiteDatabase 인스턴스의 execSQL() 메소드의 인자로 넣어 호출하면 된다.

다음 예제코드를 살펴보자.

db.execSQL("CREATE TABLE constants (_id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT, value REAL);");

위 예제코드의 의미는 아래와 같다. 

  1. constants라는 테이블이 생성되고, constants 테이블 기본키로 _id 컬럼이 사용되며 _id 컬럼은 정수형의 숫자값이 자동으로 증가되는 컬럼이다.
  2. 실제 데이터는 title이라는 문자열 컬럼과 value라는 실수형 컬럼에 들어간다.


안드로이드의 SQLite는 키본키(Primary key)에 해당하는 컬럼에 대해서는 자동으로 색인(index)을 생성한다. 그리고 다른 컬럼에도 색인이 필요하다면 CREATE INDEX 구문으로 색인을 걸어 준다. 알아두자.

그리고 만약 테이블이나 색인을 제거해야 하는 상황이라면 DROP INDEX나 DROP TABLE 구문을 execSQL()로 실행하면 된다.


데이터 추가

위에서 생성한 DB테이블에 데이터를 추가하는 방법은 2가지가 있다.


첫번째는 execSQL( ) 메소드를 사용하는 방법이다. 이는 값을 가져오는 문장이 아닌 INSERT, UPDATE, DELETE등의 모든 SQL 구문을 String 타입의 인자로 넣어 처리할 수 있다. 아래와 같이 말이다.

db.execSQL("INSERT INTO widgets (name, inventory)" + "VALUES ('Sprocket', 5)");


두번째는 SQLiteDatabase 클래스에서 제공하는 insert( ), update( ), delete( ) 등의 개별 메소드를 사용하는 방법이 있다. 이와같은 개별 메소드는 인자로 입력받은 값을 조합해 최종적으로 SQL 문장을 동일하게 실핼하도록 만들어져 있다. 개별 메소드는 Map과 비슷한 구조로 만들어져 있으면서 SQLite의 데이터 타입에 맞춰 동작하도록 구성된 ContentValues 객체를 사용해 동작한다.

그리고 지정한 키에 해당하는 값을 찾아올때는 단순하게 get() 메소드를 사용하는 대신, getAsInteger( ), getAsString( ) 등의 메소드를 호출하면 된다.

insert( ) 메소드는 대상이 되는 테이블 이름, null 처리 컬럼명, ContentValues 객체에 컬럼별 값을 넣어 인자로 넘겨 준다.

참고로, 안드로이드의 SQLite는 값이 하나도 없는 행은 허용하지 않는다. 따라서 ContentValues 인스턴스 값이 하나도 없는 경우 행이 생성되지 않기 때문에 이런경우 null 처리 컬럼 이름으로 지정된 컬럼값으로 NULL을 지정해 행이 생성되게 해야 한다. 

contentValues cv = new ContentValues();
cv.put(ContentValues.TITLE, "Gravity, Death Star I");
cv.put(ContentValues.VALUE, SensorManager.GRAVITY_DEATH_STAR_I);
db.insert("constants", getNullColumnHack(), cv);

update() 메소드는 대상 테이블 이름과 변경할 값이 들어 있는 ContentValues 객체를 넘겨준다. 그리고 값을 변경할 대상을 한정지으려면 WHERE 구문과 함께 WHERE 조건에 해당하는 값 역시 넘겨주면 된다. WHERE 구문은 물음표로 지정된 위치에 각자의 값이 배치돼 처리된다.

update() 메소드는 다른 정보를 사용해 계산된 값이 아닌 고정된 값을 갖는 컬럼만 변경할 수 있으므로, 필요한 경우에는 execSQL() 메소드를 사용해야 할 수도 있다. WHERE 구문에 표시된 물음표와 각 조건값을 지정하는 방법은 다른 SQL API에서 많이 사용하던 방법과 별반 다르지 않다.

// replacements는 ContentValues 인스턴스
String[] parms = new String[] {"snicklefritz"};
db.update("widgets", replacements, "name=?", parms);


delete() 메소드 역시 테이블 이름과 WHERE 구문을 사용한다. 변경할 값을 지정하지 않는다는 점만 제외하면 update()와 동일한 방법으로 동작한다.


데이터 불러오기

INSERT, UPDATE, DELETE와 비슷하게 SELECT 구문으로 데이터를 가져올때도 2가지 방법을 사용할 수 있다.

첫번째는 rawQuery( ) 메소드를 사용해 SELECT 구문을 직접 실행하는 방법이고, 두번째 방법은 query( ) 메소드의 인자로 각 부분의 값을 넘겨 실행하는 방법이다. SQLiteQueryBuilder 클래스와 관련된 부분과 커서와 커서 팩토리에 관한 부분이 가장 복잡하다.



SQL문 직접 작성 하는 방법을 좀더 살펴보자. API 호출 방법만 놓고 보면 rawQuery() 메소드를 사용하는 방법이 가장 간단하다. rawQuery() 메소드에 SELECT 구문을 인자로 넘겨 주기만 하면 되기때문이다.

SELECT 구문 역시 위치에 맞는 인자 배열을 함께 넘겨 줄수 있다. 아래 예제 코드를 살펴보자.

 

Cursor c = db.rawQuery("SELECT name FROM sqlite_master WHERE type='table' AND name='constants'", null);


위의 코드는 SQLite의 기본 테이블 가운데 하나인 sqlite_master 테이블 내용을 가져오는데, 내용으로 보면 constants라는 이름의 테이블이 만들어져 있는지 확인하는 구문이다. 결과로 받아오는 값은 Cursor 인스턴스인데, Cursor를 사용하면 여러 건의 결과를 하나씩 받아오면서 처리할수 있다. SELECT 문장이 동적으로 변경되지 않고 아예 프로그램 내부에 고정시켜버릴 예정이라면 위 방법이 가장 간단하다.

SELECT 구문 가운데 이부분이 동적으로 변경되거나, 위치로 인자값을 지정하는 방법으로 한계가 있는 수준이라면 굉장히 복잡해진다. 예를 들어 값을 가져와야 할 컬럼 개수가 개발 당시에 정해지지 않고 동적으로 변경된다면 처리하기 쉽지 않다. 그렇다고 해서 컬럼 이름을 필요할 때마다 쉼표로 연결해 사용하는건 물론 좋은 방법이 아니다. 이런 경우에는 query() 메소드를 사용하는게 훨씬 간편하다.



이번엔 일정한 형식의 쿼리를 위한 query( ) 메소드를 사용하는 방법을 좀 살펴보자.

query() 메소드는 SELECT 구문의 각 부분을 분할하여 각 인자로 넘겨받고, 최종적으로는 SELECT문을 생성해 실행한다.

query() 가 받아서 처리하는 인자의 순서는 아래와 같다.


  1. 대상 테이블 이름
  2. 값을 가져올 컬럼 이름의 배열
  3. WHERE 구문. 물음표를 사용해 인자의 위치를 지정할 수 있다.
  4. WHERE 구문에 들어가는 인자값
  5. GROUP BY 구문
  6. ORDER BY 구문
  7. HAVING 구문

테이블 이름을 제외한 각 값이 필요없는 경우라면 null을 지정하도록 한다.

String[] columns={"ID", "inventory"};
Steing[] parms={"snicklefritz"};
Cursor result=db.query("widgets", columns, "name=?", parms, null, null, null);

 

쿼리 구문 생성을 위한 자세한 방법도 알아보자.

SQLiteQueryBuilder 클래스를 활용하면 훨씬 다양한 방법으로 UNION이나 하위 쿼리 등을 포함하는 복잡한 구문을 생성할 수 있다. SQLiteQueryBuilder 클래스가 ContentProvider 인터페이스와 완벽하게 맞아 떨어진다. 컨텐트 프로바이더의 query() 메소드를 구현하는 가장 일반적인 방법은 SQLiteQueryBuilder 인스턴스를 생성하고 기본값을 일부 채워넣은 다음 전체 쿼리를 생성하고 실행할수 있게 구성하는 것이다. 


SQLiteQueryBuilder 클래스를 사용해 요청을 처리하는 컨텐츠 프로바이더의 예제 코드를 살펴보면서 이해해보자.

@Override
public Cursor query(Uri url, String[] projection, String selection, String[] selectionArgs, String sort) {
	SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
	qb.setTables(getTableName());
	if (isCollectionUri(url)){
		qb.setProjectionMap(getDefaultProjection());
	} else {
		qb.appendWhere(getIdColumnName()+"="+url.getPathSegments().get(1));
	}

	String orderBy;

	if (TextUtils.isEmpty(sort)){
		orderBy = getDefaultSortOrder();
	} else {
		orderBy = sort;
	}

	Cursor c = qb.query(db, projection, selection, selectionArgs, null, null, orderBy); 
	c.setNotificationUri(getContext().getContentResolver(), url);
	return c;
}


위 코드는 아래와 같은 일을 한다.

  1. SQLiteQueryBuilder 인스턴스를 생성함.
  2. 쿼리에 사용할 테이블 이름을 설정함 -> setTables(getTableName( ))
  3. 값을 가져올 기본 컬럼 이름의 목록을 지정하거나 (setProjectionMap( )), 또는 Uri값에 들어 있는 ID값으로 테이블 항목 가운데 특정한 값을 가져올 수 있도록 WHERE 구문을 추가했다. -> (appendWhere( ))
  4. 마지막으로 기본값과 요청이 들어온 값을 조합해 생성된 쿼리 구문을 실행한다. -> qb.query(db, projection, selection, selectionArgs, null, null, orderBy)

 SQLiteQueryBuilder에서 쿼리를 직접 실행하는 대신 buildQuery( ) 메소드를 호출해 최종 생성된 SELECT 구문만을 리턴하게 할 수도 있다. 그러면 넘겨 받은 SELECT문을 필요할때 실행할수 있다.


커서 활용

안드로이드 SQLite에서는 SELECT 쿼리를 어떻게 실행하건 간에 그 결과로는 Cursor 인스턴스를 결과로 반환 받게된다. 커서 개념을 안드로이드와 SQLite에서 구현한 클래스가 바로 Cursor다. 

이 Cursor의 대표적인 메소드 몇가지를 잠깐 살펴보자.


  • getCount() 메소드 : 전체 결과 건수가 몇개인지 확인할 수 있다.
  • moveToFirst(), moveToNext(), isAfterLast() 등의 메소드 : 결과건을 모두 확인할수있다.
  • getColumnNames() 메소드 : 결과에 포함된 전체 컬럼 이름을 알수 있다.
  • requery() 메소드 : 쿼리를 재실행 할수 있다.
  • close() 메소드 : 커서가 확보한 자원을 모두 해제한다.

 

아래 예제코드를 보자. widgets 테이블에 있는 항목을 모두 가져온 후, 반환받은 Cursor의 인스턴스로부터 모든 결과값을 뽑아내는 반복문 예제이다.

Cursor result = db.rawQuery("SELECT ID, namem inventory FROM widgets");
result.moveToFirst();
while (!result.isAfterLast()){
	int id = result.getInt(0);
	String name = result.getString(1);
	int inventory = result.getInt(2);
	//실제 필요한 작업 처리
	result.moveToNext();
}
result.close();


그런데 때때로 기본적으로 제공하는 Cursor 인스턴스 대신 Cursor를 상속받아 새로운 커서를 구현해야 할 피요가 있을 수도 있다. queryWithFactory() 메소드나 rawQueryWithFactory() 메소드에 SQLiteDatabase.CursorFactory 인스턴스를 인자로 넘겨 사용한다.

CursorFactory 클래스는 newCursor() 메소드가 구현된 내용에 따라 새로운 Cursor를 생성한다. 그런데 다행인 것은 일반적인 안드로이드 애플리케이션을 개발하고 있다면 커서를 새로 구현해야할 일이 많지는 않다는 것이다.


데이터 직접 다루기

안드로이드 SDK 에뮬레이터에는 sqlite3 프로그램이 포함되어 있고, adb shell 명령을 통해 실행해 사용할수있다. 에뮬레이터의 adb shell에 접속한 다음 sqlite3명령을 실행하면서 데이터베이스 파일이 위치한 경로를 함께 지정해 주면 된다.

데이터베이스 파일의 일반적인 위치는 '/data/data/your.app.package/database/your-db-name' 와 같다.


  • your.app.package 부분은 애플리케이션이 들어 있는 자바 패키지 명을 의미한다.
  • your-db-name 부분은 createDatabase() 명령을 실행할 때 지정했던 데이터베이스 이름을 넣는다.


sqlite3 프로그램은 충분한 기능을 갖추고 있으며, 콘솔 화면에서 데이터베이스를 다루는게 익숙하다면 adb shell도 괜찮은 방법이다.

그렇지만 만약 콘솔 인터페이스가 익숙치 않다면, GUI를 갖춘 화면이 필요할 수 있다. 위의 특정 경로에 보관되어 있는 데이터베이스 파일을 개발에 사용중인 Host PC(데스크탑, 안드로이드 SDK가 설치된 PC)으로 복사한 다음,  SQLite 데이터베이스 파일을 인식하는 다양한 프로그램을 활용해 데이터를 조회하고 다룰 수 있다. 이때는 adb pull을 사용하거나, 안드로이드 SDK의 에뮬레티어 file explorer를 활용하여 꺼낼 수 있다. 바깥으로 불러낸 복사본에 변경 작업을 진행했다면 변경된 데이터베이스 파일을 다시 기기(에뮬레티어)에 업로드해야 반영된다.

데이터베이스 파일을 기기에서 뽑아내려면 콘솔 (윈도우라면 cmd 혹은 동등한 커맨드라인 툴, 리눅스나 맥이라면 shell)에서 adb pull을 사용하여, 원본 경로와 대상 디렉토리 등을 적어주어 파일을 복사할 수 있으니 참고하자. 또한 변경 완료된 데이터베이스 파일을 기기에 업로드 하려면 adb push 명령을 사용한다. adb push도 adb pull과 마찬가지로 원본 파일 경로와 대상 디렉토리 등을 알려줘야 한다.

 일반적으로 가장 많이 사용되는 SQLite 클라이언트 프로그램은 파이어폭스 브라우저의 확장 기능으로 구현돼있는 SQLite Manager이다. 파이어 폭스 확장 기능이기 때문에 운영체제 플랫폼에 상관없이 어디서든 사용할 수 있다는 장점이 있다.



이 댓글을 비밀 댓글로
  1. 어.. 형 ㅋㅋㅋㅋ 구글링 하다가 들어왔어요. ㅋㅋ 잘 보고 갑니다~~

[안드로이드] 안드로이드 SurfaceView 캡쳐 예제

by Blogger 하얀쿠아
2011. 7. 4. 09:36 소프트웨어 Note/Android

안드로이드 SurfaceView 캡쳐 예제


다음과 같은 상황이라고 가정하자.

아이템의 사진을 찍는 앱이 있는데, 이 아이템들은 ActivityScreen의 특정한 고정 된 영역에서 볼 수 있어야 한다는 요구사항 있다.
문제는 저장된 비트맵이 액티비티에 표시될 때 인데, 카메라로 보고 찍은 것과는 보여지는게 다르다. 줌을 땡겨서 사진을 찍을 때, 프리뷰 화면에서는 보여지지 않았던 영역까지 액티비티에 그려진다.

해결을 위한 주요 코드 줄은 다음과 같다.


이미지 캡쳐 (IMAGE CAPTURE)
surfHolder.setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
captImageView = (ImageView)findViewById(R.id.CamImageView);
byte [] photoData;
Camera cam;
SurfaceHolder surfHolder;
SurfaceView surfView;
Camera.PictureCallback cback;

cback = new Camera.PictureCallback() {
	@Override
	public void onPictureTaken(byte[] data, Camera camera) {
		final int length = data.length;
		opts.inSampleSize = 4;
		final BitmapFactory.Options opts = new BitmapFactory.Options();
		Bitmap bMap = null;

		try {
			bMap = BitmapFactory.decodeByteArray(data, 0, length, opts);
		} catch(OutOfMemoryError e) {
			// OutOfMemoryError handle
		} catch(Exception e) {
			// Exception handle
		}

		captImageView.setImageBitmap(bitmap);

		if(bMap==null) {
			cam.startPreview();
		} else {
			savePhotoButton.setVisibility(ImageView.VISIBLE);
			takePhotoButton.setVisibility(ImageView.INVISIBLE);
			photoData = data;
		}
	}
}

이미지 저장 (IMAGE SAVE)
captImageView.setImageBitmap(null);
// SAVE THE PICTURE   THIS SAVES PICTURE IN WRONG SIZE.  LOOKS LIKED ZOOMED IN NOT WHAT WAS PREVIEWED!!!

public void photoSave(byte[] data) {
	final int length = data.length;
	final BitmapFactory.Options options = new BitmapFactory.Options();
	options.inSampleSize = 2;

	try {
		Bitmap bMap = BitmapFactory.decodeByteArray(data, 0, length, options);
		int quality = 75;
		File file = new File(getDir(), fname);
		fileOutputStream = new FileOutputStream(file);
		bMap.compress(CompressFormat.JPEG, quality, fileOutputStream);
	} catch(FileNotFoundException e) {
		// Handle FileNotFoundException 
	} catch(OutOfMemoryError e) {
		// Handle OutOfMemoryError
	} 
}


액티비티에 이미지 보이기(IMAGE DISPLAY as DRAWABLE in ACTIVITY)

사진이 찍히면 언제나 미리보기에서 보이지 않는 확대 또는 축소 된 부분으로 이미지가 표시되곤 했었다.
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = inSampSize;
Bitmap bitmap = BitmapFactory.decodeFile(path, options);

Drawable drawable = bitmap;
ImageView pictureView = (ImageView)findViewById(R.id.pictureViewer);
pictureView.setImageDrawable(drawable);


이 댓글을 비밀 댓글로
  1. 구찌 핸드백은 90 년 긴 역사를 온 세계의 가장 유명한 핸드백의 하나입니다.
  2. 패션을 가진 사람과 유명 인사들은 슬로프와 레드 카펫에 Moncler를 착용하십시오.

[안드로이드] Push Notification 구현

by Blogger 하얀쿠아
2011. 6. 22. 17:04 소프트웨어 Note/Android

[안드로이드] Push Notification 구현


안드로이드에서 Push notification을 구현하기 위한 방법으로 XMPP를 이용하는 방법과 MQTT를 이용하는 방법이 있다. 
여기서는 MQTT를 이용하여 C2DM을 지원하지 않는 안드로이드 2.1이하 버전에서 PUSH notification을 지원하기 위한 방법에 대해 설명한다.

안드로이드 앱에서 Push notification을 지원하기 위한 방안은 3가지정도의 방안이 있다.

폴링

이게 진정 push일까?? 어쨌든 단말에서 주기적으로 서버에 가져갈 메세지가 있는지 확인하여 push event를 수신할 수 있다.

  장점 : 구현이 쉽고 비용도 안든다.
  단정 : 실시간이 아니지 않은가... 게다가 이는 배터리소모까지 발생시킨다.. 끔찍하다. 이에 대한 정보는 이 링크를 참조하자. 



SMS

  안드로이드는 SMS message의 가로채기가 가능하다. 서버에서 특별한 SMS를 단말에 날리면 앱에서는 모든 SMS 메세지를 가로채서 서버에서 날린것인지 확인하고 Popup Message를 띄울 수 있을것이다.

  장점 : 구현이 쉽다. 완전한 실시간이 보장된다. 알려진 솔루션도 있다. Ericsson lab의 자료를 참조하라 -> https://labs.ericsson.com/apis/mobile-java-push/
  단점 : SMS 발송 시, 건당 비용이 발생한다.


끊김없는 TCP/IP(persistent TCP/IP)

폰과 서버가 TCP/IP 연결을 유지한다. 그리고 주기적으로 keepalive메세지를 날린다. 서버는 이 연결을 통해 필요할경우 메세지를 보낸다.

  장점 : 완전한 실시간이 보장된다.
  단점 : 신뢰성을 보장하는 구현이 아주 까다롭다. 폰과 서버쪽 모두 구현하려면 이야기가 복잡해진다. 또 안드로이드는 low memory등의 상황에서 서비스가 종료될 수도 있다. 그러면 동작을 안할것이다. 또한 단말이 sleep에 들어가지 않아야 하므로 유저들은 배터리문제로 불만을 제기하거나, 소리없이 당신의 앱을 삭제해버릴 수 있다.


1, 2의 방법은 중대한 단점이 있다. 3번째는 가능하기는 하다. 하지만 역시 개운치는 않다.

구글링을 통해서 몇몇 개발자들의 TCP/IP방식의 몇가지 좋은 시도를 찾았다.

Josh guilfoyle은 AlarmManager에 기반하여  오랬동안 살아있는 connection을 어떻게 만들것인가에 대해 언급했다.
그는 백그라운드에서 동작하면서 그 연결을 만들어내는 멋진 샘플코드도 제공하였다. 


Dave Rea는 Deacon project를 최근에 시작했다. Meteor server상에서 comet technology를 사용하여 안드로이드에서 push notification을 지원하는 3rd party library를 개발할 예정이다. 아주 초기 단계이다.  


지금은 페이지가 삭제되어 더이상 접근이 되지 않는다.

Dale Lane는 IBM에서 개발한 MQTT protocol로 android에서 push notification을 지원하는것과 관련한 많은 자료를 만들었으며 아주 훌룡한 샘플코드도 제공한다. http://dalelane.co.uk/blog/?p=938

위의 훌룡한 시도들을 기반으로 예제를 만들었다. 이는 Josh Guilfoyle의 TestKeepAlive project 와 Dale Lane의 MQTT를 이용한 것이다.

TestKeepAlive project의 문제점은 raw TCP connection을 만든다는 것이다. 이는 push를 관리하는 서버를 별도로 작성해야 한다는것을 의미한다. MQTT를 이용한 예제의 경우 서버작업을 위해 IBM의 MQTT broker를 이용한다. 

mqtt는 publish/subscribe messaging protocol로 가볍게 설계 되었다. 가볍게 설계 되었다는 것은 '저전력 소모'를 지원한다는 것이다. 이는 끊김없는 TCP/IP connection을 고려해야 하는 모바일환경에서 가장 이상적인 해결책이다. 다만 MQTT의 단점은 개인의 프라이버시 보장이 약하다는 것 등 몇가지 단점이 있기는 하다.

KeepAliveService와 raw TCP/IP connection을 MQTT connection으로 변경하는 것이 아이디어의 핵심이다.

Architecture

예제에서는 서버에서 PHP를 사용하였다. 이는 Simple Asynchronous Messaging library(http://project-sam.awardspace.com/)를 사용하였다.




wmqtt.tar 는 IBM에서 제공하는 MQTT protocol의 간단한 drop-in implementation이다.  http://www-01.ibm.com/support/docview.wss?rs=171&uid=swg24006006에서 다운로드 가능하다. 

Really Small Message Broker(RSMB)는 간단한 MQTT broker로 마찬가지로 IBM에서 제공한다. http://www.alphaworks.ibm.com/tech/rsmb에서 다운가능하다. 1833포트가 디폴트로 동작한다. 위 아키텍쳐에서 서버로부터 메세지를 받아서 단말에 전송하는 역할을 한다. RSMB는 Mosquitto server(http://mosquitto.atchoo.org/)로 변경이 가능하다.
SAM은 MQTT와 다른 요소들을 모아놓은 PHP library이다. http://pecl.php.net/package/sam/download/0.2.0에서 다운가능하다.

send_mqtt.php는 POST를 통해 메세지를 받아서 SAM을 이용하여 broker에 메세지를 전달하는 역할을 한다.

Sample code and Demo

이 예제는 TextView하나와 두개의 Button이 있다. 폰에 설치 후 서비스를 시작한 후 
http://tokudu.com/demo/android-push/  에 가서 deviceID를 입력하고 메세지를 입력하라. 그러고 난 후 "Send Push Message"를 누르면 폰에서 메세지를 받을 것이다. 
MQTT는 사실 안드로이드에서 Push를 지원하기 위한 최적의 방법은 아니다. 하지만 잘 동작한다. MQTT의 가장 취약한 점은 broker가 동작하는 IP와 PORT정보를 누군가 알아낸다면 모든 Push message들을 가로챌수 있다는 것이다. 따라서 그런 정보를 encrypt하는것은 좋은 대안이 될것이다. 대안으로 당신만의 broker를 작성하여 MQTT에 인증을 추가하는것이 있을 수 있다.

이 예제는 더 테스트되어야 한다. 안정성은 보장못한다. 연결이 끊어지거나 예외상황들에 대한 처리가 더 개선되어야 한다. 



이 댓글을 비밀 댓글로