Рабочий многоточечный запрос POST с волейболом и без HttpEntity

На самом деле это не вопрос, однако я хотел бы поделиться с вами некоторым моим рабочим кодом для справки, когда вам это нужно.

Как мы знаем, HttpEntity устарела с API22 и полностью удалена с API23. В настоящее время мы больше не можем получить доступ к HttpEntity Reference на Android Developer (404). Итак, вот мой рабочий пример кода для POST Multipart Request с Volley и без HttpEntity. Это работает, протестировано с Asp.Net Web API. Конечно, код, возможно, является просто базовым примером, в котором публикуются два существующих отрисовываемых файла, а также не является лучшим решением для всех случаев и не является хорошей настройкой.

MultipartActivity.java:

package com.example.multipartvolley;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.support.v4.content.ContextCompat;
import android.view.Menu;
import android.view.MenuItem;
import android.widget.Toast;

import com.android.volley.NetworkResponse;
import com.android.volley.Response;
import com.android.volley.VolleyError;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;


public class MultipartActivity extends Activity {

    private final Context context = this;
    private final String twoHyphens = "--";
    private final String lineEnd = "\r\n";
    private final String boundary = "apiclient-" + System.currentTimeMillis();
    private final String mimeType = "multipart/form-data;boundary=" + boundary;
    private byte[] multipartBody;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_multipart);

        byte[] fileData1 = getFileDataFromDrawable(context, R.drawable.ic_action_android);
        byte[] fileData2 = getFileDataFromDrawable(context, R.drawable.ic_action_book);

        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        DataOutputStream dos = new DataOutputStream(bos);
        try {
            // the first file
            buildPart(dos, fileData1, "ic_action_android.png");
            // the second file
            buildPart(dos, fileData2, "ic_action_book.png");
            // send multipart form data necesssary after file data
            dos.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd);
            // pass to multipart body
            multipartBody = bos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }

        String url = "http://192.168.1.100/api/postfile";
        MultipartRequest multipartRequest = new MultipartRequest(url, null, mimeType, multipartBody, new Response.Listener<NetworkResponse>() {
            @Override
            public void onResponse(NetworkResponse response) {
                Toast.makeText(context, "Upload successfully!", Toast.LENGTH_SHORT).show();
            }
        }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                Toast.makeText(context, "Upload failed!\r\n" + error.toString(), Toast.LENGTH_SHORT).show();
            }
        });

        VolleySingleton.getInstance(context).addToRequestQueue(multipartRequest);
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        // Inflate the menu; this adds items to the action bar if it is present.
        getMenuInflater().inflate(R.menu.menu_multipart, menu);
        return true;
    }

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();

        //noinspection SimplifiableIfStatement
        if (id == R.id.action_settings) {
            return true;
        }

        return super.onOptionsItemSelected(item);
    }

    private void buildPart(DataOutputStream dataOutputStream, byte[] fileData, String fileName) throws IOException {
        dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd);
        dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"uploaded_file\"; filename=\""
                + fileName + "\"" + lineEnd);
        dataOutputStream.writeBytes(lineEnd);

        ByteArrayInputStream fileInputStream = new ByteArrayInputStream(fileData);
        int bytesAvailable = fileInputStream.available();

        int maxBufferSize = 1024 * 1024;
        int bufferSize = Math.min(bytesAvailable, maxBufferSize);
        byte[] buffer = new byte[bufferSize];

        // read file and write it into form...
        int bytesRead = fileInputStream.read(buffer, 0, bufferSize);

        while (bytesRead > 0) {
            dataOutputStream.write(buffer, 0, bufferSize);
            bytesAvailable = fileInputStream.available();
            bufferSize = Math.min(bytesAvailable, maxBufferSize);
            bytesRead = fileInputStream.read(buffer, 0, bufferSize);
        }

        dataOutputStream.writeBytes(lineEnd);
    }

    private byte[] getFileDataFromDrawable(Context context, int id) {
        Drawable drawable = ContextCompat.getDrawable(context, id);
        Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.PNG, 0, byteArrayOutputStream);
        return byteArrayOutputStream.toByteArray();
    }
}

