Google (malheureusement) prévoit de ruiner l'autorisation de stockage afin que les applications ne puissent pas accéder au système de fichiers en utilisant la norme API de fichiers (et chemins de fichiers). Beaucoup sont ( contre car cela change la façon dont les applications peuvent accéder au stockage et à bien des égards, c'est une API restreinte et limitée.
Par conséquent, nous devrons utiliser SAF (framework d'accès au stockage) entièrement sur certaines versions futures Android (on Android Q nous pouvons, au moins temporairement, utilisez un indicateur pour utiliser l'autorisation de stockage normale), si nous souhaitons traiter différents volumes de stockage et y accéder à tous les fichiers .
Par exemple, supposons que vous souhaitiez créer un gestionnaire de fichiers et afficher tous les volumes de stockage du périphérique, et montrer pour chacun d'eux le nombre total et libre d'octets. Une telle chose semble très légitime, mais comme je ne trouve pas de moyen de le faire.
À partir de l'API 24 ( ici ), nous avons enfin la possibilité de lister tous les volumes de stockage, en tant que tels:
val storageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager
val storageVolumes = storageManager.storageVolumes
Le fait est qu'il n'y a pas de fonction pour chacun des éléments de cette liste pour obtenir sa taille et son espace libre.
Cependant, d'une certaine manière, l'application "Fichiers de Google" de Google parvient à obtenir ces informations sans qu'aucune autorisation ne soit accordée:
Et cela a été testé sur Galaxy Note 8 avec Android 8. Pas même la dernière version d'Android.
Cela signifie donc qu'il devrait y avoir un moyen d'obtenir ces informations sans aucune autorisation, même sur Android 8.
Il y a quelque chose de similaire à obtenir de l'espace libre, mais je ne sais pas si c'est bien cela. Il semble cependant que tel. Voici le code pour cela:
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)}")
}
}
Cependant, je ne trouve rien de similaire pour obtenir l'espace total de chacune des instances de StorageVolume. En supposant que j'ai raison à ce sujet, je l'ai demandé ici .
Vous pouvez trouver plus de ce que j'ai trouvé dans la réponse que j'ai écrite à cette question, mais actuellement, c'est tout un mélange de solutions de contournement et de choses qui ne sont pas des solutions de contournement mais fonctionnent dans certains cas.
getAllocatableBytes
est-il vraiment le moyen d'obtenir l'espace libre?Ce qui suit utilise fstatvfs(FileDescriptor)
pour récupérer les statistiques sans recourir à la réflexion ou aux méthodes traditionnelles du système de fichiers.
Pour vérifier la sortie du programme pour vous assurer qu'il produit un résultat raisonnable pour l'espace total, utilisé et disponible, j'ai exécuté la commande "df" sur un Android émulateur exécutant l'API 29.
Sortie de la commande "df" dans adb Shell rapportant des blocs 1K:
"/ data" correspond à l'UUID "principal" utilisé lorsque StorageVolume # isPrimary est true.
"/ storage/1D03-2E0E" correspond à l'UUID "1D03-2E0E" signalé par 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
Signalé par l'application en utilisant fstatvfs (en blocs de 1K):
Pour/tree/primary:/document/primary: Total = 793 488 espace utilisé = 647 652 disponible = 129 452
Pour/tree/1D03-2E0E:/document/1D03-2E0E: Total = 522 228 espaces utilisés = 90 disponibles = 522 138
Les totaux correspondent.
fstatvfs est décrit ici .
Détails sur ce que fstatvfs les retours peuvent être trouvés ici .
La petite application suivante affiche les octets utilisés, gratuits et totaux pour les volumes accessibles.
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>
Trouvé une solution de contournement, en utilisant ce que j'ai écrit ici , et en mappant chaque StorageVolume avec un vrai fichier comme je l'ai écrit ici . Malheureusement, cela pourrait ne pas fonctionner à l'avenir, car il utilise beaucoup de "trucs":
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")
}
}
Semble fonctionner à la fois sur l'émulateur (qui dispose d'un stockage principal et d'une carte SD) et sur un appareil réel (Pixel 2), tous deux sur Android Q beta 4.
Une solution un peu meilleure qui n'utiliserait pas la réflexion, pourrait être de mettre un fichier unique dans chacun des chemins que nous empruntons ContextCompat.getExternalCacheDirs
, puis essayez de les trouver via chacune des instances StorageVolume. Il est cependant délicat car vous ne savez pas quand commencer la recherche, vous devrez donc vérifier différents chemins jusqu'à ce que vous atteigniez la destination. Non seulement cela, mais comme je l'ai écrit ici , je ne pense pas qu'il existe un moyen officiel d'obtenir l'Uri ou DocumentFile ou File ou file-path de chaque StorageVolume.
Quoi qu'il en soit, chose étrange, c'est que l'espace total est inférieur au vrai. Probablement car c'est une partition de ce qui est vraiment le maximum disponible pour l'utilisateur.
Je me demande comment diverses applications (telles que les applications de gestion de fichiers, comme Total Commander) obtiennent le véritable stockage total de l'appareil.
EDIT: OK a obtenu une autre solution de contournement, qui est probablement plus fiable, basée sur la fonction storageManager.getStorageVolume (File) .
Voici donc la fusion des 2 solutions:
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
}
Et pour montrer l'espace disponible et total, nous utilisons les StatF comme précédemment:
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: version plus courte, sans utiliser le vrai chemin de fichier du stockage Volume:
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")
}
Notez que cette solution ne nécessite aucune autorisation.
-
EDIT: J'ai en fait découvert que j'ai essayé de le faire dans le passé, mais pour une raison quelconque, il s'est écrasé pour moi sur la carte SD StoraveVolume sur l'émulateur:
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")
}
La bonne nouvelle est que pour le volume de stockage principal, vous obtenez l'espace total réel de celui-ci.
Sur un appareil réel, il se bloque également pour la carte SD, mais pas pour la carte principale.
Voici donc la dernière solution pour cela, rassemblant ce qui précède:
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")
}
Réponse mise à jour pour 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 est-il vraiment le moyen d'obtenir l'espace libre?
Fonctionnalités et API Android 8. indique que getAllocatableBytes (UUID):
Enfin, lorsque vous devez allouer de l'espace disque pour des fichiers volumineux, envisagez d'utiliser la nouvelle API allocateBytes (FileDescriptor, long), qui effacera automatiquement les fichiers mis en cache appartenant à d'autres applications (selon les besoins) pour répondre à votre demande. Lorsque vous décidez si le périphérique dispose de suffisamment d'espace disque pour contenir vos nouvelles données, appelez getAllocatableBytes (UUID) au lieu d'utiliser getUsableSpace (), car le premier considérera toutes les données mises en cache que le système est prêt à effacer en votre nom.
Ainsi, getAllocatableBytes () indique le nombre d'octets qui pourraient être libres pour un nouveau fichier en effaçant le cache pour d'autres applications mais qui ne sont peut-être pas actuellement libres. Cela ne semble pas être le bon appel à un utilitaire de fichiers à usage général.
Dans tous les cas, getAllocatableBytes (UUID) ne semble pas fonctionner pour tout volume autre que le volume principal en raison de l'impossibilité d'obtenir des UUID acceptables de StorageManager pour les volumes de stockage autres que le volume principal. Voir ID non valide de stockage obtenu à partir de Android StorageManager? et Rapport de bogue # 62982912 . (Mentionné ici pour être complet; je me rends compte que vous avez déjà savoir à ce sujet.) Le rapport de bogue a maintenant plus de deux ans, sans résolution ni allusion à une solution de contournement, donc pas d'amour là-bas.
Si vous voulez le type d'espace libre signalé par "Files by Google" ou d'autres gestionnaires de fichiers, vous voudrez aborder l'espace libre d'une manière différente, comme expliqué ci-dessous.
Comment puis-je obtenir l'espace total libre et réel (dans certains cas, j'ai des valeurs inférieures pour une raison quelconque) de chaque StorageVolume, sans demander aucune autorisation, tout comme sur l'application de Google?
Voici une procédure pour obtenir de l'espace libre et total pour les volumes disponibles:
Identifiez les répertoires externes: Utilisez getExternalFilesDirs (null) pour découvrir les emplacements externes disponibles. Ce qui est retourné est un Fichier []. Ce sont des répertoires que notre application est autorisée à utiliser.
extDirs = {Fichier 2 @ 9489
0 = {Fichier @ 9509} "/storage/emulated/0/Android/data/com.example.storagevolumes/files"
1 = {File @ 9510} "/storage/14E4-120B/Android/data/com.example.storagevolumes/files"
(N.B. Selon la documentation, cet appel renvoie ce qui est considéré comme des appareils stables tels que les cartes SD. Cela ne renvoie pas les lecteurs USB connectés.)
Identifiez les volumes de stockage: Pour chaque répertoire renvoyé ci-dessus, utilisez StorageManager # getStorageVolume (File) = pour identifier le volume de stockage qui contient le répertoire. Nous n'avons pas besoin d'identifier le répertoire de niveau supérieur pour obtenir le volume de stockage, juste un fichier du volume de stockage, donc ces répertoires feront l'affaire.
Calculez l'espace total et utilisé: Déterminez l'espace sur les volumes de stockage. Le volume principal est traité différemment d'une carte SD.
Pour le volume principal: À l'aide de StorageStatsManager # getTotalBytes (UUID obtenez le nombre total d'octets de stockage nominal sur le périphérique principal à l'aide de StorageManager # UUID_DEFAULT . La valeur renvoyée traite un kilo-octet comme 1 000 octets (au lieu de 1 024) et un gigaoctet comme 1 000 000 000 octets au lieu de 2.30. Sur mon SamSung Galaxy S7, la valeur indiquée est de 32 000 000 000 octets. Sur mon émulateur Pixel 3 exécutant l'API 29 avec 16 Mo de stockage, la valeur indiquée est 16 000 000 000.
Voici l'astuce: Si vous voulez les chiffres rapportés par "Files by Google", utilisez 103 pour un kilo-octet, 106 pour un mégaoctet et 109 pour un gigaoctet. Pour les autres gestionnaires de fichiers 2dix, 220 et 230 est ce qui fonctionne. (Ceci est démontré ci-dessous.) Voir this pour plus d'informations sur ces unités.
Pour obtenir des octets gratuits, utilisez StorageStatsManager # getFreeBytes (uuid) . Les octets utilisés sont la différence entre le total des octets et les octets libres.
Pour les volumes non primaires: Les calculs d'espace pour les volumes non primaires sont simples: pour l'espace total utilisé File # getTotalSpace et - File # getFreeSpace pour l'espace libre.
Voici quelques captures d'écran qui affichent les statistiques de volume. La première image montre la sortie de l'application StorageVolumeStats (incluse sous les images) et "Files by Google". Le bouton bascule en haut de la section supérieure fait basculer l'application entre l'utilisation de 1 000 et 1 024 pour les kilo-octets. Comme vous pouvez le voir, les chiffres sont d'accord. (Il s'agit d'une capture d'écran d'un appareil exécutant Oreo. Je n'ai pas pu obtenir la version bêta de "Files by Google" chargée sur un Android Q).)
L'image suivante montre l'application StorageVolumeStats en haut et la sortie de "EZ File Explorer" en bas. Ici, 1 024 est utilisé pour les kilo-octets et les deux applications s'accordent sur l'espace total et libre disponible, sauf pour l'arrondi.
MainActivity.kt
Cette petite application n'est que l'activité principale. Le manifeste est générique, compileSdkVersion et targetSdkVersion sont définis sur 29. minSdkVersion est 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
Mettons-nous à l'aise avec l'utilisation de getExternalFilesDirs ():
Nous appelons Context # getExternalFilesDirs () dans le code. Dans cette méthode, un appel est fait à Environment # buildExternalStorageAppFilesDirs () qui appelle Environment # getExternalDirs () pour obtenir la liste des volumes à partir de - StorageManager . Cette liste de stockage est utilisée pour créer les chemins que nous voyons renvoyés par Context # getExternalFilesDirs () en ajoutant des segments de chemin statiques au chemin identifié par chaque volume de stockage.
Nous voudrions vraiment avoir accès à Environment # getExternalDirs () afin que nous puissions déterminer immédiatement l'utilisation de l'espace, mais nous sommes limités. Étant donné que l'appel que nous faisons dépend d'une liste de fichiers générée à partir de la liste des volumes, nous pouvons être sûrs que tous les volumes sont couverts par du code et nous pouvons obtenir les informations d'utilisation de l'espace dont nous avons besoin.