Accedere a documenti e altri file dallo spazio di archiviazione condiviso

Sui dispositivi che eseguono Android 4.4 (livello API 19) e versioni successive, la tua app può interagire con un fornitore di documenti, inclusi volumi di archiviazione esterni e spazio di archiviazione basato su cloud, utilizzando Storage Access Framework. Questo framework consente agli utenti di interagire con un selettore di sistema per scegliere un fornitore di documenti e selezionare documenti e altri file specifici che la tua app può creare, aprire o modificare.

Poiché l'utente è coinvolto nella selezione dei file o delle directory a cui può accedere la tua app, questo meccanismo non richiede autorizzazioni di sistema e il controllo e la privacy degli utenti vengono migliorati. Inoltre, questi file, che sono archiviati all'esterno di una directory specifica dell'app e al di fuori del media store, rimangono sul dispositivo dopo la disinstallazione dell'app.

L'utilizzo del framework prevede i seguenti passaggi:

  1. Un'app richiama un intent che contiene un'azione relativa allo spazio di archiviazione. Questa azione corrisponde a un caso d'uso specifico reso disponibile dal framework.
  2. L'utente vede un selettore di sistema, che può sfogliare un fornitore di documenti e scegliere una posizione o un documento in cui viene eseguita l'azione relativa allo spazio di archiviazione.
  3. L'app ottiene l'accesso in lettura e scrittura a un URI che rappresenta la posizione o il documento scelto dall'utente. Utilizzando questo URI, l'app può eseguire operazioni nella località scelta.

Per supportare l'accesso ai file multimediali sui dispositivi con Android 9 (livello API 28) o versioni precedenti, dichiara l'autorizzazione READ_EXTERNAL_STORAGE e imposta maxSdkVersion su 28.

Questa guida illustra i diversi casi d'uso supportati dal framework per lavorare con file e altri documenti. Viene inoltre spiegato come eseguire operazioni nella località selezionata dall'utente.

Casi d'uso per l'accesso a documenti e altri file

Storage Access Framework supporta i seguenti casi d'uso per l'accesso a file e altri documenti.

Creare un nuovo file
L'azione di intent di ACTION_CREATE_DOCUMENT consente agli utenti di salvare un file in una posizione specifica.
Aprire un documento o un file
L'azione di intent di ACTION_OPEN_DOCUMENT consente agli utenti di selezionare un documento o un file specifico da aprire.
Concedere l'accesso ai contenuti di una directory
L'azione di intent di ACTION_OPEN_DOCUMENT_TREE, disponibile su Android 5.0 (livello API 21) e versioni successive, consente agli utenti di selezionare una directory specifica, concedendo alla tua app l'accesso a tutti i file e alle sottodirectory al suo interno.

Le sezioni seguenti forniscono indicazioni su come configurare ogni caso d'uso.

Crea un nuovo file

Utilizza l'azione di intent ACTION_CREATE_DOCUMENT per caricare il selettore file di sistema e consentire all'utente di scegliere una posizione in cui scrivere i contenuti di un file. Questa procedura è simile a quella utilizzata nelle finestre di dialogo "Salva con nome" utilizzate da altri sistemi operativi.

Nota: ACTION_CREATE_DOCUMENT non può sovrascrivere un file esistente. Se la tua app tenta di salvare un file con lo stesso nome, il sistema aggiunge un numero tra parentesi alla fine del nome del file.

Ad esempio, se la tua app tenta di salvare un file denominato confirmation.pdf in una directory in cui è già presente un file con quel nome, il sistema salva il nuovo file con il nome confirmation(1).pdf.

Quando configuri l'intent, specifica il nome e il tipo MIME del file e, se vuoi, specifica l'URI del file o della directory che il selettore file deve mostrare al momento del caricamento iniziale utilizzando l'intent aggiuntivo EXTRA_INITIAL_URI.

Il seguente snippet di codice mostra come creare e richiamare l'intent per la creazione di un file:

Kotlin

// Request code for creating a PDF document.
const val CREATE_FILE = 1

private fun createFile(pickerInitialUri: Uri) {
    val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
        addCategory(Intent.CATEGORY_OPENABLE)
        type = "application/pdf"
        putExtra(Intent.EXTRA_TITLE, "invoice.pdf")

        // Optionally, specify a URI for the directory that should be opened in
        // the system file picker before your app creates the document.
        putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
    }
    startActivityForResult(intent, CREATE_FILE)
}

Java

// Request code for creating a PDF document.
private static final int CREATE_FILE = 1;

