Android: Attaching files from internal cache to Gmail

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:

  1. The ContentProvider that 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.

  1. 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();
}
  1. 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;
}
  1. 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.

45 Comments

  • Ricardo
    02/29/2012 - 5:03 pm | Permalink

    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 Removed

  • Martin
    03/03/2012 - 11:12 am | Permalink

    Thank 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

  • Whatzit
    03/10/2012 - 7:11 am | Permalink

    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!!!

  • Whatzit
    03/10/2012 - 6:22 pm | Permalink

    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!

  • Shelly
    03/13/2012 - 5:34 am | Permalink

    You should cover deleting the cache files, lest they build up unnecessarily and hog space on the storage.

    • 04/07/2012 - 12:01 pm | Permalink

      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 FilenameFilter if 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 with ACTION_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.

  • rany
    03/23/2012 - 9:00 am | Permalink

    I am a newbie to android. Wanna know from where this Utils word came from in onclick method.

    • 03/23/2012 - 3:15 pm | Permalink

      Hi Rany,

      The Utils is simply the name of my custom class that contains the createCachedFile(...) & getSendEmailIntent(...) methods. I’ve put those methods into a separate class to the Activity that has the onclick, so that they can be easily re-used. As those methods are static I invoke them by using the class name, rather than a reference to an object of that class. Hopefully that makes sense :)

  • tos
    03/29/2012 - 3:55 pm | Permalink

    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

    • 04/07/2012 - 3:36 pm | Permalink

      Hi tos,

      Thanks for that, you’re correct – there does appear to be an issue using this method with some of the ACTION_SEND handlers.

      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;

  • 05/03/2012 - 1:28 am | Permalink

    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

    • 05/03/2012 - 11:53 am | Permalink

      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

  • ReCo
    06/08/2012 - 11:15 am | Permalink

    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)));

    • 02/15/2013 - 9:30 pm | Permalink

      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!

  • sunu
    06/19/2012 - 12:09 pm | Permalink

    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

    • 07/26/2012 - 10:20 am | Permalink

      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?

  • Dheeraj
    06/21/2012 - 2:43 pm | Permalink

    I’d been struggling with this problem all day. Thanks for sharing the code!

  • hemanth
    07/20/2012 - 11:29 am | Permalink

    I have the same problem as sunu.

  • hemanth
    07/20/2012 - 12:44 pm | Permalink

    can i get an email with working source code zip?
    my email id is: hemanthkumar.uppada@gmail.com

  • hemanth
    07/20/2012 - 12:45 pm | Permalink

    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.

    • 07/26/2012 - 10:22 am | Permalink

      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.

  • Abdur Rahman
    07/21/2012 - 12:15 pm | Permalink

    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.

  • amy
    07/21/2012 - 7:01 pm | Permalink

    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

    • 07/26/2012 - 10:39 am | Permalink

      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 CachedFileProvider does the same context.getCacheDir() call (line 56) to find out where to read the files from.

      Hopefully that makes sense :)

  • 07/25/2012 - 12:01 pm | Permalink

    What version of Android did you test this on? Doesn’t seem to be working with my N1 android 2.3.3

    • 07/25/2012 - 12:13 pm | Permalink

      Ah right if you use the folder: getFilesDir() it won’t work, but does work with getCacheDir()

      • 07/26/2012 - 10:30 am | Permalink

        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).

  • Faison
    08/01/2012 - 10:43 am | Permalink

    Superb… :)

  • 09/06/2012 - 5:32 am | Permalink

    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+

  • Danielle
    09/13/2012 - 8:28 am | Permalink

    Is it possible sending Bitmaps using this method?

  • 09/25/2012 - 3:57 pm | Permalink

    Thanks for this, I’ve been looking all over for an explanation to this idiotic dilemma. Cheers!

  • John
    02/26/2013 - 6:27 am | Permalink

    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

  • 03/01/2013 - 1:49 am | Permalink

    obrigado Nicolas, muito bom seu post!! ;)

  • 04/12/2013 - 11:57 pm | Permalink

    Heya Nicholas,

    Would you mind sharing the solution to build the query(…) cursor necessary to provide metadata about the item?

  • Mariusz
    04/14/2013 - 8:50 am | Permalink

    Perfect. Thank you.

  • k
    04/16/2013 - 4:14 am | Permalink

    Gosh, thanks – appreciate you posting this.

  • Harry
    06/12/2013 - 8:46 am | Permalink

    Thanks Stephen for providing this tutorial. Actually it pretty cumbersome to send attachments using mail… Any idea what is missing for me:

    Implemented a similar content provider, stored files to the cached dir, declared the provider for my app, built an ACTION_SENT_MULTIPLE intent (that’s a difference) and starting it through a chooser presenting the standard email and the Gmail client. I’m using a Nexus 4 Android 4.2.2.

    Problem: when selecting the standard email client, the mail appears – but without the (single) attachment added. Adding some logs shows the mail client is contacting my content provider calling query () twice, then getType () once, but never calls openFile ()

    I had a permission missing message in LogCat first, but have been able to get rid of it adding android:exported=”true” to the provider declaration.

    Any idea what is missing? LogCat shows no other messages then the query / query / getType sequence, the attachment is simply missing.

    I didn’t test it with Gmail so far as this beast is syncing my account forever after having it setup.

    The whole code kind of worked not using a content provider but instead adding the Uris using the file scheme. I ran into the usual problem however the attachments were shown in the composer but not send as the mail app is not allowed to access files in my app. SD card is not an option as the Nexus has none…

    Thanks for any hint.

    Harry

  • 06/14/2013 - 8:30 am | Permalink

    Thank you for this tutorial! Though I will not use it or any of the code, it helped me gain a great insight!

  • Paul Dingemans
    07/16/2013 - 6:40 pm | Permalink

    @Stephen: thank for your tutorial which was a very good starting point.

    Like many others I had problems with the default email app of android. But I have found why the native email app doesn’t work with the provider and have a solution as well. Now the content provider works well both with gmail and the native android app.

    The source for the native email app can be found at https://android.googlesource.com/platform/packages/apps/Email. File src\com\android\email\activity\MessageCompose.java contains procedure loadAttachmentInfo(Uri uri) which calls the query method of the content provider to determine the size of the file. As this method, and method getType, are not implemented the native email app can not include attachment specified with a content-uri.

    Change following:

    @Override
    public String getType(Uri uri) {
    switch (uriMatcher.match(uri)) {

    // If it returns 1 - then it matches the Uri defined in onCreate
    case 1:
    return "text/plain"; // Use an appropriate mime type here
    default:
    return null;
    }
    }

    @Override
    public Cursor query(Uri uri, String[] arg1, String arg2, String[] arg3,
    String arg4) {
    switch (uriMatcher.match(uri)) {

    // If it returns 1 - then it matches the Uri defined in onCreate
    case 1:
    MatrixCursor cursor = null;

    File file = new File( getContext().getCacheDir() + File.separator
    + uri.getLastPathSegment());
    if (file.exists()) {
    cursor = new MatrixCursor(new String[] {
    OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE });
    cursor.addRow(new Object[] { uri.getLastPathSegment(),
    file.length() });
    }

    return cursor;
    default:
    return null;
    }
    }

  • Eric Smith
    07/19/2013 - 8:48 am | Permalink

    I found the need for setting the exported attribute to true in the manifest the hard way, not having noticed that Harry’s reply above mentions it. It might be a good idea to edit the notes about the manifest in the article to show that.

  • Leave a Reply

    Your email address will not be published. Required fields are marked *

    You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>