android contentProvider를 이용하여 공유 저장소 미디어 파일에 엑세스 하는 방법을 통한 예시와 공홈 번역 및 공부

2023. 5. 27. 17:21android

안드로이드의 가장 중요한 4가지 컴포넌트 중 하나인 content provider를 오늘 공홈의 영어판을 보며 공부하는 시간과 실제로 내 프로젝트에서 어떻게 사용했는지 한 번 확인하도록 하겠습니다. 자세한 contentProvider의 사용법이나 메서드, 구성 방법 등은 아직 미흡하고 경험이 부족한 관계로 자세하게는 다루지 못하고 기기 저장소에 접근하여 사진을 가져오는 방식을 예로 함께 공부하도록 하겠습니다.

 

컨텐트 프로바이더는 안드로이드 4가지 앱 구성 요소 중 하나입니다.

 

https://developer.android.com/guide/topics/providers/content-provider-basics

 

콘텐츠 제공자 기본 사항  |  Android 개발자  |  Android Developers

콘텐츠 제공자 기본 사항 컬렉션을 사용해 정리하기 내 환경설정을 기준으로 콘텐츠를 저장하고 분류하세요. 콘텐츠 제공자는 중앙 저장소로의 데이터 액세스를 관리합니다. 제공자는 Android 애

developer.android.com

https://developer.android.com/training/data-storage/shared/media?hl=ko#query-collection 

 

공유 저장소의 미디어 파일에 액세스  |  Android 개발자  |  Android Developers

DataStore는 로컬 데이터를 저장하는 최신 방법을 제공합니다. SharedPreferences 대신 DataStore를 사용해야 합니다. 자세한 내용은 DataStore 가이드를 참고하세요. 공유 저장소의 미디어 파일에 액세스 컬

developer.android.com


컨텐트 프로바이더는 데이터의 중앙 저장소에 대한 access를 관리한다고 합니다. 프로바이더는 앱의 일부분이며, 종종 데이터와 함께 작업하기 위한 자신만의 UI를 제공합니다. 그러나 컨텐트 프로바이더는 주로 다른 앱에 의해 프로바이더에 접근하려는 프로바이더 클라이언트 오브젝트에서 사용됩니다. 더불어, 프로바이더와 프로바이더 클라이언트는 프로세스 간 통신과 데이터 접근 보안을 다루는 데이터에 대한 일관되고 기준 인터페이스를 제공합니다. 

 

일반적으로 두 가지 상황, 시나리오에서 사용할 거라고 합니다.

1. 다른 앱에 존재하는 콘텐츠 프로바이더에 접근하려는 코드 구현

2. 두 번째는 내 앱에 다른 앱과 데이터를 공유하기 위한 new 콘텐츠 프로바이더를 만드는 것. 

 

이 페이지에서는 기존에 이미 있는 콘텐츠 프로바이더와의 기본 작업을 다룬다고 합니다. 즉 첫 번째 상황을 다룬다는 말인 거죠?? 그리고 내 앱에서 콘텐츠 프로바이더를 구현하는 방법은 링크를 참조하라고 하네요.



 

 

개요에는 여러 컴포넌트들과의 관계를 그림으로 보여주고 있네요. 첫 문단을 보면 콘텐츠 프로바이더는 데이터를 외부의 애플리케이션에 하나 이상의 관계형 데이터 베이스와 비슷한 테이블 형태로 제공한다고 합니다. 행은 콘텐츠 프로바이더가 수집한 데이터의 몇 개의 타입에 인스턴스를 표현하고 각 각의 열은 개별 데이터라고 합니다. 열의 구성요소는 뭐가 있을지는 봐야 알겠네요.

 

콘텐츠 프로바이더는 액세스를 조정하는데 앱의 데이터 스토리지 계층의 많은 API 들과 컴포넌트들의 액세스를 coordinate(효율적으로 서로 다른 애들이 작동하도록 만드는 것) 한다고 합니다. 

 

5가지가 있는데

1. 다른 애플리케이션과 내 애플리케이션의 데이터의 액세스를 공유