private void createFile(Uri pickerInitialUri) {
    Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT);
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    intent.setType("application/pdf");
    intent.putExtra(Intent.EXTRA_TITLE, "invoice.pdf");

    // Optionally, specify a URI for the directory that should be opened in
    // the system file picker when your app creates the document.
    intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri);

    startActivityForResult(intent, CREATE_FILE);
}

Apri un file

La tua app potrebbe utilizzare i documenti come unità di archiviazione in cui gli utenti inseriscono dati che potrebbero voler condividere con i peer o importare in altri documenti. Diversi esempi includono l'apertura di un documento di produttività da parte di un utente o l'apertura di un libro salvato come file EPUB.

In questi casi, consenti all'utente di scegliere il file da aprire richiamando l'intent ACTION_OPEN_DOCUMENT, che apre l'app di selezione file del sistema. Per mostrare solo i tipi di file supportati dalla tua app, specifica un tipo MIME. Inoltre, facoltativamente, puoi specificare l'URI del file che il selettore file deve visualizzare quando viene caricato per la prima volta utilizzando l'intent aggiuntivo EXTRA_INITIAL_URI.

Il seguente snippet di codice mostra come creare e richiamare l'intent per l'apertura di un documento PDF:

Kotlin

// Request code for selecting a PDF document.
const val PICK_PDF_FILE = 2

fun openFile(pickerInitialUri: Uri) {
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
        addCategory(Intent.CATEGORY_OPENABLE)
        type = "application/pdf"

        // Optionally, specify a URI for the file that should appear in the
        // system file picker when it loads.
        putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
    }

    startActivityForResult(intent, PICK_PDF_FILE)
}

Java

// Request code for selecting a PDF document.
private static final int PICK_PDF_FILE = 2;

private void openFile(Uri pickerInitialUri) {
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT);
    intent.addCategory(Intent.CATEGORY_OPENABLE);
    intent.setType("application/pdf");

    // Optionally, specify a URI for the file that should appear in the
    // system file picker when it loads.
    intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri);

    startActivityForResult(intent, PICK_PDF_FILE);
}

Limitazioni di accesso

Su Android 11 (livello API 30) e versioni successive, non puoi utilizzare l'azione di intent ACTION_OPEN_DOCUMENT per richiedere all'utente di selezionare singoli file dalle seguenti directory:

  • La directory Android/data/ e tutte le sottodirectory.
  • La directory Android/obb/ e tutte le sottodirectory.

Concedere l'accesso ai contenuti di una directory

Le app di gestione dei file e di creazione di contenuti multimediali in genere gestiscono gruppi di file in una gerarchia di directory. Per fornire questa funzionalità nella tua app, utilizza l'azione di intent ACTION_OPEN_DOCUMENT_TREE, che consente all'utente di concedere l'accesso a un intero albero di directory, con alcune eccezioni a partire da Android 11 (livello API 30). L'app può quindi accedere a qualsiasi file nella directory selezionata e nelle sue sottodirectory.

Quando utilizzi ACTION_OPEN_DOCUMENT_TREE, l'app ottiene l'accesso solo ai file nella directory selezionata dall'utente. Non hai accesso ai file di altre app che risiedono al di fuori di questa directory selezionata dall'utente. Questo accesso controllato dagli utenti consente loro di scegliere esattamente quali contenuti condividere con la tua app.

Facoltativamente, puoi specificare l'URI della directory che il selettore file deve mostrare al momento del caricamento iniziale utilizzando l'intent aggiuntivo EXTRA_INITIAL_URI.

Il seguente snippet di codice mostra come creare e richiamare l'intent per aprire una directory:

Kotlin

fun openDirectory(pickerInitialUri: Uri) {
    // Choose a directory using the system's file picker.
    val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE).apply {
        // Optionally, specify a URI for the directory that should be opened in
        // the system file picker when it loads.
        putExtra(DocumentsContract.EXTRA_INITIAL_URI, pickerInitialUri)
    }

    startActivityForResult(intent, your-request-code)
}

Java

public void openDirectory(Uri uriToLoad) {
    // Choose a directory using the system's file picker.
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);

    // Optionally, specify a URI for the directory that should be opened in
    // the system file picker when it loads.
    intent.putExtra(DocumentsContract.EXTRA_INITIAL_URI, uriToLoad);

    startActivityForResult(intent, your-request-code);
}

Limitazioni di accesso

Su Android 11 (livello API 30) e versioni successive, non puoi utilizzare l'azione intent ACTION_OPEN_DOCUMENT_TREE per richiedere l'accesso alle seguenti directory:

  • La directory principale del volume della memoria interna.
  • La directory root di ogni volume della scheda SD che il produttore del dispositivo considera affidabile, a prescindere dal fatto che la scheda sia emulata o rimovibile. Un volume affidabile è un volume a cui un'app può accedere correttamente nella maggior parte del tempo.
  • La directory Download.

