I’ve recently finished work on an app that registers itself as a handler for a given file extension, let’s call it “.mytype”, so if the user attempts to open a file named “file1.mytype” our app would launch and receive an Intent containing the information on the file’s location and its data can be imported. Specifically I wanted this to happen when the user opened an email attachment, as data is shared between users via email attachment for this app.

There are many pitfalls to doing this, and the Stack Overflow answers I saw given for the question had various side-effects or problems. The most common was that your app would appear in the chooser dialog whenever the user clicked on an email notification, for any email – not just those with your attachment. After some trial and error, I came up with this method.

Create IntentFilters in AndroidManifest.xml

The first step is to add <intent-filter> nodes to the application node of the AndroidManifest.xml. Here’s an example of that:

<intent-filter>
  <action android:name="android.intent.action.VIEW" />
  <action android:name="android.intent.action.EDIT" />
  <category android:name="android.intent.category.DEFAULT" />
  <data
    android:mimeType="application/octet-stream"
    android:host="*" 
    android:pathPattern=".*\\.mytype"
  />
</intent-filter>
<intent-filter>
  <action android:name="android.intent.action.VIEW" />
  <action android:name="android.intent.action.EDIT" />
  <category android:name="android.intent.category.DEFAULT" />
  <data
    android:mimeType="application/mytype"
    android:host="*" 
    android:pathPattern=".*\\.mytype"
  />
</intent-filter>

Now something to note here, I’ve specified a filter for both “application/mytype” mimetype and also the more generic “application/octet-stream” mime type. The reason for this is because we can’t guarantee the attachment’s mime-type has been set correctly. We have iOS users and Android users sharing timers via email, and with iOS the mime type is set, with Android, at least in my tests on Android 4.2, the mime-type reverts to application/octet-stream for attachments sent from within the app.

Permissions

I initially put these IntentFilters on the “home” Activity of my app, however I soon started encountering security exceptions in LogCat detailing how my Activity didn’t have access to the data from the other process (Gmail). I realised this was because my Activity’s tag had the launch mode set to:

android:launchMode="singleTask"

Which prevents multiple instances of it being launched, this is important when users can launch the app from either the launcher icon or in this case via attachment (I didn’t want to have multiple instances of my home Activity running as that would confuse the user). So the solution was simply to create a new “ImportDataActivity” that handled the data import from the attachment, and then launched the home Activity with the Intent.FLAG_ACTIVITY_CLEAR_TOP flag added.

Importing Data

So in ImportDataActivity we need to import the data stored in the attachment, in my case this was JSON. The following shows how you might go about doing this:

@Override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);

  Uri data = getIntent().getData();
  if(data!=null) {
    getIntent().setData(null);
    try {
      importData(data);
    } catch (Exception e) {
      // warn user about bad data here
      finish(); 
      return;
  }

  // launch home Activity (with FLAG_ACTIVITY_CLEAR_TOP) here…
}

private void importData(Uri data) {
  final String scheme = data.getScheme();

  if(ContentResolver.SCHEME_CONTENT.equals(scheme)) {
    try {
      ContentResolver cr = context.getContentResolver();
      InputStream is = cr.openInputStream(data);
      if(is == null) return;

      StringBuffer buf = new StringBuffer();			
      BufferedReader reader = new BufferedReader(new InputStreamReader(is));
      String str;
      if (is!=null) {							
        while ((str = reader.readLine()) != null) {	
          buf.append(str + "\n" );
        }				
      }		
      is.close();

      JSONObject json = new JSONObject(buf.toString());

      // perform your data import here…

  }
}

That’s all that’s needed to register-for, and read data from custom file-types.

Sending Email with Attachments

Now how about sending an email with a custom attachment. Here’s a sample of how you might do that:

String recipient = "", 
  subject = "Sharing example", 
  message = "";

final Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND);
emailIntent.setType("message/rfc822");

emailIntent.putExtra(android.content.Intent.EXTRA_EMAIL, new String[]{recipient});
emailIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, subject);
emailIntent.putExtra(android.content.Intent.EXTRA_TEXT, message);

// create attachment
String filename = "example.mytype";

File file = new File(getExternalCacheDir(), filename);
FileOutputStream fos = new FileOutputStream(file);
byte[] bytes = json.toString().getBytes();
fos.write(bytes);
fos.close();

if (!file.exists() || !file.canRead()) {
  Toast.makeText(this, "Problem creating attachment", 
      Toast.LENGTH_SHORT).show();
  return;
}

Uri uri = Uri.parse("file://" + file.getAbsolutePath());
emailIntent.putExtra(Intent.EXTRA_STREAM, uri);

startActivityForResult(Intent.createChooser(emailIntent, 
        "Email custom data using..."), 
        REQUEST_SHARE_DATA);

Please note that “REQUEST_SHARE_DATA” is just an static int const in the class, used in onActivityResult() when the user returns from sending the email. This code will prompt the user to select an email client if they have multiple apps installed.

As always, please do point out any inaccuracies or improvements in the comments.