[Android] 11(2). 콘텐트 리졸버

8 minute read


콘텐트 리졸버

콘텐트 리졸버는 다른 앱에서 콘텐트 프로바이더를 통해 제공하는 데이터를 사용하기 위한 도구입니다.

image-20210826172133472

만약 내가 만든 앱의 데이터를 다른 앱에서도 사용할 수 있게 하려면 콘텐트 프로바이더를 구현해야 합니다.

하지만 보통 앱을 개발하면서 콘텐트 프로바이더를 사용하는 일은 거의 없습니다. 대부분 다른 앱 또는 OS에 이미지 구현되어 있는 콘텐트 프로바이더로부터 데이터를 제공받아 사용합니다.


콘텐트 리졸버 사용하기


콘텐트 리졸버로 사진, 음악 파일 등을 읽어오려면 미디어 정보가 저장된 구조를 이해해야 합니다.

안드로이드는 미디어 정보를 저장하는 저장소 용도로 MediaStore를 사용합니다. MediaStore 안에 각각의 미디어가 종류별로 DB의 테이블처럼 있고, 각 테이블 당 주소가 하나씩 제공됩니다.

미디어의 종류마다 1개의 주소를 가진 콘텐트 프로바이더가 구현되어 있다고 생각하면 됩니다.

image-20210826172527594

그리고 미디어를 읽어오기 위해 콘텐트 리졸버를 사용합니다.

콘텐트 리졸버로 미디어 정보를 읽어오는 과정은 다음과 같습니다.


1. 데이터 주소 정의

MediaStore는 테이블 주소들을 사수로 제공하며 데이터베이스에서 테이블명과 같은 역할을 합니다. 데이터를 가져올 주소를 변수에 미리 저장합니다.

val listUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI


2. 가져올 컬럼명 정의

미디어 정보의 상세 데이터 중 원하는 데이터만 선택해서 읽어올 수 있습니다.

테이블 주소와 마찬가지로 컬럼명도 상수로 제공합니다. 가져올 컬럼명을 배열에 저장해서 사용합니다.

val proj = arrayOf(
	MediaStore.Audio.Media._ID, 
	MediaStore.Audio.Media.TITLE
)


3. 데이터 클래스 정의

앞에서 정의한 컬럼명에 맞춰 데이터 클래스를 생성합니다.

클래스를 미리 정의해두면 읽어온 미디어 정보를 다루기가 수월해집니다. 꼭 데이터 클래스를 사용해야 하는 것은 아닙니다.

data class Music(val id: String, val title: String)


4. 쿼리 실행

콘텐트 리졸버가 제공하는 query( ) 메서드에 앞에서 정의한 주소와 컬럼명을 담아서 호출하면 쿼리를 실행한 결과를 커서라는 형태로 반환합니다.

세번째, 네번째, 다섯 번째 파라미터는 쿼리에 조건을 설정하는 옵션용입니다. ‘null’ 을 입력하면 전체 데이터를 읽어옵니다.

val cursor = contentResolver.query(listUrl, proj, null, null, null)


query( )의 파라미터 5개

파라미터 설명
uri: Uri 테이블의 주소 Uri
projection: String[ ] 테이블의 컬럼명 배열
selection: String 데이터 검색 조건. 어떤 컬럼을 검색할 것인지 컬럼명 지정
(name = ?, title = ?의 형태로 물음표와 함께 검색 컬럼을 지정)
selectionArgs: String[ ] 조건의 값. 세번재 컬럼명에 입력할 값
(selection에서 지정한 물음표(?)를 앞에서부터 순서대로 대체(물음표의 개수만큼 필요))
sortOrder: String 정렬 순서. 정렬할 컬럼이 오름차순인지 내림차순인지를 설정
(ORDER BY title ASC)


5. 커서를 이용하여 읽은 데이터를 데이터 클래스에 저장

전달받은 커서 객체를 반복문으로 반복하며 레코드(컬럼으로 구성된 데이터 한 줄)를 한 줄씩 읽어서 데이터 클래스에 저장합니다.

