Android에서 MediaStore는 디바이스에 저장된 사진/동영상/오디오 등의 미디어 파일을 관리합니다. MediaStore는 Query API를 제공하며, 이 API를 사용하여 파일들을 탐색할 수 있습니다.

예제를 통해 미디어 파일 정보는 가져오는 방법에 대해서 알아보겠습니다.

1. 미디어 파일 접근 권한

미디어 파일은 OS 버전 별로 요구하는 권한이 조금씩 다릅니다. Android 13에서 READ_EXTERNAL_STORAGE는 더 이상 사용되지 않고, READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, READ_MEDIA_AUDIO 3개의 권한으로 분리되었습니다. 기존에는 READ_EXTERNAL_STORAGE 1개의 권한으로 모든 종류의 미디어 파일을 읽을 수 있었지만, Android 13부터 미디어 파일 종류에 따라서 READ 권한이 3개로 분리되었습니다.

  • Android 12L 이하 버전 : READ_EXTERNAL_STORAGE
  • Android 13 이상 버전 : READ_MEDIA_IMAGES, READ_MEDIA_VIDEO, READ_MEDIA_AUDIO

앱의 AndroidManifest에는 아래와 같이 OS 별로 권한을 추가할 수 있습니다.

<!-- Devices running Android 12L (API level 32) or lower  -->
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" android:maxSdkVersion="32" />

<!-- Devices running Android 13 (API level 33) or higher -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
<uses-permission android:name="android.permission.READ_MEDIA_VIDEO" />
<uses-permission android:name="android.permission.READ_MEDIA_AUDIO" />

2. 모든 이미지 파일 가져오기

MediaStore를 통해 이미지 파일 정보를 가져오려면 READ_MEDIA_IMAGES 권한을 갖고 있어야 합니다. Runtime 권한이기 때문에, 권한 팝업을 통해 사용자에게 받아야 합니다.

Android 14에서 사용자는 앱에 모든 Read 권한을 부여하지 않고 파일 단위로 접근 권한을 부여할 수 있게 변경되었습니다. 자세한 내용은 Android 14 미디어 파일 일부 접근 권한을 참고해주세요.

권한을 갖고 있다면 아래와 같이 MediaStore의 Query API를 사용하여 이미지 파일을 가져올 수 있습니다.

  • contentResolver.query() : 인자로 전달된 projection에 해당하는 이미지 파일 정보를 cursor로 리턴
  • cursor.getColumnIndexOrThrow() : cursor에서 특정 항목에 대한 데이터를 리턴
  • URI를 통해 파일에 접근할 수 있음
data class Media(
    val uri: Uri,
    val name: String,
    val size: Long,
    val mimeType: String,
)

private fun getImages(contentResolver: ContentResolver): List<Media> {
    val projection = arrayOf(
        MediaStore.Images.Media._ID,
        MediaStore.Images.Media.DISPLAY_NAME,
        MediaStore.Images.Media.SIZE,
        MediaStore.Images.Media.MIME_TYPE,
    )

    val collectionUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        // Query all the device storage volumes instead of the primary only
        MediaStore.Images.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
    } else {
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI
    }

    val images = mutableListOf<Media>()

    contentResolver.query(
        collectionUri,
        projection,
        null,
        null,
        "${MediaStore.Images.Media.DATE_ADDED} DESC"
    )?.use { cursor ->
        val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media._ID)
        val displayNameColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DISPLAY_NAME)
        val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.SIZE)
        val mimeTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.MIME_TYPE)

        while (cursor.moveToNext()) {
            val uri = ContentUris.withAppendedId(collectionUri, cursor.getLong(idColumn))
            val name = cursor.getString(displayNameColumn)
            val size = cursor.getLong(sizeColumn)
            val mimeType = cursor.getString(mimeTypeColumn)

            val image = Media(uri, name, size, mimeType)
            images.add(image)
        }
    }

    return images
}

위 함수를 아래와 같이 호출하여 파일 리스트를 출력할 수 있습니다.

val images = getImages(contentResolver)
for (img : Media in images) {
    Log.d(TAG, "Image name: ${img.name}, uri: ${img.uri},"
            + " size: ${img.size}, type ${img.mimeType}")
}

Output:

10-14 12:39:13.365  7906  7906 D MainActivity: Image name: IMG_20231014_122514.jpg, uri: content://media/external/images/media/1000000037, size: 31298, type image/jpeg
10-14 12:39:13.365  7906  7906 D MainActivity: Image name: IMG_20231014_122513.jpg, uri: content://media/external/images/media/1000000036, size: 30806, type image/jpeg
10-14 12:39:13.366  7906  7906 D MainActivity: Image name: IMG_20231014_122512.jpg, uri: content://media/external/images/media/1000000034, size: 30530, type image/jpeg
10-14 12:39:13.366  7906  7906 D MainActivity: Image name: IMG_20231014_122512_1.jpg, uri: content://media/external/images/media/1000000035, size: 31368, type image/jpeg
10-14 12:39:13.366  7906  7906 D MainActivity: Image name: IMG_20231014_122511.jpg, uri: content://media/external/images/media/1000000033, size: 30515, type image/jpeg

3. 모든 비디오 파일 가져오기

MediaStore를 통해 비디오 파일 정보를 가져오려면 READ_MEDIA_VIDEO 권한을 갖고 있어야 합니다.

