How to get free and total size of each StorageVolume?

后端 未结 3 819
星月不相逢
星月不相逢 2020-12-14 03:06

Background

Google (sadly) plans to ruin storage permission so that apps won\'t be able to access the file system using the standard File API (and file-paths). Many

相关标签:
3条回答
  • 2020-12-14 03:26

    The following uses fstatvfs(FileDescriptor) to retrieve stats without resorting to reflection or traditional file system methods.

    To check the output of the program to make sure it is producing reasonable result for total, used and available space I ran the "df" command on an Android Emulator running API 29.

    Output of "df" command in adb shell reporting 1K blocks:

    "/data" corresponds to the "primary" UUID used when by StorageVolume#isPrimary is true.

    "/storage/1D03-2E0E" corresponds to the "1D03-2E0E" UUID reported by StorageVolume#uuid.

    generic_x86:/ $ df
    Filesystem              1K-blocks    Used Available Use% Mounted on
    /dev/root                 2203316 2140872     46060  98% /
    tmpfs                     1020140     592   1019548   1% /dev
    tmpfs                     1020140       0   1020140   0% /mnt
    tmpfs                     1020140       0   1020140   0% /apex
    /dev/block/vde1            132168   75936     53412  59% /vendor
    
    /dev/block/vdc             793488  647652    129452  84% /data
    
    /dev/block/loop0              232      36       192  16% /apex/com.android.apex.cts.shim@1
    /data/media                793488  647652    129452  84% /storage/emulated
    
    /mnt/media_rw/1D03-2E0E    522228      90    522138   1% /storage/1D03-2E0E
    

    Reported by the app using fstatvfs (in 1K blocks):

    For /tree/primary:/document/primary: Total=793,488 used space=647,652 available=129,452

    For /tree/1D03-2E0E:/document/1D03-2E0E: Total=522,228 used space=90 available=522,138

    The totals match.

    fstatvfs is described here.

    Detail on what fstatvfs returns can be found here.

    The following little app displays used, free and total bytes for volumes that are accessible.

    MainActivity.kt

    class MainActivity : AppCompatActivity() {
        private lateinit var mStorageManager: StorageManager
        private val mVolumeStats = HashMap<Uri, StructStatVfs>()
        private val mStorageVolumePathsWeHaveAccessTo = HashSet<String>()
        private lateinit var mStorageVolumes: List<StorageVolume>
        private var mHaveAccessToPrimary = false
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            mStorageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager
            mStorageVolumes = mStorageManager.storageVolumes
    
            requestAccessButton.setOnClickListener {
                val primaryVolume = mStorageManager.primaryStorageVolume
                val intent = primaryVolume.createOpenDocumentTreeIntent()
                startActivityForResult(intent, 1)
            }
    
            releaseAccessButton.setOnClickListener {
                val takeFlags =
                    Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
                val uri = buildVolumeUriFromUuid(PRIMARY_UUID)
    
                contentResolver.releasePersistableUriPermission(uri, takeFlags)
                val toast = Toast.makeText(
                    this,
                    "Primary volume permission released was released.",
                    Toast.LENGTH_SHORT
                )
                toast.setGravity(Gravity.BOTTOM, 0, releaseAccessButton.height)
                toast.show()
                getVolumeStats()
                showVolumeStats()
            }
            getVolumeStats()
            showVolumeStats()
    
        }
    
        private fun getVolumeStats() {
            val persistedUriPermissions = contentResolver.persistedUriPermissions
            mStorageVolumePathsWeHaveAccessTo.clear()
            persistedUriPermissions.forEach {
                mStorageVolumePathsWeHaveAccessTo.add(it.uri.toString())
            }
            mVolumeStats.clear()
            mHaveAccessToPrimary = false
            for (storageVolume in mStorageVolumes) {
                val uuid = if (storageVolume.isPrimary) {
                    // Primary storage doesn't get a UUID here.
                    PRIMARY_UUID
                } else {
                    storageVolume.uuid
                }
    
                val volumeUri = uuid?.let { buildVolumeUriFromUuid(it) }
    
                when {
                    uuid == null ->
                        Log.d(TAG, "UUID is null for ${storageVolume.getDescription(this)}!")
                    mStorageVolumePathsWeHaveAccessTo.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 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.nice()}")
                sb.appendln(" Free space: ${freeSpace.nice()}")
                sb.appendln("Total space: ${totalSpace.nice()}")
                sb.appendln("----------------")
            }
            volumeStats.text = sb.toString()
            if (mHaveAccessToPrimary) {
                releaseAccessButton.visibility = View.VISIBLE
                requestAccessButton.visibility = View.GONE
            } else {
                releaseAccessButton.visibility = View.GONE
                requestAccessButton.visibility = View.VISIBLE
            }
        }
    
        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)
        }
    
        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()
        }
    
        companion object {
            fun Long.nice(fieldLength: Int = 12): String = String.format(Locale.US, "%,${fieldLength}d", this)
    
            const val EXTERNAL_STORAGE_AUTHORITY = "com.android.externalstorage.documents"
            const val PRIMARY_UUID = "primary"
            const val TAG = "AppLog"
        }
    }
    

    activity_main.xml

    <LinearLayout 
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:orientation="vertical"
            tools:context=".MainActivity">
    
        <TextView
                android:id="@+id/volumeStats"
                android:layout_width="match_parent"
                android:layout_height="0dp"
                android:layout_marginBottom="16dp"
                android:layout_weight="1"
                android:fontFamily="monospace"
                android:padding="16dp" />
    
        <Button
                android:id="@+id/requestAccessButton"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:layout_marginBottom="16dp"
                android:visibility="gone"
                android:text="Request Access to Primary" />
    
        <Button
                android:id="@+id/releaseAccessButton"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="center_horizontal"
                android:layout_marginBottom="16dp"
                android:text="Release Access to Primary" />
    </LinearLayout>   
    
    0 讨论(0)
  • 2020-12-14 03:26

    Is getAllocatableBytes indeed the way to get the free space?

    Android 8.0 Features and APIs states that getAllocatableBytes(UUID):

    Finally, when you need to allocate disk space for large files, consider using the new allocateBytes(FileDescriptor, long) API, which will automatically clear cached files belonging to other apps (as needed) to meet your request. When deciding if the device has enough disk space to hold your new data, call getAllocatableBytes(UUID) instead of using getUsableSpace(), since the former will consider any cached data that the system is willing to clear on your behalf.

    So, getAllocatableBytes() reports how many bytes could be free for a new file by clearing cache for other apps but may not be currently free. This does not seem to be the right call for a general-purpose file utility.

    In any case, getAllocatableBytes(UUID) doesn't seem to work for any volume other than the primary volume due to the inability to get acceptable UUIDs from StorageManager for storage volumes other than the primary volume. See Invalid UUID of storage gained from Android StorageManager? and Bug report #62982912. (Mentioned here for completeness; I realize that you already know about these.) The bug report is now over two years old with no resolution or hint at a work-around, so no love there.

    If you want the type of free space reported by "Files by Google" or other file managers, then you will want to approach free space in a different way as explained below.

    How can I get the free and real total space (in some cases I got lower values for some reason) of each StorageVolume, without requesting any permission, just like on Google's app?

    Here is a procedure to get free and total space for available volumes:

    Identify external directories: Use getExternalFilesDirs(null) to discover available external locations. What is returned is a File[]. These are directories that our app is permitted to use.

    extDirs = {File2@9489
    0 = {File@9509} "/storage/emulated/0/Android/data/com.example.storagevolumes/files"
    1 = {File@9510} "/storage/14E4-120B/Android/data/com.example.storagevolumes/files"

    (N.B. According to the documentation, this call returns what are considered to be stable devices such as SD cards. This does not return attached USB drives.)

    Identify storage volumes: For each directory returned above, use StorageManager#getStorageVolume(File) to identify the storage volume that contains the directory. We don't need to identify the top-level directory to get the storage volume, just a file from the storage volume, so these directories will do.

    Calculate total and used space: Determine the space on the storage volumes. The primary volume is treated differently from an SD card.

    For the primary volume: Using StorageStatsManager#getTotalBytes(UUID get the nominal total bytes of storage on the primary device using StorageManager#UUID_DEFAULT . The value returned treats a kilobyte as 1,000 bytes (rather than 1,024) and a gigabyte as 1,000,000,000 bytes instead of 230. On my SamSung Galaxy S7 the value reported is 32,000,000,000 bytes. On my Pixel 3 emulator running API 29 with 16 MB of storage, the value reported is 16,000,000,000.

    Here is the trick: If you want the numbers reported by "Files by Google", use 103 for a kilobyte, 106 for a megabyte and 109 for a gigabyte. For other file managers 210, 220 and 230 is what works. (This is demonstrated below.) See this for more information on these units.

    To get free bytes, use StorageStatsManager#getFreeBytes(uuid). Used bytes is the difference between total bytes and free bytes.

    For non-primary volumes: Space calculations for non-primary volumes is straightforward: For total space used File#getTotalSpace and File#getFreeSpace for the free space.

    Here are a couple of screens shots that display volume stats. The first image shows the output of the StorageVolumeStats app (included below the images) and "Files by Google." The toggle button at the top of the top section switches the app between using 1,000 and 1,024 for kilobytes. As you can see, the figures agree. (This is a screen shot from a device running Oreo. I was unable to get the beta version of "Files by Google" loaded onto an Android Q emulator.)

    The following image shows the StorageVolumeStats app at the top and output from "EZ File Explorer" on the bottom. Here 1,024 is used for kilobytes and the two apps agree on the total and free space available except for rounding.

    MainActivity.kt

    This small app is just the main activity. The manifest is generic, compileSdkVersion and targetSdkVersion are set to 29. minSdkVersion is 26.

    class MainActivity : AppCompatActivity() {
        private lateinit var mStorageManager: StorageManager
        private val mStorageVolumesByExtDir = mutableListOf<VolumeStats>()
        private lateinit var mVolumeStats: TextView
        private lateinit var mUnitsToggle: ToggleButton
        private var mKbToggleValue = true
        private var kbToUse = KB
        private var mbToUse = MB
        private var gbToUse = GB
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            if (savedInstanceState != null) {
                mKbToggleValue = savedInstanceState.getBoolean("KbToggleValue", true)
                selectKbValue()
            }
            setContentView(statsLayout())
    
            mStorageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager
    
            getVolumeStats()
            showVolumeStats()
        }
    
        override fun onSaveInstanceState(outState: Bundle) {
            super.onSaveInstanceState(outState)
            outState.putBoolean("KbToggleValue", mKbToggleValue)
        }
    
        private fun getVolumeStats() {
            // We will get our volumes from the external files directory list. There will be one
            // entry per external volume.
            val extDirs = getExternalFilesDirs(null)
    
            mStorageVolumesByExtDir.clear()
            extDirs.forEach { file ->
                val storageVolume: StorageVolume? = mStorageManager.getStorageVolume(file)
                if (storageVolume == null) {
                    Log.d(TAG, "Could not determinate StorageVolume for ${file.path}")
                } else {
                    val totalSpace: Long
                    val usedSpace: Long
                    if (storageVolume.isPrimary) {
                        // Special processing for primary volume. "Total" should equal size advertised
                        // on retail packaging and we get that from StorageStatsManager. Total space
                        // from File will be lower than we want to show.
                        val uuid = StorageManager.UUID_DEFAULT
                        val storageStatsManager =
                            getSystemService(Context.STORAGE_STATS_SERVICE) as StorageStatsManager
                        // Total space is reported in round numbers. For example, storage on a
                        // SamSung Galaxy S7 with 32GB is reported here as 32_000_000_000. If
                        // true GB is needed, then this number needs to be adjusted. The constant
                        // "KB" also need to be changed to reflect KiB (1024).
    //                    totalSpace = storageStatsManager.getTotalBytes(uuid)
                        totalSpace = (storageStatsManager.getTotalBytes(uuid) / 1_000_000_000) * gbToUse
                        usedSpace = totalSpace - storageStatsManager.getFreeBytes(uuid)
                    } else {
                        // StorageStatsManager doesn't work for volumes other than the primary volume
                        // since the "UUID" available for non-primary volumes is not acceptable to
                        // StorageStatsManager. We must revert to File for non-primary volumes. These
                        // figures are the same as returned by statvfs().
                        totalSpace = file.totalSpace
                        usedSpace = totalSpace - file.freeSpace
                    }
                    mStorageVolumesByExtDir.add(
                        VolumeStats(storageVolume, totalSpace, usedSpace)
                    )
                }
            }
        }
    
        private fun showVolumeStats() {
            val sb = StringBuilder()
            mStorageVolumesByExtDir.forEach { volumeStats ->
                val (usedToShift, usedSizeUnits) = getShiftUnits(volumeStats.mUsedSpace)
                val usedSpace = (100f * volumeStats.mUsedSpace / usedToShift).roundToLong() / 100f
                val (totalToShift, totalSizeUnits) = getShiftUnits(volumeStats.mTotalSpace)
                val totalSpace = (100f * volumeStats.mTotalSpace / totalToShift).roundToLong() / 100f
                val uuidToDisplay: String?
                val volumeDescription =
                    if (volumeStats.mStorageVolume.isPrimary) {
                        uuidToDisplay = ""
                        PRIMARY_STORAGE_LABEL
                    } else {
                        uuidToDisplay = " (${volumeStats.mStorageVolume.uuid})"
                        volumeStats.mStorageVolume.getDescription(this)
                    }
                sb
                    .appendln("$volumeDescription$uuidToDisplay")
                    .appendln(" Used space: ${usedSpace.nice()} $usedSizeUnits")
                    .appendln("Total space: ${totalSpace.nice()} $totalSizeUnits")
                    .appendln("----------------")
            }
            mVolumeStats.text = sb.toString()
        }
    
        private fun getShiftUnits(x: Long): Pair<Long, String> {
            val usedSpaceUnits: String
            val shift =
                when {
                    x < kbToUse -> {
                        usedSpaceUnits = "Bytes"; 1L
                    }
                    x < mbToUse -> {
                        usedSpaceUnits = "KB"; kbToUse
                    }
                    x < gbToUse -> {
                        usedSpaceUnits = "MB"; mbToUse
                    }
                    else -> {
                        usedSpaceUnits = "GB"; gbToUse
                    }
                }
            return Pair(shift, usedSpaceUnits)
        }
    
        @SuppressLint("SetTextI18n")
        private fun statsLayout(): SwipeRefreshLayout {
            val swipeToRefresh = SwipeRefreshLayout(this)
            swipeToRefresh.setOnRefreshListener {
                getVolumeStats()
                showVolumeStats()
                swipeToRefresh.isRefreshing = false
            }
    
            val scrollView = ScrollView(this)
            swipeToRefresh.addView(scrollView)
            val linearLayout = LinearLayout(this)
            linearLayout.orientation = LinearLayout.VERTICAL
            scrollView.addView(
                linearLayout, ViewGroup.LayoutParams.MATCH_PARENT,
                ViewGroup.LayoutParams.WRAP_CONTENT
            )
    
            val instructions = TextView(this)
            instructions.text = "Swipe down to refresh."
            linearLayout.addView(
                instructions, ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT
            )
            (instructions.layoutParams as LinearLayout.LayoutParams).gravity = Gravity.CENTER
    
            mUnitsToggle = ToggleButton(this)
            mUnitsToggle.textOn = "KB = 1,000"
            mUnitsToggle.textOff = "KB = 1,024"
            mUnitsToggle.isChecked = mKbToggleValue
            linearLayout.addView(
                mUnitsToggle, ViewGroup.LayoutParams.WRAP_CONTENT,
                ViewGroup.LayoutParams.WRAP_CONTENT
            )
            mUnitsToggle.setOnClickListener { v ->
                val toggleButton = v as ToggleButton
                mKbToggleValue = toggleButton.isChecked
                selectKbValue()
                getVolumeStats()
                showVolumeStats()
            }
    
            mVolumeStats = TextView(this)
            mVolumeStats.typeface = Typeface.MONOSPACE
            val padding =
                16 * (resources.displayMetrics.densityDpi.toFloat() / DisplayMetrics.DENSITY_DEFAULT).toInt()
            mVolumeStats.setPadding(padding, padding, padding, padding)
    
            val lp = LinearLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 0)
            lp.weight = 1f
            linearLayout.addView(mVolumeStats, lp)
    
            return swipeToRefresh
        }
    
        private fun selectKbValue() {
            if (mKbToggleValue) {
                kbToUse = KB
                mbToUse = MB
                gbToUse = GB
            } else {
                kbToUse = KiB
                mbToUse = MiB
                gbToUse = GiB
            }
        }
    
        companion object {
            fun Float.nice(fieldLength: Int = 6): String =
                String.format(Locale.US, "%$fieldLength.2f", this)
    
            // StorageVolume should have an accessible "getPath()" method that will do
            // the following so we don't have to resort to reflection.
            @Suppress("unused")
            fun StorageVolume.getStorageVolumePath(): String {
                return try {
                    javaClass
                        .getMethod("getPath")
                        .invoke(this) as String
                } catch (e: Exception) {
                    e.printStackTrace()
                    ""
                }
            }
    
            // See https://en.wikipedia.org/wiki/Kibibyte for description
            // of these units.
    
            // These values seems to work for "Files by Google"...
            const val KB = 1_000L
            const val MB = KB * KB
            const val GB = KB * KB * KB
    
            // ... and these values seems to work for other file manager apps.
            const val KiB = 1_024L
            const val MiB = KiB * KiB
            const val GiB = KiB * KiB * KiB
    
            const val PRIMARY_STORAGE_LABEL = "Internal Storage"
    
            const val TAG = "MainActivity"
        }
    
        data class VolumeStats(
            val mStorageVolume: StorageVolume,
            var mTotalSpace: Long = 0,
            var mUsedSpace: Long = 0
        )
    }
    

    Addendum

    Let's get more comfortable with using getExternalFilesDirs():

    We call Context#getExternalFilesDirs() in the code. Within this method a call is made to Environment#buildExternalStorageAppFilesDirs() which calls Environment#getExternalDirs() to obtain the volume list from StorageManager. This storage list is used to create the paths we see returned from Context#getExternalFilesDirs() by appending some static path segments to the path identified by each storage volume.

    We would really want access to Environment#getExternalDirs() so we can immediately determine space utilization, but we are restricted. Since the call we make depends upon a file list that is generated from the volume list, we can be comfortable that all volumes are covered by out code and we can get the space utilization information we need.

    0 讨论(0)
  • 2020-12-14 03:31

    Found a workaround, by using what I wrote here , and mapping each StorageVolume with a real file as I wrote here. Sadly this might not work in the future, as it uses a lot of "tricks" :

            for (storageVolume in storageVolumes) {
                val volumePath = FileUtilEx.getVolumePath(storageVolume)
                if (volumePath == null) {
                    Log.d("AppLog", "storageVolume \"${storageVolume.getDescription(this)}\" - failed to get volumePath")
                } else {
                    val statFs = StatFs(volumePath)
                    val availableSizeInBytes = statFs.availableBytes
                    val totalBytes = statFs.totalBytes
                    val formattedResult = "availableSizeInBytes:${android.text.format.Formatter.formatShortFileSize(this, availableSizeInBytes)} totalBytes:${android.text.format.Formatter.formatShortFileSize(this, totalBytes)}"
                    Log.d("AppLog", "storageVolume \"${storageVolume.getDescription(this)}\" - volumePath:$volumePath - $formattedResult")
                }
            }
    

    Seems to work on both emulator (that has primary storage and SD-card) and real device (Pixel 2), both on Android Q beta 4.

    A bit better solution which wouldn't use reflection, could be to put a unique file in each of the paths we get on ContextCompat.getExternalCacheDirs, and then try to find them via each of the StorageVolume instances. It is tricky though because you don't know when to start the search, so you will need to check various paths till you reach the destination. Not only that, but as I wrote here, I don't think there is an official way to get the Uri or DocumentFile or File or file-path of each StorageVolume.

    Anyway, weird thing is that the total space is lower than the real one. Probably as it's a partition of what's the maximum that's really available to the user.

    I wonder how come various apps (such as file manager apps, like Total Commander) get the real total device storage.


    EDIT: OK got another workaround, which is probably more reliable, based on the storageManager.getStorageVolume(File) function.

    So here is the merging of the 2 workarounds:

    fun getStorageVolumePath(context: Context, storageVolumeToGetItsPath: StorageVolume): String? {
        //first, try to use reflection
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
            return null
        try {
            val storageVolumeClazz = StorageVolume::class.java
            val getPathMethod = storageVolumeClazz.getMethod("getPath")
            val result = getPathMethod.invoke(storageVolumeToGetItsPath) as String?
             if (!result.isNullOrBlank())
                return result
        } catch (e: Exception) {
            e.printStackTrace()
        }
        //failed to use reflection, so try mapping with app's folders
        val storageVolumeUuidStr = storageVolumeToGetItsPath.uuid
        val externalCacheDirs = ContextCompat.getExternalCacheDirs(context)
        val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
        for (externalCacheDir in externalCacheDirs) {
            val storageVolume = storageManager.getStorageVolume(externalCacheDir) ?: continue
            val uuidStr = storageVolume.uuid
            if (uuidStr == storageVolumeUuidStr) {
                //found storageVolume<->File match
                var resultFile = externalCacheDir
                while (true) {
                    val parentFile = resultFile.parentFile ?: return resultFile.absolutePath
                    val parentFileStorageVolume = storageManager.getStorageVolume(parentFile)
                            ?: return resultFile.absolutePath
                    if (parentFileStorageVolume.uuid != uuidStr)
                        return resultFile.absolutePath
                    resultFile = parentFile
                }
            }
        }
        return null
    }
    

    And to show the available and total space, we use StatFs as before:

    for (storageVolume in storageVolumes) {
        val storageVolumePath = getStorageVolumePath(this@MainActivity, storageVolume) ?: continue
        val statFs = StatFs(storageVolumePath)
        val availableSizeInBytes = statFs.availableBytes
        val totalBytes = statFs.totalBytes
        val formattedResult = "availableSizeInBytes:${android.text.format.Formatter.formatShortFileSize(this, availableSizeInBytes)} totalBytes:${android.text.format.Formatter.formatShortFileSize(this, totalBytes)}"
        Log.d("AppLog", "storageVolume \"${storageVolume.getDescription(this)}\" - storageVolumePath:$storageVolumePath - $formattedResult")
    }
    

    EDIT: shorter version, without using the real file-path of the storageVolume:

    fun getStatFsForStorageVolume(context: Context, storageVolumeToGetItsPath: StorageVolume): StatFs? {
        //first, try to use reflection
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
            return null
        try {
            val storageVolumeClazz = StorageVolume::class.java
            val getPathMethod = storageVolumeClazz.getMethod("getPath")
            val resultPath = getPathMethod.invoke(storageVolumeToGetItsPath) as String?
            if (!resultPath.isNullOrBlank())
                return StatFs(resultPath)
        } catch (e: Exception) {
            e.printStackTrace()
        }
        //failed to use reflection, so try mapping with app's folders
        val storageVolumeUuidStr = storageVolumeToGetItsPath.uuid
        val externalCacheDirs = ContextCompat.getExternalCacheDirs(context)
        val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
        for (externalCacheDir in externalCacheDirs) {
            val storageVolume = storageManager.getStorageVolume(externalCacheDir) ?: continue
            val uuidStr = storageVolume.uuid
            if (uuidStr == storageVolumeUuidStr) {
                //found storageVolume<->File match
                return StatFs(externalCacheDir.absolutePath)
            }
        }
        return null
    }
    

    Usage:

            for (storageVolume in storageVolumes) {
                val statFs = getStatFsForStorageVolume(this@MainActivity, storageVolume)
                        ?: continue
                val availableSizeInBytes = statFs.availableBytes
                val totalBytes = statFs.totalBytes
                val formattedResult = "availableSizeInBytes:${android.text.format.Formatter.formatShortFileSize(this, availableSizeInBytes)} totalBytes:${android.text.format.Formatter.formatShortFileSize(this, totalBytes)}"
                Log.d("AppLog", "storageVolume \"${storageVolume.getDescription(this)}\" - $formattedResult")
            }
    

    Note that this solution doesn't require any kind of permission.

    --

    EDIT: I actually found out that I tried to do it in the past, but for some reason it crashed for me on the SD-card StoraveVolume on the emulator:

            val storageStatsManager = getSystemService(Context.STORAGE_STATS_SERVICE) as StorageStatsManager
            for (storageVolume in storageVolumes) {
                val uuidStr = storageVolume.uuid
                val uuid = if (uuidStr == null) StorageManager.UUID_DEFAULT else UUID.fromString(uuidStr)
                val availableSizeInBytes = storageStatsManager.getFreeBytes(uuid)
                val totalBytes = storageStatsManager.getTotalBytes(uuid)
                val formattedResult = "availableSizeInBytes:${android.text.format.Formatter.formatShortFileSize(this, availableSizeInBytes)} totalBytes:${android.text.format.Formatter.formatShortFileSize(this, totalBytes)}"
                Log.d("AppLog", "storageVolume \"${storageVolume.getDescription(this)}\" - $formattedResult")
            }
    

    The good news is that for the primary storageVolume, you get the real total space of it.

    On a real device it also crashes for the SD-card, but not for the primary one.


    So here's the latest solution for this, gathering the above:

            for (storageVolume in storageVolumes) {
                val availableSizeInBytes: Long
                val totalBytes: Long
                if (storageVolume.isPrimary) {
                    val storageStatsManager = getSystemService(Context.STORAGE_STATS_SERVICE) as StorageStatsManager
                    val uuidStr = storageVolume.uuid
                    val uuid = if (uuidStr == null) StorageManager.UUID_DEFAULT else UUID.fromString(uuidStr)
                    availableSizeInBytes = storageStatsManager.getFreeBytes(uuid)
                    totalBytes = storageStatsManager.getTotalBytes(uuid)
                } else {
                    val statFs = getStatFsForStorageVolume(this@MainActivity, storageVolume)
                            ?: continue
                    availableSizeInBytes = statFs.availableBytes
                    totalBytes = statFs.totalBytes
                }
                val formattedResult = "availableSizeInBytes:${android.text.format.Formatter.formatShortFileSize(this, availableSizeInBytes)} totalBytes:${android.text.format.Formatter.formatShortFileSize(this, totalBytes)}"
                Log.d("AppLog", "storageVolume \"${storageVolume.getDescription(this)}\" - $formattedResult")
            }
    

    Updated answer for Android R:

            fun getStorageVolumesAccessState(context: Context) {
                val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
                val storageVolumes = storageManager.storageVolumes
                val storageStatsManager = context.getSystemService(Context.STORAGE_STATS_SERVICE) as StorageStatsManager
                for (storageVolume in storageVolumes) {
                    var freeSpace: Long = 0L
                    var totalSpace: Long = 0L
                    val path = getPath(context, storageVolume)
                    if (storageVolume.isPrimary) {
                        totalSpace = storageStatsManager.getTotalBytes(StorageManager.UUID_DEFAULT)
                        freeSpace = storageStatsManager.getFreeBytes(StorageManager.UUID_DEFAULT)
                    } else if (path != null) {
                        val file = File(path)
                        freeSpace = file.freeSpace
                        totalSpace = file.totalSpace
                    }
                    val usedSpace = totalSpace - freeSpace
                    val freeSpaceStr = Formatter.formatFileSize(context, freeSpace)
                    val totalSpaceStr = Formatter.formatFileSize(context, totalSpace)
                    val usedSpaceStr = Formatter.formatFileSize(context, usedSpace)
                    Log.d("AppLog", "${storageVolume.getDescription(context)} - path:$path total:$totalSpaceStr used:$usedSpaceStr free:$freeSpaceStr")
                }
            }
    
            fun getPath(context: Context, storageVolume: StorageVolume): String? {
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
                    storageVolume.directory?.absolutePath?.let { return it }
                try {
                    return storageVolume.javaClass.getMethod("getPath").invoke(storageVolume) as String
                } catch (e: Exception) {
                }
                try {
                    return (storageVolume.javaClass.getMethod("getPathFile").invoke(storageVolume) as File).absolutePath
                } catch (e: Exception) {
                }
                val extDirs = context.getExternalFilesDirs(null)
                for (extDir in extDirs) {
                    val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
                    val fileStorageVolume: StorageVolume = storageManager.getStorageVolume(extDir)
                            ?: continue
                    if (fileStorageVolume == storageVolume) {
                        var file = extDir
                        while (true) {
                            val parent = file.parentFile ?: return file.absolutePath
                            val parentStorageVolume = storageManager.getStorageVolume(parent)
                                    ?: return file.absolutePath
                            if (parentStorageVolume != storageVolume)
                                return file.absolutePath
                            file = parent
                        }
                    }
                }
                try {
                    val parcel = Parcel.obtain()
                    storageVolume.writeToParcel(parcel, 0)
                    parcel.setDataPosition(0)
                    parcel.readString()
                    return parcel.readString()
                } catch (e: Exception) {
                }
                return null
            }
    
    0 讨论(0)
提交回复
热议问题