Requête POST Multipart avec Volley et sans HttpEntity

106

Ce n'est pas vraiment une question, cependant, je voudrais partager une partie de mon code de travail ici pour votre référence lorsque vous en avez besoin.

Comme nous le savons, il HttpEntityest obsolète d'API22 et entièrement supprimé depuis API23. Pour le moment, nous ne pouvons plus accéder à HttpEntity Reference sur Android Developer (404). Ainsi, voici mon exemple de code de travail pour POST Multipart Request avec Volley et sans HttpEntity . Cela fonctionne, testé avec Asp.Net Web API. Bien sûr, le code n'est peut-être qu'un exemple de base qui publie deux fichiers dessinables existants, n'est pas non plus la meilleure solution dans tous les cas et n'est pas un bon réglage.

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

METTRE À JOUR:

Pour la partie texte, veuillez vous référer à la réponse de @ Oscar ci-dessous.

BNK
la source
1
Je viens de copier le commentaire de @Kevin à la question suivante : Certains serveurs sont TRÈS difficiles. Si vous rencontrez des problèmes, ajoutez un ESPACE entre ";" et "filename =" lors de la construction de Content-Disposition et "multipart / form-data; boundary =" + boundary; :)
BNK
1
si vous souhaitez ajouter le type mim: dataOutputStream.writeBytes ("Content-Type: image / jpeg" + lineEnd);
Maor Hadad
1
@MaorHadad: merci pour votre commentaire :)
BNK
1
Merci pour cette excellente solution. après la mise à jour vers appcompat 23, ce problème était douloureux dans le a **
Maor Hadad
1
Cher BNK, cela fonctionne-t-il pour le téléchargement de vidéos?
mok

Réponses:

63

Je réécris votre code @RacZo et @BNK plus modulable et facile à utiliser comme

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

Vérifiez plein de code VolleyMultipartRequestà mon sens .

Angga Ari Wijaya
la source
Je ne veux pas convertir en octet ou en chaîne. pour mon cas, côté serveur attend un fichier et plusieurs textes (paire clé-valeur, données de formulaire en plusieurs parties), pas l'octet ou la chaîne. est-ce possible?
Milon
oui, vous pouvez traiter vos données côté serveur comme des données de formulaire en plusieurs parties sous forme Web, en fait, le code modifie la demande d'en-tête http pour l'adapter à un formulaire Web similaire, c'est donc la solution que vous recherchez ...
Angga Ari Wijaya
@AhamadullahSaikat vous avez quelque chose parce que la même chose dans mon projet envoie plusieurs requêtes avec le même nom
Ricky Patel
20

Je veux juste ajouter à la réponse. J'essayais de comprendre comment ajouter des champs de texte au corps et j'ai créé la fonction suivante pour le faire:

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

Cela fonctionne plutôt bien.

Oscar Salguero
la source
J'ai intégré la solution @BNK dans mon application. Je sélectionne une photo dans la bibliothèque de mon téléphone et je l'envoie via des données de formulaire en plusieurs parties, mais cela prend quelques secondes (~ 10-15) avant qu'elle ne soit envoyée au serveur. Existe-t-il un moyen de réduire ces frais généraux? ou toute autre recommandation?
casillas
1
Après la dépréciation de HttpEntity, les téléchargements en plusieurs parties avec volley sont devenus très fastidieux, et la mise en œuvre des requêtes PATCH est un casse-tête, alors j'ai fini par m'éloigner de volley et implémenté RetroFit ( square.github.io/retrofit ) dans toutes mes applications. Je vous recommanderais de faire de même car RetroFit vous offre une meilleure compatibilité ascendante et des preuves futures de votre application.
Oscar Salguero
Je suis d'accord avec @RacZo, mais je préfère OkHttp :), j'utilise toujours Volley pour d'autres requêtes réseau. J'ai personnalisé la librairie Apache de suppression de volée de Google, publiée sur github.com/ngocchung/volleynoapache , mais uniquement testé pour Get, Post et Multipart.
BNK
J'ai besoin d'utiliser la requête PATCH en utilisant la bibliothèque Volley. Comment puis-je y parvenir.
Jagadesh Seeram
8

Pour ceux qui ont du mal à envoyer les paramètres utf-8 et qui n'ont toujours pas de chance, le problème que j'ai eu était dans le dataOutputStream, et changez le code de @RacZo en code ci-dessous:

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
la source
2

Voici une version Kotlin d'une classe permettant la requête en plusieurs parties avec Volley 1.1.1.

Il est principalement basé sur la solution de @ BNK mais légèrement simplifié. Je n'ai remarqué aucun problème de performances particulier. J'ai téléchargé une photo de 5 Mo en 3 secondes environ.

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
la source
Salut. Avez-vous plus d'informations sur la façon dont nous pouvons utiliser la classe ci-dessus? Par exemple, comment pouvons-nous télécharger une image .jpg avec?
Thanasis
0

J'ai trouvé un wrapper de la bibliothèque de volée originale qui est plus facile à intégrer pour les demandes en plusieurs parties. Il prend également en charge le téléchargement des données en plusieurs parties avec d'autres paramètres de demande. Par conséquent, je partage mon code pour les futurs développeurs qui pourraient rencontrer le problème que j'avais (c'est-à-dire télécharger des données en plusieurs parties en utilisant volley avec d'autres paramètres).

Ajoutez la bibliothèque suivante dans le build.gradlefichier.

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

Veuillez noter que j'ai supprimé la bibliothèque de volée d'origine de monbuild.gradle et utilisé la bibliothèque ci-dessus à la place, qui peut gérer à la fois les demandes en plusieurs parties et normales ayant une technique d'intégration similaire.

Ensuite, je devais juste écrire la classe suivante qui gère l'opération de requête 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);
    }
}

Exécutez maintenant la tâche comme suit.

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

Vous pouvez télécharger un fichier à la fois en utilisant cette bibliothèque. Cependant, je pourrais réussir à télécharger plusieurs fichiers, simplement en lançant plusieurs tâches.

J'espère que cela pourra aider!

Reaz Murshed
la source