22

On my application I write a file to the internal storage as covered on android developer. Then later on I wish to email the file I wrote into the internal storage. Here is my code and the error I am getting, any help will be appreciated.

FileOutputStream fos = openFileOutput(xmlFilename, MODE_PRIVATE);
fos.write(xml.getBytes());
fos.close();
Intent intent = new Intent(android.content.Intent.ACTION_SEND);
intent.setType("text/plain");
...
Uri uri = Uri.fromFile(new File(xmlFilename));
intent.putExtra(android.content.Intent.EXTRA_STREAM, uri);
startActivity(Intent.createChooser(intent, "Send eMail.."));

And the error is

file:// attachment path must point to file://mnt/sdcard. Ignoring attachment file://...

javanna
  • 59,145
  • 14
  • 144
  • 125
Mr Jackson
  • 1,023
  • 2
  • 10
  • 11

7 Answers7

24

I think you may have found a bug (or at least unnecessary limitation) in the android Gmail client. I was able to work around it, but it strikes me as too implementation specific, and would need a little more work to be portable:

First CommonsWare is very much correct about needing to make the file world readable:

fos = openFileOutput(xmlFilename, MODE_WORLD_READABLE);

Next, we need to work around Gmail's insistence on the /mnt/sdcard (or implementation specific equivalent?) path:

Uri uri = Uri.fromFile(new File("/mnt/sdcard/../.."+getFilesDir()+"/"+xmlFilename));

At least on my modified Gingerbread device, this is letting me Gmail an attachment from private storage to myself, and see the contents using the preview button when I receive it. But I don't feel very "good" about having to do this to make it work, and who knows what would happen with another version of Gmail or another email client or a phone which mounts the external storage elsewhere.

Chris Stratton
  • 39,853
  • 6
  • 84
  • 117
  • Please keep in mind that this is a hack to directly overcome a particular odd expectation on the part of the authors of one particular target program. Today, one should give consideration to looking at the Content Provider and similar methods as discussed in the other answers which are more more characteristically "Android" in style and hopefully more general in utility. – Chris Stratton May 29 '15 at 15:08
13

I have been struggling with this issue lately and I would like to share the solution I found, using FileProvider, from the support library. its an extension of Content Provider that solve this problem well without work-around, and its not too-much work.

As explained in the link, to activate the content provider: in your manifest, write:

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

the meta data should indicate an xml file in res/xml folder (I named it file_paths.xml):

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

the path is empty when you use the internal files folder, but if for more general location (we are now talking about the internal storage path) you should use other paths. the name you write will be used for the url that the content provider with give to the file.

and now, you can generate a new, world readable url simply by using:

Uri contentUri = FileProvider.getUriForFile(context, "com.yourdomain.yourapp.fileprovider", file);

on any file from a path in the res/xml/file_paths.xml metadata.

and now just use:

    Intent mailIntent = new Intent(Intent.ACTION_SEND);
    mailIntent.setType("message/rfc822");
    mailIntent.putExtra(Intent.EXTRA_EMAIL, recipients);

    mailIntent.putExtra(Intent.EXTRA_SUBJECT, subject);
    mailIntent.putExtra(Intent.EXTRA_TEXT, body);
    mailIntent.putExtra(Intent.EXTRA_STREAM, contentUri);

    try {
        startActivity(Intent.createChooser(mailIntent, "Send email.."));
    } catch (android.content.ActivityNotFoundException ex) {
        Toast.makeText(this, R.string.Message_No_Email_Service, Toast.LENGTH_SHORT).show();
    }

you don't need to give a permission, you do it automatically when you attach the url to the file.

and you don't need to make your file MODE_WORLD_READABLE, this mode is now deprecated, make it MODE_PRIVATE, the content provider creates new url for the same file which is accessible by other applications.

I should note that I only tested it on an emulator with Gmail.

Jadamec
  • 913
  • 11
  • 13
Yaniv006
  • 131
  • 1
  • 3
  • 1
    Seems to work nicely. Most elegang solution I've seen for this (really annoying) issue I've encountered so far. – Michael A. May 07 '16 at 19:44
7

Chris Stratton proposed good workaround. However it fails on a lot of devices. You should not hardcode /mnt/sdcard path. You better compute it:

String sdCard = Environment.getExternalStorageDirectory().getAbsolutePath();
Uri uri = Uri.fromFile(new File(sdCard + 
          new String(new char[sdCard.replaceAll("[^/]", "").length()])
                    .replace("\0", "/..") + getFilesDir() + "/" + xmlFilename));
Dmitry Kochin
  • 3,830
  • 4
  • 22
  • 14
5

