- Sharing Content with Intents
- Building a DocumentsProvider
- What is a DocumentsProvider anyways?
- It all starts at the root
- Working with Cursors in a DocumentsProvider
- Dynamic Roots
- Onto the documents
- Loading from the network
- Recents and Search
- Getting to the meat of a document: the bytes!
- Providing Thumbnails
- Virtual Files
- Alternate File Formats
- Going beyond ACTION_GET_CONTENT
- ACTION_OPEN_DOCUMENT_TREE
- ACTION_CREATE_DOCUMENT
- Document Management
- DocumentsProvider: storage for the modern era
Sharing Content with Intents
Intents allow us to communicate data between Android apps and implicit intents can also accept actions. One of those actions is the ACTION_SEND command which indicates we want to send data across apps. To send data, all you need to do is specify the data and its type, and the system will identify compatible receiving activities and display them to the user.
Sending and receiving data between applications with intents is most commonly used for social sharing of content. Intents allow users to share information quickly and easily, using their favorite applications.
You can send content by invoking an implicit intent with ACTION_SEND .
To send images or binary data:
Sending URL links should simply use text/plain type:
In certain cases, we might want to send an image along with text. This can be done with:
Sharing multiple images can be done with:
See this stackoverflow post for more details.
Note: Facebook does not properly recognize multiple shared elements. See this facebook specific bug for more details and share using their SDK.
Facebook doesn’t work well with normal sharing intents when sharing multiple content elements as discussed in this bug. To share posts with facebook, we need to:
- Create a new Facebook app here (follow the instructions)
- Add the Facebook SDK to your Android project
- Share using this code snippet:
You may want to send an image that were loaded from a remote URL. Assuming you are using a third party library like Glide, here is how you might share an image that came from the network and was loaded into an ImageView. There are two ways to accomplish this. The first way, shown below, takes the bitmap from the view and loads it into a file.
and then later assuming after the image has completed loading, this is how you can trigger a share:
Make sure to setup the «SD Card» within the emulator device settings:
Note that if you are using API 24 or above, see the section below on using a FileProvider to work around new file restrictions.
If you are targeting Android API 24 or higher, private File URI resources (file:///) cannot be shared. You must instead wrap the File object as a content provider (content://) using the FileProvider class.
First, you must declare this FileProvider in your AndroidManifest.xml file within the tag:
Next, create a resource directory called xml and create a fileprovider.xml . Assuming you wish to grant access to the application’s specific external storage directory, which requires requesting no additional permissions, you can declare this line as follows:
Finally, you will convert the File object into a content provider using the FileProvider class:
If you see a INSTALL_FAILED_CONFLICTING_PROVIDER error when attempting to run the app, change the string com.codepath.fileprovider in your Java and XML files to something more unique, such as com.codepath.fileprovider.YOUR_APP_NAME_HERE .
Note that there are other XML tags you can use in the fileprovider.xml , which map to the File directory specified. In the example above, we use Context.getExternalFilesDir(Environment.DIRECTORY_PICTURES) , which corresponded to the XML tag in the declaration with the Pictures path explicitly specified. Here are all the options you can use too:
XML tag | Corresponding storage call | When to use |
---|---|---|
Context.getFilesDir() | data can only be viewed by app, deleted when uninstalled ( /data/data/[packagename]/files ) | |
Context.getExternalFilesDir() | data can be read/write by the app, any apps granted with READ_STORAGE permission can read too, deleted when uninstalled ( /Android/data/[packagename]/files ) | |
Context.getCacheDir() | temporary file storage | |
Environment.getExternalStoragePublicDirectory() | data can be read/write by the app, any apps can view, files not deleted when uninstalled | |
Context.getExternalCacheDir() | temporary file storage with usually larger space |
If you are using API 23 or above, then you’ll need to request runtime permissions for Manifest.permission.READ_EXTERNAL_STORAGE and Manifest.permission.WRITE_EXTERNAL_STORAGE in order to share the image as shown above since newer versions require explicit permisions at runtime for accessing external storage.
Note: There is a common bug on emulators that will cause MediaStore.Images.Media.insertImage to fail with E/MediaStore﹕ Failed to insert image unless the media directory is first initialized as described in the link.
This is how you can easily use an ActionBar share icon to activate a ShareIntent. The below focuses on the support ShareActionProvider for use with AppCompatActivity .
Note: This is an alternative to using a sharing intent as described in the previous section. You either can use a sharing intent or the provider as described below.
First, we need to add an ActionBar menu item in res/menu/ in the XML specifying the ShareActionProvider class.
Next, get access to share provider menu item in the Activity so we can attach the share intent later:
Note: ShareActionProvider does not respond to onOptionsItemSelected() events, so you set the share action provider as soon as it is possible.
Now, once you’ve setup the ShareActionProvider menu item, construct and attach the share intent for the provider but only after image has been loaded as shown below using the RequestListener for Glide .
Note: Be sure to call attachShareIntentAction method both inside onCreateOptionsMenu AND inside the onResourceReady for Glide to ensure that the share attaches properly.
We can use a similar approach if we wish to create a share action for the current URL that is being loaded in a WebView:
Check out the official guide for easy sharing for more information.
Источник
Building a DocumentsProvider
One of the Android’s strengths has always been its intent system: rather than whitelist only specific apps your app works with, you can rely on common intents that define standard actions that apps can register to handle. One of these common actions is retrieving a specific type of file, often implemented with ACTION_GET_CONTENT.
Prior to KitKat, this meant building an Activity that has an ACTION_GET_CONTENT intent filter, writing a UI that allows selecting files managed by your app, and handle a myriad of flags like multi-select and local only. And then users had to learn each app’s UI. The wild west of file selection. So in KitKat, we introduced a new standard with the Storage Access Framework and the class that underlies the entire system: DocumentsProvider.
This allows users to have a single standard user interface to access files from any app — whether they are local files from the included local storage DocumentsProvider or from a custom DocumentsProvider you build.
What is a DocumentsProvider anyways?
A key distinction between the old system and the Storage Access Framework is that the UI is provided by the system, not directly by your app.
A DocumentsProvider then has a single focus: providing the information needed to populate that UI with the directories and files (collectively known as ‘documents’) managed by your app.
You might have guessed it, but a DocumentsProvider extends ContentProvider — one of the high level components available on Android that are particularly well suited for allowing other apps (or, in this case, the system) to read information from your app and provide access to files you own.
And just like any ContentProvider, that means your DocumentsProvider needs to be registered in your manifest:
You’ll note the authorities attribute — this need to be a unique string you can think of as a prefix for all of the URIs that your DocumentsProvider builds. We’ll be referring to this in code as well, so it sometimes makes more sense to do some Gradle magic to ensure they always stay in sync:
Now we can update our provider to use android:authorities=”$
Note: applicationId will be null in a library module so this technique only works if your DocumentsProvider is within an application module.
Thankfully, DocumentsProvider takes care of the high level ContentProvider APIs, giving you a very document specific API to implement.
It all starts at the root
And that document specific API starts with queryRoots(). A ‘root’ is the topmost entry that appears in the Documents UI and includes information such as a unique root ID, the root’s ‘display name’ (the user visible name), an icon, an optional summary, and most critically the document ID of the topmost directory for that root (which is what it’ll use to actually enumerate the rest of your content).
For most apps, this is pretty straightforward — you’d have one root for your app, but this doesn’t necessarily need to be the case. For example, the UsbDocProvider in the above diagram has a root for each connected USB drive (which in practice means most of the time it has no Roots at all and doesn’t appear in the list — as you’d expect). You could also consider the case where you support multiple accounts: you can and should have a separate root for each user account.
Working with Cursors in a DocumentsProvider
When you actually look at the full definition of queryRoots(), you’re met with two concepts right from the start: a Cursor and a projection. These are common terms when it comes to working with databases (and actually the basis behind a ContentProvider), but that doesn’t mean you need to know the details of databases to write a DocumentsProvider.
So just like a traditional database has a number of rows with each row made up of a number of columns, queryRoots() is expecting a Cursor that has a row for each root you want to return with each root having a number of columns representing the various bits of information. The projection that is passed in is an array of which columns are being requested or null if you get to pick what columns you want to return.
In the case of a document root, the valid columns are found in DocumentsContract.Root — this ‘contract’ is what the system and your DocumentsProvider need to agree on. In the Root’s case, there are a number of required columns:
- COLUMN_ROOT_ID — a unique String defining the root. As long as this is unique within your app, it can be anything you want
- COLUMN_ICON — a resource ID of an icon to display for the root. Ideally this should be something branded such that it is clear what app the root is associated with
- COLUMN_TITLE — the title of the root — this should be a user friendly name (keep in mind there’s a separate, optional COLUMN_SUMMARY for things like an account name)
- COLUMN_FLAGS — an integer representing what optional behavior your root supports such as whether it represents local only data, or if you support creating new files, sorting by recency, or searching. If you don’t support anything, this can just be 0
- COLUMN_DOCUMENT_ID — a String for the topmost directory of the root — this is how the Storage Access Framework is going to start exploring your root once someone selects it
So it makes sense to build a default root projection that includes at least these fields:
Now you can use a MatrixCursor to manually build a Cursor with the required columns:
And for each root you want to add, call newRow():
Don’t worry about checking whether the projection includes each column you want to add — they’ll be ignored if they aren’t needed.
Dynamic Roots
If you’re doing anything more than a static set of roots, it is critical that the document UI stay in sync — the user shouldn’t have to see an already disconnected USB device or a signed out account. Thankfully, being built on a ContentProvider gives us a pre-built mechanism for notifying listeners of changes via notifyChange():
Onto the documents
Once the user sees and selects your root, you’ll want to actually return some documents from queryChildDocuments(). This may take the form as more directories (which would then be recursively explored by the user) or files at that level in the hierarchy.
Just like queryRoots(), this takes a projection and returns a Cursor. These function the same, but will use the columns defined in DocumentsContract.Document, which also has a number of required columns:
- COLUMN_DOCUMENT_ID — a unique String that identifies this document
- COLUMN_DISPLAY_NAME — the user visible name of the document
- COLUMN_MIME_TYPE — the MIME type of the document e.g., “image/png” or “application/pdf” — use MIME_TYPE_DIR to represent a directory
- COLUMN_FLAGS — an integer representing what optional behavior this specific document supports.
- COLUMN_SIZE — a long representing the size of the document in bytes (if you don’t know, you can add null)
- COLUMN_LAST_MODIFIED — the last modified date in milliseconds (if you don’t know, you can add null)
And the same MatrixCursor based approach works here as well.
Keep in mind that the COLUMN_DOCUMENT_ID must be a uniquely describe a single document, a document can have more than one parent — it is completely valid to have the same document appear in multiple places in your directory structure. For example, your DocumentsProvider could have one branch of its directories sort images by user defined tags and another by year — the same image might appear until multiple tags as well as under a year directory.
While queryChildDocuments() is the primary method that’ll be called when the user is exploring your DocumentsProvider, you must also implement queryDocument() — this method should return the exact same metadata about just a single document as you would have returned in queryChildDocuments(). (This is a good opportunity to dedup your code and have one set of code that builds a row for both queryChildDocuments() and queryDocument()).
Loading from the network
Of course, if you’re working with user’s files, one user will eventually have a directory of tens of thousands of files. If you’re loading the file metadata across the network, the user might be sitting there for quite some time. Instead of loading the entire set in one chunk, a DocumentsProvider allows you to specify EXTRA_LOADING to indicate that there’s more documents coming:
Yes, you need to extend your Cursor class to override getExtras(). (There’s also EXTRA_INFO and EXTRA_ERROR which you may find useful in displaying information to the user using this same technique).
Then we can use the same notifyChange() based approach as used for dynamic roots, but here we need to set a specific notification Uri on our Cursor (as there are many Cursors when it comes to documents):
Recents and Search
Two of the optional flags you can include when returning your root are FLAG_SUPPORTS_RECENTS and FLAG_SUPPORTS_SEARCH. These flags denote that users can get a list of recently modified documents and search the root, respectively. In both cases, you’ll use the same technique as queryChildDocuments().
Recent documents are returned via queryRecentDocuments() and should be limited to at most 64 documents in descending order of COLUMN_LAST_MODIFIED. Documents you return here will be combined with others under the system provided ‘Recent’ root, allowing users to get an overview of their most recent documents across all installed DocumentsProviders.
Note: Recent documents does not support notifyChange(), unlike queryChildDocuments() and querySearchDocuments() given that the results are combined across many DocumentProviders by default (too much shifting around if they were all updated at different times!).
Searching a root requires that you implement querySearchDocuments(). Here you’re given a specific query string and need to return the most relevant documents. While at a minimum this should attempt to match the COLUMN_DISPLAY_NAME (case insensitive), but it could certainly look at other metadata — tags on an file, OCR of images, etc. Just make sure it is actually returning relevant results!
Getting to the meat of a document: the bytes!
In most cases, the reason the user is going through this whole process to actually select and open a file. So it makes sense that the last method you must implement is openDocument() — how you actually provide the raw bytes of the document.
Here, you’re tasked to return a ParcelFileDescriptor — a bit more than your standard OutputStream, sure, but surprisingly a bit more flexible, supporting both reading and writing. If you already have a local file representing your document, this becomes a rather trivial method to write:
Note: if you’re syncing the file elsewhere and need to know when the file was closed, consider using the open() call that takes an OnCloseListener to know exactly when the other app is done writing to the file — be sure to check the IOException to know if the remote side actually succeeded or ran into an error.
Of course, if you have a more complicated structure or are streaming the file, you’ll want to look at createReliablePipe() or createReliableSocketPair() which allow you to create a pair of ParcelFileDescriptors where you’d return one of them and send data across the other via a AutoCloseOutputStream (for sending data) or AutoCloseInputStream (for receiving). These cases wouldn’t support the “rw” read+write state — that case assumes random access and a local file.
Note: if the CancellationSignal is not null, you should check its isCanceled() method occasionally to abandon long running operations.
Providing Thumbnails
By default, each document uses a default icon based on its mime type. This can be overridden by providing a custom icon by including the COLUMN_ICON, but for documents like images or videos (or even documents/PDFs), a thumbnail can make all the difference in letting the user figure out which is the correct document to select.
When you add FLAG_SUPPORTS_THUMBNAIL to a document, the system will call openDocumentThumbnail(), passing in a size hint — a suggested size for the thumbnail (as mentioned, the image should never be more than double the hinted size). Since these are going to be visible as part of the browsing process, caching these thumbnails (say, in getCacheDir()) is strongly recommended.
Note: you’ll find that the AssetFileDescriptor can be created from a ParcelFileDescriptor by using new AssetFileDescriptor(parcelFileDescriptor, 0, AssetFileDescriptor.UNKNOWN_LENGTH)
And if you have a whole directory of documents that support thumbnails, your users will probably appreciate it if you set FLAG_DIR_PREFERS_GRID on the parent directory to get larger thumbnails by default.
Virtual Files
While it makes sense that documents with MIME_TYPE_DIR aren’t openable (they are directories after all!), there’s another class of documents that aren’t actually directly openable — these are called virtual files. New to Android Nougat and API 24, a virtual file is denoted by the FLAG_VIRTUAL_DOCUMENT. These files won’t be selectable when apps include the CATEGORY_OPENABLE category in their intent and won’t ever call openDocument().
Why include them at all then? Well, the ACTION_VIEW intent sent when a user clicks on the file in the included file explorer (Settings->Storage->Explore on Nexus devices) will still work with these files, allowing the user to open the files within your own app.
Virtual files also particularly benefit from one of the other features added in API 24 — alternate file formats. This allows virtual files to have alternate openable file export formats (such as a PDF file for a cloud document).
Alternate File Formats
There’s an implicit connection between the COLUMN_MIME_TYPE you return and what you return in openDocument() — if you say you’re an “image/png”, you had better be delivering a PNG file. On API 24+ devices there’s an additional option though: allowing apps to access your document through alternate mime types. For example, you might be providing “image/svg+xml” files and want to also allow apps to use a fixed resolution “image/png” given the lack of native SVG parsing which would normally make an image/svg+xml file less useful.
Here, your DocumentsProvider can implement getDocumentStreamTypes() to return a full list of mime types that match the given mime type filter (e.g., “image/*” or “*/*”) that are supported by the given document id. Keep in mind that you should include your default mime type if it represents an openable mime type.
Then when clients use openTypedAssetFileDescriptor() with one of those mime types, that’ll trigger a call to openTypedDocument(), which is the mime type equivalent to openDocument().
Going beyond ACTION_GET_CONTENT
Much of what we’ve talked about is in building the best experience on KitKat devices for client apps using ACTION_GET_CONTENT and while clients can use DocumentsContract.isDocumentUri() to determine if they’re actually receiving a document Uri (a good way of conditionally using the more advanced functionality provided), by implementing a DocumentsProvider you’ll also allow clients to use ACTION_OPEN_DOCUMENT, which ensures that all Uris returned are document Uris as well as allow persistent document access). There are two other actions which your DocumentsProvider can optionally handle: ACTION_OPEN_DOCUMENT_TREE and ACTION_CREATE_DOCUMENT.
Note: If your app contains a DocumentsProvider and also persists URIs returned from ACTION_OPEN_DOCUMENT, ACTION_OPEN_DOCUMENT_TREE, or ACTION_CREATE_DOCUMENT, be aware that you won’t be able to persist access to your own URIs via takePersistableUriPermission() — despite it failing with a SecurityException, you’ll always have access to URIs from your own app. You can add the boolean EXTRA_EXCLUDE_SELF to your Intents if you want to hide your own DocumentsProvider(s) on API 23+ devices for any of these actions.
ACTION_OPEN_DOCUMENT_TREE
While ACTION_GET_CONTENT and ACTION_OPEN_DOCUMENT focus on providing access to one or more individual documents, ACTION_OPEN_DOCUMENT_TREE was added in API 21 to allow a user to select an entire directory, giving the other app persistent access to the entire directory.
Supporting ACTION_OPEN_DOCUMENT_TREE involves adding FLAG_SUPPORTS_IS_CHILD to your root and implementing isChildDocument(). This allows the framework to confirm that the given document ID is part of a certain document tree: remember a document can be in multiple places in your hierarchy so this is checking ‘downward’ as it were from parent to potential descendant (child, grandchild, etc).
Ideally, this request shouldn’t rely on the network as it can be called frequently and in rapid succession so if you want to support this use case, make sure you can handle this request locally.
ACTION_CREATE_DOCUMENT
If ACTION_OPEN_DOCUMENT is the ‘open file’ of a traditional operating system, ACTION_CREATE_DOCUMENT is the ‘save file’ dialog and allows clients to create entirely new documents within any root that has FLAG_SUPPORTS_CREATE in directories that have FLAG_DIR_SUPPORTS_CREATE (think of the root flag as a flag on whether your root should appear at all in the UI for ACTION_CREATE_DOCUMENT, rather than a blanket grant to create documents anywhere).
When the user selects a directory to place the new document, you’ll receive a call to createDocument() with the parent document ID of the selected directory, the mime type specified by the client app, and a display name (ideally, what you should set as the COLUMN_DISPLAY_NAME, but you can certainly edit it, add an extension, etc. if needed). All you have to do is generate a new COLUMN_DOCUMENT_ID which the client app can then use to call openDocument() to actually write the contents of the document.
Document Management
While the core experience will always be driven by reading, writing, and creating documents, a traditional file manager has quite a few more features and many of them are supported when building your DocumentsProvider, allowing the system UI to offer additional functionality for those wanting to browse or manage your documents.
Each functionality has a flag you need to add to document’s COLUMN_FLAGS and an associated method you need to implement to perform the operation:
- FLAG_SUPPORTS_DELETE (API 19) to delete the document entirely. Implement deleteDocument().
- FLAG_SUPPORTS_RENAME (API 21) to rename the document (changing its COLUMN_DISPLAY_NAME and optionally its COLUMN_DOCUMENT_ID). Implement renameDocument().
- FLAG_SUPPORTS_COPY (API 24) to create a copy of the document under a new parent directory within your DocumentsProvider. Implement copyDocument().
- FLAG_SUPPORTS_MOVE (API 24) to move a document from an existing parent directory to a new parent directory within your DocumentsProvider. Implement moveDocument().
- FLAG_SUPPORTS_REMOVE (API 24) to remove a document from its parent directory. Implement removeDocument().
Keep in mind the subtle difference between delete and remove: remove is focused more on the relationship between a document and its parent directory. In the case where you only have one parent of the document, a single remove would orphan the document and you’d usually want to trigger the same code as delete (since there’d be no way to navigate to that document any more). But in the case of multiple parent directories for a single document, delete would affect every parent while remove would be a local operation only affecting a single parent directory.
When it comes to operations that can remove existing document IDs (such as delete, move, and removing from the last parent), make sure you call revokeDocumentPermission() — this is what tells the system that the document ID is no longer valid and access should be revoked from all other apps.
DocumentsProvider: storage for the modern era
Local storage is certainly still incredibly important and that’s why the system provides DocumentsProviders for internal storage, SD cards, and USB attached storage devices. With the Storage Access Framework, your app and the data you store on behalf of the user is now just as accessible as if it was right there on the device with them.
Источник