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

## 対象者

* 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](http://square.github.io/picasso/)を使いましたが、[Glide](https://github.com/bumptech/glide)や標準の[BitmapFactory](https://developer.android.com/reference/android/graphics/BitmapFactory.html)でも代替可能です。
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:PutObject`と`s3: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とか汎用的なインタフェースを扱うせいか、変換処理でコード量が多くなってしまい結構面倒でした。
向きの補正とかはまだできていないので、今後も改善しつつもっといい実装方法を模索したいです。
（画像扱うの大変。。）