Android 10 ๋ณ€๊ฒฝ ์‚ฌํ•ญ : Scoped Storage

Android 10 ๋ณ€๊ฒฝ ์‚ฌํ•ญ : Scoped Storage

Android 10์˜ ํฐ ๋ณ€๊ฒฝ ์‚ฌํ•ญ ์ค‘ ํ•˜๋‚˜์ธ Scoped Storage์— ๋Œ€ํ•ด ์ •๋ฆฌํ•ด ๋ณด์•˜์Šต๋‹ˆ๋‹ค.

(์˜ˆ์ œ ์ฝ”๋“œ๋Š” ๋ชจ๋‘ Kotlin ์ž…๋‹ˆ๋‹ค.)

Scoped Storage

์‚ฌ์šฉ์ž์—๊ฒŒ ํŒŒ์ผ์— ๋Œ€ํ•œ ๋” ๋งŽ์€ ๊ถŒํ•œ์„ ์ฃผ๊ธฐ ์œ„ํ•ด ์•ฑ ๋‚ด๋ถ€ ๋””๋ ‰ํ† ๋ฆฌ ๋ฐ ์•ฑ์—์„œ ์ƒ์„ฑํ•œ ์™ธ๋ถ€ ๋””๋ ‰ํ† ๋ฆฌ์˜ ํŒŒ์ผ ์™ธ์—๋Š” ์ ‘๊ทผ์ด ์ œํ•œ

์ ์šฉ๋˜๋Š” ๋ฒ„์ „

  • TargetSdkVersion โ‰ฅ 29 ๋ถ€ํ„ฐ ์ ์šฉ
  • ์ผ์‹œ์ ์œผ๋กœ ํ•ด์ œ ๊ฐ€๋Šฅ (๊ธฐ์กด ๋ฐฉ์‹ ์‚ฌ์šฉ)
<manifest ... >
  <!-- Android 10 ์ด์ƒ์—์„œ default = "false" -->
  <application android:requestLegacyExternalStorage="true" ... >
    ...
  </application>
</manifest>


  • Android 11๋ถ€ํ„ฐ๋Š” TargetSdkVersion๊ณผ ์ƒ๊ด€์—†์ด ์ ์šฉ๋  ์˜ˆ์ •์ด๋ผ๋Š” ์–˜๊ธฐ๋„ ์žˆ์Œ

๋‹ฌ๋ผ์ง€๋Š” ์ 

์•ฑ ์ „์šฉ ๋””๋ ‰ํ† ๋ฆฌ

  • Internal
  • External

ํŒŒ์ผํƒ€์ž…

  • persist file
  • cache file
    • ์ผ์‹œ์ ์ธ ์บ์‹œ ํŒŒ์ผ
    • ๊ธฐ๊ธฐ ๋ฉ”๋ชจ๋ฆฌ๊ฐ€ ๋ถ€์กฑํ•ด์ง€๋ฉด ์‹œ์Šคํ…œ์—์„œ ์‚ญ์ œํ•  ์ˆ˜๋„ ์žˆ์Œ

Internal

  • ๋‹ค๋ฅธ ์•ฑ์ด ์ ‘๊ทผํ•  ์ˆ˜ ์—†์Œ
  • Android 10 ์ด์ƒ ๊ธฐ๊ธฐ๋ถ€ํ„ฐ๋Š” ์•”ํ˜ธํ™”๋˜๋Š” ๊ณต๊ฐ„
  • Internal์— ํ• ๋‹น๋œ ์šฉ๋Ÿ‰์ด ์ ๊ธฐ ๋•Œ๋ฌธ์—, ํŒŒ์ผ์„ ์“ฐ๊ธฐ ์ „์— ์—ฌ์œ  ๊ณต๊ฐ„ ํ™•์ธ ํ•„์š”

  • persistent file
    //ํŒŒ์ผ ์ ‘๊ทผ ๋ฐ ์ƒ์„ฑ
    val file = File(context.filesDir, filename)
    
  • cache file
    //์ƒ์„ฑ
    File.createTempFile(filename, null, context.cacheDir)
    //์ ‘๊ทผ
    val cacheFile = File(context.cacheDir, filename)
    