권한을 갖고 있다면 아래와 같이 MediaStore의 Query API를 사용하여 비디오 파일을 가져올 수 있습니다.

  • contentResolver.query() : 인자로 전달된 projection에 해당하는 이미지 파일 정보를 cursor로 리턴
  • cursor.getColumnIndexOrThrow() : cursor에서 특정 항목에 대한 데이터를 리턴
  • URI를 통해 파일에 접근할 수 있음
data class Media(
    val uri: Uri,
    val name: String,
    val size: Long,
    val mimeType: String,
)

위 함수를 아래와 같이 호출하여 파일 리스트를 출력할 수 있습니다.

val videos = getVideos(contentResolver)
for (video : Media in videos) {
    Log.d(TAG, "Video name: ${video.name}, uri: ${video.uri},"
            + " size: ${video.size}, type ${video.mimeType}")
}

private fun getVideos(contentResolver: ContentResolver): List<Media> {
    val projection = arrayOf(
        MediaStore.Video.Media._ID,
        MediaStore.Video.Media.DISPLAY_NAME,
        MediaStore.Video.Media.SIZE,
        MediaStore.Video.Media.MIME_TYPE,
    )

    val collectionUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        // Query all the device storage volumes instead of the primary only
        MediaStore.Video.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
    } else {
        MediaStore.Video.Media.EXTERNAL_CONTENT_URI
    }

    val videos = mutableListOf<Media>()

    contentResolver.query(
        collectionUri,
        projection,
        null,
        null,
        "${MediaStore.Video.Media.DATE_ADDED} DESC"
    )?.use { cursor ->
        val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media._ID)
        val displayNameColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.DISPLAY_NAME)
        val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.SIZE)
        val mimeTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.Video.Media.MIME_TYPE)

        while (cursor.moveToNext()) {
            val uri = ContentUris.withAppendedId(collectionUri, cursor.getLong(idColumn))
            val name = cursor.getString(displayNameColumn)
            val size = cursor.getLong(sizeColumn)
            val mimeType = cursor.getString(mimeTypeColumn)

            val video = Media(uri, name, size, mimeType)
            videos.add(video)
        }
    }

    return videos
}

Output:

10-14 12:45:33.935  7906  7906 D MainActivity: Video name: VID_20231014_123042.mp4, uri: content://media/external/video/media/1000000040, size: 511422, type video/mp4
10-14 12:45:33.935  7906  7906 D MainActivity: Video name: VID_20231014_123037.mp4, uri: content://media/external/video/media/1000000039, size: 641758, type video/mp4
10-14 12:45:33.935  7906  7906 D MainActivity: Video name: VID_20231014_123034.mp4, uri: content://media/external/video/media/1000000038, size: 445506, type video/mp4

4. 모든 오디오 파일 가져오기

MediaStore를 통해 오디오 파일 정보를 가져오려면 READ_MEDIA_AUDIO 권한을 갖고 있어야 합니다.

권한을 갖고 있다면 아래와 같이 MediaStore의 Query API를 사용하여 오디오 파일을 가져올 수 있습니다.

  • contentResolver.query() : 인자로 전달된 projection에 해당하는 이미지 파일 정보를 cursor로 리턴
  • cursor.getColumnIndexOrThrow() : cursor에서 특정 항목에 대한 데이터를 리턴
  • URI를 통해 파일에 접근할 수 있음
data class Media(
    val uri: Uri,
    val name: String,
    val size: Long,
    val mimeType: String,
)

private fun getAudios(contentResolver: ContentResolver): List<Media> {
    val projection = arrayOf(
        MediaStore.Audio.Media._ID,
        MediaStore.Audio.Media.DISPLAY_NAME,
        MediaStore.Audio.Media.SIZE,
        MediaStore.Audio.Media.MIME_TYPE,
    )

    val collectionUri = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
        // Query all the device storage volumes instead of the primary only
        MediaStore.Audio.Media.getContentUri(MediaStore.VOLUME_EXTERNAL)
    } else {
        MediaStore.Audio.Media.EXTERNAL_CONTENT_URI
    }

    val audios = mutableListOf<Media>()

    contentResolver.query(
        collectionUri,
        projection,
        null,
        null,
        "${MediaStore.Audio.Media.DATE_ADDED} DESC"
    )?.use { cursor ->
        val idColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media._ID)
        val displayNameColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.DISPLAY_NAME)
        val sizeColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.SIZE)
        val mimeTypeColumn = cursor.getColumnIndexOrThrow(MediaStore.Audio.Media.MIME_TYPE)

        while (cursor.moveToNext()) {
            val uri = ContentUris.withAppendedId(collectionUri, cursor.getLong(idColumn))
            val name = cursor.getString(displayNameColumn)
            val size = cursor.getLong(sizeColumn)
            val mimeType = cursor.getString(mimeTypeColumn)

            val audio = Media(uri, name, size, mimeType)
            audios.add(audio)
        }
    }

    return audios
}

위 함수를 아래와 같이 호출하여 파일 리스트를 출력할 수 있습니다.

val audios = getAudios(contentResolver)
for (audio : Media in audios) {
    Log.d(TAG, "Audio name: ${audio.name}, uri: ${audio.uri},"
            + " size: ${audio.size}, type ${audio.mimeType}")
}