Solicitud de trabajo POST de varias partes con Voley y sin HttpEntity

106

Esto no es realmente una pregunta, sin embargo, me gustaría compartir algo de mi código de trabajo aquí para su referencia cuando lo necesite.

Como sabemos, HttpEntityestá obsoleto de API22 y completamente eliminado desde API23. Por el momento, ya no podemos acceder a HttpEntity Reference en Android Developer (404). Entonces, el siguiente es mi código de muestra de trabajo para POST Multipart Request con Volley y sin HttpEntity . Está funcionando, probado con Asp.Net Web API. Por supuesto, el código tal vez sea solo una muestra básica que publica dos archivos dibujables existentes, tampoco es la mejor solución para todos los casos, y no es un buen ajuste.

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);
    }
}

ACTUALIZAR:

Para la parte de texto, consulte la respuesta de @ Oscar a continuación.

BNK
fuente
1
Acabo de copiar el comentario de @Kevin en la siguiente pregunta : Algunos servidores son MUY exigentes. Si tiene problemas, agregue un ESPACIO entre ";" y "filename =" al construir Content-Disposition y "multipart / form-data; boundary =" + boundary; :)
BNK
1
si desea agregar mimtype: dataOutputStream.writeBytes ("Content-Type: image / jpeg" + lineEnd);
Maor Hadad
1
@MaorHadad: gracias por tu comentario :)
BNK
1
Gracias por esta gran solución. después de actualizar a appcompat 23, este problema fue doloroso **
Maor Hadad
1
Estimado BNK, ¿esto funciona para subir videos?
Mok

Respuestas:

63

Reescribo tu código @RacZo y @BNK más modular y fácil de usar como

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);

Verifique el código completo VolleyMultipartRequesten mi esencia .

Angga Ari Wijaya
fuente
No quiero convertir en bytes o cadenas. para mi caso, el lado del servidor espera un archivo y varios textos (par clave-valor, datos de formulario de varias partes), no el byte o la cadena. ¿es eso posible?
Milon
sí lo es, puede tratar sus datos en el lado del servidor como datos de formularios de varias partes en un formulario web, en realidad, el código es modificar la solicitud de encabezado http para que se ajuste a un formulario web similar, por lo que es la solución que está buscando ...
Angga Ari Wijaya
@AhamadullahSaikat tienes algo porque lo mismo en mi proyecto envía una solicitud múltiple con el mismo nombre
Ricky Patel
20

Solo quiero agregar algo a la respuesta. Estaba tratando de averiguar cómo agregar campos de texto al cuerpo y creé la siguiente función para hacerlo:

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);
}

Funciona bastante bien.

Oscar Salguero
fuente
He integrado la solución @BNK en mi aplicación. Estoy seleccionando una foto en la biblioteca de mi teléfono y la envío a través de datos de formulario de varias partes, pero se necesitan un par de segundos (~ 10-15) antes de que se envíe al servidor. ¿Hay alguna forma de reducir estos gastos generales? o alguna otra recomendación?
casillas
1
Después de la desaprobación de HttpEntity, las cargas de varias partes con voley se volvieron muy engorrosas, además de implementar solicitudes de PATCH es un dolor de cabeza, así que terminé alejándome de voley e implementé RetroFit ( square.github.io/retrofit ) en todas mis aplicaciones. Te recomendaría que hicieras lo mismo porque RetroFit te ofrece una mejor compatibilidad con versiones anteriores y prueba el futuro de tu aplicación.
Oscar Salguero
Estoy de acuerdo con @RacZo, sin embargo, prefiero OkHttp :), sigo usando Volley para otras solicitudes de red. He personalizado la descarga de Google para eliminar Apache lib, publicado en github.com/ngocchung/volleynoapache , sin embargo, solo probé para Get, Post y Multipart.
BNK
Necesito usar la solicitud PATCH usando la biblioteca Volley. ¿Cómo puedo lograr esto?
Jagadesh Seeram
8

Para aquellos que están luchando por enviar parámetros utf-8 y aún no han tenido suerte, el problema que tuve fue en el dataOutputStream y cambiar el código de @RacZo al código siguiente:

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);
    } 
Sepehr Nozaryian
fuente
2

Aquí hay una versión de Kotlin de una clase que permite la solicitud de varias partes con Volley 1.1.1.

Se basa principalmente en la solución de @ BNK, pero está ligeramente simplificado. No noté ningún problema de rendimiento en particular. Subí una foto de 5 Mb en unos 3 segundos.

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)
        }
    }
}
Ika
fuente
Hola. ¿Más información sobre cómo podemos usar la clase anterior? Por ejemplo, ¿cómo podemos cargar una imagen .jpg con él?
Thanasis
0

Encontré un contenedor de la biblioteca de volley original que es más fácil de integrar para solicitudes de varias partes. También admite la carga de datos de varias partes junto con otros parámetros de solicitud. Por lo tanto, estoy compartiendo mi código para los futuros desarrolladores que podrían encontrarse con el problema que estaba teniendo (es decir, cargar datos de varias partes usando volley junto con algunos otros parámetros).

Agregue la siguiente biblioteca en el build.gradlearchivo.

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

Tenga en cuenta que eliminé la biblioteca de volley original de mibuild.gradle y usé la biblioteca anterior en su lugar, que puede manejar solicitudes de varias partes y normales con una técnica de integración similar.

Luego solo tuve que escribir la siguiente clase que maneja la operación de solicitud 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);
    }
}

Ahora ejecute la tarea como la siguiente.

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

Puede cargar un archivo a la vez utilizando esta biblioteca. Sin embargo, pude subir varios archivos con solo iniciar varias tareas.

¡Espero que ayude!

Reaz Murió
fuente