External

  • ์‚ฌ์šฉ์ž๊ฐ€ ๋ฌผ๋ฆฌ์ ์œผ๋กœ ์ œ๊ฑฐํ•  ์ˆ˜๋„ ์žˆ๋Š” ์™ธ์žฅ ๊ณต๊ฐ„ (ex, sd card)
  • ๊ถŒํ•œ์„ ๊ฐ€์ง„ ๋‹ค๋ฅธ ์•ฑ์ด ์ ‘๊ทผํ•  ์ˆ˜๋Š” ์žˆ์ง€๋งŒ, ์ด ๊ณต๊ฐ„์˜ ์˜๋„๋Š” ์†Œ์œ ํ•œ ์•ฑ์—์„œ๋งŒ ๋˜๋Š” ๊ณต๊ฐ„
    • -> ์ƒ์„ฑํ•œ ํŒŒ์ผ์ด ๋‹ค๋ฅธ ์•ฑ์—์„œ๋„ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•ด์•ผ ํ•œ๋‹ค๋ฉด, External storage ๋ณด๋‹ค๋Š” ๊ณต์šฉ ๊ณต๊ฐ„์— ์ €์žฅํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅ
//Persistent file
val appSpecificExternalDir = File(context.getExternalFilesDir(), filename)
//Cache file
val externalCacheFile = File(context.externalCacheDir, filename)

์šฉ๋Ÿ‰ ํ™•์ธ

StorageStatsManager.getFreeBytes() / StorageStatsManager.getTotalBytes()

๊ณต์šฉ ๊ณต๊ฐ„์˜ ํŒŒ์ผ ์ ‘๊ทผ

  • MediaStore
  • Documents & etc.

  • ๋‹ค๋ฅธ ์•ฑ์—์„œ ์ ‘๊ทผ ๊ฐ€๋Šฅ
  • ํŒŒ์ผ์„ ์ƒ์„ฑํ•œ ์•ฑ์ด ์‚ญ์ œ๋˜๋”๋ผ๋„ ํŒŒ์ผ ์œ ์ง€

Media Store

  • ์‹œ์Šคํ…œ์—์„œ ํŒŒ์ผ ์ข…๋ฅ˜์— ๋”ฐ๋ผ public directory๋ฅผ ์ œ๊ณต
  • ์ด๋ฏธ์ง€, ์˜ค๋””์˜ค, ๋น„๋””์˜ค ๋“ฑ ํŒŒ์ผ ์ข…๋ฅ˜์— ๋งž๋Š” ๊ณตํ†ต ๊ณต๊ฐ„์— ์ €์žฅ๋จ

  • ์ด๋ฏธ์ง€: ์‚ฌ์ง„, ์Šคํฌ๋ฆฐ์ƒท ๋“ฑ / MediaStore.Images
  • ๋น„๋””์˜ค: MediaStore.Video
  • ์˜ค๋””์˜ค: MediaStore.Audio
  • ๋‹ค์šด๋กœ๋“œ ํŒŒ์ผ: Android 10 ์ด์ƒ ๊ธฐ๊ธฐ๋ถ€ํ„ฐ / MediaStore.Downloads
  • MediaStore.Files
    • Scoped Storage - On: ์•ฑ์ด ์ƒ์„ฑํ•œ ์ด๋ฏธ์ง€, ๋น„๋””์˜ค, ์˜ค๋””์˜ค๋งŒ ๋ฐ˜ํ™˜
    • Scoped Storage - Off: ๋ชจ๋“  ๋ฏธ๋””์–ด ํŒŒ์ผ ๋ฐ˜ํ™˜

Permission ๋ณ€๊ฒฝ์‚ฌํ•ญ

