천복만복 프로그래밍/천복만복 안드로이드

Access app-specific files 가이드 문서 정리

U&MeBlue 2022. 9. 17. 18:22

이 글은 구글 안드로이드 가이드 문서인 Access app-specific files 을 읽고 정리한 내용입니다. 틀린 내용이 있을 수 있습니다.


많은 경우에 앱은 자신들만 사용할 파일들을 저장하고, 관리하는 경우가 많다. 안드로이드에서는 이러한 파일들을 관리하기 위해서 다음과 같은 저장소를 관리한다.

  • Internal Storage : 다른 앱은 접근할 수 없는 오직 본인의 앱만이 접근할 수 있는 영역이다. 영구적인 데이터를 저장하는 디렉토리와 캐시 데이터를 저장하는 디렉토리로 나뉜다. 앱이 제거된 경우 이 저장소에 있던 데이터도 함께 제거된다.
  • External Storage : 권한만 있다면 다른 앱도 접근할 수 있는 영역이다. 역시 영구적인 데이터르르 저장하는 디렉토리와 캐시 데이터를 저장하는 디렉토리로 나뉠 수 있다. 앱이 관리하는 고유 파일들은 앱이 제거되면 함께 제거된다. SD 카드와 같은 별도의 물리적 볼륨에 할당될 수 있기 때문에 이런 물리적 볼륨이 제거되어 접근이 안될 수 있다. Android 11 이 후부터 Scoped Storage 가 적용되어 다른 앱의 external storage 영역을 접근하는 것에 제한이 생겼다.

두 저장소 영역에 저장한 app specific 파일들은 앱이 제거되는 경우 함께 제거되기 때문에 앱이 제거되고 파일들이 남아있는 동작을 구현하려면 Media Storage API 를 사용해야 한다.

External Storage 에서 헷갈렸던 부분이 'Internal Storage == 내장 메모리', 'External Storage == 외장 메모리, SD 카드' 라고 생각했던 부분이다. 이렇게 생각하는 것은 틀렸다. 이 문서에서 말하는 Internal Storage, External Storage 의 구분은 다른 앱에서 접근 가능한지 여부이고, 실제로 단말기에서 구분짓는 디스크 볼륨의 종류와는 상관이 없다. 아래 사진과 같이 내장 메모리만 존재하는 단말기도 Internal Storage, External Storage 구분이 있으니 오해하지 말자.

Access from internal storage

각각의 앱마다 앱의 private 파일들을 저장할 수 있는 디렉토리가 주어진다. 이 디렉토리가 Internal Storage 이다. 이 디렉토리는 다시 영구적인 파일들을 저장하는 디렉토리와 캐시 파일들을 저장하는 캐시 디렉토리로 나누어질 수 있다.

Internal Storage 는 다른 앱에서 접근할 수 없기 때문에 민감한 데이터나 다른 앱이 접근해서는 안되는 데이터를 저장할 때 유용하게 사용할 수 있다.

그러나 internal storage 는 크기가 작은 편이기 때문에 작은 크기의 파일들을 저장할때만 사용해야 하며, 파일을 저장하기 전에 현재 디렉토리의 여유 공간을 체크하고 저장해야한다.

Access persistent files

앱이 영구적으로 저장될 필요가 있는 파일들을 관리할때는 Context.fileDir 속성을 이용해서 반환받을 수 있는 디렉토리 Flie 을 이용할 수 있다. 이 File 의 File API 직접 이용해서 파일을 저장하거나 접근할 수 있지만, 안드로이드에서 제공하는 일종의 Helper 함수들을 이용해서 조금 더 쉽게 관리할 수 있다.

Access and store files

일반적인 File API 를 이용해서 파일을 생성하거나 접근할 수 있다.

val file = File(context.fileDir, fileName)

Store a file using a stream

만약 파일에 데이터를 쓰는 등의 작업을 조금 더 편하게 하고 싶다면 context.openFileOut 과 같은 헬퍼 함수를 이용할 수 있다. 이 함수를 이용하면 자동으로 FileOutputStream 을 생성하며 반환해주기 때문에 코드를 줄여줄 수 있다.

