출처 : http://www.winapi.co.kr/android/annex/18-3.htm

 

 

 

 

미디어 DB

1.미디어 스캐닝

이미지, 오디오, 비디오 등의 미디어들은 네트워크로 스트리밍되는 특수한 경우를 제외하고는 통상 저장장치에 파일 형태로 존재한다. 새로운 음악이나 동영상을 구했으면 PC와 연결한 후 장비의 SD 카드로 복사해야 폰에서 감상할 수 있다. 이때 미디어 재생 프로그램들이 새로 복사된 파일들을 어떻게 인식하는지 간단하게 테스트해 보자. 깔끔한 실험을 위해 테스트용 AVD를 새로 만들고 DDMS를 통해 이미지 파일 몇 개와 MP3 파일을 복사한다.

그리고 이미지 뷰어인 갤러리와 음악 재생기인 뮤직을 실행해 보자. 분명히 복사를 했음에도 재생기들은 아직 파일의 존재를 인식하지 못할 것이다. SD 카드에 물리적으로 복사는 되었지만 논리적 목록에 추가되지 않았기 때문이다. DevTool에서 Media Scanner를 실행하여 새로 복사된 파일을 목록에 추가해야 한다. 미디어 스캔이란 SD 카드를 검색하여 미디어 목록을 갱신하는 동작이다.

 

스캔 후에 갤러리와 뮤직을 다시 실행해 보면 새로 복사한 파일이 보이며 재생도 가능하다. 오른쪽 그림은 뮤직에서 MP3 파일을 재생하는 모습인데 파일명뿐만 아니라 가수 이름, 노래 제목은 물론이고 앨범 자켓 이미지까지도 표시된다. 단, 앨범 자켓은 법에 의해 보호되는 저작물이라 책에서 함부로 표시할 수 없어 흐릿하게 편집했다.

안드로이드 미디어 관리 정책의 핵심은 스캐닝이다. 물리적인 파일의 목록을 바로 읽지 않고 스캐닝을 통해 파일의 정보를 데이터베이스로 정리한 후 DB에서 미디어의 정보를 구한다. 에뮬레이터뿐만 아니라 모든 안드로이드 장비는 이 방식대로 미디어를 관리한다. 한 단계를 더 거치는 대신 여러 가지 장점이 있다.

 

① 재생기의 시작 속도가 빨라진다. 여기 저기 흩어져 있는 파일에서 정보를 일일이 조사하는 것이 아니라 이미 정리된 DB에서 목록을 바로 구하므로 재생기가 신속하게 실행된다. 음악을 듣고 싶을 때 재생기가 꾸물거리면 짜증날 것이다.

② 재생기의 동작 속도도 빨라지는데 특히 스크롤이 부드러워진다. 실시간으로 DB를 읽어들여도 스크롤 속도에 충분히 맞출 수 있어 원하는 미디어를 신속하게 선택할 수 있다. 파일에서 정보를 읽어서는 제 속도를 낼 수 없다.

③ 다양한 부가 정보를 같이 표시할 수 있다. 이미지의 썸네일, 방향, 촬영 위치 등이 미디어 DB에 기록되며 MP3는 가수, 앨범, 자켓 이미지 등도 미리 조사된다. 개별 파일을 열지 않아도 목록에 상세 정보를 출력할 수 있어 보기에 좋고 선택하기도 편리하다.

④ DB로부터 필터링할 수 있어 대량의 미디어라도 관리하기 쉽다. 특정 가수의 노래만 듣는다거나 고해상도의 동영상만 골라서 감상할 수 있다. 임의 순서로 정렬하기도 쉬워 감상 순서도 마음대로 조정할 수 있다.

 

요약하자면 스캐닝은 빠른 속도와 부가 정보 획득을 위해 모든 미디어 파일의 정보들을 미디어 DB라는 한 곳에서 중앙 집중적으로 관리하는 기법이다. 일단 정보가 모이기만 하면 DB의 모든 혜택을 마음껏 누릴 수 있다. 다음은 실장비에 표시된 음악 목록이다.

파일이 아무리 많아도 전체 목록을 일목요연하게 볼 수 있으며 스크롤 속도도 환상적으로 빠르다. 목록에는 상세한 부가 정보들도 표시되고 위쪽 탭에서 앨범별, 가수별로 필터링을 할 수 있으며 듣고 싶은 노래만 선별하여 자신만의 목록을 만들어 둘 수도 있다.

속도나 제공되는 정보의 양에서 아주 만족스러워 사용자 입장에서는 정말 훌륭한 기능이다. 그러나 한단계를 더 거침으로 인해 초래되는 불편함도 만만치 않으며 기계가 작성하는 목록이 항상 완벽한 것도 아니다. 다음은 미디어 스캐닝의 일반적인 단점이다.

 

① 실제 미디어 목록과 DB의 불일치가 발생할 수 있다. 새로 복사한 파일은 스캐닝을 하기 전에는 인식되지 않으며 지워진 파일도 아직 존재하는 것으로 오인된다. DB의 정보는 어디까지나 사본일 뿐이므로 항상 원본과 달라질 가능성이 있다.

