41

My goal is to create a XML file on internal storage and then send it through the share Intent.

I'm able to create a XML file using this code

FileOutputStream outputStream = context.openFileOutput(fileName, Context.MODE_WORLD_READABLE);
PrintStream printStream = new PrintStream(outputStream);
String xml = this.writeXml(); // get XML here
printStream.println(xml);
printStream.close();

I'm stuck trying to retrieve a Uri to the output file in order to share it. I first tried to access the file by converting the file to a Uri

File outFile = context.getFileStreamPath(fileName);
return Uri.fromFile(outFile);

This returns file:///data/data/com.my.package/files/myfile.xml but I cannot appear to attach this to an email, upload, etc.

If I manually check the file length, it's proper and shows there is a reasonable file size.

Next I created a content provider and tried to reference the file and it isn't a valid handle to the file. The ContentProvider doesn't ever seem to be called a any point.

Uri uri = Uri.parse("content://" + CachedFileProvider.AUTHORITY + "/" + fileName);
return uri;

This returns content://com.my.package.provider/myfile.xml but I check the file and it's zero length.

How do I access files properly? Do I need to create the file with the content provider? If so, how?

Update

Here is the code I'm using to share. If I select Gmail, it does show as an attachment but when I send it gives an error Couldn't show attachment and the email that arrives has no attachment.

public void onClick(View view) {
    Log.d(TAG, "onClick " + view.getId());

    switch (view.getId()) {
        case R.id.share_cancel:
            setResult(RESULT_CANCELED, getIntent());
            finish();
            break;

        case R.id.share_share:

            MyXml xml = new MyXml();
            Uri uri;
            try {
                uri = xml.writeXmlToFile(getApplicationContext(), "myfile.xml");
                //uri is  "file:///data/data/com.my.package/files/myfile.xml"
                Log.d(TAG, "Share URI: " + uri.toString() + " path: " + uri.getPath());

                File f = new File(uri.getPath());
                Log.d(TAG, "File length: " + f.length());
                // shows a valid file size

                Intent shareIntent = new Intent();
                shareIntent.setAction(Intent.ACTION_SEND);
                shareIntent.putExtra(Intent.EXTRA_STREAM, uri);
                shareIntent.setType("text/plain");
                startActivity(Intent.createChooser(shareIntent, "Share"));
            } catch (FileNotFoundException e) {
                e.printStackTrace();
            }

            break;
    }
}

I noticed that there is an Exception thrown here from inside createChooser(...), but I can't figure out why it's thrown.

E/ActivityThread(572): Activity com.android.internal.app.ChooserActivity has leaked IntentReceiver com.android.internal.app.ResolverActivity$1@4148d658 that was originally registered here. Are you missing a call to unregisterReceiver()?

I've researched this error and can't find anything obvious. Both of these links suggest that I need to unregister a receiver.

I have a receiver setup, but it's for an AlarmManager that is set elsewhere and doesn't require the app to register / unregister.

Code for openFile(...)

In case it's needed, here is the content provider I've created.

public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
    String fileLocation = getContext().getCacheDir() + "/" + uri.getLastPathSegment();

    ParcelFileDescriptor pfd = ParcelFileDescriptor.open(new File(fileLocation), ParcelFileDescriptor.MODE_READ_ONLY);
    return pfd;
}
Community
  • 1
  • 1
Kirk
  • 15,831
  • 19
  • 73
  • 105
  • I think the first method should work, rather than creating a contentProvider. ContentProvider is used to return data rather than a whole file, it wont serve your needs. My guess is there are no apps to handle your xml. Can you post the code where your share intent is being created – nandeesh Aug 29 '12 at 03:25
  • Added more of the code in case it makes it more obvious as well as an Exception I don't understand if its related or not. – Kirk Aug 30 '12 at 01:44
  • Have you created a ContentProvider? If you have, can you post your code for it. If you haven't you need to create a ContentProvider and override the openFile method. That method will be called by gmail when it attempts to open the file associated with the content://com.my.package.provider/myfile.xml uri. – Rob Aug 30 '12 at 01:49
  • I appended the openFile(...) method of the ContentProvider I created. – Kirk Aug 30 '12 at 02:03
  • see my answer here: http://stackoverflow.com/questions/15377373/how-to-put-a-video-file-in-android-custom-content-provider – dangalg Jan 26 '15 at 12:14

4 Answers4

40

It is possible to expose a file stored in your apps private directory via a ContentProvider. Here is some example code I made showing how to create a content provider that can do this.

