본문 바로가기

Android + Kotlin

[Android+Kotlin] 외부저장소에 파일 저장하기(Android Q 이상)

반응형

얼마전에 앱 내부에 저장된 녹음 파일을 외부저장소(내장 메모리 > Music)로 내보내기(export) 해달라는 요구사항이 들어왔습니다.

Android 10(Q) 이상부터 Scoped Storage가 적용되어 MediaStore를 이용하여 파일을 옮기는 방법으로 개발을 해보겠습니다.

 

간략한 파일을 옮기는 방법은

 

1. MediaStore 에 저장 파일 할 파일의 정보(ContentValues)를 생성

2. ContentValues를 MediaStore에 insert

3. insert하고 얻은 uri에 파일 저장

4. MediaStore에 IS_PENDING 값 update

 

위의 순서로 개발을 시작하겠습니다.


 

1. MediaStore 에 저장 파일 할 파일의 정보(ContentValues)를 생성

val values = ContentValues()
values.put(MediaStore.Audio.Media.DISPLAY_NAME, {파일이름})
values.put(MediaStore.Audio.Media.MIME_TYPE, "audio/mp4")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    values.put(MediaStore.Audio.Media.RELATIVE_PATH, "Music")
    values.put(MediaStore.Audio.Media.IS_PENDING, 1);
}

 

저장할 파일이 오디오 파일이므로 MediaStore.Audio.Media... 를 사용합니다.

 

원본 파일의 확장자가 .mp4인데, audio만 사용하기 때문에 mime type을 "audio/mp4"로 입력해줍니다.

그러면, 자동으로 확장자가 .m4a 로 저장이 됩니다. 그래서, 파일이름에 확장자를 제거하고 넣어주는 센스!! 가 필요하더군요.ㅎㅎ

 

외부저장소 위치를 "내장메모리 > Music"로 요구하였기에, RELATIVE_PATH에 "Music"를 추가하여 Music 폴더에 저장되도록 해줍니다.

이미지라면 "DCIM", 동영상이면 "DCIM", "Movies" 등에 저장할 수 있습니다.

그런데, mime type이 맞지 않는 디렉토리에는 저장할 수 없습니다.

예를 들어, 오디오 파일을 "DCIM" 에 저장하려면 에러가 발생합니다.

(저도 원본이 .mp4라서 mime type에 "video/mp4" 입력하였는데, 에러가 발생하여 "audio/mp4"로 수정하였습니다.)

 

마지막으로 IS_PENDING을 1로 저장합니다.

이유인즉슨, 아직 파일 저장을 안했으니 목록요청에 대해 이 항목은 무시하라는 의미입니다.

뒤에 파일을 write한 후에 이 값을 0으로 업데이트 해야 합니다.


 

2. ContentValues를 MediaStore에 insert

val contentResolver = context.contentResolver
val collection = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
val insertUri = contentResolver.insert(collection, values)

위에서 생성한 ContentValues를 MediaStore에 insert하고 uri를 받습니다.


 

3. insert하고 얻은 uri에 파일 저장

val fileDescriptor = context.contentResolver.openFileDescriptor(insertUri!!, "w")
val outputStream = FileOutputStream(fileDescriptor?.fileDescriptor)
val inputStream = context.contentResolver.openInputStream(fileUri)
val bytes = ByteArray(8192)
while (true) {
    val read = inputStream?.read(bytes)
    if (read == -1) {
        break
    }
    outputStream.write(bytes, 0, read!!)
}
outputStream.close()
inputStream?.close()
fileDescriptor?.close()

이제 insert하고 받은 uri로 FileDescripter를 open합니다.

그리고, input/output stream에 write하면 됩니다.

전송하는 파일이 음원파일이고 사이즈가 클수도 있기때문에 buffer를 이용하도록 합니다.


 

4. MediaStore에 IS_PENDING 값 update

values.clear()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    values.put(MediaStore.Audio.Media.IS_PENDING, 0)
    values.put(MediaStore.Audio.Media.IS_PENDING, 0)
}
context.contentResolver.update(insertUri, values, null, null)

IS_PENDING 값을 0으로 설정하여 업데이트 합니다.

그런데, 똑같은 코드로 values에 IS_PENDING를 2번 해주었죠.

원래는 1번하면 되는 것인데, 뭐가 문제인지 업데이트가 안되더라구요.

구글링을 해보니, 위와같이 2번 해주면 된다라고해서.. 해보니 되더라구요.

이게 무슨 경우인지.......

뭐.. 일단 그래서 저도 2번 넣었습니다.

(1번만 해서 업데이트가 되시는 분은, 저처럼 2번 쓰지 마세요. 안되는 분만 해보세요.ㅋ)


 

아래는 전체 코드입니다.

필요하신 분은 참고하세요.

 

그러면 오늘도 존버!!!!

 

fun moveTemporaryFileToMediaStore(context: Context, file: File) : Boolean {
    val fileUri = file.toUri()
    val fileName = fileUri.lastPathSegment!!.replace(".mp4", "")

    val values = ContentValues()
    values.put(MediaStore.Audio.Media.DISPLAY_NAME, fileName)
    values.put(MediaStore.Audio.Media.MIME_TYPE, "audio/mp4")
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        values.put(MediaStore.Audio.Media.RELATIVE_PATH, "Music")
        values.put(MediaStore.Audio.Media.IS_PENDING, 1);
    }

    val contentResolver = context.contentResolver
    val collection = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
    val insertUri = contentResolver.insert(collection, values)

    try {
        val fileDescriptor = context.contentResolver.openFileDescriptor(insertUri!!, "w")
        val outputStream = FileOutputStream(fileDescriptor?.fileDescriptor)
        val inputStream = context.contentResolver.openInputStream(fileUri)
        val bytes = ByteArray(8192)
        while (true) {
            val read = inputStream?.read(bytes)
            if (read == -1) {
                break
            }
            outputStream.write(bytes, 0, read!!)
        }
        outputStream.close()
        inputStream?.close()
        fileDescriptor?.close()
        values.clear()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            values.put(MediaStore.Audio.Media.IS_PENDING, 0)
            values.put(MediaStore.Audio.Media.IS_PENDING, 0)
        }

        context.contentResolver.update(insertUri, values, null, null)
        file.delete()   // 원본 삭제함.
        return true
    } catch (e: Exception) {
        e.printStackTrace()
        return false
    }
}
반응형