② 스캐닝은 백그라운드에서 실행되는데 파일의 개수가 많을 경우 시간이 상당히 오래 걸린다. 모든 미디어를 일일이 열어 정보를 조사하므로 장시간이 소요되며 CPU 점유율도 높아 스캐닝중에 다른 작업을 하기 어렵다.

③ 감상용 미디어가 아닌 잡스러운 미디어들도 덩달아 스캐닝된다. 게임 효과음이나 벨소리는 감상용 음악은 아니지만 오디오 파일의 일종이므로 재생 목록에 표시된다. 스캐닝 대상에서 제외하고 싶은 디렉토리에 .nomedia 파일을 배치해 두는 해결책이 있지만 여전히 불편하다.

④ 남에게 보여 주기 싫은 미디어까지도 목록에 죄다 다 까발려진다. 폰은 개인적인 장비이므로 숨겨 놓고 혼자만 살짝 보고 싶은 동영상도 가끔은 있을 것이다.

 

게다가 조만간 수정되겠지만 아직도 스캐닝 기능에 버그가 존재한다. ID3 태그가 완전하지 못한 파일은 엉뚱한 정보가 조사되기도 하며 한글이 깨지는 문제도 있다. 미디어 파일의 정보들이 불완전하다는 것도 실용성을 반감시키는데 동일 가수임에도 철자가 틀리면 다른 가수로 인식되며 구형 포맷은 정보가 없어 제대로 분류되지 않는다.

미디어 스캐닝은 장점만큼이나 단점도 많아서 사람에 따라 호불호가 갈리는 기능이다. 개발자 입장에서 볼 때 이 기능이 살짝 못마땅하며 사용자 입장에서도 불편한 면이 있는데 아마도 습관의 문제인 듯 하다. 유용한 기능인 것은 분명하지만 운영체제 수준에서 지원하는 것보다 응용 프로그램 수준에서 관리하는 것이 바람직해 보인다.

몇 가지 문제가 있지만 구글은 최종 사용자 입장에서 유용한 기능이라고 판단하여 결국 미디어 스캐닝을 운영체제 차원에서 지원하기로 결정했다. 아무리 음악을 좋아하는 사람이라도 하루에 한번 이상 새로 음악을 받지는 않으므로 가끔 스캐닝해 두고 편리하게 사용하자는 정책도 설득력이 있다. 또 모바일 장비의 부족한 성능을 DB로 극복하려는 이유도 있으며 경쟁 제품과 기능적인 보조를 맞출 필요도 있었다.

운영체제 차원에서 스캐닝을 사용하므로 안드로이드에서 실행되는 프로그램들은 DB에서 미디어 목록윽 구해야 한다. 또한 미디어를 생성하는 프로그램은 항상 실제 목록과 DB를 동기화할 의무가 있다. 카메라나 녹음기는 생성한 파일의 정보를 SD카드 뿐만 아니라 DB에도 기록해야 한다. 응용 프로그램들이 DB를 실시간으로 정확하게 잘 관리한다면 최종 사용자들은 아무 불편없이 편리하게 멀티미디어를 즐길 수 있을 것이다.

2.DB의 구조

안드로이드 운영체제가 미디어를 DB로 관리하므로 멀티미디어 응용 프로그램들은 미디어 DB를 통해 정보를 얻어야 한다. 미디어의 목록을 관리하는 클래스는 android.provider 패키지에 소속된 MediaStore이되 이 클래스 자체는 몇 가지 상수만 제공할 뿐이고 실제 관리 기능은 중첩된 내부 클래스들이 담당한다. MediaStore는 문법적으로는 클래스이지만 차제적인 기능은 거의 없고 내부 클래스와 인터페이스를 담는 통에 불과하다.

내부 클래스인 Images, Audio, Video는 각각 이미지, 오디오, 비디오를 관리하는데 이름만 봐도 기능이 너무 명백하다. 각 내부 클래스들도 실제 기능은 정의되어 있지 않고 중첩된 다른 인터페이스나 클래스들만 가진다. 클래스들이 3중 4중으로 중첩되어 있고 상속 관계도 복잡해서 구조가 한눈에 잘 들어오지 않아 혼란스러운데 먼저 클래스 구조부터 잘 파악해야 한다. 다음 그림은 클래스간의 포함관계를 나타낸 것으로서 사각형은 클래스이고 타원은 인터페이스이다.

이 그림에서 클래스 사이를 잇는 직선은 상속 관계가 아니라 포함 관계임을 주의하자. MediaStore 클래스에 내부 클래스들이 여러 겹으로 중첩되어 있는 것이다. 따라서 특정 클래스를 사용하려면 외부 클래스부터 경로를 순차적으로 밝혀야 한다. 예를 들어 이미지 목록을 관리하는 클래스의 완전 경로는 다음과 같다.

 

MediaStore.Images.Media

 

데이터베이스의 구조를 빠르게 파악하려면 테이블을 구성하는 필드의 구조, 즉 스키마부터 우선적으로 관찰해 보아야 한다. 컬럼들의 이름은 내부 인터페이스에 상수로 정의되어 있으며 컬럼 인터페이스끼리는 다음과 같은 상속 계층을 구성한다.

