web-dev-qa-db-ja.com

各StorageVolumeの合計サイズを無料で取得するにはどうすればよいですか?

バックグラウンド

グーグル(悲しいことに) ストレージのアクセス許可を台無しにする計画 アプリが標準を使用してファイルシステムにアクセスできないようにするファイルAPI(およびファイルパス)。多くは against it であり、アプリがストレージにアクセスする方法を変更し、多くの点で制限された制限付きのAPIです。

その結果、将来は完全にSAF(ストレージアクセスフレームワーク)を使用する必要がありますAndroidバージョン(Android Qでは、少なくとも一時的に、 フラグを使用してください 通常のストレージ権限を使用します)、さまざまなストレージボリュームを処理し、そこにあるすべてのファイルに到達する場合。

たとえば、ファイルマネージャーを作成して、デバイスのすべてのストレージボリュームを表示し、それぞれに合計および空きバイト数を表示するとします。そのようなことは非常に正当なことのようですが、私がそのようなことをする方法を見つけることができないので。

問題

API 24( here )以降、ようやくすべてのストレージボリュームを一覧表示できるようになりました。

    val storageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager
    val storageVolumes = storageManager.storageVolumes

ことは、このリストの各アイテムがそのサイズと空き領域を取得する機能はないということです。

ただし、どういうわけか、Googleの "Files by Google" アプリは、いかなる種類の許可も付与されずにこの情報を取得することができます。

enter image description here

そして、これはGalaxy Note 8でAndroid 8。

つまり、Android 8.であっても、許可なくこの情報を取得する方法があるはずです。

私が見つけたもの

空き領域を取得するのと同じようなものがありますが、それが本当にそれであるかどうかはわかりません。しかし、それはそのようです。そのコードは次のとおりです。

    val storageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager
    val storageVolumes = storageManager.storageVolumes
    AsyncTask.execute {
        for (storageVolume in storageVolumes) {
            val uuid: UUID = storageVolume.uuid?.let { UUID.fromString(it) } ?: StorageManager.UUID_DEFAULT
            val allocatableBytes = storageManager.getAllocatableBytes(uuid)
            Log.d("AppLog", "allocatableBytes:${Android.text.format.Formatter.formatShortFileSize(this,allocatableBytes)}")
        }
    }

ただし、各StorageVolumeインスタンスの合計スペースを取得するための同様のものが見つかりません。これが正しいと仮定して、私はそれを要求しました here

私がこの質問に書いた回答で私が見つけたものの多くを見つけることができますが、現在のところ、これはすべて回避策と回避策ではないものの混合であり、場合によっては機能します。

質問

  1. getAllocatableBytesは実際に空き領域を取得する方法ですか?
  2. Googleのアプリのように、許可を要求せずに、各StorageVolumeの無料の実際の合計スペース(場合によっては、何らかの理由で値が低くなった)を取得するにはどうすればよいですか?
18

以下は、fstatvfs(FileDescriptor)を使用して、リフレクションまたは従来のファイルシステムメソッドに頼ることなく統計を取得します。

プログラムの出力をチェックして、合計、使用済み、および使用可能なスペースの妥当な結果を生成していることを確認するには、Android Emulator running API 29)で「df」コマンドを実行しました。

1Kブロックを報告するadb Shellの「df」コマンドの出力:

「/ data」は、StorageVolume#isPrimaryがtrueの場合に使用される「プライマリ」UUIDに対応します。

「/ storage/1D03-2E0E」は、StorageVolume#uuidによって報告される「1D03-2E0E」の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

fstatvfsを使用してアプリによって報告されます(1Kブロック単位):

/ tree/primary:/ document/primaryの場合:合計= 793,488使用済みスペース= 647,652使用可能= 129,452

/ tree/1D03-2E0Eの場合:/ document/1D03-2E0E:合計= 522,228使用スペース= 90利用可能= 522,138

合計が一致します。

fstatvfsについて説明します ここ

fstatvfsが返すものの詳細は here にあります。

