7

I'm using Spring for Android as a REST template for remote calls in Android app.

Currently working on uploading images to the server.

I came up with something like that:

public Picture uploadPicture(String accessToken, String fileToUpload) throws RestClientException {
    RestTemplate rest = new RestTemplate();
    FormHttpMessageConverter formConverter = new FormHttpMessageConverter();
    formConverter.setCharset(Charset.forName("UTF8"));
    CustomGsonHttpMessageConverter jsonConverter = new CustomGsonHttpMessageConverter();
    rest.getMessageConverters().add(formConverter);
    rest.getMessageConverters().add(jsonConverter);
    String uploadUri = AppConfig.ROOT_URL.concat(AppConfig.ADD_PHOTO);

    HashMap<String, Object> urlVariables = new HashMap<String, Object>();
    urlVariables.put("accessToken", accessToken);

    HttpHeaders httpHeaders = new HttpHeaders();
    httpHeaders.setAccept(Collections.singletonList(MediaType.parseMediaType("application/json")));

    MultiValueMap<String, Object> parts = new LinkedMultiValueMap<String, Object>();
    parts.add("picture", new FileSystemResource(fileToUpload));
    Picture response = rest.postForObject(uploadUri, parts, Picture.class, urlVariables);
    return response;
}

which works OK, but now I'd like to get progress updates from it.

Does anyone know if it's possible and how to do that?

Thanks in advance :)

Damian Walczak
  • 1,334
  • 14
  • 21

2 Answers2

3

So I had this same problem and decided to take a look into Spring-Android sources. After a lot of digging I found out what I need to extend. Got some of my inspiration from this link.

ProgressListener

public interface ProgressListener {
  void transferred(long num);
}

CountingInputStream

public class CountingInputStream extends FileInputStream {

 private final ProgressListener listener;
 private long transferred;

 public CountingInputStream(File file, ProgressListener listener) throws FileNotFoundException {
    super(file);
    this.listener = listener;
    this.transferred = 0;
 }

 @Override
 public int read(byte[] buffer) throws IOException {
    int bytesRead = super.read(buffer);
    if (bytesRead != -1) {
        this.transferred += bytesRead;
    }
    this.listener.transferred(this.transferred);
    return bytesRead;
 }

}

ListenerFileSystemResource

public class ListenerFileSystemResource extends FileSystemResource {

 private final ProgressListener listener;

 public ListenerFileSystemResource(File file, ProgressListener listener) {
     super(file);
     this.listener = listener;
 }

 @Override
 public InputStream getInputStream() throws IOException {
     return new CountingInputStream(super.getFile(), listener);
 }

}

SendFileTask

private class SendFileTask extends AsyncTask<String, Integer, Boolean> {
    private ProgressListener listener;
    private long totalSize;

    @Override
    protected Boolean doInBackground(String... params) {
        File file = new File(filePath);
        totalSize = file.length();
        listener = new ProgressListener() {
            @Override
            public void transferred(long num) {
                publishProgress((int) ((num / (float) totalSize) * 100));
            }
        };
        ListenerFileSystemResource resource = new ListenerFileSystemResource(file, listener);
        MyResult result = new MyService().uploadFile(resource);
    }

MyService

public FileResult uploadFile(ListenerFileSystemResource resource, Long userId, FileType type) {
    String[] pathParams = {ConnectorConstants.FILE_RESOURCE };
    String[] headerKeys = {"manager_user_id"};
    String[] headerValues = {String.valueOf(userId)};
    String[] formKeys = {ConnectorConstants.FILE_FORM_PARAM};
    Object[] formValues = {resource};

    MultiValueMap<String, Object> body = createMultiValueMap(formKeys, formValues);

    HttpConnector<FileResult> connector = new HttpConnector<FileResult>(FileResult.class);
    return connector.path(pathParams).header(createValuePairs(headerKeys, headerValues)).multipart().body(body).post();
}

HttpConnector

public final class HttpConnector<T> {

    public static String API_URL = "https://myapi.com";

    private UriComponentsBuilder builder;