Taking into account recommendations from here: http://developer.android.com/reference/android/content/Context.html#MODE_WORLD_READABLE, since API 17 we're encouraged to use ContentProviders etc. Thanks to that guy and his post http://stephendnicholas.com/archives/974 we have a solution:

public class CachedFileProvider extends ContentProvider {
public static final String AUTHORITY = "com.yourpackage.gmailattach.provider";
private UriMatcher uriMatcher;
@Override
public boolean onCreate() {
    uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
    uriMatcher.addURI(AUTHORITY, "*", 1);
    return true;
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
    switch (uriMatcher.match(uri)) {
        case 1:// If it returns 1 - then it matches the Uri defined in onCreate
            String fileLocation = AppCore.context().getCacheDir() + File.separator +     uri.getLastPathSegment();
            ParcelFileDescriptor pfd = ParcelFileDescriptor.open(new File(fileLocation),     ParcelFileDescriptor.MODE_READ_ONLY);
            return pfd;
        default:// Otherwise unrecognised Uri
            throw new FileNotFoundException("Unsupported uri: " + uri.toString());
    }
}
@Override public int update(Uri uri, ContentValues contentvalues, String s, String[] as) { return     0; }
@Override public int delete(Uri uri, String s, String[] as) { return 0; }
@Override public Uri insert(Uri uri, ContentValues contentvalues) { return null; }
@Override public String getType(Uri uri) { return null; }
@Override public Cursor query(Uri uri, String[] projection, String s, String[] as1, String s1) {     return null; }
}

Than create file in Internal cache:

    File tempDir = getContext().getCacheDir();
    File tempFile = File.createTempFile("your_file", ".txt", tempDir);
    fout = new FileOutputStream(tempFile);
    fout.write(bytes);
    fout.close();

Setup Intent:

...
emailIntent.putExtra(Intent.EXTRA_STREAM, Uri.parse("content://" + CachedFileProvider.AUTHORITY + "/" + tempFile.getName()));

And register Content provider in AndroidManifest file:

<provider android:name="CachedFileProvider" android:authorities="com.yourpackage.gmailattach.provider"></provider>
far.be
  • 689
  • 6
  • 8
4
File.setReadable(true, false);

worked for me.

RobinBobin
  • 555
  • 7
  • 17
  • I was able to send a file from internal storage using Gmail with a FileProvider as long as the File object that created the URI was set to readable – joates Aug 01 '14 at 13:53
  • Worked for me with Gmail but not with other mail apps on Android (like Mailbox). I think the content provider solution from far.be below is the most "correct" approach. – ToddH Oct 20 '14 at 19:41
  • Content Providers are indeed the *intended* solution in the Android architecture for exposing an otherwise private file. – Chris Stratton May 29 '15 at 14:56
1

If you are going to use internal storage, try to use the exact storage path:

Uri uri = Uri.fromFile(new File(context.getFilesDir() + File.separator + xmlFilename));

or additionally keep changing the file name in the debugger and just call "new File(blah).exists()" on each of the file to see quickly what exact name is your file

it could also be an actual device implementation problem specific to your device. have you tried using other devices/emulator?

David T.
  • 22,301
  • 23
  • 71
  • 123
1

The error is enough specific: you should use file from external storage in order to make an attachment.

Vladimir Ivanov
  • 42,730
  • 18
  • 77
  • 103
  • 3
    Or change `MODE_PRIVATE` to `MODE_WORLD_READABLE`. Since you did not write the email program, it cannot read your private files. – CommonsWare May 20 '11 at 13:53
  • Thanks for your answer. I can read that, but I develop an application to a phone without external storage since the phone has 32GB built storage. – Mr Jackson May 20 '11 at 13:58
  • Then use @CommonsWare comment: MODE_WORLD_READABLE can save you. – Vladimir Ivanov May 20 '11 at 13:59
  • cannot do that, the File() constructor does not handle permission. and the FileReader() is not supported by the Uri.fromFile(). – Mr Jackson May 20 '11 at 14:02
  • Stop shooting answers from your belly and not your head. Changing the permission does not affects the path. please refer to the error! I am looking for a way to direct it to the right path. Or an alternative way to achieve what I have depicted! – Mr Jackson May 20 '11 at 14:05
  • 2
    @Mr Jackson: " a phone without external storage since the phone has 32GB built storage" -- your device has external storage. "External storage" does not mean "SD card", it means "accessible from a host PC". "the File() constructor does not handle permission" -- it does not have to, if you follow the very simple instructions in the first words of my previous comment ("Or change `MODE_PRIVATE` to `MODE_WORLD_READABLE`"). – CommonsWare May 20 '11 at 14:10
  • 2
    even triedget Uri uri = Uri.fromFile(FileStreamPath(xmlFilename)) but still no good. – Mr Jackson May 20 '11 at 14:22