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.
En conséquence, 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 de l'appareil, pour montrer à quoi l'utilisateur peut accorder l'accès, et si vous avez déjà accès à chacun, il vous suffit de l'entrer. 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
Et, pour la toute première fois, nous pouvons avoir l'intention de demander l'accès à un volume de stockage ( ici ). Donc, si nous voulons, par exemple, demander à l'utilisateur d'autoriser l'accès au principal (qui commencera à partir de là, en fait, et ne demandera vraiment rien), nous pourrions utiliser ceci:
startActivityForResult(storageManager.primaryStorageVolume.createOpenDocumentTreeIntent(), REQUEST_CODE__DIRECTORTY_PERMISSION)
Au lieu de startActivityForResult(Intent(Intent.ACTION_OPEN_DOCUMENT_TREE), REQUEST_CODE__DIRECTORTY_PERMISSION)
, et en espérant que l'utilisateur choisira la bonne chose là-bas.
Et pour enfin avoir accès à ce que l'utilisateur a choisi, nous avons ceci:
@TargetApi(Build.VERSION_CODES.KitKat)
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_CODE__DIRECTORTY_PERMISSION && resultCode == Activity.RESULT_OK && data != null) {
val treeUri = data.data ?: return
contentResolver.takePersistableUriPermission(treeUri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION)
val pickedDir = DocumentFile.fromTreeUri(this, treeUri)
...
Jusqu'à présent, nous pouvons demander une autorisation sur les différents volumes de stockage ...
Cependant, le problème se pose si vous voulez savoir à qui vous avez obtenu la permission et ce que vous n'avez pas.
Il y a une vidéo sur "Scoped Directory Access" par Google ( ici ), dont ils parlent spécifiquement de la classe StorageVolume. Ils donnent même des informations sur l'écoute des événements de montage de StorageVolume, mais ils ne disent rien sur l'identification de ceux auxquels nous avons eu accès.
Le seul ID de la classe StorageVolume est uuid , mais il n'est même pas garanti de retourner quoi que ce soit. Et en effet il retourne null dans divers cas. Par exemple le cas du stockage principal.
Lorsque j'utilise la fonction createOpenDocumentTreeIntent
, j'ai remarqué qu'il y a un Uri caché à l'intérieur, indiquant probablement par où commencer. C'est à l'intérieur des extras, dans une clé appelée "Android.provider.extra.INITIAL_URI". Lors de la vérification de sa valeur sur le stockage principal, par exemple, j'ai obtenu ceci:
contenu: //com.Android.externalstorage.documents/root/primary
Quand je regarde l'URI que j'obtiens en retour dans onActivityResult, j'obtiens quelque chose d'un peu similaire à # 2, mais différent pour la variable treeUri
que j'ai montrée:
contenu: //com.Android.externalstorage.documents/tree/primary%3A
Pour obtenir la liste de ce à quoi vous avez accès jusqu'à présent, vous pouvez utiliser this :
val persistedUriPermissions = contentResolver.persistedUriPermissions
Cela vous renvoie une liste de UriPermission , chacun a un Uri. Malheureusement, quand je l'utilise, j'obtiens la même chose que sur # 3, que je ne peux pas vraiment comparer à ce que je reçois de StorageVolume:
content://com.Android.externalstorage.documents/tree/primary%3A
Donc, comme vous pouvez le voir, je ne trouve aucun type de mappage entre la liste des volumes de stockage et ce que l'utilisateur accorde.
Je ne peux même pas savoir si l'utilisateur a choisi un volume de stockage, car la fonction de createOpenDocumentTreeIntent
envoie uniquement l'utilisateur au StorageVolume, mais il est toujours possible de sélectionner un dossier à la place.
La seule chose que j'ai, c'est un ensemble de fonctions de contournement que j'ai trouvées sur d'autres questions ici, et je ne pense pas qu'elles soient fiables, surtout maintenant que nous n'avons pas vraiment accès à l'API de fichier et au chemin de fichier .
Je les ai écrites ici, au cas où vous pensez qu'elles sont utiles:
@TargetApi(VERSION_CODES.Lollipop)
private static String getVolumeIdFromTreeUri(final Uri treeUri) {
final String docId = DocumentsContract.getTreeDocumentId(treeUri);
final int end = docId.indexOf(':');
String result = end == -1 ? null : docId.substring(0, end);
return result;
}
private static String getDocumentPathFromTreeUri(final Uri treeUri) {
final String docId = DocumentsContract.getTreeDocumentId(treeUri);
//TODO avoid using spliting of a string (because it uses extra strings creation)
final String[] split = docId.split(":");
if ((split.length >= 2) && (split[1] != null))
return split[1];
else
return File.separator;
}
public static String getFullPathOfDocumentFile(Context context, DocumentFile documentFile) {
String volumePath = getVolumePath(context, getVolumeIdFromTreeUri(documentFile.getUri()));
if (volumePath == null)
return null;
DocumentFile parent = documentFile.getParentFile();
if (parent == null)
return volumePath;
final LinkedList<String> fileHierarchy = new LinkedList<>();
while (true) {
fileHierarchy.add(0, documentFile.getName());
documentFile = parent;
parent = documentFile.getParentFile();
if (parent == null)
break;
}
final StringBuilder sb = new StringBuilder(volumePath).append(File.separator);
for (String fileName : fileHierarchy)
sb.append(fileName).append(File.separator);
return sb.toString();
}
/**
* Get the full path of a document from its tree URI.
*
* @param treeUri The tree RI.
* @return The path (without trailing file separator).
*/
public static String getFullPathFromTreeUri(Context context, final Uri treeUri) {
if (treeUri == null)
return null;
String volumePath = getVolumePath(context, getVolumeIdFromTreeUri(treeUri));
if (volumePath == null)
return File.separator;
if (volumePath.endsWith(File.separator))
volumePath = volumePath.substring(0, volumePath.length() - 1);
String documentPath = getDocumentPathFromTreeUri(treeUri);
if (documentPath.endsWith(File.separator))
documentPath = documentPath.substring(0, documentPath.length() - 1);
if (documentPath.length() > 0)
if (documentPath.startsWith(File.separator))
return volumePath + documentPath;
else return volumePath + File.separator + documentPath;
return volumePath;
}
/**
* Get the path of a certain volume.
*
* @param volumeId The volume id.
* @return The path.
*/
private static String getVolumePath(Context context, final String volumeId) {
if (VERSION.SDK_INT < VERSION_CODES.Lollipop)
return null;
try {
final StorageManager storageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
if (VERSION.SDK_INT >= VERSION_CODES.N) {
final Class<?> storageVolumeClazz = StorageVolume.class;
final Method getPath = storageVolumeClazz.getMethod("getPath");
final List<StorageVolume> storageVolumes = storageManager.getStorageVolumes();
for (final StorageVolume storageVolume : storageVolumes) {
final String uuid = storageVolume.getUuid();
final boolean primary = storageVolume.isPrimary();
// primary volume?
if (primary && PRIMARY_VOLUME_NAME.equals(volumeId)) {
return (String) getPath.invoke(storageVolume);
}
// other volumes?
if (uuid != null && uuid.equals(volumeId))
return (String) getPath.invoke(storageVolume);
}
return null;
}
final Class<?> storageVolumeClazz = Class.forName("Android.os.storage.StorageVolume");
final Method getVolumeList = storageManager.getClass().getMethod("getVolumeList");
final Method getUuid = storageVolumeClazz.getMethod("getUuid");
//noinspection JavaReflectionMemberAccess
final Method getPath = storageVolumeClazz.getMethod("getPath");
final Method isPrimary = storageVolumeClazz.getMethod("isPrimary");
final Object result = getVolumeList.invoke(storageManager);
final int length = Array.getLength(result);
for (int i = 0; i < length; i++) {
final Object storageVolumeElement = Array.get(result, i);
final String uuid = (String) getUuid.invoke(storageVolumeElement);
final Boolean primary = (Boolean) isPrimary.invoke(storageVolumeElement);
// primary volume?
if (primary && PRIMARY_VOLUME_NAME.equals(volumeId)) {
return (String) getPath.invoke(storageVolumeElement);
}
// other volumes?
if (uuid != null && uuid.equals(volumeId))
return (String) getPath.invoke(storageVolumeElement);
}
// not found.
return null;
} catch (Exception ex) {
return null;
}
}
Comment puis-je mapper entre la liste de StorageVolume et la liste des UriPermission accordées?
En d'autres termes, étant donné une liste de StorageVolume, comment puis-je savoir à quoi j'ai accès et auquel je n'ai pas accès, et si j'y ai accès, pour l'ouvrir et voir ce qu'il y a à l'intérieur?
Voici une autre façon d'obtenir ce que vous voulez. Il s'agit d'une solution de contournement comme celle que vous avez publiée sans utiliser de chemin de réflexion ou de fichier.
Sur un émulateur, je vois les éléments suivants pour lesquels j'ai autorisé l'accès.
contenu du tableau persistedUriPermissions (valeur de l'URI uniquement):
0 uri = content: //com.Android.externalstorage.documents/tree/primary%3A
1 uri = content: //com.Android.externalstorage.documents/tree/1D03-2E0E%3ADownload
2 uri = content: //com.Android.externalstorage.documents/tree/1D03-2E0E%3A
3 uri = content: //com.Android.externalstorage.documents/tree/primary%3ADCIM
4 uri = content: //com.Android.externalstorage.documents/tree/primary%3AAlarms
"% 3A" est un deux-points (":"). Il apparaît donc que l'URI est construit comme suit pour un volume où "<volume>" est l'UUID du volume.
uri = "content: //com.Android.externalstorage.documents/tree/ <volume>:"
Si l'uri est un répertoire directement sous un volume, alors la structure est:
uri = "content: //com.Android.externalstorage.documents/tree/ <volume>: <directory>"
Pour les répertoires plus profonds de la structure, le format est le suivant:
uri = "content: //com.Android.externalstorage.documents/tree/ <volume>: <directory>/<directory>/<directory> ..."
Il s'agit donc simplement d'extraire des volumes à partir d'URI dans ces formats. Le volume extrait peut être utilisé comme clé pour StorageManager.storageVolumes
. Le code suivant fait exactement cela.
Il me semble qu'il devrait y avoir un moyen plus simple de procéder. Il doit y avoir une liaison manquante dans l'API entre les volumes de stockage et les URI. Je ne peux pas dire que cette technique couvre toutes les circonstances.
Je remets également en question l'UUID renvoyé par storageVolume.uuid
qui semble être une valeur 32 bits. Je pensais que les UUID avaient une longueur de 128 bits. Est-ce un format alternatif pour un UUID ou dérivé d'une manière ou d'une autre de l'UUID? Intéressant, et il est sur le point de tomber! :(
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val storageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager
var storageVolumes = storageManager.storageVolumes
val storageVolumePathsWeHaveAccessTo = HashSet<String>()
checkAccessButton.setOnClickListener {
checkAccessToStorageVolumes()
}
requestAccessButton.setOnClickListener {
storageVolumes = storageManager.storageVolumes
val primaryVolume = storageManager.primaryStorageVolume
val intent = primaryVolume.createOpenDocumentTreeIntent()
startActivityForResult(intent, 1)
}
}
private fun checkAccessToStorageVolumes() {
val storageVolumePathsWeHaveAccessTo = HashSet<String>()
val persistedUriPermissions = contentResolver.persistedUriPermissions
persistedUriPermissions.forEach {
storageVolumePathsWeHaveAccessTo.add(it.uri.toString())
}
val storageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager
val storageVolumes = storageManager.storageVolumes
for (storageVolume in storageVolumes) {
val uuid = if (storageVolume.isPrimary) {
// Primary storage doesn't get a UUID here.
"primary"
} else {
storageVolume.uuid
}
val volumeUri = uuid?.let { buildVolumeUriFromUuid(it) }
when {
uuid == null ->
Log.d("AppLog", "UUID is null for ${storageVolume.getDescription(this)}!")
storageVolumePathsWeHaveAccessTo.contains(volumeUri) ->
Log.d("AppLog", "Have access to $uuid")
else -> Log.d("AppLog", "Don't have access to $uuid")
}
}
}
private fun buildVolumeUriFromUuid(uuid: String): String {
return DocumentsContract.buildTreeDocumentUri(
"com.Android.externalstorage.documents",
"$uuid:"
).toString()
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
Log.d("AppLog", "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("AppLog", "granted uri: ${uri.path}")
}
}
EDIT: Trouvé une solution de contournement, mais cela pourrait ne pas fonctionner un jour.
Il utilise la réflexion pour obtenir le chemin réel de l'instance StorageVolume, et il utilise ce que j'avais avant pour obtenir le chemin de persistedUriPermissions. S'il y a des intersections entre eux, cela signifie que j'ai accès au volume de stockage.
Semble fonctionner sur l'émulateur, qui a enfin à la fois un stockage interne et une carte SD.
J'espère que nous aurons une API appropriée et que nous n'aurons pas besoin d'utiliser des réflexions.
S'il y a une meilleure façon de le faire, sans ce genre de trucs, faites-le moi savoir.
Voici donc un exemple:
MainActivity.kt
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val storageManager = getSystemService(Context.STORAGE_SERVICE) as StorageManager
val storageVolumes = storageManager.storageVolumes
val primaryVolume = storageManager.primaryStorageVolume
checkAccessButton.setOnClickListener {
val persistedUriPermissions = contentResolver.persistedUriPermissions
val storageVolumePathsWeHaveAccessTo = HashSet<String>()
Log.d("AppLog", "got access to paths:")
for (persistedUriPermission in persistedUriPermissions) {
val path = FileUtilEx.getFullPathFromTreeUri(this, persistedUriPermission.uri)
?: continue
Log.d("AppLog", "path: $path")
storageVolumePathsWeHaveAccessTo.add(path)
}
Log.d("AppLog", "storage volumes:")
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 hasAccess = storageVolumePathsWeHaveAccessTo.contains(volumePath)
Log.d("AppLog", "storageVolume \"${storageVolume.getDescription(this)}\" - volumePath:$volumePath - gotAccess? $hasAccess")
}
}
}
requestAccessButton.setOnClickListener {
val intent = primaryVolume.createOpenDocumentTreeIntent()
startActivityForResult(intent, 1)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
Log.d("AppLog", "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)
val fullPathFromTreeUri = FileUtilEx.getFullPathFromTreeUri(this, uri)
Log.d("AppLog", "granted uri:$uri $fullPathFromTreeUri")
}
}
FileUtilEx.Java
/**
* Get the full path of a document from its tree URI.
*
* @param treeUri The tree RI.
* @return The path (without trailing file separator).
*/
public static String getFullPathFromTreeUri(Context context, final Uri treeUri) {
if (treeUri == null)
return null;
String volumePath = getVolumePath(context, getVolumeIdFromTreeUri(treeUri));
if (volumePath == null)
return File.separator;
if (volumePath.endsWith(File.separator))
volumePath = volumePath.substring(0, volumePath.length() - 1);
String documentPath = getDocumentPathFromTreeUri(treeUri);
if (documentPath.endsWith(File.separator))
documentPath = documentPath.substring(0, documentPath.length() - 1);
if (documentPath.length() > 0)
if (documentPath.startsWith(File.separator))
return volumePath + documentPath;
else return volumePath + File.separator + documentPath;
return volumePath;
}
public static String getVolumePath(StorageVolume storageVolume){
if (VERSION.SDK_INT < VERSION_CODES.Lollipop)
return null;
try{
final Class<?> storageVolumeClazz = StorageVolume.class;
final Method getPath = storageVolumeClazz.getMethod("getPath");
return (String) getPath.invoke(storageVolume);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
e.printStackTrace();
}
return null;
}
/**
* Get the path of a certain volume.
*
* @param volumeId The volume id.
* @return The path.
*/
@SuppressLint("ObsoleteSdkInt")
private static String getVolumePath(Context context, final String volumeId) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Lollipop)
return null;
try {
final StorageManager storageManager = (StorageManager) context.getSystemService(Context.STORAGE_SERVICE);
if (Android.os.Build.VERSION.SDK_INT >= Android.os.Build.VERSION_CODES.N) {
final Class<?> storageVolumeClazz = StorageVolume.class;
//noinspection JavaReflectionMemberAccess
final Method getPath = storageVolumeClazz.getMethod("getPath");
final List<StorageVolume> storageVolumes = storageManager.getStorageVolumes();
for (final StorageVolume storageVolume : storageVolumes) {
final String uuid = storageVolume.getUuid();
final boolean primary = storageVolume.isPrimary();
// primary volume?
if (primary && PRIMARY_VOLUME_NAME.equals(volumeId)) {
return (String) getPath.invoke(storageVolume);
}
// other volumes?
if (uuid != null && uuid.equals(volumeId))
return (String) getPath.invoke(storageVolume);
}
return null;
}
final Class<?> storageVolumeClazz = Class.forName("Android.os.storage.StorageVolume");
final Method getVolumeList = storageManager.getClass().getMethod("getVolumeList");
final Method getUuid = storageVolumeClazz.getMethod("getUuid");
//noinspection JavaReflectionMemberAccess
final Method getPath = storageVolumeClazz.getMethod("getPath");
final Method isPrimary = storageVolumeClazz.getMethod("isPrimary");
final Object result = getVolumeList.invoke(storageManager);
final int length = Array.getLength(result);
for (int i = 0; i < length; i++) {
final Object storageVolumeElement = Array.get(result, i);
final String uuid = (String) getUuid.invoke(storageVolumeElement);
final Boolean primary = (Boolean) isPrimary.invoke(storageVolumeElement);
// primary volume?
if (primary && PRIMARY_VOLUME_NAME.equals(volumeId)) {
return (String) getPath.invoke(storageVolumeElement);
}
// other volumes?
if (uuid != null && uuid.equals(volumeId))
return (String) getPath.invoke(storageVolumeElement);
}
// not found.
return null;
} catch (Exception ex) {
return null;
}
}
/**
* Get the document path (relative to volume name) for a tree URI (Lollipop).
*
* @param treeUri The tree URI.
* @return the document path.
*/
@TargetApi(VERSION_CODES.Lollipop)
private static String getDocumentPathFromTreeUri(final Uri treeUri) {
final String docId = DocumentsContract.getTreeDocumentId(treeUri);
//TODO avoid using spliting of a string (because it uses extra strings creation)
final String[] split = docId.split(":");
if ((split.length >= 2) && (split[1] != null))
return split[1];
else
return File.separator;
}
/**
* Get the volume ID from the tree URI.
*
* @param treeUri The tree URI.
* @return The volume ID.
*/
@TargetApi(VERSION_CODES.Lollipop)
private static String getVolumeIdFromTreeUri(final Uri treeUri) {
final String docId = DocumentsContract.getTreeDocumentId(treeUri);
final int end = docId.indexOf(':');
String result = end == -1 ? null : docId.substring(0, end);
return result;
}
activity_main.xml
<LinearLayout
xmlns:Android="http://schemas.Android.com/apk/res/Android" xmlns:tools="http://schemas.Android.com/tools" Android:layout_width="match_parent" Android:layout_height="match_parent"
Android:gravity="center" Android:orientation="vertical" tools:context=".MainActivity">
<Button
Android:id="@+id/checkAccessButton" Android:layout_width="wrap_content" Android:layout_height="wrap_content" Android:text="checkAccess"/>
<Button
Android:id="@+id/requestAccessButton" Android:layout_width="wrap_content" Android:layout_height="wrap_content" Android:text="requestAccess"/>
</LinearLayout>
Pour le mettre dans une fonction simple, voici:
/** for each storageVolume, tells if we have access or not, via a HashMap (true for each iff we identified it has access*/
fun getStorageVolumesAccessState(context: Context): HashMap<StorageVolume, Boolean> {
val storageManager = context.getSystemService(Context.STORAGE_SERVICE) as StorageManager
val storageVolumes = storageManager.storageVolumes
val persistedUriPermissions = context.contentResolver.persistedUriPermissions
val storageVolumePathsWeHaveAccessTo = HashSet<String>()
// Log.d("AppLog", "got access to paths:")
for (persistedUriPermission in persistedUriPermissions) {
val path = FileUtilEx.getFullPathFromTreeUri(context, persistedUriPermission.uri)
?: continue
// Log.d("AppLog", "path: $path")
storageVolumePathsWeHaveAccessTo.add(path)
}
// Log.d("AppLog", "storage volumes:")
val result = HashMap<StorageVolume, Boolean>(storageVolumes.size)
for (storageVolume in storageVolumes) {
val volumePath = FileUtilEx.getVolumePath(storageVolume)
val hasAccess = volumePath != null && storageVolumePathsWeHaveAccessTo.contains(volumePath)
result[storageVolume] = hasAccess
}
return result
}