getColumnIndex( ) 메서드는 접근할 컬럼이 현재 테이블의 몇 번째 컬럼인지 확인한 다음 인덱스를 반환합니다.

val musicList = mutableListOf<Music>()
while (cursor.moveToNext()){
    var index = cursor.getColumnIndex(proj[0])
    val id = cursor.getString(index)
    
    index = cursor.getColumnIndex(proj[1])
    val title = cursor.getString(index)
    
    val music = Music(id, title)
    musicList.add(music)
}

커서로 반환된 값들을 proj 배열의 컬럼 순서대로 반환되지 않기 때문에 반드시 인덱스를 확인하는 과정이 필요합니다.



음원 목록 앱 만들기


앞에서 살펴본 콘텐트 리졸버 사용법을 응용해서 MediaStore에서 실제 음원 목록을 가져와 화면에 출력하는 앱을 만들어보겠습니다.


메니페스트에 명세하고 권한 요청하기

1. AndroidManifest.xml에 외부 저장소 권한 명세하기

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


2. 프로젝트에 BaseActivity( ) 추가하기

권한을 요청하고 처리하기 위해 앞선 [Android] 6(3). BaseActivity 설계하기 포스팅에서 설계했던 BaseActivity를 프로젝트에 추가합니다.


3. BaseActivity 상속해서 메서드 구현하기

MainActivity가 BaseActivity를 상속하도록 변경하고 구현해야 할 추상 메서드들을 오버라이드합니다.

permissionGranted 에서 호출되는 startProcess 메서드는 뒤에서 작성합니다.

class MainActivity : BaseActivity() {

    companion object{
        const val PERM_STORAGE = 99
    }

    val binding by lazy {ActivityMainBinding.inflate(layoutInflater)}

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding.root)

        // 외부 저장소 권한 요청
        requirePermissions(arrayOf(Manifest.permission.READ_EXTERNAL_STORAGE), PERM_STORAGE)
    }

    override fun permissionGranted(requestCode: Int) {
        startProcess()
    }

    override fun permissionDenied(requestCode: Int) {
        when(requestCode){
            PERM_STORAGE -> {
                Toast.makeText(this,
                    "외부 저장소 권한 승인이 필요합니다. 앱을 종료합니다.",
                    Toast.LENGTH_LONG).show()

                finish()
            }
        }
    }
    // 음원 목록을 불러오는 메서드
    // 어댑터와 화면, 데이터를 가져와서 연결
    fun startProcess(){
    }
}


음원 클래스 정의하기

데이터베이스나 컨텐트 리졸버 등을 통해 데이터를 주고받을 때는 데이터에 대한 클래스를 정의하는 것이 좋습니다.

1. Music 클래스 정의하기

한 묶음의 데이터를 정의할 Music 클래스를 다음과 같이 정의합니다.

class Music(id: String, title: String?, artist: String?, albumId: String?, duration: Long?) {

    /* 프로퍼티 정의 */
    var id: String = ""
    var title: String?
    var artist: String?
    var albumId: String?
    var duration: Long?

    init{
        this.id = id
        this.title = title
        this.artist = artist
        this.albumId = albumId
        this.duration = duration
    }
}


2. getMusicUri( ) 메서드 정의

음원의 URI를 생성하는 getMusicUri( ) 메서드를 정의합니다. 음원 URI는 기본 MediaStore의 주소와 음원 ID를 조합해서 만들기 때문에 메서드로 만들어놓고 사용하는 것이 편리합니다.

    // 음원의 URI 생성
    fun getMusicUri(): Uri{
        return Uri.withAppendedPath(
            // 음원 URI는 기본 MediaStore의 주소와 음원 ID를 조합
            MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id
        )
    }


3. getAlbumUri( ) 메서드 정의

음원 파일별로 썸네일을 지정할 수 있습니다.

