2

Using a mix of the code in related answers, I managed to make a call with multipart. However, I'm failing to send it with the correct parameter name.

How the request must be (taken from the iOS app):

enter image description here

enter image description here

How my request looks:

enter image description here

enter image description here

Code:

MultipartRequest is supposed to be a base multipart request.

import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.graphics.Matrix;
import android.graphics.RectF;
import android.util.Log;

import com.android.volley.Request;
import com.android.volley.Response;

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

import com.android.volley.AuthFailureError;
import com.android.volley.toolbox.StringRequest;

import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class MultipartRequest extends StringRequest {
private final int maxImageWidth = 200;
private final int maxImageHeight = 200;

static String mimeType;
private final File file;
DataOutputStream dos = null;
String lineEnd = "\r\n";
static String boundary = "apiclient-" + System.currentTimeMillis();
String twoHyphens = "--";
int bytesRead, bytesAvailable, bufferSize;
byte[] buffer;
int maxBufferSize = 124 * 124;

public static MultipartRequest newInstance(final PlayEarnAPIImpl.OnPlayEarnAPIResponse listener, File file, Response.Listener<String> responseListener, Response.ErrorListener errorListener, String serviceURL) {
    mimeType = "multipart/form-data;boundary=" + boundary;
    return new MultipartRequest(Request.Method.PUT, serviceURL, responseListener, errorListener, file, serviceURL);
}

public MultipartRequest(int method, String url, Response.Listener<String> listener, Response.ErrorListener errorListener, File file, String serviceURL) {
    super(method, url, listener, errorListener);
    this.file = file;
}

private byte[] decodeFile(File file) {
    BitmapFactory.Options options = new BitmapFactory.Options();
    options.inPreferredConfig = Bitmap.Config.ARGB_8888;
    options.inSampleSize = calculateInSampleSize(options, 400, 400);
    Bitmap bitmap = BitmapFactory.decodeFile(file.getAbsolutePath(), options);
    ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
    bitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream);
    return byteArrayOutputStream.toByteArray();
}

public static int calculateInSampleSize(
        BitmapFactory.Options options, int reqWidth, int reqHeight) {
    // Raw height and width of image
    final int height = options.outHeight;
    final int width = options.outWidth;
    int inSampleSize = 1;

    if (height > reqHeight || width > reqWidth) {

        final int halfHeight = height / 2;
        final int halfWidth = width / 2;

        // Calculate the largest inSampleSize value that is a power of 2 and keeps both
        // height and width larger than the requested height and width.
        while ((halfHeight / inSampleSize) > reqHeight
                && (halfWidth / inSampleSize) > reqWidth) {
            inSampleSize *= 2;
        }
    }

    return inSampleSize;
}

//solution 2
private byte[] shrinkImage(File file) {
    try {
        int inWidth = 0;
        int inHeight = 0;

        InputStream in = new FileInputStream(file.getAbsolutePath());

        // decode image size (decode metadata only, not the whole image)
        BitmapFactory.Options options = new BitmapFactory.Options();
        options.inJustDecodeBounds = true;
        BitmapFactory.decodeStream(in, null, options);
        in.close();
        in = null;

        // save width and height
        inWidth = options.outWidth;
        inHeight = options.outHeight;

        // decode full image pre-resized
        in = new FileInputStream(file.getAbsolutePath());
        options = new BitmapFactory.Options();
        // calc rought re-size (this is no exact resize)
        options.inSampleSize = Math.max(inWidth / maxImageWidth, inHeight / maxImageHeight);
        // decode full image
        Bitmap roughBitmap = BitmapFactory.decodeStream(in, null, options);

        // calc exact destination size
        Matrix m = new Matrix();
        RectF inRect = new RectF(0, 0, roughBitmap.getWidth(), roughBitmap.getHeight());
        RectF outRect = new RectF(0, 0, maxImageWidth, maxImageHeight);
        m.setRectToRect(inRect, outRect, Matrix.ScaleToFit.CENTER);
        float[] values = new float[9];
        m.getValues(values);

        // resize bitmap
        Bitmap resizedBitmap = Bitmap.createScaledBitmap(roughBitmap, (int) (roughBitmap.getWidth() * values[0]), (int) (roughBitmap.getHeight() * values[4]), true);

        // save image

        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        resizedBitmap.compress(Bitmap.CompressFormat.PNG, 100, byteArrayOutputStream);
        return byteArrayOutputStream.toByteArray();
    } catch (IOException e) {
        Log.e("Image", e.getMessage(), e);
    }
    return null;
}

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