BaseColumns는 안드로이드에 존재하는 모든 테이블 스키마의 루트에 해당하며 레코드 개수와 레코드의 유일한 PK인 id 필드를 정의한다. 이 두 필드는 미디어 DB뿐만 아니라 모든 테이블에 공통적으로 필요하다. MediaColumns 인터페이스도 이 두 필드를 상속받으며 하위의 다른 인터페이스들도 물론이다.

미디어 DB 관련 파생 인터페이스들은 MediaStore 클래스의 내부 인터페이스로 정의되어 있다. 사진, 음악, 동영상 파일들의 속성이 다르므로 필요한 컬럼의 목록들도 당연히 다르다. 모든 미디어 파일의 공통 속성들은 MediaColumns 인터페이스가 정의한다. 다음 도표의 위쪽 두 필드는 BaseColumns로부터 상속받은 것이며 나머지는 미디어 DB가 정의하는 것이다.

 

필드명 상수(필드명)

필드의 타입

설명

_COUNT(_count)

int

레코드 개수

_ID(_id)

long

레코드의 pk

DATA(_data)

DATA_STREAM

데이터 스트림. 파일의 경로

SIZE(_size)

long

파일 크기

DISPLAY_NAME(_display_name)

text

파일 표시명

MIME_TYPE(mime_type)

text

마임 타입

TITLE(title)

text

제목

DATE_ADDED(date_added)

long

추가 날짜. 초단위

DATE_MODIFIED (date_modified)

long

최후 갱신 날짜. 초단위

 

컬럼 인터페이스는 DB를 구성하는 필드의 이름 문자열을 상수로 제공하는 역할을 한다. 예를 들어 미디어의 크기를 저장하는 필드명은 다음과 같이 정의되어 있다.

 

public static final String SIZE = "_size"

 

컬럼 인터페이스는 필드의 이름을 정의하므로 모든 상수 멤버는 문자열 타입이다. 주의할 것은 상수 멤버의 타입과 필드 자체의 타입은 다르다는 것이다. 필드 자체의 타입은 저장하는 정보에 따라 달라지는데 예를 들어 미디어의 크기값을 저장하는 SIZE 필드는 정수형의 long 타입으로 정의되어 있다. 각 필드의 실제 타입은 레퍼런스에서 확인할 수 있다.

MediaColumns 인터페이스는 표시명, 크기, 경로, 타입, 제목, 날짜 등 모든 미디어에 공통적으로 필요한 컬럼을 정의한다. 날짜는 1970년을 기준으로 한 절대초이며 단위는 필드마다 달라지는데 추가, 갱신 날짜의 경우는 초단위이다. MediaColumns가 정의하는 필드는 모든 미디어 파일에 공통되는 것이며 미디어별 속성은 서브 인터페이스가 정의한다. 이미지의 필드 목록은 ImageColumns 인터페이스에서 정의한다.

 

필드명 상수(필드명)

필드의 타입

설명

DESCRIPTION(description)

text

이미지에 대한 설명

PICASA_ID(picasa_id)

text

피카사에서 매기는 id

IS_PRIVATE(isprivate)

int

공개 여부

LATITUDE(latitude)

double

위도

LONGITUDE(longitude)

double

경도

DATE_TAKEN(datetaken)

int

촬영날짜. 1/1000초 단위

ORIENTATION(orientation)

int

사진의 방향. 0, 90, 180, 270

MINI_THUMB_MAGIC(mini_thumb_magic)

int

작은 썸네일

BUCKET_ID(bucket_id)

text

버킷 ID

BUCKET_DISPLAY_NAME(bucket_display_name)

text

버킷의 이름

 

이미지에 대한 간단한 설명, 촬영 위치, 촬영 방향, 썸네일 등의 속성들이 추가로 정의되어 있다. 이미지는 보통 카메라를 통해 촬영되므로 촬영 당시 획득할 수 있는 정보들이 DB에 저장된다. 물론 모든 정보들이 항상 다 존재하는 것은 아니며 일부 컬럼은 정보가 없는 null 상태일 수도 있다.

다음은 오디오의 속성을 정의하는 AudioColumns 인터페이스의 컬럼 목록을 보자. 그림을 저장하는 이미지의 속성과는 확연히 다름을 알 수 있다. 주로 음반 형태로 발매되므로 앨범이나 가수에 대한 정보들이 추가된다. 개수가 굉장히 많으므로 일부 주요 필드만 정리했는데 레퍼런스에는 모든 컬럼이 다 정리되어 있다.

 

필드명 상수(필드명)

필드의 타입

설명

ALBUM(album)

text

앨범명

ARTIST(artist)

text

가수명

BOOKMARK(bookmark)

long

마지막 재생 위치

DURATION(duration)

long

총 재생 시간

IS_MUSIC(is_music)

int

음악 파일 여부

TRACK(track)

int

앨범내의 트랙 위치

YEAR(year)

int

발표 년도

 

동영상의 속성을 정의하는 VideoColumns 인터페이스의 컬럼 목록은 오디오와 비슷하지만 추가되는 정보들이 있다. 동영상은 해상도가 곧 품질과 직결되므로 해상도 정보가 포함되고 자막이 포함될 경우를 고려하여 언어에 대한 정보도 제공된다.

 

