HOME > android > basic

안드로이드 - MediaStore(Media Provider)에 파일 저장하는 방법

By JS | 19 Oct 2019

Android10(Q)에서 Scoped Storage가 적용되면서 미디어 데이터를 저장할 때는 MediaStore를 이용하기를 권장하고 있습니다. Scoped Storage를 통해 미디어 데이터를 저장할 수 있겠지만, MediaStore는 데이터를 저장하는데 별도의 권한을 요구하지 않기 때문입니다.

Android10(Q) 이전에도 MediaStore(Media Provider)를 통해 data를 insert할 수 있었습니다. P와 Q에서 MediaStore에 데이터를 저장하는 방법을 알아보고, 차이점에 이야기 해보겠습니다.

MediaStore에서 데이터를 query하는 방법은 이전 글에서 소개하였습니다.

MediaStore에 Insert하는 방법 (Android 10, Q)

Android 10(Q)에서 MediaStore를 통해 데이터를 읽을 때는 READ_EXTERNAL_STORAGE 권한을 요구하고, 쓸 때는 어떤 권한을 요구하지 않습니다.

다음 코드는 MediaStore에 Image를 저장하는 예제입니다.

val values = ContentValues().apply {
    put(MediaStore.Images.Media.DISPLAY_NAME, "my_image_q.jpg")
    put(MediaStore.Images.Media.MIME_TYPE, "image/jpg")
    put(MediaStore.Images.Media.IS_PENDING, 1)
}

val collection = MediaStore.Images.Media
    .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val item = contentResolver.insert(collection, values)!!

contentResolver.openFileDescriptor(item, "w", null).use {
    // write something to OutputStream
    FileOutputStream(it!!.fileDescriptor).use { outputStream ->
        val imageInputStream = resources.openRawResource(R.raw.my_image)
        while (true) {
            val data = imageInputStream.read()
            if (data == -1) {
                break
            }
            outputStream.write(data)
        }
        imageInputStream.close()
        outputStream.close()
    }
}

values.clear()
values.put(MediaStore.Images.Media.IS_PENDING, 0)
contentResolver.update(item, values, null, null)

코드는 다음 순서로 진행됩니다.

  • ContentResolver에 ContentValues를 insert
  • ContentResolver의 FD에 파일을 write
  • write 완료 후 contentResolver update

위의 코드를 작게 나눠서 설명하겠습니다.

ContentValues

ContentValues 클래스는 ContentResolver에 데이터를 저장하는데 사용합니다.

ContentValues에 파일 정보를 입력하고 insert()의 인자로 저장할 URI와 ContentValues를 전달합니다. 그럼 등록된 파일을 가리키는 Uri 객체가 리턴됩니다.

val values = ContentValues().apply {
    put(MediaStore.Images.Media.DISPLAY_NAME, "my_image_q.jpg")
    put(MediaStore.Images.Media.MIME_TYPE, "image/jpg")
    put(MediaStore.Images.Media.IS_PENDING, 1)
}

val collection = MediaStore.Images.Media
    .getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
val item: Uri = contentResolver.insert(collection, values)!!

리턴 받은 Uri에 데이터 write를 할 수 있습니다.

위의 코드에서 IS_PENDING라는 속성을 1로 set해주는 부분이 있는데요. 이 속성은 아직 내가 파일을 write하지 않았으니, 다른 곳에서 이 데이터를 요구하면 무시하라는 의미입니다. 파일을 모두 write한 뒤에 이 속성을 0으로 update해줘야 합니다.

put(MediaStore.Images.Media.IS_PENDING, 1)

Data 저장

Insert로 얻은 Uri에 파일을 write하면 됩니다. Q에서 권한 없이 MediaStore에 접근하기 때문에 파일의 절대경로를 알 수 없습니다. Uri를 통해 FD를 얻고 그 FD에 내가 등록하려는 파일을 write해줘야 합니다.

Uri로 FD를 얻고 write하는 코드는 다음과 같습니다.

contentResolver.openFileDescriptor(item, "w", null).use {
    FileOutputStream(it!!.fileDescriptor).use { outputStream ->
        val imageInputStream = resources.openRawResource(R.raw.my_image) // getting XML
        while (true) {
            val data = imageInputStream.read()
            if (data == -1) {
                break
            }
            outputStream.write(data)
        }
        imageInputStream.close()
        outputStream.close()
    }
}

Uri에서 FD를 얻을 때 ContentResolver.openFileDescriptor()를 사용하면 됩니다. 저는 앱의 /res/raw/my_image.jpg 경로에 이미지 파일을 넣어두었고, 이것을 FD에 write해주었습니다.

IS_PENDING = 0으로 update

파일을 모두 write하였다면, IS_PENDING 속성을 0으로 변경해줘야 합니다. 그럼 다른 앱에서 이 파일을 사용할 수 있습니다.

values.put(MediaStore.Images.Media.IS_PENDING, 0)
contentResolver.update(item, values, null, null)