MultipartRequest.java:

package com.example.multipartvolley;

import com.android.volley.AuthFailureError;
import com.android.volley.NetworkResponse;
import com.android.volley.ParseError;
import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.android.volley.toolbox.HttpHeaderParser;

import java.util.Map;

class MultipartRequest extends Request<NetworkResponse> {
    private final Response.Listener<NetworkResponse> mListener;
    private final Response.ErrorListener mErrorListener;
    private final Map<String, String> mHeaders;
    private final String mMimeType;
    private final byte[] mMultipartBody;

    public MultipartRequest(String url, Map<String, String> headers, String mimeType, byte[] multipartBody, Response.Listener<NetworkResponse> listener, Response.ErrorListener errorListener) {
        super(Method.POST, url, errorListener);
        this.mListener = listener;
        this.mErrorListener = errorListener;
        this.mHeaders = headers;
        this.mMimeType = mimeType;
        this.mMultipartBody = multipartBody;
    }

    @Override
    public Map<String, String> getHeaders() throws AuthFailureError {
        return (mHeaders != null) ? mHeaders : super.getHeaders();
    }

    @Override
    public String getBodyContentType() {
        return mMimeType;
    }

    @Override
    public byte[] getBody() throws AuthFailureError {
        return mMultipartBody;
    }

    @Override
    protected Response<NetworkResponse> parseNetworkResponse(NetworkResponse response) {
        try {
            return Response.success(
                    response,
                    HttpHeaderParser.parseCacheHeaders(response));
        } catch (Exception e) {
            return Response.error(new ParseError(e));
        }
    }

    @Override
    protected void deliverResponse(NetworkResponse response) {
        mListener.onResponse(response);
    }

    @Override
    public void deliverError(VolleyError error) {
        mErrorListener.onErrorResponse(error);
    }
}

ОБНОВИТЬ:

Для текстовой части, пожалуйста, обратитесь к ответу @Oscar ниже.

Ответ 1

Я переписываю ваш код @RacZo и @BNK более модульным и простым в использовании, как

VolleyMultipartRequest multipartRequest = new VolleyMultipartRequest(Request.Method.POST, url, new Response.Listener<NetworkResponse>() {
    @Override
    public void onResponse(NetworkResponse response) {
        String resultResponse = new String(response.data);
        // parse success output
    }
}, new Response.ErrorListener() {
    @Override
    public void onErrorResponse(VolleyError error) {                
        error.printStackTrace();
    }
}) {
    @Override
    protected Map<String, String> getParams() {
        Map<String, String> params = new HashMap<>();
        params.put("api_token", "gh659gjhvdyudo973823tt9gvjf7i6ric75r76");
        params.put("name", "Angga");
        params.put("location", "Indonesia");
        params.put("about", "UI/UX Designer");
        params.put("contact", "[email protected]");
        return params;
    }

    @Override
    protected Map<String, DataPart> getByteData() {
        Map<String, DataPart> params = new HashMap<>();
        // file name could found file base or direct access from real path
        // for now just get bitmap data from ImageView
        params.put("avatar", new DataPart("file_avatar.jpg", AppHelper.getFileDataFromDrawable(getBaseContext(), mAvatarImage.getDrawable()), "image/jpeg"));
        params.put("cover", new DataPart("file_cover.jpg", AppHelper.getFileDataFromDrawable(getBaseContext(), mCoverImage.getDrawable()), "image/jpeg"));

        return params;
    }
};

VolleySingleton.getInstance(getBaseContext()).addToRequestQueue(multipartRequest);

Проверьте полный код VolleyMultipartRequest в моей сути.

Ответ 2

Просто хочу добавить в ответ. Я пытался понять, как добавить текстовые поля к телу, и создал для этого следующую функцию:

