Introduction :

Après avoir terminé la section précédente, vous devriez déjà savoir comment utiliser le ContentProvider fourni par le système ou définir un ContentProvider personnalisé, ce qui répond globalement aux besoins de développement quotidiens. Fait intéressant, j'ai vu ces autres Providers dans la documentation officielle :

此处输入图片的描述

Calendar ProviderCalendar Provider : une bibliothèque de ressources pour les événements liés au calendrier. Grâce à son API, nous pouvons effectuer des opérations CRUD sur les calendriers, les horaires, les réunions, les rappels, et plus encore !
Contacts ProviderContacts Provider : pas besoin d'en dire plus. C'est celui qui est le plus utilisé. Je traduirai cet article quand j'aurai le temps plus tard !
Storage Access Framework(SAF)Storage Access Framework : une nouvelle fonctionnalité introduite après 4.4, qui permet aux utilisateurs de parcourir facilement le contenu de stockage sur leur téléphone. Le contenu accessible comprend non seulement les documents, les images, les vidéos, l'audio et les téléchargements, mais aussi tout contenu fourni par des ContentProviders spécifiques (qui doivent avoir l'API convenue). Peu importe d'où vient ce contenu, ou quelle application invoque la commande pour parcourir le contenu des fichiers système, le système vous fournira une interface unifiée pour parcourir.
En fait, il s'agit d'une application intégrée appelée DocumentsUI. Parce que son IntentFilter n'a pas de LAUNCHER, on ne trouve pas cette chose sur le bureau ! Héhé, essayez le code ci-dessous. Ici, nous avons choisi deux téléphones pour comparaison : un Lenovo S898T sous 4.2 et un Nexus 5 sous 5.0.1. Exécutez le code suivant :

 Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.setType("image/*");
        startActivity(intent);
Voici le résultat de l'exécution :

Celui de droite est la nouvelle chose que 4.4 nous a apportée. On peut généralement l'utiliser quand on a besoin d'obtenir une URL de fichier. Ensuite, parcourons brièvement la documentation.


2. Bref aperçu de la documentation :

1) Composants du framework SAF :

  • Fournisseur de documentsDocumentsProvider : un ContentProvider spécial qui permet à un service de stockage (comme Google Drive) d'exposer les fichiers qu'il gère à l'extérieur. Il estDocumentsProviderune sous-classe de ContentProvider. De plus, le format de stockage du fournisseur de documents est cohérent avec le format de stockage de fichiers traditionnel. Quant à la façon dont votre contenu est stocké, cela dépend entièrement de vous. Android a intégré plusieurs fournisseurs de documents de ce type, comme ceux pour les téléchargements, les images et les vidéos !
  • Appli clienteApplication cliente : une application cliente normale, en déclenchantACTION_OPEN_DOCUMENTet/ouACTION_CREATE_DOCUMENTpeut recevoir le contenu renvoyé par le fournisseur de documents, par exemple, sélectionner une image puis renvoyer un Uri.
  • SélecteurSélecteur : une interface similaire à un gestionnaire de fichiers, et c'est une interface système qui fournit un canal pour accéder au contenu du fournisseur de documents qui correspond aux conditions de filtre du client. C'est le programme DocumentsUI mentionné précédemment !

Quelques fonctionnalités :

  • Les utilisateurs peuvent parcourir le contenu fourni par tous les fournisseurs de documents, pas seulement une seule application.
  • Il fournit un accès à long terme et durable aux fichiers dans les fournisseurs de documents ainsi qu'une persistance des données. Les utilisateurs peuvent ajouter, supprimer, modifier et enregistrer le contenu maintenu par le fournisseur de documents.
  • Il prend en charge plusieurs utilisateurs et des services de contenu temporaires. Par exemple, les fournisseurs de stockage USB n'apparaissent que lorsque le pilote est installé avec succès.

2) Aperçu :

Le cœur de SAF est une sous-classe qui implémente DocumentsProvider, et il s'agit toujours d'un ContentProvider. Au sein d'un fournisseur de documents, les fichiers sont organisés selon une arborescence de répertoires de fichiers traditionnelle :

3) Diagramme de flux :

Comme mentionné ci-dessus, les données d'un fournisseur de documents sont basées sur une hiérarchie de fichiers traditionnelle, mais ce n'est qu'une représentation externe. La façon dont vous stockez vos données vous appartient entièrement, tant que l'interface que vous exposez à l'extérieur peut être accessible via l'API DocumentsProvider. Le diagramme de flux suivant montre une structure possible pour une application photo utilisant SAF :

Analyse :