2. 위젯에 데이터 보내기

3. 내 애플리케이션에게 커스텀 서치 추천을 search framework의 SearchRecentSuggestionsProvider를 통해 반환하기

4. AbstractThreadedSyncAdapter 구현을 사용하여 애플리케이션 데이터를 서버와 동기화

5. CursorLoader를 사용하여 UI에 데이터 로딩

 

 


 

첫 줄에 중요한 정보가 나옵니다. 콘텐츠 프로바이더의 데이터에 접근하려면 애플리케이션의 Context에서ContentResolver 객체를 사용해 클라이언트로 제공자와 통신하라고 합니다. 콘텐츠 Resolver 객체는 콘텐츠 프로바이더를 구현하는 클래스의 인스턴스인 프로바이더 오브젝트와 통신한다고 합니다.

 

Provider 객체는 클라이언트로부터 즉 데이터를 사용하려는 쪽으로부터 요청을 받고, 요청된 작업을 수행한 다음 결과를 반환합니다. 이 Provider 객체는 메서드를 가지고 있는데 어떤 메서드냐 Content Provider의 하위 클래스에 인스턴스와 같은 이름의  동일한 이름의 메서드를 가지고 있다고 합니다. Content Resolver 메서드들은 기본적으로 CRUD 기능을 제공합니다. 

 

콘텐츠 프로바이더에 접근하기 위한 일반적인 패턴, 방법은 CursorLoader를 사용해 비동기 쿼리를 백그라운드에서 실행하는 거라고 합니다. 액티비티 혹은 프래그먼트에서 CusorLoader를 부르고 Content Resolver를 통해 Content Provider를 불러온다고 하네요

 

아래에는 이제 User Dictionary를 이용해 코드를 설명합니다. 여기에 제가 적은 스토리지에서 미디어를 가져오는 방법과 함께 각 기능을 살펴보겠습니다.

 

이제 content resolver를 이용해 데이터를 쿼리 하기 위한 각 매개변수를 알려주고 있습니다. 5개가 있으며 

1. Uri : 검색할 콘텐츠에 대해 content:// 방식을 사용하는 URI를 적어야 합니다. 

2. progection : 각 행에 대해 어떤 열을 가져올지 정하는 열 목록입니다. 여기에 어떻게 넣냐에 따라 반환하려는 열의 목록이 변경됩니다. null을 넣으면 하나의 항목 content에 관한 모든 열의 정보가 나오므로 비효율적입니다. 따라서 무엇을 가져올지 정해야 합니다.

3. selection : 여기는 where 절에 해당합니다. 어떤 행을 가져올지 여기서 필터 합니다. null을 지정하면 모든 행 즉 Uri에 있는 모든 content를 가져옵니다.

4. selectionArgs : 위와 약간 다르게 없으면?로 반환한다고 합니다.

5. sortOrder : 정렬 방식을 여기서 정할 수 있습니다.

 

 

만약 저장소에 있는 사진을 전부 가져오려고 합니다. 그런데 필요한 정보는 몇 개만 담은 채 모든 사진을 가져오려고 합니다. 일단 contentResolver를 context를 통해 선언합니다.

private val _context = context
private val _contentResolver = _context.contentResolver

 

 

그리고 projection으로 열에 무엇을 가져올지 결정합니다.

val projection = arrayOf(
    MediaStore.Images.Media._ID,
    MediaStore.Images.Media.DISPLAY_NAME,
    MediaStore.Images.Media.DATE_TAKEN,
    MediaStore.Images.Media.DATA
)

 

 

모든 사진을 가져올 것이기 때문에 selection과 selectionArgs는 넘어갑니다. 그리고 sortOrder로 정렬기준을 정해줍니다.

val sortOrder = "${MediaStore.Images.Media.DATE_TAKEN} DESC"

 

 

이제 선언한 contentResolver로 query 합니다.

_contentResolver.query(
    MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
    projection,
    null,
    null,
    sortOrder
)?

 

그리고 중요한 한 가지 Permission이 필요합니다.

 