@Override
public byte[] getBody() throws AuthFailureError {
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    dos = new DataOutputStream(bos);
    byte[] bitmapData = null;
    try {
        //dos.writeBytes(twoHyphens + boundary + lineEnd);
        dos.writeBytes(lineEnd);
        dos.writeBytes(" ------------------12345");
        dos.writeBytes(lineEnd);
        dos.writeBytes("Content-Disposition: form-data; name=\"picture\"; filename=\"file.png\"");
        dos.writeBytes(lineEnd);
        dos.writeBytes("Content-Type: image/png");
        //dos.writeBytes("Content-Disposition: form-data; name=\"picture\";filename=\""
        //        + file.getName() + "\"" + lineEnd);
        dos.writeBytes(lineEnd);
        bitmapData = shrinkImage(this.file);
        ByteArrayInputStream fileInputStream = new ByteArrayInputStream(bitmapData);
        bytesAvailable = fileInputStream.available();

        bufferSize = Math.min(bytesAvailable, maxBufferSize);
        Log.d("MultipartRequest", "bufferSize:" + bufferSize);
        buffer = new byte[bufferSize];

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

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

        // send multipart form data necesssary after file data...
        dos.writeBytes(lineEnd);
        dos.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd);

        return bos.toByteArray();

    } catch (IOException e) {
        e.printStackTrace();
    }
    return bitmapData;
}

This is the request I'm using:

import com.android.volley.AuthFailureError;
import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.VolleyError;
import com.google.gson.Gson;

import java.io.File;
import java.util.Map;

public class ChangeProfileImageRequest extends MultipartRequest {
private static final String UPDATE_PATH = "users/update";
String serviceURL = APIImpl.API_URL + UPDATE_PATH;

public static ChangeProfileImageRequest newInstance(final APIImpl.OnAPIResponse listener, File file) {
    String serviceURL = APIImpl.API_URL + UPDATE_PATH;

    return new ChangeProfileImageRequest(Request.Method.PUT, serviceURL, new ResponseListener(listener), new ErrorListener(listener), file, serviceURL);
}

public ChangeProfileImageRequest(int method, String url, Response.Listener<String> listener, Response.ErrorListener errorListener, File file, String serviceURL) {
    super(method, url, listener, errorListener, file, serviceURL);
}

@Override
public Map<String, String> getHeaders() throws AuthFailureError {
    Map<String, String> headers = new APIImpl().getTokenHeader();
    //headers.put("Accept", "*/*");
    headers.put("Content-Type", "multipart/form-data;boundary=----------------12345");
    AppController.getInstance().addSessionCookie(headers);
    return headers;
}

So I guess it has something to do with the screenshot that says "Failed to decode multipart body", but I don't know what's the specific problem. Also from server side the picture parameter is not being received.

RominaV
  • 3,335
  • 1
  • 29
  • 59
  • IMO, perhaps `dos.writeBytes(" ------------------12345");`, however, you use `dos.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd);` later, causes this issue. You can see a sample at https://www.asp.net/web-api/overview/advanced/sending-html-form-data-part-2 (pay attention to `The format of a multipart MIME message is easiest to understand by looking at an example request`). Try my code at http://stackoverflow.com/questions/32240177/working-post-multipart-request-with-volley-and-without-httpentity to see if it can help – BNK Mar 24 '16 at 01:24

2 Answers2

0

I managed to make it work adding com.squareup.okhttp to gradle, and changing this class just a bit.

Base Multipart request:

import com.android.volley.AuthFailureError;
import com.android.volley.DefaultRetryPolicy;
import com.android.volley.NetworkResponse;
import com.android.volley.Request;
import com.android.volley.Response;
import com.android.volley.RetryPolicy;
import com.android.volley.VolleyLog;
import com.android.volley.toolbox.HttpHeaderParser;

import com.squareup.okhttp.Headers;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.MultipartBuilder;
import com.squareup.okhttp.RequestBody;

import android.util.Log;

import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.util.HashMap;
import java.util.Map;

import okio.Buffer;

/**
 * Multipart request for Google's Volley using Square's OkHttp.
 *
 * @author Hussain Al-Derry
 * @version 1.0
 */
public class VolleyMultipartRequest extends Request<String> {

