kakueki61's dev history

備忘録的に記録を残しています

Androidでサーバーに画像データをPOSTする

画像に限らず、ファイルをサーバー側には送りたい時にはmultipart/form-data形式でデータを送ります。

こちらの記事が大変参考になりました↓
[Androidアプリ開発] ネットワーク通信ライブラリ Volley | まるーんの開発備忘録 Volleyでmultipart/form-dataを送信する - fly1tkg blog

ただ、少しはまった箇所があったのでそこに関してだけ書いておこうと思います。

基本的な流れとしては、

  1. multipart/form-data送信用にVolleyのRequestクラスを継承した独自Request(MultipartRequest)を定義する。
  2. VolleyのRequestQueueにmultipart/form-data用のRequestが詰められた時に、HttpClientでファイルデータを送信するような処理を行う。VolleyのHurlStackを継承した独自HttpStackを定義する。

詳細は上記リンクを参照して下さい。

さて、問題になったのは 複数の画像データを同時にPOSTしたい時 でした。

私の場合、以下のようなMultipartJsonRequestというクラスを、multipart/form-data送信用Requestクラスとして定義しました。バイナリデータとして、byte[], File, InputStreamが渡せるようになっています。

public class MultipartJsonRequest extends JsonRequest<JSONObject> {
    private static final String TAG = MultipartJsonRequest.class.getSimpleName();

    private Map<String, String> mStringParams;
    private Map<String, ?> mBinaryParams;
    private MultipartEntityBuilder mMultipartEntityBuilder = MultipartEntityBuilder.create();

    public MultipartJsonRequest(String url, Map<String, String> stringParams, Map<String, ?> bynaryParams,
                                Response.Listener<JSONObject> listener, Response.ErrorListener errorListener) {
        super(Method.POST, url, null, listener, errorListener);

        mStringParams = stringParams;
        mBinaryParams = bynaryParams;
        buildMultipartEntity();
    }

    private void buildMultipartEntity() {
        mMultipartEntityBuilder.setMode(HttpMultipartMode.BROWSER_COMPATIBLE);
        mMultipartEntityBuilder.setBoundary("___________________" + Long.toString(System.currentTimeMillis()));
        mMultipartEntityBuilder.setCharset(Consts.UTF_8);

        for (Map.Entry<String, String> entry : mStringParams.entrySet()) {
            mMultipartEntityBuilder.addTextBody(entry.getKey(), entry.getValue());
        }

        for (Map.Entry<String, ?> entry : mBinaryParams.entrySet()) {
            ContentType imageContentType = ContentType.create("image/jpeg");

            if (entry.getValue() instanceof byte[]) {
                Log.d(TAG, "entry.getValue() => byte[]");
                mMultipartEntityBuilder.addBinaryBody("uploadFiles", (byte[]) entry.getValue(), imageContentType, entry.getKey());
            } else if (entry.getValue() instanceof File) {
                Log.d(TAG, "entry.getValue() => File");
                mMultipartEntityBuilder.addBinaryBody("uploadFiles", (File) entry.getValue(), imageContentType, entry.getKey());
            } else if (entry.getValue() instanceof InputStream) {
                Log.d(TAG, "entry.getValue() => InputStream");
                Log.d(TAG, "key: " + entry.getKey());
                mMultipartEntityBuilder.addBinaryBody("uploadFiles[]", (InputStream) entry.getValue(), imageContentType, entry.getKey());
            }
        }
    }

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

    public HttpEntity getEntity() {

        return mMultipartEntityBuilder.build();
    }

    @Override
    protected Response<JSONObject> parseNetworkResponse(NetworkResponse response) {
        byte[] responseData = response.data;
        try {
            String jsonString = new String(responseData, HttpHeaderParser.parseCharset(response.headers));
            return Response.success(new JSONObject(jsonString), HttpHeaderParser.parseCacheHeaders(response));
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
            return Response.error(new ParseError(e));
        } catch (JSONException e) {
            e.printStackTrace();
            Log.d(TAG, new String(responseData));
            return Response.error(new ParseError(e));
        }
    }
}

ポイントとなったのは、buildMultipartEntity()の中でMultipartEntityBuilderにaddBinaryBodyしている箇所です。
POSTされたファイルをPHPで処理することを想定していたのですが、始めはうまくいきませんでした。
addBinaryBodyの第1引数を見ていただきますと、"uploadFiles"あるいは"uploadFiles[]"となっているかと思います。 これは、HTMLで書かれたWebページからファイルをアップロードさせる場合に対応させると、

<form method="post" action="../index.php?action=image/upload" enctype="multipart/form-data">
    <input type="file" name="uploadFiles[]" multiple><br>
    <input type="submit" value="Submit"><br>
</form>

上記のような<input>タグのname属性に当たる部分です。 PHP側では、メインの処理は以下の様な感じです。

if (isset($_FILES[self::POST_FILE_KEY])) {
            $num_files = count($_FILES['uploadFiles']['name']);
            for ($i = 0; $i < $num_files; $i++) {
                $name = $_FILES['uploadFiles']['name'][$i];
                $tmp_name = $_FILES['uploadFiles']['tmp_name'][$i];
                $ext = 'jpg';
       
                // the path of uploaded files are defined according to the rule below.
                // timestamp + sequential number + "_"
                // + MD5 generated by micro seconds, original file name and accessed IP + extension
                $new_file_name = time() . $i . '_'
                    . md5(microtime() . $name . $_SERVER['REMOTE_ADDR'])
                    . '.' . $ext;
                $move_to = IMG_DIR . $new_file_name;
              
                move_uploaded_file($tmp_name, $move_to);
            }

"uploadFiles"でPOSTされたデータを受け取っています。
送信側では"uploadFiles"のように、を付けることで配列としてデータを送ることができます。これを忘れていたことでえらいハマっていまいました。[]を付けていないと一番最後にaddBinaryBodyしたデータのみ送信されます。なので、上記のサンプルコードの場合、InputStreamを詰めた場合のみ複数のデータをPOSTしてくれます。

久々に記事を書いたので、とりあえずこれくらいのネタから。。。


POSTの進捗を管理するための機能を実装しました。↓

Androidでmultipart/form-dataをPOST送信しながら、POSTの進捗をActivityに通知する。 - kakueki61's dev history