D'après la figure ci-dessus, nous pouvons voir que le Picker est un pont reliant l'appelant et les fournisseurs de contenu ! Il fournit et indique à l'appelant quels fournisseurs de contenu peuvent être sélectionnés, tels que DriveDocProvider, UsbDocProvider et CloundDocProvider ici.
Lorsque le client déclencheACTION_OPEN_DOCUMENTouACTION_CREATE_DOCUMENTl'Intent, l'interaction ci-dessus se produit. Bien sûr, nous pouvons également ajouter des conditions de filtre à l'Intent, par exemple en limitant le type MIME à "image" !

Voilà pour tout ce qui précède. Si vous avez installé d'autres logiciels de visualisation d'images, ils apparaîtront également ici ! En termes simples : après que le client a envoyé un Intent avec les deux Actions ci-dessus, l'interface du Picker s'ouvrira. Les fournisseurs de documents pertinents disponibles seront affichés ici pour que l'utilisateur puisse choisir. Après que l'utilisateur a choisi, les informations liées au fichier peuvent être obtenues !


4) Appel côté client et obtention de l'Uri renvoyé

Le code d'implémentation est le suivant :

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private static final int READ_REQUEST_CODE = 42;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Button btn_show = (Button) findViewById(R.id.btn_show);
        btn_show.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
        intent.addCategory(Intent.CATEGORY_OPENABLE);
        intent.setType("image/*");
        startActivityForResult(intent, READ_REQUEST_CODE);
    }

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) {
            Uri uri;
            if (data != null) {
                uri = data.getData();
                Log.e("HeHe", "Uri: " + uri.toString());
            }
        }
    }
}

Résultat d'exécution :Par exemple, si nous sélectionnons ce chien, l'interface du Picker se fermera automatiquement, et nous pouvons alors voir un uri de ce type dans Logcat :


5) Obtention des paramètres de fichier à partir de l'uri

Le code principal est le suivant :

public void dumpImageMetaData(Uri uri) {
    Cursor cursor = getContentResolver()
            .query(uri, null, null, null, null, null);
    try {
        if (cursor != null && cursor.moveToFirst()) {
            String displayName = cursor.getString(
                    cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
            Log.e("HeHe", "Display Name: " + displayName);
            int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
            String size = null;
            if (!cursor.isNull(sizeIndex)) {
                size = cursor.getString(sizeIndex);
            }else {
                size = "Unknown";
            }
            Log.e("HeHe", "Size: " + size);
        }
    }finally {
        cursor.close();
    }
}

Résultat d'exécution :Toujours ce chien. Après avoir appelé la méthode, il affichera le nom du fichier et la taille du fichier en octets.


6) Obtention d'un Bitmap à partir de l'Uri

Le code principal est le suivant :

private Bitmap getBitmapFromUri(Uri uri) throws IOException {
        ParcelFileDescriptor parcelFileDescriptor =
        getContentResolver().openFileDescriptor(uri, "r");
        FileDescriptor fileDescriptor = parcelFileDescriptor.getFileDescriptor();
        Bitmap image = BitmapFactory.decodeFileDescriptor(fileDescriptor);
        parcelFileDescriptor.close();
        return image;
}

Résultat de l'exécution:

7) Obtention d'un flux d'entrée à partir de l'Uri

Le code principal est le suivant :

private String readTextFromUri(Uri uri) throws IOException {
    InputStream inputStream = getContentResolver().openInputStream(uri);
    BufferedReader reader = new BufferedReader(new InputStreamReader(
            inputStream));
    StringBuilder stringBuilder = new StringBuilder();
    String line;
    while ((line = reader.readLine()) != null) {
        stringBuilder.append(line);
    }
    fileInputStream.close();
    parcelFileDescriptor.close();
    return stringBuilder.toString();
}

Le contenu ci-dessus vous indique seulement ce que vous pouvez savoir au moyen d'un Uri, et l'Uri est obtenu via SAF !


8) Création de nouveaux fichiers et suppression de fichiers :

Création d'un fichier :

private void createFile(String mimeType, String fileName) {
    Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    intent.setType(mimeType);
    intent.putExtra(Intent.EXTRA_TITLE, fileName);
    startActivityForResult(intent, WRITE_REQUEST_CODE);
}

Vous pouvez obtenir l'uri du fichier créé dans onActivityResult().

Suppression d'un fichier :

Le prérequis est que Document.COLUMN_FLAGS contienneSUPPORTS_DELETE

DocumentsContract.deleteDocument(getContentResolver(), uri);

9) Écriture d'un Document Provider personnalisé

