GDSC HUFS 3기/Android with Kotlin Team 1

[1팀] 컨텐트 제공자

dev-yen 2021. 11. 30. 23:29

이 글은 이것이 안드로이드다 with 코틀린(개정판)를 참고하여 작성하였습니다.

작성자 : 김예은

개발환경은 Windows, Android Studio입니다.

 

 

1. Android 4대 컴포넌트

Android 앱은 Kotlin, Java, C++ 언어를 사용하여 작성할 수 있다. Android SDK 도구는 모든 데이터 및 리소스 파일과 함께 코드를 컴파일하여 하나의 APK를 만든다. (즉 Android package는 접미사가 .apk인 아카이브 파일이다) 한 개의 APK 파일에는 Android 앱의 모든 콘텐츠가 들어 있으며, Android로 구동하는 기기가 앱을 설치할 때 바로 이 파일을 사용한다.

 

각 Android 앱은 자체적인 보안 샌드박스에 속하며, 이는 다음과 같은 Andorid 보안 기능으로 보호된다.

  • Android 운영체제는 멀티유저 Linux 시스템으로, 여기서 각 앱은 각기 다른 사용자와 같다.
  • 기본적으로 시스템이 각 앱에 고유한 Linux ID를 할당한다. 시스템은 앱 안의 모든 파일에 대해 권한을 설정하여 해당 앱에 할단된 사용자 ID만 이에 액세스할 수 있도록 한다.
  • 각 프로세스에는 자체적인 가상 머신(VM)이 있고, 그렇기 때문에 한 앱의 코드가 다른 앱과는 격리된 상태로 실행된다.
  • 기본적으로 모든 앱이 앱 자체의 Linux 프로세스에서 실행된다. Android 시스템은 앱의 구성 요소 중 어느 하나라도 실행해야 하는 경우 프로세스를 시작하고, 더 이상 필요없거나 시스템이 다른 앱을 위해 메모리를 복구해야 하는 경우 해당 프로세스를 종료한다.

앱 구성 요소

Android 4대 컴포넌트

앱 구성 요소는 Android 앱의 필수적인 기본 구성 요소이다. 각 구성 요소는 시스템이나 사용자가 앱에 들어올 수 있는 진입점이다. 다른 구성 요소에 종속되는 구성 요소도 있따.

앱 구성 요소에는 네 가지 유형이 있다.

  • 액티비티
  • 서비스
  • Broadcast Receiver
  • 콘텐츠 제공자

각 유형은 뚜렷한 목적을 수행하고 각자 나름의 수명주기가 있어 구성 요소의 생성 및 소멸 방식을 정의한다.

 

Activity

액티비티는 사용자와 상호작용하기 위한 진입점이다. 이것은 사용자 인터페이스를 포함한 화면 하나를 나타낸다.

  • Example
    • 예를 들어 이메일 앱의 경우 새 이메일 목록을 표시하는 액티비티가 하나 있고, 이메일을 작성하는 액티비티가 또 하나, 그리고 이메일을 읽는 데 쓰는 액티비티가 또 하나 있을 수 있다. 여러 액티비티가 함께 작동하여 해당 이메일 앱에서 짜임새 있는 사용자 환경을 구성하는 것은 사실이지만, 각자 서로 독립되어 있다. 따라서 이메일 앱에서 허용할 경우 다른 앱이 이런 액티비티 중 하나를 시작할 수 있다. 예를 들어 카메라 앱이라면 이메일 앱 안의 액티비티를 시작하여 사용자가 새 이메일을 작성하고 사진을 공유하게 할 수 있다.