파일이 저장되는 위치

위의 코드에서 Insert()의 인자로 MediaStore.VOLUME_EXTERNAL_PRIMARY 를 전달하였습니다.

저장된 파일의 실제 경로를 shell로 확인해보면 다음과 같습니다.

generic_x86:/sdcard/Pictures $ ls
my_image_q.jpg

만약 경로를 변경해주고 싶다면 ContentValues 객체에 RELATIVE_PATH 속성으로 경로를 설정해주어야 합니다.

val values = ContentValues().apply {
    put(MediaStore.Audio.Media.RELATIVE_PATH, "DCIM/My Images")
    put(MediaStore.Images.Media.DISPLAY_NAME, "my_image_q2.jpg")
    put(MediaStore.Images.Media.MIME_TYPE, "image/jpg")
    put(MediaStore.Images.Media.IS_PENDING, 1)
}

위처럼 저장하면 RELATIVE_PATH로 설정된 "/DCIM/My Images/" 아래에 파일이 저장됩니다.

generic_x86:/sdcard/DCIM/My Images $ ls
my_image_q2.jpg

Images의 경우 아래 두개의 경로에만 데이터를 저장할 수 있습니다.

  • /DCIM
  • /Pictures

다른 경로를 설정하면 다음과 같이 에러가 발생하며 저장이 되지 않습니다.

java.lang.IllegalArgumentException: Primary directory DCIM1 not allowed for content://media/external_primary/images/media; allowed directories are [DCIM, Pictures]
    at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:170)

MediaStore에 Insert하는 방법 (Android P 이하)

P 이하에서는 Q와 조금 다릅니다. 우선 MediaStore에 write하려면 WRITE_EXTERNAL_STORAGE 권한이 필요합니다.

다음은 P에서 이미지 파일을 MediaStore에 등록하는 예제입니다.

// copy /res/raw/my_image.jpg to /data/data/[app package]/files/my_image.jpg
val inputStream = resources.openRawResource(R.raw.my_image)
val filePath = "$filesDir/my_image.jpg"
val outputStream = FileOutputStream(filePath)
while (true) {
    val data = inputStream.read()
    if (data == -1) {
        break
    }
    outputStream.write(data)
}
inputStream.close()
outputStream.close()

// Insert my file to MediaStore
val values= ContentValues().apply {
    put(MediaStore.Images.Media.DISPLAY_NAME, "my_image6.jpg")
    put(MediaStore.Images.Media.MIME_TYPE, "image/jpg")
    put(MediaStore.Images.Media.DATA, filePath)
}
val item = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)!!

이 코드는 다음과 같은 작업을 수행합니다.

  1. /res/raw/에 저장된 파일을 앱의 파일 폴더에 복사합니다.
  2. 복사된 파일을 MediaStore에 저장합니다.

다음 코드가 MediaStore에 이미지 파일을 저장하는 코드인데, 직접 write하지 않고 DATA 속성에 절대 경로를 설정하면 알아서 저장이 됩니다. (이쪽에 대해서 관심이 없어서 다른 방식으로도 파일을 Insert할 수 있는진 모르겠네요)

val values= ContentValues().apply {
    put(MediaStore.Images.Media.DISPLAY_NAME, "my_image6.jpg")
    put(MediaStore.Images.Media.MIME_TYPE, "image/jpg")
    put(MediaStore.Images.Media.DATA, filePath)
}
val item = contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)!!

파일의 절대경로가 필요하기 때문에, 다음과 같이 raw에 저장된 이미지 파일을 앱의 파일 폴더에 저장할 수 밖에 없었습니다.

val inputStream = resources.openRawResource(R.raw.my_image)
val filePath = "$filesDir/my_image.jpg"
val outputStream = FileOutputStream(filePath)
while (true) {
    val data = inputStream.read()
    if (data == -1) {
        break
    }
    outputStream.write(data)
}
inputStream.close()
outputStream.close()

Video, Audio 파일을 MediaStore에 Insert

ContentValues에 설정하는 속성이 Images가 아니라 Video, Audio 라는 것만 다르고 다른 코드들은 동일합니다.

정리

Q에서 Scoped Storage가 도입되면서 READ/WRITE EXTERNAL STORAGE 권한이 무의미해졌습니다. MediaStore에서 파일을 읽을 때는 READ 권한을 요구하지만, 쓸 때는 WRITE 권한을 요구하지 않습니다. 다만 정해진 폴더에만 쓸 수 있습니다.

구글의 의도대로, Scoped Storage를 지원하는 디바이스에서 미디어 파일들은 MediaStore를 이용하여 접근하는 것이 좋을 것 같습니다. 하지만, 아직 Q 디바이스가 많이 나오지 않았고, 어떤 불편함(또는 버그)이 있을지 모르기 때문에 주의를 기울이면서 개발하시면 좋을 것 같습니다.

참고