Si vous voulez que les données de votre application puissent également être ouvertes dans documentsui, vous devez écrire votre propre document provider. Voici les étapes pour définir un DocumentsProvider personnalisé :

  • Version d'API 19 ou supérieure.
  • Enregistrez ce fournisseur dans manifest.xml.
  • Le nom du fournisseur est le nom de la classe suivi du nom du package, par exemple :com.example.android.storageprovider.MyCloudProvider
  • L'autorité est le nom du package + le nom du type de fournisseur, par exemple :com.example.android.storageprovider.documents
  • La valeur de l'attribut android:exported est true.

Ci-dessous se trouve un exemple de la façon d'écrire le fournisseur :

<manifest... >
    ...
    <uses-sdk
        android:minSdkVersion="19"
        android:targetSdkVersion="19" />
        ....
        <provider
            android:name="com.example.android.storageprovider.MyCloudProvider"
            android:authorities="com.example.android.storageprovider.documents"
            android:grantUriPermissions="true"
            android:exported="true"
            android:permission="android.permission.MANAGE_DOCUMENTS"
            android:enabled="@bool/atLeastKitKat">
            <intent-filter>
                <action android:name="android.content.action.DOCUMENTS_PROVIDER" />
            </intent-filter>
        </provider>
    </application>

</manifest>

10) Sous-classes de DocumentsProvider

Implémentez au moins les méthodes suivantes :

  • queryRoots()
  • queryChildDocuments()
  • queryDocument()
  • openDocument()

Il existe d'autres méthodes, mais elles ne sont pas obligatoires. Ci-dessous se trouve un exemple approximatif d'implémentation d'un DocumentsProvider qui accède au système de fichiers.

Implémenter queryRoots

@Override
public Cursor queryRoots(String[] projection) throws FileNotFoundException {

    // Create a cursor with either the requested fields, or the default
    // projection if "projection" is null.
    final MatrixCursor result =
            new MatrixCursor(resolveRootProjection(projection));

    // If user is not logged in, return an empty root cursor.  This removes our
    // provider from the list entirely.
    if (!isUserLoggedIn()) {
        return result;
    }

    // It's possible to have multiple roots (e.g. for multiple accounts in the
    // same app) -- just add multiple cursor rows.
    // Construct one row for a root called "MyCloud".
    final MatrixCursor.RowBuilder row = result.newRow();
    row.add(Root.COLUMN_ROOT_ID, ROOT);
    row.add(Root.COLUMN_SUMMARY, getContext().getString(R.string.root_summary));

    // FLAG_SUPPORTS_CREATE means at least one directory under the root supports
    // creating documents. FLAG_SUPPORTS_RECENTS means your application's most
    // recently used documents will show up in the "Recents" category.
    // FLAG_SUPPORTS_SEARCH allows users to search all documents the application
    // shares.
    row.add(Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE |
            Root.FLAG_SUPPORTS_RECENTS |
            Root.FLAG_SUPPORTS_SEARCH);

    // COLUMN_TITLE is the root title (e.g. Gallery, Drive).
    row.add(Root.COLUMN_TITLE, getContext().getString(R.string.title));

    // This document id cannot change once it's shared.
    row.add(Root.COLUMN_DOCUMENT_ID, getDocIdForFile(mBaseDir));

    // The child MIME types are used to filter the roots and only present to the
    //  user roots that contain the desired type somewhere in their file hierarchy.
    row.add(Root.COLUMN_MIME_TYPES, getChildMimeTypes(mBaseDir));
    row.add(Root.COLUMN_AVAILABLE_BYTES, mBaseDir.getFreeSpace());
    row.add(Root.COLUMN_ICON, R.drawable.ic_launcher);

    return result;
}

Implémenter queryChildDocuments

public Cursor queryChildDocuments(String parentDocumentId, String[] projection,
                              String sortOrder) throws FileNotFoundException {

    final MatrixCursor result = new
            MatrixCursor(resolveDocumentProjection(projection));
    final File parent = getFileForDocId(parentDocumentId);
    for (File file : parent.listFiles()) {
        // Adds the file's display name, MIME type, size, and so on.
        includeFile(result, null, file);
    }
    return result;
}

Implémenter queryDocument

@Override
public Cursor queryDocument(String documentId, String[] projection) throws
        FileNotFoundException {

    // Create a cursor with the requested projection, or the default projection.
    final MatrixCursor result = new
            MatrixCursor(resolveDocumentProjection(projection));
    includeFile(result, documentId, null);
    return result;
}

Bon, c'est à peu près tout le contenu de la documentation. Au début, je voulais le traduire moi-même, mais j'ai ensuite trouvé une traduction chinoise de ce document sur « Days Online », alors j'ai pris la solution de facilité~