Scoped Storage - On

  • ์•ฑ์—์„œ ์ƒ์„ฑํ•œ ๋ฏธ๋””์–ด ํŒŒ์ผ์—๋งŒ ์ ‘๊ทผํ•œ๋‹ค๋ฉด Android 10 ์ด์ƒ ๊ธฐ๊ธฐ์—์„œ๋Š” ๋ณ„๋„์˜ ๊ถŒํ•œ ์š”์ฒญ์ด ํ•„์š”์—†์Œ
    • 28 ์ดํ•˜ ๊ธฐ๊ธฐ์—์„œ๋งŒ ์š”์ฒญํ•˜๋„๋ก ๋ถ„๊ธฐ์ฒ˜๋ฆฌ ๊ฐ€๋Šฅ
      <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"
                       android:maxSdkVersion="28" />
    
  • ๋‹ค๋ฅธ ์•ฑ์ด ์ƒ์„ฑํ•œ ๋ฏธ๋””์–ด ํŒŒ์ผ์„ ์ฝ๋Š”๋‹ค๋ฉด READ_EXTERNAL_STORAGE ๊ถŒํ•œ ํ•„์š”
  • ๋ฏธ๋””์–ด์˜ ์œ„์น˜ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋ ค๋ฉด ACCESS_MEDIA_LOCATION ๊ถŒํ•œ ํ•„์š” -> ๋Ÿฐํƒ€์ž„ ์š”์ฒญ ๊ถŒํ•œ

Scoped Storage - Off || 28์ดํ•˜ ๊ธฐ๊ธฐ

  • ๋™์ž‘์— ๋”ฐ๋ผ READ_EXTERNAL_STORAGE / WRITE_EXTERNAL_STORAGE ๊ถŒํ•œ ํ•„์š”

๋ฏธ๋””์–ด ๊ณต์œ 

  • content:// URI ์‚ฌ์šฉํ•˜์—ฌ ๊ณต์œ 

ํŒŒ์ผ ๊ฒฝ๋กœ๋กœ ๋ฏธ๋””์–ด์— ์ ‘๊ทผ

์˜ˆ) /sdcard/DCIM/IMG1024.JPG

  • Scoped Storage ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ, READ_EXTERNAL_STORAGE ๊ถŒํ•œ์ด ์žˆ์–ด๋„ ์•ฑ ๋””๋ ‰ํ† ๋ฆฌ๊ฐ€ ์•„๋‹Œ ํŒŒ์ผ์€ ๊ฒฝ๋กœ๋กœ ์ ‘๊ทผ ๋ถˆ๊ฐ€๋Šฅ
    • ์•ฑ๋ณ„ ๋””๋ ‰ํ† ๋ฆฌ๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ FileNotFoundException ๋ฐœ์ƒ
    • ์ด์œ : /sdcard๊ฐ€ ๊ฐ ์•ฑ๋ณ„ ํ™ˆ ๋””๋ ‰ํ† ๋ฆฌ๋กœ ๋งคํ•‘ -> ์‹ค์ œ๋กœ๋Š” /sdcard/Android/sandbox/<package-name>/DCIM/IMG1024.JPG
  • MediaStore API ์‚ฌ์šฉํ•ด์•ผ ํ•จ

๋ฏธ๋””์–ด ํŒŒ์ผ ์ถ”๊ฐ€ํ•˜๊ธฐ

// Add a specific media item.
val resolver = applicationContext.contentResolver

// primary external storage์—์„œ ์˜ค๋””์˜ค ์ปฌ๋ ‰์…˜ ์ฟผ๋ฆฌ
val audioCollection = MediaStore.Audio.Media.getContentUri(
        MediaStore.VOLUME_EXTERNAL_PRIMARY) // API <= 28 : VOLUME_EXTERNAL ์‚ฌ์šฉ

// ์ƒˆ ์˜ค๋””์˜ค ์ปจํ…ํŠธ ์ƒ์„ฑ
val newSongDetails = ContentValues().apply {
    put(MediaStore.Audio.Media.DISPLAY_NAME, "My Song.mp3")
}

