본문 바로가기
앱 제작/SSAFY 서명 앱

#9 파일은 저장 되는데 갤러리에 사진이 왜 안보이는거야!

by Kim Juhwan 2022. 1. 4.

1. 사건 배경
   1-1. 상황
   1-2. 새로운 정보

2. 시도해본 방법
   2-1. 파일 삭제 후 저장
   2-2. 이름 중복 피하기

3. Bitmap 이미지를 저장하는 새로운 방법

 

 

 


 

 

2022/01/02 개발 내용

1. 사건 배경

1-1. 상황

저번에 내가 겪은 문제는 이미지 파일이 저장되지 않는 문제였다. 굉장히 어이없는 실수로 생겨난 문제였지...

그 뒤로 잘 작동해서 음 문제가 없겠거니 했는데 출시하기 직전에 다른 폰으로 테스트해보는데 아뿔싸 문제가 있었다.

파일 탐색기로 들어가보면 사진이 분명히 있는데 갤러리에 들어가면 사진이 없다. 대체 이게 무슨 상황???

내 머리로는 이해가 가지 않았다.

 

지정은 지정한 이름으로만 저장하는 것 (중복), 랜덤은 현재 시간으로 저장하는 것 (중복 없음)

 

더더욱 이해가 안가는 것은 내가 어떤 기기를 쓰느냐, 어떤 경로를 택하냐, 어떤 이름 규칙을 사용하느냐에 따라

갤러리에 보이는 유무가 갈린다는 것이었다.

기기에 따라 달라지는 건 백번 양보해서 그렇다 쳐 대체 경로랑 이름은 무슨 상관인 건데!?

정말 머리가 터져버릴 것 같았다. 검색해도 아무런 정보도 안 나오고... 😭

 

1-2. 새로운 정보

다운로드한 파일이 보이지 않자 고객센터에게 누군가 문의를 했고, 그에 대한 답변

 

그러다가 이런 정보를 듣게 되었다.

안드로이드 12부터 정책이 바뀌었다고라고라고라고라? 😱

마침 내가 가지고 있는 샤오미폰이 안드로이드 12였고 갤럭시가 11이었다.

둘 다 테스트용이라 아이폰을 메인으로 쓰기 때문에 난 전혀 이 사실을 모르고 있었다. 대체 언제부터 이렇게 바뀐 거람?

도대체 정책은 왜 이리 매번 바뀌는 건지 🤦🏻‍♂️

앞으로 새로운 버전이 나오면 공식문서를 정독해야겠다는 다짐을 하게 됐다.

 

 

동작 변경사항: 모든 앱  |  Android 12  |  Android Developers

Android 12를 타겟팅하는 앱에 영향을 주는 Android 12의 변경사항을 알아봅니다.

developer.android.com

그리하여 공식문서를 쭉 읽어보는데...

음? 뭐지? 왜 관련 내용이 적혀있지 않는 거지?

삼성 고객센터에서 사실 확인도 안 하고 답변했을 리는 없고

날짜를 보아하니 최근에 업데이트된 내역이라 그런지 관련 내용을 구글에서도 찾을 수 없었다.

 

어쨌든 나는 이 삽질을 통해 기기나 파일명 때문이 아닌 버전 때문에 문제가 발생했을 수도 있겠구나라는 힌트를 얻었고

문제를 해결할 수 있었다.

[목차 2에서 계속...]

 

2. 시도해본 방법

2-1. 파일 삭제 후 저장

삽질했던 내용이 아까워서 쓰는 목차이다.

해결 방법은 [목차 3]으로 가면 된다.

 

유일하게 갤러리에 사진이 뜨는 경우가

샤오미(12 버전) + Download 경로 + 랜덤한 이름명 사용했을 때였다.

근데 내가 만들고자 한 앱은 파일명이 이름_지역_반.png로 고정되어있어야 했다.

이미 존재하는 파일명으로 저장하면 알아서 저장되지 않을까? 하는 내 물음에

 

FileNotFoundException

 

안드로이드 스튜디오는 exception으로 대답했다. 망할놈

FileNotFoundException: open failed: EEXITST (File existes)가 어떤 에러인지 검색해도 딱히 검색 결과가 나오는 게 없었다.

이런 멍청한 짓을 하는 건 나뿐인 걸까?

아무튼 대충 해석해보면 이미 파일이 존재해서 안된다는 말 같아서