Lien de la traduction chinoise :Android Storage Access Framework


3. Le problème d'obtention des chemins de ressources dans Android 4.4 :

En fait, l'endroit où nous utilisons le plus SAF est simplement pour obtenir l'Uri d'une image. Et à partir de l'exemple ci-dessus, nous avons aussi constaté que le lien que nous obtenons de cette façon ressemble à ceci :

content://com.android.providers.media.documents/document/image%3A69983

Pour un tel lien, nous pouvons directement utiliser la méthode ci-dessus pour obtenir l'uri !

Bien sûr, cela vaut pour 4.4 ou version ultérieure.

S'il s'agit d'une version antérieure, l'uri peut ressembler à ceci :

content://media/external/images/media/image%3A69983

Voici une solution complète que j'ai vue ailleurs. Lien original :Le problème d'obtention des chemins de ressources dans Android 4.4

public static String getPath(final Context context, final Uri uri) {
    final boolean isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT;
    // DocumentProvider  
    if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) {
        // ExternalStorageProvider  
        if (isExternalStorageDocument(uri)) {
            final String docId = DocumentsContract.getDocumentId(uri);
            final String[] split = docId.split(":");
            final String type = split[0];

            if ("primary".equalsIgnoreCase(type)) {
                return Environment.getExternalStorageDirectory() + "/" + split[1];
            }

            // TODO handle non-primary volumes  
        }
        // DownloadsProvider  
        else if (isDownloadsDocument(uri)) {

            final String id = DocumentsContract.getDocumentId(uri);
            final Uri contentUri = ContentUris.withAppendedId(
                    Uri.parse("content://downloads/public_downloads"), Long.valueOf(id));

            return getDataColumn(context, contentUri, null, null);
        }
        // MediaProvider  
        else if (isMediaDocument(uri)) {
            final String docId = DocumentsContract.getDocumentId(uri);
            final String[] split = docId.split(":");
            final String type = split[0];
            Uri contentUri = null;
            if ("image".equals(type)) {
                contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI;
            } else if ("video".equals(type)) {
                contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI;
            } else if ("audio".equals(type)) {
                contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI;
            }
            final String selection = "_id=?";
            final String[] selectionArgs = new String[] {
                    split[1]
            };
            return getDataColumn(context, contentUri, selection, selectionArgs);
        }
    }
    // MediaStore (and general)  
    else if ("content".equalsIgnoreCase(uri.getScheme())) {
        return getDataColumn(context, uri, null, null);
    }
    // File  
    else if ("file".equalsIgnoreCase(uri.getScheme())) {
        return uri.getPath();
    }
    return null;
}

/**
 * Get the value of the data column for this Uri. This is useful for 
 * MediaStore Uris, and other file-based ContentProviders. 
 *
 * @param context The context. 
 * @param uri The Uri to query. 
 * @param selection (Optional) Filter used in the query. 
 * @param selectionArgs (Optional) Selection arguments used in the query. 
 * @return The value of the _data column, which is typically a file path. 
 */
public static String getDataColumn(Context context, Uri uri, String selection,
                                   String[] selectionArgs) {

    Cursor cursor = null;
    final String column = "_data";
    final String[] projection = {
            column
    };

    try {
        cursor = context.getContentResolver().query(uri, projection, selection, selectionArgs,
                null);
        if (cursor != null && cursor.moveToFirst()) {
            final int column_index = cursor.getColumnIndexOrThrow(column);
            return cursor.getString(column_index);
        }
    } finally {
        if (cursor != null)
            cursor.close();
    }
    return null;
}


/**
 * @param uri The Uri to check. 
 * @return Whether the Uri authority is ExternalStorageProvider. 
 */
public static boolean isExternalStorageDocument(Uri uri) {
    return "com.android.externalstorage.documents".equals(uri.getAuthority());
}

/**
 * @param uri The Uri to check. 
 * @return Whether the Uri authority is DownloadsProvider. 
 */
public static boolean isDownloadsDocument(Uri uri) {
    return "com.android.providers.downloads.documents".equals(uri.getAuthority());
}

/**
 * @param uri The Uri to check. 
 * @return Whether the Uri authority is MediaProvider. 
 */
public static boolean isMediaDocument(Uri uri) {
    return "com.android.providers.media.documents".equals(uri.getAuthority());
}

Résumé :

Bon, c'est tout pour cette section sur l'Android Storage Access Framework (SAF). Il n'y a pas d'exemples. Nous approfondirons plus tard quand nous aurons besoin de l'utiliser. Sachez simplement que cela existe. Après 4.4, obtenir les chemins de fichiers est bien plus simple~