Inoltre, su Android 11 (livello API 30) e versioni successive, non puoi utilizzare l'azione intent ACTION_OPEN_DOCUMENT_TREE per richiedere all'utente di selezionare singoli file dalle seguenti directory:

  • La directory Android/data/ e tutte le sottodirectory.
  • La directory Android/obb/ e tutte le sottodirectory.

Esegui operazioni nella località scelta

Dopo che l'utente ha selezionato un file o una directory utilizzando il selettore file del sistema, puoi recuperare l'URI dell'elemento selezionato utilizzando il seguente codice in onActivityResult():

Kotlin

override fun onActivityResult(
        requestCode: Int, resultCode: Int, resultData: Intent?) {
    if (requestCode == your-request-code
            && resultCode == Activity.RESULT_OK) {
        // The result data contains a URI for the document or directory that
        // the user selected.
        resultData?.data?.also { uri ->
            // Perform operations on the document using its URI.
        }
    }
}

Java

@Override
public void onActivityResult(int requestCode, int resultCode,
        Intent resultData) {
    if (requestCode == your-request-code
            && resultCode == Activity.RESULT_OK) {
        // The result data contains a URI for the document or directory that
        // the user selected.
        Uri uri = null;
        if (resultData != null) {
            uri = resultData.getData();
            // Perform operations on the document using its URI.
        }
    }
}

Mediante un riferimento all'URI dell'elemento selezionato, l'app può eseguire diverse operazioni sull'elemento. Ad esempio, puoi accedere ai metadati dell'elemento, modificarlo ed eliminarlo.

Le seguenti sezioni mostrano come completare le azioni sui file selezionati dall'utente.

Determinare le operazioni supportate da un provider

Diversi fornitori di contenuti consentono diverse operazioni sui documenti, come la copia o la visualizzazione della miniatura di un documento. Per determinare quali operazioni sono supportate da un determinato provider, controlla il valore di Document.COLUMN_FLAGS. L'UI dell'app può quindi mostrare solo le opzioni supportate dal fornitore.

Mantieni autorizzazioni

Quando la tua app apre un file per la lettura o la scrittura, il sistema concede all'app una concessione dell'autorizzazione URI per tale file, che dura fino al riavvio del dispositivo dell'utente. Supponiamo, tuttavia, che la tua app sia un'app di modifica delle immagini e che tu voglia che gli utenti siano in grado di accedere alle 5 immagini modificate più di recente, direttamente dalla tua app. Se il dispositivo dell'utente è stato riavviato, dovrai reindirizzare l'utente al selettore di sistema per trovare i file.

Per preservare l'accesso ai file nei riavvii del dispositivo e creare una migliore esperienza utente, la tua app può "richiedere" l'autorizzazione URI permanente che il sistema offre, come mostrato nel seguente snippet di codice:

Kotlin

val contentResolver = applicationContext.contentResolver

val takeFlags: Int = Intent.FLAG_GRANT_READ_URI_PERMISSION or
        Intent.FLAG_GRANT_WRITE_URI_PERMISSION
// Check for the freshest data.
contentResolver.takePersistableUriPermission(uri, takeFlags)

Java