액티비티는 다음과 같이 시스템과 앱의 주요 상호작용을 돕는다.

  • 사용자가 현재 관심을 가지고 있는 사항(화면에 표시된 것)을 추적하여 액티비티를 호스팅하는 프로세스를 시스템에서 계속 실행하도록 한다.
  • 이전에 사용한 프로세스에 사용자가 다시 찾을만한 액티비티(중단된 액티비티)가 있다는 것을 알고, 해당 프로세스를 유지하는 데 더 높은 우선순위를 부여한다.
  • 앱이 프로세스를 종료하도록 도와서 이전 상태가 복원되는 동시에 사용자가 액티비티로 돌아갈 수 있게 한다.
  • 앱이 서로 사용자 플로우를 구현하고 시스템이 이러한 플로우를 조정하기 위한 수단을 제공한다.

서비스

서비스는 여러 가지 이유로 백그라운드에서 앱을 계속 실행하기 위한 다목적 진입점이다. 이는 백그라운드에서 실행되는 구성 요소로, 오랫동안 실행되는 작업을 수행하거나 원격 프로세스를 위한 작업을 수행한다. 서비스는 사용자 인터페이스(UI)를 제공하지 않는다.

  • Example
    • 예를 들어, 서비스는 사용자가 다른 앱에 있는 동안에 백그라운드에서 음악을 재생하거나, 사용자와 액티비티 간의 상호작용을 차단하지 않고 네트워크를 통해 데이터를 가져올 수도 있다.

Broadcast Receiver

Broadcast Receiver는 시스템이 정기적인 사용자 플로우 밖에서 이벤트를 앱에 전달하도록 지원하는 구성 요소로, 앱이 시스템 전체의 브로드캐스트 알림에 응답할 수 있게 한다. Broadcast Receiver도 앱으로 들어갈 수 있는 또 다른 명확한 진입점이기 때문에 현재 실행되지 않은 앱에도 시스템이 브로드캐스트를 전달할 수 있다.

  • Example
    • 예를 들어, 앱이 사용자에게 예정된 이벤트에 대해 알리는 알림을 게시하기 위한 알람을 예약한 경우, 그 알람을 앱의 Broadcast Receiver에 전달하면 알람이 울릴 때까지 앱을 실행하고 있을 필요가 없다.

대다수의 브로드캐스트는 시스템에서 발생한다. 예컨대 화면이 꺼졌거나 배터리가 부족하거나 사진을 캡처했다고 알리는 브로드캐스트가 대표적이다.

 

Content Provider

콘텐츠 제공자는 파일 시스템, SQLite 데이터베이스, 웹상이나 앱이 액세스할 수 있는 다른 모든 영구 저장 위치에 저장 가능한 앱 데이터의 공유형 집합을 관리한다. 다른 앱은 콘텐츠 제공자를 통해 해당 앱 데이터를 쿼리하거나, 콘텐츠 제공자가 허용할 경우에는 수정도 가능하다.