val filename = "myfile"
val fileContents = "Hello world!"
context.openFileOutput(filename, Context.MODE_PRIVATE).use {
        it.write(fileContents.toByteArray())
}

Android 7 (api level 24) 이 후부터는 context.openFileOutput 함수의 두번째 인자로 Context.MODE_PRIVATE 를 전달하면 SecurityException 이 발생하기 때문에 주의해야한다.

Internal Storage 에 있는 파일들은 기본적으로 다른 앱에서 접근할 수 없지만, 앱에서 다른 앱에 파일을 공유해줄 수는 있는데 이럴때는 FileProvider api 를 이용해서 Internal Storage 에 있는 파일에 임시로 FLAG_GRANT_READ_URI_PERMISSION 권한을 부여해서 다른 앱에서 접근할 수 있도록 해준다.

Access a file using a stream

파일에 데이터를 쓸때와 비슷하게 파일에서 데이터를 읽어오기 위해서 직접 FileInputStream 을 만들지 않고 context.openFileInputStream 함수를 이용할 수 있다.

context.openFileInput(filename).bufferedReader().useLines { lines ->
    lines.fold("") { some, text ->
        "$some\n$text"
    }
}

만약 앱 설치 시점에 파일에 대한 inputStream 을 생성해서 사용하고 싶다면 원하는 파일을 res/raw 디렉토리에 저장해서 앱을 출시하고, openRawResource 메소드를 이용해서 해당 파일을 읽어들여 사용할 수 있다. res/raw 디렉토리에 저장한 파일들은 읽기 전용이라 수정은 불가능하다.

View list of files

context.fileList() 메소드를 이용해서 간단하게 Internal Storage 에 있는 File 들의 List 를 받아올 수 있다.

var files: Array<String> = context.fileList()

Create nested directories

Internal Storage 디렉토리 하위에 서브 디렉토리를 생성하고 사용하고 싶다면 context.getDir 를 이용해서 만들 수 있다(부모 디렉토리는 fileDir). 아니면 File API 를 이용해서 직접 서브 디렉토리들을 만들어 줄 수 있다.

context.getDir(dirName, Context.MODE_PRIVATE)

Create cache files

만약 Internal Storage 에 저장할 파일들이 일시적인 시간만 유지할 필요가 있다면 cache directory 를 이용해야 한다. 여기에 있는 파일들이 주기적으로 제거된다고 하더라도 앱이 직접 불필요해진 파일들을 그때그때 제거해주는 것이 좋다. context.cacheDir 로 접근할 수 있다.

Internal Storage 내부의 cache 디렉토리는 크기가 매우 작기 때문에 아주 작은 파일들을 관리할때 사용해야 한다. 현재 캐시에 얼만큼의 여유 공간이 있는지 체크하려면 getCacheQuotaBytes() 를 사용하자.

역시 context.cacheDir 와 File API 를 이용해서 직접 파일들을 생성하고 관리할 수도 있지만 몇가지 헬퍼 메소드를 통해서 손쉽게 파일들을 관리할 수 있다.

캐시 파일을 생성하기 위해서 File.createTempFile() 를 사용할 수 있다.