    private RestTemplate template;

    private Class<T> generic;

    private HttpEntity<?> requestEntity;

    private HttpHeaders headers;

    /**
     * 
     * @param generic
     */
    protected HttpConnector(Class<T> generic)
    {
            this.builder = UriComponentsBuilder.fromUriString(API_URL);
            this.template = new RestTemplate();
    this.template.setRequestFactory(new HttpComponentsClientHttpRequestFactory());
            this.generic = generic;
            this.template.getMessageConverters().add(new GsonHttpMessageConverter(getGson()));
            this.headers = new HttpHeaders();
    }

    /**
     * 
     * @param pathSegments
     * @return
     */
    protected HttpConnector<T> path(String[] pathSegments)
    {
            this.builder = builder.pathSegment(pathSegments);
            return this;
    }

    /**
     * 
     * @param headerParams
     * @return
     */
    protected HttpConnector<T> header(List<NameValuePair> headerParams)
    {
            for (NameValuePair param : headerParams)
            {
                    headers.add(param.getName(), param.getValue());
            }
            return this;
    }

    /**
     * 
     * @param queryParams
     * @return
     */
    protected HttpConnector<T> query(List<NameValuePair> queryParams)
    {
            for (NameValuePair param : queryParams)
            {
                    this.builder = builder.queryParam(param.getName(), param.getValue());
            }
            return this;
    }

    /**
     * 
     * @param body
     * @return
     */
    protected HttpConnector<T> body(MultiValueMap<String, ? extends Object> body)
    {
            this.requestEntity = new HttpEntity<Object>(body, headers);
            return this;
    }

    /**
     * 
     * @param body
     * @return
     */
    protected HttpConnector<T> body(Object body)
    {
            this.requestEntity = new HttpEntity<Object>(body, headers);
            headers.setContentType(MediaType.APPLICATION_JSON);
            return this;
    }

    /**
     * 
     * @return
     */
    protected HttpConnector<T> form()
    {
            headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
            return addFormConverter();
    }

    /**
     * 
     * @return
     */
    protected HttpConnector<T> multipart()
    {
            headers.setContentType(MediaType.MULTIPART_FORM_DATA);
            return addFormConverter();
    }

    /**
     * 
     * @return
     */
    private HttpConnector<T> addFormConverter()
    {
            this.template.getMessageConverters().add(new FormHttpMessageConverter());
            return this;
    }

    /**
     * 
     * @return
     * @throws MeplisNotFoundException 
     */
    protected T post() throws MeplisNotFoundException
    {
            return sendRequest(HttpMethod.POST);
    }

    /**
     * 
     * @return
     * @throws MeplisNotFoundException 
     */
    protected T put() throws MeplisNotFoundException
    {
            return sendRequest(HttpMethod.PUT);
    }

    /**
     * 
     * @return
     * @throws MeplisNotFoundException 
     */
    protected T get() throws MeplisNotFoundException
    {
            return sendRequest(HttpMethod.GET);
    }

    /**
     * 
     * @param method
     * @return
     * @throws MeplisNotFoundException 
     */
    private T sendRequest(HttpMethod method) throws MyServiceNotFoundException
    {
            HttpStatus status = null;
            ResponseEntity<T> response;
            try
            {
                    response = template.exchange(toUri(), method, getRequestEntity(), generic);
                    status = response.getStatusCode();
                    if (HttpStatus.OK.equals(status))
                    {
                            return response.getBody();
                    }
            } catch (HttpClientErrorException e)
            {
                    if (HttpStatus.NOT_FOUND.equals(e.getStatusCode()))
                    {
                            throw new MyServiceNotFoundException();
                    }
                    else
                    {
                            Log.e(getClass().toString(), String.format("Error %s request, status[%s]", method.toString(), e.getStatusCode()), e);
                    }
            } catch (Exception e)
            {
                    Log.e(getClass().toString(), String.format("Error %s request, status: %s", method.toString(), status), e);
            }
            return null;
    }

