티스토리 뷰

android

Android 저장소 시스템

길 킴 2020. 3. 5. 11:52
728x90

<내부저장소>

다른 앱에서 접근 불가능한 고유 앱 영역. (다른앱과 공유 안된다.) 

* 사용 함수

  1. getFilesDir() : 앱과 고유하게 연결된 파일 시스템의 디렉터리를 표시하는 File 개체를 반환
  2. getDir(name, mode) : 앱의 고유한 파일 시스템 디렉터리 내에 새 디렉터리를 생성하거나 기존 디렉터리를 엽니다. 
  3. getCacheDir() : 앱과 고유하게 연결된 파일 시스템의 캐시 디렉터리를 표시하는 File 개체를 반환
                             이 디렉터리를 임시파일용으로 정기적으로 지워야하고, 디스크 공간이 부족해지면 시스템에서 알아서 삭제한다. 

* 사용자에게 받아야 할 권한 종류: 없음

* 예시 코드

fun saveInternalStorage(context: Context, fileName: String, bitmap: Bitmap) {
    val INTERNAL_STORAGE_IMAGE_FOLDER = "imgDIr"
    
    val storageDir = context.getDir(INTERNAL_STORAGE_IMAGE_FOLDER, Context.MODE_PRIVATE)
    val imgFile = File(storageDir, fileName + ".jpg")

    val fileOutputStream = FileOutputStream(imgFile.absoluteFile)
    bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fileOutputStream)
    fileOutputStream.close()
}	

 

<외부저장소>

다른 앱에서 접근 가능한 영역. Public, Private 영역으로 나눠져 있다. 

정확히 말하면, Public역역과 Private을 구분하는것은 DB의 정보이다.
특정 앱 패키지에 저장을 해놓기만 하면 Private 영역이지만, Public 영역으로 보이고 싶다면 MediaStore 함수로 Android 시스템 DB에 Public 으로 보여지도록 요청만 하면 된다. 

* 사용 함수

1. 공개 디렉터리에 저장
호출 함수 : Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM)

공개 디렉토리 위치: sdcard > DCIM

 

2. 비공개 디렉터리에 저장
호출 함수: context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS + File.separator + "my_folder_name")

비공개 디렉터리 위치: sdcard > Android> data> 패키지명

But! 비공개 디렉터리를 MediaStore 함수를 이용해서 Public 으로 전환할 수 있다. 
(예를 들어 비공개 디렉터리에 이미지 파일을 저장하고, 나중에 Public으로 전환해서 앨범 목록에서 보이게 할 수 있다.)

사용자에게 받아야 할 권한 종류 : WRITE_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE

  • 권한이 필요 없는 경우
    • 자신의 앱의 정보를 가져오거나 쓸 때는 권한 부여가 필요 없다. 

      => 이 부분이 중요하다.

           많은 개발자들이 앱에서 비공개 외부 저장소만을 사용하는데, 불필요한 권한을 요청하는 경우가 있다. (그 개발자가 나임ㅠ)
  • 권한이 필요한 경우
    • 다른 패키지 앱의 정보를 읽거나 쓸 때
      • Environment.getExternalStoragePublicDirectory() 함수는 자신의 앱이 아닌 공유 영역이므로 권한이 필요하다.
      • 다른 앱의 패키지 정보를 이용해서 파일구조를 읽어올 때  
    • MediaStore 사용시

잠깐, MediaStore란? 

미디어 공급자와 응용 프로그램 간의 계약이라고 되어있다.
정확히는 안드로이드 시스템에서 제공하는 미디어 데이터 DB의 관리자이다.
MediaStore는 contract를 건드릴 수 있다. 즉, 미디어 테이터 DB를 건드릴 수 있다. 

이 부분이 이해하기가 참 어렵다. 

예를들어, 비공개 외부저장소에 이미지 파일을 저장을 하고, MediaStore를 이용해서 미디어 데이터 DB에 계약 관계를 등록만 시켜놓으면 앨범에서 보여진다.

MediaStore의 전용 공간이 있는 것이 아니다. 해당 파일을 미디어 공급자의 DB에 넣어 계약 관계를 만든다. 

 

* 사용 예시

android 10 이전 버전에서는 파일을 먼저 저장시키고, MediaStore 함수를 사용하는 방식을 많이 사용했다.

android 10 이상부터는 MediaStore와 contextResolver를 이용해서 공개 외부 저장소에 바로 폴더와 파일을 저장할 수 있게 되었다. 