보통 앨범 이미지를 사용하며 이것을 앨범 아트라고 하는데, 앨범 아트 URI를 생성하는 getAlbumUri( ) 메서드를 정의합니다. 앨범 아트의 URI 문자열을 Uri.parse( ) 메서드로 해석해서 URI를 생성합니다.

    // 음원 파일별로 썸네일 지정(앨범 아트 Uri 생성)
    fun getAlbumUri(): Uri{
        return Uri.parse(
            "content://media/external/audio/albumart/" + albumId
        )
    }



음원 목록 화면 만들기

음원 목록을 화면에 출력하기 위해 리사이클러 뷰를 사용합니다.

1. activity_main.xml 레이아웃 구성

activity_main.xml을 열고 레이아웃을 다음과 같이 구성합니다.

image-20210826212426221

기존에 있던 텍스트뷰를 삭제하고 리사이클러 뷰를 배치한 뒤 컨스트레인트를 연결한 것이 전부입니다.


2. item_recycler.xml 레이아웃 구성

리사이클러 뷰의 아이템의 레이아웃을 다음과 같이 구성합니다.

image-20210826212600578

  • 리니어 레이아웃: layout_height=100dp
  • 이미지뷰: id = imageAlbum
  • 텍스트뷰 1: id=textArtist
  • 텍스트뷰 2: id=textTitle
  • 텍스트뷰 3: id=textDuration



어댑터 만들기

리사이클러 뷰의 어댑터 클래스 MusicRecyclerAdapter 클래스를 생성합니다.

// 리사이클러 뷰 어댑터 클래스
class MusicRecyclerAdapter: RecyclerView.Adapter<MusicRecyclerAdapter.Holder>() {

    // 앨범 아이템 목록 리스트
    var musicList = mutableListOf<Music>()

    // 뷰 홀더 생성
    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
        val binding = ItemRecyclerBinding.inflate(LayoutInflater.from(parent.context),
            parent, false)
        return Holder(binding)
    }
    // 뷰 홀더를 화면에 출력
    override fun onBindViewHolder(holder: Holder, position: Int) {
        val music = musicList.get(position)
        holder.setMusic(music)
    }
    // 아이템 목록 개수 반환
    override fun getItemCount(): Int {
        return musicList.size
    }
}

// 뷰 홀더 클래스
class Holder(val binding: ItemRecyclerBinding): RecyclerView.ViewHolder(binding.root){
    // 현재 선택된 음원(음악 플레이 대비)
    var currentMusic: Music? = null
    // 아이템에 음원 정보 세팅
    fun setMusic(music: Music){
        binding.run{
            imageAlbum.setImageURI(music.getAlbumUri())
            textArtist.text = music.artist
            textTitle.text = music.title

            val duration = SimpleDateFormat("mm:ss").format(music.duration)
            textDuration.text = duration
        }
        this.musicUri = music.getMusicUri()
    }
}

어댑터에 대한 코드는 앞선 포스팅에서 충분히 다뤘으니 설명은 주석으로 대체하도록 하겠습니다.

혹시 리사이클러 뷰 어댑터 클래스의 코드가 익숙하지 않으신 분은 [Android] 5(2). 컨테이너 (목록 만들기) 포스팅을 참고해주세요.



MainActivity에서 음원 목록 보여주기

이제 MainActivity.kt에 음원 정보를 읽어오고 리사이클러 뷰에 음원 목록을 보여주는 코드를 작성하겠습니다.

1. 음원을 읽어오는 getMusicList( ) 메서드 정의

    // 음원을 읽어오는 메서드
    fun getMusicList(): List<Music>{
        // 음원 정보의 테이블 주소를 listUrl 변수에 저장
        val listUrl = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
        // 음원 정보 테이블에서 읽어올 컬럼명을 배열로 정의
        val proj = arrayOf(
            MediaStore.Audio.Media._ID,
            MediaStore.Audio.Media.TITLE,
            MediaStore.Audio.Media.ARTIST,
            MediaStore.Audio.Media.ALBUM_ID,
            MediaStore.Audio.Media.DURATION
        )
        // 콘텐트 리졸버의 query() 메서드에 테이블 주소와 컬럼명을 전달하여 호출(커서 반환)
        val cursor = contentResolver.query(listUrl, proj, null, null, null)
        // 커서로 전달받은 데이터를 꺼내서 저장할 목록 변수 생성
        val musicList = mutableListOf<Music>()
        // 데이터를 읽어서 musicList 에 담기
        while(cursor?.moveToNext() == true){
            val id = cursor.getString(0)
            val title = cursor.getString(1)
            val artist = cursor.getString(2)
            val albumId = cursor.getString(3)
            val duration = cursor.getLong(4)
        }
        // 데이터가 담긴 musicList 반환
        return musicList

    }