필드명 상수(필드명)

필드의 타입

설명

ALBUM(album)

text

앨범명

ARTIST(artist)

text

가수명

BOOKMARK(bookmark)

long

마지막 재생 위치

DURATION(duration)

long

총 재생 시간

CATEGORY(category)

text

유튜브의 범주

LANGUAGE(language)

text

언어

RESOLUTION(resolution)

text

해상도

 

MediaStore의 인터페이스들이 정의하는 컬럼들은 어디까지나 일반적으로 많이 사용되는 목록일 뿐이다. 이 정보들 외에도 제조사의 필요에 따라 추가 정보들이 테이블에 더 포함될 수 있으며 일부 불필요한 컬럼은 삭제될 수도 있다. 인터페이스는 어디까지나 자주 사용하는 컬럼의 목록을 정의할 뿐이지 모든 장비가 이 컬럼들을 제공해야 할 의무가 있다는 뜻은 아니다.

각 장비들의 미디어 DB를 조사해 보면 필드 목록이 조금씩 다른데 제조사의 정책이나 구현 편의상 필요한 컬럼이 추가되기도 한다. 가령 예를 든다면 최근 재생한 음악인지 등의 런타임 정보나 유료 결제 파일인지의 여부 등의 정보가 더 필요할 수도 있다. 물론 그렇다고 해서 완전히 다르지는 않으며 일반적인 필드들은 대부분 제공되므로 호환성에는 별 문제가 없다.

3.미디어 덤프

DB 구조를 연구해 보았으므로 이제 실제 미디어 DB에 등록된 레코드를 조사하여 덤프해 보자. 대표적으로 이미지 목록에 대해서 연구해 보기로 하되 오디오나 비디오의 경우도 거의 유사하다. 이미지의 목록을 관리하는 클래스는 Images.Media 이며 목록과 관련된 다음 상수들을 정의한다.

 

상수(실제값)

설명

EXTERNAL_CONTENT_URI

content://media/external/images/media

외부 저장 장치의 모든 이미지에 대한 Uri이다.

INTERNAL_CONTENT_URI

content://media/internal/images/media

내부 저장 장치의 모든 이미지에 대한 Uri이다.

CONTENT_TYPE

vnd.android.cursor.dir/image

이미지 디렉토리의 마임 타입을 정의한다. 디렉토리내의 파일은 적절한 이미지 마임 타입을 가져야 한다.

DEFAULT_SORT_ORDER

bucket_display_name

정렬 순서를 지정한다.

 

쿼리 실행시 전달할 URI와 마음 타입 등이 정의되어 있다. 상수들 외에 다음 몇 가지 메서드도 제공되기는 하나 컨텐트 리졸브의 메서드를 대신 호출하는 래퍼 기능을 수행하며 이미지 삽입과 추출 등의 유틸리티 기능 정도에 불과하다.

 

Cursor query (ContentResolver cr, Uri uri, String[] projection, String where, String orderBy)

String insertImage (ContentResolver cr, String imagePath, String name, String description)

Bitmap getBitmap (ContentResolver cr, Uri url)

 

미디어의 목록을 관리하는 실제 작업은 범용 컨텐트 관리 클래스인 ContentResolver의 다음 메서드들이 처리한다. 이 메서드로 미디어 DB 뿐만 아니라 안드로이드에서 실행되는 모든 CP의 정보를 조사할 수 있다.

 

Cursor query (Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder)

Uri insert (Uri url, ContentValues values)

int delete (Uri url, String where, String[] selectionArgs)

int update (Uri uri, ContentValues values, String where, String[] selectionArgs)

 

query 메서드로 목록을 조사하되 조상 대상 URI를 각 미디어 관리 클래스가 정의하는 URI로 전달하며 리턴된 커서로부터 필드값을 조사할 때 미디어 컬럼 정의 인터페이스가 정의하는 컬럼명을 사용한다. 필드 목록, 필터링 조건, 정렬 순서 등에 대한 지정은 일반적인 데이터베이스 문법을 따른다. 결국 MediaStore와 그 중첩 클래스들은 쿼리문을 위한 상수만 제공할 뿐이지 실제 목록 관리는 컨텐트 리졸브가 하는 셈이다.

Images.Thumbnails는 미디어 DB에 저장된 이미지 썸네일에 대한 상수와 유틸리티 메서드를 정의한다. 같은 방식으로 Audio.Media는 오디오 목록, Video.Media는 비디오에 대한 목록을 관리한다. 정의된 상수의 종류나 유틸리티 메서드에서 약간씩의 차이가 있을 뿐 목록을 관리하는 방법은 이미지의 경우와 거의 동일하다.

다음 예제는 DB의 컬럼 목록과 모든 미디어 목록을 조사하여 문자열 형태로 출력한다. 실용성은 없지만 미디어 DB의 구조를 탐구해 보고 현재 장비의 미디어 상태를 점검해 보는 유틸리티로 쓸만하다. 당연한 얘기겠지만 이 예제가 제대로 동작하려면 미디어 스캔이 먼저 수행되어 있어야 한다.

 

