Androidで画像を選択して、S3にアップロードする

  • 3
    いいね
  • 0
    コメント

対象者

  • Androidアプリで画像アップロード機能を実装したい人
  • 基本的なAndroid開発、AWS(S3、IAM等)の知識がある人

概要

AndroidでS3を使用した写真投稿機能を実装しようとした時に意外とやることが多かったので、整理して手順をまとめました。
以下、「画像を選択して、S3にアップロードする」までの今回の実装の流れです。

  1. 「画像の選択」
    1. Intentでピッカーを起動する
    2. 選択した画像のUriを取得する
  2. 「アップロード用の一時ファイル作成」
    1. UriからBitmapを生成
    2. アップロード用のFileを作成
  3. 「S3へのアップロード」
    1. TransferUtilityでアップロードを実行して、TransferObserverを取得する
    2. TransferListenerをアップロード状況にあわせて実装し、TransferObserverにセットする

他にもいろいろやり方がありそうですが、今回はACTION_GET_CONTENTで画像を選択して、AWS Mobile SDK を使用してS3にアップロードしています。
注意点としては、1で選択した画像がUriで、3でS3アップロード時に指定するのがFileとインタフェースが異なるため、2でUriからBitmapを生成してアップロード用の一時Fileを作成しています。
一時ファイル作成時に画像のリサイズ等の加工が可能です。

1. 画像の選択

Intentでピッカーを起動する

以下のパラメータでIntentを作成して、画像選択用のピッカーを起動します。

ImageUploadActivity.java
// Create Intent to pick up an image.
Intent i = new Intent();
i.setType("image/*");
i.addCategory(Intent.CATEGORY_OPENABLE);
i.setAction(Intent.ACTION_GET_CONTENT);
startActivityForResult(i, REQUEST_PICK_UP_IMAGE);

