Email from internal storage
Asked Answered
F

7

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://...

Franctireur answered 20/5, 2011 at 13:50 Comment(0)
H
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.

Headboard answered 20/5, 2011 at 16:14 Comment(1)
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.Headboard
H
14

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.

Highwrought answered 5/3, 2015 at 9:42 Comment(1)
Seems to work nicely. Most elegang solution I've seen for this (really annoying) issue I've encountered so far.Toleration
B
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));
Bioastronautics answered 14/8, 2012 at 15:7 Comment(0)
B
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>
Barbarous answered 11/8, 2013 at 10:57 Comment(1)
Don't forget to add android:grantUriPermissions="true" to the provider.Tinishatinker
C
4
File.setReadable(true, false);

worked for me.

Conium answered 16/7, 2014 at 15:40 Comment(3)
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 readableMetabolic
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.Apologia
Content Providers are indeed the intended solution in the Android architecture for exposing an otherwise private file.Headboard
S
1

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

Syllepsis answered 20/5, 2011 at 13:52 Comment(7)
Or change MODE_PRIVATE to MODE_WORLD_READABLE. Since you did not write the email program, it cannot read your private files.Greyhound
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.Franctireur
Then use @Greyhound comment: MODE_WORLD_READABLE can save you.Syllepsis
cannot do that, the File() constructor does not handle permission. and the FileReader() is not supported by the Uri.fromFile().Franctireur
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!Franctireur
@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").Greyhound
even triedget Uri uri = Uri.fromFile(FileStreamPath(xmlFilename)) but still no good.Franctireur
B
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?

Bine answered 19/9, 2013 at 1:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.