kakueki61's dev history

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

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

[Draft] 前回、multipart/form-dataでファイルなどをPOSTする方法を書きましたが、

Androidでサーバーに画像データをPOSTする - kakueki61's dev history

アップロード中に進捗表示をしてあげた方がユーザーフレンドリーだろうということで、 進捗表示を実装しましたので書いておきたい思います。

やったこととしては、

  1. multipart/form-dataの形式に則ってデータを構築
  2. HttpUrlConnectionからOutputStreamを取得
  3. データをOutputStreamに流す
  4. どれだけ流しおえたかをActivityに通知する。

前回のおさらい

org.apache.http.entity.mime.MultipartEntityBuilderでmultipart/form-data用のデータを作成。
自作のHurlStack派生クラスを作成し、performRequest()をoverride.
performRequest()内で、HttpClient#execute(HttpPost request)によってMultipartFormEntityをPOST。

今回、データアップロード中の進捗を表示をするために問題だったのは、
HttpClientがネットワークへのデータ書き出しの部分を隠蔽してしまっているので
そこをこちらでハンドリングするのが困難だという点でした。
ので、HttpClientを使うのはやめてHttpURLConnectionを使うようにします。
実際、Volleyでデフォルトで使われているHurlStackではHttpURLConnectionを使っています。

HttpURLConnectionを使うと大変なこと

MultipartEntityBuilderが使えません。
つまり、multipart/form-data用のデータをHTTPの仕様に沿って自前で構築する必要があります。 実際にどのような形式のデータを送る必要があるかはこちらを参考にして下さい。

MultipartJsonRequest.java

public class MultipartJsonRequest extends JsonRequest<JSONObject> {
    private static final String TAG = MultipartJsonRequest.class.getSimpleName();
    private static final String BOUNDARY = "___________________" + Long.toString(System.currentTimeMillis());

    private Activity mActivity;
    private Map<String, String> mStringParams;
    private Map<String, InputStream> mBinaryParams;
    private INetworkProgressListener mProgressListener;
    private Handler mHandler;

    public MultipartJsonRequest(Activity activity, String url, Map<String, String> stringParams, Map<String, InputStream> bynaryParams,
                                INetworkProgressListener progressListener,
                                Response.Listener<JSONObject> listener,
                                Response.ErrorListener errorListener) {
        
        super(activity, url, null, false, listener, errorListener);

        mStringParams = stringParams;
        mBinaryParams = bynaryParams;
        mProgressListener = progressListener;

        // Activityに通知するためのHandler
        mHandler = new Handler(Looper.getMainLooper(), null);
    }

    public Map<String, String> getStringParams() {
        return mStringParams;
    }

    public Map<String, InputStream> getBinaryParams() {
        return mBinaryParams;
    }

    @Override
    public String getBodyContentType() {
        return "multipart/form-data; boundary=" + BOUNDARY + "; charset=UTF-8";
    }

    public String getBoundaryString() {
        return BOUNDARY;
    }

    public void deliverProgress(final int numCompleted) {
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                mProgressListener.onNetworkProgress(numCompleted);
            }
        });
    }

    @Override
    protected Response<JSONObject> parseNetworkResponse(NetworkResponse response) {
        // 省略
    }
}