private void buildTextPart(DataOutputStream dataOutputStream, String parameterName, String parameterValue) throws IOException {
    dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd);
    dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"" + parameterName + "\"" + lineEnd);
    dataOutputStream.writeBytes("Content-Type: text/plain; charset=UTF-8" + lineEnd);
    dataOutputStream.writeBytes(lineEnd);
    dataOutputStream.writeBytes(parameterValue + lineEnd);
}

Это работает довольно хорошо.

Ответ 3

Для тех, кто изо всех сил пытается отправить параметры utf-8 и все еще не везет, проблема, с которой я столкнулся, была в dataOutputStream, и измените код @RacZo на код ниже:

private void buildTextPart(DataOutputStream dataOutputStream, String parameterName, String parameterValue) throws IOException {
        dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd);
        dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"");
        dataOutputStream.write(parameterName.getBytes("UTF-8"));
        dataOutputStream.writeBytes(lineEnd);
        dataOutputStream.writeBytes("Content-Type: text/plain; charset=UTF-8" + lineEnd);
        dataOutputStream.writeBytes(lineEnd);
        dataOutputStream.write(parameterValue.getBytes("UTF-8"));
        dataOutputStream.writeBytes(lineEnd);
    } 

Ответ 4

Вот версия Kotlin класса, разрешающего многочастный запрос с Volley 1.1.1.

В основном он основан на решении @BNK, но немного упрощен. Я не заметил какой-либо конкретной проблемы с производительностью. Я загрузил 5-мегабайтную картинку примерно за 3 секунды.

class MultipartWebservice(context: Context) {

    private var queue: RequestQueue? = null

    private val boundary = "apiclient-" + System.currentTimeMillis()
    private val mimeType = "multipart/form-data;boundary=$boundary"

    init {
        queue = Volley.newRequestQueue(context)
    }

    fun sendMultipartRequest(
        method: Int,
        url: String,
        fileData: ByteArray,
        fileName: String,
        listener: Response.Listener<NetworkResponse>,
        errorListener: Response.ErrorListener
    ) {

        // Create multi part byte array
        val bos = ByteArrayOutputStream()
        val dos = DataOutputStream(bos)
        buildMultipartContent(dos, fileData, fileName)
        val multipartBody = bos.toByteArray()

        // Request header, if needed
        val headers = HashMap<String, String>()
        headers["API-TOKEN"] = "458e126682d577c97d225bbd73a75b5989f65e977b6d8d4b2267537019ad9d20"

        val request = MultipartRequest(
            method,
            url,
            errorListener,
            listener,
            headers,
            mimeType,
            multipartBody
        )

        queue?.add(request)

    }

    @Throws(IOException::class)
    private fun buildMultipartContent(dos: DataOutputStream, fileData: ByteArray, fileName: String) {

        val twoHyphens = "--"
        val lineEnd = "\r\n"

        dos.writeBytes(twoHyphens + boundary + lineEnd)
        dos.writeBytes("Content-Disposition: form-data; name=\"file\"; filename=\"$fileName\"$lineEnd")
        dos.writeBytes(lineEnd)
        dos.write(fileData)
        dos.writeBytes(lineEnd)
        dos.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd)
    }

    class MultipartRequest(
        method: Int,
        url: String,
        errorListener: Response.ErrorListener?,
        private var listener: Response.Listener<NetworkResponse>,
        private var headers: MutableMap<String, String>,
        private var mimeType: String,
        private var multipartBody: ByteArray
    ) : Request<NetworkResponse>(method, url, errorListener) {

        override fun getHeaders(): MutableMap<String, String> {
            return if (headers.isEmpty()) super.getHeaders() else headers
        }

        override fun getBodyContentType(): String {
            return mimeType
        }

        override fun getBody(): ByteArray {
            return multipartBody
        }

        override fun parseNetworkResponse(response: NetworkResponse?): Response<NetworkResponse> {
            return try {
                Response.success(response, HttpHeaderParser.parseCacheHeaders(response))
            } catch (e: Exception) {
                Response.error(ParseError(e))
            }
        }

        override fun deliverResponse(response: NetworkResponse?) {
            listener.onResponse(response)
        }
    }
}