参考: https://developer.android.com/reference/android/content/Intent.html
ACTION_GET_CONTENT with MIME type */* and category CATEGORY_OPENABLEを参照

選択した画像のUriを取得する

onActivityResultで選択した画像のUriを受け取ります。

ImageUploadActivity.java
@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (resultCode == RESULT_OK && requestCode == REQUEST_PICK_UP_IMAGE) {
        Uri uri = data.getData();
    }
}

2. アップロード用の一時ファイル作成

UriからBitmapを生成

UriからBitmapを生成します。
今回はアップロード用にリサイズも行っています。

build.gradle
dependencies {
    compile 'com.squareup.picasso:picasso:2.5.2'
}
ImageUploadActivity.java

private ImageView mPreview;
private File mUploadFile;

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == REQUEST_PICK_UP_IMAGE && resultCode == RESULT_OK) {
        Uri uri = data.getData();
        Picasso.with(ImageUploadActivity.this).load(uri).resize(200, 200).centerCrop().into(new Target() {
            @Override
            public void onBitmapLoaded(final Bitmap bitmap, Picasso.LoadedFrom from) {
                new Handler(Looper.getMainLooper()).post(new Runnable() {
                    @Override
                    public void run() {
                        mUploadFile = createUploadFile(ImageUploadActivity.this, bitmap);
                        mPreview.setImageBitmap(bitmap);
                    }
                });
            }

            @Override
            public void onBitmapFailed(Drawable errorDrawable) {
                // TODO handle error
            }

            @Override
            public void onPrepareLoad(Drawable placeHolderDrawable) {
                // Do nothing
            }
        });
    }
}

今回はPicassoを使いましたが、Glideや標準のBitmapFactoryでも代替可能です。
BitmapFactoryを使用する場合はBitmapFactory.Optionsを使用してサイズの大きい画像でOutOfMemoryにならないように気をつけたほうがよさそうです。
参考: https://developer.android.com/topic/performance/graphics/load-bitmap.html
また、端末によっては画像の向きが正しく反映されないこともあるので、今回の実装例ではできていませんが、Exif情報を参照して向きを補正する処理を入れた方がベターだと思います。
参考: http://qiita.com/murapon/items/1a39746cd4aab7b2c245

アップロード用のFileを作成

Bitmap生成後、アップロード用のファイルをアプリのキャッシュ用ストレージに作成します。

ImageUploadActivity.java
private static final String TEMP_FILE_NAME = "temp_upload_image";

private File createUploadFile(Context context, Bitmap bitmap) {
    File file = new File(new File(String.valueOf(context.getExternalCacheDir())), TEMP_FILE_NAME);
    FileOutputStream fos = null;
    try {
        file.createNewFile();
        fos = new FileOutputStream(file);
        bitmap.compress(Bitmap.CompressFormat.JPEG, 100, fos);
    } catch (IOException e) {
        e.printStackTrace();
        // TODO handle error
    } finally {
        try {
            if (fos != null) {
                fos.flush();
                fos.close();
            }
        } catch (IOException e) {
            e.printStackTrace();
            // TODO handle error
        }
    }
    return file;
}

getExternalCacheDir()を使うと、Androidの「設定」->アプリのキャッシュ削除等でファイルが消される可能性があるため、残したい場合は、getExternalFilesDir()等を使用した方がよいです。
参考: https://developer.android.com/guide/topics/data/data-storage.html

3. S3へのアップロード

TransferUtilityでアップロードを実行して、TransferObserverを取得する

AWS Mobile SDKのTransferUtilityを使用します。

build.gradle
dependencies {
    compile 'com.amazonaws:aws-android-sdk-s3:2.+'
}
AndroidManifesto.xml
<service
    android:name="com.amazonaws.mobileconnectors.s3.transferutility.TransferService"
    android:enabled="true" />
ImageUploadActivity.java
private void uploadFile(final Context context, File file) {
    CognitoCachingCredentialsProvider credentialsProvider = AWSUtil.getCredentialsProvider(context);
    final AmazonS3 s3 = new AmazonS3Client(credentialsProvider);
    TransferUtility transferUtility = new TransferUtility(s3, context.getApplicationContext());
    TransferObserver transferObserver = transferUtility.upload("my_bucket", "my_images/01.jpg", file, CannedAccessControlList.PublicRead);
    ...省略
}

TransferUtility.upload()の第4引数でアップロードするファイルのアクセス権限を指定できます。
403エラーになる場合は、CognitoIdentityPoolに紐付いているIAMロールでs3:PutObjects3:PutObjectAclが許可されているか確認してください。

TransferListenerをアップロード状況にあわせて実装し、TransferObserverにセットする

ImageUploadActivity.java
private void uploadFile(final Context context, File file) {
    ...省略
    TransferObserver transferObserver = transferUtility.upload("my_bucket", "my_images/01.jpg", file, CannedAccessControlList.PublicRead);
    transferObserver.setTransferListener(new TransferListener() {
        @Override
        public void onStateChanged(int id, TransferState state) {
            switch (state) {
                case COMPLETED:
                    Toast.makeText(context, "Completed to upload an image to S3.", Toast.LENGTH_SHORT).show();
                    mUploadFile.delete();
                    break;
                case IN_PROGRESS:
                    break;
                default:
                    mUploadFile.delete();
            }
        }

        @Override
        public void onProgressChanged(int id, long bytesCurrent, long bytesTotal) {
            // Do nothing
        }

        @Override
        public void onError(int id, Exception ex) {
            ex.printStackTrace();
            Toast.makeText(context, "Failed to upload an image to S3.", Toast.LENGTH_SHORT).show();
            mUploadFile.delete();
        }
    });
}

参考: http://docs.aws.amazon.com/ja_jp/mobile/sdkforandroid/developerguide/s3transferutility.html

まとめ

UriとかFileとか汎用的なインタフェースを扱うせいか、変換処理でコード量が多くなってしまい結構面倒でした。
向きの補正とかはまだできていないので、今後も改善しつつもっといい実装方法を模索したいです。
(画像扱うの大変。。)