mm_DumpMedia

     ContentResolver mCr;

     TextView mResult;

     RadioGroup mMedia;

     ToggleButton mStorage;

    

     public void onCreate(Bundle savedInstanceState) {

          super.onCreate(savedInstanceState);

          setContentView(R.layout.mm_dumpmedia);

          mCr = getContentResolver();

          mResult = (TextView)findViewById(R.id.result);

          mStorage = (ToggleButton)findViewById(R.id.storage);

          mMedia = (RadioGroup)findViewById(R.id.media);

         

          mMedia.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener() {

              public void onCheckedChanged(RadioGroup group, int checkedId) {

                   dumpQuery();

              }            

          });

          mStorage.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() {

              public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {

                   dumpQuery();

              }

          });

 

          dumpQuery();

     }

    

     void dumpQuery() {

          StringBuilder result = new StringBuilder();

          Uri uri;

          boolean instorage = mStorage.isChecked();

         

          // 미디어 종류와 메모리 위치로부터 URI 결정

          switch (mMedia.getCheckedRadioButtonId()) {

          case R.id.image:

              default:

              uri = instorage ? Images.Media.INTERNAL_CONTENT_URI:

                   Images.Media.EXTERNAL_CONTENT_URI;

              break;

          case R.id.audio:

              uri = instorage ? Audio.Media.INTERNAL_CONTENT_URI:

                   Audio.Media.EXTERNAL_CONTENT_URI;

              break;

          case R.id.video:

              uri = instorage ? Video.Media.INTERNAL_CONTENT_URI:

                   Video.Media.EXTERNAL_CONTENT_URI;

              break;

          }

          Cursor cursor = mCr.query(uri, null, null, null, null);

         

          // 필드 목록 출력

          int nCount = cursor.getColumnCount();

          result.append("num colume = " + nCount + "\n\n");

          for (int i = 0; i < nCount; i++) {

              result.append(i);

              result.append(":" + cursor.getColumnName(i) + "\n");

          }

 

          result.append("\n======================\n\n");

         

          // 레코드 목록 출력

          result.append("num media = " + cursor.getCount() + "\n\n");

          int count = 0;

          while (cursor.moveTo!Next()) {

              result.append(getColumeVal!ue(cursor, MediaColumns._ID));

              result.append(getColumeVal!ue(cursor, MediaColumns.DISPLAY_NAME));

              result.append(getColumeVal!ue(cursor, MediaColumns.TITLE));

              result.append(getColumeVal!ue(cursor, MediaColumns.SIZE));

              result.append(getColumeVal!ue(cursor, MediaColumns.DATE_ADDED));

              result.append(getColumeVal!ue(cursor, MediaColumns.MIME_TYPE));

 

              switch (mMedia.getCheckedRadioButtonId()) {

              case R.id.image:

                   result.append(getColumeVal!ue(cursor, Images.ImageColumns.DATE_TAKEN));

                   result.append(getColumeVal!ue(cursor, Images.ImageColumns.DESCRIPTION));

                   result.append(getColumeVal!ue(cursor, Images.ImageColumns.ORIENTATION));

                   result.append(getColumeVal!ue(cursor, Images.ImageColumns.LATITUDE));

                   break;

              case R.id.audio:

                   result.append(getColumeVal!ue(cursor, Audio.AudioColumns.ALBUM));

                   result.append(getColumeVal!ue(cursor, Audio.AudioColumns.ARTIST));

                   result.append(getColumeVal!ue(cursor, Audio.AudioColumns.YEAR));

                   result.append(getColumeVal!ue(cursor, Audio.AudioColumns.DURATION));

                   break;

              case R.id.video:

                   result.append(getColumeVal!ue(cursor, Video.VideoColumns.DURATION));

                   result.append(getColumeVal!ue(cursor, Video.VideoColumns.RESOLUTION));

                   break;

              }

              result.append("\n");

              count++;

              if (count == 32) break;

          }

          cursor.close();

 

          mResult.setText(result.toString());

     }

    

     String getColumeVal!ue(Cursor cursor, String cname) {

          String value = cname + " : " +

              cursor.getString(cursor.getColumnIndex(cname)) + "\n";

          return value;

     }

}

 

레이아웃에는 미디어 종류와 메모리 위치를 선택할 수 있는 위젯들과 덤프 결과 확인을 위한 텍스트 뷰가 배치되어 있다. 선택을 변경하는 즉시 해당 정보를 조사하여 아래쪽의 텍스트 뷰에 출력하므로 별도의 조사 명령을 내릴 필요는 없다. 실행 결과는 장비의 파일 목록에 따라 조금씩 달라질 것이다.

 

이 예제의 핵심은 덤프를 수행하는 dumpQuery 메서드이다. 위젯에서 선택한 메모리 위치와 미디어 종류에 따라 조사할 대상의 URI를 선택하는데 각 미디어 관련 클래스에 정의된 상수중 하나를 대입한다. 이미지, 오디오, 비디오 각각에 대해 내부, 외부 메모리 두 가지의 조합을 취할 수 있으므로 URI는 여섯 개 중 하나가 된다.