// ๋‚˜์ค‘์— ์ˆ˜์ •์ด ํ•„์š”ํ•  ๋•Œ ์ด URI ํ†ตํ•ด์„œ ์ ‘๊ทผ
val myFavoriteSongUri = resolver.insert(audioCollection, newSongDetails)

ํŒŒ์ผ ์ถ”๊ฐ€์‹œ ๋””๋ ‰ํ† ๋ฆฌ ์ง€์ •ํ•˜๊ธฐ

Android 10 ์ด์ƒ ๊ธฐ๊ธฐ์—์„œ๋Š” dafault - ํŒŒ์ผ ํƒ€์ž… ๊ธฐ๋ฐ˜์œผ๋กœ ๋ถ„๋ฅ˜ ์˜ˆ) ์ด๋ฏธ์ง€ ํŒŒ์ผ์„ ์ถ”๊ฐ€ํ•˜๋ฉด Environment.DIRECTORY_PICTURES ๊ฒฝ๋กœ์— ์ €์žฅ

Pictures/MyVacationPictures ์ฒ˜๋Ÿผ ํ•˜์œ„ ํด๋”๋ฅผ ์ง€์ •ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ํŒŒ์ผ ์ถ”๊ฐ€์‹œ MediaColumns.RELATIVE_PATH ์นผ๋Ÿผ ์„ค์ •

์—…๋ฐ์ดํŠธ / ์‚ญ์ œ

๋‚ด ์•ฑ์ด ์ƒ์„ฑํ•œ ๋ฏธ๋””์–ด

Scoped Storage On/Off์— ์ƒ๊ด€์—†์ด ๋ชจ๋‘ ๋™์ž‘

๋‹ค๋ฅธ ์•ฑ์ด ์ƒ์„ฑํ•œ ๋ฏธ๋””์–ด && Scoped Storage ์‚ฌ์šฉ

์—…๋ฐ์ดํŠธ / ์‚ญ์ œ ๋™์ž‘์ด ํ•„์š”ํ•  ๋•Œ๋งˆ๋‹ค ํ•ด๋‹น ๋ฏธ๋””์–ด ํŒŒ์ผ์— ๋Œ€ํ•œ ๊ถŒํ•œ์„ ๋ฐ›์•„์•ผ ํ•จ

  • ์˜ˆ์‹œ ์ฝ”๋“œ
    try {
      contentResolver.openFileDescriptor(image-content-uri, "w")?.use {
          setGrayscaleFilter(it)
      }
    } catch (securityException: SecurityException) {
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
          val recoverableSecurityException = securityException as?
              RecoverableSecurityException ?:
              throw RuntimeException(securityException.message, securityException)
    
          val intentSender = recoverableSecurityException.userAction.actionIntent.intentSender
          intentSender?.let {
              //๊ถŒํ•œ ์š”์ฒญ
              startIntentSenderForResult(intentSender, image-request-code,
              null, 0, 0, 0, null)
          }
      } else {
          throw RuntimeException(securityException.message, securityException)
      }
    }
    

MediaStore๊ฐ€ ์•„๋‹Œ ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์„ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š” Use cases

๋ฏธ๋””์–ด ํŒŒ์ผ ์—ฌ๋Ÿฌ ๊ฐœ๋ฅผ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒฝ์šฐ

ACTION_OPEN_DOCUMENT_TREE ์ธํ…ํŠธ ์‚ฌ์šฉ

๋‹ค๋ฅธ ํƒ€์ž…์˜ ํŒŒ์ผ์„ ์ฒ˜๋ฆฌํ•˜๋Š” ๊ฒฝ์šฐ

MediaStore์—์„œ ์ธํ„ฐํŽ˜์ด์Šค๋ฅผ ์ œ๊ณตํ•˜์ง€ ์•Š๋Š” ๋ฌธ์„œ๋‚˜ ๋‹ค๋ฅธ ํŒŒ์ผ์€ ACTION_OPEN_DOCUMENT ์ธํ…ํŠธ ์‚ฌ์šฉ

