How to Use Support Fileprovider For Sharing Content to Other Apps

How to use support FileProvider for sharing content to other apps?

Using FileProvider from support library you have to manually grant and revoke permissions(at runtime) for other apps to read specific Uri. Use Context.grantUriPermission and Context.revokeUriPermission methods.

For example:

//grant permision for app with package "packegeName", eg. before starting other app via intent
context.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);

//revoke permisions
context.revokeUriPermission(uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);

As a last resort, if you can't provide package name you can grant the permission to all apps that can handle specific intent:

//grant permisions for all apps that can handle given intent
Intent intent = new Intent();
intent.setAction(Intent.ACTION_SEND);
...
List<ResolveInfo> resInfoList = context.getPackageManager().queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY);
for (ResolveInfo resolveInfo : resInfoList) {
String packageName = resolveInfo.activityInfo.packageName;
context.grantUriPermission(packageName, uri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);
}

Alternative method according to the documentation:

  • Put the content URI in an Intent by calling setData().
  • Next, call the method Intent.setFlags() with either FLAG_GRANT_READ_URI_PERMISSION or FLAG_GRANT_WRITE_URI_PERMISSION
    or both.
  • Finally, send the Intent to another app. Most often, you do this by calling setResult().

    Permissions granted in an Intent remain in effect while the stack
    of the receiving Activity is active. When the stack finishes, the

    permissions are automatically removed. Permissions granted to one

    Activity in a client app are automatically extended to other

    components of that app.

Btw. if you need to, you can copy source of FileProvider and change attachInfo method to prevent provider from checking if it is exported.

Possible to use multiple authorities with FileProvider?

My solution to this problem has actually been to avoid relying on a single FileProvider parsing multiple authorities. While this doesn't directly address the question as stated, I'm posting it for posterity.


I updated my library to leverage an empty subclass of FileProvider, so that the library's updated manifest provider entry is now:

<provider
android:name=".flow.email.screenshot.BugShakerFileProvider"
android:authorities="${applicationId}.bugshaker.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/library_file_paths" />
</provider>

The merged manifest of an application that (1) uses a stock FileProvider and (2) consumes my library will now contain both of the entries shown below (no collision!):

<provider
android:name="android.support.v4.content.FileProvider"
android:authorities="com.consuming.application.fileprovider"
android:exported="false"
android:grantUriPermissions="true" >
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/application_file_paths" />
</provider>

<provider
android:name="com.github.stkent.bugshaker.flow.email.screenshot.BugShakerFileProvider"
android:authorities="com.consuming.application.bugshaker.fileprovider"
android:exported="false"
android:grantUriPermissions="true" >
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/library_file_paths" />
</provider>

I didn't realize that this was a potential solution until a coworker pointed it out. My assumption had previously (and incorrectly) been that all FileProviders in the manifest must set

android:name="android.support.v4.content.FileProvider"

but a quick check of the documentation revealed my error:

The name of the class that implements the content provider, a subclass of ContentProvider. This should be a fully qualified class name (such as, "com.example.project.TransportationProvider"). [...]

Android photo sharing with FileProvider

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

createChooser() intent exception requires the provider be exported, or grantUriPermission()

One of the limitations of FileProvider.getUriForFile() is that it does not check to see if the file exists. There are legit reasons for getting a Uri to a file that does not exist, such as for ACTION_IMAGE_CAPTURE. Still, it means that just getting the Uri is no guarantee that that the Uri is useful for reading content.

Compounding that problem is that "does the file exist" via exists() feels like it may be a bit dicey, especially for external storage.

So, it's pretty important to make sure that you have the right File object, and that it should point to an already-existing file, before you call getUriForFile().



Related Topics



Leave a reply



Submit