CWAC-Provider: Helping to Make Content Providers Sane
This project offers a StreamProvider
, based on Google's FileProvider
. Like FileProvider
, StreamProvider
is designed to serve up files, for reading and writing, through the ContentProvider
interface ( content://
Uri
values). StreamProvider
offers:
- Serving files from assets and raw resources
- Serving files from
getExternalFilesDir()
andgetExternalCacheDir()
in addition to FileProvider
's support for serving files from getFilesDir()
, getCacheDir()
, and Environment.getExternalStoragePublicDirectory()
.
You can simply use the StreamProvider
directly, without creating your own subclass — everything can be handled through configuration rather than coding.
This Android library project is available as a JAR or as an artifact for use with Gradle. To use that, add the following blocks to your build.gradle
file:
repositories {
maven {
url "https://s3.amazonaws.com/repo.commonsware.com"
}
}
dependencies {
compile 'com.commonsware.cwac:provider:0.5.2'
}
Or, if you cannot use SSL, use http://repo.commonsware.com
for the repository URL.
NOTE: The JAR name, as of v0.2.1, has a cwac-
prefix, to help distinguish it from other JARs.
Usage: StreamProvider
Once you add the JAR or artifact to your project, it works much along the lines of FileProvider
:
-
Define an XML metadata file with a
<paths>
root element, containing one or more elements describing what you want the provider to serve (described in greater detail below) -
Define a
<provider>
in your manifest as follows:
<provider android:name="com.commonsware.cwac.provider.StreamProvider" android:authorities="..." android:exported="false" android:grantUriPermissions="true"> <meta-data
android:name="com.commonsware.cwac.provider.STREAM_PROVIDER_PATHS"
android:resource="@xml/..."/> </provider>
(where you fill in your desired authority name and reference to your XML metadata from step #1)
Notably, the provider must have the <meta-data>
element pointing to your XML metadata.
- Use
FLAG_GRANT_READ_URI_PERMISSION
andFLAG_GRANT_WRITE_URI_PERMISSION
inIntent
objects you use to have third parties use the files theStreamProvider
serves, to allow those apps selective, temporary access to the file.
Exporting and Usage Patterns
If your StreamProvider
is exported, all of your streams will be considered read-only, regardless of any other configuration. Mostly, this mode is here for cases where you need a streaming provider and cannot grant Uri
permissions (e.g., implementing a ChooserTargetService
).
If your StreamProvider
is not exported, and it has android:grantUriPermissions
set, then you can control, on a per- Uri
basis, which clients get access to your streams. This works identically to how FileProvider
works. Whether a particular source of streams is read-only or read-write will depend on whether the stream is a file and your metadata configuration.
Wherever possible, elect to not export the provider and use FLAG_GRANT_READ_URI_PERMISSIONS
or similar techniques to selectively grant access to your content.
Note that the exported-and-read-only rule is on a per-provider basis. If you have some content that needs to be published globally and others that are not:
-
Use
StreamProvider
and one<provider>
element for one set of content, with one authority andandroid:exported
setting -
Subclass
StreamProvider
and have a separate<provider>
element for the other set of content, with a separate authority andandroid:exported
setting
Metadata Elements
Google's FileProvider
supports:
-
<files-path>
for serving files from your app'sgetFilesDir()
-
<external-path>
for serving files fromEnvironment.getExternalStoragePublicDirectory()
-
<cache-path>
for serving files from your app'sgetCacheDir()
-
<external-files-path>
for serving files fromgetExternalFilesDir()
-
<external-cache-path>
for serving files fromgetExternalCacheDir()
Each of those take a name
attribute, indicating the first path segment of the Uri
that should identify this particular source of files. For example, a name
of foo
would mean that content://your.authority.here/foo/...
would look for a ...
file in that particular element's source of files.
Each of those optionally take a path
attribute, indicating a subdirectory under the element-defined root to use as the source of files, rather than the root itself. So, a <files-path>
with a path="stuff"
attribute would serve files from the stuff/
subdirectory within getFilesDir()
. Note that path
can point to a file as well, to limit access to a single file rather than a directory. Note that path
is required for <files-path>
, so you do not accidentally serve everything under getFilesDir()
.
Also, each can optionally take a readOnly
attribute. If this is set to true
, then the files will be readable, but not writeable.
<external-files-path>
also can take an optional dir
attribute. If missing, the files are served from getExternalFilesDir()
. If a valid value of dir
is supplied, that value is passed into getExternalFilesDir()
. As such, dir
is limited to be one of the Environment.DIRECTORY_*
constants:
Alarms
DCIM
Documents
Download
Movies
Music
Notifications
Pictures
Podcasts
Ringtones
However, you cannot have both <external-files-path>
with no dir
(indicating that you are serving from getExternalFilesDir(null)
) and one or more <external-files-path>
elements with dir
values, as they will conflict.
StreamProvider
adds support for:
-
<raw-resource>
for serving a particular raw resource, where thepath
is the name of the raw resource (without file extension) -
<asset>
for serving files fromassets/
-
<dir-path>
, for serving files from locations identified bygetDir()
-
<external-public-path>
, for serving files from locations identified byEnvironment.getExternalStoragePublicDirectory()
In the case of <dir-path>
, two attributes are required:
-
dir
, which indicates what directory to serve (this is passed intogetDir()
) -
path
, which serves its normal role, to determine what to serve from the directory identified bydir
In the case of <external-public-path>
, dir
is required. It needs to be the string value of one of the Environment.DIRECTORY_*
constants, listed above.
Of course, your metadata can have one or more of each of these types as needed to declare what you want to be served.
Supporting Legacy Apps
Some apps assume that any content://
Uri
that they get must be from the MediaStore
or otherwise have a MediaStore.MediaColumns.DATA
column that can be queried. This, of course, was never the case, and is less the case nowadays. But, it sometimes takes firms a while to get with the program, and in the meantime, StreamProvider
could have issues working with such apps.
Adding another <meta-data>
element to the <provider>
can help improve compatibility:
<meta-data
android:name="com.commonsware.cwac.provider.USE_LEGACY_CURSOR_WRAPPER"
android:value="true"/>
This tells StreamProvider
to include a fake MediaStore.MediaColumns.DATA
in the result set, with a null
value, to try to cajole these legacy apps into using the Uri
as they are supposed to: via ContentResolver
and openInputStream()
.
Similarly, you can add this <meta-data>
element to the <provider>
:
<meta-data
android:name="com.commonsware.cwac.provider.USE_URI_FOR_DATA_COLUMN"
android:value="true"/>
Clients of a streaming ContentProvider
should not be assuming that they can query()
for a _DATA
column. Alas, some developers still do, thinking that all content:
Uri
values come from the MediaStore
. By default, StreamProvider
returns null
for the _DATA
column, should somebody query()
for it. However, with the above <meta-data>
element, StreamProvider
will return the Uri
used for the query()
as the value for _DATA
.
Gradle Settings
Starting with version 0.3.0 of the library, for files you are looking to share from your app's assets/
, you will need to teach the build system to avoid compressing those files. On the plus side, we can now use AssetFileDescriptor
for those, and greatly improve compatibility with apps using our streams.
To do this, add the following closure to your android
closure in your module's build.gradle
file:
aaptOptions {
noCompress 'pdf', 'mp4', 'ogg'
}
(here, the file extensions are from the demo app — you would list the file extensions that you are looking to share)
Getting Uri Values
For files served through StreamProvider
(as opposed to assets or raw resources), StreamProvider
offers a static getUriForFile()
method that works akin to its equivalent on FileProvider
. It takes two parameters:
-
The authority name of the provider you are interested in
-
The
File
object that you want to serve
It returns a Uri
pointing to that file or null
if the File
does not seem to be served by that provider.
For anything else, you have to assemble the Uri
yourself:
private static final String AUTHORITY=
"com.commonsware.cwac.provider.demo"; private static final Uri PROVIDER=
Uri.parse("content://"+AUTHORITY);
private Uri buildUri(String path) {
return(PROVIDER
.buildUpon()
.appendPath(StreamProvider.getUriPrefix(AUTHORITY))
.appendPath(path)
.build());
}
Uri Prefixes
To help defeat some security attacks, StreamProvider
, starting with 0.4.0, by default, puts a per-install UUID into every Uri
, as the first path segment after the authority name. So, for example, in the following Uri
, some-prefix
is the prefix:
content://com.commonsware.hithere/some-prefix/foo/bar.txt
If you are constructing a Uri
supported by a StreamProvider
— and you cannot use getUriForFile()
(e.g., you are serving assets or raw resources) — call the static getUriPrefix()
method, passing in the authority name of the provider. If it returns a non- null
value, that is the prefix to put into the Uri
. If getUriPrefix()
returns null
, there is no prefix.
Extending StreamProvider
You are welcome to create custom subclasses of StreamProvider
, to handle cases that are not covered by StreamProvider
itself. This process is covered in a separate documentation page.
Limitations
Compared to FileProvider
, StreamProvider
has the following limitations:
-
FileProvider
has support for an additional, undocumented metadata element;StreamProvider
drops support for that element. -
StreamProvider
no longer allows you to serve everything fromgetFilesDir()
, for security reasons. Thepath
attribute is required.
Upgrading to 0.5.0+ From Earlier Versions
If you created your own subclass of StreamProvider
and overrode buildStrategy()
, note that the method signature has changed and is now:
protected StreamStrategy buildStrategy(Context context,
String tag, String name,
String path, boolean readOnly,
HashMap<String, String> attrs)
readOnly
is a boolean indicating if this content should be treated as read-only ( true
) or read-write ( false
).
Also, if you are using LegacyCompatCursorWrapper
, it now has an additional, three-parameter constructor:
public LegacyCompatCursorWrapper(Cursor cursor, String mimeType,
Uri uriForDataColumn)
That third parameter should be the Uri
to use for the value of the _DATA
column, should somebody attempt to request that column from this Cursor
. The default value is null
. A likely alternative would be whatever Uri
generated this Cursor
(e.g., from the provider's query()
implementation).
Also, if you are using LegacyCompatCursorWrapper
, its fields are now marked final private
. If you had been referencing those fields, and this now breaks your code, please file an issue and explain your use case, so an appropriate API can be added to LegacyCompatCursorWrapper
.
Upgrading to 0.4.0+ From Earlier Versions
If you are upgrading an existing StreamProvider
implementation to 0.4.0 or higher, please note the new Uri
prefix discussed earlier in the documentation. Your provider's Uri
values will have this prefix by default, and you need to include the prefix in any Uri
values that you publish.
Usage: LegacyCompatCursorWrapper
Some consumers of content://
Uri
values make unfortunate assumptions, that they can query()
on that Uri
and get columns back other than those in OpenableColumns
. Of note, one or more popular consumers request:
-
MediaStore.MediaColumns.DATA
(erroneously thinking that theUri
must be known to theMediaStore
) -
MediaStore.MediaColumns.MIME_TYPE
(rather than callinggetType()
on aContentResolver
, the way talented developers would)
For Google's FileProvider
, or other ContentProvider
implementations that also have a need for these fake columns in the query()
result, this library offers LegacyCompatCursorWrapper
. Just wrap your Cursor
in the LegacyCompatCursorWrapper
(e.g., new LegacyCompatCursorWrapper(cursor)
), and return the LegacyCompatCursorWrapper
. It will automatically add the fake columns for queries that request them, delegating all other requests to the underlying Cursor
.
Many thanks to Stefan Rusek for pioneering the basic approach.
Dependencies
This project has no dependencies.
Version
This is version v0.5.2 of this module, meaning it is pretty new.
Demo
In the demo/
sub-project you will find a sample project demonstrating the use of StreamProvider
.
Additional Documentation
The Busy Coder's Guide to Android Development contains a section dedicated to StreamProvider
. It also uses LegacyCompatCursorWrapper
in all of its FileProvider
samples.
License
The code in this project is licensed under the Apache Software License 2.0, per the terms of the included LICENSE file.
Questions
If you have questions regarding the use of this code, please post a question on Stack Overflow tagged with commonsware-cwac
and android
after searching to see if there already is an answer. Be sure to indicate what CWAC module you are having issues with, and be sure to include source code and stack traces if you are encountering crashes.
If you have encountered what is clearly a bug, or if you have a feature request, please post an issue. Be certain to include complete steps for reproducing the issue. The contribution guidelines provide some suggestions for how to create a bug report that will get the problem fixed the fastest.
You are also welcome to join the CommonsWare Community and post questions and ideas to the CWAC category.
Do not ask for help via social media.
Also, if you plan on hacking on the code with an eye for contributing something back, please open an issue that we can use for discussing implementation details. Just lobbing a pull request over the fence may work, but it may not. Again, the contribution guidelines provide a bit of guidance here.
Release Notes
- v0.5.2: added exported read-only support, published JavaDoc/source JARs in repo
- v0.5.1: fixed bug blocking use of multiple authorities in a single provider
- v0.5.0:
- Added support for
Environment.getExternalStoragePublicDirectory()
- Added support for
getDir()
- Added support for non-
null
versions ofgetExternalFilesDir()
- Added
com.commonsware.cwac.provider.USE_URI_FOR_DATA_COLUMN
option per issue #20
- Added support for
- v0.4.4: fix for
CompositeStreamStrategy
bug - v0.4.3: exposed yet another method for extending
StreamProvider
- v0.4.2: exposed a few more methods for extending
StreamProvider
- v0.4.1: fix for
CompositeStreamStrategy
bug - v0.4.0: added Uri prefix, clearer subclassing support, refactored into Android Studio project structure, etc.
- v0.3.1: fixed local path bug, added support for
MediaStore.MediaColumns.MIME_TYPE
toLegacyCompatCursorWrapper
- v0.3.0: switched to
openAssetFile()
where possible for better compatibility - v0.2.5: pulled out permissions check into separate method
- v0.2.4: added
LegacyCompatCursorWrapper
andUSE_LEGACY_CURSOR_WRAPPER
- v0.2.3: resolved issue #8, supporting actual length for assets and raw resources
- v0.2.2: updated for Android Studio 1.0 and new AAR publishing system
- v0.2.1: updated Gradle, fixed manifest for merger, added
cwac-
prefix to JAR - v0.2.0: migrated to Gradle, published AAR
- v0.1.0: initial release