본문 바로가기
Android 개발/Android

Contentprovider 와 ContentResolver 이용한 CRUD # Mime-type Uri

by Developer88 2022. 11. 18.
반응형

오늘은 ContentProvider 와 ContentResolver에 대해서 정리해 보도록 하겠습니다.

 

1. Content Provider 와 ContentResolver

1-1. Content Provider

원래 Data들은 되도록이면 각 앱별로 Private하게 저장하고 Access하는 것이 안전한데요.

 

반대로 주소록이나 전화기록, Media(Audio, Video, Photo) 데이터들은 그렇지 않습니다.

예를 들어서, 주소록 데이터를 앱별로 앱내 Private Directory에 저장하기 보다는,

ContentProvider에 저장하고, 다른 앱에서도 공유해서 사용하는 것이지요.

 

이러한 Data들은 앱들간에 자유롭게 Access 할 수 있어야 하는 부분입니다.

ContentProvider는 컨텐츠를 앱에 제공해주는(Provide) Android의 중요한 컴포넌트 입니다.

 

ContentProvider를 시스템에서 제공하는 DB라고 생각하는 것이 이해하기가 쉬운데요.

API1부터 만들어져 있던 고전적이고 중요한 abstract class 입니다.

그래서인지, API 사용방법은 RoomDB같은 최신 라이브러리만큼 편하지 않습니다.

 

 

대표적으로 앱간에 공유하는 데이터들은 다음과 같은 것들이 있습니다.

  • CalendarContract(일정)
  • CallLog(전화기록)
  • ContactsContract(주소록)
  • MediaStore(미디어 파일들)
    • Image
    • Audio
    • Video
  • SMS/MMS(API19)

 

 

ContentProvider는 데이터를 캡슐화하구요.

이에 접근하기 위해서는 ContentResolver 를 사용해야 합니다.

 

1-2. ContentResolver

Content Provider의 데이터 model에 접근하기 위해서는 ApplicationContent의 ContentResolver 객체를 이용해야 합니다.

쉽게 말해서, 공용DB격인 ContentProvider를 사용하기 위해서는,

ContentResolver객체를 이용해야 한다는 것 이지요.

 

이 클래스의 함수들은 Uri를 이용해서,

ContentProvider로부터 데이터의 CRUD(create, retrieve, update, and delete)를 가능하게 해줍니다.

역시나 API1부터 있던 API입니다.

 

 

2. URI와 Mime-type

위에서 ContentResolver의 함수들이 Uri를 이용해서 CRUD를 구현해 준다고 하였는데요.

ContentResolver를 잘 사용하기 위해서는 Uri 와 Mime-type 에 대한 이해가 필요합니다.

특히 URI는 해당 데이터에 접근하는 액세스포인트이기 때문에 잘 알고 있어야 합니다.

 

2-1. URI

데이터를 얻어올 때 가장 중요한 것은 해당 데이터의 Uri입니다.

Uri(Uniform Resource Identifier)라고 하는 리소스에 부여된 Id를 알아야 해당 데이터를 가져올 수 있는 것 인데요.

보통은 아래와 같은 형식을 띄게 됩니다.

이 URI는 rfc3986 이라는 규약에 정의되어 있는데요.

아래와 같이, 프로토콜 스킴, authority, path 등으로 구성이 되어 집니다.

참고로 ContentProvider의 프로토콜 스킴은 "content" 입니다.

 

 

 

ContentProvider의 주소록은 어떻게 될까요?

프로토콜스킴은 "content"를 사용합니다.

그리고 authority는 주소록의 "contacts"이구요. 

path는 "/x/1"로 정의하였습니다.

 