코드의 흐름은 다음과 같습니다.

  1. 테이블 주소 지정
  2. 읽어올 컬럼명 지정
  3. 콘텐트 리졸버의 query( ) 메서드에 전달하며 호출하여 커서 반환
  4. 커서를 이용해 데이터를 읽으며 리스트에 담기
  5. 데이터가 담긴 리스트 반환


2. startProcess( ) 메서드 정의

앞에서 작성하지 않았던 startProcess( ) 메서드를 작성합니다.

startProcess( ) 메서드는 지금까지 생성한 어댑터와 화면 그리고 데이터를 가져와서 연결합니다.

    // 음원 목록을 불러오는 메서드
    // 어댑터와 화면, 데이터를 가져와서 연결
    fun startProcess(){
        // 어댑터 생성
        val adapter = MusicRecyclerAdapter()
        // 읽어온 음원 리스트를 어댑터에 전달
        adapter.musicList.addAll(getMusicList())
        // 리사이클러 뷰에 어댑터와 레이아웃 매니저 연결
        binding.recyclerView.adapter = adapter
        binding.recyclerView.layoutManager = LinearLayoutManager(this)
    }


에뮬레이터에 MP3 다운로드 받기

  1. 에뮬레이터에서 웹 브라우저를 실행하고 검색창에 ‘free mp3 downloads’ 를 입력한 후 검색합니다.
  2. 검색 내역 중에 Last.fm이라는 음원 사이트를 클릭하고 Download Free Music 페이지로 이동합니다.
  3. 다시 스크롤 해보면 음원 모곩이 나타나는데, 목록 오른쪽의 다운로드 버튼을 클릭하면 MP3 파일을 에뮬레이터에 다운로드할 수 있습니다.

몇 개 다운로드 한 다음 앱을 테스트해보세요.

image-20210826214513489



목록을 클릭해서 음원 실행하기

마지막으로 리사이클러 뷰의 목록에 있는 아이템을 클릭하면 음원이 실행되도록 코드를 수정/추가해보겠습니다.

1. Music 클래스에 isPlay 프로퍼티 추가

선택된 음원의 실행 중이었는 지를 나타내기 위해 Music 클래스에 isPlay 프로퍼티를 추가합니다.

class Music(id: String, title: String?, artist: String?, albumId: String?, duration: Long?) {

    /* 프로퍼티 정의 */
    var id: String = ""
    var title: String?
    var artist: String?
    var albumId: String?
    var duration: Long?
    // 목록을 클릭해서 음원 실행하기: 실행 여부 플래그
    var isPlay = false

    init{
        this.id = id
        this.title = title
        this.artist = artist
        this.albumId = albumId
        this.duration = duration
    }

    // 음원의 URI 생성
    fun getMusicUri(): Uri{
        return Uri.withAppendedPath(
            // 음원 URI는 기본 MediaStore의 주소와 음원 ID를 조합
            MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id
        )
    }

    // 음원 파일별로 썸네일 지정(앨범 아트 Uri 생성)
    fun getAlbumUri(): Uri{
        return Uri.parse(
            "content://media/external/audio/albumart/" + albumId
        )
    }
}


2. Holder 클래스를 어댑터 클래스 내부로 이동

아이템을 클릭하면 음원이 실행되도록 해야 하므로 이 클릭 이벤트는 Holder 클래스에서 받게 됩니다.