콘텐츠 제공자는 앱 전용이어서 공유되지 않는 데이터를 읽고 쓰는데도 유용하다.

  • Example
    • 예를 들어, Android 시스템은 사용자의 연락처 정보를 관리하는 콘텐츠 제공자를 제공한다. 적절한 권한을 가진 앱이라면 콘텐츠 제공자를 쿼리하여 특정한 인물에 대한 정보를 읽고 쓸 수 있다.
    • 콘텐츠 제공자를 데이터베이스에 대한 추상화로 생각하기 쉽다. 이런 일반적인 사례에 대해 콘텐츠 제공자에 빌드된 API 및 지원이 많기 때문이다. 다만 시스템 설계 관점에서 볼 때 핵심 목적이 서로 다르다. 시스템의 경우 콘텐츠 제공자는 URI 구성표로 식별되고 이름이 지정된 데이터 항목을 게시할 목적으로 앱에 진입하기 위한 입구이다. 따라서 앱에서 URI 네임스페이스에 넣을 데이터를 매핑 할 방식을 결정하고, 해당 URI를 다른 엔터티에 전달할 수 있다. 이를 전달 받은 엔티티는 URI를 사용하여 데이터에 액세스 한다.
  • 콘텐츠 제공자는 외부 애플리케이션에 데이터를 표시하며, 이때 데이터는 관계형 데이터베이스에서 찾을 수 있는 테이블과 유사한 하나 이상의 테이블로 표시된다.
    • 행은 제공자가 수집하는 특정 데이터 유형의 인스턴스를 나타내고, 행의 각 열은 인스턴스에 대해 수집된 개별 데이터를 나타낸다.
    • 다른 애플리케이션과 내 애플리케이션 데이터에 대한 액세스 공유
    • 위젯에 데이터 전송
    • SearchRecentSuggestionsProvider 를 사용하여 검색 프레임워크를 통해 애플리케이션의 맞춤 추천 검색어 반환
    • AbstractThreadedSyncAdapter를 구현하여 서버와 애플리케이션 데이터 동기화
    • CursorLoader를 사용하여 UI에서 데이터 로드콘텐츠 제공자는 다음을 포함하여 그림 1에서와 같이 여러가지 API 및 구성요소에 관해 애플리케이션의 데이터 저장소 레이어 액세스를 조정한다.
  • ContentProvider Access 조정
  • 콘텐츠 제공자 내의 데이터에 액세스하고자 하는 경우, 애플리케이션의 Context에 있는 ContentResolver 객체를 사용하여 클라이언트로서 제공자와 통신을 주고 받으면 된다.
    • ContentResolver 객체가 제공자 객체와 통신하며, 이 객체는 ContentProvider를 구현하는 클래스의 인스턴스이다.
    • 제공자 객체가 클라이언트로부터 데이터 요청을 받아 요청된 작업을 실행하고 결과를 반환한다.
    • 이 객체에는 제공자 객체 내의 같은 이름을 가진 메서드를 호출하는 메서드가 있다.
    • ContentResolver 메서드는 영구 저장소의 기본적인 CRUD 기능을 제공한다.
  • UI에서 ContentProvider에 액세스하기 위한 일반적인 패턴에서는 CursorLoader를 사용하여 백그라운드에서 비동기식 쿼리를 실행한다.
    • UI의 Activity 또는 Fragment가 쿼리에 대해 CursorLoader를 호출하고 ContentResolver를 사용하여 ContentProvider를 가져온다.
    • 이렇게 하면 쿼리를 실행하는 동안 사용자에게 UI를 계속 제공할 수 있다.

ContentProvider 패턴

 


2. 뮤직 플레이어 만들어보기

layout 및 사전작업

1. AndroidManifest 에 권한 설정을 해준다.

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

 

2. RecyclerView에 사용할 item_layout 을 만든다.

item_layout.xml

3. activity_main을 RecyclerView로 설정하고 만들어둔 item_layout을 적용시킨다. 

activity_main.xml

->listitem 에서 만들어둔 layout 을 불러와서 적용시켜볼 수 있다.

 

Music class 

package com.example.contentresolver

import android.net.Uri
import android.provider.MediaStore

class Music(id: String, title:String?, artist: String?, albumId: String?, duration:Long?) {
    var id: String = "" // 음원 자체의 ID
    var title: String? = ""
    var artist: String? = ""
    var albumID: String? = "" //앨범 이미지 ID
    var duration: Long? = 0

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

    fun getMusicUri(): Uri {
        return Uri.withAppendedPath(MediaStore.Audio.Media.EXTERNAL_CONTENT_URI, id)
    }

    fun getAlbumUri(): Uri{
        return Uri.parse("content://media/external/audio/albumart/$albumID")
    }
}
  • 음원의 id, 제목, 가수, 앨범명, 재싱 길이를 담는 Music Class
  • id는 ContentResolver에서 하나의 음원을 가리키는 자체의 주소이며 null이 될 수 없다.
  • getMusicUri()메서드를 통해 음악 Uri를 가져온다.(음원을 가져옴)
  • getAlbumUri()메서드를 사용해 앨범아트 Uri를 가져온다. albumID의 경우 MediaStore에 지정된 path 메서드가 없으므로 path를 다 써주었다.

 