Manifest

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
  package="com.example.providertest"
  android:versionCode="1"
  android:versionName="1.0">

  <uses-sdk android:minSdkVersion="11" android:targetSdkVersion="15" />

  <application android:label="@string/app_name"
    android:icon="@drawable/ic_launcher"
    android:theme="@style/AppTheme">

    <activity
        android:name=".MainActivity"
        android:label="@string/app_name">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>

    <provider
        android:name="MyProvider"
        android:authorities="com.example.prov"
        android:exported="true"
        />        
  </application>
</manifest>

In your ContentProvider override openFile to return the ParcelFileDescriptor

@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {       
     File cacheDir = getContext().getCacheDir();
     File privateFile = new File(cacheDir, "file.xml");

     return ParcelFileDescriptor.open(privateFile, ParcelFileDescriptor.MODE_READ_ONLY);
}

Make sure you have copied your xml file to the cache directory

    private void copyFileToInternal() {
    try {
        InputStream is = getAssets().open("file.xml");

        File cacheDir = getCacheDir();
        File outFile = new File(cacheDir, "file.xml");

        OutputStream os = new FileOutputStream(outFile.getAbsolutePath());

        byte[] buff = new byte[1024];
        int len;
        while ((len = is.read(buff)) > 0) {
            os.write(buff, 0, len);
        }
        os.flush();
        os.close();
        is.close();

    } catch (IOException e) {
        e.printStackTrace(); // TODO: should close streams properly here
    }
}

Now any other apps should be able to get an InputStream for your private file by using the content uri (content://com.example.prov/myfile.xml)

For a simple test, call the content provider from a seperate app similar to the following

    private class MyTask extends AsyncTask<String, Integer, String> {

    @Override
    protected String doInBackground(String... params) {

        Uri uri = Uri.parse("content://com.example.prov/myfile.xml");
        InputStream is = null;          
        StringBuilder result = new StringBuilder();
        try {
            is = getApplicationContext().getContentResolver().openInputStream(uri);
            BufferedReader r = new BufferedReader(new InputStreamReader(is));
            String line;
            while ((line = r.readLine()) != null) {
                result.append(line);
            }               
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            try { if (is != null) is.close(); } catch (IOException e) { }
        }

        return result.toString();
    }

    @Override
    protected void onPostExecute(String result) {
        Toast.makeText(CallerActivity.this, result, Toast.LENGTH_LONG).show();
        super.onPostExecute(result);
    }
}
Rob
  • 4,593
  • 3
  • 29
  • 29
  • 1
    Thank you for the reply. I'm hoping to use a ContentProvider if possible to access the private data. getExternalCacheDir() isn't supported on API 7 which I need to target. – Kirk Aug 30 '12 at 03:28
  • On API 7 you can use getExternalStorageDirectory() to get a public directory. I'm not sure it is possible to provide public access to the apps private portion of the file system, even with a ContentProvider. – Rob Aug 30 '12 at 03:36
  • Thank you for the suggestion. I'm looking to write to internal storage rather than the sdcard. Any thoughts on how to accomplish that? – Kirk Aug 31 '12 at 02:19
  • 1
    Sorry Kirk I was wrong, it is possible to access private files via a ContentProvider. Your problem was bugging me so I gave it a go, answer has been updated to show you how I constructed the ContentProvider so i can expose private files. – Rob Aug 31 '12 at 06:14
  • Thank you for your work. I won't be able to try it until later, but this is what I'm going for. – Kirk Aug 31 '12 at 14:15
  • 1
    I went to try to implement this and the error must be someplace else then since my implementation is essentially the same already. Sadly, I'm giving up on this now since I've spent 4 days of varying attempts to simply share a file from internal storage. VERY frustrated. I'll mark yours as the answer since you've done so much work. – Kirk Sep 01 '12 at 00:43
  • The most likely problem you are having with your ContentProvider is the URI you are passing matching the URI in your manifest, double check they are compatible. Try to isolate your ContentProvider debugging from the sharing portion of your app, make a small test app that communicates with your main apps content provider, once you get that working start trying to share the URI with the email app. Good luck =) – Rob Sep 01 '12 at 04:10
  • There is a flaw in this approach. Actually the method copyFileToInternal() is creating a duplicate file in cache memory for each file in internal memory. So, if you have 100 MB of files in Internal Memory, this approach will increase the Phone memory to 200 MB by creating the duplicate files in Cache Memory. I think, its not a reliable solution. – Yasir Ali May 09 '14 at 12:12
  • Thank you. This is very helpful and time saver answer. – mifthi Aug 05 '16 at 11:47
15

So Rob's answer is correct I assume but I did it a bit differently. As far as I understand, with the setting in in provider:

android:exported="true"

