[Kotlin] - 버전에 따라 다르게 갤러리에 사진 저장하기

버전에 따라 다른 사진 저장 방식

문제 상황

생성된 조언을 갤러리에 저장하는 과정에서, 안드로이드 버전에 따라 다르게 구현해야 하는 문제에 직면했다.

에뮬레이터는 Q 버전 이 후를 사용했기 때문에 MediaStore를 이용하여 개발을 하였다. 하지만, 테스트용 기기는 Q 버전 이전이라 실제로 저장이 안되는 문제가 발생하였다.

해결 방법

  • *Q버전 이전과 이후를 분리한다.
    • 실행되는 기기의 버전을 확인하는 방법은 Build.VERSION.SDK_INT 로 확인이 가능하다.
    • if 문을 통해 버전을 체크한다.
      • if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
  • 파일 이름
    • 타임 스탬프를 사용하여 중복을 방지한다.
      • 파일 식별자에서는 이름이 곧 고유 식별자이므로, 이름이 동일하게 되면 덮어쓰여지게 된다.
      • ${System.currentTimeMillis()}.jpg
  • Q버전 이전 - File 사용
    • 갤러리 디랙토리 경로 알아내기
      • Environment.getExternalStoragePublicDirectory()
        • 인자로 Environment.DIRECTORY_PICTURES를 사용하여 사진을 저장하는 경로를 알아낸다.
    • 파일 객체 생성
      • File(imagesDir, filename)
        • imagesDirfilenameFile객체를 생성한다.
      • fileOutputStream()
        • 데이터 쓰기
    • 미디어 스캐닝
      • 저장된 이미지를 미디어 라이브러리에서 확인하기 위해 스캔하는 작업을 수행한다.
  • Q버전 이후 - MediaStore 사용
    • contentsValue 생성
      • 이미지 이름
        • put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
      • MIME타입 설정
        • put(MediaStore.MediaColumns.MIME_TYPE, "image/jpg")
      • 경로 설정
        • put(MediaStore.MediaColumns.PATH)
    • 이미지 저장
      • resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
        • 이미지 저장의 결과로 Uri를 반환한다.
      • fileOutputStream()
        • 데이터 쓰기
    • 미디어 스캐닝

전체 코드

class ImageSaveRepository @Inject constructor(private val context : Context ){
    fun saveImageToStorage(bitmap: Bitmap) : Flow<Status<String>> = flow{
        emit(Status.Loading())
        try {
            val filename = "${System.currentTimeMillis()}.jpg"
            var fos: OutputStream? = null
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                context.contentResolver?.also { resolver ->
                    val contentValues = ContentValues().apply {
                        put(MediaStore.MediaColumns.DISPLAY_NAME, filename)
                        put(MediaStore.MediaColumns.MIME_TYPE, "image/jpg")
                        put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
                    }
                    val imageUri: Uri? = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
                    fos = imageUri?.let { resolver.openOutputStream(it) }
                    // 미디어 스캐닝 실행
                    MediaScannerConnection.scanFile(
                        context,
                        arrayOf(imageUri.toString()),
                        null
                    ) { _, _ -> }
                }
            } else {
                val imagesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
                val image = File(imagesDir, filename)
                fos = FileOutputStream(image)

                // 미디어 스캐닝 실행
                MediaScannerConnection.scanFile(
                    context,
                    arrayOf(image.absolutePath),
                    null
                ) { _, _ -> }
            }
            fos?.use {
                bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it)
                emit(Status.Success(null))
            }
            fos?.close()
        }
        catch (e: Exception) {
            emit(Status.Error(message = e.localizedMessage ?: ""))
        }
    }

}