21

I've followed this Google tutorial to start an intent to capture an image with new Intent(MediaStore.ACTION_IMAGE_CAPTURE). The tutorial recommends using the public directory with getExternalStoragePublicDirectory which would work great for my app. But then their example instead uses getExternalFilesDir. Then to pass the URI of the file to the intent with MediaStore.EXTRA_OUTPUT I have to get a content URI because I'd like to target Android N. Before targeting N I would just pass a file:// URI and everyone but Google was happy. Now on N I started getting the FileUriExposedException that seems to be not very favorable.

So given that I have a File like this...

private File createImageFile() throws IOException {
    String timeStamp = new SimpleDateFormat("yyyyMMdd_HHmmss").format(new Date());
    File storageDir = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "MyAppFolder");
    if (!storageDir.exists() && !storageDir.mkdir())
        Log.w(TAG, "Couldn't create photo folder: " + storageDir.getAbsolutePath());
    File image = new File(storageDir, timeStamp + ".jpg");
    mCurrentPhotoPath = image.getAbsolutePath();
    return image;
}

...can I use a built-in provider for the public pictures directory to get a content URI? If so how?

I've tried something like

takePictureIntent.putExtra(
    MediaStore.EXTRA_OUTPUT, 
    FileProvider.getUriForFile(this, MediaStore.AUTHORITY, createImageFile()));

but it just throws

IllegalArgumentException: Missing android.support.FILE_PROVIDER_PATHS meta-data

Is that Authority correct? If I must use my own provider to share the public file then what path can I specify in my FILE_PROVIDER_PATHS meta-data? All of the options I can find are for private directories.

Paul
  • 1,907
  • 2
  • 21
  • 29
  • 1
    "Is that Authority correct?" -- no, because you are not the `MediaStore`. "If I must use my own provider to share the public file then what path can I specify in my FILE_PROVIDER_PATHS meta-data?" -- `` gives you the root of external storage. There is no way with `FileProvider` to specify the directory that you are trying to use specifically. – CommonsWare Jun 21 '16 at 20:02
  • @CommonsWare If there is no way for a FileProvider to use that directory then are my only options to use a private directory or stop targeting N? That seems silly. Thanks for the advise. – Paul Jun 22 '16 at 15:40
  • "If there is no way for a FileProvider to use that directory" -- that is not what I wrote. That being said, since the details of your desired subdirectory vary by device, trying to use `` would be risky. "then are my only options to use a private directory or stop targeting N?" -- no. You can write your own `ContentProvider`. Or, you can write a plugin for my `StreamProvider`. Or you can use a custom directory on external storage, one that you control its path from the external storage root directory (though this may be what you meant by "private directory"). – CommonsWare Jun 22 '16 at 15:45
  • Thanks CommonsWare and sorry about my misunderstanding. Until I see a better way I'll just create the file in a directory that I can host with my `FileProvider`. Then in `onActivityResult` I can copy the file to its final resting place in the external storage public directory. The custom and third-party ContentProviders seem a bit heavy for the job. – Paul Jun 22 '16 at 21:30

2 Answers2

16

TL:DR <external-path name="MyAppFolder" path="." /> works, but is it safe?

You will need to make your own FileProvider. It's worth reading its documentation, but here's a brief rundown.

As @CommonsWare mentioned, you'll need to replace MediaStore.AUTHORITY from your example with "com.example.fileprovider", which is the authority you will define in your manifest like so...

Within the <application> tag in the AndroidManifest, type the following:

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

The exception you're getting is due to not having that <meta-data> tag that links to an xml file with all the paths that your FileProvider is allowed to touch. But it's what goes in this file that I've been struggling with myself.

Towards the end of the tutorial you linked, at the end of the Save the Full-size Photo section, the example Google gives for this file_paths.xml is:

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <external-path name="my_images" path="Android/data/com.example.package.name/files/Pictures" />
</paths>

"my_images" would be replaced with "MyAppFolder" in your example. In that tutorial Google claims

The path component corresponds to the path that is returned by getExternalFilesDir() when called with Environment.DIRECTORY_PICTURES. Make sure that you replace com.example.package.name with the actual package name of your app.

...and typing that out made me realize that that is NOT referring to the public Pictures directory that you and I are looking for. Which explains why using that path produces this exception:

java.lang.IllegalArgumentException: Failed to find configured root that contains /storage/emulated/0/Pictures/MyAppFolder/{filename}.jpg

I'll leave that in here for future reference, and move on to answering your big question--what path can you specify to allow other apps to interact with a file in the public Pictures directory created by your app targeting API level 24+?

As @CommonsWare said, "the details of your desired subdirectory vary by device" so you can't declare the path to the public Pictures directory, but I did find a way that works.

<external-path name="MyAppFolder" path="." /> will allow your FileProvider to give a content URI to other apps (like the Camera), which can then read and write to the file it resolves to.

If there are any dangers or downsides to this, I would love to hear them.

James Davis
  • 474
  • 4
  • 10
pumpkinpie65
  • 960
  • 2
  • 14
  • 16
  • I vote for "this is safe." The provider appears to be locked down pretty well, based on the manifest XML; it is not exported, so other apps only get permissions to your files based on an explicit grant in the intent (Intent.FLAG_GRANT_READ_URI_PERMISSION). – greeble31 Aug 15 '18 at 15:29
7

Here is what I'm using in my app for Pictures public directory.

  1. Create the file like this:

    File theFile = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES), "YOUR_APP_NAME" + File.separator + "YOUR_FILE_NAME");
    
  2. Specify the following path in file provider's meta data:

    <paths>
        <external-path name="secure_name" path="Pictures/YOUR_APP_NAME" />
    </paths>
    

"secure_name" is used to hide the name of the subdirectory ("YOUR_APP_NAME" in this case) you're sharing for security reasons.

"Pictures" corresponds to Environment.DIRECTORY_PICTURES.

  1. Obtain Uri to send with Intent:

    final Uri theUri = FileProvider.getUriForFile(activity,
                        getString(R.string.fileprovider_authorities),
                        theFile);
    theIntent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
    theIntent.putExtra(MediaStore.EXTRA_OUTPUT, theUri);
    
Stan Mots
  • 1,193
  • 2
  • 15
  • 16