안드로이드에서는 오디오/비디오/이미지 등등의 미디어 파일을 MediaStore에 미리 등록시키고, MediaStore를 통해서 미디어의 정보를 접근하는 방식을 쓴다.

그런데, 아무래도 대부분의 유저는 폴더식 브라우징에 익숙하기 때문에 그런 것인지 폴더보기를 더 선호한다. 상당수의 음악 앱에서 지원하는 장르별/앨범별/아티스트별 브라우징을 좋아하지 않는 이유는 다름이 아니라 대부분의 오디오파일이 앨범/아티스트/장르 등등의 정보가 제대로 들어있지 않기 때문이다. wav파일에 INFO 메다 정보가 들어있는 경우는 드물고, mp3 ID2/ID3 태그 정보등은 텅텅 비어있는 경우가 많다. 요즘에는 음원 사이트에서 다운로드 받은 파일의 경우는 태그 정보가 잘 들어있지만, CD에 있던 오디오를 MP3로 인코딩 했다거나, 인터넷을 통해 다운받은 어학 자료같은 경우에는 태그 정보가 그다지 도움이 되지 않는 경우가 많다.

아무튼 이러한 이유로, 꽤 인기 있다는 오디오 앱들의 상당수는 폴더식 브라우징을 잘 지원하고 있으며, 유명한 오디오 앱은 거의 예외 없이 폴더식 보기를 지원한다. 예를 들어보면

  1. 제트오디오
  2. Player Pro

일부 앱에서는 약간 다른 폴더식 보기를 지원하는데, 미디어 파일을 포함한 모든 폴더를 한꺼번에 보여준다. 이 경우의 장점은 단 한번의 클릭으로도 해당 폴더의 모든 미디어를 볼 수 있다는 점이지만, 단점은 중복된 이름의 폴더가 있는 경우에 사용자가 혼란스럽다고 느낄 수 있다.

또 어떤 앱에서는 이상하다 싶을 정도로 폴더식 브라우징의 반응 속도가 더딘 경우도 있다.

미디어스토어(MediaStore)에 대해서도 사용자가 폴더식 브라우징의 일관된 경험을 느낄 수 있으면서도 효과적인 방식을 구현하려면 어떻게 해야 할까?

우선 미디어스토어에서 오디오 파일 리스트를 얻어오려면 다음과 같이 해야 한다.

getContentResolver().query(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, columns, null, null, null);

여기서 columns에 가지고 오고싶은 Audio.Media의 필드를 넣어주면 된다. EXTERNAL_CONTENT_URI는 여기서 외장 저장장치에 대응하는 MediaStore URI이다.

위의 쿼리는 다음과 같은 식의 SQL 구문에 대응된다고 보면 된다.

SELECT * from audio

SQLite db 파일은 다음 소스에 정의되어 있으며

packages/providers/MediaProvider/src/com/android/providers/media/MediaProvider.java

실제로 저장된 위치는 /data/data/com.android.providers.media/databases/internal.db 혹은 external.db 파일이다. (http://stackoverflow.com/questions/3592497/android-mediastore-sqlite-db-location 참조)

특정 폴더 아래의 모든 오디오 파일 목록 얻기

그러면 특정 폴더에 있는 파일 목록을 얻어오려면 어떻게 해야 할까? 예를 들어 기본 내장 SD에 있는 /sdcard/Download 폴더 하위에 있는 모든 오디오 파일 목록을 가져오려면 다음과 같이 해야 한다.

SELECT * FROM audio WHERE _data LIKE '/storage/emulated/0/Download/%'

즉, /storage/emulated/0/Download로 시작하는 모든 파일 목록을 가져오는 것이다.

그러나 이렇게 하면 Download 폴더 아래의 또 다른 폴더에 들어있는 모든 오디오 파일도 가져오기때문에, 폴더식 보기를 위해서 하위 디렉토리는 제외하고 폴더 바로 아래에 있는 파일 목록을 가져오려면 다음과 같이 조건문을 만들어야 한다.

substr(_data, length('/storage/emulated/0/Download/')+1) NOT LIKE '%/%')

_data 필드(실제 파일 경로)에서 /storage/emulated/0/Download/ 부분을 잘라내고 그 나머지 파일 경로에 / 문자가 포함되지 않은 경우가 바로 해당 경로 바로 아래에 있는 오디오 파일이 된다.

선택된 경로 바로 아래에 있는 오디오 파일 목록을 가져오는 방법을 알았으니 이제는 선택된 경로 아래에 있는 폴더 목록을 가져오는 방법을 알아보자.

특정 폴더 아래의 폴더 목록 얻기

폴더명은 해당 오디오 파일 경로에서 파일 이름 부분을 지우면 된다. 즉 오디오 파일 경로에 해당하는 _data 필드값에 파일 이름값을 저장하고 있는 _display_name 필드값 부분을 잘라내어 지워주는 것이다. SQLite의 replace() 함수를 사용해서 지워준다고 하면

