page.title=Estrutura de acesso ao armazenamento @jd:body
O Android 4.4 (API de nível 19) introduz a Estrutura de Acesso ao Armazenamento (SAF). A SAF simplifica para os usuários a busca e abertura de documentos, imagens e outros arquivos dentre todos os provedores de armazenamento de documentos de preferência. A interface gráfica padrão é fácil de usar e permite aos usuários buscar arquivos e acessar arquivos recentes de modo coerente em todos os aplicativos e provedores.
Serviços de armazenamento local ou em nuvem podem participar desse ecossistema por meio da implementação de um {@link android.provider.DocumentsProvider} que encapsula os serviços. Aplicativos clientes que precisam acessar documentos de um provedor podem integrar-se com a SAF com apenas algumas linhas de código.
A SAF contém:
A seguir há alguns recursos oferecidos pela SAF:
A SAF consiste em um provedor de conteúdo que é uma subclasse da classe {@link android.provider.DocumentsProvider}. Dentro de um provedor de documentos, os dados são estruturados como uma hierarquia de arquivo tradicional:
Observe o seguinte:
Como indicado anteriormente, o modelo de dados do provedor de documentos se baseia em uma hierarquia de arquivo tradicional. Contudo, é possível armazenar os dados fisicamente como quiser desde que eles possam ser acessados pela API {@link android.provider.DocumentsProvider}. Por exemplo: seria possível usar armazenamento em nuvem com base em tag para os dados.
A figura 2 ilustra um exemplo de um aplicativo de foto que usa a SAF para acessar dados armazenados:
Observe o seguinte:
A figura 3 exibe um seletor em que um usuário em busca de imagens selecionou uma conta do Google Drive:
Quando o usuário seleciona o Google Drive, as imagens são exibidas como ilustrado na figura 4. Desse momento em diante, o usuário poderá interagir com eles de todos os modos compatíveis com o provedor e o aplicativo cliente.
No Android 4.3 e em versões anteriores, para o aplicativo recuperar um arquivo de outro aplicativo, ele precisa chamar uma intenção como {@link android.content.Intent#ACTION_PICK} ou {@link android.content.Intent#ACTION_GET_CONTENT}. Em seguida, o usuário deve selecionar um único aplicativo do qual deseja retirar um arquivo e o aplicativo selecionado deve fornecer uma interface do usuário para a busca e a seleção dos arquivos disponíveis.
No Android 4.4 e em versões posteriores, existe a opção adicional de usar a intenção {@link android.content.Intent#ACTION_OPEN_DOCUMENT}, que exibe uma IU do seletor controlada pelo sistema que permite ao usuário buscar todos os arquivos que outros aplicativos disponibilizaram. Nessa IU exclusiva, o usuário pode selecionar um arquivo de qualquer aplicativo compatível.
{@link android.content.Intent#ACTION_OPEN_DOCUMENT} não substitui {@link android.content.Intent#ACTION_GET_CONTENT}. O uso de cada um deles depende da necessidade do aplicativo:
Esta seção descreve como programar aplicativos clientes com base nas intenções {@link android.content.Intent#ACTION_OPEN_DOCUMENT} e {@link android.content.Intent#ACTION_CREATE_DOCUMENT}.
O fragmento a seguir usa {@link android.content.Intent#ACTION_OPEN_DOCUMENT} para buscar provedores de documentos que contenham arquivos de imagem:
private static final int READ_REQUEST_CODE = 42; ... /** * Fires an intent to spin up the "file chooser" UI and select an image. */ public void performFileSearch() { // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's file // browser. Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); // Filter to only show results that can be "opened", such as a // file (as opposed to a list of contacts or timezones) intent.addCategory(Intent.CATEGORY_OPENABLE); // Filter to show only images, using the image MIME data type. // If one wanted to search for ogg vorbis files, the type would be "audio/ogg". // To search for all documents available via installed storage providers, // it would be "*/*". intent.setType("image/*"); startActivityForResult(intent, READ_REQUEST_CODE); }
Observe o seguinte:
Quando o usuário seleciona um documento no seletor, {@link android.app.Activity#onActivityResult onActivityResult()} é chamado. A URI direcionada ao documento selecionado está presente no parâmetro {@code resultData}. Extraia a URI usando {@link android.content.Intent#getData getData()}. Depois, será possível usá-la para recuperar o documento que o usuário deseja. Por exemplo:
@Override public void onActivityResult(int requestCode, int resultCode, Intent resultData) { // The ACTION_OPEN_DOCUMENT intent was sent with the request code // READ_REQUEST_CODE. If the request code seen here doesn't match, it's the // response to some other intent, and the code below shouldn't run at all. if (requestCode == READ_REQUEST_CODE && resultCode == Activity.RESULT_OK) { // The document selected by the user won't be returned in the intent. // Instead, a URI to that document will be contained in the return intent // provided to this method as a parameter. // Pull that URI using resultData.getData(). Uri uri = null; if (resultData != null) { uri = resultData.getData(); Log.i(TAG, "Uri: " + uri.toString()); showImage(uri); } } }
Quando tiver a URI de um documento, você terá acesso aos seus metadados. Este fragmento apanha os metadados de um documento especificado pela URI e registra-os:
public void dumpImageMetaData(Uri uri) { // The query, since it only applies to a single document, will only return // one row. There's no need to filter, sort, or select fields, since 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 since an // int can't be null in Java, the behavior is implementation-specific, // which is just a fancy term for "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(); } }
Assim que tiver a URI de um documento, você poderá abri-lo ou fazer o que quiser com ele.
A seguir há um exemplo de como abrir um {@link android.graphics.Bitmap}:
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; }
Observe que não se deve realizar essa operação no encadeamento da IU. Faça isso em segundo plano usando {@link android.os.AsyncTask}. Assim que abrir o bitmap, você poderá exibi-lo em uma {@link android.widget.ImageView}.
Abaixo há um exemplo de como obter uma {@link java.io.InputStream} dessa URI. Neste fragmento, as linhas do arquivo estão sendo lidas em uma string:
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(); }
O aplicativo pode criar um novo documento em um provedor de documentos usando a intenção {@link android.content.Intent#ACTION_CREATE_DOCUMENT}. Para criar um arquivo, deve-se fornecer um tipo MIME e um nome para o arquivo à intenção e ativá-la com um código de solicitação exclusivo. O resto você não precisa fazer:
// Here are some examples of how you might call this method. // The first parameter is the MIME type, and the second parameter is the name // of the file you are creating: // // createFile("text/plain", "foobar.txt"); // createFile("image/png", "mypicture.png"); // Unique request code. private static final int WRITE_REQUEST_CODE = 43; ... private void createFile(String mimeType, String fileName) { Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); // Filter to only show results that can be "opened", such as // a file (as opposed to a list of contacts or timezones). intent.addCategory(Intent.CATEGORY_OPENABLE); // Create a file with the requested MIME type. intent.setType(mimeType); intent.putExtra(Intent.EXTRA_TITLE, fileName); startActivityForResult(intent, WRITE_REQUEST_CODE); }
Ao criar um novo documento, é possível obter sua URI em {@link android.app.Activity#onActivityResult onActivityResult()} para que seja possível continuar a gravar nele.
Se você tem uma URI de um documento e os {@link android.provider.DocumentsContract.Document#COLUMN_FLAGS Document.COLUMN_FLAGS} do documento contêm {@link android.provider.DocumentsContract.Document#FLAG_SUPPORTS_DELETE SUPPORTS_DELETE}, você pode excluir o documento. Por exemplo:
DocumentsContract.deleteDocument(getContentResolver(), uri);
Você pode usar a SAF para editar o documento de texto no local em que está armazenado. Esse fragmento dispara a intenção {@link android.content.Intent#ACTION_OPEN_DOCUMENT} e usa a categoria {@link android.content.Intent#CATEGORY_OPENABLE} para exibir somente documentos que possam ser abertos. Ela filtra ainda mais para exibir somente arquivos de texto:
private static final int EDIT_REQUEST_CODE = 44; /** * Open a file for writing and append some text to it. */ private void editDocument() { // ACTION_OPEN_DOCUMENT is the intent to choose a file via the system's // file browser. Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); // Filter to only show results that can be "opened", such as a // file (as opposed to a list of contacts or timezones). intent.addCategory(Intent.CATEGORY_OPENABLE); // Filter to show only text files. intent.setType("text/plain"); startActivityForResult(intent, EDIT_REQUEST_CODE); }
Em seguida, de {@link android.app.Activity#onActivityResult onActivityResult()} (consulte Processar resultados), você pode chamar o código para realizar a edição. O fragmento a seguir obtém um {@link java.io.FileOutputStream} do {@link android.content.ContentResolver}. Por padrão, ele usa o modo de "gravação". Recomenda-se solicitar a menor quantidade de acesso necessária, portanto não solicite acesso de leitura e programação se só for necessária a programação:
private void alterDocument(Uri uri) { try { ParcelFileDescriptor pfd = getActivity().getContentResolver(). openFileDescriptor(uri, "w"); FileOutputStream fileOutputStream = new FileOutputStream(pfd.getFileDescriptor()); fileOutputStream.write(("Overwritten by MyCloud 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(); } }
Quando o aplicativo abre um arquivo para leitura ou programação, o sistema lhe fornece uma concessão da permissão da URI para tal arquivo. Ela vigora até a reinicialização do dispositivo do usuário. Contudo, suponhamos que seu aplicativo seja de edição de imagens e você queira que os usuários acessem as últimas 5 imagens que editaram diretamente do aplicativo. Se o dispositivo do usuário reiniciou, você teria que enviar o usuário de volta ao seletor do sistema para encontrar os arquivos, o que, obviamente, não é o ideal.
Para evitar que isso aconteça, você pode manter as permissões que o sistema forneceu ao aplicativo. Efetivamente, o aplicativo "toma" a concessão de permissão da URI persistente que o sistema está oferecendo. Isso concede ao usuário um acesso contínuo aos arquivos por meio do aplicativo mesmo se o dispositivo for reiniciado:
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);
Há uma etapa final. Você pode ter salvo as URIs mais recentes acessadas pelo seu aplicativo, mas elas não serão mais válidas — outro aplicativo pode ter excluído ou modificado um documento. Portanto, deve-se sempre chamar {@code getContentResolver().takePersistableUriPermission()} para verificar se há dados mais recentes.
Se você está desenvolvimento um aplicativo que fornece serviços de armazenamento para arquivos (como um serviço de armazenamento em nuvem), é possível disponibilizar os arquivos pela SAF criando um provedor de documentos personalizado. Esta seção mostra como fazê-lo.
Para implementar um provedor de documentos personalizado, adicione ao manifesto do aplicativo:
<provider>
que declare o provedor de armazenamento
personalizado. com.example.android.storageprovider.MyCloudProvider
.com.example.android.storageprovider
) e o tipo de provedor de conteúdo
(documents
). Por exemplo: {@code com.example.android.storageprovider.documents}.android:exported
definido como "true"
.
É necessário exportar o provedor para que outros aplicativos possam vê-lo.android:grantUriPermissions
definido como
"true"
. Essa configuração permite ao sistema conceder acesso ao conteúdo
no provedor a outros aplicativos. Para ver como manter uma concessão
para determinado documento, consulte Manutenção de permissões.<bool name="atLeastKitKat">false</bool>
<bool name="atLeastKitKat">true</bool>
Abaixo há alguns excertos de amostra de um manifesto que contém um provedor:
<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>
A intenção {@link android.content.Intent#ACTION_OPEN_DOCUMENT} está disponível somente em dispositivos que executam o Android 4.4 ou posteriores. Se você deseja que o aplicativo seja compatível com {@link android.content.Intent#ACTION_GET_CONTENT} para adaptar-se a dispositivos que executam o Android 4.3 ou versões anteriores, é necessário desativar o filtro de intenção {@link android.content.Intent#ACTION_GET_CONTENT} no manifesto para dispositivos que executam Android 4.4 ou versões posteriores. Um provedor de documentos e {@link android.content.Intent#ACTION_GET_CONTENT} devem ser avaliados de forma mutuamente exclusiva. Se houver compatibilidade com ambos simultaneamente, o aplicativo aparecerá duas vezes na IU do seletor do sistema, oferecendo dois meios de acesso diferentes aos dados armazenados. Isso pode confundir os usuários.
A seguir apresenta-se a forma recomendada de desativar o filtro de intenções {@link android.content.Intent#ACTION_GET_CONTENT} para dispositivos que executam o Android 4.4 ou versões posteriores:
<bool name="atMostJellyBeanMR2">true</bool>
<bool name="atMostJellyBeanMR2">false</bool>
<!-- This activity alias is added so that GET_CONTENT intent-filter can be disabled for builds on API level 19 and higher. --> <activity-alias android:name="com.android.example.app.MyPicker" android:targetActivity="com.android.example.app.MyActivity" ... android:enabled="@bool/atMostJellyBeanMR2"> <intent-filter> <action android:name="android.intent.action.GET_CONTENT" /> <category android:name="android.intent.category.OPENABLE" /> <category android:name="android.intent.category.DEFAULT" /> <data android:mimeType="image/*" /> <data android:mimeType="video/*" /> </intent-filter> </activity-alias>
Normalmente, ao criar um provedor de conteúdo personalizado, uma das tarefas é implementar classes de contrato, como descrito no guia dos desenvolvedores de Provedores de conteúdo. Classe de contrato é uma classe {@code public final} que contém definições constantes das URIs, nomes de coluna, tipos MIME e outros metadados que pertencem ao provedor. A SAF fornece essas classes de contrato, portanto não é necessário programá-las:
Por exemplo, eis as colunas que podem retornar em um cursor ao consultar documentos ou a raiz do provedor de documentos:
private static final String[] DEFAULT_ROOT_PROJECTION = new String[]{Root.COLUMN_ROOT_ID, Root.COLUMN_MIME_TYPES, Root.COLUMN_FLAGS, Root.COLUMN_ICON, Root.COLUMN_TITLE, Root.COLUMN_SUMMARY, Root.COLUMN_DOCUMENT_ID, Root.COLUMN_AVAILABLE_BYTES,}; private static final String[] DEFAULT_DOCUMENT_PROJECTION = new String[]{Document.COLUMN_DOCUMENT_ID, Document.COLUMN_MIME_TYPE, Document.COLUMN_DISPLAY_NAME, Document.COLUMN_LAST_MODIFIED, Document.COLUMN_FLAGS, Document.COLUMN_SIZE,};
A próxima etapa na criação de um provedor de documentos personalizado é atribuir uma subclasse à classe {@link android.provider.DocumentsProvider} abstrata. No mínimo, é necessário implementar os seguintes métodos:
Esses são os únicos métodos que obrigatoriamente devem ser implementados, mas há muitos outros que podem ser usados. Consulte {@link android.provider.DocumentsProvider} para ver mais detalhes.
A implementação de {@link android.provider.DocumentsProvider#queryRoots queryRoots()} deve retornar um {@link android.database.Cursor} que aponta para todos os diretórios raiz dos provedores de documentos, usando colunas definidas em {@link android.provider.DocumentsContract.Root}.
No fragmento a seguir, o parâmetro {@code projection} representa os campos específicos que o autor da chamada quer receber de volta. O fragmento cria um novo cursor e adiciona-lhe uma linha — uma raiz, um diretório de nível superior, como Downloads ou Imagens. A maioria dos provedores tem somente uma raiz. É possível ter mais de uma, por exemplo, no caso de diversas contas de usuário. Nesse caso, adicione somente uma segunda linha ao cursor.
@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; }
A implementação de {@link android.provider.DocumentsProvider#queryChildDocuments queryChildDocuments()} deve retornar um {@link android.database.Cursor} que aponta para todos os arquivos no diretório especificado com colunas definidas em {@link android.provider.DocumentsContract.Document}.
Esse método é chamado quando uma raiz do aplicativo é escolhida na IU do seletor. Ele coleta os documentos filhos de um diretório na raiz. Ele pode ser chamado em qualquer nível na hierarquia de arquivos, não somente na raiz. Esse fragmento cria um novo cursor com as colunas solicitadas e, em seguida, adiciona informações ao cursor sobre cada filho imediato no diretório pai. O filho pode ser uma imagem, outro diretório — qualquer arquivo:
@Override 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; }
A implementação de {@link android.provider.DocumentsProvider#queryDocument queryDocument()} deve retornar um {@link android.database.Cursor} que aponta para o arquivo especificado com colunas definidas em {@link android.provider.DocumentsContract.Document}.
O método {@link android.provider.DocumentsProvider#queryDocument queryDocument()} retorna as mesmas informações passadas em {@link android.provider.DocumentsProvider#queryChildDocuments queryChildDocuments()}, mas para um arquivo específico.
@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; }
Deve-se implementar {@link android.provider.DocumentsProvider#openDocument openDocument()} para retornar um {@link android.os.ParcelFileDescriptor} que represente o arquivo especificado. Outros aplicativos podem usar o {@link android.os.ParcelFileDescriptor} retornado para transmitir dados. O sistema chama esse método quando o usuário seleciona um arquivo e o aplicativo cliente solicita acesso a ele chamando {@link android.content.ContentResolver#openFileDescriptor openFileDescriptor()}. Por exemplo:
@Override public ParcelFileDescriptor openDocument(final String documentId, final String mode, CancellationSignal signal) throws FileNotFoundException { Log.v(TAG, "openDocument, mode: " + mode); // It's OK to do network operations in this method to download the document, // as long as you periodically check the CancellationSignal. If you have an // extremely large file to transfer from the network, a better solution may // be pipes or sockets (see ParcelFileDescriptor for helper methods). final File file = getFileForDocId(documentId); final boolean isWrite = (mode.indexOf('w') != -1); if(isWrite) { // Attach a close listener if the document is opened in write mode. try { Handler handler = new Handler(getContext().getMainLooper()); return ParcelFileDescriptor.open(file, accessMode, handler, new ParcelFileDescriptor.OnCloseListener() { @Override public void onClose(IOException e) { // Update the file with the cloud server. The client is done // writing. Log.i(TAG, "A file with id " + documentId + " has been closed! Time to " + "update the server."); } }); } catch (IOException e) { throw new FileNotFoundException("Failed to open document with id " + documentId + " and mode " + mode); } } else { return ParcelFileDescriptor.open(file, accessMode); } }
Suponha que o provedor de documentos seja um serviço de armazenamento em nuvem protegido por senha e que você queira certificar-se de que os usuários estejam conectados antes de iniciar o compartilhamento dos arquivos. O que o aplicativo deve fazer se o usuário não estiver conectado? A solução é retornar zero raiz na implementação de {@link android.provider.DocumentsProvider#queryRoots queryRoots()}, ou seja, um cursor de raiz vazio:
public Cursor queryRoots(String[] projection) throws FileNotFoundException { ... // If user is not logged in, return an empty root cursor. This removes our // provider from the list entirely. if (!isUserLoggedIn()) { return result; }
A outra etapa é chamar {@code getContentResolver().notifyChange()}. Lembra-se do {@link android.provider.DocumentsContract}? Estamos usando-o para criar esta URI. O fragmento a seguir pede ao sistema que consulte as raízes do provedor de documentos sempre que o status de login do usuário mudar. Se o usuário não estiver conectado, uma chamada de {@link android.provider.DocumentsProvider#queryRoots queryRoots()} retornará um cursor vazio, como exibido anteriormente. Isso garante que os documentos do provedor estejam disponíveis somente se o usuário tiver acesso ao provedor.
private void onLoginButtonClick() { loginOrLogout(); getContentResolver().notifyChange(DocumentsContract .buildRootsUri(AUTHORITY), null); }