final int takeFlags = intent.getFlags()
            & (Intent.FLAG_GRANT_READ_URI_PERMISSION
            | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
// Check for the freshest data.
getContentResolver().takePersistableUriPermission(uri, takeFlags);

Esamina i metadati del documento

Quando hai l'URI di un documento, puoi accedere ai relativi metadati. Questo snippet recupera i metadati per un documento specificato dall'URI e li registra:

Kotlin

val contentResolver = applicationContext.contentResolver

fun dumpImageMetaData(uri: Uri) {

    // The query, because it only applies to a single document, returns only
    // one row. There's no need to filter, sort, or select fields,
    // because we want all fields for one document.
    val cursor: Cursor? = contentResolver.query(
            uri, null, null, null, null, null)

    cursor?.use {
        // moveToFirst() returns false if the cursor has 0 rows. Very handy for
        // "if there's anything to look at, look at it" conditionals.
        if (it.moveToFirst()) {

            // Note it's called "Display Name". This is
            // provider-specific, and might not necessarily be the file name.
            val displayName: String =
                    it.getString(it.getColumnIndex(OpenableColumns.DISPLAY_NAME))
            Log.i(TAG, "Display Name: $displayName")

            val sizeIndex: Int = it.getColumnIndex(OpenableColumns.SIZE)
            // If the size is unknown, the value stored is null. But because an
            // int can't be null, the behavior is implementation-specific,
            // and unpredictable. So as
            // a rule, check if it's null before assigning to an int. This will
            // happen often: The storage API allows for remote files, whose
            // size might not be locally known.
            val size: String = if (!it.isNull(sizeIndex)) {
                // Technically the column stores an int, but cursor.getString()
                // will do the conversion automatically.
                it.getString(sizeIndex)
            } else {
                "Unknown"
            }
            Log.i(TAG, "Size: $size")
        }
    }
}

Java

public void dumpImageMetaData(Uri uri) {

    // The query, because it only applies to a single document, returns only
    // one row. There's no need to filter, sort, or select fields,
    // because we want all fields for one document.
    Cursor cursor = getActivity().getContentResolver()
            .query(uri, null, null, null, null, null);

    try {
        // moveToFirst() returns false if the cursor has 0 rows. Very handy for
        // "if there's anything to look at, look at it" conditionals.
        if (cursor != null && cursor.moveToFirst()) {

            // Note it's called "Display Name". This is
            // provider-specific, and might not necessarily be the file name.
            String displayName = cursor.getString(
                    cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME));
            Log.i(TAG, "Display Name: " + displayName);

            int sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE);
            // If the size is unknown, the value stored is null. But because an
            // int can't be null, the behavior is implementation-specific,
            // and unpredictable. So as
            // a rule, check if it's null before assigning to an int. This will
            // happen often: The storage API allows for remote files, whose
            // size might not be locally known.
            String size = null;
            if (!cursor.isNull(sizeIndex)) {
                // Technically the column stores an int, but cursor.getString()
                // will do the conversion automatically.
                size = cursor.getString(sizeIndex);
            } else {
                size = "Unknown";
            }
            Log.i(TAG, "Size: " + size);
        }
    } finally {
        cursor.close();
    }
}

Apri un documento

Avendo un riferimento all'URI di un documento, puoi aprire un documento per un'ulteriore elaborazione. Questa sezione mostra esempi di apertura di una bitmap e di un flusso di input.

Bitmap

Il seguente snippet di codice mostra come aprire un file Bitmap dato il suo URI:

Kotlin

val contentResolver = applicationContext.contentResolver

@Throws(IOException::class)
private fun getBitmapFromUri(uri: Uri): Bitmap {
    val parcelFileDescriptor: ParcelFileDescriptor =
            contentResolver.openFileDescriptor(uri, "r")
    val fileDescriptor: FileDescriptor = parcelFileDescriptor.fileDescriptor
    val image: Bitmap = BitmapFactory.decodeFileDescriptor(fileDescriptor)
    parcelFileDescriptor.close()
    return image
}

Java

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;
}

Dopo aver aperto la bitmap, puoi visualizzarla in una ImageView.

Stream di input

Il seguente snippet di codice mostra come aprire un oggetto InputStream in base al relativo URI. In questo snippet, le righe del file vengono lette in una stringa:

Kotlin

val contentResolver = applicationContext.contentResolver

@Throws(IOException::class)
private fun readTextFromUri(uri: Uri): String {
    val stringBuilder = StringBuilder()
    contentResolver.openInputStream(uri)?.use { inputStream ->
        BufferedReader(InputStreamReader(inputStream)).use { reader ->
            var line: String? = reader.readLine()
            while (line != null) {
                stringBuilder.append(line)
                line = reader.readLine()
            }
        }
    }
    return stringBuilder.toString()
}

Java

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

Modificare un documento

Puoi utilizzare Storage Access Framework per modificare un documento di testo.

Il seguente snippet di codice sovrascrive i contenuti del documento rappresentato dall'URI specificato:

Kotlin

val contentResolver = applicationContext.contentResolver

private fun alterDocument(uri: Uri) {
    try {
        contentResolver.openFileDescriptor(uri, "w")?.use {
            FileOutputStream(it.fileDescriptor).use {
                it.write(
                    ("Overwritten at ${System.currentTimeMillis()}\n")
                        .toByteArray()
                )
            }
        }
    } catch (e: FileNotFoundException) {
        e.printStackTrace()
    } catch (e: IOException) {
        e.printStackTrace()
    }
}

Java