REPLACE(_data, _display_name, '')

물론 이렇게 하면 경로명의 다른 부분을 엉뚱하게 지워버릴 수 있으므로 다음과 같이 해야 의도된 결과를 얻을 수 있다.

SUBSTR(_data, 0, LENGTH(_data) - LENGTH(_display_name))

즉, 전체 경로에서 파일 이름 부분(_display_name)을 그 길이만큼 잘라버리면 그것이 폴더명이 된다.

그러면 다음과 같은 방식으로 오디오 파일을 포함하고 있는 모든 경로명을 가져올 수 있다.

SELECT DISTINCT SUBSTR(_data, 0, LENGTH(_data) - LENGTH(_display_name))

예를 들어서 다음과 같이 /sdcard/ 폴더가 있다고 하자.


sdcard +--Music (mp3 있음)
       +-+DownLoad (mp3 있음)
         +-- song (mp3 있음)
         +-+IU (mp3 없음)
           +-- 1st (mp3 있음)
           +-- 2nd (mp3 있음)

이 경우 다음과 같은 폴더 리스트가 얻어진다.


/storage/emulated/0/Music
/storage/emulated/0/DownLoad
/storage/emulated/0/DownLoad/song
/storage/emulated/0/DownLoad/IU/1st
/storage/emulated/0/DownLoad/IU/2nd

이때 /storage/emulated/0/DownLoad/IU 폴더가 목록에 포함되지 않음에 유의하자.

중간 단계의 모든 폴더를 포함시키려면, 예를 들어 /storage 아래의 모든 폴더 목록을 포함시키려면 getParentFile()을 이용해야 한다. 예를 들자면 /storage/emulated/0/Music 폴더에 대해서 getParentFile()을 이용해 Music, 0, emulated를 모두 얻으려면

List<File> folders = new ArrayList<File>();
File root = new File("/storage");
File dir = new File("/storage/emulated/0/Download");
do {
    if (dir.equals(root))
        break;
    if (folders.contains(dir))
        continue;
    folders.add(dir);
    dir = dir.getParentFile();
} while (dir != null);

(그런데 여기서 가만히 살펴보면, folders.contains(dir) 이 참이 되면 (폴더가 이미 포함되어 있으면) 그 상위 폴더도 모두 포함되어 있는 경우가 된다. 따라서 이 경우 continue 대신에 break를 쓰면 최종적으로 얻어지는 folders에 중복된 항목이 없고 더 간단하게 된다.

최종적으로 얻어지게 되는 모든 폴더 리스트는 다음과 같게 된다.

최상위 폴더를 /storage/라고 했을 경우
/storage/emulated
/storage/emulated/0
/storage/emulated/0/Music
/storage/emulated/0/DownLoad
/storage/emulated/0/DownLoad/song
/storage/emulated/0/DownLoad/IU
/storage/emulated/0/DownLoad/IU/1st
/storage/emulated/0/DownLoad/IU/2nd

이 폴더 목록은 /storage 하위의 모든 폴더 목록이다. 실제로 우리가 화면에 표시해야 할 목록은 선택된 폴더 바로 아래의 폴더 목록이다. 예를 들어 Download 폴더 아래의 하위 폴더는 song, IU 둘 뿐이므로 이 두개의 폴더를 얻어야 하는데, 이것은 dest 파일을 Download 폴더라고 했을 때 dir.getParentFile().equals(dest)인 dir 파일 목록을 찾는 것이다.

폴더목록 + 오디오 파일 목록 합치기

이제 우리가 얻은 폴더 목록과 오디오 파일 목록을 합해서 ListView로 보여주면 된다. 하나는 query()를 통해 얻은 Cursor이고, 다른 하나는 query()를 통해 그 결과값을 재 가공해서 얻은 배열/리스트이다. 이 둘을 ListView로 보여주려고 ArrayListAdapter()를 쓴다면 Cusor를 통해 얻은 목록을 재 가공해서 배열로 만드는 과정이 필요한데 이때 메모리를 별도로 필요로 하게 되는 등의 오버헤드가 더 발생한다. 따라서 이 방법 보다는 CursorAdapter를 써서 오버헤드가 덜 드는 방식이 나을 것이다. CursorAdapter를 쓰려면 배열(혹은 리스트)로 얻은 폴더 목록을 MaxtrixCursor 클래스를 사용해서 Cursor로 재가공해서 MergeCursor()를 사용해서 하나의 Cursor로 만들면 SimpleCursorAdapter를 사용할 수 있게 된다.

이러한 방식을 사용하여 클론 리플레이어의 오디오 보기 기본 리스트뷰를 폴더식 뷰와 함께 제공하게 되었다.


by dumpcookie 2015. 4. 14. 13:07