URI를 결정한 후 컨텐트 리졸브의 query 메서드를 호출하여 미디어 목록을 조회한다. 전체 목록을 조건이나 정렬없이 조회했는데 query의 나머지 인수를 활용하면 원하는 정보만 뽑아볼 수 있다. 컨텐트 리졸브는 URI로부터 미디어 DB를 관리하는 CP를 찾아 호출하며 CP는 요청된 쿼리를 수행하여 결과셋을 커서 객체로 리턴할 것이다. 커서의 필드 목록을 먼저 조사하여 출력했는데 이는 어디까지나 DB의 구조를 들여다 보기 위한 학습용이다. 필드의 개수나 종류가 장비마다 조금씩 차이가 날 것이다.

이후 커서에 저장된 레코드를 읽음으로써 미디어 목록과 각 미디어의 부가 정보를 구할 수 있다. MediaColumns에 정의된 공통 컬럼을 먼저 조사하고 미디어 종류별로 고유한 속성들도 조사하여 하나의 문자열로 조립한 후 아래쪽의 텍스트 뷰에 뿌린다. 루프를 끝까지 돌면 모든 미디어 목록을 다 조사할 수 있지만 실장비에서는 시간이 오래 걸릴 수도 있으므로 32개까지만 조사했다.

예제에서는 단순히 목록을 조사해 보기만 했는데 조사된 목록을 어떻게 활용할 것인가는 프로그램의 용도에 따라 달라진다. 이미지 뷰어는 순차적으로 이미지를 보여줌으로써 슬라이드 쇼를 하고 MP3 플레이어는 오디오 목록을 리스트 뷰에 출력해 놓고 사용자가 선택한 음악을 재생할 것이다.

4.미디어 방송

DumpMedia 예제는 미디어의 목록을 정확하고 신속하게 잘 보여주기는 하지만 현재 상태만 보여줄 뿐이지 이후의 변화에는 제대로 반응하지 못한다. 어떤 문제점이 있는지 에뮬레이터에서 다음 실험을 해 보면 알 수 있는데 귀찮다면 굳이 해 보지 않더라도 결과를 이해할 수 있을 것이다.

 

① MediaDump를 실행하여 이미지 파일의 현재 개수를 확인한다. 상황에 따라 다르겠지만 예를 들어 5개 있었다고 가정하자.

② 이 상태에서 DDMS로 이미지 파일 하나를 SD 카드로 복사한다. 그리고 미디어 스캐너를 실행하여 DB를 갱신하면 새로 추가된 파일의 정보가 DB에 기록될 것이다.

③ MediaDump 프로그램으로 돌아와 이미지 개수를 확인해 보면 여전히 5개이다. Audio를 선택했다가 다시 Image를 돌아와 강제로 다시 조사하면 제대로 갱신된다.

 

MediaDump가 조사해 놓은 목록은 새 이미지를 추가하기 전의 정보여서 현재 상황과는 맞지 않다. 조사하던 시점에는 정확했지만 이후에도 미디어 목록은 언제든지 첨삭될 수 있어 불일치가 발생하는 것이다. 미디어를 편집하는 프로그램은 방송을 하여 DB를 동기화할 의무가 있으며 미디어 CP는 이 방송을 수신할 때마다 새로 추가된 이미지를 DB에 삽입할 것이다. DDMS는 장비 외부에서 파일을 밀어 넣는 것이므로 방송을 하지 않으며 그래서 위 실험에서는 미디어 스캐너를 수동으로 실행했다.

마찬가지로 미디어 목록을 참고하는 프로그램은 항상 방송에 귀를 기울여 변화를 감지할 때마다 최신 목록으로 갱신해야 할 의무가 있다. 하지만 MediaDump 예제는 귀를 틀어 막고 방송에 전혀 관심을 보이지 않으므로 새 이미지를 인식하지 못하는 것이다. 삭제에 대해서도 마찬가지인데 지워진 이미지도 여전히 목록에 표시된다. 미디어 DB와 관련된 방송은 다음 세 가지가 있다.

 

방송

설명

ACTION_MEDIA_SCANNER_STARTED

디렉토리를 스캐닝하기 시작했다.

ACTION_MEDIA_SCANNER_FINISHED

디렉토리 스캐닝이 완료되었다.

ACTION_MEDIA_SCANNER_SCAN_FILE

파일 하나를 스캐닝하여 DB에 추가했다.

 

이 방송들을 보내면 스캐닝을 새로 하라는 명령으로 인식된다. 명령을 받는 주체는 물론 미디어 DB를 관리하는 CP이다. 이 방송을 수신하면 스캐닝이 시작되는 시점과 끝나는 시점을 알 수 있으며 이때 목록을 갱신해야 한다. 다음 예제는 미디어 변경에 대한 방송을 BR로 수신하여 항상 최신 목록을 유지한다. DumpMedia 예제에 약간의 코드를 더 추가한 것이다.

 

