[Android]VolleyでMultipart/form-dataを使った画像アップロード


Androidの定番通信処理ライブラリVolleyを使って、画像ファイルをアップロードをするための実装に関して書きました。


## Volleyをプロジェクトに追加する
Volleyを追加する方法としては、jarファイルを作成して外部ライブラリとして追加する方法など考えられますが、今回は今後の管理しやすさも踏まえ、サブモジュールとしてAndroid Studioに追加する方法を選択しました。

サブモジュールとして追加するにあたっては、こちらのブログ(<http://vividcode.hatenablog.com/entry/android-app/volley-preparation-android-studio>)を参考にしました。

###手順

1.プロジェクト直下でターミナルから以下コマンドを入力します。"modules/volley"の部分のディレクトリ設定は任意です。

```
git submodule add https://android.googlesource.com/platform/frameworks/volley modules/volley
```

参考：サブモジュールについては こちら-> <http://git-scm.com/book/ja/Git-%E3%81%AE%E3%81%95%E3%81%BE%E3%81%96%E3%81%BE%E3%81%AA%E3%83%84%E3%83%BC%E3%83%AB-%E3%82%B5%E3%83%96%E3%83%A2%E3%82%B8%E3%83%A5%E3%83%BC%E3%83%AB>

2.先ほどのブログにはサブモジュールのbuild.gradleにはビルドスクリプトが書いてないとありますが、9/28/2014時点での最新(<https://android.googlesource.com/platform/frameworks/volley/+/9a311e2f20745f1c71fa9c18ce008db29188ed39>)ではサブモジュールにビルドスクリプトの記載があります。  
ですが、gradleのバージョンが古いので、最新にします。

```
//サブモジュールのbuild.gradle
buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'com.android.tools.build:gradle:0.12.2'
    }
}
```


3.Volleyのビルドツールバージョンを自分のandroid Studioで利用している最新のものと合わせます。

```
android {
    compileSdkVersion 19
    buildToolsVersion = "19.1.0"
    (中略)
    }

```

4.あとは、自分のメインのモジュールのbuild.gradle(Topのbuild.gradleではないです)にサブモジュールの関連付けをします。

``` 
//メインモジュールのbuild.gradle
dependencies {
    // (略) 
    compile project(':modules:volley') 
    }
```

## Multipart/form-data形式でアップロードするための実装
Volleyで画像ファイルのような大きいファイルを送信するにはVolleyを拡張してMultipart/form-data形式に対応させる必要があります。
実装にあたってはこちらのブログ(<http://b.fly1tkg.com/2014/03/volley-multipart-form-data/>)を参考にさせていただきました。
実装はブログを見ていただいたほうが良いと思うのですが、一部実装の変更が必要な部分や注意点があるので、追記しておきます。

### Apacheのhttpcore,httpmimeの最新版を導入する
httpcoreはApache HttpClientのコンポーネントの一つです。 HTTPプロトコルに関する実装が含まれています。  
httpmimeはMultipart/form-data形式であるMultipartEntityに対応するためのライブラリです。httpmimeのdependencyとしてhttpcoreが必須となっています。  
なお、それぞれ、以下で最新のバージョンが確認できます。  
<http://mvnrepository.com/artifact/org.apache.httpcomponents/httpmime>
<http://mvnrepository.com/artifact/org.apache.httpcomponents/httpcore>

追加はメインモジュールのbuild.gradleに記載すればOKです。

```
//メインモジュールのbuild.gradle
dependencies {
    // (略) 
    compile project(':modules:volley') 
    compile 'org.apache.httpcomponents:httpcore:4.3.2'
    compile 'org.apache.httpcomponents:httpmime:4.3.5'
    }
```


### MultipartEntityがdeprecated -> MultipartEntityBuilderを使う

最新のhttpmimeではMultipartEntityがdeprecatedとなっています。  
Javadocを見ると、MultipartEntityBuilderを利用する必要がありそうです。  
MultipartEntityBuilderを使った場合、以下のような実装となります。  


```java

public class MultipartRequest extends Request<String> {

    MultipartEntityBuilder entity = MultipartEntityBuilder.create();
    HttpEntity httpEntitiy;
    private final Response.Listener<String> mListener;
    private final Map<String, String> mStringParts;
    private final Map<String, File> mFileParts;

    public MultipartRequest(String url, Response.Listener<String> listener,
                            Response.ErrorListener errorListener,
                            Map<String, String> stringParts, Map<String, File> fileParts) {
        super(Method.POST, url, errorListener);

        mListener = listener;
        mStringParts = stringParts;
        mFileParts = fileParts;
        buildMultipartEntity();
    }

    private void buildMultipartEntity() {
        entity.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
        //送信するリクエストを設定する
        //StringData
        for (Map.Entry<String, String> entry : mStringParts.entrySet()) {
            entity.addTextBody(entry.getKey(), entry.getValue());

        }
        //File Data
        for (Map.Entry<String, File> entry : mFileParts.entrySet()) {
            entity.addPart(entry.getKey(), new FileBody(entry.getValue()));
        }
        httpEntitiy = entity.build();
    }

    @Override
    public String getBodyContentType() {
        return httpEntitiy.getContentType().getValue();
    }

    public HttpEntity getEntity() {
        return httpEntitiy;
    }


    @Override
    protected Response<String> parseNetworkResponse(NetworkResponse response) {
        return Response.success("Uploaded", getCacheEntry());

    }

    //リスナーにレスポンスを返す
    @Override
    protected void deliverResponse(String response) {
        mListener.onResponse(response);
    }
}

```


###Activityでの利用方法

実際に利用する場合は以下のようにVolleyを呼び出せばOKです。

```java

public class SampleActivity{
    private RequestQueue mQueue;
    
    protected void onCreate(Bundle savedInstanceState){
        super.onCreate(savedInsanceState);
        //Volleyのキュー設定
        mQueue = Volley.newRequestQueue(this, new MultiPartStack());
        doUpload();
    }
    
    doUpload(){
        Map<String,String> stringMap = new HashMap<String, String>();
        Map<String,File> fileMap = new HashMap<String, File>();
        stringMap.put("text", "hogege"); //textも送るとき利用
        fileMap.put("img", new File("/file/hoge.jpg"));
        MultipartRequest multipartRequest = new MultipartRequest(
                url,
                new Response.Listener<String>() {
                    @Override
                    public void onResponse(String response) {
                        //Upload成功
                        Logger.d(Tag.DEBUG,"Upload success: " + response);

                    }
                },
                new Response.ErrorListener() {
                    @Override
                    public void onErrorResponse(VolleyError error) {
                        //Upload失敗
                        Logger.d(Tag.DEBUG,"Upload error: " + error.getMessage());
                    }
                },
                stringMap,
                fileMap);
        mQueue.add(multipartRequest);
    
    } 

}

```


### duplicate files during packaging of apkというエラー
いざプロジェクトを実行しようとすると、以下のようなエラーが発生しました。

```
Error:Execution failed for task ':app:packageDebug'. Duplicate files copied in APK META-INF/DEPENDENCIES
Error:Execution failed for task ':app:packageDebug'. Duplicate files copied in APK META-INF/LICENSE
Error:Execution failed for task ':app:packageDebug'. Duplicate files copied in APK META-INF/NOTICE
```

httpmimeとhttpcoreの一部のファイルがかぶっていることが原因のようです。  
参考：
<http://stackoverflow.com/questions/22467127/error-duplicate-files-during-packaging-of-apk>

ビルドする際に対象のファイルを除外するようにしたら解決しました。

```
//メインモジュールのbuild.gradle
packagingOptions {
        exclude 'META-INF/DEPENDENCIES'
        exclude 'META-INF/LICENSE'
        exclude 'META-INF/NOTICE'

    }
```