Warning: the method described in the post works well for Gmail, but apparently has some issues with other ACTION_SEND handlers (e.g. the MMS composer). A workaround is described in the comments.
I’ve just spent the day modifying one of my Android applications so that it no longer requires the use of the SD card; making it use the internal cache for what little storage is required. Everything went pretty smoothly, until I got to a part of the app that tries to share some data by sending an email with an attachment via Gmail (using an ACTION_SEND intent). Then things started behaving very oddly.
What I saw was that the Compose activity would launch correctly and my file would be shown as attached (as an example, I’ve attached a text file called Test.txt here); however when I sent the message, the attachment was not on the received email:

Or even shown in my Gmail sent folder.
A quick browse through the log showed the reason:
02-28 21:01:28.434: E/Gmail(19673): file:// attachment paths must point to file:///mnt/sdcard. Ignoring attachment file:///data/data/com.stephendnicholas.gmailattach/cache/Test.txt
So, it looks like this is something Gmail has explicitly ruled out; though I’m not sure why. At this point I could easily get all moany and ranty, but today was a glass half full day and so it was time for a workaround.
After a few false starts and a lot of searching, what I actually ended up doing was using a ContentProvider to provide access to the file from my application’s internal cache; as apparently Gmail can happily resolve attachments this way.
Unfortunately there wasn’t much example code to help me on my way, which is why I’ve decided to include a simple example here. There’s four main parts to this:
- The
ContentProviderthat provides access to the files from the application’s internal cache. Complete code, with comments, is shown below:
package com.stephendnicholas.gmailattach;
import java.io.File;
import java.io.FileNotFoundException;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.util.Log;
public class CachedFileProvider extends ContentProvider {
private static final String CLASS_NAME = "CachedFileProvider";
// The authority is the symbolic name for the provider class
public static final String AUTHORITY = "com.stephendnicholas.gmailattach.provider";
// UriMatcher used to match against incoming requests
private UriMatcher uriMatcher;
@Override
public boolean onCreate() {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
// Add a URI to the matcher which will match against the form
// 'content://com.stephendnicholas.gmailattach.provider/*'
// and return 1 in the case that the incoming Uri matches this pattern
uriMatcher.addURI(AUTHORITY, "*", 1);
return true;
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode)
throws FileNotFoundException {
String LOG_TAG = CLASS_NAME + " - openFile";
Log.v(LOG_TAG,
"Called with uri: '" + uri + "'." + uri.getLastPathSegment());
// Check incoming Uri against the matcher
switch (uriMatcher.match(uri)) {
// If it returns 1 - then it matches the Uri defined in onCreate
case 1:
// The desired file name is specified by the last segment of the
// path
// E.g.
// 'content://com.stephendnicholas.gmailattach.provider/Test.txt'
// Take this and build the path to the file
String fileLocation = getContext().getCacheDir() + File.separator
+ uri.getLastPathSegment();
// Create & return a ParcelFileDescriptor pointing to the file
// Note: I don't care what mode they ask for - they're only getting
// read only
ParcelFileDescriptor pfd = ParcelFileDescriptor.open(new File(
fileLocation), ParcelFileDescriptor.MODE_READ_ONLY);
return pfd;
// Otherwise unrecognised Uri
default:
Log.v(LOG_TAG, "Unsupported uri: '" + uri + "'.");
throw new FileNotFoundException("Unsupported uri: "
+ uri.toString());
}
}
// //////////////////////////////////////////////////////////////
// Not supported / used / required for this example
// //////////////////////////////////////////////////////////////
@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;
}
}
As you can see, all you really need to do is to overwrite the openFile(...) method. Although you can also override the query(...) method to provide more information to the application that calls you (it would make this example unnecessarily complicated, but I’m happy to provide code on request).
To make this provider available to use, you need to add a line to your AndroidManifest.xml defining the class and the authority (symbolic name) used to reference it:
<provider android:name="CachedFileProvider" android:authorities="com.stephendnicholas.gmailattach.provider"></provider>
Note: this needs to go inside the <application>...</application> definition in your AndroidManifest.xml.
- The utility method that creates a file in the internal cache:
public static void createCachedFile(Context context, String fileName,
String content) throws IOException {
File cacheFile = new File(context.getCacheDir() + File.separator
+ fileName);
cacheFile.createNewFile();
FileOutputStream fos = new FileOutputStream(cacheFile);
OutputStreamWriter osw = new OutputStreamWriter(fos, "UTF8");
PrintWriter pw = new PrintWriter(osw);
pw.println(content);
pw.flush();
pw.close();
}
- The utility method that creates the intent to send the content via Gmail (explicitly using Gmail, rather than using a chooser):
public static Intent getSendEmailIntent(Context context, String email,
String subject, String body, String fileName) {
final Intent emailIntent = new Intent(
android.content.Intent.ACTION_SEND);
//Explicitly only use Gmail to send
emailIntent.setClassName("com.google.android.gm","com.google.android.gm.ComposeActivityGmail");
emailIntent.setType("plain/text");
//Add the recipients
emailIntent.putExtra(android.content.Intent.EXTRA_EMAIL,
new String[] { email });
emailIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, subject);
emailIntent.putExtra(android.content.Intent.EXTRA_TEXT, body);
//Add the attachment by specifying a reference to our custom ContentProvider
//and the specific file of interest
emailIntent.putExtra(
Intent.EXTRA_STREAM,
Uri.parse("content://" + CachedFileProvider.AUTHORITY + "/"
+ fileName));
return emailIntent;
}
- The code that calls 2 & 3 to do something on a button press. Triggered in my dummy app by a button click:
Button button = (Button) findViewById(R.id.dostuff);
button.setOnClickListener(new OnClickListener() {
@Override
public void onClick(View v) {
try {
Utils.createCachedFile(GmailAttacherActivity.this,
"Test.txt", "This is a test");
startActivity(Utils.getSendEmailIntent(
GmailAttacherActivity.this,
"<YOUR_EMAIL_HERE>@<YOUR_DOMAIN>.com", "Test",
"See attached", "Test.txt"));
} catch (IOException e) {
e.printStackTrace();
} catch (ActivityNotFoundException e) {
Toast.makeText(GmailAttacherActivity.this,
"Gmail is not available on this device.",
Toast.LENGTH_SHORT).show();
}
}
});
Hopefully that’s all pretty straight forward, however feel free to ask any questions in the comments and I’ll help where I can.
The full source is now available as a complete (albeit very simple) Android app on github: Gmail Attacher.
39 Comments
Hello, i need to do the same but with a photo recently taken, but i don’t know how to addapt your part :( can you help me?? please!! here is the code
Code RemovedThank you very much!
Your solution also works for Google Docs.
Why Gmail limits attachments to the external memory is quite strange since it does not even validate relative URLs (file:///mnt/sdcard/../../data/xyz also works).
Martin
Thanks a lot!!!
I ran into the same issue and this is the example that I needed! This tutorial was long overdue by someone and I’m glad I found your page!
I wish the Android documentation would be better. What’s so hard about saying, hey, use a content provider to share a private file and this is how it’s done…?
Thanks again!!!
Your code worked like a charm and can also be used when storing private files via FileOutputStream!
The one thing I’d like to add, as a reminder is that the provider declaration in the manifest must be done within the application (e.g. <application>
<provider android:name=”MyProviderClassHere” android:authorities=”my.package.content.provider.here”/provider>/application>).
Thank you!
You should cover deleting the cache files, lest they build up unnecessarily and hog space on the storage.
Hi Shelly,
The files can be deleted using the standard Java mechanisms. For example:
For a single cache file:
File cacheFile = new File(context.getCacheDir() + File.separator + fileName);
Log.i("SDN", "Cache file '" + fileName + "' successfully deleted: " + cacheFile.delete());
For all files in the cache directory:
File[] cacheFiles = context.getCacheDir().listFiles();
for (File file : cacheFiles){
Log.e("SDN", "Cache file '" + file + "' deleted: " + file.delete());
}
You could also use a
FilenameFilterif the files of interest match a particular pattern.Unfortunately there’s not an easy way to tell when the cache file has been shared (
startActivityForResult(...)doesn’t work withACTION_SEND) so there is some question as to when you would do the clean up.You could put something in the
onResume(...)of your activity and this shouldn’t get called until the share intent returns; however I guess there’s no guarantee that the file has been finished with.I am a newbie to android. Wanna know from where this Utils word came from in onclick method.
Hi Rany,
The
Utilsis simply the name of my custom class that contains thecreateCachedFile(...)&getSendEmailIntent(...)methods. I’ve put those methods into a separate class to theActivitythat has theonclick, so that they can be easily re-used. As those methods arestaticI invoke them by using the class name, rather than a reference to an object of that class. Hopefully that makes sense :)Thanks for this piece of code. I was able to use it successfuly.
However, in my case this method disables attachments for other apps, such as a regular mail client, a twitter client. The MMS composer even crashes.
Is your code also sending attachments with these apps ? If so I may have another problem on my end..
Thank you for reading.
Tos
Hi tos,
Thanks for that, you’re correct – there does appear to be an issue using this method with some of the
ACTION_SENDhandlers.It seems to be very variable though, e.g. Evernote and Gmail work fine, Twitter doesn’t handle text files (and does so gracefully) and the MMS composer does seem to have serious issues.
I’ve changed the post so that the example code explicitly only calls the Gmail composer.
Also, a possible workaround is to use Martin’s approach, simply using relative paths to specify the file.
E.g. ‘file:///mnt/sdcard/../../data/xyz/file.txt’
I.e.
"file:///mnt/sdcard/../../" + context.getCacheDir() + File.separator + fileName;Hello Stephen !
You made my day, great code and pretty good idea.
I have tried the workaround using relative path but it not worked for me. I do not have a SD card, though.
One question: what is the license ? Public domain ? BSD ? Beer license ? :D
Thanks again
Thanks Marcelo, glad to hear it’s useful. I’ve not really considered the license issue before. Basically I’m happy for anyone to use it to do anything, with no need to pay, attribute, etc. However, at the same time, I don’t want anything unpleasant to come my way. So I think the MIT license is probably the best fit. I’ve added a page to this site explicitly stating that’s the case for any code on here, but let me know if you need it more formal than that.
Pingback: How to bundle the data captured from customized dialog(edittext,datepicker,spinner e.t.c) in .csv and attach to email in android? | PHP Developer Resource
Thanks for the interesting article.
Strange, but I have it worked out well for both Gmail and for Bluetooth:
Intent shareIntent = new Intent(Intent.ACTION_SEND);
shareIntent.setType(“application/zip”);
File shareFile = new File(getCacheDir() + File.separator + zip_filename);
shareFile.setReadable(true, false);
shareIntent.putExtra(Intent.EXTRA_STREAM, Uri.fromFile(shareFile));
startActivity(Intent.createChooser(shareIntent, getString(R.string.text_share)));
Indeed, simply calling “shareFile.setReadable(true, false);” is all it took for the simple approach to work. Seems the provider method is not needed. Thanks though!
i am trying to implement this, but i get a crash saying activity com.google.android.gm not found are you sure declared in manifest ? i am testing it in simulator, what should i do ?
in my case i am able to send attachment from my simulator using action_send, but in device it does not work, hence i am looking for a solution
if my package name is different, where should i change ?
regards
Hi Sunu,
I believe the problem is that that Gmail is not present on the emulator; meaning that the exception is legitimate. I have updated the code above so that it handles this better and now displays a Toast, instead of allowing an exception to occur.
I think you can get Gmail installed on the emulator, but I don’t believe it is an easy process. There’s a few tutorials online, but it takes a bit of work.
Are you experiencing the same problem on a real device?
I’d been struggling with this problem all day. Thanks for sharing the code!
I have the same problem as sunu.
can i get an email with working source code zip?
my email id is: hemanthkumar.uppada@gmail.com
i get a crash saying activity com.google.android.gm not found are you sure declared in manifest ?
I have declared it in manifest this way:
but still get activity not found exception Please suggest a solution.
Hi Hemanth,
As I’ve replied to Sunu above (and assuming that you are using the emulator as well), I believe the problem is that that Gmail is not present on the emulator; meaning that the exception is legitimate (that activity is not present on the device). I have updated the code above so that it handles this better and now displays a Toast, instead of allowing an exception to occur.
I’ve put the complete source of the demo app up on Github, so you should be able to get hold of it there: https://github.com/stephendnicholas/Android-Apps/tree/master/Gmail%20Attacher.
Hi,
New to android and really need this functionality in my app. Can you provide a complete file so that i can see its proper usage.
Thanks for the great article.
Hi Abdur,
I’ve put the complete source of the demo app up on Github, so you should be able to get hold of it there: https://github.com/stephendnicholas/Android-Apps/tree/master/Gmail%20Attacher.
It’s a rather simple app, but it shows how to use this approach.
Hi Stephen,
I am trying your code it’s working fine for me, But I am facing problem with 3.1 . to Gmail file not attaching. Please give me any solution for this problem.
Thanks .
from what part of the code is the app taking the file?and from where? i dont gt where is the test file stored and how does my app know where it is
Hi Amy,
The app is storing and retrieving the file from the application’s internal cache.
In my demo app, the file is created by the
createCachedFile(...)function:File cacheFile = new File(context.getCacheDir() + File.separator+ fileName);
cacheFile.createNewFile();
The call to
context.getCacheDir()returns the path to the cache folder for the application to use.The
CachedFileProviderdoes the samecontext.getCacheDir()call (line 56) to find out where to read the files from.Hopefully that makes sense :)
What version of Android did you test this on? Doesn’t seem to be working with my N1 android 2.3.3
Ah right if you use the folder: getFilesDir() it won’t work, but does work with getCacheDir()
Hi Blundell,
Yes, that’s correct. In this case I’ve only focused on getCacheDir().
If you wanted, you could easily write another ContentProvider for getFilesDir() (you’d just need to change line 56 in CachedFileProvider). Or you could modify the existing one to either use a slightly different URI structure, which specified which to fetch the file from. Or do a simple preference order (i.e. check cache first, if not found, then look in files, if not found, then error).
Superb… :)
I was able to share images with all apps using the MediaStore as follows:
// Step 1: Save the image
FileOutputStream fos = openFileOutput(filename + “.jpg”, MODE_WORLD_READABLE);
fos.write(bytes.toByteArray());
fos.close();
// Step 2: Get the path as a string
File jpg = getFileStreamPath(filename + “.jpg”);
// Step 3: Add the image to the MediaStore
String path = MediaStore.Images.Media.insertImage(getApplicationContext().getContentResolver(), jpg.getAbsolutePath(), jpg.getName(), filename);
// Step 4: Share using the context:// URI from the MediaStore
share.putExtra(Intent.EXTRA_STREAM, Uri.parse(path));
While it worked, I did have a number of complaints with this method. There was a conflict with Dropbox. I have Dropbox setup to upload every picture I take with my phone’s camera. Well it appears that Dropbox implemented this functionality by uploading every image added to the MediaStore. So that means if I share an image from my app, it also goes to Dropbox. Not exactly a great user experience. Also, I couldn’t figure out how to add .png files to the MediaStore. Lastly, the MediaStore renames your file, so the attachment comes out as 12914014980.jpg.
Martin’s method worked for me on everything except Google+
Is it possible sending Bitmaps using this method?
Thanks for this, I’ve been looking all over for an explanation to this idiotic dilemma. Cheers!
Can you show the code to attach more than one file to the mail?
I believe ACTION_SEND_MULTIPLE should be used and also putParcelableArrayListExtra,
Pingback: Default mail client - Error attached file - How-To Video
obrigado Nicolas, muito bom seu post!! ;)
Perfect. Thank you.
Gosh, thanks – appreciate you posting this.