    /**
     * 
     * @return
     */
    private HttpEntity<?> getRequestEntity()
    {
            if (this.requestEntity == null)
            {
                    this.requestEntity = new HttpEntity<Object>(headers);
            }
            return requestEntity;
    }

    /**
     * 
     * @return
     */
    private URI toUri()
    {
            return this.builder.build().toUri();
    }

    /**
     * 
     * @return
     */
    private Gson getGson()
    {
            return new GsonBuilder().create();
    }

    /**
     * 
     * @return
     */
    public HttpHeaders getHeaders()
    {
            return headers;
    }

}

And I use ListenerFileSystemResource instead of FileSystemResource and works. Hope this will be helpful for someone in the future, since I didn't found any info on this for Spring framework.

Thanh Nguyen Van
  • 1,923
  • 1
  • 18
  • 18
Davi Alves
  • 1,704
  • 2
  • 19
  • 24
  • Yes I'm using Spring in my case. I added the ListenerFileSystemResource class in the answer. – Davi Alves Oct 15 '13 at 16:18
  • could you add the uploading code to the answer (where you're actually using the spring), so I can accept the answer? – Damian Walczak Oct 16 '13 at 15:37
  • done. but as I said you just need to use ListenerFileSystemResource instead of using FileSystemResource from Spring. – Davi Alves Oct 17 '13 at 16:39
  • Hey! Your solution is clever but it works only when there is no connection interruptions. If connection is interrupted in some way, read process from InputStream will still go on until timeout period is over, however read bytes are not received by remote server so there will be state misrepresentation in your client app - you won't know if this interruption is only temporary(temporary lack of network) and read data will still be delivered to remote server or it's more like permanent in period of time(connection lost cannot be estabilished back) so read data will not be delivered. – bpawlowski Dec 28 '13 at 19:17
  • 1
    I tried to implement this. But I don't think this is actually working. See I have a request interceptor that prints the request headers before the request is sent. And I am also printing the progress to logcat and it's printed right before the request headers. I investigated the RestTemplate source code and the different parts of the request are first copied to an array of bytes. You can see this in the `FormHttpMessageConverter` `messageConverter.write(partBody, partContentType, multipartOutputMessage);`. So the progress you are listening to is not happening during the network call. – grandouassou Feb 18 '15 at 12:52
0

You need to override FormHttpMessageConverter and ResourceHttpMessageConverter :

public class ProgressFormHttpMessageConverter extends FormHttpMessageConverter {

    OnProgressListener mOnProgressListener;

    public ProgressFormHttpMessageConverter() {
        super();

        List<HttpMessageConverter<?>> partConverters = new ArrayList<HttpMessageConverter<?>>();
        partConverters.add(new ByteArrayHttpMessageConverter());
        StringHttpMessageConverter stringHttpMessageConverter = new StringHttpMessageConverter();
        stringHttpMessageConverter.setWriteAcceptCharset(false);
        partConverters.add(stringHttpMessageConverter);
        partConverters.add(new ProgressResourceHttpMessageConverter();
        setPartConverters(partConverters);
    }

    public ProgressFormHttpMessageConverter setOnProgressListener(OnProgressListener listener) {
        mOnProgressListener = listener;
        return this;
    }

    class ProgressResourceHttpMessageConverter extends ResourceHttpMessageConverter {

        @Override
        protected void writeInternal(Resource resource, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
            InputStream inputStream = resource.getInputStream();
            OutputStream outputStream = outputMessage.getBody();

            byte[] buffer = new byte[StreamUtils.BUFFER_SIZE];
            long contentLength = resource.contentLength();
            int byteCount = 0;
            int bytesRead = -1;
            while ((bytesRead = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, bytesRead);
                byteCount += bytesRead;

                if(mOnProgressListener != null) {
                    mOnProgressListener.onProgress(resource, byteCount, contentLength);
                }
            }
            outputStream.flush();
        }
    }

    public interface OnProgressListener {
        void onProgress(Resource resource, int downloaded, int downloadSize);
    }
}
Thomas G.
  • 882
  • 9
  • 10