mm_DumpMedia2

     public void onCreate(Bundle savedInstanceState) {

          ....

          dumpQuery();

 

          // 미디어 변화에 대한 BR을 등록한다.

          IntentFilter filter = new IntentFilter();

          filter.addAction(Intent.ACTION_MEDIA_SCANNER_STARTED);

          filter.addAction(Intent.ACTION_MEDIA_SCANNER_FINISHED);

          filter.addAction(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE);

          filter.addDataScheme("file");

          registerReceiver(mScanReceiver, filter);

     }

    

     // 미디어 변경시 목록을 갱신한다.

     BroadcastReceiver mScanReceiver = new BroadcastReceiver() {

          public void onReceive(Context context, Intent intent) {

              dumpQuery();

          }

     };

 

     // 종료시 BR을 해제한다.

     public void onDestroy() {

          super.onDestroy();

          unregisterReceiver(mScanReceiver);

     }

 

onCreate에서 BR을 등록하여 모든 스캐닝 방송을 감시한다. 새로 스캐닝이 시작되면 dumpQuery 메서드를 호출하여 목록을 완전히 새로 조사하므로 항상 최신 정보를 보여줄 것이다. 제대로 동작하는지 테스트해 보자. 방법은 앞에서 한 실험과 동일하되 굳이 새 파일을 복사할 필요없이 복사했던 파일을 삭제해 보면 된다. 삭제한 파일이 목록에서 즉시 사라질 것이다.

액티비티가 종료될 때는 더 이상 감시를 할 필요가 없으므로 BR을 해제했다. 만약 액티비티가 활성 상태일 때만 감시를 하려면 onResume에서 BR을 등록하고 onPause에서 BR을 해제하는 것이 정석이다. 단, 이 경우 다시 활성화될 때 목록을 다시 조사해야 백그라운드 상태에서 발생한 변경 사항이 제대로 갱신된다.

5.이미지 뷰어

미디어 DB가 이미지의 목록을 제공하므로 이미지 뷰어 정도는 아주 쉽게 만들 수 있다. DB로부터 구한 커서를 어댑터 뷰에 연결해 놓으면 목록 출력 및 관리가 자동으로 수행되므로 목록을 보여주는 것은 거의 공짜이다. 개수가 몇 개이든 서브 디렉토리 곳곳에 흩어져 있건 전혀 신경쓸 필요없이 쿼리만 날리면 된다. 이미지를 출력할 때는 이미지 뷰를 사용할 수 있으므로 리스트 뷰의 클릭만 처리하면 된다.

 

mm_ImageView

public class mm_ImageView extends Activity {

     ImageView mImage;

     Cursor mCursor;

    

     public void onCreate(Bundle savedInstanceState) {

          super.onCreate(savedInstanceState);

          setContentView(R.layout.mm_imageview);

         

          ListView list = (ListView)findViewById(R.id.list);

          mImage = (ImageView)findViewById(R.id.image);

         

          ContentResolver cr = getContentResolver();

          mCursor = cr.query(Images.Media.EXTERNAL_CONTENT_URI, null, null, null, null);

          SimpleCursorAdapter Adapter = new SimpleCursorAdapter(this,

                   android.R.layout.simple_list_item_1,

                   mCursor, new String[] { MediaColumns.DISPLAY_NAME },

                   new int[] { android.R.id.text1});

          list.setAdapter(Adapter);

          list.setOnItemClickListener(mItemClickListener);

          startManagingCursor(mCursor);

     }

 

     AdapterView.OnItemClickListener mItemClickListener = new AdapterView.OnItemClickListener() {

          public void onItemClick(AdapterView<?> parent, View view, int position, long id) {

              mCursor.moveTo!Position(position);

              String path = mCursor.getString(mCursor.getColumnIndex(Images.ImageColumns.DATA));

              try {

                   BitmapFactory.Options opt = new BitmapFactory.Options();

                   opt.inSampleSize = 4;

                   Bitmap bm = BitmapFactory.decodeFile(path, opt);

                   mImage.setImageBitmap(bm);

              }

              catch (OutOfMemoryError e) {

                   Toast.makeText(mm_ImageView.this,"이미지가 너무 큽니다.",0).show();

              }

          }

     };

}

 

레이아웃은 절반을 잘라 위쪽에는 목록을 보여줄 리스트 뷰를 배치하고 아래쪽에는 선택된 이미지를 출력할 이미지 뷰를 배치해 두었다. onCreate에서 Images.Media의 외부 메모리 URI로 쿼리를 실행하면 SD 카드의 모든 이미지 목록이 커서로 조사될 것이다. 이 커서를 어댑터로 전달하되 DISPLAY_NAME 필드를 항목 뷰의 텍스트 뷰와 짝을 지워 파일명을 출력했다.

필요하다면 커스텀 항목 뷰를 제작하고 파일명 뿐만 아니라 이미지의 크기나 날짜, 썸네일 등의 상세 정보도 같이 출력할 수 있다. 어댑터 생성자에서 어떤 정보를 어디에 출력할 것인가만 지정하면 나머지 처리는 어댑터와 리스트 뷰가 알아서 척척 처리한다. 이미지 개수가 아무리 많아도 스크롤에는 더 이상 신경쓸 필요가 없다.

리스트 뷰의 항목을 클릭하면 미디어 DB에서 DATA 필드를 읽어 이미지 파일의 실제 경로를 구한다. 경로로부터 비트맵을 읽어 아래쪽의 이미지 뷰로 던지기만 하면 그림이 나타날 것이다. 아주 거대한 이미지인 경우 메모리 부족으로 인해 디코딩이 실패할 수도 있어 1/4로 축소해서 읽었으며 약간의 예외 처리가 작성되어 있다. 에뮬레이터에서는 이런 경우가 종종 발생하지만 실장비에서는 왠만한 이미지는 다 읽어들인다.