[getCacheQuotaBytes()](https://developer.android.com/reference/android/os/storage/StorageManager#getCacheQuotaBytes(java.util.UUID)).
``

캐시 파일에 접근하기 위해서 File API 를 사용한다.

```kt
val cacheFile = File(context.cacheDir, filename)

Remove cache files

안드로이드 시스템이 주기적으로 캐시 디렉토리 내부의 파일들을 제거해주기는 하지만 이런 동작에 의존하기 보다 불필요한 데이터를 그때 그때 제거해주는 것이 좋다. 역시 File API 를 이용해서 파일을 제거하거나 헬퍼 함수를 이용할 수 있다.

cacheFile.delete()
context.deleteFile(cacheFileName)

Access from external storage

만약 Internal Storage 가 앱 전용 파일들을 저장하고 관리하는데 크기가 적절하지 않다면 External Storage 사용을 고려할 수 있다. 안드로이드는 앱 전용 파일들을 관리하기 위해 External Storage 에 여러 디렉토리를 제공해주고 있다. 하나는 영구적인 파일들을 저장하는 디렉토리이며, 다른 하나는 캐시 파일들을 저장하는 디렉토리이다.

Android 4.4 (api level 19) 이상부터 External Storage 에 있는 본인의 앱 파일들을 접근하는데에 어떠한 저장소 권한도 필요하지 않게 되었다. 이 앱 전용 파일들은 앱이 제거될때 함께 제거된다.

주의할 것은 External Storage 가 SD 카드처럼 물리적으로 제거할 수 있는 볼륨에 할당될 수 있기 때문에 앱이 여기에 저장된 파일들에 항상 접근할 수는 없을 수도 있다. 앱이 동작하는데 필수적으로 필요한 파일들을 관리하기 위해서는 Internal Storage 를 사용하는 것이 바람직하다.

Android 9 (api level 28) 이하 버전에서는 앱이 적절한 저장소 권한만 있다면 External Storage 에서 다른 앱이 관리하는 파일들에도 접근할 수 있었다. Android 10 (api level 29) 이상부터는 파일들이 파편화를 막고, 사용자에게 조금 더 파일 관리의 접근성을 높여주기 위해서 앱은 한정된 저장소에만 접근할 수 있게 되었다. 만약 Scoped Storage 가 활성화된 경우라면 앱은 External Storage 에 있는 다른 앱의 파일들에 일반적인 방법으로는 접근할 수 없다.

Verify that storage is available

External Storage 는 사용자가 물리적으로 제거할 수 있는 볼륨에 할당될 수 있기 때문에 External Storage 에 저장된 파일을 읽거나 쓰기 전에 External Storage 에 접근할수 있는지부터 체크해야 한다. Enviorment.getExternalStorageState() 함수를 통해 반환받은 값을 통해서 현재 상태를 체크할 수 있다.

// Checks if a volume containing external storage is available
// for read and write.
fun isExternalStorageWritable(): Boolean {
    return Environment.getExternalStorageState() == Environment.MEDIA_MOUNTED
}

// Checks if a volume containing external storage is available to at least read.
fun isExternalStorageReadable(): Boolean {
     return Environment.getExternalStorageState() in
        setOf(Environment.MEDIA_MOUNTED, Environment.MEDIA_MOUNTED_READ_ONLY)
}

Select a physical storage location

때때로 어떤 단말기는 Internal 메모리의 일부를 External Storage 로 파티션 분할해서 사용함과 동시에 SD 카드 슬롯도 제공하는 경우가 있다. 이는 External Storage 가 할당된 여러개의 디스크 볼륨이 존재할 수 있다는 의미이다. 그래서 어떤 볼륨을 앱 전용 데이터를 관리하는데 사용할지 선택해야 한다.

여러 볼륨의 External Storage 들에 접근하기 위해서 ContextCompat.getExternalFilesDirs() 를 사용할 수 있다. 아래 코드에서 볼 수 있듯이 이 함수의 반환값으로 전달된 리스트의 첫번째 요소가 primary 볼륨으로 여겨진다. 이 볼륨의 저장공간이 모자르지 않는한, 이 공간을 사용하는 것이 좋다.

val externalStorageVolumes: Array<out File> =
        ContextCompat.getExternalFilesDirs(applicationContext, null)
val primaryExternalStorage = externalStorageVolumes[0]

Access persistent files

External Storage 에 있는 앱 전용 파일들에 접근하기 위해 getExternalFilesDir() 를 사용할 수 있다.

val appSpecificExternalDir = File(context.getExternalFilesDir(null), filename)

android 11 (api level 30) 이상부터는 external storage 에 앱 전용 디렉토리를 생성할 수 없다.

Create cache files

External Storage 에 캐시 파일들을 관리하기 위해서는 externalCacheDir 를 사용할 수 있다.

val externalCacheFile = File(context.externalCacheDir, filename)

Remove cache files

External Storage 에 캐시 파일들을 제거하기 위해서는 File api 를 이용해서 제거한다.

externalCacheFile.delete()

Media content

만약 사진과 같은 앱 전용 Media 파일들을 관리한다면 External Storage 에 미리 정의된 디렉토리에 이 파일들을 저장하는 것이 좋다.

fun getAppSpecificAlbumStorageDir(context: Context, albumName: String): File? {
    // Get the pictures directory that's inside the app-specific directory on
    // external storage.
    val file = File(context.getExternalFilesDir(
            Environment.DIRECTORY_PICTURES), albumName)
    if (!file?.mkdirs()) {
        Log.e(LOG_TAG, "Directory not created")
    }
    return file
}

파일들을 저장할때 Environment.DIRECTORY_PICTURES 와 같은 유효한 디렉토리 이름을 사용하는 것이 좋다. 이렇게 미디어 파일들을 적절한 전용 디렉토리에 저장해야 시스템에 의해서 해당 파일들이 적절한 방법으로 관리될 수 있다. 만약 Environment 의 상수로 명시된 그 어떤 디렉토리들에도 부합하지 않는 미디어 파일이 있다면 context.getExternalFilesDir 메소드에 첫번째 인자로 null 을 전달하여 파일을 저장할 수 있다. 이 디렉토리는 external storage 에서 앱 전용 파일들을 관리하는 루트 디렉토리를 지칭한다.

Query free space

많은 사용자의 단말기에는 앱 전용 파일들을 저장할 여유 공간이 없는 경우가 있을 수 있다. 만약 파일을 저장하기 전에 현재 여유 공간을 확인하기 위해서 getAllocatableBytes() 를 사용할 수 있다. 이 메소드의 반환값으로 나오는 수치는 실제 여유 공간보다 클 수 있는데, 이유는 안드로이드 시스템이 다른 앱에서 관리하는 캐시 데이터의 크기도 제거할 수 있다고 보고 여유 공간으로 생각하기 때문이다.

만약 이런 캐시 데이터를 제거해서 getAllocatableBytes() 에 나온 범위 내로 여유 공간을 확보하고 싶다면 [allocateBytes()](https://developer.android.com/reference/android/os/storage/StorageManager#allocateBytes(java.io.FileDescriptor,%20long)) 메소드를 사용할 수 있다. 또는 사용자에게 직접 파일들을 제거하거나 다른 앱들의 캐시 데이터를 삭제하도록 요청할 수도 있다.

// App needs 10 MB within internal storage.
const val NUM_BYTES_NEEDED_FOR_MY_APP = 1024 * 1024 * 10L;

val storageManager = applicationContext.getSystemService<StorageManager>()!!
val appSpecificInternalDirUuid: UUID = storageManager.getUuidForPath(filesDir)
val availableBytes: Long =
        storageManager.getAllocatableBytes(appSpecificInternalDirUuid)
if (availableBytes >= NUM_BYTES_NEEDED_FOR_MY_APP) {
    storageManager.allocateBytes(
        appSpecificInternalDirUuid, NUM_BYTES_NEEDED_FOR_MY_APP)
} else {
    val storageIntent = Intent().apply {
        // To request that the user remove all app cache files instead, set
        // "action" to ACTION_CLEAR_APP_CACHE.
        action = ACTION_MANAGE_STORAGE
    }
}

Create a storage management activity

앱에서 직접 앱 내부의 데이터를 관리하는 커스텀 액티비티를 구현할 수 있다. AndroidManifest.xml 파일에 android:manageSpaceActivity 태그를 추가하면 이 액티비티를 export 하지 않더라도 파일 매니저 앱에서 이 액티비티를 실행시키는 등의 동작이 가능하다.

Ask user to remove some device files

ACTION_MANAGE_STORAGE 인텐트를 사용해서 사용자에게 일부 파일을 제거하도록 요청할 수 있다. 추가적으로 현재 사용가능한 용량을 사용자에게 부가 정보로 제공할 수 있는데 다음과 같은 계산을 사용할 수 있다.

StorageStatsManager.getFreeBytes() / StorageStatsManager.getTotalBytes()

Ask user to remove all cache files

대안적으로 사용자에게 다른 모든 앱들의 캐시데이터를 삭제하도록 ACTION_CLEAR_APP_CACHE 인텐트를 이용해서 요청할 수 있다.

728x90