グーグル(悲しいことに) ストレージのアクセス許可を台無しにする計画 アプリが標準を使用してファイルシステムにアクセスできないようにするファイル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" アプリは、いかなる種類の許可も付与されずにこの情報を取得することができます。
そして、これは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 。
私がこの質問に書いた回答で私が見つけたものの多くを見つけることができますが、現在のところ、これはすべて回避策と回避策ではないものの混合であり、場合によっては機能します。
getAllocatableBytes
は実際に空き領域を取得する方法ですか?以下は、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 にあります。
次の小さなアプリは、アクセス可能なボリュームの使用済み、空き、合計バイト数を表示します。
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>
私が書いたもの 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
}
getAllocatableBytesは実際に空き領域を取得する方法ですか?
Android 8.0の機能とAPI は次のように述べていますgetAllocatableBytes(UUID):
最後に、大きなファイルにディスクスペースを割り当てる必要がある場合は、新しいallocateBytes(FileDescriptor、long)APIの使用を検討してください。これにより、他のアプリに属するキャッシュファイルが(必要に応じて)自動的にクリアされ、リクエストに対応できます。新しいデータを保持するのに十分なディスク容量がデバイスにあるかどうかを判断するときは、getUsableSpace()を使用する代わりにgetAllocatableBytes(UUID)を呼び出します。前者は、システムがユーザーに代わって消去するキャッシュデータを考慮しているためです。
したがって、getAllocatableBytes()は、他のアプリのキャッシュをクリアすることにより、新しいファイル用に何バイト解放できるかを報告しますが、現在は解放されていない場合があります。これは、汎用ファイルユーティリティの正しい呼び出しではないようです。
いずれの場合も、getAllocatableBytes(UUID)は、から受け入れ可能なUUIDを取得できないため、プライマリボリューム以外のボリュームでは機能しないようですプライマリボリューム以外のストレージボリュームのStorageManager。 Android 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を使用してください3 キロバイトの場合、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エミュレーターにロードすることができませんでした。)
次の画像は、上部にStorageVolumeStatsアプリを示し、下部に「EZ File Explorer」からの出力を示しています。ここでは、キロバイトに1,024が使用され、2つのアプリは丸めを除いて使用可能な合計空き容量に同意します。
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() へのアクセスが本当に必要なので、スペースの使用率をすぐに判断できますが、制限されています。呼び出しはボリュームリストから生成されたファイルリストに依存するため、すべてのボリュームがコードでカバーされ、必要なスペース使用率情報を取得できます。