Ответ 5

Я нашел обертку из исходной библиотеки залпов, которую легче интегрировать для запросов из нескольких частей. Он также поддерживает загрузку данных из нескольких частей вместе с другими параметрами запроса. Поэтому я делюсь своим кодом для будущих разработчиков, которые могут столкнуться с проблемой, с которой я столкнулся (т.е. загружать данные из нескольких частей, используя залп вместе с некоторыми другими параметрами).

Добавьте следующую библиотеку в файл build.gradle.

dependencies {
    compile 'dev.dworks.libs:volleyplus:+'
}

Обратите внимание, что я удалил исходную библиотеку залпов из своего build.gradle и вместо этого использовал вышеуказанную библиотеку, которая может обрабатывать как многочастные, так и обычные запросы, имеющие сходную технику интеграции.

Тогда мне просто нужно было написать следующий класс, который обрабатывает операцию запроса POST.

public class POSTMediasTask {
    public void uploadMedia(final Context context, String filePath) {

        String url = getUrlForPOSTMedia(); // This is a dummy function which returns the POST url for you
        SimpleMultiPartRequest multiPartRequestWithParams = new SimpleMultiPartRequest(Request.Method.POST, url,
                new Response.Listener<String>() {
                    @Override
                    public void onResponse(String response) {
                        Log.d("Response", response);
                        // TODO: Do something on success
                    }
                }, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                // TODO: Handle your error here
            }
        });

        // Add the file here
        multiPartRequestWithParams.addFile("file", filePath);

        // Add the params here
        multiPartRequestWithParams.addStringParam("param1", "SomeParamValue1");
        multiPartRequestWithParams.addStringParam("param2", "SomeParamValue2");

        RequestQueue queue = Volley.newRequestQueue(context);
        queue.add(multiPartRequestWithParams);
    }
}

Теперь выполните задачу следующим образом.

new POSTMediasTask().uploadMedia(context, mediaPath);

Вы можете загрузить один файл за один раз, используя эту библиотеку. Однако мне удалось загрузить несколько файлов, просто инициировав несколько задач.

Надеюсь, это поможет!

Ответ 6

изменил @Angga Ari Wijaya помощник, вместо того, чтобы использовать растровое изображение ImageView, использовать необработанное растровое изображение, на которое пользователь загрузил:

VolleyMultiPartЗапрос от Ангга ты

Хелпер класс

import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.support.v4.content.ContextCompat;

import java.io.ByteArrayOutputStream;

/**
 * Sketch Project Studio
 * Created by Angga on 12/04/2016 14.27.
 */
public class AppHelper {

    /**
     * Turn drawable resource into byte array.
     *
     * @param context parent context
     * @param id      drawable resource id
     * @return byte array
     */
    public static byte[] getFileDataFromDrawable(Context context, Bitmap bitmap) {
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.PNG, 0, byteArrayOutputStream);
        return byteArrayOutputStream.toByteArray();
    }

    /**
     * Turn drawable into byte array.
     *
     * @param drawable data
     * @return byte array
     */
    public static byte[] getFileDataFromDrawable(Context context, Drawable drawable) {
        Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        bitmap.compress(Bitmap.CompressFormat.JPEG, 80, byteArrayOutputStream);
        return byteArrayOutputStream.toByteArray();
    }
}

Деятельность

 private void initImageUpload() {
    Intent it = new Intent();
    it.setType("image/*");
    String[] mimeTypes = {"image/jpeg", "image/png"};
    it.setAction(Intent.ACTION_GET_CONTENT);
    it.putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes);
    startActivityForResult(it, GALLERY_REQUEST_CODE);
}


