Marco Di Scala Marco Di Scala - 1 month ago 14
Java Question

Android photo sharing with FileProvider

I have read all the answer about this argument but I receive always an error of the application that receive my photo.


The only way that worked for me, for all application, was this (It works because sd card files are public to all applications):

final File tmpFile = new File(context.getExternalCacheDir(), "exported.jpg");
Uri tmpFileUri = Uri.fromFile(tmpFile);

Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.setDataAndType(tmpFileUri, "image/jpeg");
shareIntent.putExtra(Intent.EXTRA_STREAM, tmpFileUri);
context.startActivity(Intent.createChooser(shareIntent, context.getString(R.string.share_image)));






Now, I'm stuck on how to share a file that is located in a private folder.
I used the code provided by the google documentation:

<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.test.myapp.fileprovider"
android:exported="false"
android:grantUriPermissions="true" >
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
...
...
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<files-path name="internal_files" path="/"/>
<cache-path name="internal_cache" path="/" />
</paths>




This is the code to share files using the
FileProvider
but doesn't work with any application except whats up:

final File tmpFile = new File(context.getCacheDir(), "exported.jpg");
Uri tmpFileUri = FileProvider.getUriForFile(context, context.getPackageName() + ".fileprovider", tmpFile);
//Remove the uri permission because we overwrite the file
context.revokeUriPermission(tmpFileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);

saveBitmapToPath(bitmap, tmpFile);
bitmap.recycle();

Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.setDataAndType(tmpFileUri, "image/jpeg");
shareIntent.putExtra(Intent.EXTRA_STREAM, tmpFileUri);
//Grant again the permissions
shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
context.startActivity(Intent.createChooser(shareIntent, context.getString(R.string.share_image)));


Why do I keep getting errors in other applications, like this:

java.lang.SecurityException: Permission Denial: content://com.test.myapp.fileprovider/internal_cache/exported.jpg (pid=675, uid=10052) requires null


Or

IllegalArgumentException: Failed to find configuration root that contains content://com.test.myapp.fileprovider/internal_cache/exported.jpg

Answer

Finally looking at the source code of the receiving app, I got the solution.
This is the complete, working code that I share.
I hope to help somebody:

<!-- AndroidManifest.xml -->
<provider
    android:name="com.test.myapp.fileprovider.FileProvider"
    android:authorities="com.test.myapp.fileprovider"
    android:exported="true"
    tools:ignore="ExportedContentProvider" />


//EntryPoint
private void mySharer() {
    ArrayList<Uri> streamUris = new ArrayList<Uri>();
    for (int i = 0; i < 10; i++) {
        File tmpFile = new File(getContext().getCacheDir(), "tmp" + i + ".jpg");
        Uri tmp = FileProvider.getUriForFile("com.test.myapp.fileprovider", tmpFile);
        streamUris.add(tmp);
    }
}

//Share Intent creator
public final void shareUris(ArrayList<Uri> streamUris) {
    if (!streamUris.isEmpty()) {
        Intent shareIntent = new Intent();
        shareIntent.putExtra(ShareCompat.EXTRA_CALLING_PACKAGE, getPackageName());
        shareIntent.putExtra(ShareCompat.EXTRA_CALLING_ACTIVITY, getComponentName());
        shareIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET | Intent.FLAG_GRANT_READ_URI_PERMISSION);
        shareIntent.setType("image/jpeg");

        if (streamUris.size() == 1) {
            shareIntent.setAction(Intent.ACTION_SEND);
            shareIntent.putExtra(Intent.EXTRA_STREAM, streamUris.get(0));
        } else {
            shareIntent.setAction(Intent.ACTION_SEND_MULTIPLE);
            shareIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, streamUris);
        }

        //For multiple images copy all images in the baseDir and use startActivityForResult
        startActivityForResult(Intent.createChooser(shareIntent, getString(R.string.share_image)), 500);
    }
}

//onResult you can delete all temp images/files with specified extensions
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    switch (requestCode) {
        case 500:
            getContentResolver().delete(FileProvider.getUriForFile(getPackageName() + ".fileprovider", null), FileProvider.WHERE_EXTENSION, new String[]{"jpg"});
            break;
        default:
            break;
    }
}

/**
 * This class extends the ContentProvider
 */
abstract class AbstractFileProvider extends ContentProvider {

    private final static String OPENABLE_PROJECTION_DATA = "_data";
    private final static String[] OPENABLE_PROJECTION = { OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE, OPENABLE_PROJECTION_DATA };

    @Override
    public Cursor query(Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
        if (projection == null) {
            projection = OPENABLE_PROJECTION;
        }

        final MatrixCursor cursor = new MatrixCursor(projection, 1);
        MatrixCursor.RowBuilder b = cursor.newRow();

        for (String col : projection) {
            if (OpenableColumns.DISPLAY_NAME.equals(col)) {
                b.add(getFileName(uri));
            } else if (OpenableColumns.SIZE.equals(col)) {
                b.add(getDataLength(uri));
            } else if (OPENABLE_PROJECTION_DATA.equals(col)) {
                b.add(getFileName(uri));
            } else {
                b.add(null);
            }
        }

        return cursor;
    }

    @Override
    public String getType(Uri uri) {
        return URLConnection.guessContentTypeFromName(uri.toString());
    }

    protected String getFileName(Uri uri) {
        return uri.getLastPathSegment();
    }

    protected long getDataLength(Uri uri) {
        return AssetFileDescriptor.UNKNOWN_LENGTH;
    }

    @Override
    public Uri insert(Uri uri, ContentValues initialValues) {
        throw new RuntimeException("Operation not supported");
    }

    @Override
    public int update(Uri uri, ContentValues values, String where, String[] whereArgs) {
        throw new RuntimeException("Operation not supported");
    }

    @Override
    public int delete(Uri uri, String where, String[] whereArgs) {
        throw new RuntimeException("Operation not supported");
    }
}

/**
 * This class extends the AbstractFileProvider
 */
public class FileProvider extends AbstractFileProvider {

    public static final String CONTENT_URI = "content://";
    private File baseDir;

    @Override
    public boolean onCreate() {
        baseDir = getContext().getCacheDir();

        if (baseDir != null && baseDir.exists()) {
            return true;
        }

        Log.e("FileProvider", "Can't access cache directory");
        return false;
    }

    @Override
    public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
        File f = new File(baseDir, uri.getPath());

        if (f.exists()) {
            return ParcelFileDescriptor.open(f, ParcelFileDescriptor.MODE_READ_ONLY);
        }

        throw new FileNotFoundException(uri.getPath());
    }

    @Override
    protected long getDataLength(Uri uri) {
        File f = new File(baseDir, uri.getPath());

        return f.length();
    }

    public static Uri getUriForFile(String authority, File file) {
        return Uri.parse(CONTENT_URI + authority + "/" + file.getName());
    }
}



-------------EDIT: 05/11/16--------------
Added support for multiple images:

  1. Copy all images in the baseDir folder
  2. Implement delete() method in the FileProvider
  3. Use startActivityForResult
  4. Listen onActivityResult
  5. Now you can delete all temp images

For email attachment you must wait for email to be sent before delete the file, otherwise you'll send an empty attachment