음원을 실행하려면 MediaPlayer 클래스를 사용해야 하는데, 각 아이템마다 MediaPlayer 인스턴스를 하나씩 갖게 되면 자원이 낭비됩니다.

따라서 Holder 클래스를 어댑터 클래스 내부로 이동시키고 어댑터 클래스에 MediaPlayer 인스턴스를 하나 생성합니다.

class MusicRecyclerAdapter: RecyclerView.Adapter<MusicRecyclerAdapter.Holder>() {

    var musicList = mutableListOf<Music>()
    // 목록을 클릭해서 음원 실행하기: MediaPlayer 인스턴스 생성
    var mediaPlayer: MediaPlayer? = null

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
        val binding = ItemRecyclerBinding.inflate(LayoutInflater.from(parent.context),
            parent, false)
        return Holder(binding)
    }
    override fun onBindViewHolder(holder: Holder, position: Int) {
        val music = musicList.get(position)
        holder.setMusic(music)
    }
    override fun getItemCount(): Int {
        return musicList.size
    }

    // 목록을 클릭해서 음원 실행하기: 뷰 홀더 클래스를 내부 클래스로
    // 뷰 홀더 클래스
    inner class Holder(val binding: ItemRecyclerBinding): RecyclerView.ViewHolder(binding.root){
        var currentMusic: Music? = null
        
        fun setMusic(music: Music){
            binding.run{
                imageAlbum.setImageURI(music.getAlbumUri())
                textArtist.text = music.artist
                textTitle.text = music.title

                val duration = SimpleDateFormat("mm:ss").format(music.duration)
                textDuration.text = duration
            }
        }
    }
}


3. 음원 재생/정지 로직 추가

이제 Holder 클래스 내부에 아이템이 선택되었을 때 상황에 따라 음원을 재생/정지하는 로직을 추가합니다.

inner class Holder(val binding: ItemRecyclerBinding): RecyclerView.ViewHolder(binding.root){
        var currentMusic: Music? = null

        // 목록을 클릭해서 음원 실행하기: itemView 에 클릭리스너 연결
        init{
            // 뷰홀더에 클릭 리스너 달기
            itemView.setOnClickListener {
                // 선택된 음악이 실행 중이 아니라면,
                if (currentMusic?.isPlay == false) {
                    // 현재 실행 중인 음악이 있으면 종료
                    if (mediaPlayer != null) {
                        mediaPlayer?.release()
                        mediaPlayer = null
                    }
                    // 선택한 아이템의 음악 플레이
                    mediaPlayer = MediaPlayer.create(itemView.context, currentMusic?.getMusicUri())
                    // mediaPlayer = MediaPlayer.create(itemView.context, musicUri)
                    mediaPlayer?.start()
                    currentMusic?.isPlay = true
                }else{ // 선택된 음악이 실행 중이었다면 현재 음악 중지
                    mediaPlayer?.stop()
                    mediaPlayer = null
                    currentMusic?.isPlay = false
                }
            }
        }



이로써 콘텐트 리졸버를 이용한 음원 목록 앱 만들기도 끝났습니다.

여기에 몇 가지 코드를 수정/추가함으로써 버튼으로 음원을 재생/정지하도록 할 수도 있습니다.

이에 대한 힌트는 깃허브 저장소를 확인해주세요.



정리


  • 콘텐트 프로바이더는 내 앱의 데이터를 다른 앱에서 사용할 수 있도록 인터페이스를 제공하는 안드로이드 컴포넌트입니다.
  • 콘텐트 리졸버는 다른 앱에서 콘텐트 프로바이더를 통해 제공하는 데이터를 사용하기 위한 보조 도구입니다.
  • 콘텐트 리졸버를 통해 미디어 정보를 읽어오는 과정은 데이터 주소 정의 ➡ 가져올 컬럼명 정의 ➡ 데이터 클래스 정의 ➡ 쿼리 실행 ➡ 커서를 이용하여 읽은 데이터를 클래스에 저장 순으로 이어집니다.
  • 음원을 실행할 때는 MediaPlayer 클래스를 이용합니다.

Categories:

Updated:

Leave a comment