Music Adapter

package com.example.contentresolver

import android.media.MediaPlayer
import android.net.Uri
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import com.example.contentresolver.databinding.ItemLayoutBinding
import java.text.SimpleDateFormat

class MusicAdapter : RecyclerView.Adapter<MusicAdapter.Holder>() {
    val musicList = mutableListOf<Music>() 
    var mediaPlayer:MediaPlayer? = null

    override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): Holder {
        val binding = ItemLayoutBinding.inflate(LayoutInflater.from(parent.context), parent, false)
        return Holder(binding)
    }

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

    override fun getItemCount(): Int {
        return musicList.size
    }

    inner class Holder(val binding:ItemLayoutBinding) : 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){
            musicUri = music.getMusicUri()
            with(binding){
                imagealbum.setImageURI(music.getAlbumUri())
                textArtist.text = music.artist
                textTitle.text = music.title
                val sdf = SimpleDateFormat("mm:ss")
                textDuration.text = sdf.format(music.duration)
            }
        }
    }

}
  • musicList 는 어댑터가 사용할 컬렉션이다
  • mediaPlayer는 재생 중인 음악을 담는다
  • Holder 의 setOnClickListener는 음원을 클릭하면 재생하는 리스너인데, mediaPlayer 가 null이 아니라면, 다른 음원이 재생중인 것이므로, 해당 음원을 release시키고, 미디어 플레이어가 음원을 재생시킬 수 있도록 한다.
  • 이때, Holder를 adapter클래스 안에 inner class로 생성한 이유는, 음원이 클릭될때마다 mediaPlayer를 생성하는게 리소스 낭비가 있기 때문에, 아예 adapter 클래스의 전역변수로 mediaPlayer를 만들어서 사용하기위해 inner class로 생성하였다.

 

MainActivity

package com.example.contentresolver

import android.Manifest
import android.content.pm.PackageManager
import android.os.Build.ID
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.provider.MediaStore
import android.widget.LinearLayout
import android.widget.Toast
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager
import com.example.contentresolver.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

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

    val permission = Manifest.permission.READ_EXTERNAL_STORAGE
    val REQ_READ = 99


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        if(isPermitted()){
            startProcess()
        }else{
            ActivityCompat.requestPermissions(this, arrayOf(permission), REQ_READ)
        }
    }

    fun startProcess(){
        val adapter = MusicAdapter()
        adapter.musicList.addAll(getMusiclist())

        binding.recyclerView.adapter = adapter
        binding.recyclerView.layoutManager = LinearLayoutManager(this)
    }

    fun getMusiclist() : List<Music> {
        //컨텐트 리졸버로 음원 목록 가져오기
        //1. 데이터 테이블 주소
        val musicListUri = 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(musicListUri, proj, null, null, null)
        //4. 커서로 전달받은 데이터를 꺼내서 저장
        val musicList = mutableListOf<Music>()
        while(cursor?.moveToNext() ?: false){
            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
    }


    fun isPermitted() : Boolean{
        return ContextCompat.checkSelfPermission(this, permission) == PackageManager.PERMISSION_GRANTED
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if(requestCode == REQ_READ){
            if(grantResults[0] == PackageManager.PERMISSION_GRANTED){
                startProcess()
            }else{
                Toast.makeText(this, "권한 요청을 승인해야 앱을 실행할 수 있슴다.", Toast.LENGTH_SHORT).show()
                finish()
            }
        }
    }
}

 

✔About ContentResolver query

// Queries the user dictionary and returns results
cursor = contentResolver.query(
        UserDictionary.Words.CONTENT_URI,   // The content URI of the words table
        projection,                        // The columns to return for each row
        selectionClause,                   // Selection criteria
        selectionArgs.toTypedArray(),      // Selection criteria
        sortOrder                          // The sort order for the returned rows
)

query(Uri, projection, selection, selectionArgs, sortOrder)이는 SQL SELECT문과 일치한다.