Design-loving front-end engineer
Ryong
Design-loving front-end engineer
전체 방문자
오늘
어제
    • Framework
    • React
      • Concept
      • Library
      • Hook
      • Component
      • Test
    • NodeJS
    • Android
      • Concept
      • Code
      • Sunflower
      • Etc
    • Flutter
      • Concept
      • Package
    • Web
    • Web
    • CSS
    • Language
    • JavaScript
    • TypeScript
    • Kotlin
    • Dart
    • Algorithm
    • Data Structure
    • Programmers
    • Management
    • Git
    • Editor
    • VSCode
    • Knowledge
    • Voice
Design-loving front-end engineer

Ryong

[Android] [Kotlin] 콘텐트 리졸버
Android/Concept

[Android] [Kotlin] 콘텐트 리졸버

2021. 9. 23. 10:00

콘텐트 리졸버란?

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

✔  안드로이드에 있는 연락처, 갤러리, 음악 파일과 같은 기본 데이터를 이용하는 용도로 미리 만들어져 있는 콘텐트 프로바이더로부터 데이터를 가져오는 도구가 콘텐트 리졸버이다.

 

콘텐트 리졸버 사용하기

✔  안드로이드는 미디어 정보를 저장하는 저장소 용도로 MediaStore를 사용한다. 

✔  MediaStore 안에 각각의 미디어가 종류별로 DB의 테이블처럼 있고, 각 테이블당 주소가 하나씩 제공된다고 이해하면 된다.

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

✔  콘텐트 리졸버로 미디어 정보를 읽어오는 과정은 아래와 같다.

// 데이터 주소 지정
val listUrl = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI

◽  MediaStore는 테이블 주소들을 상수로 제공하며, 데이터베이스에서 테이블명과 같은 역할을 한다.

◽  데이터를 가져올 주소를 변수에 미리 저장한다.

 

// 가져올 컬럼명 지정
val proj = arrayOf(
    MediaStore.Audio.Media._ID,
    MediaStore.Audio.Media.TITLE
)

◽  테이블 주소와 마찬가지로 컬럼명도 상수로 제공한다.

◽  가져올 컬럼명을 배열에 저장해서 사용한다.

 

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

◽  클래스를 미리 만들어두면 읽어온 미디어 정보를 다루기가 쉬워진다.

 

// 쿼리 실행
val cursor = contentResolver.query(listUrl, proj, null, null, null)

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

 

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)
}

◽  커서 객체를 반복문에 따라 한 줄씩 읽어서 데이터 클래스에 저장한다.

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

 

음원 목록 앱 만들기

AndroidManifest.xml

더보기

<!-- MediaStore는 외부 저장소에 있기 때문에 외부 저장소 읽는 권한 -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>

 

BaseActivity.kt

더보기
abstract class BaseActivity : AppCompatActivity() {

    abstract fun permissionGranted(requestCode: Int)
    abstract fun permissionDenied(requestCode: Int)

    fun requirePermissions(permissions: Array<String>, requestCode: Int) {
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
            permissionGranted(requestCode)
        } else {
            val isAllPermissionsGranted = permissions.all { 
                checkSelfPermission(it) == PackageManager.PERMISSION_GRANTED }
            if (isAllPermissionsGranted) {
                permissionGranted(requestCode)
            } else {
                ActivityCompat.requestPermissions(this, permissions, requestCode)
            }
        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
            permissionGranted(requestCode)
        } else {
            permissionDenied(requestCode)
        }
    }
}

 

MainActivity.kt

더보기
class MainActivity : BaseActivity() {

    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), 999)
    }

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

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

    fun startProcess() {
        val adapter = MusicRecyclerAdapter()
        adapter.musicList.addAll(getMusicList())
        binding.recyclerView.adapter = adapter
        binding.recyclerView.layoutManager = LinearLayoutManager(this)
    }

    fun getMusicList() : List<Music> {
        // 1. 음원 정보 주소
        val listUrl = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI

        // 2. 음원 정보 컬럼들
        val proj = arrayOf(
            MediaStore.Audio.Media._ID,
            MediaStore.Audio.Media.TITLE,
            MediaStore.Audio.Media.ARTIST,
            MediaStore.Audio.Media.ALBUM_ID,
            MediaStore.Audio.Media.DURATION
        )

        // 3. 컨텐트리졸버의 쿼리에 주소와 컬럼을 입력하면 커서형태로 반환받는다
        val cursor = contentResolver.query(listUrl, proj, null, null, null)
        val musicList = mutableListOf<Music>()
        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)

            val music = Music(id, title, artist, albumId, duration)
            musicList.add(music)
        }
        return musicList
    }
}

 