次の小さなアプリは、アクセス可能なボリュームの使用済み、空き、合計バイト数を表示します。

enter image description here

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>   
5
Cheticamp

私が書いたもの here を使用して回避策を見つけ、私が書いたように各StorageVolumeを実際のファイルにマッピングします ここ 。悲しいことに、これは多くの「トリック」を使用するため、将来は機能しない可能性があります。

        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")
            }
        }

エミュレーター(プライマリストレージとSDカードを搭載)と実際のデバイス(Pixel 2)の両方で機能しているようです。どちらもAndroid Q beta 4)で動作します。

リフレクションを使用しないもう少し良い解決策は、取得する各パスに一意のファイルを配置することですContextCompat.getExternalCacheDirsをクリックし、各StorageVolumeインスタンスを介してそれらを検索してみます。ただし、検索を開始するタイミングがわからないため、目的地に到達するまでにさまざまなパスを確認する必要があります。それだけでなく、私が here を書いたように、Uriを取得する公式の方法はないと思います各StorageVolumeのDocumentFileまたはFileまたはfile-path。

とにかく、奇妙なことに、総スペースが実際のスペースよりも少ないのです。おそらくそれは、ユーザーが実際に利用できる最大のパーティションです。

さまざまなアプリ(Total Commanderなどのファイルマネージャーアプリなど)が実際の合計デバイスストレージを取得するのはなぜですか。


編集:OKは storageManager.getStorageVolume(File) 関数に基づいて、おそらくより信頼できる別の回避策を得ました。

そこで、2つの回避策をマージします。

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
}

また、使用可能な合計スペースを示すために、以前と同じようにStatFを使用します。

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")
}

編集: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
}

使用法:

        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")
        }

このソリューションは、いかなる種類の許可も必要としないことに注意してください。

-

編集:私は実際に過去にそれをやろうとしたことがわかりましたが、何らかの理由でエミュレータのSDカードStoraveVolumeでクラッシュしました:

        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")
        }

良いニュースは、プライマリstorageVolumeの場合、実際の合計容量が得られることです。

実際のデバイスでは、SDカードでもクラッシュしますが、プライマリカードではクラッシュしません。


したがって、これを解決するための最新のソリューションを以下に示します。

        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")
        }

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
        }
1

getAllocatableBytesは実際に空き領域を取得する方法ですか?

Android 8.0の機能とAPI は次のように述べていますgetAllocatableBytes(UUID)

最後に、大きなファイルにディスクスペースを割り当てる必要がある場合は、新しいallocateBytes(FileDescriptor、long)APIの使用を検討してください。これにより、他のアプリに属する​​キャッシュファイルが(必要に応じて)自動的にクリアされ、リクエストに対応できます。新しいデータを保持するのに十分なディスク容量がデバイスにあるかどうかを判断するときは、getUsableSpace()を使用する代わりにgetAllocatableBytes(UUID)を呼び出します。前者は、システムがユーザーに代わって消去するキャッシュデータを考慮しているためです。

したがって、getAllocatableBytes()は、他のアプリのキャッシュをクリアすることにより、新しいファイル用に何バイト解放できるかを報告しますが、現在は解放されていない場合があります。これは、汎用ファイルユーティリティの正しい呼び出しではないようです。

いずれの場合も、getAllocatableBytes(UUID)は、から受け入れ可能なUUIDを取得できないため、プライマリボリューム以外のボリュームでは機能しないようですプライマリボリューム以外のストレージボリュームのStorageManagerAndroid StorageManager? から取得したストレージの無効なUUID)および バグレポート#62982912 を参照してください。これらについて知っておいてください。)バグレポートは現在2年以上前であり、解決策や回避策のヒントがないため、そこに愛はありません。

「Files by Google」またはその他のファイルマネージャーによって報告される種類の空き領域が必要な場合は、以下で説明するように、別の方法で空き領域にアプローチする必要があります。

Googleのアプリのように、許可を要求せずに、各StorageVolumeの無料の実際の合計スペース(場合によっては、何らかの理由で値が低くなった)を取得するにはどうすればよいですか?