/**
* 앨범에 이미지 저정하기
* (앨범 경로: sdcard/DCIM)
*/
 fun saveImageInAlbum(context: Context, fileName: String, bitmap: Bitmap) {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
            val values = ContentValues()
            with(values) {
                put(MediaStore.Images.Media.TITLE, fileName)
                put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
                put(MediaStore.Images.Media.RELATIVE_PATH, "DCIM/my_folder")
                put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
            }

            val uri = context.getContentResolver()
                .insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
            val fos = context.contentResolver.openOutputStream(uri!!)
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos)
            fos?.run {
                flush()
                close()
            }
        } else {
            val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DCIM).toString() +
                    File.separator +
                    "my_folder"
            val file = File(dir)
            if (!file.exists()) {
                file.mkdirs()
            }

            val imgFile = File(file, "test_capture.jpg")
            val os = FileOutputStream(imgFile)
            bitmap.compress(Bitmap.CompressFormat.JPEG, 100, os)
            os.flush()
            os.close()

            val values = ContentValues()
            with(values) {
                put(MediaStore.Images.Media.TITLE, fileName)
                put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis())
                put(MediaStore.Images.Media.BUCKET_ID, fileName)
                put(MediaStore.Images.Media.DATA, imgFile.absolutePath)
                put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
            }

            context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
        }
 }

위의 코드를 보면 10 이전 버전에선 getExternalStoraDirectory() 를 이용해서 바로 파일을 만들 수 있다. 

10 이상의 버전에선 그 코드가 MediaQuery를 통해 모두 처리하고 있다. 

android P 버전까지 잘 쓰던 getExternalStorageDirectory()의 슬픈 Deprecated 소식..ㅠ

 

현재 방식의 문제점!

문제점은 Private 영역이 있지만, 아주 쉽게 들여다 볼 수 있고 파일을 가져와 쓸 수 있다.
권한이 없더라도 마음만 먹으면 내 앱에서 다른앱의 비공개 외부저장소의 내용을 받아오거나, 파일을 덮어 쓸 수 있다.
그래서 내 앱에서 다른 앱의 데이터를 가져오는 해킹 시나리오를 실현해 보았다. 

해킹 시나리오: 타 앱의 특정 이미지를 빼와 내 앱에 저장시킨다. (capture_202003~.jpg 파일을 해킹한다.)

해킹할 앱의 패키지 구성요소
내 앱의 디버깅 모드에서 다른앱 패키지명만 알고 있다면, 이렇게 까보는건 일도 아니다.
내 앱으로 파일을 빼왔다!

fun hackingPhoto(context: Context) {
        // 파일 가져오기
        val toHackingPackageName = "/storage/emulated/0/Android/data/패키지명/폴더명"
        val imgName = File(toHackingPackageName).list().get(0)
        val hackingFile = File(toHackingPackageName + imgName)
        val fis = FileInputStream(hackingFile)
        val hackedBitmap = BitmapFactory.decodeStream(fis)

        // 가져온 파일 내 프로젝트에 저장하기
        val dir = context.getExternalFilesDir(Environment.DIRECTORY_PICTURES 
        			+ File.separator + "my_folder")
        dir?.let {
            if (!it.exists()) {
                it.mkdirs()
            }
        }
        val file = File(dir, imgName)
        val fos = FileOutputStream(file)
        hackedBitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos)
 }


이름이 비공개 외부저장소인데, 외부에서 볼 수 있고 파일을 맘대로 건드릴 수 있으면 좀 이상하지 않나요? 

(저는 아직도 헷갈립니다.)


<정리> 

  Version < Q (10) Version >= Q (10)
내부저장소 권한 요청 X X
비공개 외부저장소 권한 요청 X X
공개 외부저장소 권한 요청

<권한 종류>
WRITE_EXTERNAL_STORAGE, READ_EXTERNAL_STORAGE


<요청 시점>
내 앱 외의 영역을 읽거나 쓸 때
(MediaStore 사용 포함)

<권한 종류>
READ_EXTERNAL_STORAGE

<요청 시점>
MediaStore에서 파일을 읽을 때 
(MediaStore에서 공개영역 파일을 생성할 때는 필요 없습니다.)

외부저장소 파일 읽기 모든 영역 가능 
(다른 앱 영역까지 가능)

자신의 앱 영역만 가능

외부저장소 파일 쓰기  모든 영역 가능 
(다른 앱 영역까지 가능)

자신의 앱 영역만 가능

 


<마치며>

Android Q 가 2019년 9월에 출시됐으니 벌써 꽤 되었네요.

안드로이드 플레이 스토어 심사 기간도 늘어나고, 파일 저장소 관련 정책들이 많이 바뀌는 걸 보니, 보안에 신경을 많이 쓰고 있는 것 같습니다.

또한 지금까지 MediaStore 사용을 거의 하지 않았습니다만, 앞으로는 많이 사용하게 될 거 같습니다. 

 


 

잘못된 정보가 있을 수 있습니다. 많은 지적 부탁드립니다!

'android' 카테고리의 다른 글

Android Animation System 정리  (0) 2020.04.05
jitpack - Data Binding 분석하기  (0) 2020.03.13
RxJava 의 핵심 기술 - concat, merge, zip  (0) 2020.02.27
RxJava 란  (0) 2020.02.04
How to draw Rect Overlay View?  (0) 2020.01.05
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함