@Override
public void onActivityResult(int requestCode,int resultCode,Intent data){
    // Result code is RESULT_OK only if the user selects an Image
    if (resultCode == Activity.RESULT_OK)
        switch (requestCode){
            case GALLERY_REQUEST_CODE:
                //data.getData returns the content URI for the selected Image
                filePath = data.getData();

                // Set the Image in ImageView
                try {
                    bitmap = null;
                    profilePicture.setImageBitmap(null);
                    bitmap = MediaStore.Images.Media.getBitmap(getContentResolver(), filePath);
                    profilePicture.setImageBitmap(bitmap);
                } catch (IOException e) {
                    e.printStackTrace();
                }

                //profilePicture.setImageURI(selectedImage);
                //Picasso.get().load(filePath).fit().into(profilePicture);

                // upload image
                uploadImage(filePath);
                break;
        }
}


private void uploadImage(Uri filePath) {
    // getPath
    String path = getPath(filePath);

    // get the acess token
    final String accessToken = logOutSingleton.getLoginToken();

    VolleyMultipartRequest multipartRequest = new VolleyMultipartRequest(Request.Method.PATCH, baseUrl + "api/v0.6/user-profile/edit-image/", new Response.Listener<NetworkResponse>() {
        @Override
        public void onResponse(NetworkResponse response) {
            String resultResponse = new String(response.data);
            Log.d("response", resultResponse);
            try {
                JSONObject result = new JSONObject(resultResponse);
                String url = result.getString("local_image");
                logOutSingleton.setImgUrl(url);
            } catch (JSONException e) {
                e.printStackTrace();
            }

        }
    }, new Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError volleyError) {
            String message = "";
            if (volleyError instanceof NetworkError) {
                message = "Cannot connect to Internet...Please check your connection!";
            } else if (volleyError instanceof ServerError) {
                message = "The server could not be found. Please try again after some time!!";
            } else if (volleyError instanceof AuthFailureError) {
                message = "Cannot connect to Internet...Please check your connection!";
            } else if (volleyError instanceof ParseError) {
                message = "Parsing error! Please try again after some time!!";
            } else if (volleyError instanceof NoConnectionError) {
                message = "Cannot connect to Internet...Please check your connection!";
            } else if (volleyError instanceof TimeoutError) {
                message = "Connection TimeOut! Please check your internet connection.";
            }
            Log.d("volleyError", message);
        }
    }) {
        @Override
        public Map<String, String> getHeaders() {
            String bearer = "Bearer " + accessToken;
            Map<String, String> params = new HashMap<>();
            params.put("Authorization", bearer);
            return params;
        }

        @Override
        protected Map<String, DataPart> getByteData() {
            Map<String, DataPart> params = new HashMap<>();
            // file name could found file base or direct access from real path
            // for now just get bitmap data from ImageView
            params.put("local_image", new DataPart("local_image.png", AppHelper.getFileDataFromDrawable(getBaseContext(), bitmap), "image/png"));
            return params;
        }
    };

    MySingleton.getInstance(getApplicationContext()).addToRequestQueue(multipartRequest);
}



protected String getPath(Uri uri){
    Cursor cursor = getContentResolver().query(uri, null, null, null, null);
    cursor.moveToFirst();
    String document_id = cursor.getString(0);

    document_id = document_id.substring(document_id.lastIndexOf(":")+1);
    cursor.close();

    cursor = getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
            null, MediaStore.Images.Media._ID + " = ? ", new String[]{document_id}, null
    );
    cursor.moveToFirst();
    String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
    cursor.close();
    return path;

}

Какая разница?

  • в реальном мире мы просим пользователя загрузить изображение, поэтому, если мы получим растровое изображение, оно может потерять качество или форму.

  • Я тестировал изменение изображения, и иногда растровое изображение, отправленное на сервер, было повреждено в некоторых пикселях из-за этого.

  • решение простое, так как хранение необработанного растрового изображения наActivityResult

@Angga Ari Wijaya, ваш шаблон MultipartRequest был идеальным, надеюсь, вы увидите это как дополнение к реальному окружению.

Рад был помочь

PS: в этом проекте я использую однонаправленный тип аутентификации, поэтому я переопределяю заголовки.