IAN LAKE: If you've implemented a get content activity
so other apps could access files or photos from your app,
your life got a lot easier in KitKat.
Rather than having to implement your own activity,
build a whole U/I, and deal with options like multi-select,
you can rely on the Storage Access Framework
and build a DocumentsProvider.
A quick refresher on the Storage Access Framework.
On KitKat and higher devices, when
apps start a get content intent, they'll
first get a system-provided U/I, courtesy of the Storage Access
Framework.
This U/I automatically connects to each app
that provides a DocumentsProvider
and gives users a consistent look
and feel and functionality across all those apps.
Think of it as the open dialog from a traditional operating
system.
Only at the end of that list of apps
does any app use a get content activity.
These document providers are like the data backends
for your app.
With no U/I component, it's completely
focused on returning a list of files and some meta data
to make available to the user to select right
there in the system-provided U/I,
even allowing users to select files
from multiple DocumentsProviders at the same time.
There's only four required methods for implementing
DocumentsProvider-- queryRoots(),
queryChildDocuments(), queryDocument(),
and openDocument().
A root is the entry point to your DocumentsProvider,
so queryRoots() is where you'd return metadata,
like a unique root ID; the title and icon for your root
for display to the user; an optional summary for maybe
an account name; as well as flags for enabling optional
functionality, such as returning recent items, searching,
or creating new files.
It also includes a document ID for the topmost directory.
It turns out when we say "document,"
it can mean two different things-- a traditional file--
something the user can open-- and a directory, a folder which
can contain other documents.
queryChildDocuments() is where you're given
a directory's document ID, and you return metadata about all
of its direct children of that directory.
This metadata would include document IDs; a display name--
what gets shown to the user-- a MIME type,
and flags indicating whether it could be written, deleted,
renamed, and more.
You might already be guessing what queryDocument() does then.
It provides the same metadata, just about a single document.
It's important to note that while each document ID must
be unique, the same document can appear
in different directories, say if you categorize pictures
by both tags and date.
Then, of course, you do actually need to provide
the document's contents, and that's what openDocument()
does.
Now, it seems easy enough.
Yet when you look at the actual API,
you note that many of these methods return a cursor.
And many have a projection as a parameter.
For those not working on databases all day,
a cursor is just an interface for iterating over rows of data
with each row having a set of columns.
The projection that's passed in indicates
what columns are being requested by the Storage Access
Framework.
And your resulting cursor should have a row
for each root or document.
If you don't have a database, that's totally fine.
You can construct a cursor manually using a matrix cursor.
You create one by passing in the projection you want to use.
You'll note that a null projection parameter means
you decide what fields to return,
although there are some required ones for DocumentsProviders,
so look through the links in the description for the details.
Then for each row you want, you first create it using newRow,
then call add.
For queryRoots(), use the constants in the Documents
Contract Root class as the keys for each piece of data.
And queryChildDocuments() and queryDocuments() aren't much
different, although in that case you'd use the Documents
Contract Document Class for the constants.
Now, for openDocument, you're tasked to return a parcel file
descriptor.
Thankfully, if you already have a local file,
this is really straightforward.
Thanks to ParcelFileDescriptor's Open method.
You can also create a ParcelFileDescriptor directly
from a socket though, so having the full local file
locally isn't a hard requirement.
Of course, the best implementation
won't do any good unless you add your DocumentsProvider
to your Android manifest.
A couple things of note here.
You need a unique authority for your DocumentsProvider,
generally something prefaced with your application ID,
your package name.
Then you'll need to make sure that it's exported,
so the Storage Access Framework can access it,
grant-uri-permissions mean the system can grant permissions
on your behalf, and the permission
ensures only the Storage Access Framework can connect
to your DocumentsProvider.
Now, that's it for the basics.
You should be able to see your root and documents when
apps invoke the standard documents U/I via get content,
or that ACTION_OPEN_DOCUMENT intent.
No more need to build your own U/I,
and a consistent experience, whether users are operating
on local files or files you provide
or files from some other app.
But this is really only the beginning of the Storage Access
Framework.
There are optimizations for loading huge lists of document,
with EXTRA_LOADING; ACTION_OPEN_DOCUMENT_TREE,
for letting users select whole directories to share;
ACTION_CREATE_DOCUMENT, for saving a file, not
just locally, but to any DocumentsProvider;
providing thumbnails; document management
features like Deleting, Renaming, Copying, Moving,
and Removing; alternate file formats; and virtual files.
You'll find all the details in the links in the description
and how you can use the Storage Access
Framework to build a great DocumentsProvider to build
better apps.
[MUSIC PLAYING]