위쪽의 목록을 스크롤해가며 원하는 파일을 선택하면 아래쪽에 이미지가 즉시 나타난다. 이미지 목록을 굳이 표시할 필요가 없다면 이미지를 풀화면으로 표시하고 터치 입력을 받아 이전/다음 이미지로 전환하여 순서대로 감상하도록 한다. 여기에 타이머로 슬라이더 쇼 기능 정도를 구현한다면 좀 초라해도 충분히 쓸만한 이미지 뷰어가 될 것이다.

이 예제도 미디어 DB를 참조하지만 방송은 수신하지 않아도 상관없다. 왜냐하면 onCreate에서 목록을 새로 조사하므로 활성화될 때 항상 최신 정보로 갱신되는데다 커서가 DB와 직접 연결되어 있으므로 DB의 변화를 바로 감지해내기 때문이다. startManagingCursor를 호출하여 커서의 운명을 액티비티에게 맡겨 버리기만 하면 된다.

다음 예제는 이미지의 썸네일을 그리드 뷰로 출력한다. 다수의 이미지를 한꺼번에 보여줄 때는 표 형태의 그리드를 사용하는 것이 편리하다. 이미지 중 하나를 선택할 때 파일의 이름이 아닌 썸네일을 보면서 고를 수 있다. 이 예제를 조금만 변경하여 선택된 이미지의 경로를 인텐트로 리턴하면 이미지 선택 대화상자로 활용할 수 있다.

 

mm_ImageGrid

public class mm_ImageGrid extends Activity {

     GridView mGrid;

     Cursor mCursor;

 

     public void onCreate(Bundle savedInstanceState) {

          super.onCreate(savedInstanceState);

          setContentView(R.layout.mm_imagegrid);

         

          mGrid = (GridView) findViewById(R.id.imagegrid);

 

          ContentResolver cr = getContentResolver();

          mCursor = cr.query(Images.Thumbnails.EXTERNAL_CONTENT_URI, null, null, null, null);

          ImageAdapter Adapter = new ImageAdapter(this);

          mGrid.setAdapter(Adapter);

         

          mGrid.setOnItemClickListener(mItemClickListener);

     }

    

     AdapterView.OnItemClickListener mItemClickListener = new AdapterView.OnItemClickListener() {

          public void onItemClick(AdapterView<?> parent, View view, int position, long id) {

              mCursor.moveTo!Position(position);

              String path = mCursor.getString(mCursor.getColumnIndex(Images.ImageColumns.DATA));

              Intent intent = new Intent(mm_ImageGrid.this, mm_ImageGridFull.class);

              intent.putExtra("path", path);

              startActivity(intent);

          }

     };

 

     class ImageAdapter extends BaseAdapter {

          private Context mContext;

 

          public ImageAdapter(Context c) {

              mContext = c;

          }

 

          public int getCount() {

              return mCursor.getCount();

          }

 

          public Object getItem(int position) {

              return position;

          }

 

          public long getItemId(int position) {

              return position;

          }

 

          public View getView(int position, View convertView, ViewGroup parent) {

              ImageView imageView;

              if (convertView == null) {

                   imageView = new ImageView(mContext);

                   mCursor.moveTo!Position(position);

                   Uri uri = Uri.withAppendedPath(MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI,

                             mCursor.getString(mCursor.getColumnIndex(MediaStore.Images.Thumbnails._ID)));

                   imageView.setImageURI(uri);

                   imageView.setAdjustViewBounds(true);

                   imageView.setScaleType(ImageView.ScaleType.CENTER_CROP);

              } else {

                   imageView = (ImageView) convertView;

              }

 

              return imageView;

          }

     }

}

 

레이아웃에는 그리드 뷰 하나만 배치되어 있으며 onCreate에서 썸네일 목록을 가지는 커서와 연결한다. 그리드의 항목 뷰는 DB의 정보를 바로 출력하는 것이 아니라 이미지를 표시해야 하므로 표준 어댑터를 쓸 수 없으며 커스텀 어댑터와 연결해야 한다. 커스텀 어댑터는 DB에서 이미지 경로를 구하고 경로의 비트맵을 읽어 이미지 뷰 표면에 출력한다. 이 뷰들의 집합이 그리드 뷰에 나타나는 것이다.

시원스럽게 보이기 위해 행당 2개의 이미지만 출력했는데 좀 더 많은 이미지를 촘촘하게 배치하고 싶다면 레이아웃의 그리드 열 개수를 조정하면 된다. 썸네일을 클릭하면 해당 이미지 원본을 읽어 출력한다. 전체 이미지를 출력하는 mm_ImageGridFull 액티비티에는 이미지 뷰 하나만 배치되어 있으며 인텐트로 전달된 경로의 이미지를 읽어 보여준다. 아주 간단한 액티비티라 소스는 생략했다.

arrow
arrow
    全站熱搜

    戮克 發表在 痞客邦 留言(0) 人氣()