ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Storage] 안드로이드 저장소 정리
    Android/Storage 2021. 11. 3. 03:01

    목차

    이글은 안드로이드 11 변경사항에 대해 정리하지 않았습니다.

    1. 외부저장소 vs 내부저장소 vs 내장메모리

    2. 앱 개별파일과 앱 개별공간, 외부저장소 공유공간이란?

    3. 내부저장소 앱 개별공간, 외부저장소 앱 개별공간, 외부저장소 공유공간 위치 File Explorer로 확인

    (feat. /sdcard, /self,  /primary, /emulated, /mnt )

    4. Legacy Storage vs Scope Storage( 내부저장소 앱 개별공간, 외부저장소 앱 개별공간 )

    + 내부저장소에 앱 개별파일 쓰기 예제

    5. 외부저장소 볼륨이 여러개일 경우 -> 외부저장소 앱 개별공간은 여러개가 존재가능

    + Environment의 외부저장소 볼륨과 관련된 메서드( +Context, ContextCompat)

    + 외부저장소 앱 개별파일 쓰기 예제

    6. Legacy Storage vs Scoped Storage ( 외부저장소 공유공간(파일 공유공간) )

    + Legacy Storage 공유공간에 파일 쓰기 예제

    (MediaStore와 Storage Access Framework)

     

    외부저장소 권한

    [Android/Permission] - [Permissioni] 권한 체크, 획득

    [Android/Permission] - [Permission] 전반적인 권한 처리 로직


    1. 외부저장소(External Storage) vs 내부저장소(Internal Storage) vs 내장메모리(Internal Memory)

    내장메모리

    디바이스 내부에 존재하는 메모리입니다.

     

    내부저장소, 외부저장소

    내부 저장소의 내부라는 단어를 통해 -> '디바이스 내부에만' 존재,

    외부 저장소의 외부라는 단어를 통해 ->  '디바이스 외부에만 존재' 라고 오해하기 쉽습니다.

    그 결과 '외부 저장소를 외부에 존재하는 SD카드만이 외부 저장소에 해당한다' 라고 오해해버립니다.

     

    • 내부 저장소는 항상 디바이스 내부에 존재한다.
    • 외부 저장소는 다바이스 내부(내장 메모리)에 존재할 수도 있고, SD카드로 존재할 수도 있다.

     

    SD카드 슬롯을 분리하면 외부저장소가 분리되어 디바이스 내부에(내장 메모리에) 외부저장소가 하나도 없는 것일까요?

    SD카드를 지원하지 않는 디바이스는 외부 저장소가 없는 디바이스일까요?

    이미지 출처: https://mond-al.github.io/scopedstorage-filewrite

    근래 안드로이드 버전의 디바이스에는 대부분 내장 메모리에 박혀있는 빌트인 외부저장소가 존재합니다.

    그러므로 SD카드를 분리했다고 해서 디바이스 내부에 존재하는 외부저장소가 하나도 없다라고 결론지어서는 안됩니다.

    디바이스에 연결된 외부저장소 볼륨이 몇개인지 개수 체크를 필수적으로 해야겠지만, built-in 외부저장소가 대부분 존재한다고 봐도 좋을 정도로 기본적으로 다 내장 메모리에 존재합니다.

     

    built-in 외부저장소가 존재하며, SD카드를 지원하는 디바이스라면 다음과 같이 됩니다.

    내장 메모리 : 내부저장소 + built in 외부저장소

    외장 메모리 : SD카드 외부저장소 

     

    built-in 외부저장소가 존재하며, SD카드를 지원하지 않는 디바이스라면 다음과 같이 됩니다.

    내장 메모리 - 내부저장소 + built in 외부저장소

     

    대부분의 디바이스에서는 내부저장소가 외부저장소보다 더 작은 크기를 가집니다.

    (내부저장소의 경우 사용자가 디바이스에 존재하는 파일 매니저를 통해 접근이 불가능합니다)

     

    위에서 적었던 내용을 통해 다시 정리하면

    내부저장소의 경우 항상 디바이스 내부에 존재합니다 

    -> 항상 파일 접근이 가능합니다.

    외부저장소의 경우 항상 디바이스 내부에 존재할 것이라는 보장이 없습니다.

    -> 항상 파일 접근이 가능하다고 결론지어서는 안됩니다.

     

    빌트인 외부저장소의 경우 디바이스 내부에 존재하므로 항상 접근 가능하기 때문에

    내부저장소의 대안으로 빌트인 외부저장소에 파일을 저장하는 것이 좋습니다.

    SD카드 외부저장소의 경우 디바이스에서 제거될 가능성있으므로 항상 접근 가능하지 않기 때문에

    피치못해 파일을 쓸 경우 SD카드 외부저장소를 이용가능한지 확인하고 파일을 저장해야 합니다.

     

    그러나 후자의 경우는 접근 가능성이 보장되지 않으므로 대부분의 글에서 앱 개별파일을 SD카드 외부저장소 볼륨에 저장하는 내용을 다루지 않고 있습니다. 그냥 이렇게 해야하는구나라고만 생각하면 좋을 것 같습니다.


    2. 앱 개별파일과 앱 개별공간, 외부저장소 공유공간

     

    안드로이드 9 포함 이전의 저장소 구조에 해당하는 Legacy Storage 구조입니다.

    각 공간에 접근하는 법과, 필요한 권한, 파일접근 , Legacy Storage와 Scoped Storage는 뒤에서 설명하겠습니다.

     

    https://brunch.co.kr/@huewu/8

    앱 개별공간

    의미상으로 자신의 앱 내에서만 사용될 목적으로 사용될 파일을 앱 개별 파일(App specific file)이라고 부릅니다.

    앱 개별 파일(일반 파일, 캐시 파일에 해당)을 저장하는 공간을 앱 개별공간이라고 부릅니다.

     

    앱 개별공간은 내부저장소에도 존재하며 외부저장소에도 존재합니다.

    외부저장소의 경우 앱 개별공간은 외부저장소 볼륨마다 존재할 수도 있습니다.

    ex)빌트인 외부저장소 볼륨에 앱 개별공간이 존재하며, SD 외부저장소 볼륨에 앱 개별공간이 존재

    (해당 내용 참고 - 6. 외부저장소 각 볼륨에 앱 개별파일 쓰기)

     

    외부저장소 공유공간

    자신의 앱에서만이 아니라 다른 앱도 사용할 수 있도록 공유될 수 있는 파일들을 저장하는 공유공간이라고 부릅니다.

    특정 앱을 통해 저장된 파일들이 다른 앱에서 사용가능될 필요가 있으면 공유공간에 저장해야 합니다.


    3. File Explorer로 각 공간이 어디에 해당하는지 확인

    File Explorer에서 보여주는 디렉토리 구조는 실제 디바이스 경로와 조금 다르게 보여주게 되는데 시스템 파일이 존재하거나, 볼 수 없는 파일이 존재하는 디렉토리가 존재하는 경우 같은 디렉토리를 참조하는 다른 대안 디렉토리를 만들어 보여주기 때문에 클래스의 메서드에서 뽑은 절대경로와 다릅니다.

    너무 크게 신경쓰지 마시기 바랍니다.

    또한  안드로이드 버전, 디바이스마다 보여주는 디렉토리 이름이 다를 수 있습니다.

     

    테스트는 SD카드가 존재하는 안드로이드9, SD카드지원안하는 안드로이드 11 두 디바이스로 테스트하였고 

    각 공간에 파일을 저장하는 예제들인 계속 읽다보면 나옵니다. 각 예제의 결과만 땡겨가져왔습니다.

     

    내부저장소의 app specific file 

     

    안드로이드 9, 11동일

     

    외부저장소의 공유공간

     

    File Explorer에서 헷갈리게 보여주기 때문에, 외부저장소 각 볼륨부터 정확히 체크하고 외부저장소 공유공간, 앱 개별 공간을 확인하도록 합니다.

     

    SD카드 지원하는 안드로이드 9

     

    mnt/sdcard 

     sdcard

    storage/self/primary

    mnt/sdcard    sdcard    storage/self/primary  모두 동일한 공간을 나타내는데

    SD카드 외부저장소가 아니라 primary 외부저장소(빌트인 외부저장소 볼륨)에 해당합니다.

    SD카드 아님에 주의!!

     

    SD카드에 해당하는 외부저장소 볼륨은 storage/A70D-3DDD에 해당합니다.

    SD카드 언마운트, 마운트시 디렉토리가 사라졌다 나타났다 하는지 테스트 하기위해 만들어준 디렉토리가 하나 보이네요

    SD카드를 언마운트하고 FileExplorer를 새로고침해보면 A70D-3DDD가 없어집니다.

    mnt/sdcard  sdcard  storage/self/primary 는 모두 그대로 존재하고 있습니다.

    (새로고침 -> 디바이스 다시선택하거나 변경된 디렉토리만 우클릭의 synchronize)

     

    과연 File Explorer에서  sdcard문자열이 primary 외부저장소(빌트인 외부저장소) 볼륨일까요?

    SD카드를 지원하지 않는 안드로이드 11 디바이스로 재차 확인해봤습니다.

     

    /mnt/sdcard

    sdcard

    storage/emulated/0 , storage/self/primary 

    SD카드도 없는데 /mnt/sdcard 등등을 띄워주고 있습니다. 그러므로 primary 외부저장소 볼륨이라고 볼수 있습니다.

    또한 SD카드 외부저장소 볼륨이 존재하지 않기 때문에, 안드로이드 9 디바이스의 결과와 다르게 Storage/A70D-3DDD같이 해당하는 것이 없는 것을 확인하였습니다.

     

    기본적으로 우리가 자주보는 /Pictures /DICIM /Movies /Notification 등등의 디렉토리는 안드로이드에서 제공하는 built-in 외부저장소의 공유공간에 해당하는 각각의 디렉토리입니다. 각각의 디렉토리는 Environment클래스의DIRECTORY_XXX상수에 해당하는 디렉토리입니다.

    그러므로 SD카드를 제거해도 built-in 외부저장소는 항상 디바이스 내부에 존재하기 때문에 각 디렉토리를 관찰 가능합니다.

     

    외부저장소 앱 개별 공간

    외부저장소 볼륨이 여러개이면 외부저장소 앱  개별 공간도 여러개일수 있다고 했습니다.

    ( 추가적으로 SD카드 외부저장소 볼륨 앱 개별 공간에 저장가능한데 이 볼륨은 항상 접근가능하지 않아 그냥 이런 것이 있구나 정도로 보면 될 것 같습니다. 대부분의 글들에 이 내용이 없는 것이 왜인지 알겠죠 )

     

    SD카드가 존재하는 안드9 디바이스로 확인해봅시다

    (SD카드 마운트 상태에서 존재하는 모든 외부저장소 볼륨의 앱 개별공간의 경로를 뽑아 앱 개별파일을 쓴 결과)

     

    primary 외부저장소 볼륨의 앱 개별 공간

     

    secondary외부저장소(SD카드) 볼륨의 앱 개별 공간

    primary외부저장소(빌트인 외부저장소)에는 primary.txt파일이 저장되었고

    SD카드 외부저장소에는 secondary.txt파일이 저장된 것을 확인할 수 있습니다.

     

    지금까지 내부저장소 앱 개별공간, 외부저장소 앱 개별공간, 외부저장소 공유공간이 어디에 위치하는지 File Explorer로 확인하였습니다.

    이제부터 본격적으로 저장소를 알아봅시다.

    (밑에서 설명할 예제들에서 필요할  파일과 디렉토리를 다루는 File클래스를 잘 모른다면 참고하면 좋을 듯 합니다)

    [Android/Storage] - [Android] Java File 클래스 공부


    4. Legacy Storage vs Scope Storage( 내부저장소 앱 개별공간, 외부저장소 앱 개별공간 )

     

    Android 10(API 29) 에서 Scoped Storage(범위 지정 저장소)가 도입되었습니다.

    Android 9(API 28) 이하의 저장소를 Legacy Storage라고 부릅니다.

     

    Scoped Storage의 도입으로 내부저장소의 구조는 변하지 않았으며, 외부저장소의 구조만 변하게 되었습니다.

     

    SQLite 데이터베이스와 SharedPreference의 저장위치는 다룰내용이 별로 없어서 다루지 않았습니다.

     

    Legacy Storage

    https://brunch.co.kr/@huewu/8

    Scoped Storage

    https://brunch.co.kr/@huewu/8

     

    내부저장소의 앱 개별공간

    내부저장소의 앱 개별공간은 Scoped Storage에서 변경사항이 없습니다 (Legacy Storage와 동일)

    - 내부저장소 앱 개별공간은 한 앱이 다른 앱의 공간에 접근이 불가능한 샌드박스 공간

    - 자신의 앱에서 개별파일을 쓰고 읽을 때 권한이 필요 x

    - 앱 삭제시 앱 개별공간에 존재하는 앱 개별파일들이 모두 제거 o

     

    내부저장소의 앱 개별공간에 파일을 쓰는 예제입니다.

    (파일명 입력 EditText, 텍스트파일내용 입력 EditText 후 버튼 클릭하여 파일 write)

     

    activity_main.xml

    <?xml version="1.0" encoding="utf-8"?>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        xmlns:tools="http://schemas.android.com/tools"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical"
        tools:context=".MainActivity">
    
        <EditText
            android:id="@+id/et_file_name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:ems="10"
            android:hint="파일명"
            android:inputType="textPersonName" />
    
        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="0dp"
            android:layout_weight="1"
            android:orientation="vertical">
    
            <EditText
                android:id="@+id/et_file_content"
                android:layout_width="match_parent"
                android:layout_height="match_parent"
                android:ems="10"
                android:gravity="start|top"
                android:hint="내용"
                android:inputType="textPersonName|textMultiLine" />
        </LinearLayout>
    
        <Button
            android:id="@+id/btn_write_file_internal"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center"
            android:onClick="onClick"
            android:text="내부저장소 텍스트 파일쓰기" />
    
    </LinearLayout>

     

    Context클래스의 메서드로 내부저장소 앱 개별공간 영구파일 디렉토리 경로, 캐시파일 디렉토리 절대경로를 뽑습니다.

     

    https://developer.android.com/reference/kotlin/android/content/Context#getfilesdir

     

    뽑힌 경로에 파일명을 붙인 File인스턴스를 생성한뒤 파일을 write하면 됩니다.

     

    MainActivity

    package com.source.storage.internalstorage.appspecificfiletest
    
    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    import android.util.Log
    import android.view.View
    import android.widget.EditText
    import android.widget.Toast
    import java.io.BufferedWriter
    import java.io.File
    import java.io.FileWriter
    
    
    class MainActivity : AppCompatActivity() {
    
        private lateinit var editFileName: EditText
        private lateinit var editFileContent: EditText
    
        private fun log(str: String) {
            Log.d("Internal_App_Specific_File", str)
        }
    
        private fun toast(str: String) {
            Toast.makeText(this, str, Toast.LENGTH_LONG).show()
        }
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            editFileName = findViewById(R.id.et_file_name)
            editFileContent = findViewById(R.id.et_file_content)
        }
    
        private fun writeFileToInternalStorage(pathString: String) {
    
            val file = File(pathString)
            val fileWriter = FileWriter(file, false)
            val bufferedWriter = BufferedWriter(fileWriter)
            bufferedWriter.append(editFileContent.text.toString())
            bufferedWriter.close()
    
        }
    
        fun onClick(view: View) {
            when (view.id) {
                R.id.btn_write_file_internal -> {
    
                    val fileDirString: String = filesDir.path
                    // getfilesDir() -> return File, getPath() -> return String
                    log("내부저장소 내 앱 file 디렉토리: $fileDirString")
    
                    val fileNameString: String = editFileName.text.toString()
                    // EditText의 텍스트가 파일명
                    log("파일명: $fileNameString")
    
                    val savedPathString: String = "$fileDirString/$fileNameString"
                    log("파일이 저장될 경로 : $savedPathString")
                    writeFileToInternalStorage(savedPathString)
    
                    toast("내부저장소에 텍스트 파일 저장")
    
                }
            }
    
        }
    
    }​

    Device File Explorer를 통해 파일이 제대로 생성되었는지 확인합니다.

     

     

    다른 앱에서 바로 위의 앱의 내부저장소 앱 개별공간에 파일을 쓴 경우 입니다.

    자신의 앱에서 하드코딩된 경로로 파일을 썼을 경우에는 파일이 저장되었는데, 다른 앱에서 동일하게 하드코딩된 경로로 파일을 썼을 경우 예외가 발생합니다. 

    D: 예외 발생
    D: java.io.FileNotFoundException: /data/user/0/com.source.storage.internalstorage.appspecificfiletest/files/test.txt (Permission denied)

     

    다시 외부저장소의 내용으로 돌아갑니다.

     

    외부저장소의 앱 개별 공간

    Legacy Storage의 외부저장소 앱 개별 공간

    - 내부저장소 앱 개별공간과 다르게 샌드박스 공간 x

    의미상만 앱 개별 파일을 저장하는 앱 개별 공간이지, 외부저장소의 앱 개별 공간은 실제로는 다른 앱에서 외부저장소 권한만 있으면 접근이 가능합니다.

    [Android/Storage] - [Android] Legacy Storage 외부저장소 다른 앱 개별공간에 저장 가능?

    https://developer.android.com/training/data-storage/app-specific?hl=en

     

    엉만인 reference가 후반후에서야 Legacy와 Scoped의 차이를 제대로 나눠놨습니다...

    빨간색 부분들처럼 Legacy 외부저장소 앱 개별공간은 의미상으로만 앱 개별파일을 저장하는 공간으로 만들어졌지 외부저장소권한만 있으면 접근이 가능하다라고 나와있습니다.

     

    - 자신의 앱에서 파일을 쓰고 읽을 때는 권한이 필요없습니다( 근래 버전의 디바이스 )

    - 앱 삭제시 관련 파일이 같이 삭제 o

     

    Scoped Storage의 외부저장소 앱 개별 공간

    - 앱 삭제시 관련 파일 제거되지 않는 것은 Legacy 외부저장소 앱 개별 공간과 같습니다

    - 자신의 앱 개별공간에 파일 쓰고 읽을 때는 권한이 필요없는것은 Legacy 외부저장소 앱 개별 공간과 같습니다

    - 샌드박스 공간으로 변경되었습니다.

    Legacy Storage에서는 외부저장소 권한만 있으면 앱 개별 공간(외부저장소) 일지라도 다른 앱에서 접근 가능했던 것이 Scoped Storage에서는 불가능해졌습니다.

     

    외부저장소 앱 개별 공간에 파일을 쓰는 예제를 다루기 전에 먼저 확인해야 할 내용을 짚고 넘어가도록 합니다.


    5. 외부저장소 볼륨이 여러개일 경우 -> 외부저장소 앱 개별공간은 여러개가 존재 가능

    https://developer.android.com/training/data-storage/app-specific#external-select-location

    내장메모리에 빌트인 외부저장소를 제공하는 경우 SD카드 슬롯을 지원하는 경우가 종종있다.

    이는 외부 저장소를 포함하는 물리적 볼륨이 여러개가 있다는 것을 의미한다. 

    어느 app-specific 공간을 사용해야할지 정해야 할 필요가 있다. 해당 메서드의 리턴(배열)값에서 첫 요소가 primary 외부저장소 볼륨에 해당하는 앱 개별공간에 해당한다

    ( 이용불가능하거나(size가 0) , 꽉차지않았으면 primary 외부저장소 볼륨의 앱 개별공간을 사용해라 ) 

    https://developer.android.com/reference/kotlin/androidx/core/content/ContextCompat?hl=en#getexternalfilesdirs

    앱에서 앱 개별파일을 놓을 수 있는 디바이스의 모든 외부저장소 앱 개별 공간을 배열로 리턴한다. 뒤에는 뭔말인지 모르겠습니다.

     

    primary 외부저장소 앱 개별공간에 해당하는 배열의 첫요소는 Context의 getExternalFilessDir(String)과 같은 값을 리턴한다.

     

    영구 파일 말고 캐시 파일의 경우

    모든 외부 저장소 앱 개별 캐시 디렉토리를 배열로 리턴-> ContextCompat, Context의 getExternalCachesDirs()

    외부 저장소 단일 볼륨의 경우 -> primary는 getExternalCachesDir(), 볼륨경로를 넣어 getExternalCachesDir(경로) 로 사용하면 됩니다.

     

    Environment 클래스의 메서드들

    외부저장소 볼륨의 정보,상태, 접근등에 관한 메서드를 포함하고 있습니다.

    사용자의 볼륨 제거, 삽입에 대해 안전한 primary 외부저장소(빌트인 외부저장소)에 파일을 읽고쓰기는 경우가 대부분입니다.

    자주사용하는 만큼 primary 외부저장소와 관련된 메서드들은 파라메터에 경로를 넣지 않게 제공합니다.

    파라메터가 존재하는 메서드들은 파라메터에 해당하는 경로에 대한 외부저장소 볼륨에 관한 정보,상태,접근등과 관련된 메서드입니다.

     

     

    api 1

    primary 외부저장소 볼륨에 파일을 쓰거나 읽을 수 있는지를 리턴해주는 메서드

    인자가 필요없습니다.

     

    api 21

    File 경로에 해당하는 외부저장소 볼륨에 파일을 쓰거나 읽을 수 있는지를 리턴해주는 메서드

    (실재하는 디렉토리나 파일에 해당하는 File 인스턴스여야합니다.)

     

    (리턴값을 Environment의 MEDIA_XXXXX들과 비교하여 파일을 쓰거나 읽을 수 있는지 체크)

    MEDIA_MOUNTED - 마운트 되어있음. 읽기 쓰기 가능

    MEDIA_MOUNTED_READ_ONLY - 마운트 되어있지만. 읽기만 가능

    MEDIA_UNMOUNTED - 언마운트 되어있음

    MEDIA_EJECTING - 언마운트중

    MEDIA_CHECKING - 마운트중 ( 분리된 상태에서 집어넣거나, 언마운트 상태에서 마운트로 변경시)

    MEDIA_BAD_REMOVAL - 언마운트시키지 않고 분리

    MEDIA_REMOVAL - 언마운트시키고 분리

    MEDIA_UNKOWN - 알 수 없는 볼륨

     

    리턴값이 MEDIA_MOUNTED, MEDIA_MOUNTED_READ_ONLY 둘중하나면 최소 읽기 가능입니다.

     

    아래와 같이 오버로딩된 메서드가 하나씩 존재하는데 파라메터가 없는 메서드는 primary 외부저장소 볼륨 정보와 관련된 것이고, 파라메터에 경로를 넣어 추가적인 볼륨의 정보를 확인할 수 있는 메서드입니다.

     

    onCreate에서 테스트 ( 안드로이드9 SD카드 지원. SD카드 마운트 상태에서 외부저장소 앱 개별공간들에 파일 쓰기)

     

    package com.source.storage.external.externalappspecificfiletest
    
    
    import androidx.appcompat.app.AppCompatActivity
    import android.os.Bundle
    import android.os.Environment
    import android.util.Log
    import android.view.View
    import android.widget.Toast
    import androidx.core.content.ContextCompat
    import java.io.BufferedWriter
    import java.io.File
    import java.io.FileWriter
    
    //앱에서 앱 개별 파일을 저장할 수 있는 모든 외부저장소 앱 개별 공간
    class MainActivity : AppCompatActivity() {
    
        private fun log(str: String?) {
            Log.d("External_App_Specific_File", str.toString())
        }
    
        private fun toast(str: String) {
            Toast.makeText(this, str, Toast.LENGTH_LONG).show()
        }
    
        override fun onCreate(savedInstanceState: Bundle?) {
    
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            val allExternalAppSpecificFiles: Array<File> =
                ContextCompat.getExternalFilesDirs(this, null)
            log(
                "외부저장소 볼륨 개수: "
                        + allExternalAppSpecificFiles.size + " (첫번째 요소가 primary)"
            )
    
            if (allExternalAppSpecificFiles.isEmpty()) {
                log("외부저장소 없음")
                log("............................................")
            } else if (allExternalAppSpecificFiles.size == 1) {
                log("primary 외부저장소만 존재")
    
                log("primary 외부저장소 정보")
                //파라메터가 없는 메서드나, primary 외부저장소 경로를 넣는 메서드나 동일한 결과
                log("can write,read file: ${Environment.getExternalStorageState()}")
                log("physically removable: ${Environment.isExternalStorageRemovable()}")
                log("media is emulated: ${Environment.isExternalStorageEmulated()}")
    
                if (allExternalAppSpecificFiles[0] == null) {
                    log("볼륨은 존재하나 언마운트 상태")
                    log("경로 null")
                } else {
                    log("경로: ${allExternalAppSpecificFiles[0].path}")
                    if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
                        writeTextToAllExternalAppSpecificFileSpace(
                            allExternalAppSpecificFiles[0].path
                                    + "/primary.txt"
                        )
                        log("primary 외부저장소에 파일 생성")
                        toast("primary 외부저장소에 파일 생성")
    
                    } else {
                        log("primary 외부저장소 mounted 이외의 다른 한 상태")
                    }
                }
    
                log("............................................")
    
            } else if (allExternalAppSpecificFiles.size == 2) {
                log("primary 외부저장소, secondary 외부저장소 볼륨 존재")
                log("............................................")
                log("primary 외부저장소 정보")
    
                //primary 외부저장소의 체크는 경로를 넣어주지 않는 메서드를 이용가능
                log("physically removable: ${Environment.isExternalStorageRemovable()}")
                log("media is emulated: ${Environment.isExternalStorageEmulated()}")
                log("can write,read file: ${Environment.getExternalStorageState()}")
                log("mounted면 읽고쓰기 가능, mounted read only이면 읽기만 가능")
    
                if (allExternalAppSpecificFiles[0] == null) {
                    log("볼륨은 존재하나 언마운트 상태")
                    log("경로 null")
    
                } else {
                    log("경로: ${allExternalAppSpecificFiles[0].path}")
                    if (Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED) {
                        writeTextToAllExternalAppSpecificFileSpace(
                            allExternalAppSpecificFiles[0].path
                                    + "/primary.txt"
                        )
                        log("primary 외부저장소에 파일 생성")
                        toast("primary 외부저장소에 파일 생성")
                    } else {
                        log("primary 외부저장소 마운트 상태아님")
                    }
                }
                log("............................................")
    
    
                log("secondary 외부저장소 정보")
    
                if (allExternalAppSpecificFiles[1] == null) {
                    log("언마운트 상태")
                    log("경로 null")
    
                } else {
                    log("경로: ${allExternalAppSpecificFiles[1].path}")
    
                    log(
                        "physically removable: ${
                            Environment.isExternalStorageRemovable(
                                allExternalAppSpecificFiles[1]
                            )
                        }"
                    )
                    log(
                        "media is emulated: ${
                            Environment.isExternalStorageEmulated(
                                allExternalAppSpecificFiles[1]
                            )
                        }"
                    )
                    log(
                        "can write,read file: ${
                            Environment.getExternalStorageState(
                                allExternalAppSpecificFiles[1]
                            )
                        }"
                    )
                    log("mounted면 읽고쓰기 가능, mounted read only이면 읽기만 가능")
    
                    if (Environment.getExternalStorageState(allExternalAppSpecificFiles[1])
                        == Environment.MEDIA_MOUNTED
                    ) {
                        writeTextToAllExternalAppSpecificFileSpace(
                            allExternalAppSpecificFiles[1].path
                                    + "/secondary.txt"
                        )
                        log("secondary 외부저장소에 파일 생성")
                        toast("secondary 외부저장소에 파일 생성")
                    } else {
                        log("secondary 외부저장소 마운트 상태아님")
                    }
                }
    
            }
    
            log("............................................")
        }
    
        //File에 해당하는 String을 받아서 파일 write
        //default 내용 "테스트"
        private fun writeTextToAllExternalAppSpecificFileSpace(pathString: String) {
            val file = File(pathString)
            val fileWriter = FileWriter(file, false)
            val bufferedWriter = BufferedWriter(fileWriter)
            bufferedWriter.append("테스트")
            bufferedWriter.close()
    
        }
    
    }

     

     

    앱 삭제시 primary 외부저장소 앱 개별 공간의 파일만 삭제되지, SD카드 외부저장소의 앱 개별 공간의 파일은 삭제되지 않습니다. 

     

    SD카드를 언마운트 시킨상태에서 다시 앱을 실행시키면

    SD카드 볼륨은 배열에 포함되지만 배열 요소의 경로를 뽑아보면 null이 리턴됩니다.

     

    SD카드 슬롯을 열어 제거하고 다시 앱을 실행시키면

    모든 앱 개별공간을 리턴해주는 배열에서 사이즈가 하나 줄어 빌트인 외부저장소만 남은 것을 확인할 수 있습니다.

     

    마운트 상태가 궁금하면 BroadcastRecevier를 사용하여 알림을 받아 체크해볼 수도 있습니다.

    [Android/Storage] - [Android] SD카드 BroadcastReceiver

     

    공유공간으로 넘어갑니다.


    6. Legacy Storage vs Scoped Storage ( 외부저장소 공유공간(파일 공유공간) )

    Legacy Storage 공유공간

    Legacy Storage 공유공간은 Legacy Storage 외부저장소 앱 개별공간과 다르게 개별의 개념이 존재하지 않습니다.

    따라서 항상 파일 read, write시 외부저장소 권한이 필요합니다.

     

    [Android/Storage] - [Android] Legacy Storage 외부저장소 공유공간에 파일쓰기

     

    [Android] Android 9 외부저장소 공용공간에 파일쓰기

    Android 9이하에서 공유저장공간에 파일을 write해보고 권한이 없을 때 파일을 write하면 어떤 결과가 나타나는지 확인해본다. 필요한 내용만 정리하였기 때문에 권한이 획득, 거부 상태인지 체크하

    hellose7.tistory.com

     

    Scoped Storage 공유공간

    https://youngest-programming.tistory.com/386
    https://youngest-programming.tistory.com/386
    https://youngest-programming.tistory.com/386
    https://medium.com/microsoft-mobile-engineering/scoped-storage-in-android-10-android-11-28d58d989f3c

    Images, Video, Audio, Downloads의 파일들은 파일마다 소유자(앱)가 확실하게 분리되었기 때문에 개별파일의 개념이 존재하게 됩니다. 즉 공유공간에 자신의 파일을 공유공간에 read, write할 때 권한이 필요 없어졌습니다. 다른 앱의 공유공간 파일에 접근할 때만 read, write가 필요합니다.

     

    MediaStore 와 Storage Access Framework(SAF)

     

    MediaStore는 Android1 부터 존재해왔고 Storage Access Framework는 Android4.4 부터 존재해왔습니다.

    위에서 설명했던 Legacy Storage의 공유공간에 접근할 때(파일read,write) File api로 접근하였는데, 이것대신 MediaStore, SAF를 사용하는 것도 가능합니다. 

     

    reference에서는 Scoped Storage부터는 공유공간에 접근할 때, 기존해 존재했던 MediaStore와 SAF를 사용하라고 권장하고 있습니다.

     

    안드로이드 9이전, 10, 11 저장소 개요 도표로부터 외부저장소의 공유공간인 미디어 파일들과 기타 파일들만 채취해왔습니다. 

    --Documents and other files ( 미디어 파일을 제외한 나머지 파일들) 

     

    reference에서는 Storage Access Framework(SAF)를 사용하도록 권장하고 있고, 옆에 None의 권한 마찬가지로 SAF를 사용하였을 때는 권한이 필요없다는 말이됩니다. 시스템 파일 픽커라는 ui를 통해 사용자에게 파일,디렉토리 선택,생성을 위임시키기 때문에 권한이 필요없습니다. 

     

    SAF 같은경우

    인텐트를 날려 시스템 파일 픽커를 띄워 인텐트에 해당하는 작업을 사용자가 완료한후 uri를 리턴받는데, 아마 각 디렉토리나 파일의 소유자에 따라서 uri의 유효성의 차이가 있는 것 같습니다.

     

    권장사항이 Storage Access Framework이지  MediaStore api도 사용할 수 있습니다.

    MediaStore.Files, MediaStore.Downloads부분을 사용합니다.

    MediaStore.Files는 api11에 도입되어 미디어 파일이외의 파일들을 지원합니다.

    안드로이드 10(api 29)에서 MediaStore.Downloads 가 추가되었는데, 추가된 결과로 Scoped Storage가 활성상태인지에 따라 MediaStore.Files에 담기는 것들이 변하였습니다.

     

    --미디어 파일의 경우 MediaStore를 사용하기를 권장하고 있습니다.

     

    권한은 Scoped Storage에서 설명한 것과 같네요

    (MediaStore.Images, MediaStore.Video, MediaStore.Audio 를 이용합니다)

     

    MediaStore api를 통해 접근하는 경우

     

    • 권한을 가지고 있지 않은경우

     

    Legacy Storage->  외부저장소 접근이 불가능하므로 미디어 파일 쿼리시 받는 것이 아무것도 없습니다.

    또한 쓰기,수정도 불가능 합니다.

    Scoped Storage-> 미디어 파일 쿼리시 자신의 앱에서 생성한 파일이 있다면 쿼리시 자신의 앱에 해당하는 것만 담깁니다. 기존에 자신의 앱에서 생성했던 미디어 파일이 있다면 수정가능하며, 새 파일 쓰기가 가능합니다.

     

    • READ_EXTERNAL_STORAGE 권한을 가지고 있는 경우

    Legacy Storage -> 미디어 파일을 쿼리할 때, 자신+다른앱들이 생성한 미디어 파일의 정보를 모두 받습니다.

    쓰기는 권한이 없기 때문에  어떠한 것이든 불가능합니다.

    Scoped Storage -> 미디어 파일을 쿼리할 때, 자신+다른앱들이 생성한 미디어 파일의 정보를 모두 받습니다.

    쓰기는 권한이 없기 때문에 자신의 앱에서 생성했던 것만, 새로 생성할 것만 가능합니다.

     

    •  WRITE_EXTERNAL_STORAGE 권한을 가지고 있는 경우

    Legacy Storage -> 누가 생성한 미디어 파일이든 수정하거나, 새로운 미디어 파일을 생성 할 수 있습니다.

    Scoped Storage -> WRITE권한을 가지고 있더라하더라도 다른 앱의 미디어 파일은 수정할 수 없습니다.

     

    다른 앱의 파일에 관한 ContentUri를  인자로 넣어주어 파일을 오픈할때,

    던져지는 RecoverableSecurityException을 통해 사용자로부터 동의를 받아와야합니다.(added in api 29)

     

     

    아래와 같다고 합니다.

    https://codechacha.com/ko/android-mediastore-remove-media-files/

     


    https://codechacha.com/ko/android-mediastore-remove-media-files/

     

    안드로이드 MediaStore에서 미디어 파일 삭제하는 방법

    ContentResolver.delete()는 Android MediaStore의 Image/Video/Audio 를 삭제할 수 있습니다. Android 10(targetsdk 29)에서는 Scoped Storage로, 사용자에게 허락을 받아야 합니다. 반면에 API 29 미만에서는 WRITE_EXTERNAL_STORAGE

    codechacha.com

    https://developer.android.com/reference/android/provider/MediaStore#getRequireOriginal(android.net.Uri) 

     

    MediaStore  |  Android Developers

     

    developer.android.com

    https://developer.android.com/reference/kotlin/android/app/RecoverableSecurityException#getuseraction

     

    RecoverableSecurityException  |  Android Developers

     

    developer.android.com

    https://developer.android.com/reference/kotlin/android/app/RemoteAction

     

    RemoteAction  |  Android Developers

     

    developer.android.com

    https://developer.android.com/training/data-storage/shared/media?hl=en#kotlin 

     

    공유 저장소의 미디어 파일에 액세스  |  Android 개발자  |  Android Developers

    공유 저장소의 미디어 파일에 액세스 많은 앱에서 더욱 풍부한 사용자 환경을 제공하기 위해 사용자가 외부 저장소 볼륨에서 사용 가능한 미디어를 제공하고 액세스할 수 있게 합니다. 프레임

    developer.android.com

    https://developer.android.com/reference/kotlin/android/content/ContentResolver#update

     

    ContentResolver  |  Android Developers

     

    developer.android.com

    https://developer.android.com/reference/kotlin/android/database/Cursor.html

     

    Cursor  |  Android Developers

     

    developer.android.com

    https://developer.android.com/reference/kotlin/android/content/ContentUris?hl=en 

     

    ContentUris  |  Android Developers

     

    developer.android.com

    https://developer.android.com/reference/kotlin/android/content/ContentValues

     

    ContentValues  |  Android Developers

     

    developer.android.com

    https://developer.android.com/reference/android/media/ExifInterface

     

    ExifInterface  |  Android Developers

     

    developer.android.com

    https://developer.android.com/reference/java/io/File

     

    File  |  Android Developers

     

    developer.android.com

    https://developer.android.com/reference/kotlin/android/os/Environment

     

    Environment  |  Android Developers

     

    developer.android.com

     

    https://developer.android.com/reference/kotlin/android/content/Context

     

    Context  |  Android Developers

     

    developer.android.com

    https://developer.android.com/reference/kotlin/androidx/core/content/ContextCompat?hl=en 

     

    ContextCompat  |  Android Developers

     

    developer.android.com

     

    댓글

Designed by Tistory.