콘텐트 리졸버란?
✔ 콘텐트 리졸버는 다른 앱에서 콘텐트 프로바이더를 통해 제공하는 데이터를 사용하기 위한 도구이다.
✔ 안드로이드에 있는 연락처, 갤러리, 음악 파일과 같은 기본 데이터를 이용하는 용도로 미리 만들어져 있는 콘텐트 프로바이더로부터 데이터를 가져오는 도구가 콘텐트 리졸버이다.
콘텐트 리졸버 사용하기
✔ 안드로이드는 미디어 정보를 저장하는 저장소 용도로 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 |