web-dev-qa-db-fra.com

Comment obtenir la taille totale et gratuite de chaque StorageVolume?

Contexte

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.

Le problème

À 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:

enter image description here

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.

Ce que j'ai trouvé

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.

Questions

  1. getAllocatableBytes est-il vraiment le moyen d'obtenir l'espace libre?
  2. 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?
18
android developer

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.

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

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

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).)

enter image description here

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.

enter image description here

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.

1
Cheticamp