만약 이미 파일이 존재하면 -> 파일을 삭제하고 -> 파일을 새로 저장하는 코드를 짜 보기로 했다.

 

    // 특정 레이아웃 캡쳐해서 저장하기
    fun Request_Capture(view: View?, title: String) {
        if (view == null) { // Null Point Exception ERROR 방지
            println("::::ERROR:::: view == NULL")
            return
        }

        /* 캡쳐 파일 저장 */
        view.buildDrawingCache() //캐시 비트 맵 만들기
        val bitmap = view.drawingCache

        val path = "/Download/"
        //val path = "/DCIM/SSAIGN/"

        /* 저장할 폴더 Setting */
        val uploadFolder = Environment.getExternalStoragePublicDirectory(path) //저장 경로 (File Type형 변수)
        if (!uploadFolder.exists()) { //만약 경로에 폴더가 없다면
            uploadFolder.mkdirs() //폴더 생성
        }

        /* 파일 경로 */
        val Str_Path = Environment.getExternalStorageDirectory().absolutePath + path //저장 경로 (String Type 변수)

        /* 기존 파일이 있다면 삭제 */
        var file = File("$Str_Path$title.jpg")
        try{
            if(file.exists()) {
                file.delete()
            }
        } catch (e: Exception) {
            e.printStackTrace()
        }

        /* 파일 저장 */
        try {
            val fos = FileOutputStream("$Str_Path$title.png") // 경로 + 제목 + .jpg로 FileOutputStream Setting
            bitmap.compress(Bitmap.CompressFormat.PNG, 100, fos) // 파일 압축 (압축률 설정 1~100)
            showToastMessage("사진이 저장되었습니다.")
        } catch (e: Exception) {
            e.printStackTrace()
            showToastMessage("사진 저장을 실패했습니다.")
        }
    }

라는 내용의 코드를 짜 봤는데 계속 똑같은 에러가 떴다.

한참을 시도하다가 다른 방법을 찾아보기로 함

 

2-2. 이름 중복 피하기

    // 특정 레이아웃 캡쳐해서 저장하기
    fun Request_Capture(view: View?, title: String) {
        if (view == null) { // Null Point Exception ERROR 방지
            println("::::ERROR:::: view == NULL")
            return
        }

        /* 캡쳐 파일 저장 */
        view.buildDrawingCache() //캐시 비트 맵 만들기
        val bitmap = view.drawToBitmap()

        /* 저장할 폴더 Setting */
        val uploadFolder = Environment.getExternalStoragePublicDirectory("/SSAIGN/") //저장 경로 (File Type형 변수)
        if (!uploadFolder.exists()) { //만약 경로에 폴더가 없다면
            uploadFolder.mkdirs() //폴더 생성
        }


        /* 파일 저장 */
        val Str_Path = Environment.getExternalStorageDirectory().absolutePath + "/SSAIGN/" //저장 경로 (String Type 변수)
        try {
            val fos = FileOutputStream("$Str_Path$title.jpg") // 경로 + 제목 + .jpg로 FileOutputStream Setting
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos) // 파일 압축 (압축률 설정 1~100)
            showToastMessage("사진이 저장되었습니다.")
        } catch (e: FileNotFoundException) {
            //e.printStackTrace()
            renameFile(bitmap, Str_Path, title)
        } catch (e: Exception) {
            e.printStackTrace()
            showToastMessage("사진 저장을 실패했습니다.")
        }
    }

    fun renameFile(bitmap: Bitmap, Str_Path: String, title: String) {
        for(cnt in 2..10) {
            try {
                val fos = FileOutputStream("$Str_Path$title ($cnt).jpg") // 경로 + 제목 + .jpg로 FileOutputStream Setting
                bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos) // 파일 압축 (압축률 설정 1~100)
                showToastMessage("사진이 저장되었습니다.")
                return
            } catch (e: Exception) {

            }
        }

        showToastMessage("테스트")
    }

중복되는 파일명이 있으면 이름 뒤에 숫자를 카운트하도록 코드를 짜 봤다.

지역_반_이름 (2).png 이렇게 말이다.

성공은 했으나 여전히 특정 버전에서 갤러리에 사진이 보이지 않는 현상은 여전했다.

역시 파일명 때문이 아니라 버전 때문이 맞구나라는 걸 깨닫고 다른 방법을 찾아 나섰다.

 

3. Bitmap 이미지를 저장하는 새로운 방법

구글에 검색하면 대부분 내가 위에서 사용한 방법(= 구식 방법)으로 이미지 파일을 저장하곤 하는데

