493: Undecipherable

-Blog-

-Projects-

-About me-

-RSS-

Android's ContentProvider: how to share a virtual file

Dennis Guse

While working on OpenTracks, I wanted to send geographical data (e.g., points as well as KML, GPX) to another installed application that could show this kind of data (e.g., OsmAnd or Maps.ME). The main reason for this, was that I did not want this functionality in OpenTracks as it was not a core feature.

To achieve this usually, the FileProvider is used and it's usage is really simple: just get the Uri of the file to be shared and send an intent (be aware of grantUriPermission). This is awesome as long as the file is already stored on the device - it is painless simple.

However, what happens if the data is not yet stored in the file system? For example, the data resides in a SQLite database? This is actually achivable using FileProvider: 1. One could store the data in a temporary file and store it somewhere (e.g., the app's cache directory). 2. Send the intent. 3. Later delete the temporary file.

This solution works quite well and is probably in use quite often. However, it does not feel very beautiful (at least not for me).

What I actually wanted was the a ContentProvider that can share a virtual (i.e., file is created on the fly from database). Thus, I do not need to create the temporary file and delete it later. Luckily, this is possible and this is actually quite straight forward. The following shows an example that is inspired by FileProvider, but the interesting part is in openFile() and how a ParcelFileDescriptor can be created without an actual file.

public class ShareContentProvider extends ContentProvider

    private static final String[] COLUMNS = {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE};

    //Provide file name and size.
    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
        // Inspired from FileProvider
        // ContentProvider has already checked granted permissions
        if (projection == null) {
            projection = COLUMNS;
        }

        String[] cols = new String[projection.length];
        Object[] values = new Object[projection.length];
        int i = 0;
        for (String col : projection) {
            if (OpenableColumns.DISPLAY_NAME.equals(col)) {
                cols[i] = OpenableColumns.DISPLAY_NAME;
                values[i++] = uri.getLastPathSegment();
            } else if (OpenableColumns.SIZE.equals(col)) {
                cols[i] = OpenableColumns.SIZE;
                values[i++] = -1; //Return a file size of -1
            }
        }

        cols = Arrays.copyOf(cols, i);
        values = Arrays.copyOf(values, i);

        final MatrixCursor cursor = new MatrixCursor(cols, 1);
        cursor.addRow(values);
        return cursor;
    }

    @Nullable
    @Override
    public String getType(@NonNull Uri uri) {
        return "PutYourMimeTypeHere";
    }

    @Nullable
    @Override
    public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
        PipeDataWriter pipeDataWriter = new PipeDataWriter<String>() {
            @Override
            public void writeDataToPipe(@NonNull ParcelFileDescriptor output, @NonNull Uri uri, @NonNull String mimeType, @Nullable Bundle opts, @Nullable String args) {
                try (FileOutputStream fileOutputStream = new FileOutputStream(output.getFileDescriptor())) {
                     //TODO: Write your actual data
                     byte[] data = new byte[]{255, 255, 255};
                     fileOutputStream.write(data);
                } catch (IOException e) {
                    Log.w(TAG, "there occurred an error while sharing a file: " + e);
                }
            }
        };

        return openPipeHelper(uri, getType(uri), null, null, pipeDataWriter);
}

The full code is available on Github. In addition, please be aware to make the ShareContentProvider accessible to the calling app (i.e., grantUriPermission).

For OpenTracks, I ran, however, into another issue related to Android's security. OpenTracks uses internally a ContentProvider that manages access to the internal SQLite database. My initial approach was to implemented the ShareContentProvider and let it forward requests to the internal ContentProvider (i.e., two separate instances), but both belonging to OpenTracks. In ShareContentProvider, I used getContext().getContentResolver() to access the internal ContentProvider. This works quite well. However, if ShareContentProvider receives a request from another app (access granted via temporary grantUriPermission), Android's security infrastructure does not allow to forward requests to the internal ContentProvider. The reason is rather simple as the calling app only got permission for one URI managed ShareContentProvider and not for the internal ContentProvider URIs. One (im)possible solution would be to make the internal ContentProvder to be world readable (i.e., exported="true"), but this would expose all data of your app to all other installed apps. And this is an absolute no go!

I solved this issue by using only one ContentProvider for OpenTracks (i.e., merging ShareContentProvider and the internal ContentProvider). The resulting ContentProvider can then use himself to acquire the necessary data for file sharing URIs. The implementation is rather simple as ShareContentProvider inherits from the internal ContentProvider and forwards all internal requests to it's parent. For convenience, ShareContentProvider has a static function to create a sharing URI as it later needs to parse the URI again.

OpenTracks can now share geo-data files with other map application without using temporary files.