프로바이더로부터 데이터를 읽어오려면 앱 내에서 읽기 액세스 퍼미션이 필요하다고 합니다. 런타임에 퍼미션을 요청할 수 없고 매니패스트 안에 프로바이더에 의해 정의된 정확한 퍼미션을 사용해야 합니다. 정확한 퍼미션을 알려면 provider 다큐먼트를 확인하라고 하네요 

 

현재 갤러리 접근을 위해 아래 두 개의 퍼미션을 선언했습니다.

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

 

이제 쿼리를 통해 결과를 받아오는 걸 알아보도록 하겠습니다.

 

그리고 미디어 스토리지에서 가져오는 방식의 코드를 보겠습니다. 

어떤가요?? 약간의 차이가 있지만 contentResolver를 통해 query 하고 while문을 통해 커서를 끝까지 반복적으로 확인하는 똑같은 구조를 하고 있습니다. 

 

이제 아래에 실제 미디어 스토리지에서 사진을 가져오는 코드를 보겠습니다.

 

class QueryImages @Inject constructor(
    @ApplicationContext val context: Context
)  {

    suspend fun getGalleryImages(): List<MediaStoreImage> {

        val images = mutableListOf<MediaStoreImage>()
        val projection = arrayOf(
            MediaStore.Images.Media._ID,
            MediaStore.Images.Media.DISPLAY_NAME,
            MediaStore.Images.Media.DATE_TAKEN,
            MediaStore.Images.Media.DATA
        )
        val sortOrder = "${MediaStore.Images.Media.DATE_TAKEN} DESC"

        val query = context.contentResolver.query(
            MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            projection,
            null,
            null,
            sortOrder
        )
        
        withContext(Dispatchers.IO) {
            query?.use { cursor ->
                val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
                val dateTakenColumn =
                    cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATE_TAKEN)
                val displayNameColumn =
                    cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)
                val dataColumn =
                    cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA)
                while (cursor.moveToNext()) {
                    val id = cursor.getLong(idColumn)
                    val dateTaken = Date(cursor.getLong(dateTakenColumn))
                    val displayName = cursor.getString(displayNameColumn)
                    val contentUri = Uri.withAppendedPath(
                        MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
                        id.toString()
                    )
                    val path = cursor.getString(dataColumn)
                    val image = MediaStoreImage(id, displayName, dateTaken, contentUri, path)
                    images += image
                }
            }
        }
        return images
    }
}

 

콘텐츠 프로바이더에 접근하기 위해 context를 이용한다고 하였는데 context에 있는 contextResolver에 접근하는 모습을 볼 수 있습니다. 그 후 쿼리에 변수로 projection 열에 해당하는 정보들을 선정하고 sortOrder를 설정합니다. query를 실행하면 첫 번째 항목 앞에 위치한 cursor를 반환합니다. 이때 오류나 반환할 게 없으면 null을 반환하고 아니라면 커서를 반환합니다. 이제 각 커서에 따른 정보를 getColumnIndexOrThrow를 통해 컬럼 이름을 매개변수로 대입하여 열 index를 가져오고 그걸 다시 cursor.getLong 이나 getString을 통해 요청된 열의 value를 반환합니다. 가져온 value들을 저는 data class를 만들어서 안에 두고 리스트에 넣어 반환하였습니다.

 

data class MediaStoreImage(
    val id: Long,
    val displayName: String,
    val contentUri: Uri,
    val path: String,
)

 

저장소의 미디어를 가져오기 위해 Context의 ContentResolver를 통해 사진을 가져오는 방법을 보았습니다.

 

컨텐트 프로바이더의 Insert, Delete, Update의 기능과 더 자세하게 다뤄야 하는 부분들이 남아있지만 오늘은 여기까지만 알아보고 이만 글을 마치겠습니다. 

 

요약

 

1. 컨텐트 프로바이더의 정의를 알아야 한다.

2. Content Provider를 사용하려면 Permission 을 Manifest에 선언해야 한다.

3. Provider와 Resolver의 차이를 알아야 한다.

3. Resolver로 데이터를 가져오기 위한 과정을 이해해야 한다.