private void alterDocument(Uri uri) {
    try {
        ParcelFileDescriptor pfd = getActivity().getContentResolver().
                openFileDescriptor(uri, "w");
        FileOutputStream fileOutputStream =
                new FileOutputStream(pfd.getFileDescriptor());
        fileOutputStream.write(("Overwritten at " + System.currentTimeMillis() +
                "\n").getBytes());
        // Let the document provider know you're done by closing the stream.
        fileOutputStream.close();
        pfd.close();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
    } catch (IOException e) {
        e.printStackTrace();
    }
}

Eliminare un documento

Se hai l'URI di un documento e Document.COLUMN_FLAGS del documento contiene SUPPORTS_DELETE, puoi eliminare il documento. Ecco alcuni esempi:

Kotlin

DocumentsContract.deleteDocument(applicationContext.contentResolver, uri)

Java

DocumentsContract.deleteDocument(applicationContext.contentResolver, uri);

Recupera un URI multimediale equivalente

Il metodo getMediaUri() fornisce un URI del negozio multimediale equivalente all'URI del fornitore di documenti specificato. I due URI si riferiscono allo stesso elemento sottostante. Utilizzando l'URI del negozio multimediale, puoi accedere più facilmente ai file multimediali dallo spazio di archiviazione condiviso.

Il metodo getMediaUri() supporta URI ExternalStorageProvider. Su Android 12 (livello API 31) e versioni successive, il metodo supporta anche gli URI MediaDocumentsProvider.

Aprire un file virtuale

Su Android 7.0 (livello API 25) e versioni successive, la tua app può utilizzare i file virtuali messi a disposizione da Storage Access Framework. Anche se i file virtuali non hanno una rappresentazione binaria, l'app può aprire i relativi contenuti forzandoli in un tipo di file diverso o visualizzando i file tramite l'azione per intent ACTION_VIEW.

Per aprire i file virtuali, l'app client deve includere una logica speciale per gestirli. Se vuoi ottenere una rappresentazione in byte del file, ad esempio per visualizzarne l'anteprima, devi richiedere un tipo MIME alternativo al provider dei documenti.

Dopo che l'utente ha effettuato una selezione, utilizza l'URI nei dati dei risultati per determinare se il file è virtuale, come mostrato nel seguente snippet di codice:

Kotlin

private fun isVirtualFile(uri: Uri): Boolean {
    if (!DocumentsContract.isDocumentUri(this, uri)) {
        return false
    }

    val cursor: Cursor? = contentResolver.query(
            uri,
            arrayOf(DocumentsContract.Document.COLUMN_FLAGS),
            null,
            null,
            null
    )

    val flags: Int = cursor?.use {
        if (cursor.moveToFirst()) {
            cursor.getInt(0)
        } else {
            0
        }
    } ?: 0

    return flags and DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT != 0
}

Java

private boolean isVirtualFile(Uri uri) {
    if (!DocumentsContract.isDocumentUri(this, uri)) {
        return false;
    }

    Cursor cursor = getContentResolver().query(
        uri,
        new String[] { DocumentsContract.Document.COLUMN_FLAGS },
        null, null, null);

    int flags = 0;
    if (cursor.moveToFirst()) {
        flags = cursor.getInt(0);
    }
    cursor.close();

    return (flags & DocumentsContract.Document.FLAG_VIRTUAL_DOCUMENT) != 0;
}

Dopo aver verificato che il documento è un file virtuale, puoi costringere il file a un tipo MIME alternativo, come "image/png". Il seguente snippet di codice mostra come verificare se un file virtuale può essere rappresentato come un'immagine e, in questo caso, ottiene un flusso di input dal file virtuale:

Kotlin

@Throws(IOException::class)
private fun getInputStreamForVirtualFile(
        uri: Uri, mimeTypeFilter: String): InputStream {

    val openableMimeTypes: Array<String>? =
            contentResolver.getStreamTypes(uri, mimeTypeFilter)

    return if (openableMimeTypes?.isNotEmpty() == true) {
        contentResolver
                .openTypedAssetFileDescriptor(uri, openableMimeTypes[0], null)
                .createInputStream()
    } else {
        throw FileNotFoundException()
    }
}

Java

private InputStream getInputStreamForVirtualFile(Uri uri, String mimeTypeFilter)
    throws IOException {

    ContentResolver resolver = getContentResolver();

    String[] openableMimeTypes = resolver.getStreamTypes(uri, mimeTypeFilter);

    if (openableMimeTypes == null ||
        openableMimeTypes.length < 1) {
        throw new FileNotFoundException();
    }

    return resolver
        .openTypedAssetFileDescriptor(uri, openableMimeTypes[0], null)
        .createInputStream();
}

Risorse aggiuntive

Per ulteriori informazioni su come archiviare e accedere a documenti e altri file, consulta le risorse seguenti.

Campioni

Video