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에 대한 접근 권한을 얻고, 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 ~'
를 누르면 해당 루트 패스에 대한 접근 권한을 부여할 수 있습니다.
만약 SDCARD에 대한 접근 권한을 부여하고 싶다면, 네비게이션에서 SDCARD를 선택하고 권한을 주면 됩니다.
'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()
}
확인
위에서 코드로 계산한 사이즈가 맞는지 궁금할 수 있습니다.
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에서 확인할 수 있습니다.
참고
Related Posts
- Android 14 - 사진/동영상 파일, 일부 접근 권한 소개
- Android - adb push, pull로 파일 복사, 다운로드
- Android 14 - 암시적 인텐트 변경사항 및 문제 해결
- Jetpack Compose - Row와 Column
- Android 13, AOSP 오픈소스 다운로드 및 빌드
- Android 13 - 세분화된 미디어 파일 권한
- Android 13에서 Notification 권한 요청, 알림 띄우기
- Android 13에서 'Access blocked: ComponentInfo' 에러 해결
- 에러 해결: android gradle plugin requires java 11 to run. you are currently using java 1.8.
- 안드로이드 - 코루틴과 Retrofit으로 비동기 통신 예제
- 안드로이드 - 코루틴으로 URL 이미지 불러오기
- Android - 진동, Vibrator, VibrationEffect 예제
- Some problems were found with the configuration of task 에러 수정
- Query method parameters should either be a type that can be converted into a database column or a List
- 우분투에서 Android 12 오픈소스 다운로드 및 빌드
- Android - ViewModel을 생성하는 방법
- Android - Transformations.map(), switchMap() 차이점
- Android - Transformations.distinctUntilChanged() 소개
- Android - TabLayout 구현 방법 (+ ViewPager2)
- Android - 휴대폰 전화번호 가져오는 방법
- Android 12 - Splash Screens 알아보기
- Android 12 - Incremental Install (Play as you Download) 소개
- Android - adb 명령어로 bugreport 로그 파일 추출
- Android - adb 명령어로 App 데이터 삭제
- Android - adb 명령어로 앱 비활성화, 활성화
- Android - adb 명령어로 특정 패키지의 PID 찾기
- Android - adb 명령어로 퍼미션 Grant 또는 Revoke
- Android - adb 명령어로 apk 설치, 삭제
- Android - adb 명령어로 특정 패키지의 프로세스 종료
- Android - adb 명령어로 screen capture 저장
- Android - adb 명령어로 System 앱 삭제, 설치
- Android - adb 명령어로 settings value 확인, 변경
- Android 12 - IntentFilter의 exported 명시적 선언
- Android - adb 명령어로 공장초기화(Factory reset)
- Android - adb logcat 명령어로 로그 출력