content://contacts/x/1 
(프로토콜 스킴://autority/path)

 

아래의 예들도 모두 URI의 형식으로 정의된 것 입니다.

 

ftp://ftp.is.co.za/rfc/rfc1808.txt

http://www.ietf.org/rfc/rfc2396.txt

ldap://[2001:db8::7]/c=GB?objectClass?one

mailto:John.Doe@example.com

news:comp.infosystems.www.servers.unix

tel:+1-816-555-1212

telnet://192.0.2.16:80/

urn:oasis:names:specification:docbook:dtd:xml:4.1.2

 

2-2. Mime-type

ContentResolver를 이용할 때 한가지 더 알아두어야 할 것이 Mime-type인데요.

보통 아래와 같은 형식을 가지고 있습니다.

아래에서는 audio타입의 mp3인데요.

 

audio/mp3
(type/subtype)

 

이러한 Mime-type을 알아야 유저에게 어떠한 방식으로 그 데이터를 보여줄지 알수 있기 때문인데요.

예를 들어, 음악이면 들려주고, 영상이라면 화면에 보여주어야 하겠지요.

Mime-type은 큰 카테고리의 type과 하위 카테고리인 subtype으로 규정되어 있습니다.

mp3파일이라면, 아래와 같이 규정할 수 있는 것 이지요.

 

자주 사용하는 mime type들은 다음과 같습니다.

 

mime-type 종류
image image/gif, image/png, image/jpeg, image/bmp, image/webp
audio audio/mp3, audio/mpeg, audio/webm, audio/ogg, audio/wav, audio/midi
video video/webm, video/ogg
application application/pdf, 
application/octet-stream,
application/xml,
application/zip

 

 

해당 데이터의 Uri를 알고 있다면, getType()함수를 통해서 Mime-type을 알아낼 수도 있습니다.

 

 

3. Content Resolver

3-1. Content Resolver가 할 수 있는 일들

위에서 ContentProvider에 접근하기 위해서는, ContentResolver 클래스를 이용해야 한다고 하였는데요.

단순하게 Content Provider에 접근해서 데이터를 변경하는 일 이외에도,

ContentProvider의 데이터와 동기화 한다거나, Observer를 등록하거나 해지해서 변경사항에 대해 알림을 받거나,

InputStream이나 OutputStream을 생성해서 File을 생성할 수도 있게 해 줍니다.

 

구분 주요 함수들
ContentProvider에 접근해서 데이터 변경 query(), insert(), bulkInsert(), update(), delete(), 
applyBatch(),
getType()
Sync
(동기화)
addPeriodicSync(), getPeriodicSyncs(), removePeriodicSync()
File Input/Output openInputStream, openOutputStream(), openAssetFileDescriptor()[*api19]
Observer 등록 registerContentObserver(), unregisterContentObserver(),
notifyChange()

 

 

4. ContentResolver를 이용한 CRUD

이제 ContentResolver를 이용한 Crud(Create, Read, Update, Delete) 에 대해서 정리해 보겠습니다.

 

4-1. insert

ContentResolver를 이용한 insert는 간단한데요.

return값은 Uri인데요. ContentProvider가 crash가 나거나 할 경우는 null을 return해 줍니다.

 

 

첫번째 인자로 들어가는 값은, table의 URL이구요.

두번째 인자로 들어가는 값은 ContentValues입니다.

 

contentValues는 key/value저장되어지는데요.

key는 Column이름으로 사용이 됩니다. 만약, null을 전달하면, 비어있는 row가 생성되어 집니다.

ContentValues클래스는 put()함수들을 가지고 있어서, key/value페어로 값을 저장할 수도 있습니다.

 

아무래도 코드를 보는 것이 이해가 빠른데요.

아래 코드를 보면, ContentValue를 생성하는 것을 볼 수 있습니다.

위에서 알아본데로, MediaStore에 컬럼이름을 key값으로 하고, value에 각각의 값들을 아래와 같이 저장할 수 있습니다.

 

이렇게 생성된 contentValue는 insert()함수를 이용해서 contentResolver를 통해 ContentProvider DB에 저장되어 지는데요.

당연히 이런 value만 저장되는 것이 아니라, 이미지자체도 저장되어야 하므로,

insert함수로부터 return받은 uri를 이용해서, 

openOutputStream()함수로 outputStream을 열구요.

저장할 이미지에서 얻은 bitmap객체에 compress함수를 사용해서 outputstream에 파일을 작성해주면 끝이 나게 됩니다. 

 

val mContentValues = ContentValues().apply {
  put(MediaStore.MediaColumns.DISPLAY_NAME, "test.png")
  put(MediaStore.MediaColumns.MIME_TYPE, "image/png")
  put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
}
val mUri = contentResolver.insert(
  MediaStore.Images.Media.EXTERNAL_CONTENT_URI, 
  mContentValues
)

val outputStream = contentResolver.openOutputStream(mUri)
outputStream.use { 
    mBitmap.compress(Bitmap.CompressFormat.PNG, 85, it)	
}

 

 

openOutpuStream함수는 아래에 설명된 것과 같이 컨텐트의 OutputStream을 오픈해 줍니다.

 

 

참고로 위에서 사용된 Bitmap클래스의 compress함수는 아래와 같은데요.

outputStream에 압축되어진 bitmap을 write 해 줍니다.

 

 

4-2. Query

ContentResolver를 이용해서 대표적인 API인 query()를 구현해 보도록 하겠습니다.

아래의 query API는 cancellation을 위한 옵션을 제공해 주는데요.

그런데, 이것이 API26부터 적용되는 API라서,

Jetpack라이브러리의 ContentResolverCompat을 이용하면 하위호환성을 확보할 수 있습니다. 

 

 

각각의 인자는 다음과 같이 사용해주면 됩니다.

 

인자 의미
resolver Context에서 getContentResolver를 통해서 얻을수 있는 resolver객체
uri 조회할 컨텐츠의 URI로 "content:// scheme" 형식을 가지고 있습니다.

예>
val collection =
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        MediaStore.Video.Media.getContentUri(
            MediaStore.VOLUME_EXTERNAL
        )
    } else MediaStore.Video.Media.EXTERNAL_CONTENT_URI
