一つの画像を添付する場合のコードはこんな感じ:
String externalStoragePathStr = Environment.getExternalStorageDirectory().toString() + File.separatorChar;
String filenameStr = "MyPhoto.jpg";
String absPath = "file://" + externalStoragePathStr + filenameStr;
Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND);
emailIntent.setType("text/plain");
emailIntent.putExtra(android.content.Intent.EXTRA_EMAIL, new String[] {"wocks@gmail.com"});
emailIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, "Test Subject");
emailIntent.putExtra(android.content.Intent.EXTRA_TEXT, "Here's your stuff");
Uri retUri = Uri.parse(absPath);
emailIntent.putExtra(Intent.EXTRA_STREAM, retUri);
startActivity(Intent.createChooser(emailIntent, "Send attachment with what app?"));
そして複数の場合は、ACTION_SEND_MULTIPLEを利用し、ParcelできるUriを格納したArrayListを実装する。とすると、こんな↓感じ:
String externalStoragePathStr = Environment.getExternalStorageDirectory().toString() + File.separatorChar;
String filePaths[] = {
externalStoragePathStr + "MyPhoto.jpg", // /mnt/sdcard/MyPhoto.jpg
externalStoragePathStr + "MyPhoto-2.jpg", // /mnt/sdcard/MyPhoto-2.jpg
externalStoragePathStr + "MyPhoto-3.jpg"}; // /mnt/sdcard/MyPhoto-3.jpg
Intent emailIntent = new Intent(android.content.Intent.ACTION_SEND_MULTIPLE);
emailIntent.setType("text/plain");
emailIntent.putExtra(android.content.Intent.EXTRA_EMAIL, new String[] {"wocks@gmail.com"});
emailIntent.putExtra(android.content.Intent.EXTRA_SUBJECT, "Test Subject");
emailIntent.putExtra(android.content.Intent.EXTRA_TEXT, "Here's your stuff");
ArrayList<Uri> uris = new ArrayList<Uri>();
for (String file : filePaths) {
File fileIn = new File(file);
Uri u = Uri.fromFile(fileIn);
uris.add(u);
}
emailIntent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris);
startActivity(Intent.createChooser(emailIntent, "Send attachment with what app?"));
、、、だが、色んなパーターンや組み合わせでうまくいかないことを多く経験した。例えば、
- 新規メール作成画面に添付ファイル名が表示されるが、相手に届いたファイルが壊れる(メールクライアント側に添付ファイルが画像として認識されず、文字化けたテキストが表示されるなど
- 新規メール作成画面にファイルが添付すらされない(ファイル名が画面に表示されない)
結論:
私が主に苦労したのは、メールクライアントに複数の画像が添付されなかったこと。GMailは特に問題なかった。色々調べたり試したりした結果、メールアプリは/mnt/sdcard/DCIM/Camera/photo.jpgやfile:///mnt/sdcard/MyPhoto.jpgのような絶対パスでArrayList of Uriの配列渡されたら嫌がるっぽい。「content」というcontent://media/external/images/media/1075みたいなUriなら大丈夫だった.
次に衝突したのは、絶対パスの「/mnt/sdcard/DCIM/Camera/photo.jpg」を「content://media/external/images/media/1075」フォーマットに変換する問題だった。マルチスレッドによるハックが必要。反面、逆の変換はよりと容易なのにねー
上記の問題点を解決する方法をシンプルなアプリにまとめた。ソース内にファイル名を定義し、実行し、一つか複数添付画像を添付するかとの質問に答え、メールを送信する流れとなる。以下の方法を明確にやり方を見せる:
1) メールにもGmailにも対応した、一つ・複数の添付画像を送信する方法
2) 絶対パスをcontentのURIパスに変換する方法
3) contentのURIパスを絶対パスに変換する方法
以下のJavaファイル名をクリックしソースを表示させるか、またはこちらからEclipseのプロジェクトをダウンロードする。
package ws.aroha.android.pilcrowpipe;
import java.io.File;
import java.io.FileNotFoundException;
import java.util.ArrayList;
import java.util.Stack;
import ws.aroha.android.pilcrowpipe.R;
import android.app.Activity;
import android.app.AlertDialog;
import android.app.ProgressDialog;
import android.content.ContentResolver;
import android.content.DialogInterface;
import android.content.Intent;
import android.database.Cursor;
import android.media.MediaScannerConnection;
import android.media.MediaScannerConnection.MediaScannerConnectionClient;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.Handler;
import android.provider.MediaStore;
import android.widget.Toast;
/**
* SendMultipleAttachments
*
* @author Simon McCorkindale
*
* Sample code to illustrate 2 concepts on Android:
*
* 1. Opening a new e-mail intent with single or multiple image attachments using {@link Intent.ACTION_SEND} and {@link Intent.ACTION_SEND_MULTIPLE}
* 2. Converting between absolute paths and "content://..." type URIs
*
* The reason for this sample code is for the situation where you only know the real (absolute) path of
* the file(s) you want to attach. Unfortunately it seems E-mail/G-mail apps require the paths be a URI
* in the format of "content://..." in order to attach them so we do the conversion here. Conversion
* from "content://..." URI to an absolute path string is easy but not the other way around, which
* requires a little a little multi-threading work with the Media Scanner API.
*
* See more detail on my blog at:
* {@link http://pilcrowpipe.blogspot.com}
*
* Disclaimer: Use this code however you see fit with no restrictions. Kudos to those
* people who wrote code on the sites I've referenced in my comments.
*
*/
public class SendMultipleAttachments extends Activity implements MediaScannerConnectionClient {
/**
* Path to external storage media (SD card in most cases).
*/
private final static String EXTERNAL_MEDIA_PATH =
Environment.getExternalStorageDirectory().toString() +
File.separatorChar;
/**
* Alert dialog item IDs.
*/
private final int SINGLE_ITEM = 0x10; // Single file attachment
private final int MULTIPLE_ITEMS = 0x1a; // Multiple file attachments
private int mSendTypeSelection = SINGLE_ITEM; // Send single attachment by default
/**
* Define the absolute path to your multiple attachment files.
* MODIFY TO MATCH FILES ON YOUR PHONE.
*/
private final String mMultipleFilesPaths[] = {
EXTERNAL_MEDIA_PATH + "MyPhoto.jpg", // -> /mnt/sdcard/MyPhoto.jpg on my Android
EXTERNAL_MEDIA_PATH + "MyPhoto-2.jpg", // -> /mnt/sdcard/MyPhoto-2.jpg on my Android
EXTERNAL_MEDIA_PATH + "MyPhoto-3.jpg", // -> /mnt/sdcard/MyPhoto-3.jpg on my Android
EXTERNAL_MEDIA_PATH + "MyPhoto-4.jpg" // -> /mnt/sdcard/MyPhoto-4.jpg on my Android
};
/**
* Define the absolute path to your single attachment file.
* MODIFY TO MATCH FILE ON YOUR PHONE.
*/
private final String mSingleFilePath =
EXTERNAL_MEDIA_PATH + "MyPhoto.jpg"; // -> /mnt/sdcard/MyPhoto.jpg on my Android
/**
* The send e-mail {@link android.content.Intent}.
*/
private Intent mIntent = null;
/**
* The number of URIs we expect to be converted to content:// type. This figure
* is used so we can tell whether the conversion for all files has been complete or not.
*/
private int mNumberExpectedConvertedUris = 0;
/**
* Handler to accept communiqué from the Media Scanner Client worker thread. "IPC" made easy!
*/
private Handler mHandler = new Handler();
/**
* Progress bar dialog to display when converting URIs and waiting for the worker thread.
*/
private ProgressDialog mProgressDialog = null;
/** Called when the activity is first created. */
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.main);
// Check external media state
String mediaState = Environment.getExternalStorageState();
if (mediaState == null ||
!(mediaState.equals(Environment.MEDIA_MOUNTED) ||
mediaState.equals(Environment.MEDIA_MOUNTED_READ_ONLY))) {
// Can't access external media so quit graciously notifying the user
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setMessage("External media is unavailable (not mounted?). Please check.");
AlertDialog dialog = builder.create();
dialog.setButton("OK",
new DialogInterface.OnClickListener() {
public void onClick(DialogInterface dialog, int which) {
dialog.dismiss();
finish();
}
}
);
dialog.setTitle("Tiene un problema"); // Houston, we have a problem
dialog.show();
return;
}
// Prompt the user to send either the one or multiple files registerd in
// fields at the top of this class
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("Send one or multiple attachments?");
CharSequence[] items = {"Single", "Multiple"};
builder.setItems(items, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// Determine if user clicked to send single or multiple files
ContentResolver contentResolver = getContentResolver();
switch (which) {
case 0: // Single selected
// Check file exists
try {
contentResolver.openFileDescriptor(Uri.parse("file://" + mSingleFilePath), "r");
} catch (FileNotFoundException e) {
Toast.makeText(getApplicationContext(), "File " + mSingleFilePath +
" doesn't exist",
Toast.LENGTH_SHORT).show();
}
mSendTypeSelection = SINGLE_ITEM;
break;
case 1: // Multiple selected
// Check files exist
for (String filename : mMultipleFilesPaths) {
try {
contentResolver.openFileDescriptor(Uri.parse("file://" + filename), "r");
} catch (FileNotFoundException e) {
Toast.makeText(getApplicationContext(), "File " + filename +
" doesn't exist",
Toast.LENGTH_SHORT).show();
}
}
mSendTypeSelection = MULTIPLE_ITEMS;
break;
}
requestSendAttachments();
}
});
builder.create().show();
}
/**
* Request to send the attachments. This method invokes the thread that will
* convert the attachments' URIs from absolute path strings to "content://..."
* type URIs needed for attaching attachments. This Media Scanner client thread
* will then initiate the actual sending process (i.e. opening of the application
* to send the attachments by firing our intention).
*/
private void requestSendAttachments() {
mIntent = new Intent();
// Set some basic e-mail stuff
mIntent.setType("text/plain");
mIntent.putExtra(Intent.EXTRA_TEXT, "Test e-mail with attachment(s)");
mIntent.putExtra(Intent.EXTRA_SUBJECT, "Your file(s)");
mIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
// Because we wait on a thread show we're doing something meanwhile
mProgressDialog = new ProgressDialog(this);
mProgressDialog.setTitle("Converting URI(s)...");
mProgressDialog.setMessage("Espere un minuto"); // Please wait a minute
mProgressDialog.show();
// Intent logic differs slightly depending on whether we are to
// send single or multiple attachments.
//
// The stock standard E-mail / G-mail mail client applications
// require the URIs to the attachments be in the form of
// "content://...", not the absolute path type such as
// /mnt/sdcard/MyPhoto.jpg. It requires some work to
// be done in a separate thread (see the Media Scanner code below).
//
// I've only tried this with images and it may not work with
// other types of files for which the underlying Android system
// doesn't generate content type URIs for.
switch (mSendTypeSelection) {
case SINGLE_ITEM: // Send single attachment file
mIntent.setAction(Intent.ACTION_SEND);
mNumberExpectedConvertedUris = 1;
convertAbsPathToContentUri(mSingleFilePath);
break;
case MULTIPLE_ITEMS: // Send multiple attachment files
mIntent.setAction(Intent.ACTION_SEND_MULTIPLE);
mNumberExpectedConvertedUris = mMultipleFilesPaths.length;
convertAbsPathToContentUri(mMultipleFilesPaths);
break;
}
}
/***************************** BEGIN URI CONVERSION CODE ***************************
* Code snippet referenced from http://stackoverflow.com/questions/3004713/get-content-uri-from-file-path-in-android
*
* Logic is something like:
* 1. UI thread invokes convertFileToContentUri() which causes a new Media Scanner Connection thread
* to be spawned.
* 2. Once the thread has done it's stuff onScanCompleted() is called with the converted Uri
* 3. My custom hack is to finally send a Runnable back to the UI thread via Handler to add
* the new URI to the list of attachments, and if all URIs have been processed fire the
* activity to open a new e-mail with attachments.
*/
/** List of content URIs of files to attach */
private ArrayList<Uri> mContentUris = new ArrayList<Uri>();
/** Instance of our {@link android.media.MediaScannerConnection} client */
private MediaScannerConnection mMsc = null;
/** The path of the current URI being queried (scanned) */
private String mAbsPath = null;
/**
* Convert absolute paths to content:// type URIs and store in mContentUris array list.
* E.g. /mnt/sdcard/DCIM/Camera/photo.jpg -> content://media/external/images/media/1075
*
* @param absPaths[] array of absolute path strings to convert
*/
private Stack<String> mContentUrisStack = new Stack<String>();
private void convertAbsPathToContentUri(final String absPaths[]) {
// In order to achieve sequential processing of each path
// we utilise a stack, and upon completion of converting
// one URI it will be popped and the next processed. If we
// don't process sequential then we get illegal state exceptions
// from the Media Scanner client saying it isn't connected and
// some of the paths don't get processed properly.
for (String absPath : absPaths) {
mContentUrisStack.push(absPath);
}
// Pop and convert
if (!mContentUrisStack.empty()) {
convertAbsPathToContentUri(mContentUrisStack.pop());
}
}
/**
* Convert absolute paths to content:// type URIs and store in mContentUris array list.
* E.g. /mnt/sdcard/DCIM/Camera/photo.jpg -> content://media/external/images/media/1075
*
* @param absPath absolute path string to convert
*/
private void convertAbsPathToContentUri(String absPath) {
mAbsPath = absPath;
mMsc = new MediaScannerConnection(getApplicationContext(), this);
mMsc.connect();
}
@Override
public void onMediaScannerConnected() {
mMsc.scanFile(mAbsPath, null);
}
@Override
public void onScanCompleted(final String path, final Uri uri) {
mHandler.post(new Runnable() {
@Override
public void run() {
// Add converted content URI to list of content URIs to be attached to the e-mail
synchronized (mContentUris) {
mContentUris.add(uri);
}
// Just for sake of completeness here's demonstrating the reverse;
// proving what the conversion is correct
mHandler.post(new Runnable() {
@Override
public void run() {
Toast.makeText(getApplicationContext(), "Original path:\n" +
path + "\n\n" +
"Converted content URI:\n" +
uri.toString() + "\n\n" +
"Reverse converted path:\n " +
convertContentToAbsolutePath(uri),
Toast.LENGTH_LONG).show();
// Pop and convert
if (mSendTypeSelection == MULTIPLE_ITEMS &&
!mContentUrisStack.empty()) {
convertAbsPathToContentUri(mContentUrisStack.pop());
}
}
});
// Conversion is complete, start our intent
if (mContentUris.size() == mNumberExpectedConvertedUris) {
mProgressDialog.dismiss();
// Add the list of converted URIs to the e-mail intent so the
// e-mail app can know their location
switch (mSendTypeSelection) {
case SINGLE_ITEM:
// Single URI, add Parcelable Uri object only
mIntent.putExtra(Intent.EXTRA_STREAM, mContentUris.get(0));
break;
case MULTIPLE_ITEMS:
// Multiple URIs, add ArrayList
mIntent.putExtra(Intent.EXTRA_STREAM, mContentUris);
break;
}
// Open a new compose e-mail window
startActivity(Intent.createChooser(mIntent, "Send with what application?"));
}
}
});
mMsc.disconnect();
}
/**
* Convert "content://..." style URI to an absolute path string.
* E.g. content://media/external/images/media/1075 -> /mnt/sdcard/DCIM/Camera/photo.jpg
*
* Code snippet referenced from http://stackoverflow.com/questions/3401579/get-filename-and-path-from-uri-from-mediastore
*
* @param contentUri {@link contentUri} object containing the "content://..." URI
* @return string containing the absolute path (including filename)
*/
public String convertContentToAbsolutePath(Uri contentUri) {
String[] proj = { MediaStore.Images.Media.DATA };
Cursor cursor = managedQuery(contentUri, proj, null, null, null);
int columnIndex = cursor.getColumnIndexOrThrow(MediaStore.Images.Media.DATA);
cursor.moveToFirst();
return cursor.getString(columnIndex);
}
/*************************** END URI CONVERSION CODE ***************************/
}
// EOF