次に、使用可能なボリュームの空き容量と合計容量を取得する手順を示します。

外部ディレクトリを特定:getExternalFilesDirs(null) を使用して、利用可能な外部ディレクトリを検出します場所。返されるのはFile []です。これらは、アプリが使用を許可されているディレクトリです。

extDirs = {ファイル 2 @ 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"

(ドキュメントによると、この呼び出しは、SDカードなどの安定したデバイスと見なされるものを返します。接続されたUSBドライブは返しません。)

ストレージボリュームを特定します:上記で返されたディレクトリごとに、 StorageManager#getStorageVolume(File ) は、ディレクトリを含むストレージボリュームを識別します。ストレージボリュームを取得するために最上位のディレクトリを特定する必要はありません。ストレージボリュームからのファイルのみなので、これらのディレクトリで実行できます。

合計容量と使用容量を計算します:ストレージボリュームの容量を決定します。プライマリボリュームは、SDカードとは異なる方法で処理されます。

プライマリボリュームの場合:StorageStatsManager#getTotalBytes(UUID を使用して、プライマリデバイス上のストレージの公称総バイト数を StorageManager#UUID_DEFAULT)を使用して取得します 。返される値は、キロバイトを1,000バイト(1,024ではなく)として扱い、ギガバイトを2ではなく1,000,000,000バイトとして扱います。30。私のSamSung Galaxy S7で報告される値は32,000,000,000バイトです。 16 MBのストレージでAPI 29を実行している私のPixel 3エミュレーターでは、報告される値は16,000,000,000です。

これがコツです:「Files by Google」によって報告された数値が必要な場合は、10を使用してください キロバイトの場合、106 メガバイトと109 ギガバイト。他のファイルマネージャの場合210、220 と230 うまくいくものです。 (これは以下に示されています。)これらの単位の詳細については this を参照してください。

空きバイトを取得するには、 StorageStatsManager#getFreeBytes(uuid) を使用します。使用バイト数は、合計バイト数と空きバイト数の差です。

非プライマリボリュームの場合:非プライマリボリュームのスペース計算は簡単です:使用される合計スペースの場合 File#getTotalSpace および- File#getFreeSpace 空き容量。

音量の統計情報を表示するスクリーンショットをいくつか示します。最初の画像は、StorageVolumeStatsアプリ(画像の下に含まれています)および「Files by Google」の出力を示しています。上部セクションの上部にあるトグルボタンで、キロバイトの使用を1,000と1,024の間で切り替えます。ご覧のとおり、数値は一致しています。 (これはOreoを実行しているデバイスからのスクリーンショットです。「Files by Google」のベータ版をAndroid Qエミュレーターにロードすることができませんでした。)

enter image description here

次の画像は、上部にStorageVolumeStatsアプリを示し、下部に「EZ File Explorer」からの出力を示しています。ここでは、キロバイトに1,024が使用され、2つのアプリは丸めを除いて使用可能な合計空き容量に同意します。

enter image description here

MainActivity.kt

この小さなアプリは主なアクティビティにすぎません。マニフェストは汎用的であり、compileSdkVersionおよびtargetSdkVersionは29に設定されます。minSdkVersion 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
    )
}

補遺

getExternalFilesDirs()を使用してより快適にしましょう:

コードでは Context#getExternalFilesDirs() を呼び出します。このメソッド内で Environment#buildExternalStorageAppFilesDirs() が呼び出され、 Environment#getExternalDirs() が呼び出され、StorageManager。このストレージリストは、Context#getExternalFilesDirs()から返されるパスを作成するために使用され、各ストレージボリュームによって識別されるパスにいくつかの静的パスセグメントを追加します。

Environment#getExternalDirs() へのアクセスが本当に必要なので、スペースの使用率をすぐに判断できますが、制限されています。呼び出しはボリュームリストから生成されたファイルリストに依存するため、すべてのボリュームがコードでカバーされ、必要なスペース使用率情報を取得できます。

1
Cheticamp