projection 어떤 컬럼의 데이터를 리턴받을지를 전달.
null을 넣어주면 모든 컬럼이 출력되게 됩니다.
selection 위에서 어느 Column을 조회할지를 정하였다면, 어떤 row를 조회할 지 filter하는 조건을 적어주는 곳입니다.
SQL의 WHERE절과 같이 사용해줄 수 있구요.
null을 주면, 위에서 언급한 uri에 해당하는 모든 row들이 return되어집니다.

ex> val selection = "${MediaStore.Video.Media.DURATION} >= ?"

공식문서에서는 'phone=?' 과 같이 물음표를 사용하는 것을 권장하고 있는데요.
?표는 아래의 selectionArgs를 가르킵니다.
selectionArgs selection에서 사용한 ?는 selectionArgs로 replace될 수 있습니다.
여러개를 사용할 수 있는데요. replace되는 순서는 selection에 나오는 순서입니다.

예> val selectionArgs = arrayOf(TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES).toString())
sortOrder 오름차순으로 할지, 내림차순으로 할지등 정렬하는 순서를 결정할 때 사용합니다.
null을 사용하면, 정렬이 되지 않은 상태로 return되어집니다.

SQL ORDER BY 의 문법을 따라서 사용하면 됩니다. ASC 또는 DESC를 사용해주면 되겠지요.
아래에서는 DISPLAY_NAME을 기준으로 오름차순으로 정렬하였습니다.

예> val sortOrder = "${MediaStore.Video.Media.DISPLAY_NAME} ASC"
cancellationSignal 진행중인 operation을 취소할 신호를 ConcellationSignal객체를 전달해서 주는 것 입니다.
만약 operation이 취소되면, OperationCanceledException이 throw되므로 이를 활용할 수 있습니다.

저도 아직 이 부분은 사용해본적이 없습니다. jetpack라이브러리에 ConcellationSignal이 나와있으므로 참조해볼 수 있습니다.

 

아래 코드를 다 이해할 필요는 없구요.

중간에 ContentResovlerCompat에 들어가는 인자들과 위의 API명세의 인자들과 비교해 보면서 생각해보면 좋을 것 같습니다.

 

val mUri =
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
    } else MediaStore.Video.Media.EXTERNAL_CONTENT_URI

val mProjection = arrayOf(
    MediaStore.Video.Media._ID,
    MediaStore.Video.Media.DISPLAY_NAME,
    MediaStore.Video.Media.DURATION,
    MediaStore.Video.Media.SIZE
)

val mSelection = "${MediaStore.Video.Media.DURATION} >= ?"
val mSelectionArgs = arrayOf(
    TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES).toString()
)

val mSortOrder = "${MediaStore.Video.Media.DISPLAY_NAME} ASC"

val query = ContentResolverCompat.query(
    mUri,
    mProjection,
    mSelection,
    mSelectionArgs,
    mSortOrder, 
    null) //cancellation signal은 null
val videoList = mutableListOf<Video>()
try {
    query?.use { cursor ->
      val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)
      val nameColumn =
            cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME)
      val durationColumn =
            cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DURATION)
      val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE)

      while (cursor.moveToNext()) {
         val id = cursor.getLong(idColumn)
         val name = cursor.getString(nameColumn)
         val duration = cursor.getInt(durationColumn)
         val size = cursor.getInt(sizeColumn)

         val contentUri: Uri = ContentUris.withAppendedId(
             MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
             id)
         videoList += Video(contentUri, name, duration, size)
      }
}
} catch(e: Exception){
    //todo Exception처리
}

 

 4-3. update, delte

update와 delete도 위에서 query와 insert 함수를 사용해 보았다면 그리 어렵지 않습니다.

update에는 인자로 uri, ContentValue 그리고 where 절 스트링과 와일드 카드를 대체할 selectionArgs를 넣어주면 됩니다.

 

 

4-4. Delete

delete에서는 uri와 where절 String, 그리고 와일드카드를 대체할 selectonArgs 들을 인자로 받습니다.

 

 

이상으로 ContentProvider와 ContentResolver 그리고,

ContentResovler의 query, insert, update, delete에 대해서 알아보았습니다.

 

728x90

댓글