    /* Used for debugging */
    private static final String TAG = VolleyMultipartRequest.class.getSimpleName();

    /* MediaTypes */
    public static final MediaType MEDIA_TYPE_JPEG = MediaType.parse("image/jpeg");
    public static final MediaType MEDIA_TYPE_PNG = MediaType.parse("image/png");
    public static final MediaType MEDIA_TYPE_TEXT_PLAIN = MediaType.parse("text/plain");

    private MultipartBuilder mBuilder = new MultipartBuilder();
    private final Response.Listener<String> mListener;
    private RequestBody mRequestBody;

    public VolleyMultipartRequest(int method, String url,
                                  Response.ErrorListener errorListener,
                                  Response.Listener<String> listener) {
        super(method, url, errorListener);
        mListener = listener;
        mBuilder.type(MultipartBuilder.FORM);
    }

    /**
     * Adds a collection of string values to the request.
     *
     * @param mParams {@link HashMap} collection of values to be added to the request.
     */
    public void addStringParams(HashMap<String, String> mParams) {
        for (Map.Entry<String, String> entry : mParams.entrySet()) {
            mBuilder.addPart(
                    Headers.of("Content-Disposition", "form-data; name=\"" + entry.getKey() + "\""),
                    RequestBody.create(MEDIA_TYPE_TEXT_PLAIN, entry.getValue()));
        }
    }

    /**
     * Adds a single value to the request.
     *
     * @param key   String - the field name.
     * @param value String - the field's value.
     */
    public void addStringParam(String key, String value) {
        mBuilder.addPart(
                Headers.of("Content-Disposition", "form-data; name=\"" + key + "\""),
                RequestBody.create(MEDIA_TYPE_TEXT_PLAIN, value));
    }

    /**
     * Adds a binary attachment to the request.
     *
     * @param content_type {@link MediaType} - the type of the attachment.
     * @param fileName     String - the attachment field name.
     * @param value        {@link File} - the file to be attached.
     */
    public void addAttachment(MediaType content_type, String paramName, String fileName, File value) {
        mBuilder.addFormDataPart(paramName, fileName, RequestBody.create(content_type, value));
    }

    /**
     * Builds the request.
     * Must be called before adding the request to the Volley request queue.
     */
    public void buildRequest() {
        mRequestBody = mBuilder.build();
    }

    @Override
    public String getBodyContentType() {
        return mRequestBody.contentType().toString();
    }

    @Override
    public byte[] getBody() throws AuthFailureError {
        Buffer buffer = new Buffer();
        try {
            mRequestBody.writeTo(buffer);
        } catch (IOException e) {
            Log.e(TAG, e.toString());
            VolleyLog.e("IOException writing to ByteArrayOutputStream");
        }
        return buffer.readByteArray();
    }

    @Override
    protected Response<String> parseNetworkResponse(NetworkResponse response) {
        String parsed;
        try {
            parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
        } catch (UnsupportedEncodingException e) {
            parsed = new String(response.data);
        }
        return Response.success(parsed, HttpHeaderParser.parseCacheHeaders(response));
    }

