ページ

平成23年11月30日水曜日

一個や複数の添付ファイル付きメール送信する方法

Androidのインテント(Intent)でメールを送信すること自体は簡単。一方、複数の画像を添付して送信したい場合は意外と複雑で思う通りにいかなかったりした。その経験についての投稿。

一つの画像を添付する場合のコードはこんな感じ:

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

Syntax Highlighterをブログの投稿への適用方法

ブログや掲示板などにソースコードが綺麗に表示されないと人が読もうとしない。色付きそして綺麗なフォーマットをちゃんと使ってれば読みやすくなる。プログラミングについてこのビログには当然ソースコードを投稿するので、人気のあるSyntax Highlighterをセットアップしてみた。どういうものかというと、HTMLコード内に埋め込んだソースコード、例えば以下のJavaコード、を色づけたりフォーマットを読みやすくしてくれるJavaScriptライブラリー。ほとんどのプログラミング言語のシンタックスを対応していて、プログラミングのことを書く方にはお勧め。インストールは色々大変だったので、英語版のブログの方に詳しい手順をまとめた。

public static void hola() {
    System.out.println("Hola, ¿cómo estás?");
}

平成23年11月8日火曜日

Androidでの拡張現実系(AR)の開発入門

さ~、どこから始めればいいですかね?


今日からブログするのは、Android携帯端末上でAR系のアプリを開発する冒険の物語です。


今後も新たに経験したことももちろん公開したいと思っていますが、最初にはこの1ヵ月で経験したことをまとめて公開していく予定です。特にチュートリアルの形で書くわけでもないですけど、最終的に出来上げる投稿の順番が、入門から上級へのレッスンみたいなものになれたらベストかと期待しています。


まずは、索引的にリストアップしていみると、トピックスは以下の項目に分けられます。
  1. 例の既存AR系アプリの紹介&どのARライブラリやSDKを使うか
  2. Android NDKとは&開発環境構築手順
  3. OpenGL ESの概要とVisual Studioによるデバグ (Woglesの紹介)
未着手段階ですけど、上級のトピックスで以下についても投稿するつもりです。
  1. 3Dモデル作成とOpenGL ESへのインポート方法
  2. 3DS MaxやBlenderなどのサードパーティソフトによって作成された3DアニメーションをOpenGL ES(ARのアプリ)への投入方法
  3. ARアプリのOpenGL ESのレンダリング表面にビデオを表示する方法
  4. アプリとのインタラクション:バーチャルボタンを実装する
各項目の投稿をアップしたタイミングで上記にリンクを貼り付けます。

平成23年11月7日月曜日

初ポスト

さてさて、初投稿ですね。私のブログへようこそ。


ここでは、主に「拡張現実」というものについて書いていきます。スマートフォンでのプログラミングにも興味を持っています。ここ最近、Androidプラットフォームにおける拡張現実というトピックで色々調べたり試したりしています。


拡張現実とは?って言ってる人には、以下のビデオを見てください。



拡張現実(ARとも呼ばれる)というのを簡単に説明してみます。こう考えてみてください。常に自分の周りに見えてる環境ってありますよね?普通に視野で見える環境を「現実」と呼びましょう。それにものを付け加えると、環境が拡張されるわけですよね。具体的にいうと、ウェブカメラなどからの映像に立体的なアニメ・モデルもしくはビデオを追加することで、そのものがあなたと一緒に実際に現実的環境に存在しているように見えてきます。ARは、教育、設計、医学、エンタテインメント、建築などのたくさんの業界に色んな使い方があります。もっと知りたいと思う人には「拡張現実」でグッグってみてください。

基本的に英語版のブログをメインで書いていく予定ですけど、役に立つと思われる情報を日本語でも公開したいと思っています。自分が調べた・まとめた・経験した・基礎を学ぶために開発したデモアプリ・身につけた知識などをオープンソース的にインターネットの皆さんに貢献すべきものという考えです。私が知りたいと思ったような、スマートフォン上のAR開発に関する情報がバラバラで、調べにくいです。私と同じコーディングのところで同じ苦労をしてる人もきっといると思いますし、初心者で入ったばっかりな私はお互いの経験を共有し合って学び合える場面が欲しくてこのブログをスタートしました。

どうぞ、宜しくお願いします。