Come utilizzare la nuova API di accesso alla scheda SD presentata per Android 5.0 (Lollipop)?


118

sfondo

Su Android 4.4 (KitKat), Google ha reso l'accesso alla scheda SD piuttosto limitato.

A partire da Android Lollipop (5.0), gli sviluppatori possono utilizzare una nuova API che chiede all'utente di confermare per consentire l'accesso a cartelle specifiche, come scritto in questo post di Google Gruppi .

Il problema

Il post ti indirizza a visitare due siti Web:

Sembra un esempio interno (forse da mostrare nelle demo dell'API in seguito), ma è abbastanza difficile capire cosa sta succedendo.

Questa è la documentazione ufficiale della nuova API, ma non fornisce dettagli sufficienti su come usarla.

Ecco cosa ti dice:

Se hai davvero bisogno dell'accesso completo a un'intera sottostruttura di documenti, inizia avviando ACTION_OPEN_DOCUMENT_TREE per consentire all'utente di scegliere una directory. Quindi passare il getData () risultante in fromTreeUri (Context, Uri) per iniziare a lavorare con l'albero selezionato dall'utente.

Mentre navighi nell'albero delle istanze di DocumentFile, puoi sempre usare getUri () per ottenere l'Uri che rappresenta il documento sottostante per quell'oggetto, da usare con openInputStream (Uri), ecc.

Per semplificare il codice sui dispositivi che eseguono KITKAT o versioni precedenti, è possibile utilizzare fromFile (File) che emula il comportamento di un DocumentsProvider.

Le domande

Ho alcune domande sulla nuova API:

  1. Come lo usi davvero?
  2. Secondo il post, il sistema operativo ricorderà che l'app è stata autorizzata ad accedere ai file / cartelle. Come controlli se puoi accedere ai file / cartelle? C'è una funzione che mi restituisce l'elenco di file / cartelle a cui posso accedere?
  3. Come gestisci questo problema su Kitkat? Fa parte della libreria di supporto?
  4. C'è una schermata delle impostazioni sul sistema operativo che mostra quali app hanno accesso a quali file / cartelle?
  5. Cosa succede se un'app viene installata per più utenti sullo stesso dispositivo?
  6. Esiste altra documentazione / tutorial su questa nuova API?
  7. Le autorizzazioni possono essere revocate? In tal caso, c'è un intento che viene inviato all'app?
  8. Chiedere l'autorizzazione funzionerebbe in modo ricorsivo su una cartella selezionata?
  9. L'uso dell'autorizzazione consentirebbe anche di dare all'utente una possibilità di selezione multipla per scelta dell'utente? Oppure l'app deve indicare specificamente all'intento quali file / cartelle consentire?
  10. C'è un modo sull'emulatore per provare la nuova API? Voglio dire, ha una partizione per scheda SD, ma funziona come memoria esterna principale, quindi tutti gli accessi sono già forniti (usando una semplice autorizzazione).
  11. Cosa succede quando l'utente sostituisce la scheda SD con un'altra?

FWIW, AndroidPolice ha pubblicato un piccolo articolo su questo oggi.
fattire il

@fattire Grazie. ma stanno mostrando quello che ho già letto. Comunque è una buona notizia.
sviluppatore Android

32
Ogni volta che esce un nuovo sistema operativo, la nostra vita
diventa

@ Funkystein vero. Vorrei che lo facessero su Kitkat. Ora ci sono 3 tipi di comportamenti da gestire.
sviluppatore Android

1
@ Funkystein non lo so. L'ho usato molto tempo fa. Non è così male fare questo controllo, credo. Devi ricordare che anche loro sono umani, quindi possono commettere errori e cambiare idea di tanto in tanto ...
sviluppatore Android

Risposte:


143

Molte buone domande, approfondiamo. :)

Come lo usi?

Ecco un ottimo tutorial per interagire con Storage Access Framework in KitKat:

https://developer.android.com/guide/topics/providers/document-provider.html#client

L'interazione con le nuove API in Lollipop è molto simile. Per richiedere all'utente di scegliere un albero di directory, puoi avviare un intento come questo:

    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
    startActivityForResult(intent, 42);

Quindi nel tuo onActivityResult (), puoi passare l'Uri selezionato dall'utente alla nuova classe helper DocumentFile. Ecco un rapido esempio che elenca i file nella directory selezionata e quindi crea un nuovo file:

public void onActivityResult(int requestCode, int resultCode, Intent resultData) {
    if (resultCode == RESULT_OK) {
        Uri treeUri = resultData.getData();
        DocumentFile pickedDir = DocumentFile.fromTreeUri(this, treeUri);

        // List all existing files inside picked directory
        for (DocumentFile file : pickedDir.listFiles()) {
            Log.d(TAG, "Found file " + file.getName() + " with size " + file.length());
        }

        // Create a new file and write into it
        DocumentFile newFile = pickedDir.createFile("text/plain", "My Novel");
        OutputStream out = getContentResolver().openOutputStream(newFile.getUri());
        out.write("A long time ago...".getBytes());
        out.close();
    }
}

