HOME > android > examples

안드로이드 Q - 저장소(Storage)의 크기 및 여유 공간 가져오기

By JS|21 Jul 2019

fstatvfs(FileDescriptor)는 인자로 전달된 FD의 파일시스템 정보를 리턴합니다. fstatvfs를 이용하면 파일 또는 폴더의 크기를 계산할 수 있습니다. 하지만 저장소의 사이즈를 계산하기 전에 가장 먼저 해야할 일은 파일에 대한 접근 권한을 얻는 것 입니다.

안드로이드 Q에서 새로운 외부 저장소 정책 Scoped Storage이 적용되기 때문에 Q 이전과는 다르게 접근권한을 얻어야 합니다. Q이전에는 READ_EXTERNAL_STORAGE 권한만 얻으면 Primary 저장소에 대한 모든 접근 권한을 얻을 수 있었지만, Q에서는 정책이 변경되어 앱이 접근하려는 폴더마다 SAF(Storage Access Framework)를 통해서 사용자에게 권한을 받아야 합니다. 만약 디바이스 Storage의 크기와 SDCARD의 크기를 알고 싶다면 이 두 저장소의 Root path에 대한 접근 권한을 얻어야 합니다.

fstatvfs(FileDescriptor)로 디바이스의 Primary 저장소와 SDCARD에 대한 파일시스템 정보를 확인하면 다음과 같이 출력이 됩니다. storage size

이 글에서는 Storage에 대한 접근 권한을 얻고, fstatvfs(FileDescriptor)로 Storage의 전체 크기 및 여유공간을 계산하는 방법에 대해서 알아보겠습니다.

샘플 코드는 코틀린으로 작성되었으며 GitHub: Storage Size에서 확인할 수 있습니다.

SAF로 접근 권한 받기

Q이전에는 READ_EXTERNAL_STORAGE 권한만 얻으면 Primary 저장소에 대한 모든 접근 권한을 얻을 수 있었지만, Q에서는 정책이 변경되어 앱이 접근하려는 폴더마다 SAF(Storage Access Framework)를 통해서 사용자에게 권한을 받아야 합니다.

아래 코드는 SAF의 액티비티를 띄우는 코드입니다. 사용자는 앱이 어떤 폴더에 접근할 수 있는지 권한을 부여할 수 있습니다.

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    mStorageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager
    mStorageVolumes = mStorageManager.storageVolumes

    val primaryVolume = mStorageManager.primaryStorageVolume
    val intent = primaryVolume.createOpenDocumentTreeIntent()
    startActivityForResult(intent, 1)
}

SAF의 UI를 띄우면 아래와 같이 보입니다. 화면에 보이는 'Allow Access to ~'를 누르면 해당 루트 패스에 대한 접근 권한을 부여할 수 있습니다. Storage Access Framework

만약 SDCARD에 대한 접근 권한을 부여하고 싶다면, 네비게이션에서 SDCARD를 선택하고 권한을 주면 됩니다. Storage Access Framework

'Allow Access to ~' 버튼을 누르면 자신의 앱으로 돌아오고 onActivityResult()으로 결과를 받습니다.

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
    super.onActivityResult(requestCode, resultCode, data)
    Log.d(TAG, "resultCode:$resultCode")
    val uri = data?.data ?: return
    val takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION or
            Intent.FLAG_GRANT_WRITE_URI_PERMISSION
    contentResolver.takePersistableUriPermission(uri, takeFlags)
    Log.d(TAG, "granted uri: ${uri.path}")
    getVolumeStats()
    showVolumeStats()
}

만약 단말이 reboot되면 SAF를 통해 권한을 다시 받아야 하는데요. takePersistableUriPermission()을 호출해주면 앱이 권한을 계속 유지할 수 있도록 합니다.

Storage 사이즈 계산

위에서 권한을 받았으면 fstatvfs(FileDescriptor) 으로 해당 패스에 대한 파일시스템 정보를 가져와야 합니다.

저희는 Primary 저장소와 SDCARD 저장소의 크기를 계산하려고 하는데요. 먼저 저장소의 루트패스에 대한 FD를 구해야 합니다.

아래 코드는 StorageVolume에서 UUID를 얻고, 그 UUID로 FD를 가져오는 코드입니다. 그리고 fstatvfs()으로 파일시스템 정보가 있는 StructStatVfs 객체를 얻었습니다.

