17

I am trying to allow the user to share an image to other apps on the device. The image is inside the files/ subdirectory of my app's internal storage area. It works just fine with Gmail, but Facebook and Twitter both crash when responding to my intent.

EDIT: Google+ also works fine.

Here are the relevant sections of code.

In Application.xml

<provider 
    android:name="android.support.v4.content.FileProvider"
    android:authorities="org.iforce2d.myapp.MyActivity"
    android:exported="false"
    android:grantUriPermissions="true">
    <meta-data
        android:name="android.support.FILE_PROVIDER_PATHS"
        android:resource="@xml/filepaths" />
</provider>

xml/filepaths.xml

<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path name="shared" path="shared"/>
</paths>

Here is the sharing code in my activity:

File imagePath = new File(getContext().getFilesDir(), "shared");
File newFile = new File(imagePath, "snapshot.jpg");
Uri contentUri = FileProvider.getUriForFile(getContext(),
                     "org.iforce2d.myapp.MyActivity", newFile);    

Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.setType("image/jpeg");
shareIntent.putExtra(Intent.EXTRA_STREAM, contentUri);
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

List<ResolveInfo> resInfos = 
    getPackageManager().queryIntentActivities(shareIntent,
                                              PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo info : resInfos) {
    getContext().grantUriPermission(info.activityInfo.packageName, 
                                    contentUri,
                                    Intent.FLAG_GRANT_READ_URI_PERMISSION);
}

startActivity(Intent.createChooser(shareIntent, "Share image..."));

The value of contentUri when logged is:

content://org.iforce2d.myapp.MyActivity/shared/snapshot.jpg

I have only checked this with Gmail, Facebook and Twitter as the receiving apps, but the results have been very consistent over a wide range of OS versions (from 2.2.1 to 4.4.3) and 7 devices include a Kindle.

Gmail works great. Image thumbnail appears in mail composition, and successfully attaches to mail when sent.

Twitter and Facebook both crash, as below.

Here are the stack traces from logcat showing the problem these two apps are having, it appears to be the same problem for both of them (this is taken from 4.4.3, but the errors were practically the same all the way back to 2.2.1 albeit with slightly different wording of the error message):

Caused by: 
  java.lang.IllegalStateException: Couldn't read row 0, col 0 from CursorWindow. 
  Make sure the Cursor is initialized correctly before accessing data from it.
    at android.database.CursorWindow.nativeGetString(Native Method)
    at android.database.CursorWindow.getString(CursorWindow.java:434)
    at android.database.AbstractWindowedCursor.getString(AbstractWindowedCursor.java:51)
    at android.database.CursorWrapper.getString(CursorWrapper.java:114)
    at com.twitter.library.media.util.f.a(Twttr:95)
    ...

Caused by: 
  java.lang.IllegalStateException: Couldn't read row 0, col -1 from CursorWindow.
  Make sure the Cursor is initialized correctly before accessing data from it.
    at android.database.CursorWindow.nativeGetString(Native Method)
    at android.database.CursorWindow.getString(CursorWindow.java:434)
    at android.database.AbstractWindowedCursor.getString(AbstractWindowedCursor.java:51)
    at android.database.CursorWrapper.getString(CursorWrapper.java:114)
    at com.facebook.photos.base.media.MediaItemFactory.b(MediaItemFactory.java:233)
    ...

Given that sharing images on Facebook and Twitter is something that millions of people do all day long, I'm pretty shocked that it's so hard to implement :/

Can anybody spot something I'm doing wrong here?

iforce2d
  • 8,194
  • 3
  • 29
  • 40

2 Answers2

33

Twitter (wrongly) assumes that there will be a MediaStore.MediaColumns.DATA column. Starting in KitKat the MediaStore returns null, so luckily, Twitter gracefully handles nulls, and does the right thing.

public class FileProvider extends android.support.v4.content.FileProvider {

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        Cursor source = super.query(uri, projection, selection, selectionArgs, sortOrder);

        String[] columnNames = source.getColumnNames();
        String[] newColumnNames = columnNamesWithData(columnNames);
        MatrixCursor cursor = new MatrixCursor(newColumnNames, source.getCount());

        source.moveToPosition(-1);
        while (source.moveToNext()) {
            MatrixCursor.RowBuilder row = cursor.newRow();
            for (int i = 0; i < columnNames.length; i++) {
                row.add(source.getString(i));
            }
        }