    @Override
    public Request<?> setRetryPolicy(RetryPolicy retryPolicy) {
        return super.setRetryPolicy(retryPolicy);
    }

    @Override
    protected void deliverResponse(String response) {
        if (mListener != null) {
            mListener.onResponse(response);
        }
    }
}

And subclassing it for my specific request:

public class ChangeProfileImageRequest extends VolleyMultipartRequest {
private static final String UPDATE_PATH = "users/update";

public static ChangeProfileImageRequest newInstance(final APIImpl.OnAPIResponse listener, File file) {
    String serviceURL = APIImpl.API_URL + UPDATE_PATH;

    //return new ChangeProfileImageRequest(Request.Method.PUT, serviceURL, new ResponseListener(listener), new ErrorListener(listener), file, serviceURL);
    ChangeProfileImageRequest request = new ChangeProfileImageRequest(Method.PUT, serviceURL, new ErrorListener(listener), new ResponseListener(listener));
    request.addAttachment(MEDIA_TYPE_PNG, "picture", "file.png", file);

    return request;
}

public ChangeProfileImageRequest(int method, String url, Response.ErrorListener errorListener, Response.Listener<String> listener) {
    super(method, url, errorListener, listener);
}

@Override
public Map<String, String> getHeaders() throws AuthFailureError {
    Map<String, String> headers = new APIImpl().getTokenHeader();
    headers.put("Accept", "*/*");
    AppController.getInstance().addSessionCookie(headers);
    return headers;
}

//ErrorListener and ResponseListener classes

And adding it to Volley:

public void updateProfileImage(APIImpl.OnAPIResponse listener, File image) {
    ChangeProfileImageRequest request = ChangeProfileImageRequest.newInstance(listener, image);
    request.buildRequest();
    // Access the RequestQueue through your singleton class.
    request.setRetryPolicy(new DefaultRetryPolicy(
            0,
            DefaultRetryPolicy.DEFAULT_MAX_RETRIES,
            DefaultRetryPolicy.DEFAULT_BACKOFF_MULT));
    VolleySingleton.getInstance().addToRequestQueue(request);
}

I'll accept as answer one that doesn't use anything more than Volley, though. I guess it's not ideal to use Volley AND OkHttp just because of one request not working.

RominaV
  • 3,335
  • 1
  • 29
  • 59
  • May I know what is APIImpl and AppController? – Gurupad Mamadapur Sep 18 '17 at 21:34
  • @GurupadMamadapur I don't think it's relevant to the question, but `APIImpl` is just the implementation of a facade that I had to do because I started off with dummy calls because the server wasn't done. The `AppController` is a class that inhertis from `MultiDexApplication` – RominaV Sep 19 '17 at 22:37
-1

Use Apache's MultipartEntityBuilder, then you can just use the HttpEntity to provide your getBodyContentType() and getBody() i.e:

...

    private HttpEntity mEntity;

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

    @Override
    public byte[] getBody() throws AuthFailureError {
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        try {
            mEntity.writeTo(outputStream);
        } catch (IOException e) {
            VolleyLog.e("IOException @ " + getClass().getSimpleName());
        }
        return outputStream.toByteArray();
    }

    public void setEntity(HttpEntity entity) {
        this.mEntity = entity;
    }
}
Submersed
  • 8,810
  • 2
  • 30
  • 38
  • Hi @Submersed , thanks for the response. As far as I know, org.apache.http.* is deprecated and should not be used. – RominaV Mar 23 '16 at 17:29
  • @David Hackro It still supports the same formatting for a multipart request, so -1ing for something that he's already using doesn't really make much sense. Look into the actual reasons for deprecation first. Either way, it's still preferable to the current solution in my opinion. – Submersed Mar 23 '16 at 17:48
  • The ASOP is moving away from the Http libraries in favor of UrlConnection, that doesn't mean apache's itself has deprecated the entire package -- does the documentation mark the whole class as deprecated? Nope. – Submersed Mar 23 '16 at 17:56