private fun getVolumeStats() {
    val persistedUriPermissions = contentResolver.persistedUriPermissions
    mStorageVolumePaths.clear()
    persistedUriPermissions.forEach {
        mStorageVolumePaths.add(it.uri.toString())
    }
    mVolumeStats.clear()
    mHaveAccessToPrimary = false
    for (storageVolume in mStorageVolumes) {
        val uuid: String = if (storageVolume.isPrimary) {
            PRIMARY_UUID
        } else {
            storageVolume.uuid!!
        }
        val volumeUri = buildVolumeUriFromUuid(uuid)
        if (mStorageVolumePaths.contains(volumeUri.toString())) {
            Log.d(TAG, "Have access to $uuid")
            if (uuid == PRIMARY_UUID) {
                mHaveAccessToPrimary = true
            }
            val uri = buildVolumeUriFromUuid(uuid)
            val docTreeUri = DocumentsContract.buildDocumentUriUsingTree(
                uri,
                DocumentsContract.getTreeDocumentId(uri)
            )
            mVolumeStats[docTreeUri] = getFileStats(docTreeUri)
        } else {
            Log.d(TAG, "Don't have access to $uuid")
        }
    }
}

private fun buildVolumeUriFromUuid(uuid: String): Uri {
    return DocumentsContract.buildTreeDocumentUri(
        EXTERNAL_STORAGE_AUTHORITY,
        "$uuid:"
    )
}

private fun getFileStats(docTreeUri: Uri): StructStatVfs {
    val pfd = contentResolver.openFileDescriptor(docTreeUri, "r")!!
    return fstatvfs(pfd.fileDescriptor)
}

private fun Long.sizeFormat(fieldLength: Int = 12): String {
    return String.format(Locale.US, "%,${fieldLength}d", this)
}

결과 출력

위에서 저장소의 파일시스템 정보인 StructStatVfs 객체를 얻었습니다. 아래 코드는 StructStatVfs의 전체 크기와 여유 공간을 Kilobyte 단위로 변경하여 출력하였습니다.

private fun showVolumeStats() {
    val sb = StringBuilder()
    if (mVolumeStats.size == 0) {
        sb.appendln("Nothing to see here...")
    } else {
        sb.appendln("All figures are in 1K blocks.")
        sb.appendln()
    }
    mVolumeStats.forEach {
        val lastSeg = it.key.lastPathSegment
        sb.appendln("Volume: $lastSeg")
        val stats = it.value
        val blockSize = stats.f_bsize
        val totalSpace = stats.f_blocks * blockSize / 1024L
        val freeSpace = stats.f_bfree * blockSize / 1024L
        val usedSpace = totalSpace - freeSpace
        sb.appendln(" Used space: ${usedSpace.sizeFormat()}")
        sb.appendln(" Free space: ${freeSpace.sizeFormat()}")
        sb.appendln("Total space: ${totalSpace.sizeFormat()}")
        sb.appendln("----------------")
    }
    Log.d(TAG, sb.toString())
    result.text = sb.toString()
}

출력 결과는 다음과 같습니다. Storage Access Framework

확인

위에서 코드로 계산한 사이즈가 맞는지 궁금할 수 있습니다. adb shell에서 df 명령어를 사용하면 storage의 크기를 알 수 있습니다.

아래는 df로 Storage의 사이즈를 구한 결과입니다.

generic_x86:/ $ df
Filesystem              1K-blocks    Used Available Use% Mounted on
/dev/block/dm-2           2052960 1987212     49364  98% /
tmpfs                     1020232     592   1019640   1% /dev
tmpfs                     1020232       0   1020232   0% /mnt
tmpfs                     1020232       0   1020232   0% /apex
/dev/block/dm-1             74136   73904         0 100% /vendor
/dev/block/vdc             793488  138408    622312  19% /data
/dev/block/loop0              232      36       192  16% /apex/com.android.apex.cts.shim@1
/data/media                793488  138408    622312  19% /storage/emulated
/mnt/media_rw/14E7-1B13    522228      58    522170   1% /storage/14E7-1B13

여기서 Primary의 마운트 위치는 /storage/emulated이고, SDCARD는 /storage/14E7-1B13입니다. 코드로 구한 결과와 비교해보면, 계산 방법의 차이 때문인지 작은 단위가 약간 다르지만 전체적으로 비슷합니다.

정리

안드로이드 Q에서 저장소 정책이 변경되어 SAF를 통해 사용자로부터 권한을 받았습니다. 그리고 fstatvfs(FileDescriptor)를 통해서 해당 패스의 파일시스템 정보를 가져와 전체 사이즈와 여유 공간을 계산하였습니다.

이 글에서 사용한 샘플 코드는 GitHub: Storage Size에서 확인할 수 있습니다.

참고