L'URI restituito da DocumentFile.getUri()è abbastanza flessibile da poter essere utilizzato con diverse API della piattaforma. Ad esempio, potresti condividerlo usando Intent.setData()con Intent.FLAG_GRANT_READ_URI_PERMISSION.

Se si desidera accedere a quell'URI dal codice nativo, è possibile chiamare ContentResolver.openFileDescriptor()e quindi utilizzare ParcelFileDescriptor.getFd()o detachFd()per ottenere un intero descrittore di file POSIX tradizionale.

Come controlli se puoi accedere ai file / cartelle?

Per impostazione predefinita, gli intenti Uris restituiti tramite Storage Access Frameworks non vengono mantenuti durante i riavvii. La piattaforma "offre" la possibilità di mantenere il permesso, ma è comunque necessario "prendere" il permesso se lo si desidera. Nel nostro esempio sopra, chiameresti:

    getContentResolver().takePersistableUriPermission(treeUri,
            Intent.FLAG_GRANT_READ_URI_PERMISSION |
            Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

Puoi sempre capire a cosa concede l'accesso alla tua app tramite l' ContentResolver.getPersistedUriPermissions()API. Se non è più necessario accedere a un Uri persistente, è possibile rilasciarlo con ContentResolver.releasePersistableUriPermission().

È disponibile su KitKat?

No, non possiamo aggiungere retroattivamente nuove funzionalità alle versioni precedenti della piattaforma.

Posso vedere quali app hanno accesso a file / cartelle?

Al momento non esiste un'interfaccia utente che lo mostri, ma puoi trovare i dettagli nella sezione "Autorizzazioni Uri concesse" adb shell dumpsys activity providersdell'output.

Cosa succede se un'app viene installata per più utenti sullo stesso dispositivo?

Le concessioni di autorizzazione di Uri sono isolate per utente, proprio come tutte le altre funzionalità della piattaforma multiutente. Ovvero, la stessa app in esecuzione con due utenti diversi non ha concessioni di autorizzazioni Uri sovrapposte o condivise.

Le autorizzazioni possono essere revocate?

Il DocumentProvider di supporto può revocare l'autorizzazione in qualsiasi momento, ad esempio quando viene eliminato un documento basato su cloud. Il modo più comune per scoprire queste autorizzazioni revocate è quando scompaiono da quelle ContentResolver.getPersistedUriPermissions()sopra menzionate.

Le autorizzazioni vengono revocate anche ogni volta che i dati dell'app vengono cancellati per entrambe le app coinvolte nella concessione.

Chiedere l'autorizzazione funzionerebbe in modo ricorsivo su una cartella selezionata?

Sì, l' ACTION_OPEN_DOCUMENT_TREEintento ti dà accesso ricorsivo a file e directory esistenti e appena creati.

Ciò consente la selezione multipla?

Sì, la selezione multipla è stata supportata da KitKat e puoi consentirla impostando EXTRA_ALLOW_MULTIPLEquando inizi il tuo ACTION_OPEN_DOCUMENTintento. Puoi usare Intent.setType()o EXTRA_MIME_TYPESper restringere i tipi di file che possono essere selezionati:

http://developer.android.com/reference/android/content/Intent.html#ACTION_OPEN_DOCUMENT

C'è un modo sull'emulatore per provare la nuova API?

Sì, il dispositivo di archiviazione condiviso principale dovrebbe apparire nel selettore, anche sull'emulatore. Se l'applicazione utilizza solo l'accesso quadro di archiviazione per l'accesso storage condiviso, non è più necessario i READ/WRITE_EXTERNAL_STORAGEpermessi a tutti e possibile rimuoverli o utilizzare la android:maxSdkVersionfunzione per solo a loro richiesta sulle versioni più vecchie di piattaforma.

Cosa succede quando l'utente sostituisce la scheda SD con un'altra?

Quando è coinvolto un supporto fisico, l'UUID (come il numero di serie FAT) del supporto sottostante viene sempre masterizzato nell'URI restituito. Il sistema lo utilizza per connettersi al supporto selezionato originariamente dall'utente, anche se l'utente scambia il supporto tra più slot.

Se l'utente sostituisce una seconda carta, dovrai chiedere di ottenere l'accesso alla nuova carta. Poiché il sistema ricorda le concessioni per UUID, continuerai ad avere l'accesso concesso in precedenza alla carta originale se l'utente la reinserisce in un secondo momento.

http://en.wikipedia.org/wiki/Volume_serial_number


2
Vedo. quindi la decisione è stata quella invece di aggiungerne altre all'API nota (di File), di usarne un'altra. OK. Puoi rispondere alle altre domande (scritte nel primo commento)? BTW, grazie per aver risposto a tutto questo.
sviluppatore Android

7
@ JeffSharkey Qualche modo per fornire a OPEN_DOCUMENT_TREE un suggerimento sulla posizione di partenza? Gli utenti non sono sempre i migliori nel navigare verso ciò a cui l'applicazione deve accedere.
Justin

2
C'è un modo, come cambiare Last Modified Timestamp ( metodo setLastModified () nella classe File)? È davvero fondamentale per applicazioni come gli archiviatori.
Quark

1
Supponiamo che tu abbia un documento di cartella archiviato Uri e desideri elencare i file in un secondo momento dopo il riavvio del dispositivo. DocumentFile.fromTreeUri elenca sempre i file root, non importa l'Uri che gli dai (anche il figlio Uri), quindi come crei un DocumentFile su cui puoi chiamare listfiles, dove DocumentFile non rappresenta né la radice né un SingleDocument.
AndersC

1
@JeffSharkey Come può essere utilizzato questo URI in MediaMuxer, poiché accetta una stringa come percorso del file di output? MediaMuxer (java.lang.String, int
Petrakeas

45

Nel mio progetto Android in Github, linkato di seguito, puoi trovare codice funzionante che permette di scrivere su extSdCard in Android 5. Si presume che l'utente dia accesso all'intera SD Card e quindi ti permette di scrivere ovunque su questa scheda. (Se vuoi avere accesso solo a singoli file, le cose diventano più facili.)

Snipplet di codice principale

Attivazione di Storage Access Framework:

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
private void triggerStorageAccessFramework() {
    Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
    startActivityForResult(intent, REQUEST_CODE_STORAGE_ACCESS);
}

Gestione della risposta da Storage Access Framework:

@TargetApi(Build.VERSION_CODES.LOLLIPOP)
@Override
public final void onActivityResult(final int requestCode, final int resultCode, final Intent resultData) {
    if (requestCode == SettingsFragment.REQUEST_CODE_STORAGE_ACCESS) {
        Uri treeUri = null;
        if (resultCode == Activity.RESULT_OK) {
            // Get Uri from Storage Access Framework.
            treeUri = resultData.getData();

            // Persist URI in shared preference so that you can use it later.
            // Use your own framework here instead of PreferenceUtil.
            PreferenceUtil.setSharedPreferenceUri(R.string.key_internal_uri_extsdcard, treeUri);

            // Persist access permissions.
            final int takeFlags = resultData.getFlags()
                & (Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
            getActivity().getContentResolver().takePersistableUriPermission(treeUri, takeFlags);
        }
    }
}

Ottenere un output Stream per un file tramite Storage Access Framework (facendo uso dell'URL memorizzato, assumendo che questo sia l'URL della cartella principale della scheda SD esterna)

DocumentFile targetDocument = getDocumentFile(file, false);
OutputStream outStream = Application.getAppContext().
    getContentResolver().openOutputStream(targetDocument.getUri());

Questo utilizza i seguenti metodi di supporto:

public static DocumentFile getDocumentFile(final File file, final boolean isDirectory) {
    String baseFolder = getExtSdCardFolder(file);

    if (baseFolder == null) {
        return null;
    }

    String relativePath = null;
    try {
        String fullPath = file.getCanonicalPath();
        relativePath = fullPath.substring(baseFolder.length() + 1);
    }
    catch (IOException e) {
        return null;
    }

    Uri treeUri = PreferenceUtil.getSharedPreferenceUri(R.string.key_internal_uri_extsdcard);

    if (treeUri == null) {
        return null;
    }

    // start with root of SD card and then parse through document tree.
    DocumentFile document = DocumentFile.fromTreeUri(Application.getAppContext(), treeUri);

    String[] parts = relativePath.split("\\/");
    for (int i = 0; i < parts.length; i++) {
        DocumentFile nextDocument = document.findFile(parts[i]);

        if (nextDocument == null) {
            if ((i < parts.length - 1) || isDirectory) {
                nextDocument = document.createDirectory(parts[i]);
            }
            else {
                nextDocument = document.createFile("image", parts[i]);
            }
        }
        document = nextDocument;
    }

    return document;
}

public static String getExtSdCardFolder(final File file) {
    String[] extSdPaths = getExtSdCardPaths();
    try {
        for (int i = 0; i < extSdPaths.length; i++) {
            if (file.getCanonicalPath().startsWith(extSdPaths[i])) {
                return extSdPaths[i];
            }
        }
    }
    catch (IOException e) {
        return null;
    }
    return null;
}

/**
 * Get a list of external SD card paths. (Kitkat or higher.)
 *
 * @return A list of external SD card paths.
 */
@TargetApi(Build.VERSION_CODES.KITKAT)
private static String[] getExtSdCardPaths() {
    List<String> paths = new ArrayList<>();
    for (File file : Application.getAppContext().getExternalFilesDirs("external")) {
        if (file != null && !file.equals(Application.getAppContext().getExternalFilesDir("external"))) {
            int index = file.getAbsolutePath().lastIndexOf("/Android/data");
            if (index < 0) {
                Log.w(Application.TAG, "Unexpected external file dir: " + file.getAbsolutePath());
            }
            else {
                String path = file.getAbsolutePath().substring(0, index);
                try {
                    path = new File(path).getCanonicalPath();
                }
                catch (IOException e) {
                    // Keep non-canonical path.
                }
                paths.add(path);
            }
        }
    }
    return paths.toArray(new String[paths.size()]);
}

 /**
 * Retrieve the application context.
 *
 * @return The (statically stored) application context
 */
public static Context getAppContext() {
    return Application.mApplication.getApplicationContext();
}

Riferimento al codice completo

https://github.com/jeisfeld/Augendiagnose/blob/master/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/fragments/SettingsFragment.java#L521

e

https://github.com/jeisfeld/Augendiagnose/blob/master/AugendiagnoseIdea/augendiagnoseLib/src/main/java/de/jeisfeld/augendiagnoselib/util/imagefile/FileUtil.java


1
Puoi metterlo in un progetto ridotto a icona, che gestisce solo la scheda SD? Inoltre, sai come posso verificare se tutti gli archivi esterni disponibili sono accessibili, in modo da non richiedere il loro permesso per niente?
sviluppatore Android

1
Vorrei poter votare di più. Ero a metà strada verso questa soluzione e ho trovato la navigazione del documento così orribile che ho pensato di aver sbagliato. È bello avere qualche conferma su questo. Grazie Google ... per niente.
Anthony

1
Sì, per scrivere su SD esterna non è più possibile utilizzare il normale approccio File. D'altra parte, per le versioni precedenti di Android e per la SD primaria, File è ancora l'approccio più efficiente. Pertanto è necessario utilizzare una classe di utilità personalizzata per l'accesso ai file.
Jörg Eisfeld

15
@ JörgEisfeld: ho un'app che utilizza File254 volte. Riesci a immaginare di risolverlo? Android sta diventando un incubo per gli sviluppatori con la sua totale mancanza di compatibilità con le versioni precedenti. Non ho ancora trovato alcun posto in cui spieghino perché Google ha preso tutte queste stupide decisioni riguardo all'archiviazione esterna. Alcuni affermano "sicurezza", ma ovviamente non ha senso poiché qualsiasi app può rovinare la memoria interna. La mia ipotesi è di provare a costringerci a utilizzare i loro servizi cloud. Per fortuna, il rooting risolve i problemi ... almeno per Android <6 ....
Luis A. Florit

1
OK. Magicamente, dopo aver riavviato il telefono, funziona :)
sancho21

0

È solo una risposta complementare.

Dopo aver creato un nuovo file, potrebbe essere necessario salvarne la posizione nel database e leggerlo domani. Puoi leggere recuperarlo di nuovo usando questo metodo:

/**
 * Get {@link DocumentFile} object from SD card.
 * @param directory SD card ID followed by directory name, for example {@code 6881-2249:Download/Archive},
 *                 where ID for SD card is {@code 6881-2249}
 * @param fileName for example {@code intel_haxm.zip}
 * @return <code>null</code> if does not exist
 */
public static DocumentFile getExternalFile(Context context, String directory, String fileName){
    Uri uri = Uri.parse("content://com.android.externalstorage.documents/tree/" + directory);
    DocumentFile parent = DocumentFile.fromTreeUri(context, uri);
    return parent != null ? parent.findFile(fileName) : null;
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == SettingsFragment.REQUEST_CODE_STORAGE_ACCESS && resultCode == RESULT_OK) {
        int takeFlags = Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION;
        getContentResolver().takePersistableUriPermission(data.getData(), takeFlags);
        String sdcard = data.getDataString().replace("content://com.android.externalstorage.documents/tree/", "");
        try {
            sdcard = URLDecoder.decode(sdcard, "ISO-8859-1");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
        // for example, sdcardId results "6312-2234"
        String sdcardId = sdcard.substring(0, sdcard.indexOf(':'));
        // save to preferences if you want to use it later
        SharedPreferences preferences = PreferenceManager.getDefaultSharedPreferences(this);
        preferences.edit().putString("sdcard", sdcardId).apply();
    }
}
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.