운 좋게도 Android Q(= Android 10)를 기준으로 이미지를 저장하는 새로운 방법에 대해 자세히 다룬 외국 블로그를 알게 되었다. 부족한 영어 실력이지만 간추려서 해석해보자면 다음과 같다.

 

Android 10 버전 이상의 기기의 경우 내장 스토리지에 파일을 저장하는 방식이 완전히 변경되었습니다.

구식 방법을 여전히 사용할 수 있지만 임시적인 방안일 뿐입니다.

기술은 빠르게 변화하고 있기에 당신이 안드로이드 개발자가 되고 싶다면 꾸준히 스스로를 발전시켜나가야 합니다.

 

안드로이드의 저장 공간에는 2가지 종류가 있습니다.

  • App-specific storage: 당신의 애플리케이션 파일만을 위한 저장공간입니다. 다른 외부 앱은 이 공간에 접근할 수 없습니다. 그리고 당신의 앱은 따로 권한이 없어도 저장공간에 접근할 수 있습니다.
    (이걸 읽으면서 깨달았다. 나는 사진을 내가 만든 앱 저장공간에 저장하고 있었고 갤러리는 외부 앱이라 접근하지 못하고 있는 거구나..!)
  • Shared Storage: 모든 앱이 공유하는 저장공간입니다.

저장 공간에 대한 더 많은 정보는 공식 문서를 확인하세요.

이번 게시물에서는 Shared Storage에 이미지 파일을 저장하는 방법에 대해 다루려고 합니다.

 

    // 특정 레이아웃 캡쳐해서 저장하기
    fun Request_Capture(view: View?, title: String) {
        if (view == null) { // Null Point Exception ERROR 방지
            println("::::ERROR:::: view == NULL")
            return
        }

        view.buildDrawingCache() //캐시 비트 맵 만들기
        
        // 1
        val bitmap = view.drawingCache

	// 2
        var fos: OutputStream? = null

	// 3
        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            // 4
            context?.contentResolver?.also { resolver ->

		// 5
                val contentValues = ContentValues().apply {
                    put(MediaStore.MediaColumns.DISPLAY_NAME, "$title.png")
                    put(MediaStore.MediaColumns.MIME_TYPE, "image/jpg")
                    put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
                }

		// 6
                val imageUri: Uri? = resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)
                
                // 7
                fos = imageUri?.let { resolver.openOutputStream(it) }
            }
        } else {
            val imagesDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
            val image = File(imagesDir, "$title.png")
            fos = FileOutputStream(image)
        }

        fos?.use {
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, it)
            showToastMessage("사진이 저장되었습니다.")
        }
    }

블로그를 참고해서 작성한 코드이다. 정말 잘 작동했음 ㅠㅠ 눈물 광광

번호 달린 주석에 대한 설명을 달자면

 

  1. 갤러리에 저장할 비트맵 이미지를 준비한다. (위 코드에서는 내가 지정한 view를 비트맵 이미지로 저장한다. 각자 원하는 비트맵 이미지를 준비하면 됨)
  2. 버전에 따라 분기 처리를 해야 하므로 바깥쪽에 변수를 선언해준다.
  3. 버전이 Q(= 10) 이상일 때 첫 번째 if문으로 들어간다.
  4. Context로부터 ContentResolver를 가져온다.
  5. ContentValues에 이미지 파일 정보를 담는다. 파일명, 파일 타입, 파일 경로...
  6. resolver에 insert()를 사용함으로써 Uri를 리턴 받는다.
  7. Uri를 얻은 뒤 openOutputStream()으로 output stream을 연다.

 

이렇게 나는 갤러리에 사진이 보이지 않는 문제를 해결할 수 있었다.

힘들었다 😵

 


💡 느낀 점

  • 코드를 하나씩 분석하면서 느낀 건데, 나는 ContentProvider에 대한 개념이 부족했던 것 같다. 이게 앱 데이터 영역과 공유 데이터 영역과도 연관성이 있는데 ContentProvider에 대한 경험이 부족하다 보니 전혀 캐치하지 못했다. 자주 쓰이는 놈이 아니라고 너무 관심이 없었던 건 아닐까
  • 이전에 배웠던 내용을 천천히 찾아보니 ContentProvider에 대해 수업을 들었을 때 안드로이드 Q 버전에 대한 이야기를 잠깐 들었던 기억이 난다. 그땐 와닿지 않았는데 이렇게 한껏 삽질을 하고 나니 절대 잊어먹지 않을 것 같다.
  • 앞으로 버전 업데이트 올라올 때마다 열심히 읽어봐야지.

📘 참고한 자료


 

 

반응형

댓글