MultipartHurlStack.java

    @Override
    public HttpResponse performRequest(Request<?> request, Map<String, String> additionalHeaders)
            throws IOException, AuthFailureError {
        if (!(request instanceof MultipartJsonRequest)) {
            return super.performRequest(request, additionalHeaders);
        }

        mMultipartRequest = (MultipartJsonRequest) request;
        mBoundary = mMultipartRequest.getBoundaryString();

        URL url = new URL(mMultipartRequest.getUrl());
        HttpURLConnection connection = openConnection(url, request);

        addHeaders(connection, additionalHeaders);
        addHeaders(connection, request.getHeaders());

        connection.setChunkedStreamingMode(0);

        DataOutputStream outputStream = new DataOutputStream(connection.getOutputStream());

        try {
            writeStringParams(outputStream, mMultipartRequest.getStringParams());
            writeBinaryParams(outputStream, mMultipartRequest.getBinaryParams());

            finishWriting(outputStream);
        } catch (IOException e) {
            e.printStackTrace();
            throw new IOException("IOException: during writing requests");
        }
        ProtocolVersion protocolVersion = new ProtocolVersion("HTTP", 1, 1);
        int responseCode = connection.getResponseCode();
        if (responseCode == -1) {
            // -1 is returned by getResponseCode() if the response code could not be retrieved.
            // Signal to the caller that something was wrong with the connection.
            throw new IOException("Could not retrieve response code from HttpUrlConnection.");
        }
        StatusLine responseStatus = new BasicStatusLine(protocolVersion,
                connection.getResponseCode(), connection.getResponseMessage());
        BasicHttpResponse response = new BasicHttpResponse(responseStatus);
        response.setEntity(entityFromConnection(connection));
        for (Map.Entry<String, List<String>> header : connection.getHeaderFields().entrySet()) {
            if (header.getKey() != null) {
                Header h = new BasicHeader(header.getKey(), header.getValue().get(0));
                response.addHeader(h);
            }
        }
        return response;
    }

    private HttpURLConnection openConnection(URL url, Request<?> request) throws IOException {
        HttpURLConnection connection = (HttpURLConnection) url.openConnection();
        int timeoutMs = request.getTimeoutMs();     //TODO いる?
    LogHelper.i(TAG, "timeoutMs: " + timeoutMs);
        connection.setConnectTimeout(timeoutMs);
        connection.setReadTimeout(timeoutMs);

        connection.setUseCaches(false);
        connection.setDoInput(true);
        connection.setDoOutput(true);
        connection.setRequestMethod("POST");
        connection.setRequestProperty("Content-Type", mMultipartRequest.getBodyContentType());

        return connection;
    }

    private void writeStringParams(DataOutputStream outputStream, Map<String, String> stringParams) throws IOException {
        StringBuilder builder = new StringBuilder();
        for (Map.Entry<String, String> entry : stringParams.entrySet()) {
            builder.append(TWO_HYPHENS + mBoundary + LINE_END);
            builder.append("Content-Disposition: form-data; name=\"" + entry.getKey() + "\"" + LINE_END);
            builder.append(LINE_END);
            builder.append(entry.getValue());
            builder.append(LINE_END);
        }
        LogHelper.d(TAG, "stringParams: \n" + builder.toString());

        outputStream.write(builder.toString().getBytes(Constants.DEFAULT_CHARSET));
        outputStream.flush();
    }

    private void writeBinaryParams(DataOutputStream outputStream, Map<String, InputStream> binaryParams) throws IOException {
        int numCompleted = 0;
        for (Map.Entry<String, InputStream> entry : binaryParams.entrySet()) {
            StringBuilder builder = new StringBuilder();
            builder.append(TWO_HYPHENS + mBoundary + LINE_END);
            builder.append("Content-Disposition: form-data; " +
                    "name=\"uploadFiles[]\"; " +
                    "filename=\"" + entry.getKey() + "\"" + LINE_END);
            builder.append("Content-Type: multipart/form-data" + LINE_END);     //TODO content-type確認
            builder.append(LINE_END);
    LogHelper.d(TAG, "binaryParams: \n" + builder.toString());

            outputStream.writeBytes(builder.toString());
            outputStream.flush();

            InputStream inputStream = entry.getValue();
            byte[] buffer = new byte[DEFAULT_BUFFER_SIZE];
            int readBytes = -1;
            while((readBytes = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, readBytes);
            }
            outputStream.flush();
            inputStream.close();

            outputStream.writeBytes(LINE_END);
            outputStream.flush();
    LogHelper.i(TAG, "finished!!!!");

            numCompleted++;
            mMultipartRequest.deliverProgress(numCompleted);
        }
    }

    private void finishWriting(DataOutputStream outputStream) throws IOException {
        StringBuilder builder = new StringBuilder();
        builder.append(TWO_HYPHENS + mBoundary + TWO_HYPHENS + LINE_END);
        outputStream.writeBytes(builder.toString());
        outputStream.flush();
        outputStream.close();
    }

    private void addHeaders(HttpURLConnection conn, Map<String, String> headers) {
        for (String key : headers.keySet()) {
            conn.setRequestProperty(key, headers.get(key));
        }
    }

続きは近いうちに書きます。


参考サイト

Upload files by sending multipart request programmatically

HTTPマルチパートデータ送信 « androidnote