        return cursor;
    }

    private String[] columnNamesWithData(String[] columnNames) {
        for (String columnName : columnNames)
            if (MediaStore.MediaColumns.DATA.equals(columnName))
                return columnNames;

        String[] newColumnNames = Arrays.copyOf(columnNames, columnNames.length + 1);
        newColumnNames[columnNames.length] = MediaStore.MediaColumns.DATA;
        return newColumnNames;
    }
}
Stefan Rusek
  • 4,737
  • 2
  • 28
  • 25
  • 1
    Yes. I just tested it. – Stefan Rusek Jul 29 '14 at 19:06
  • 2
    Sorry for low experience in Android Java programming: How do I use the code above? – VHanded Nov 21 '14 at 04:21
  • 2
    Great! I works like charm. The best part is that I don't have to ask for write external storage permission. – hiepnd Dec 26 '14 at 04:27
  • 6
    Excellent hack! I have published [a `LegacyCompatCursorWrapper`](https://github.com/commonsguy/cwac-provider#usage-legacycompatcursorwrapper), which is a riff on the same concept. In my case, I used a `CursorWrapper` implementation, to avoid copying the `Cursor` contents into the `MatrixCursor`. But otherwise it's the same basic idea, and thanks for pointing out that approach! – CommonsWare Feb 23 '15 at 21:05
  • 2
    To VHanded: I had the same problem. Look in the Android Manifest file and change android.support.v4.content.FileProvider to FileProvider, that should do it. – Erika Mar 24 '15 at 19:58
3

EDIT -- The workaround described in this answer works, but the solution provided by Stefan is more elegant. It should be the accepted answer.

Also, as of January 2015, the Facebook app no longer has this bug (and now works with the standard FileProvider) but Twitter still does. Hopefully it will go completely away in the future, and neither of these will be necessary.


While the FileProvider object looks wonderful in theory, it doesn't seem to work when sharing to many third-party applications (while Google ones, like G+, Gmail, &c work perfectly). Either it doesn't expose sufficient information, or those applications just assume you're sharing files, and thus crash.

It's really frustrating! :/

The only reliable way I've found to share image files is to copy them to the external storage directory (you can create a .nomedia subdirectory to avoid them appearing in the Gallery app).

For example:

Bitmap bitmapToShare = BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher);

File pictureStorage = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES);
File noMedia = new File(pictureStorage, ".nomedia");
if (!noMedia.exists())
    noMedia.mkdirs();

File file = new File(noMedia, "shared_image.png");
saveBitmapAsFile(bitmapToShare, file);

Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(file));
shareIntent.setType("image/png");

startActivity(shareIntent);

Addendum: of course, you should also check getExternalStorageState() before trying to write/copy the file there.

Community
  • 1
  • 1
matiash
  • 54,791
  • 16
  • 125
  • 154
  • That works nicely, thankyou! I should have asked you six hours ago... So is the FileProvider method new or something? Do the FB/TW developers just need to pull the finger and get it supported? – iforce2d Jun 13 '14 at 02:42
  • It's not exactly new, it was introduced on MAy 2013 -- [Support Library Changelog](http://developer.android.com/tools/support-library/index.html). I'm not sure why, but third-party applications don't seem to support it. A real pity! The other solution sure looks like a hack. – matiash Jun 13 '14 at 02:45
  • @matiash: "Either it doesn't expose sufficient information, or those applications just assume you're sharing files, and thus crash" -- what information can you provide about the nature of the crashes? We can't get `FileProvider` fixed (or forks of it, like my `StreamProvider`) without more details. – CommonsWare Jun 28 '14 at 17:14
  • @CommonsWare I'm not sure how to get additional information apart from the call stacks themselves. :/ From the `col -1` part, I assumed the problem was that the ContentProvider is missing some column (and thus `getColumnIndex()` returns -1 somewhere). – matiash Jun 28 '14 at 19:21
  • @matiash: Sorry, my fault. I was focused on your answer and less on your question. The problem is that the query is returning no rows or no columns; otherwise, the Twitter stack trace should work. Looking at the source to `FileProvider` and its `query()` method, the only way that should happen would be if the `Uri` is bad (in which case, it should crash in your app) or if the client did not as for any valid columns (and `FileProvider` already handles all `OpenableColumns` values). Very curious... – CommonsWare Jun 28 '14 at 19:30
  • 2
    FWIW, the problem in Twitter's app is that they are `query()`-ing for `_data`. There is some lousy code floating around where people think that all `content://` `Uri` values can magically be transmogrified into local file paths (involving finding a `_data` column in `MediaStore`, which has never been correct, and is very much not correct with `FileProvider`. Facebook could be doing the same thing, but I'm not a Facebook user and therefore cannot readily test the theory. – CommonsWare Jul 03 '14 at 17:04
  • @CommonsWare Yeah, that's what I suspected, incorrect assumptions from those apps. :/ Just curious, how did you determine the column exactly? By placing a breakpoint in the returned `Cursor` and seeing what Twitter was accessing? – matiash Jul 03 '14 at 17:09
  • I created an app that used my `StreamProvider` as the source of the image. I knew they were crashing accessing a `Cursor`, which suggested a `query()` of my provider. So I added some logging in `query()` on my side to see what columns were coming over in the projection. They only asked for one: `_data`, which is not one of the `OpenableColumns` and therefore not supposed to be available. – CommonsWare Jul 03 '14 at 17:10
  • @CommonsWare Very instructive. Thanks. – matiash Jul 03 '14 at 17:18