Documents & etc.

  • ๋ฌธ์„œ๋ฅผ ํฌํ•จํ•œ ๊ธฐํƒ€ ํŒŒ์ผ

  • Android 4.4๋ถ€ํ„ฐ ACTION_OPEN_DOCUMENT ์ธํ…ํŠธ ์ถ”๊ฐ€
    • ์‹œ์Šคํ…œ ํŒŒ์ผ ์„ ํƒ๊ธฐ๋ฅผ ํ†ตํ•ด ํŒŒ์ผ ์„ ํƒ ๊ธฐ๋Šฅ
Action OS Version ์—ญํ• 
ACTION_GET_CONTENT 1 ~ ๋‹จ์ˆœํžˆ ๋ฐ์ดํ„ฐ ์ฝ๊ณ  ๊ฐ€์ ธ์˜ค๊ธฐ (์‚ฌ๋ณธ์„ ์ฝ์–ด์˜ด)
ACTION_OPEN_DOCUMENT 19 ~ DocumentProvider์˜ ํŒŒ์ผ์— ์žฅ๊ธฐ์ , ์ง€์†์  ์•ก์„ธ์Šค ๊ถŒํ•œ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ
ACTION_OPEN_DOCUMENT_TREE 21 ~ ๋””๋ ‰ํ† ๋ฆฌ ์„ ํƒ
  • ๊ฐ„๋‹จํ•œ ํ”Œ๋กœ์šฐ
    1. ์•ฑ์—์„œ ํŒŒ์ผ ์ƒ์„ฑ/์—ด๊ธฐ ๋“ฑ ์Šคํ† ๋ฆฌ์ง€ ๊ด€๋ จ ์ธํ…ํŠธ ์‹คํ–‰
    2. ์‹œ์Šคํ…œ ํŒŒ์ผ ์„ ํƒ๊ธฐ UI ํ†ตํ•ด์„œ ์‚ฌ์šฉ์ž๊ฐ€ ํŒŒ์ผ ๋˜๋Š” ๋””๋ ‰ํ† ๋ฆฌ๋ฅผ ์„ ํƒ
    3. ์•ฑ์ด ํ•ด๋‹น ํŒŒ์ผ ๋˜๋Š” ๋””๋ ‰ํ† ๋ฆฌ์˜ ๊ถŒํ•œ์„ ์–ป์–ด URI๋ฅผ ํ†ตํ•ด ์ ‘๊ทผ

ํŒŒ์ผ ์ƒ์„ฑ

  • ACTION_CREATE_DOCUMENT
  • ์‚ฌ์šฉ์ž๊ฐ€ ์‹œ์Šคํ…œ ํŒŒ์ผ ์„ ํƒ๊ธฐ UI์—์„œ ํŒŒ์ผ์„ ์ €์žฅํ•  ์œ„์น˜ ์„ ํƒ
  • ๋ฎ์–ด์“ฐ๊ธฐ ์ง€์› X -> ์ด๋ฆ„์ด ์ค‘๋ณต๋œ ๊ฒฝ์šฐ ํŒŒ์ผ ์ด๋ฆ„ ๋’ค์— numbering

  • Intent์— ํŒŒ์ผ ์„ ํƒ๊ธฐ๊ฐ€ ์—ด๋ฆด ํด๋”์˜ URI ์ง€์ • ๊ฐ€๋Šฅ - EXTRA_INITIAL_URI
    • ์œ ํšจํ•˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ์—ด๋ฆด ํด๋”๋ฅผ ์‹œ์Šคํ…œ์ด ์•Œ์•„์„œ ์ง€์ •
// Request code for creating a PDF document.
const val CREATE_FILE = 1