Music.kt

더보기
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
    }

    // 음원 URI는 기본 MediaStore의 주소와 음원 ID를 조합해서 만들기 때문에 메서드로 만들어 놓고 사용하는 것이 편리
    fun getMusicUri(): Uri {
        return Uri.withAppendedPath(
            MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id
        )
    }

    // 앨범 아트 : 앨범 이미지
    fun getAlbumUri(): Uri {

        return Uri.parse(  // 문자열을 URI로 해석
            "content://media/external/audio/albumart/" + albumId
        )
    }

}

 

MusicRecyclerAdapter.kt

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

    var musicList = mutableListOf<Music>()
    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 getItemCount(): Int {
        return musicList.size
    }

    override fun onBindViewHolder(holder: Holder, position: Int) {
        val music = musicList.get(position)
        holder.setMusic(music)
    }

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

        init {
            binding.root.setOnClickListener {
                if(mediaPlayer != null) {  // 중복을 피하기 위해
                    mediaPlayer?.release()
                    mediaPlayer = null
                }
                mediaPlayer = MediaPlayer.create(binding.root.context, musicUri)
                mediaPlayer?.start()
            }
        }

        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()
        }
    }
}

 

activity_main.xml

더보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".MainActivity">
 
    <androidx.recyclerview.widget.RecyclerView
        android:id="@+id/recyclerView"
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />
 
</androidx.constraintlayout.widget.ConstraintLayout>
cs

 

item_recycler.xml

더보기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="100dp" >
 
    <ImageView
        android:id="@+id/imageAlbum"
        android:layout_width="90dp"
        android:layout_height="90dp"
        android:layout_marginStart="10dp"
        android:layout_marginLeft="10dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        tools:srcCompat="@tools:sample/avatars" />
 
    <TextView
        android:id="@+id/textArtist"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="8dp"
        android:layout_marginEnd="10dp"
        android:layout_marginRight="10dp"
        android:text="아티스트 이름"
        android:textSize="18sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/imageAlbum"
        app:layout_constraintTop_toTopOf="parent" />
 
    <TextView
        android:id="@+id/textTitle"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="10dp"
        android:layout_marginLeft="10dp"
        android:layout_marginTop="5dp"
        android:layout_marginEnd="10dp"
        android:layout_marginRight="10dp"
        android:text="음원 제목"
        android:textSize="24sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toEndOf="@+id/imageAlbum"
        app:layout_constraintTop_toBottomOf="@+id/textArtist" />
 
    <TextView
        android:id="@+id/textDuration"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginEnd="10dp"
        android:layout_marginRight="10dp"
        android:text="00:00"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textTitle" />
    
</androidx.constraintlayout.widget.ConstraintLayout>
cs

 

저작자표시 (새창열림)

'Android > Concept' 카테고리의 다른 글

[Android] [Kotlin] Retrofit  (0) 2021.09.25
[Android] [Kotlin] 네트워크  (0) 2021.09.25
[Android] [Kotlin] 서비스  (0) 2021.09.22
[Android] [Kotlin] 코루틴  (0) 2021.09.21
[Android] [Kotlin] 스레드와 루퍼  (0) 2021.09.20
    'Android/Concept' 카테고리의 다른 글
    • [Android] [Kotlin] Retrofit
    • [Android] [Kotlin] 네트워크
    • [Android] [Kotlin] 서비스
    • [Android] [Kotlin] 코루틴
    Design-loving front-end engineer
    Design-loving front-end engineer
    디자인에 관심이 많은 모바일 앱 엔지니어 Ryong입니다.

    티스토리툴바