you are giving public access to all your files?! Anyway, a way to give only access to some files is to define file path permissions in the following way:

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

and then in your XML directory you define file_paths.xml file as follows:

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

now, the "allfiles" gives the same kind of public permission I guess as the option android:exported="true" but you don't really want that I guess so to define a subdirectory is the next line. Then all you have to do is store the files you want to share, there in that dir.

Next what you have to do is, as also Rob says, obtain a URI for this file. This is how I did it:

Uri contentUri = FileProvider.getUriForFile(context, "com.your.app.package", sharedFile);

Then, when I have this URI, I had to attach to it permissions for other app to use it. I was using or sending this file URI to camera app. Anyway this is the way how I got the other app package info and granted permissions to the URI:

PackageManager packageManager = getPackageManager();
List<ResolveInfo> list = packageManager.queryIntentActivities(cameraIntent, PackageManager.MATCH_DEFAULT_ONLY);
if (list.size() < 1) {
    return;
}
String packageName = list.get(0).activityInfo.packageName;
grantUriPermission(packageName, sharedFileUri, Intent.FLAG_GRANT_WRITE_URI_PERMISSION | Intent.FLAG_GRANT_READ_URI_PERMISSION);

ClipData clipData = ClipData.newRawUri("CAMFILE", sharedFileUri);
cameraIntent.setClipData(clipData);
cameraIntent.setFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
cameraIntent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
startActivityForResult(cameraIntent, GET_FROM_CAMERA);

I left the code for camera as I did not want to take some other example I did not work on. But this way you see that you can attach permissions to URI itself.

The camera's thing is that I can set it via ClipData and then additionally set permissions. I guess in your case you only need FLAG_GRANT_READ_URI_PERMISSION as you are attaching a file to an email.

Here is the link to help on FileProvider as I based all of my post on the info I found there. Had some trouble finding a package info for camera app though.

Hope it helps.

Dusko
  • 626
  • 2
  • 6
  • 20
4

If you need to permission other apps to see your app's private files (for Share, or otherwise) you might be able to save some time and just use v4 compat library's FileProvider

Splash
  • 1,260
  • 12
  • 20
0

This is what i'm using:

I combined some answers and used the current AndroidX Doku: Sharing files Android Development

Basic Process: You change the manifest to make it possible for other apps to access your local files. the filepath's that are allowed to be accessed from outside are found in the res/xml/filepaths.xml. When sharing you create an intent to share and set a Flag that temporarily allowed the other app to access your local files. Android documentation claims this is the secure way to share files

Step1: Add FileProvider to Manifest

    <provider
        android:name="androidx.core.content.FileProvider"
        android:authorities="com.YOUR.APP.PACKAGE.fileprovider"
        android:grantUriPermissions="true"
        android:exported="false">
        <meta-data
            android:name="android.support.FILE_PROVIDER_PATHS"
            android:resource="@xml/filepaths" />
    </provider>

Step2: Add filepaths.xml to res/xml (if XML folder does not exists just create it yourself)

<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
    <files-path path="share/" name="share" />
</paths>

Step3: Use a function like this to start a file share. this function moves the file to the predefined share folder and creates a Url to it. the ShareDir is the File pointing to the files/share/ directory. the copy_File function copies the given file to the share directory in order to be accessible from the outside. The function also makes it possible to Send the File as email with given header and body. if not needed just set it to empty strings

public void ShareFiles(Activity activity, List<File> files, String header, String body) {

    ArrayList<Uri> uriList = new ArrayList<>();
    if(files != null) {
        for (File f : files) {
            if (f.exists()) {


                File file_in_share = copy_File(f, ShareDir);
                if(file_in_share == null)
                    continue;
                // Use the FileProvider to get a content URI

                try {
                    Uri fileUri = FileProvider.getUriForFile(
                            activity,
                            "com.YOUR.APP.PACKAGE.fileprovider",
                            file_in_share);

                    uriList.add(fileUri);
                } catch (IllegalArgumentException e) {
                    Log.e("File Selector",
                            "The selected file can't be shared: " + f.toString());
                }
            }
        }
    }
    if(uriList.size() == 0)
    {
        Log.w("StorageModule", "ShareFiles: no files to share");
        return;
    }
    Intent intent = new Intent(Intent.ACTION_SEND_MULTIPLE);
    intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
    intent.setType("text/html");
    intent.putExtra(Intent.EXTRA_SUBJECT, header);
    intent.putExtra(Intent.EXTRA_TEXT, body);
    intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uriList);

    activity.startActivity(Intent.createChooser(intent, "Share Files"));
}
save_jeff
  • 169
  • 1
  • 2
  • 9