private fun createFile(pickerInitialUri: Uri) {
    val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
        addCategory(Intent.CATEGORY_OPENABLE)
        type = "application/pdf"
        putExtra(Intent.EXTRA_TITLE, "invoice.pdf")

        // Optional
        putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri) //์‹œ์Šคํ…œ ํŒŒ์ผ ์„ ํƒ๊ธฐ๊ฐ€ ์—ด๋ฆด ์œ„์น˜ ์ง€์ •
    }
    startActivityForResult(intent, CREATE_FILE)
}

ํŒŒ์ผ ์—ด๊ธฐ

  • ACTION_OPEN_DOCUMENT
  • EXTRA_INITIAL_URI ์ง€์ • ๊ฐ€๋Šฅ

ํด๋”์— ๋Œ€ํ•œ ๊ถŒํ•œ ์–ป๊ธฐ

  • ACTION_OPEN_DOCUMENT_TREE (๋กค๋ฆฌํŒ ๋ฒ„์ „๋ถ€ํ„ฐ ์ถ”๊ฐ€)
  • ์„ ํƒํ•œ ํด๋”์˜ ๋ชจ๋“  ํ•˜์œ„ ํŒŒ์ผ ๋ฐ ํด๋”์— ๋Œ€ํ•œ ๊ถŒํ•œ
  • EXTRA_INITIAL_URI ์ง€์ • ๊ฐ€๋Šฅ
fun openDirectory(pickerInitialUri: Uri) {
    // Choose a directory using the system's file picker.
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
        // Provide read access to files and sub-directories in the user-selected
        // directory.
        flags = Intent.FLAG_GRANT_READ_URI_PERMISSION

        // Optionally, specify a URI for the directory that should be opened in
        // the system file picker when it loads.
        putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
    }

    startActivityForResult(intent, your-request-code)
}

๊ถŒํ•œ ํ—ˆ์šฉํ•œ URI ์–ป๊ธฐ

override fun onActivityResult(
        requestCode: Int, resultCode: Int, resultData: Intent?) {
    if (requestCode == your-request-code
            && resultCode == Activity.RESULT_OK) {
        // The result data contains a URI for the document or directory that
        // the user selected.
        resultData?.data?.also { uri ->
            // Perform operations on the document using its URI.
        }
    }
}

์˜๊ตฌ์ ์ธ ๊ถŒํ•œ ์–ป๊ธฐ

  • ์ผ๋ฐ˜์ ์ธ ๊ถŒํ•œ ์š”์ฒญ์€ ๊ธฐ๊ธฐ ์žฌ์‹œ์ž‘ ์ดํ›„์— ์ดˆ๊ธฐํ™”๋จ
  • ์˜๊ตฌ์ ์ธ ๊ถŒํ•œ์„ ์–ป์œผ๋ฉด ๊ธฐ๊ธฐ ์žฌ์‹œ์ž‘ ํ›„์—๋„ ๊ถŒํ•œ ์œ ์ง€
    • ์ฃผ์˜) ํ•ด๋‹น ํŒŒ์ผ/ํด๋”๊ฐ€ ์ด๋™ ๋˜๋Š” ์‚ญ์ œ๋œ ๊ฒฝ์šฐ ์œ ํšจํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ, ๊ถŒํ•œ์„ ๋‹ค์‹œ ๋ฐ›์•„์•ผ ํ•จ
val contentResolver = applicationContext.contentResolver

val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or
        Intent.FLAG_GRANT_WRITE_URI_PERMISSION
// Check for the freshest data.
contentResolver.takePersistableUriPermission(uri, takeFlags)

ํŒŒ์ผ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ

val documentsTree = DocumentFile.fromTreeUri(getApplication(), directoryUri) ?: return
val childDocuments = documentsTree.listFiles().toCachingList()

์‚ญ์ œํ•˜๊ธฐ

DocumentsContract.deleteDocument(applicationContext.contentResolver, uri)

ํŒŒ์ผ ์—ด๊ธฐ, ์ˆ˜์ • ๋“ฑ ์˜ˆ์‹œ ์ฝ”๋“œ๋Š”

๊ฐ€์ด๋“œ์—์„œ..

์ฐธ๊ณ  ๋งํฌ

Comments